From adb38a89d7ee271323b964d4ba8ee378cacbf83c Mon Sep 17 00:00:00 2001 From: Owen LeJeune Date: Sun, 27 Feb 2022 01:00:32 -0500 Subject: [PATCH] basis for session manager and adding rating --- .../com/owenlejeune/tvtime/MainActivity.kt | 9 ++ .../tvtime/api/tmdb/AuthenticationApi.kt | 19 +++ .../tvtime/api/tmdb/AuthenticationService.kt | 20 +++ .../tvtime/api/tmdb/GuestSessionApi.kt | 19 +++ .../tvtime/api/tmdb/GuestSessionService.kt | 29 ++++ .../owenlejeune/tvtime/api/tmdb/MoviesApi.kt | 30 +++- .../tvtime/api/tmdb/MoviesService.kt | 48 ++++-- .../owenlejeune/tvtime/api/tmdb/TmdbClient.kt | 8 + .../api/tmdb/model/DeleteSessionBody.kt | 7 + .../api/tmdb/model/DeleteSessionResponse.kt | 7 + .../api/tmdb/model/GuestSessionResponse.kt | 9 ++ .../tvtime/api/tmdb/model/RatedMedia.kt | 16 ++ .../api/tmdb/model/RatedMediaResponse.kt | 7 + .../tvtime/api/tmdb/model/RatingBody.kt | 7 + .../tvtime/api/tmdb/model/RatingResponse.kt | 8 + .../tvtime/preferences/AppPreferences.kt | 5 + .../tvtime/ui/components/Sliders.kt | 95 ++++++++++++ .../tvtime/ui/components/Widgets.kt | 20 ++- .../tvtime/ui/navigation/BottomNavItem.kt | 4 +- .../tvtime/ui/navigation/Routes.kt | 4 + .../tvtime/ui/screens/DetailView.kt | 137 +++++++++++++++++- .../ui/screens/tabs/bottom/AccountTab.kt | 24 +++ .../tvtime/utils/SessionManager.kt | 91 ++++++++++++ app/src/main/res/values/strings.xml | 9 ++ 24 files changed, 609 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/AuthenticationApi.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/AuthenticationService.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/GuestSessionApi.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/GuestSessionService.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DeleteSessionBody.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DeleteSessionResponse.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/GuestSessionResponse.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMedia.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMediaResponse.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatingBody.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatingResponse.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/ui/components/Sliders.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/AccountTab.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt diff --git a/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt b/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt index c5550f2..fc6008d 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt @@ -13,11 +13,20 @@ import androidx.navigation.compose.rememberNavController import com.owenlejeune.tvtime.ui.navigation.MainNavigationRoutes import com.owenlejeune.tvtime.ui.theme.TVTimeTheme import com.owenlejeune.tvtime.utils.KeyboardManager +import com.owenlejeune.tvtime.utils.SessionManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + CoroutineScope(Dispatchers.IO).launch { + SessionManager.initialize() + } + setContent { AppKeyboardFocusManager() val displayUnderStatusBar = remember { mutableStateOf(false) } diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/AuthenticationApi.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/AuthenticationApi.kt new file mode 100644 index 0000000..e76b327 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/AuthenticationApi.kt @@ -0,0 +1,19 @@ +package com.owenlejeune.tvtime.api.tmdb + +import com.owenlejeune.tvtime.api.tmdb.model.DeleteSessionBody +import com.owenlejeune.tvtime.api.tmdb.model.DeleteSessionResponse +import com.owenlejeune.tvtime.api.tmdb.model.GuestSessionResponse +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET + +interface AuthenticationApi { + + @GET("authentication/guest_session/new") + suspend fun getNewGuestSession(): Response + + @DELETE("authentication/session") + suspend fun deleteSession(@Body body: DeleteSessionBody): Response + +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/AuthenticationService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/AuthenticationService.kt new file mode 100644 index 0000000..fa0ade7 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/AuthenticationService.kt @@ -0,0 +1,20 @@ +package com.owenlejeune.tvtime.api.tmdb + +import com.owenlejeune.tvtime.api.tmdb.model.DeleteSessionBody +import com.owenlejeune.tvtime.api.tmdb.model.DeleteSessionResponse +import com.owenlejeune.tvtime.api.tmdb.model.GuestSessionResponse +import retrofit2.Response + +class AuthenticationService { + + private val service by lazy { TmdbClient().createAuthenticationService() } + + suspend fun getNewGuestSession(): Response { + return service.getNewGuestSession() + } + + suspend fun deleteSession(body: DeleteSessionBody): Response { + return service.deleteSession(body) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/GuestSessionApi.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/GuestSessionApi.kt new file mode 100644 index 0000000..d79d6fe --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/GuestSessionApi.kt @@ -0,0 +1,19 @@ +package com.owenlejeune.tvtime.api.tmdb + +import com.owenlejeune.tvtime.api.tmdb.model.RatedMediaResponse +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Path + +interface GuestSessionApi { + + @GET("guest_session/{session_id}/rated/movies") + suspend fun getRatedMovies(@Path("session_id") sessionId: String): Response + + @GET("guest_session/{session_id}/rated/tv") + suspend fun getRatedTvShows(@Path("session_id") sessionId: String): Response + + @GET("guest_session/{session_id}/rated/tv/episodes") + suspend fun getRatedTvEpisodes(@Path("session_id") sessionId: String): Response + +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/GuestSessionService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/GuestSessionService.kt new file mode 100644 index 0000000..f707542 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/GuestSessionService.kt @@ -0,0 +1,29 @@ +package com.owenlejeune.tvtime.api.tmdb + +import com.owenlejeune.tvtime.api.tmdb.model.RatedMedia +import com.owenlejeune.tvtime.api.tmdb.model.RatedMediaResponse +import retrofit2.Response + +class GuestSessionService { + + private val service by lazy { TmdbClient().createGuestSessionService() } + + suspend fun getRatedMovies(sessionId: String): Response { + return service.getRatedMovies(sessionId = sessionId).apply { + body()?.results?.forEach { it.type = RatedMedia.Type.MOVIE } + } + } + + suspend fun getRatedTvShows(sessionId: String): Response { + return service.getRatedTvShows(sessionId = sessionId).apply { + body()?.results?.forEach { it.type = RatedMedia.Type.SERIES } + } + } + + suspend fun getRatedTvEpisodes(sessionId: String): Response { + return service.getRatedTvEpisodes(sessionId = sessionId).apply { + body()?.results?.forEach { it.type = RatedMedia.Type.EPISODE } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt index 026ad6b..c31bfb0 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.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 MoviesApi { @@ -41,4 +39,30 @@ interface MoviesApi { @GET("movie/{id}/reviews") suspend fun getReviews(@Path("id") id: Int): Response + @POST("movie/{id}/rating") + suspend fun postMovieRatingAsGuest( + @Path("id") id: Int, + @Query("guest_session_id") guestSessionId: String, + @Body ratingBody: RatingBody + ): Response + + @POST("movie/{id}/rating") + suspend fun postMovieRatingAsUser( + @Path("id") id: Int, + @Query("session_id") sessionId: String, + @Body ratingBody: RatingBody + ): Response + + @DELETE("movie/{id}/rating") + suspend fun deleteMovieReviewAsGuest( + @Path("id") id: Int, + @Query("guest_session_id") guestSessionId: String + ): Response + + @DELETE("movie/{id}/rating") + suspend fun deleteMovieReviewAsUser( + @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/MoviesService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt index 7b22a94..61313aa 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,55 +1,79 @@ 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 service by lazy { TmdbClient().createMovieService() } + 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 service.getPopularMovies(page) + return movieService.getPopularMovies(page) } override suspend fun getNowPlaying(page: Int): Response { - return service.getNowPlayingMovies(page) + return movieService.getNowPlayingMovies(page) } override suspend fun getTopRated(page: Int): Response { - return service.getTopRatedMovies(page) + return movieService.getTopRatedMovies(page) } override suspend fun getUpcoming(page: Int): Response { - return service.getUpcomingMovies(page) + return movieService.getUpcomingMovies(page) } suspend fun getReleaseDates(id: Int): Response { - return service.getReleaseDates(id) + return movieService.getReleaseDates(id) } override suspend fun getById(id: Int): Response { - return service.getMovieById(id) + return movieService.getMovieById(id) } override suspend fun getImages(id: Int): Response { - return service.getMovieImages(id) + return movieService.getMovieImages(id) } override suspend fun getCastAndCrew(id: Int): Response { - return service.getCastAndCrew(id) + return movieService.getCastAndCrew(id) } override suspend fun getSimilar(id: Int, page: Int): Response { - return service.getSimilarMovies(id, page) + return movieService.getSimilarMovies(id, page) } override suspend fun getVideos(id: Int): Response { - return service.getVideos(id) + return movieService.getVideos(id) } override suspend fun getReviews(id: Int): Response { - return service.getReviews(id) + return movieService.getReviews(id) + } + + suspend fun postRating(id: Int, rating: RatingBody): Response { + val session = SessionManager.currentSession + return if (session.isGuest) { + movieService.postMovieRatingAsGuest(id, session.sessionId, rating) + } else { + movieService.postMovieRatingAsUser(id, session.sessionId, rating) + } + } + + suspend fun deleteRating(id: Int, rating: RatingBody): Response { + val session = SessionManager.currentSession + return if (session.isGuest) { + movieService.deleteMovieReviewAsGuest(id, session.sessionId) + } else { + movieService.deleteMovieReviewAsUser(id, session.sessionId) + } } } \ No newline at end of file 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 d5af08b..83db5fc 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 @@ -37,6 +37,14 @@ class TmdbClient: KoinComponent { return client.create(PeopleApi::class.java) } + fun createAuthenticationService(): AuthenticationApi { + return client.create(AuthenticationApi::class.java) + } + + fun createGuestSessionService(): GuestSessionApi { + return client.create(GuestSessionApi::class.java) + } + private inner class TmdbInterceptor: Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val apiParam = QueryParam("api_key", BuildConfig.TMDB_ApiKey) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DeleteSessionBody.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DeleteSessionBody.kt new file mode 100644 index 0000000..3cf7525 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DeleteSessionBody.kt @@ -0,0 +1,7 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +class DeleteSessionBody( + @SerializedName("session_id") val sessionsId: String +) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DeleteSessionResponse.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DeleteSessionResponse.kt new file mode 100644 index 0000000..e4fad12 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DeleteSessionResponse.kt @@ -0,0 +1,7 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +class DeleteSessionResponse( + @SerializedName("success") val success: Boolean +) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/GuestSessionResponse.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/GuestSessionResponse.kt new file mode 100644 index 0000000..7acc199 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/GuestSessionResponse.kt @@ -0,0 +1,9 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +class GuestSessionResponse( + @SerializedName("success") val success: Boolean, + @SerializedName("guest_session_id") val guestSessionId: String, + @SerializedName("expires_at") val expiry: String +) 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 new file mode 100644 index 0000000..93f90cb --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMedia.kt @@ -0,0 +1,16 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +class RatedMedia( + @SerializedName("id") val id: Int, + var type: Type +) { + + enum class Type { + MOVIE, + SERIES, + EPISODE + } + +} 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 new file mode 100644 index 0000000..fa32881 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMediaResponse.kt @@ -0,0 +1,7 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +class RatedMediaResponse( + @SerializedName("results") val results: List +) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatingBody.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatingBody.kt new file mode 100644 index 0000000..9ed9199 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatingBody.kt @@ -0,0 +1,7 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +class RatingBody( + @SerializedName("value") val rating: Float +) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatingResponse.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatingResponse.kt new file mode 100644 index 0000000..6577f01 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatingResponse.kt @@ -0,0 +1,8 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +class RatingResponse( + @SerializedName("status_code") val statusCode: Int, + @SerializedName("status_message") val statusMessage: String +) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt b/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt index f81f623..d9953a0 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt @@ -11,6 +11,7 @@ class AppPreferences(context: Context) { // private val USE_PREFERENCES = "use_android_12_colors" private val PERSISTENT_SEARCH = "persistent_search" private val HIDE_TITLE = "hide_title" + private val GUEST_SESSION = "guest_session_id" } private val preferences: SharedPreferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE) @@ -22,6 +23,10 @@ class AppPreferences(context: Context) { var hideTitle: Boolean get() = preferences.getBoolean(HIDE_TITLE, false) set(value) { preferences.put(HIDE_TITLE, value) } + + var guestSessionId: String + get() = preferences.getString(GUEST_SESSION, "") ?: "" + set(value) { preferences.put(GUEST_SESSION, value) } // val usePreferences: MutableState // var usePreferences: Boolean // get() = preferences.getBoolean(USE_PREFERENCES, false) 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 new file mode 100644 index 0000000..a0d97a5 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Sliders.kt @@ -0,0 +1,95 @@ +package com.owenlejeune.tvtime.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Slider +import androidx.compose.material.SliderDefaults +import androidx.compose.material.Text +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun SliderWithLabel( + value: Float, + valueRange: ClosedFloatingPointRange, + onValueChanged: (Float) -> Unit, + sliderLabel: String, + step: Int = 0, + labelMinWidth: Dp = 24.dp +) { + Column { + BoxWithConstraints( + modifier = Modifier + .fillMaxWidth() + ) { + + val offset = getSliderOffset( + 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 + ) + +// if (value > valueRange.start) { + SliderLabel( + label = sliderLabel, minWidth = labelMinWidth, modifier = Modifier + .padding(start = offset) + ) +// } + } + + Slider( + value = value, + onValueChange = onValueChanged, + valueRange = valueRange, + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), + steps = step, + colors = SliderDefaults.colors( + thumbColor = MaterialTheme.colorScheme.primary, + activeTrackColor = MaterialTheme.colorScheme.primary + ) + ) + + } +} + + +@Composable +fun SliderLabel(label: String, minWidth: Dp, modifier: Modifier = Modifier) { + Text( + label, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onPrimary, + modifier = modifier + .background( + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(10.dp) + ) + .padding(4.dp) + .defaultMinSize(minWidth = minWidth) + ) +} + + +private fun getSliderOffset( + value: Float, + valueRange: ClosedFloatingPointRange, + boxWidth: Dp, + labelWidth: Dp +): Dp { + + val coerced = value.coerceIn(valueRange.start, valueRange.endInclusive) + val positionFraction = calcFraction(valueRange.start, valueRange.endInclusive, coerced) + + return (boxWidth - labelWidth) * positionFraction +} + + +// Calculate the 0..1 fraction that `pos` value represents between `a` and `b` +private fun calcFraction(a: Float, b: Float, pos: Float) = + (if (b - a == 0f) 0f else (pos - a) / (b - a)).coerceIn(0f, 1f) \ No newline at end of file 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 05fdaeb..9977c99 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 @@ -323,29 +323,37 @@ fun ChipPreview() { Chip("Test Chip") } +/** + * @param progress The progress of the ring as a value between 0 and 1 + */ @Composable fun RatingRing( modifier: Modifier = Modifier, progress: Float = 0f, - textColor: Color = Color.White + size: Dp = 60.dp, + ringStrokeWidth: Dp = 4.dp, + ringColor: Color = MaterialTheme.colorScheme.primary, + textColor: Color = Color.White, + textSize: TextUnit = 14.sp ) { Box( modifier = modifier - .size(60.dp) - .padding(8.dp) + .size(size) +// .size(60.dp) +// .padding(8.dp) ) { CircularProgressIndicator( modifier = Modifier.fillMaxSize(), progress = progress, - strokeWidth = 4.dp, - color = MaterialTheme.colorScheme.primary + strokeWidth = ringStrokeWidth, + color = ringColor ) Text( modifier = Modifier.align(Alignment.Center), text = "${(progress*100).toInt()}%", color = textColor, - style = MaterialTheme.typography.titleSmall + fontSize = textSize ) } } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/BottomNavItem.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/BottomNavItem.kt index 62ab5bc..729e805 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/BottomNavItem.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/BottomNavItem.kt @@ -12,12 +12,13 @@ sealed class BottomNavItem(stringRes: Int, val icon: Int, val route: String): Ko val name = resourceUtils.getString(stringRes) companion object { - val Items = listOf(Movies, TV, Favourites, Settings) + val Items = listOf(Movies, TV, Account, Settings) fun getByRoute(route: String?): BottomNavItem? { return when (route) { Movies.route -> Movies TV.route -> TV + Account.route -> Account Favourites.route -> Favourites Settings.route -> Settings else -> null @@ -27,6 +28,7 @@ sealed class BottomNavItem(stringRes: Int, val icon: Int, val route: String): Ko object Movies: BottomNavItem(R.string.nav_movies_title, R.drawable.ic_movie, "movies_route") object TV: BottomNavItem(R.string.nav_tv_title, R.drawable.ic_tv, "tv_route") + object Account: BottomNavItem(R.string.nav_account_title, R.drawable.ic_person, "account_route") object Favourites: BottomNavItem(R.string.nav_favourites_title, R.drawable.ic_favorite, "favourites_route") object Settings: BottomNavItem(R.string.nav_settings_title, R.drawable.ic_settings, "settings_route") 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 fe17b36..2e86c2a 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 @@ -12,6 +12,7 @@ import com.owenlejeune.tvtime.ui.screens.DetailView import com.owenlejeune.tvtime.ui.screens.MainAppView import com.owenlejeune.tvtime.ui.screens.MediaViewType import com.owenlejeune.tvtime.ui.screens.PersonDetailView +import com.owenlejeune.tvtime.ui.screens.tabs.bottom.AccountTab import com.owenlejeune.tvtime.ui.screens.tabs.bottom.FavouritesTab import com.owenlejeune.tvtime.ui.screens.tabs.bottom.MediaTab import com.owenlejeune.tvtime.ui.screens.tabs.bottom.SettingsTab @@ -66,6 +67,9 @@ fun BottomNavigationRoutes( composable(BottomNavItem.TV.route) { MediaTab(appNavController = appNavController, mediaType = MediaViewType.TV) } + composable(BottomNavItem.Account.route) { + AccountTab() + } composable(BottomNavItem.Favourites.route) { FavouritesTab() } 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 c48a2dd..08ab98c 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 @@ -1,8 +1,11 @@ package com.owenlejeune.tvtime.ui.screens +import android.widget.Toast import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Delete @@ -11,9 +14,11 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -31,11 +36,13 @@ import com.owenlejeune.tvtime.api.tmdb.model.* import com.owenlejeune.tvtime.extensions.listItems import com.owenlejeune.tvtime.ui.components.* import com.owenlejeune.tvtime.ui.navigation.MainNavItem +import com.owenlejeune.tvtime.utils.SessionManager import com.owenlejeune.tvtime.utils.TmdbUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.text.DecimalFormat @Composable fun DetailView( @@ -65,7 +72,7 @@ fun DetailView( .verticalScroll(state = scrollState) ) { val ( - backButton, backdropImage, posterImage, titleText, contentColumn + backButton, backdropImage, posterImage, titleText, contentColumn, ratingsView ) = createRefs() Backdrop( @@ -96,6 +103,27 @@ fun DetailView( title = mediaItem.value?.title ?: "", ) + Box( + Modifier + .clip(CircleShape) + .size(60.dp) + .background(color = MaterialTheme.colorScheme.surfaceVariant) + .constrainAs(ratingsView) { + bottom.linkTo(titleText.top) + start.linkTo(posterImage.end, margin = 20.dp) + } + ) { + RatingRing( + modifier = Modifier.padding(5.dp), + textColor = MaterialTheme.colorScheme.onSurfaceVariant, + progress = mediaItem.value?.voteAverage?.let { it / 10 } ?: 0f, + textSize = 14.sp, + ringColor = MaterialTheme.colorScheme.primary, + ringStrokeWidth = 4.dp, + size = 50.dp + ) + } + BackButton( modifier = Modifier.constrainAs(backButton) { top.linkTo(parent.top)//, 8.dp) @@ -372,6 +400,8 @@ private fun ContentColumn( MiscTvDetails(mediaItem = mediaItem, service as TvService) } + ActionsView(itemId = itemId, type = mediaType) + if (mediaItem.value?.overview?.isNotEmpty() == true) { OverviewCard(mediaItem = mediaItem) } @@ -466,6 +496,111 @@ private fun MiscDetails( } } +@Composable +private fun ActionsView( + itemId: Int?, + type: MediaViewType, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + itemId?.let { + val session = SessionManager.currentSession + Row( + modifier = modifier + .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( + 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 + } + ) + 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), + text = stringResource(R.string.add_to_list_action_label), + onClick = { /*TODO*/ } + ) + ActionButton( + modifier = Modifier.weight(1f), + text = stringResource(R.string.favourite_label), + onClick = { /*TODO*/ } + ) + } + } + } +} + +@Composable +private fun ActionButton(modifier: Modifier, text: String, onClick: () -> Unit) { + Button( + modifier = modifier, + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.tertiary), + onClick = onClick + ) { + Text(text = text) + } +} + +@Composable +private fun RatingDialog(showDialog: MutableState, onValueConfirmed: (Float) -> Unit) { + + fun formatPosition(position: Float): String { + return DecimalFormat("#.#").format(position/10f) + } + + if (showDialog.value) { + var sliderPosition by remember { mutableStateOf(0f) } + AlertDialog( + modifier = Modifier.wrapContentHeight(), + onDismissRequest = { showDialog.value = false }, + title = { Text(text = stringResource(R.string.rating_dialog_title)) }, + confirmButton = { + Button( + modifier = Modifier.height(40.dp), + onClick = { + onValueConfirmed.invoke(formatPosition(sliderPosition).toFloat()) + showDialog.value = false + } + ) { + Text(stringResource(R.string.rating_dialog_confirm)) + } + }, + dismissButton = { + Button( + modifier = Modifier.height(40.dp), + onClick = { + showDialog.value = false + } + ) { + Text(stringResource(R.string.action_cancel)) + } + }, + text = { + SliderWithLabel( + value = sliderPosition, + valueRange = 0f..100f, + onValueChanged = { sliderPosition = it }, + sliderLabel = formatPosition(sliderPosition) + ) + } + ) + } +} + @Composable private fun OverviewCard(mediaItem: MutableState, modifier: Modifier = Modifier) { ContentCard( 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 new file mode 100644 index 0000000..f7c096f --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/AccountTab.kt @@ -0,0 +1,24 @@ +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.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun AccountTab() { + Column( + modifier = Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center) + ) { + Text( + text = "Account", + color = MaterialTheme.colorScheme.onBackground + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt b/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt new file mode 100644 index 0000000..70882f0 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt @@ -0,0 +1,91 @@ +package com.owenlejeune.tvtime.utils + +import com.owenlejeune.tvtime.api.tmdb.TmdbClient +import com.owenlejeune.tvtime.api.tmdb.model.RatedMedia +import com.owenlejeune.tvtime.preferences.AppPreferences +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +object SessionManager: KoinComponent { + + private val preferences: AppPreferences by inject() + + private var _currentSession: Session? = null + val currentSession: Session + get() = _currentSession!! + + private val authenticationService by lazy { TmdbClient().createAuthenticationService() } + + suspend fun initialize() { + _currentSession = if (preferences.guestSessionId.isNotEmpty()) { + val session = GuestSession() + session.initialize() + session + } else { + requestNewGuestSession() + } + } + + private suspend fun requestNewGuestSession(): Session? { + val response = authenticationService.getNewGuestSession() + if (response.isSuccessful) { + preferences.guestSessionId = response.body()?.guestSessionId ?: "" + _currentSession = GuestSession() + } + return _currentSession + } + + abstract class Session(val sessionId: String, val isGuest: Boolean) { + protected abstract var _ratedMovies: List + val ratedMovies: List + get() = _ratedMovies + + protected abstract var _ratedTvShows: List + val ratedTvShows: List + get() = _ratedTvShows + + protected abstract var _ratedTvEpisodes: List + val ratedTvEpisodes: List + get() = _ratedTvEpisodes + + fun hasRatedMovie(id: Int): Boolean { + return ratedMovies.map { it.id }.contains(id) + } + + fun hasRatedTvShow(id: Int): Boolean { + return ratedTvShows.map { it.id }.contains(id) + } + + fun hasRatedTvEpisode(id: Int): Boolean { + return ratedTvEpisodes.map { it.id }.contains(id) + } + + abstract suspend fun initialize() + } + + private class GuestSession: Session(preferences.guestSessionId, true) { + override var _ratedMovies: List = emptyList() + override var _ratedTvEpisodes: List = emptyList() + override var _ratedTvShows: List = emptyList() + + override suspend fun initialize() { + val service = TmdbClient().createGuestSessionService() + service.getRatedMovies(sessionId).apply { + if (isSuccessful) { + _ratedMovies = body()?.results ?: emptyList() + } + } + service.getRatedTvShows(sessionId).apply { + if (isSuccessful) { + _ratedTvShows = body()?.results ?: emptyList() + } + } + service.getRatedTvEpisodes(sessionId).apply { + if (isSuccessful) { + _ratedTvEpisodes = body()?.results ?: emptyList() + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 271c777..521bab3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,6 +29,11 @@ Updated at: %1$s No reviews + Rate + Delete Rating + Add to List + Favourite + Search Persistent search bar @@ -45,4 +50,8 @@ Featurettes Back Search Icon + Add a Rating + Submit rating + Cancel + Account \ No newline at end of file