diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/DetailService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/DetailService.kt index 7eaff04..bac5452 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/DetailService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/DetailService.kt @@ -17,4 +17,8 @@ interface DetailService { suspend fun getReviews(id: Int): Response + suspend fun postRating(id: Int, ratingBody: RatingBody): Response + + suspend fun deleteRating(id: Int): Response + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt index 61313aa..0609099 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt @@ -1,18 +1,13 @@ package com.owenlejeune.tvtime.api.tmdb import com.owenlejeune.tvtime.api.tmdb.model.* -import com.owenlejeune.tvtime.preferences.AppPreferences import com.owenlejeune.tvtime.utils.SessionManager import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import retrofit2.Response class MoviesService: KoinComponent, DetailService, HomePageService { - private val preferences: AppPreferences by inject() - private val movieService by lazy { TmdbClient().createMovieService() } - private val authService by lazy { TmdbClient().createAuthenticationService() } override suspend fun getPopular(page: Int): Response { return movieService.getPopularMovies(page) @@ -58,7 +53,7 @@ class MoviesService: KoinComponent, DetailService, HomePageService { return movieService.getReviews(id) } - suspend fun postRating(id: Int, rating: RatingBody): Response { + override suspend fun postRating(id: Int, rating: RatingBody): Response { val session = SessionManager.currentSession return if (session.isGuest) { movieService.postMovieRatingAsGuest(id, session.sessionId, rating) @@ -67,7 +62,7 @@ class MoviesService: KoinComponent, DetailService, HomePageService { } } - suspend fun deleteRating(id: Int, rating: RatingBody): Response { + override suspend fun deleteRating(id: Int): Response { val session = SessionManager.currentSession return if (session.isGuest) { movieService.deleteMovieReviewAsGuest(id, session.sessionId) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt index 83db5fc..bc8e0cf 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt @@ -15,8 +15,6 @@ class TmdbClient: KoinComponent { companion object { 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) } @@ -50,12 +48,8 @@ class TmdbClient: KoinComponent { val apiParam = QueryParam("api_key", BuildConfig.TMDB_ApiKey) val locale = Locale.current - val languageParam = if (SUPPORTED_LANGUAGES.contains(locale.language)) { - val languageCode = "${locale.language}-${locale.region}" - QueryParam("language", languageCode) - } else { - null - } + val languageCode = "${locale.language}-${locale.region}" + val languageParam = QueryParam("language", languageCode) val request = chain.addQueryParams(apiParam, languageParam) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvApi.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvApi.kt index 6b1196a..44d8754 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvApi.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvApi.kt @@ -2,9 +2,7 @@ package com.owenlejeune.tvtime.api.tmdb import com.owenlejeune.tvtime.api.tmdb.model.* import retrofit2.Response -import retrofit2.http.GET -import retrofit2.http.Path -import retrofit2.http.Query +import retrofit2.http.* interface TvApi { @@ -38,7 +36,33 @@ interface TvApi { @GET("tv/{id}/videos") suspend fun getVideos(@Path("id") id: Int): Response - @GET("movie/{id}/reviews") + @GET("tv/{id}/reviews") suspend fun getReviews(@Path("id") id: Int): Response + @POST("tv/{id}/rating") + suspend fun postTvRatingAsGuest( + @Path("id") id: Int, + @Query("guest_session_id") guestSessionId: String, + @Body ratingBody: RatingBody + ): Response + + @POST("tv/{id}/rating") + suspend fun postTvRatingAsUser( + @Path("id") id: Int, + @Query("session_id") sessionId: String, + @Body ratingBody: RatingBody + ): Response + + @DELETE("tv/{id}/rating") + suspend fun deleteTvReviewAsGuest( + @Path("id") id: Int, + @Query("guest_session_id") guestSessionId: String + ): Response + + @DELETE("tv/{id}/rating") + suspend fun deleteTvReviewAsUser( + @Path("id") id: Int, + @Query("session_id") sessionId: String + ): Response + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvService.kt index 8a2210e..9db10a2 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvService.kt @@ -1,6 +1,7 @@ package com.owenlejeune.tvtime.api.tmdb import com.owenlejeune.tvtime.api.tmdb.model.* +import com.owenlejeune.tvtime.utils.SessionManager import org.koin.core.component.KoinComponent import retrofit2.Response @@ -52,4 +53,22 @@ class TvService: KoinComponent, DetailService, HomePageService { return service.getReviews(id) } + override suspend fun postRating(id: Int, rating: RatingBody): Response { + 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 { + val session = SessionManager.currentSession + return if (session.isGuest) { + service.deleteTvReviewAsGuest(id, session.sessionId) + } else { + service.deleteTvReviewAsUser(id, session.sessionId) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMedia.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMedia.kt index 93f90cb..20bfb59 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMedia.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMedia.kt @@ -3,7 +3,25 @@ package com.owenlejeune.tvtime.api.tmdb.model import com.google.gson.annotations.SerializedName class RatedMedia( + @SerializedName("name", alternate = ["title"]) val title: String, @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, + @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?, + var type: Type ) { diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMediaResponse.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMediaResponse.kt index fa32881..04682a7 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMediaResponse.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMediaResponse.kt @@ -3,5 +3,8 @@ package com.owenlejeune.tvtime.api.tmdb.model import com.google.gson.annotations.SerializedName class RatedMediaResponse( - @SerializedName("results") val results: List + @SerializedName("page") val page: Int, + @SerializedName("results") val results: List, + @SerializedName("total_pages") val totalPages: Int, + @SerializedName("total_results") val totalResults: Int ) diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Sliders.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Sliders.kt index a0d97a5..7970ee7 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Sliders.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Sliders.kt @@ -19,8 +19,8 @@ fun SliderWithLabel( valueRange: ClosedFloatingPointRange, onValueChanged: (Float) -> Unit, sliderLabel: String, - step: Int = 0, - labelMinWidth: Dp = 24.dp + steps: Int = 0, + labelMinWidth: Dp = 36.dp ) { Column { BoxWithConstraints( @@ -32,15 +32,13 @@ fun SliderWithLabel( value = value, valueRange = valueRange, 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( - label = sliderLabel, minWidth = labelMinWidth, modifier = Modifier - .padding(start = offset) - ) -// } + SliderLabel( + label = sliderLabel, minWidth = labelMinWidth, modifier = Modifier + .padding(start = offset) + ) } Slider( @@ -48,7 +46,7 @@ fun SliderWithLabel( onValueChange = onValueChanged, valueRange = valueRange, modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), - steps = step, + steps = steps, colors = SliderDefaults.colors( thumbColor = MaterialTheme.colorScheme.primary, activeTrackColor = MaterialTheme.colorScheme.primary diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt index 9977c99..bfa49bd 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt @@ -597,7 +597,7 @@ fun AvatarImage( ) { Text( 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, textAlign = TextAlign.Center, style = MaterialTheme.typography.titleLarge diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/AccountTabNavItem.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/AccountTabNavItem.kt new file mode 100644 index 0000000..e7c782f --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/AccountTabNavItem.kt @@ -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 + +typealias AccountNavComposableFun = @Composable (NavHostController, MediaViewType, ListFetchFun) -> Unit \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/MainTabNavItem.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/MainTabNavItem.kt deleted file mode 100644 index 0897c28..0000000 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/MainTabNavItem.kt +++ /dev/null @@ -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 - -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) } ) -} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/MediaTabNavItem.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/MediaTabNavItem.kt new file mode 100644 index 0000000..5b9530f --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/MediaTabNavItem.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt index 2e86c2a..bef36b9 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt @@ -58,7 +58,8 @@ fun MainNavigationRoutes(navController: NavHostController, displayUnderStatusBar @Composable fun BottomNavigationRoutes( appNavController: NavHostController, - navController: NavHostController + navController: NavHostController, + appBarTitle: MutableState ) { NavHost(navController = navController, startDestination = BottomNavItem.Movies.route) { composable(BottomNavItem.Movies.route) { @@ -68,7 +69,7 @@ fun BottomNavigationRoutes( MediaTab(appNavController = appNavController, mediaType = MediaViewType.TV) } composable(BottomNavItem.Account.route) { - AccountTab() + AccountTab(appBarTitle = appBarTitle, appNavController = appNavController) } composable(BottomNavItem.Favourites.route) { FavouritesTab() diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/TabNavItem.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/TabNavItem.kt new file mode 100644 index 0000000..b9ae97a --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/TabNavItem.kt @@ -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 +} diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/DetailView.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/DetailView.kt index f3a5e95..c54fbcc 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/DetailView.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/DetailView.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.json.JSONObject import java.text.DecimalFormat @Composable @@ -400,7 +401,7 @@ private fun ContentColumn( MiscTvDetails(mediaItem = mediaItem, service as TvService) } - ActionsView(itemId = itemId, type = mediaType) + ActionsView(itemId = itemId, type = mediaType, service = service) if (mediaItem.value?.overview?.isNotEmpty() == true) { OverviewCard(mediaItem = mediaItem) @@ -500,9 +501,9 @@ private fun MiscDetails( private fun ActionsView( itemId: Int?, type: MediaViewType, + service: DetailService, modifier: Modifier = Modifier ) { - val context = LocalContext.current itemId?.let { val session = SessionManager.currentSession Row( @@ -510,23 +511,13 @@ private fun ActionsView( .fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - val itemIsRated = if (type == MediaViewType.MOVIE) { - session.hasRatedMovie(itemId) - } else { - session.hasRatedTvShow(itemId) - } - val showRatingDialog = remember { mutableStateOf(false) } - ActionButton( + RateButton( modifier = Modifier.weight(1f), - text = if (itemIsRated) stringResource(R.string.delete_rating_action_label) else stringResource(R.string.rate_action_label), - onClick = { - showRatingDialog.value = true - } + itemId = itemId, + type = type, + service = service ) - RatingDialog(showDialog = showRatingDialog, onValueConfirmed = { rating -> - // todo post rating - Toast.makeText(context, "Rating :${rating}", Toast.LENGTH_SHORT).show() - }) + if (!session.isGuest) { ActionButton( 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 private fun RatingDialog(showDialog: MutableState, onValueConfirmed: (Float) -> Unit) { fun formatPosition(position: Float): String { - return DecimalFormat("#.#").format(position/10f) + return DecimalFormat("#.#").format(position.toInt()*5/10f) } if (showDialog.value) { @@ -592,9 +641,11 @@ private fun RatingDialog(showDialog: MutableState, onValueConfirmed: (F text = { SliderWithLabel( value = sliderPosition, - valueRange = 0f..100f, - onValueChanged = { sliderPosition = it }, - sliderLabel = formatPosition(sliderPosition) + valueRange = 0f..20f, + onValueChanged = { + sliderPosition = it + }, + sliderLabel = "${sliderPosition.toInt() * 5}%", ) } ) diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MainView.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MainView.kt index 6213421..dff727e 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MainView.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MainView.kt @@ -86,7 +86,7 @@ fun MainAppView(appNavController: NavHostController, preferences: AppPreferences } ) { 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 currentRoute = navBackStackEntry?.destination?.route - NavigationBar( - containerColor = MaterialTheme.colorScheme.primaryContainer - ) { + NavigationBar { BottomNavItem.Items.forEach { item -> NavigationBarItem( icon = { Icon(painter = painterResource(id = item.icon), contentDescription = null) }, @@ -187,17 +185,10 @@ private fun BottomNavBar(navController: NavController, appBarTitle: MutableState appBarTitle = appBarTitle, item = item ) - }, - colors = NavigationBarItemDefaults - .colors( - selectedIconColor = MaterialTheme.colorScheme.secondary, - indicatorColor = MaterialTheme.colorScheme.onSecondary - ) + } ) } } - - appBarTitle.value = BottomNavItem.getByRoute(currentRoute)?.name ?: "" } private fun onBottomAppBarItemClicked( diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaViewType.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaViewType.kt index 0364261..0454549 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaViewType.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaViewType.kt @@ -7,5 +7,6 @@ enum class MediaViewType { MOVIE, @SerializedName("tv") TV, - PERSON + PERSON, + EPISODE } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/AccountTab.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/AccountTab.kt index f7c096f..25deed3 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/AccountTab.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/AccountTab.kt @@ -1,24 +1,109 @@ package com.owenlejeune.tvtime.ui.screens.tabs.bottom -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +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.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.MutableState 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 -fun AccountTab() { - Column( - modifier = Modifier - .fillMaxSize() - .wrapContentSize(Alignment.Center) - ) { - Text( - text = "Account", - color = MaterialTheme.colorScheme.onBackground +fun AccountTab(appNavController: NavHostController, appBarTitle: MutableState) { + if (SessionManager.currentSession.isGuest) { + appBarTitle.value = "Hello, Guest" + } + + val tabs = if (SessionManager.currentSession.isGuest) { + AccountTabNavItem.GuestItems + } else { + 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, + pagerState: PagerState, + appNavController: NavHostController +) { + HorizontalPager(count = tabs.size, state = pagerState) { page -> + tabs[page].screen(appNavController, tabs[page].mediaType, tabs[page].listFetchFun) + } } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/MediaTab.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/MediaTab.kt index f6c2d31..d74b396 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/MediaTab.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/MediaTab.kt @@ -2,19 +2,22 @@ package com.owenlejeune.tvtime.ui.screens.tabs.bottom import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController 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.api.tmdb.HomePageService import com.owenlejeune.tvtime.api.tmdb.MoviesService import com.owenlejeune.tvtime.api.tmdb.TvService import com.owenlejeune.tvtime.ui.components.PosterGrid 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.MediaTabNavItem import com.owenlejeune.tvtime.ui.screens.MediaViewType 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.Dispatchers import kotlinx.coroutines.launch @@ -25,13 +28,13 @@ import kotlinx.coroutines.withContext fun MediaTab(appNavController: NavHostController, mediaType: MediaViewType) { Column { val tabs = when (mediaType) { - MediaViewType.MOVIE -> MainTabNavItem.MovieItems - MediaViewType.TV -> MainTabNavItem.TvItems + MediaViewType.MOVIE -> MediaTabNavItem.MovieItems + MediaViewType.TV -> MediaTabNavItem.TvItems else -> throw IllegalArgumentException("Media type given: ${mediaType}, \n expected one of MediaViewType.MOVIE, MediaViewType.TV") // shouldn't happen } val pagerState = rememberPagerState() Tabs(tabs = tabs, pagerState = pagerState) - TabsContent( + MediaTabs( tabs = tabs, pagerState = pagerState, appNavController = appNavController, @@ -66,6 +69,28 @@ fun MediaTabContent(appNavController: NavHostController, mediaType: MediaViewTyp ) } +@OptIn(ExperimentalPagerApi::class) +@Composable +fun MediaTabs( + tabs: List, + 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 moviesList = moviesViewModel.moviePage // val movieListItems: LazyPagingItems = moviesList.collectAsLazyPagingItems() \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/top/TabsCommon.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/top/TabsCommon.kt index a44bec6..38c2e8d 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/top/TabsCommon.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/top/TabsCommon.kt @@ -5,8 +5,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ScrollableTabRow import androidx.compose.material.Tab +import androidx.compose.material.TabRow import androidx.compose.material.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.MaterialTheme 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.tooling.preview.Preview 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.HorizontalPager import com.google.accompanist.pager.PagerState 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.screens.MediaViewType import kotlinx.coroutines.launch @OptIn(ExperimentalPagerApi::class) @@ -43,12 +39,11 @@ fun Tabs( ) { val scope = rememberCoroutineScope() - ScrollableTabRow( + TabRow( modifier = modifier, selectedTabIndex = pagerState.currentPage, backgroundColor = backgroundColor, contentColor = contentColor, - edgePadding = 8.dp, indicator = { tabPositions -> SmallTabIndicator( modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]), @@ -93,30 +88,8 @@ private fun SmallTabIndicator( @Preview(showBackground = true) @Composable fun TabsPreview() { - val tabs = MainTabNavItem.MovieItems + val tabs = MediaTabNavItem.MovieItems val pagerState = rememberPagerState() Tabs(tabs = tabs, pagerState = pagerState) } -@OptIn(ExperimentalPagerApi::class) -@Composable -fun TabsContent( - tabs: List, - 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) -} - diff --git a/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt b/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt index 70882f0..ed02ec5 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt @@ -1,8 +1,11 @@ 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.model.RatedMedia 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.inject @@ -61,6 +64,8 @@ object SessionManager: KoinComponent { } abstract suspend fun initialize() + + abstract suspend fun refresh() } private class GuestSession: Session(preferences.guestSessionId, true) { @@ -68,21 +73,33 @@ object SessionManager: KoinComponent { override var _ratedTvEpisodes: List = emptyList() override var _ratedTvShows: List = emptyList() + private lateinit var service: GuestSessionApi + override suspend fun initialize() { - val service = TmdbClient().createGuestSessionService() + service = TmdbClient().createGuestSessionService() + refresh() + } + + override suspend fun refresh() { service.getRatedMovies(sessionId).apply { if (isSuccessful) { - _ratedMovies = body()?.results ?: emptyList() + withContext(Dispatchers.Main) { + _ratedMovies = body()?.results ?: _ratedMovies + } } } service.getRatedTvShows(sessionId).apply { if (isSuccessful) { - _ratedTvShows = body()?.results ?: emptyList() + withContext(Dispatchers.Main) { + _ratedTvShows = body()?.results ?: _ratedTvShows + } } } service.getRatedTvEpisodes(sessionId).apply { if (isSuccessful) { - _ratedTvEpisodes = body()?.results ?: emptyList() + withContext(Dispatchers.Main) { + _ratedTvEpisodes = body()?.results ?: _ratedTvEpisodes + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 521bab3..d6bdb44 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -54,4 +54,7 @@ Submit rating Cancel Account + Rated Movies + Rated TV Shows + Rated TV Episodes \ No newline at end of file