mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-23 04:00:53 -05:00
add ability to rate as guest and see rated content
This commit is contained in:
@@ -17,4 +17,8 @@ interface DetailService {
|
|||||||
|
|
||||||
suspend fun getReviews(id: Int): Response<ReviewResponse>
|
suspend fun getReviews(id: Int): Response<ReviewResponse>
|
||||||
|
|
||||||
|
suspend fun postRating(id: Int, ratingBody: RatingBody): Response<RatingResponse>
|
||||||
|
|
||||||
|
suspend fun deleteRating(id: Int): Response<RatingResponse>
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,13 @@
|
|||||||
package com.owenlejeune.tvtime.api.tmdb
|
package com.owenlejeune.tvtime.api.tmdb
|
||||||
|
|
||||||
import com.owenlejeune.tvtime.api.tmdb.model.*
|
import com.owenlejeune.tvtime.api.tmdb.model.*
|
||||||
import com.owenlejeune.tvtime.preferences.AppPreferences
|
|
||||||
import com.owenlejeune.tvtime.utils.SessionManager
|
import com.owenlejeune.tvtime.utils.SessionManager
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
|
||||||
class MoviesService: KoinComponent, DetailService, HomePageService {
|
class MoviesService: KoinComponent, DetailService, HomePageService {
|
||||||
|
|
||||||
private val preferences: AppPreferences by inject()
|
|
||||||
|
|
||||||
private val movieService by lazy { TmdbClient().createMovieService() }
|
private val movieService by lazy { TmdbClient().createMovieService() }
|
||||||
private val authService by lazy { TmdbClient().createAuthenticationService() }
|
|
||||||
|
|
||||||
override suspend fun getPopular(page: Int): Response<out HomePageResponse> {
|
override suspend fun getPopular(page: Int): Response<out HomePageResponse> {
|
||||||
return movieService.getPopularMovies(page)
|
return movieService.getPopularMovies(page)
|
||||||
@@ -58,7 +53,7 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
|
|||||||
return movieService.getReviews(id)
|
return movieService.getReviews(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun postRating(id: Int, rating: RatingBody): Response<RatingResponse> {
|
override suspend fun postRating(id: Int, rating: RatingBody): Response<RatingResponse> {
|
||||||
val session = SessionManager.currentSession
|
val session = SessionManager.currentSession
|
||||||
return if (session.isGuest) {
|
return if (session.isGuest) {
|
||||||
movieService.postMovieRatingAsGuest(id, session.sessionId, rating)
|
movieService.postMovieRatingAsGuest(id, session.sessionId, rating)
|
||||||
@@ -67,7 +62,7 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteRating(id: Int, rating: RatingBody): Response<RatingResponse> {
|
override suspend fun deleteRating(id: Int): Response<RatingResponse> {
|
||||||
val session = SessionManager.currentSession
|
val session = SessionManager.currentSession
|
||||||
return if (session.isGuest) {
|
return if (session.isGuest) {
|
||||||
movieService.deleteMovieReviewAsGuest(id, session.sessionId)
|
movieService.deleteMovieReviewAsGuest(id, session.sessionId)
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ class TmdbClient: KoinComponent {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val BASE_URL = "https://api.themoviedb.org/3/"
|
const val BASE_URL = "https://api.themoviedb.org/3/"
|
||||||
|
|
||||||
private val SUPPORTED_LANGUAGES = listOf("en", "fr")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val client: Client by inject { parametersOf(BASE_URL) }
|
private val client: Client by inject { parametersOf(BASE_URL) }
|
||||||
@@ -50,12 +48,8 @@ class TmdbClient: KoinComponent {
|
|||||||
val apiParam = QueryParam("api_key", BuildConfig.TMDB_ApiKey)
|
val apiParam = QueryParam("api_key", BuildConfig.TMDB_ApiKey)
|
||||||
|
|
||||||
val locale = Locale.current
|
val locale = Locale.current
|
||||||
val languageParam = if (SUPPORTED_LANGUAGES.contains(locale.language)) {
|
|
||||||
val languageCode = "${locale.language}-${locale.region}"
|
val languageCode = "${locale.language}-${locale.region}"
|
||||||
QueryParam("language", languageCode)
|
val languageParam = QueryParam("language", languageCode)
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = chain.addQueryParams(apiParam, languageParam)
|
val request = chain.addQueryParams(apiParam, languageParam)
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ package com.owenlejeune.tvtime.api.tmdb
|
|||||||
|
|
||||||
import com.owenlejeune.tvtime.api.tmdb.model.*
|
import com.owenlejeune.tvtime.api.tmdb.model.*
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.*
|
||||||
import retrofit2.http.Path
|
|
||||||
import retrofit2.http.Query
|
|
||||||
|
|
||||||
interface TvApi {
|
interface TvApi {
|
||||||
|
|
||||||
@@ -38,7 +36,33 @@ interface TvApi {
|
|||||||
@GET("tv/{id}/videos")
|
@GET("tv/{id}/videos")
|
||||||
suspend fun getVideos(@Path("id") id: Int): Response<VideoResponse>
|
suspend fun getVideos(@Path("id") id: Int): Response<VideoResponse>
|
||||||
|
|
||||||
@GET("movie/{id}/reviews")
|
@GET("tv/{id}/reviews")
|
||||||
suspend fun getReviews(@Path("id") id: Int): Response<ReviewResponse>
|
suspend fun getReviews(@Path("id") id: Int): Response<ReviewResponse>
|
||||||
|
|
||||||
|
@POST("tv/{id}/rating")
|
||||||
|
suspend fun postTvRatingAsGuest(
|
||||||
|
@Path("id") id: Int,
|
||||||
|
@Query("guest_session_id") guestSessionId: String,
|
||||||
|
@Body ratingBody: RatingBody
|
||||||
|
): Response<RatingResponse>
|
||||||
|
|
||||||
|
@POST("tv/{id}/rating")
|
||||||
|
suspend fun postTvRatingAsUser(
|
||||||
|
@Path("id") id: Int,
|
||||||
|
@Query("session_id") sessionId: String,
|
||||||
|
@Body ratingBody: RatingBody
|
||||||
|
): Response<RatingResponse>
|
||||||
|
|
||||||
|
@DELETE("tv/{id}/rating")
|
||||||
|
suspend fun deleteTvReviewAsGuest(
|
||||||
|
@Path("id") id: Int,
|
||||||
|
@Query("guest_session_id") guestSessionId: String
|
||||||
|
): Response<RatingResponse>
|
||||||
|
|
||||||
|
@DELETE("tv/{id}/rating")
|
||||||
|
suspend fun deleteTvReviewAsUser(
|
||||||
|
@Path("id") id: Int,
|
||||||
|
@Query("session_id") sessionId: String
|
||||||
|
): Response<RatingResponse>
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.owenlejeune.tvtime.api.tmdb
|
package com.owenlejeune.tvtime.api.tmdb
|
||||||
|
|
||||||
import com.owenlejeune.tvtime.api.tmdb.model.*
|
import com.owenlejeune.tvtime.api.tmdb.model.*
|
||||||
|
import com.owenlejeune.tvtime.utils.SessionManager
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
|
||||||
@@ -52,4 +53,22 @@ class TvService: KoinComponent, DetailService, HomePageService {
|
|||||||
return service.getReviews(id)
|
return service.getReviews(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun postRating(id: Int, rating: RatingBody): Response<RatingResponse> {
|
||||||
|
val session = SessionManager.currentSession
|
||||||
|
return if (session.isGuest) {
|
||||||
|
service.postTvRatingAsGuest(id, session.sessionId, rating)
|
||||||
|
} else {
|
||||||
|
service.postTvRatingAsUser(id, session.sessionId, rating)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteRating(id: Int): Response<RatingResponse> {
|
||||||
|
val session = SessionManager.currentSession
|
||||||
|
return if (session.isGuest) {
|
||||||
|
service.deleteTvReviewAsGuest(id, session.sessionId)
|
||||||
|
} else {
|
||||||
|
service.deleteTvReviewAsUser(id, session.sessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,25 @@ package com.owenlejeune.tvtime.api.tmdb.model
|
|||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
class RatedMedia(
|
class RatedMedia(
|
||||||
|
@SerializedName("name", alternate = ["title"]) val title: String,
|
||||||
@SerializedName("id") val id: Int,
|
@SerializedName("id") val id: Int,
|
||||||
|
@SerializedName("poster_path") val posterPath: String?,
|
||||||
|
@SerializedName("backdrop_path") val backdropPath: String?,
|
||||||
|
@SerializedName("release_date", alternate = ["first_air_date", "air_date"]) val releaseDate: String,
|
||||||
|
@SerializedName("rating") val rating: Float,
|
||||||
|
@SerializedName("genre_ids") val genreIds: List<Int>,
|
||||||
|
@SerializedName("original_language") val originalLanguage: String,
|
||||||
|
@SerializedName("original_title") val originalTitle: String,
|
||||||
|
@SerializedName("overview") val overview: String,
|
||||||
|
@SerializedName("popularity") val popularity: Float,
|
||||||
|
@SerializedName("vote_average") val voteAverage: Float,
|
||||||
|
@SerializedName("vote_count") val voteCount: Int,
|
||||||
|
// only for movies
|
||||||
|
@SerializedName("adult") val isAdult: Boolean?,
|
||||||
|
@SerializedName("video") val isVideo: Boolean?,
|
||||||
|
// only for tv
|
||||||
|
@SerializedName("origin_country") val originCountries: List<String>?,
|
||||||
|
|
||||||
var type: Type
|
var type: Type
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,8 @@ package com.owenlejeune.tvtime.api.tmdb.model
|
|||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
class RatedMediaResponse(
|
class RatedMediaResponse(
|
||||||
@SerializedName("results") val results: List<RatedMedia>
|
@SerializedName("page") val page: Int,
|
||||||
|
@SerializedName("results") val results: List<RatedMedia>,
|
||||||
|
@SerializedName("total_pages") val totalPages: Int,
|
||||||
|
@SerializedName("total_results") val totalResults: Int
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ fun SliderWithLabel(
|
|||||||
valueRange: ClosedFloatingPointRange<Float>,
|
valueRange: ClosedFloatingPointRange<Float>,
|
||||||
onValueChanged: (Float) -> Unit,
|
onValueChanged: (Float) -> Unit,
|
||||||
sliderLabel: String,
|
sliderLabel: String,
|
||||||
step: Int = 0,
|
steps: Int = 0,
|
||||||
labelMinWidth: Dp = 24.dp
|
labelMinWidth: Dp = 36.dp
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
@@ -32,15 +32,13 @@ fun SliderWithLabel(
|
|||||||
value = value,
|
value = value,
|
||||||
valueRange = valueRange,
|
valueRange = valueRange,
|
||||||
boxWidth = maxWidth,
|
boxWidth = maxWidth,
|
||||||
labelWidth = labelMinWidth + 8.dp // Since we use a padding of 4.dp on either sides of the SliderLabel, we need to account for this in our calculation
|
labelWidth = labelMinWidth + 8.dp
|
||||||
)
|
)
|
||||||
|
|
||||||
// if (value > valueRange.start) {
|
|
||||||
SliderLabel(
|
SliderLabel(
|
||||||
label = sliderLabel, minWidth = labelMinWidth, modifier = Modifier
|
label = sliderLabel, minWidth = labelMinWidth, modifier = Modifier
|
||||||
.padding(start = offset)
|
.padding(start = offset)
|
||||||
)
|
)
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Slider(
|
Slider(
|
||||||
@@ -48,7 +46,7 @@ fun SliderWithLabel(
|
|||||||
onValueChange = onValueChanged,
|
onValueChange = onValueChanged,
|
||||||
valueRange = valueRange,
|
valueRange = valueRange,
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
|
||||||
steps = step,
|
steps = steps,
|
||||||
colors = SliderDefaults.colors(
|
colors = SliderDefaults.colors(
|
||||||
thumbColor = MaterialTheme.colorScheme.primary,
|
thumbColor = MaterialTheme.colorScheme.primary,
|
||||||
activeTrackColor = MaterialTheme.colorScheme.primary
|
activeTrackColor = MaterialTheme.colorScheme.primary
|
||||||
|
|||||||
@@ -597,7 +597,7 @@ fun AvatarImage(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.fillMaxSize().padding(top = size/5),
|
modifier = Modifier.fillMaxSize().padding(top = size/5),
|
||||||
text = author.name[0].uppercase(),
|
text = if (author.name.isNotEmpty()) author.name[0].uppercase() else author.username[0].toString(),
|
||||||
color = MaterialTheme.colorScheme.onTertiary,
|
color = MaterialTheme.colorScheme.onTertiary,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = MaterialTheme.typography.titleLarge
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.owenlejeune.tvtime.ui.navigation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.navigation.NavHost
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import com.owenlejeune.tvtime.R
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.model.RatedMedia
|
||||||
|
import com.owenlejeune.tvtime.ui.screens.MediaViewType
|
||||||
|
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.AccountTabContent
|
||||||
|
import com.owenlejeune.tvtime.utils.ResourceUtils
|
||||||
|
import com.owenlejeune.tvtime.utils.SessionManager
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
sealed class AccountTabNavItem(stringRes: Int, route: String, val mediaType: MediaViewType, val screen: AccountNavComposableFun, val listFetchFun: ListFetchFun): TabNavItem(route) {
|
||||||
|
private val resourceUtils: ResourceUtils by inject()
|
||||||
|
|
||||||
|
override val name = resourceUtils.getString(stringRes)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val GuestItems = listOf(RatedMovies, RatedTvShows)//, RatedTvEpisodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
object RatedMovies: AccountTabNavItem(R.string.nav_rated_movies_title, "rated_movies_route", MediaViewType.MOVIE, screenContent, { SessionManager.currentSession.ratedMovies } )
|
||||||
|
object RatedTvShows: AccountTabNavItem(R.string.nav_rated_shows_title, "rated_shows_route", MediaViewType.TV, screenContent, { SessionManager.currentSession.ratedTvShows } )
|
||||||
|
object RatedTvEpisodes: AccountTabNavItem(R.string.nav_rated_episodes_title, "rated_episodes_route", MediaViewType.EPISODE, screenContent, { SessionManager.currentSession.ratedTvEpisodes } )
|
||||||
|
}
|
||||||
|
|
||||||
|
private val screenContent: AccountNavComposableFun = { appNavController, mediaViewType, listFetchFun ->
|
||||||
|
AccountTabContent(appNavController = appNavController, mediaViewType = mediaViewType, listFetchFun = listFetchFun)
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias ListFetchFun = () -> List<RatedMedia>
|
||||||
|
|
||||||
|
typealias AccountNavComposableFun = @Composable (NavHostController, MediaViewType, ListFetchFun) -> Unit
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package com.owenlejeune.tvtime.ui.navigation
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import com.owenlejeune.tvtime.R
|
|
||||||
import com.owenlejeune.tvtime.api.tmdb.HomePageService
|
|
||||||
import com.owenlejeune.tvtime.api.tmdb.model.HomePageResponse
|
|
||||||
import com.owenlejeune.tvtime.ui.screens.MediaViewType
|
|
||||||
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.MediaTabContent
|
|
||||||
import com.owenlejeune.tvtime.utils.ResourceUtils
|
|
||||||
import org.koin.core.component.KoinComponent
|
|
||||||
import org.koin.core.component.inject
|
|
||||||
import retrofit2.Response
|
|
||||||
|
|
||||||
typealias NavComposableFun = @Composable (NavHostController, MediaViewType, MediaFetchFun) -> Unit
|
|
||||||
|
|
||||||
private val screenContent: NavComposableFun = { appNavController, mediaViewType, mediaFetchFun ->
|
|
||||||
MediaTabContent(appNavController = appNavController, mediaType = mediaViewType, mediaFetchFun = mediaFetchFun)
|
|
||||||
}
|
|
||||||
|
|
||||||
typealias MediaFetchFun = suspend (service: HomePageService, page: Int) -> Response<out HomePageResponse>
|
|
||||||
|
|
||||||
abstract class TabNavItem(val route: String, val screen: NavComposableFun, val mediaFetchFun: MediaFetchFun): KoinComponent {
|
|
||||||
abstract val name: String
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class MainTabNavItem(stringRes: Int, route: String, screen: NavComposableFun, mediaFetchFun: MediaFetchFun)
|
|
||||||
: TabNavItem(route, screen, mediaFetchFun)
|
|
||||||
{
|
|
||||||
private val resourceUtils: ResourceUtils by inject()
|
|
||||||
|
|
||||||
override val name = resourceUtils.getString(stringRes)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val MovieItems = listOf(Popular, TopRated, NowPlaying, Upcoming)
|
|
||||||
val TvItems = listOf(Popular, TopRated, AiringToday, OnTheAir)
|
|
||||||
|
|
||||||
private val Items = listOf(NowPlaying, Popular, TopRated, Upcoming, AiringToday, OnTheAir)
|
|
||||||
|
|
||||||
fun getByRoute(route: String?): MainTabNavItem? {
|
|
||||||
return Items.firstOrNull { it.route == route }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object Popular: MainTabNavItem(R.string.nav_popular_title, "popular_route", screenContent, { s, p -> s.getPopular(p) } )
|
|
||||||
object TopRated: MainTabNavItem(R.string.nav_top_rated_title, "top_rated_route", screenContent, { s, p -> s.getTopRated(p) } )
|
|
||||||
object NowPlaying: MainTabNavItem(R.string.nav_now_playing_title, "now_playing_route", screenContent, { s, p -> s.getNowPlaying(p) } )
|
|
||||||
object Upcoming: MainTabNavItem(R.string.nav_upcoming_title, "upcoming_route", screenContent, { s, p -> s.getUpcoming(p) } )
|
|
||||||
object AiringToday: MainTabNavItem(R.string.nav_tv_airing_today_title, "airing_today_route", screenContent, { s, p -> s.getNowPlaying(p) } )
|
|
||||||
object OnTheAir: MainTabNavItem(R.string.nav_tv_on_the_air, "on_the_air_route", screenContent, { s, p -> s.getUpcoming(p) } )
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.owenlejeune.tvtime.ui.navigation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import com.owenlejeune.tvtime.R
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.HomePageService
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.model.HomePageResponse
|
||||||
|
import com.owenlejeune.tvtime.ui.screens.MediaViewType
|
||||||
|
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.MediaTabContent
|
||||||
|
import com.owenlejeune.tvtime.utils.ResourceUtils
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import retrofit2.Response
|
||||||
|
|
||||||
|
sealed class MediaTabNavItem(stringRes: Int, route: String, val screen: MediaNavComposableFun, val mediaFetchFun: MediaFetchFun): TabNavItem(route) {
|
||||||
|
private val resourceUtils: ResourceUtils by inject()
|
||||||
|
|
||||||
|
override val name = resourceUtils.getString(stringRes)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val MovieItems = listOf(Popular, TopRated, NowPlaying, Upcoming)
|
||||||
|
val TvItems = listOf(Popular, TopRated, AiringToday, OnTheAir)
|
||||||
|
|
||||||
|
private val Items = listOf(NowPlaying, Popular, TopRated, Upcoming, AiringToday, OnTheAir)
|
||||||
|
|
||||||
|
fun getByRoute(route: String?): MediaTabNavItem? {
|
||||||
|
return Items.firstOrNull { it.route == route }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Popular: MediaTabNavItem(R.string.nav_popular_title, "popular_route", screenContent, { s, p -> s.getPopular(p) } )
|
||||||
|
object TopRated: MediaTabNavItem(R.string.nav_top_rated_title, "top_rated_route", screenContent, { s, p -> s.getTopRated(p) } )
|
||||||
|
object NowPlaying: MediaTabNavItem(R.string.nav_now_playing_title, "now_playing_route", screenContent, { s, p -> s.getNowPlaying(p) } )
|
||||||
|
object Upcoming: MediaTabNavItem(R.string.nav_upcoming_title, "upcoming_route", screenContent, { s, p -> s.getUpcoming(p) } )
|
||||||
|
object AiringToday: MediaTabNavItem(R.string.nav_tv_airing_today_title, "airing_today_route", screenContent, { s, p -> s.getNowPlaying(p) } )
|
||||||
|
object OnTheAir: MediaTabNavItem(R.string.nav_tv_on_the_air, "on_the_air_route", screenContent, { s, p -> s.getUpcoming(p) } )
|
||||||
|
}
|
||||||
|
|
||||||
|
private val screenContent: MediaNavComposableFun = { appNavController, mediaViewType, mediaFetchFun ->
|
||||||
|
MediaTabContent(appNavController = appNavController, mediaType = mediaViewType, mediaFetchFun = mediaFetchFun)
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias MediaNavComposableFun = @Composable (NavHostController, MediaViewType, MediaFetchFun) -> Unit
|
||||||
|
|
||||||
|
typealias MediaFetchFun = suspend (service: HomePageService, page: Int) -> Response<out HomePageResponse>
|
||||||
@@ -58,7 +58,8 @@ fun MainNavigationRoutes(navController: NavHostController, displayUnderStatusBar
|
|||||||
@Composable
|
@Composable
|
||||||
fun BottomNavigationRoutes(
|
fun BottomNavigationRoutes(
|
||||||
appNavController: NavHostController,
|
appNavController: NavHostController,
|
||||||
navController: NavHostController
|
navController: NavHostController,
|
||||||
|
appBarTitle: MutableState<String>
|
||||||
) {
|
) {
|
||||||
NavHost(navController = navController, startDestination = BottomNavItem.Movies.route) {
|
NavHost(navController = navController, startDestination = BottomNavItem.Movies.route) {
|
||||||
composable(BottomNavItem.Movies.route) {
|
composable(BottomNavItem.Movies.route) {
|
||||||
@@ -68,7 +69,7 @@ fun BottomNavigationRoutes(
|
|||||||
MediaTab(appNavController = appNavController, mediaType = MediaViewType.TV)
|
MediaTab(appNavController = appNavController, mediaType = MediaViewType.TV)
|
||||||
}
|
}
|
||||||
composable(BottomNavItem.Account.route) {
|
composable(BottomNavItem.Account.route) {
|
||||||
AccountTab()
|
AccountTab(appBarTitle = appBarTitle, appNavController = appNavController)
|
||||||
}
|
}
|
||||||
composable(BottomNavItem.Favourites.route) {
|
composable(BottomNavItem.Favourites.route) {
|
||||||
FavouritesTab()
|
FavouritesTab()
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.owenlejeune.tvtime.ui.navigation
|
||||||
|
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
|
||||||
|
abstract class TabNavItem(val route: String): KoinComponent {
|
||||||
|
abstract val name: String
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.json.JSONObject
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -400,7 +401,7 @@ private fun ContentColumn(
|
|||||||
MiscTvDetails(mediaItem = mediaItem, service as TvService)
|
MiscTvDetails(mediaItem = mediaItem, service as TvService)
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionsView(itemId = itemId, type = mediaType)
|
ActionsView(itemId = itemId, type = mediaType, service = service)
|
||||||
|
|
||||||
if (mediaItem.value?.overview?.isNotEmpty() == true) {
|
if (mediaItem.value?.overview?.isNotEmpty() == true) {
|
||||||
OverviewCard(mediaItem = mediaItem)
|
OverviewCard(mediaItem = mediaItem)
|
||||||
@@ -500,9 +501,9 @@ private fun MiscDetails(
|
|||||||
private fun ActionsView(
|
private fun ActionsView(
|
||||||
itemId: Int?,
|
itemId: Int?,
|
||||||
type: MediaViewType,
|
type: MediaViewType,
|
||||||
|
service: DetailService,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
itemId?.let {
|
itemId?.let {
|
||||||
val session = SessionManager.currentSession
|
val session = SessionManager.currentSession
|
||||||
Row(
|
Row(
|
||||||
@@ -510,23 +511,13 @@ private fun ActionsView(
|
|||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
val itemIsRated = if (type == MediaViewType.MOVIE) {
|
RateButton(
|
||||||
session.hasRatedMovie(itemId)
|
|
||||||
} else {
|
|
||||||
session.hasRatedTvShow(itemId)
|
|
||||||
}
|
|
||||||
val showRatingDialog = remember { mutableStateOf(false) }
|
|
||||||
ActionButton(
|
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
text = if (itemIsRated) stringResource(R.string.delete_rating_action_label) else stringResource(R.string.rate_action_label),
|
itemId = itemId,
|
||||||
onClick = {
|
type = type,
|
||||||
showRatingDialog.value = true
|
service = service
|
||||||
}
|
|
||||||
)
|
)
|
||||||
RatingDialog(showDialog = showRatingDialog, onValueConfirmed = { rating ->
|
|
||||||
// todo post rating
|
|
||||||
Toast.makeText(context, "Rating :${rating}", Toast.LENGTH_SHORT).show()
|
|
||||||
})
|
|
||||||
if (!session.isGuest) {
|
if (!session.isGuest) {
|
||||||
ActionButton(
|
ActionButton(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
@@ -555,11 +546,69 @@ private fun ActionButton(modifier: Modifier, text: String, onClick: () -> Unit)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RateButton(
|
||||||
|
itemId: Int,
|
||||||
|
type: MediaViewType,
|
||||||
|
service: DetailService,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val session = SessionManager.currentSession
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
var itemIsRated by remember {
|
||||||
|
mutableStateOf(
|
||||||
|
if (type == MediaViewType.MOVIE) {
|
||||||
|
session.hasRatedMovie(itemId)
|
||||||
|
} else {
|
||||||
|
session.hasRatedTvShow(itemId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val showRatingDialog = remember { mutableStateOf(false) }
|
||||||
|
ActionButton(
|
||||||
|
modifier = modifier,
|
||||||
|
text = if (itemIsRated) stringResource(R.string.delete_rating_action_label) else stringResource(R.string.rate_action_label),
|
||||||
|
onClick = {
|
||||||
|
if (!itemIsRated) {
|
||||||
|
showRatingDialog.value = true
|
||||||
|
} else {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val response = service.deleteRating(itemId)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
itemIsRated = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SessionManager.currentSession.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
RatingDialog(showDialog = showRatingDialog, onValueConfirmed = { rating ->
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val response = service.postRating(itemId, RatingBody(rating = rating))
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
SessionManager.currentSession.refresh()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
itemIsRated = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val errorObj = JSONObject(response.errorBody().toString())
|
||||||
|
Toast.makeText(context, "Error: ${errorObj.getString("status_message")}", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RatingDialog(showDialog: MutableState<Boolean>, onValueConfirmed: (Float) -> Unit) {
|
private fun RatingDialog(showDialog: MutableState<Boolean>, onValueConfirmed: (Float) -> Unit) {
|
||||||
|
|
||||||
fun formatPosition(position: Float): String {
|
fun formatPosition(position: Float): String {
|
||||||
return DecimalFormat("#.#").format(position/10f)
|
return DecimalFormat("#.#").format(position.toInt()*5/10f)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDialog.value) {
|
if (showDialog.value) {
|
||||||
@@ -592,9 +641,11 @@ private fun RatingDialog(showDialog: MutableState<Boolean>, onValueConfirmed: (F
|
|||||||
text = {
|
text = {
|
||||||
SliderWithLabel(
|
SliderWithLabel(
|
||||||
value = sliderPosition,
|
value = sliderPosition,
|
||||||
valueRange = 0f..100f,
|
valueRange = 0f..20f,
|
||||||
onValueChanged = { sliderPosition = it },
|
onValueChanged = {
|
||||||
sliderLabel = formatPosition(sliderPosition)
|
sliderPosition = it
|
||||||
|
},
|
||||||
|
sliderLabel = "${sliderPosition.toInt() * 5}%",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ fun MainAppView(appNavController: NavHostController, preferences: AppPreferences
|
|||||||
}
|
}
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Box(modifier = Modifier.padding(innerPadding)) {
|
Box(modifier = Modifier.padding(innerPadding)) {
|
||||||
BottomNavigationRoutes(appNavController = appNavController, navController = navController)
|
BottomNavigationRoutes(appNavController = appNavController, navController = navController, appBarTitle = appBarTitle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,9 +173,7 @@ private fun BottomNavBar(navController: NavController, appBarTitle: MutableState
|
|||||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
val currentRoute = navBackStackEntry?.destination?.route
|
val currentRoute = navBackStackEntry?.destination?.route
|
||||||
|
|
||||||
NavigationBar(
|
NavigationBar {
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
|
||||||
) {
|
|
||||||
BottomNavItem.Items.forEach { item ->
|
BottomNavItem.Items.forEach { item ->
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
icon = { Icon(painter = painterResource(id = item.icon), contentDescription = null) },
|
icon = { Icon(painter = painterResource(id = item.icon), contentDescription = null) },
|
||||||
@@ -187,17 +185,10 @@ private fun BottomNavBar(navController: NavController, appBarTitle: MutableState
|
|||||||
appBarTitle = appBarTitle,
|
appBarTitle = appBarTitle,
|
||||||
item = item
|
item = item
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
colors = NavigationBarItemDefaults
|
|
||||||
.colors(
|
|
||||||
selectedIconColor = MaterialTheme.colorScheme.secondary,
|
|
||||||
indicatorColor = MaterialTheme.colorScheme.onSecondary
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appBarTitle.value = BottomNavItem.getByRoute(currentRoute)?.name ?: ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onBottomAppBarItemClicked(
|
private fun onBottomAppBarItemClicked(
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ enum class MediaViewType {
|
|||||||
MOVIE,
|
MOVIE,
|
||||||
@SerializedName("tv")
|
@SerializedName("tv")
|
||||||
TV,
|
TV,
|
||||||
PERSON
|
PERSON,
|
||||||
|
EPISODE
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,109 @@
|
|||||||
package com.owenlejeune.tvtime.ui.screens.tabs.bottom
|
package com.owenlejeune.tvtime.ui.screens.tabs.bottom
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import coil.compose.rememberImagePainter
|
||||||
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
import com.google.accompanist.pager.HorizontalPager
|
||||||
|
import com.google.accompanist.pager.PagerState
|
||||||
|
import com.google.accompanist.pager.rememberPagerState
|
||||||
|
import com.owenlejeune.tvtime.ui.navigation.AccountTabNavItem
|
||||||
|
import com.owenlejeune.tvtime.ui.navigation.ListFetchFun
|
||||||
|
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
|
||||||
|
import com.owenlejeune.tvtime.ui.screens.MediaViewType
|
||||||
|
import com.owenlejeune.tvtime.ui.screens.tabs.top.Tabs
|
||||||
|
import com.owenlejeune.tvtime.utils.SessionManager
|
||||||
|
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AccountTab() {
|
fun AccountTab(appNavController: NavHostController, appBarTitle: MutableState<String>) {
|
||||||
Column(
|
if (SessionManager.currentSession.isGuest) {
|
||||||
modifier = Modifier
|
appBarTitle.value = "Hello, Guest"
|
||||||
.fillMaxSize()
|
}
|
||||||
.wrapContentSize(Alignment.Center)
|
|
||||||
) {
|
val tabs = if (SessionManager.currentSession.isGuest) {
|
||||||
Text(
|
AccountTabNavItem.GuestItems
|
||||||
text = "Account",
|
} else {
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
AccountTabNavItem.GuestItems
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
val pagerState = rememberPagerState()
|
||||||
|
Tabs(tabs = tabs, pagerState = pagerState)
|
||||||
|
AccountTabs(
|
||||||
|
appNavController = appNavController,
|
||||||
|
tabs = tabs,
|
||||||
|
pagerState = pagerState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AccountTabContent(
|
||||||
|
appNavController: NavHostController,
|
||||||
|
mediaViewType: MediaViewType,
|
||||||
|
listFetchFun: ListFetchFun
|
||||||
|
) {
|
||||||
|
val contentItems = listFetchFun()
|
||||||
|
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxWidth().padding(12.dp)) {
|
||||||
|
items(contentItems.size) { i ->
|
||||||
|
val ratedItem = contentItems[i]
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
onClick = {
|
||||||
|
appNavController.navigate(
|
||||||
|
"${MainNavItem.DetailView.route}/${mediaViewType}/${ratedItem.id}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(60.dp)
|
||||||
|
.height(80.dp),
|
||||||
|
painter = rememberImagePainter(
|
||||||
|
data = TmdbUtils.getFullPosterPath(ratedItem.posterPath)
|
||||||
|
),
|
||||||
|
contentDescription = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.height(80.dp),
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(text = ratedItem.title, color = MaterialTheme.colorScheme.onBackground, fontSize = 18.sp)
|
||||||
|
|
||||||
|
Text(text = ratedItem.releaseDate, color = MaterialTheme.colorScheme.onBackground)
|
||||||
|
|
||||||
|
Text(text = "Rating: ${(ratedItem.rating*10).toInt()}%", color = MaterialTheme.colorScheme.onBackground)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AccountTabs(
|
||||||
|
tabs: List<AccountTabNavItem>,
|
||||||
|
pagerState: PagerState,
|
||||||
|
appNavController: NavHostController
|
||||||
|
) {
|
||||||
|
HorizontalPager(count = tabs.size, state = pagerState) { page ->
|
||||||
|
tabs[page].screen(appNavController, tabs[page].mediaType, tabs[page].listFetchFun)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,19 +2,22 @@ package com.owenlejeune.tvtime.ui.screens.tabs.bottom
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
import com.google.accompanist.pager.HorizontalPager
|
||||||
|
import com.google.accompanist.pager.PagerState
|
||||||
import com.google.accompanist.pager.rememberPagerState
|
import com.google.accompanist.pager.rememberPagerState
|
||||||
import com.owenlejeune.tvtime.api.tmdb.HomePageService
|
import com.owenlejeune.tvtime.api.tmdb.HomePageService
|
||||||
import com.owenlejeune.tvtime.api.tmdb.MoviesService
|
import com.owenlejeune.tvtime.api.tmdb.MoviesService
|
||||||
import com.owenlejeune.tvtime.api.tmdb.TvService
|
import com.owenlejeune.tvtime.api.tmdb.TvService
|
||||||
import com.owenlejeune.tvtime.ui.components.PosterGrid
|
import com.owenlejeune.tvtime.ui.components.PosterGrid
|
||||||
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
|
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
|
||||||
import com.owenlejeune.tvtime.ui.navigation.MainTabNavItem
|
|
||||||
import com.owenlejeune.tvtime.ui.navigation.MediaFetchFun
|
import com.owenlejeune.tvtime.ui.navigation.MediaFetchFun
|
||||||
|
import com.owenlejeune.tvtime.ui.navigation.MediaTabNavItem
|
||||||
import com.owenlejeune.tvtime.ui.screens.MediaViewType
|
import com.owenlejeune.tvtime.ui.screens.MediaViewType
|
||||||
import com.owenlejeune.tvtime.ui.screens.tabs.top.Tabs
|
import com.owenlejeune.tvtime.ui.screens.tabs.top.Tabs
|
||||||
import com.owenlejeune.tvtime.ui.screens.tabs.top.TabsContent
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -25,13 +28,13 @@ import kotlinx.coroutines.withContext
|
|||||||
fun MediaTab(appNavController: NavHostController, mediaType: MediaViewType) {
|
fun MediaTab(appNavController: NavHostController, mediaType: MediaViewType) {
|
||||||
Column {
|
Column {
|
||||||
val tabs = when (mediaType) {
|
val tabs = when (mediaType) {
|
||||||
MediaViewType.MOVIE -> MainTabNavItem.MovieItems
|
MediaViewType.MOVIE -> MediaTabNavItem.MovieItems
|
||||||
MediaViewType.TV -> MainTabNavItem.TvItems
|
MediaViewType.TV -> MediaTabNavItem.TvItems
|
||||||
else -> throw IllegalArgumentException("Media type given: ${mediaType}, \n expected one of MediaViewType.MOVIE, MediaViewType.TV") // shouldn't happen
|
else -> throw IllegalArgumentException("Media type given: ${mediaType}, \n expected one of MediaViewType.MOVIE, MediaViewType.TV") // shouldn't happen
|
||||||
}
|
}
|
||||||
val pagerState = rememberPagerState()
|
val pagerState = rememberPagerState()
|
||||||
Tabs(tabs = tabs, pagerState = pagerState)
|
Tabs(tabs = tabs, pagerState = pagerState)
|
||||||
TabsContent(
|
MediaTabs(
|
||||||
tabs = tabs,
|
tabs = tabs,
|
||||||
pagerState = pagerState,
|
pagerState = pagerState,
|
||||||
appNavController = appNavController,
|
appNavController = appNavController,
|
||||||
@@ -66,6 +69,28 @@ fun MediaTabContent(appNavController: NavHostController, mediaType: MediaViewTyp
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
|
@Composable
|
||||||
|
fun MediaTabs(
|
||||||
|
tabs: List<MediaTabNavItem>,
|
||||||
|
pagerState: PagerState,
|
||||||
|
mediaViewType: MediaViewType,
|
||||||
|
appNavController: NavHostController = rememberNavController()
|
||||||
|
) {
|
||||||
|
HorizontalPager(count = tabs.size, state = pagerState) { page ->
|
||||||
|
tabs[page].screen(appNavController, mediaViewType, tabs[page].mediaFetchFun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun MediaTabsPreview() {
|
||||||
|
val tabs = MediaTabNavItem.MovieItems
|
||||||
|
val pagerState = rememberPagerState()
|
||||||
|
MediaTabs(tabs = tabs, pagerState = pagerState, MediaViewType.MOVIE)
|
||||||
|
}
|
||||||
|
|
||||||
// val moviesViewModel = viewModel(PopularMovieViewModel::class.java)
|
// val moviesViewModel = viewModel(PopularMovieViewModel::class.java)
|
||||||
// val moviesList = moviesViewModel.moviePage
|
// val moviesList = moviesViewModel.moviePage
|
||||||
// val movieListItems: LazyPagingItems<PopularMovie> = moviesList.collectAsLazyPagingItems()
|
// val movieListItems: LazyPagingItems<PopularMovie> = moviesList.collectAsLazyPagingItems()
|
||||||
@@ -5,8 +5,8 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.ScrollableTabRow
|
|
||||||
import androidx.compose.material.Tab
|
import androidx.compose.material.Tab
|
||||||
|
import androidx.compose.material.TabRow
|
||||||
import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
|
import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -17,15 +17,11 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
import com.google.accompanist.pager.HorizontalPager
|
|
||||||
import com.google.accompanist.pager.PagerState
|
import com.google.accompanist.pager.PagerState
|
||||||
import com.google.accompanist.pager.rememberPagerState
|
import com.google.accompanist.pager.rememberPagerState
|
||||||
import com.owenlejeune.tvtime.ui.navigation.MainTabNavItem
|
import com.owenlejeune.tvtime.ui.navigation.MediaTabNavItem
|
||||||
import com.owenlejeune.tvtime.ui.navigation.TabNavItem
|
import com.owenlejeune.tvtime.ui.navigation.TabNavItem
|
||||||
import com.owenlejeune.tvtime.ui.screens.MediaViewType
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalPagerApi::class)
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
@@ -43,12 +39,11 @@ fun Tabs(
|
|||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
ScrollableTabRow(
|
TabRow(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
selectedTabIndex = pagerState.currentPage,
|
selectedTabIndex = pagerState.currentPage,
|
||||||
backgroundColor = backgroundColor,
|
backgroundColor = backgroundColor,
|
||||||
contentColor = contentColor,
|
contentColor = contentColor,
|
||||||
edgePadding = 8.dp,
|
|
||||||
indicator = { tabPositions ->
|
indicator = { tabPositions ->
|
||||||
SmallTabIndicator(
|
SmallTabIndicator(
|
||||||
modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]),
|
modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]),
|
||||||
@@ -93,30 +88,8 @@ private fun SmallTabIndicator(
|
|||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun TabsPreview() {
|
fun TabsPreview() {
|
||||||
val tabs = MainTabNavItem.MovieItems
|
val tabs = MediaTabNavItem.MovieItems
|
||||||
val pagerState = rememberPagerState()
|
val pagerState = rememberPagerState()
|
||||||
Tabs(tabs = tabs, pagerState = pagerState)
|
Tabs(tabs = tabs, pagerState = pagerState)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalPagerApi::class)
|
|
||||||
@Composable
|
|
||||||
fun TabsContent(
|
|
||||||
tabs: List<TabNavItem>,
|
|
||||||
pagerState: PagerState,
|
|
||||||
mediaViewType: MediaViewType,
|
|
||||||
appNavController: NavHostController = rememberNavController()
|
|
||||||
) {
|
|
||||||
HorizontalPager(count = tabs.size, state = pagerState) { page ->
|
|
||||||
tabs[page].screen(appNavController, mediaViewType, tabs[page].mediaFetchFun)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalPagerApi::class)
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun TabsContentPreview() {
|
|
||||||
val tabs = MainTabNavItem.MovieItems
|
|
||||||
val pagerState = rememberPagerState()
|
|
||||||
TabsContent(tabs = tabs, pagerState = pagerState, MediaViewType.MOVIE)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package com.owenlejeune.tvtime.utils
|
package com.owenlejeune.tvtime.utils
|
||||||
|
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.GuestSessionApi
|
||||||
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
|
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
|
||||||
import com.owenlejeune.tvtime.api.tmdb.model.RatedMedia
|
import com.owenlejeune.tvtime.api.tmdb.model.RatedMedia
|
||||||
import com.owenlejeune.tvtime.preferences.AppPreferences
|
import com.owenlejeune.tvtime.preferences.AppPreferences
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
@@ -61,6 +64,8 @@ object SessionManager: KoinComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract suspend fun initialize()
|
abstract suspend fun initialize()
|
||||||
|
|
||||||
|
abstract suspend fun refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
private class GuestSession: Session(preferences.guestSessionId, true) {
|
private class GuestSession: Session(preferences.guestSessionId, true) {
|
||||||
@@ -68,21 +73,33 @@ object SessionManager: KoinComponent {
|
|||||||
override var _ratedTvEpisodes: List<RatedMedia> = emptyList()
|
override var _ratedTvEpisodes: List<RatedMedia> = emptyList()
|
||||||
override var _ratedTvShows: List<RatedMedia> = emptyList()
|
override var _ratedTvShows: List<RatedMedia> = emptyList()
|
||||||
|
|
||||||
|
private lateinit var service: GuestSessionApi
|
||||||
|
|
||||||
override suspend fun initialize() {
|
override suspend fun initialize() {
|
||||||
val service = TmdbClient().createGuestSessionService()
|
service = TmdbClient().createGuestSessionService()
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun refresh() {
|
||||||
service.getRatedMovies(sessionId).apply {
|
service.getRatedMovies(sessionId).apply {
|
||||||
if (isSuccessful) {
|
if (isSuccessful) {
|
||||||
_ratedMovies = body()?.results ?: emptyList()
|
withContext(Dispatchers.Main) {
|
||||||
|
_ratedMovies = body()?.results ?: _ratedMovies
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
service.getRatedTvShows(sessionId).apply {
|
service.getRatedTvShows(sessionId).apply {
|
||||||
if (isSuccessful) {
|
if (isSuccessful) {
|
||||||
_ratedTvShows = body()?.results ?: emptyList()
|
withContext(Dispatchers.Main) {
|
||||||
|
_ratedTvShows = body()?.results ?: _ratedTvShows
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
service.getRatedTvEpisodes(sessionId).apply {
|
service.getRatedTvEpisodes(sessionId).apply {
|
||||||
if (isSuccessful) {
|
if (isSuccessful) {
|
||||||
_ratedTvEpisodes = body()?.results ?: emptyList()
|
withContext(Dispatchers.Main) {
|
||||||
|
_ratedTvEpisodes = body()?.results ?: _ratedTvEpisodes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,4 +54,7 @@
|
|||||||
<string name="rating_dialog_confirm">Submit rating</string>
|
<string name="rating_dialog_confirm">Submit rating</string>
|
||||||
<string name="action_cancel">Cancel</string>
|
<string name="action_cancel">Cancel</string>
|
||||||
<string name="nav_account_title">Account</string>
|
<string name="nav_account_title">Account</string>
|
||||||
|
<string name="nav_rated_movies_title">Rated Movies</string>
|
||||||
|
<string name="nav_rated_shows_title">Rated TV Shows</string>
|
||||||
|
<string name="nav_rated_episodes_title">Rated TV Episodes</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user