basis for session manager and adding rating

This commit is contained in:
Owen LeJeune
2022-02-27 01:00:32 -05:00
parent 86bfa78590
commit adb38a89d7
24 changed files with 609 additions and 23 deletions

View File

@@ -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) }

View File

@@ -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<GuestSessionResponse>
@DELETE("authentication/session")
suspend fun deleteSession(@Body body: DeleteSessionBody): Response<DeleteSessionResponse>
}

View File

@@ -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<GuestSessionResponse> {
return service.getNewGuestSession()
}
suspend fun deleteSession(body: DeleteSessionBody): Response<DeleteSessionResponse> {
return service.deleteSession(body)
}
}

View File

@@ -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<RatedMediaResponse>
@GET("guest_session/{session_id}/rated/tv")
suspend fun getRatedTvShows(@Path("session_id") sessionId: String): Response<RatedMediaResponse>
@GET("guest_session/{session_id}/rated/tv/episodes")
suspend fun getRatedTvEpisodes(@Path("session_id") sessionId: String): Response<RatedMediaResponse>
}

View File

@@ -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<RatedMediaResponse> {
return service.getRatedMovies(sessionId = sessionId).apply {
body()?.results?.forEach { it.type = RatedMedia.Type.MOVIE }
}
}
suspend fun getRatedTvShows(sessionId: String): Response<RatedMediaResponse> {
return service.getRatedTvShows(sessionId = sessionId).apply {
body()?.results?.forEach { it.type = RatedMedia.Type.SERIES }
}
}
suspend fun getRatedTvEpisodes(sessionId: String): Response<RatedMediaResponse> {
return service.getRatedTvEpisodes(sessionId = sessionId).apply {
body()?.results?.forEach { it.type = RatedMedia.Type.EPISODE }
}
}
}

View File

@@ -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<ReviewResponse>
@POST("movie/{id}/rating")
suspend fun postMovieRatingAsGuest(
@Path("id") id: Int,
@Query("guest_session_id") guestSessionId: String,
@Body ratingBody: RatingBody
): Response<RatingResponse>
@POST("movie/{id}/rating")
suspend fun postMovieRatingAsUser(
@Path("id") id: Int,
@Query("session_id") sessionId: String,
@Body ratingBody: RatingBody
): Response<RatingResponse>
@DELETE("movie/{id}/rating")
suspend fun deleteMovieReviewAsGuest(
@Path("id") id: Int,
@Query("guest_session_id") guestSessionId: String
): Response<RatingResponse>
@DELETE("movie/{id}/rating")
suspend fun deleteMovieReviewAsUser(
@Path("id") id: Int,
@Query("session_id") sessionId: String
): Response<RatingResponse>
}

View File

@@ -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<out HomePageResponse> {
return service.getPopularMovies(page)
return movieService.getPopularMovies(page)
}
override suspend fun getNowPlaying(page: Int): Response<out HomePageResponse> {
return service.getNowPlayingMovies(page)
return movieService.getNowPlayingMovies(page)
}
override suspend fun getTopRated(page: Int): Response<out HomePageResponse> {
return service.getTopRatedMovies(page)
return movieService.getTopRatedMovies(page)
}
override suspend fun getUpcoming(page: Int): Response<out HomePageResponse> {
return service.getUpcomingMovies(page)
return movieService.getUpcomingMovies(page)
}
suspend fun getReleaseDates(id: Int): Response<MovieReleaseResults> {
return service.getReleaseDates(id)
return movieService.getReleaseDates(id)
}
override suspend fun getById(id: Int): Response<out DetailedItem> {
return service.getMovieById(id)
return movieService.getMovieById(id)
}
override suspend fun getImages(id: Int): Response<ImageCollection> {
return service.getMovieImages(id)
return movieService.getMovieImages(id)
}
override suspend fun getCastAndCrew(id: Int): Response<CastAndCrew> {
return service.getCastAndCrew(id)
return movieService.getCastAndCrew(id)
}
override suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse> {
return service.getSimilarMovies(id, page)
return movieService.getSimilarMovies(id, page)
}
override suspend fun getVideos(id: Int): Response<VideoResponse> {
return service.getVideos(id)
return movieService.getVideos(id)
}
override suspend fun getReviews(id: Int): Response<ReviewResponse> {
return service.getReviews(id)
return movieService.getReviews(id)
}
suspend fun postRating(id: Int, rating: RatingBody): Response<RatingResponse> {
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<RatingResponse> {
val session = SessionManager.currentSession
return if (session.isGuest) {
movieService.deleteMovieReviewAsGuest(id, session.sessionId)
} else {
movieService.deleteMovieReviewAsUser(id, session.sessionId)
}
}
}

View File

@@ -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)

View File

@@ -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
)

View File

@@ -0,0 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName
class DeleteSessionResponse(
@SerializedName("success") val success: Boolean
)

View File

@@ -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
)

View File

@@ -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
}
}

View File

@@ -0,0 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName
class RatedMediaResponse(
@SerializedName("results") val results: List<RatedMedia>
)

View File

@@ -0,0 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName
class RatingBody(
@SerializedName("value") val rating: Float
)

View File

@@ -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
)

View File

@@ -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<Boolean>
// var usePreferences: Boolean
// get() = preferences.getBoolean(USE_PREFERENCES, false)

View File

@@ -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<Float>,
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<Float>,
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)

View File

@@ -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
)
}
}

View File

@@ -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")

View File

@@ -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()
}

View File

@@ -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<Boolean>, 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<DetailedItem?>, modifier: Modifier = Modifier) {
ContentCard(

View File

@@ -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
)
}
}

View File

@@ -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<RatedMedia>
val ratedMovies: List<RatedMedia>
get() = _ratedMovies
protected abstract var _ratedTvShows: List<RatedMedia>
val ratedTvShows: List<RatedMedia>
get() = _ratedTvShows
protected abstract var _ratedTvEpisodes: List<RatedMedia>
val ratedTvEpisodes: List<RatedMedia>
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<RatedMedia> = emptyList()
override var _ratedTvEpisodes: List<RatedMedia> = emptyList()
override var _ratedTvShows: List<RatedMedia> = 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()
}
}
}
}
}

View File

@@ -29,6 +29,11 @@
<string name="updated_at_label">Updated at: %1$s</string>
<string name="no_reviews_label">No reviews</string>
<string name="rate_action_label">Rate</string>
<string name="delete_rating_action_label">Delete Rating</string>
<string name="add_to_list_action_label">Add to List</string>
<string name="favourite_label">Favourite</string>
<!-- preferences -->
<string name="preference_heading_search">Search</string>
<string name="preferences_persistent_search_title">Persistent search bar</string>
@@ -45,4 +50,8 @@
<string name="video_type_featureette">Featurettes</string>
<string name="content_description_back_button">Back</string>
<string name="search_icon_content_descriptor">Search Icon</string>
<string name="rating_dialog_title">Add a Rating</string>
<string name="rating_dialog_confirm">Submit rating</string>
<string name="action_cancel">Cancel</string>
<string name="nav_account_title">Account</string>
</resources>