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.navigation.MainNavigationRoutes
import com.owenlejeune.tvtime.ui.theme.TVTimeTheme import com.owenlejeune.tvtime.ui.theme.TVTimeTheme
import com.owenlejeune.tvtime.utils.KeyboardManager 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() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
CoroutineScope(Dispatchers.IO).launch {
SessionManager.initialize()
}
setContent { setContent {
AppKeyboardFocusManager() AppKeyboardFocusManager()
val displayUnderStatusBar = remember { mutableStateOf(false) } 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 com.owenlejeune.tvtime.api.tmdb.model.*
import retrofit2.Response import retrofit2.Response
import retrofit2.http.GET import retrofit2.http.*
import retrofit2.http.Path
import retrofit2.http.Query
interface MoviesApi { interface MoviesApi {
@@ -41,4 +39,30 @@ interface MoviesApi {
@GET("movie/{id}/reviews") @GET("movie/{id}/reviews")
suspend fun getReviews(@Path("id") id: Int): Response<ReviewResponse> 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 package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.* import com.owenlejeune.tvtime.api.tmdb.model.*
import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.utils.SessionManager
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Response import retrofit2.Response
class MoviesService: KoinComponent, DetailService, HomePageService { class MoviesService: KoinComponent, DetailService, HomePageService {
private val 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> { 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> { 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> { 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> { override suspend fun getUpcoming(page: Int): Response<out HomePageResponse> {
return service.getUpcomingMovies(page) return movieService.getUpcomingMovies(page)
} }
suspend fun getReleaseDates(id: Int): Response<MovieReleaseResults> { suspend fun getReleaseDates(id: Int): Response<MovieReleaseResults> {
return service.getReleaseDates(id) return movieService.getReleaseDates(id)
} }
override suspend fun getById(id: Int): Response<out DetailedItem> { 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> { override suspend fun getImages(id: Int): Response<ImageCollection> {
return service.getMovieImages(id) return movieService.getMovieImages(id)
} }
override suspend fun getCastAndCrew(id: Int): Response<CastAndCrew> { 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> { 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> { override suspend fun getVideos(id: Int): Response<VideoResponse> {
return service.getVideos(id) return movieService.getVideos(id)
} }
override suspend fun getReviews(id: Int): Response<ReviewResponse> { 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) 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 { private inner class TmdbInterceptor: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val apiParam = QueryParam("api_key", BuildConfig.TMDB_ApiKey) 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 USE_PREFERENCES = "use_android_12_colors"
private val PERSISTENT_SEARCH = "persistent_search" private val PERSISTENT_SEARCH = "persistent_search"
private val HIDE_TITLE = "hide_title" private val HIDE_TITLE = "hide_title"
private val GUEST_SESSION = "guest_session_id"
} }
private val preferences: SharedPreferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE) private val preferences: SharedPreferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE)
@@ -22,6 +23,10 @@ class AppPreferences(context: Context) {
var hideTitle: Boolean var hideTitle: Boolean
get() = preferences.getBoolean(HIDE_TITLE, false) get() = preferences.getBoolean(HIDE_TITLE, false)
set(value) { preferences.put(HIDE_TITLE, value) } 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> // val usePreferences: MutableState<Boolean>
// var usePreferences: Boolean // var usePreferences: Boolean
// get() = preferences.getBoolean(USE_PREFERENCES, false) // 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") Chip("Test Chip")
} }
/**
* @param progress The progress of the ring as a value between 0 and 1
*/
@Composable @Composable
fun RatingRing( fun RatingRing(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
progress: Float = 0f, 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( Box(
modifier = modifier modifier = modifier
.size(60.dp) .size(size)
.padding(8.dp) // .size(60.dp)
// .padding(8.dp)
) { ) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
progress = progress, progress = progress,
strokeWidth = 4.dp, strokeWidth = ringStrokeWidth,
color = MaterialTheme.colorScheme.primary color = ringColor
) )
Text( Text(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
text = "${(progress*100).toInt()}%", text = "${(progress*100).toInt()}%",
color = textColor, 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) val name = resourceUtils.getString(stringRes)
companion object { companion object {
val Items = listOf(Movies, TV, Favourites, Settings) val Items = listOf(Movies, TV, Account, Settings)
fun getByRoute(route: String?): BottomNavItem? { fun getByRoute(route: String?): BottomNavItem? {
return when (route) { return when (route) {
Movies.route -> Movies Movies.route -> Movies
TV.route -> TV TV.route -> TV
Account.route -> Account
Favourites.route -> Favourites Favourites.route -> Favourites
Settings.route -> Settings Settings.route -> Settings
else -> null 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 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 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 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") 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.MainAppView
import com.owenlejeune.tvtime.ui.screens.MediaViewType import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.PersonDetailView 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.FavouritesTab
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.MediaTab import com.owenlejeune.tvtime.ui.screens.tabs.bottom.MediaTab
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.SettingsTab import com.owenlejeune.tvtime.ui.screens.tabs.bottom.SettingsTab
@@ -66,6 +67,9 @@ fun BottomNavigationRoutes(
composable(BottomNavItem.TV.route) { composable(BottomNavItem.TV.route) {
MediaTab(appNavController = appNavController, mediaType = MediaViewType.TV) MediaTab(appNavController = appNavController, mediaType = MediaViewType.TV)
} }
composable(BottomNavItem.Account.route) {
AccountTab()
}
composable(BottomNavItem.Favourites.route) { composable(BottomNavItem.Favourites.route) {
FavouritesTab() FavouritesTab()
} }

View File

@@ -1,8 +1,11 @@
package com.owenlejeune.tvtime.ui.screens package com.owenlejeune.tvtime.ui.screens
import android.widget.Toast
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow 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.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
@@ -11,9 +14,11 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign 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.extensions.listItems
import com.owenlejeune.tvtime.ui.components.* import com.owenlejeune.tvtime.ui.components.*
import com.owenlejeune.tvtime.ui.navigation.MainNavItem import com.owenlejeune.tvtime.ui.navigation.MainNavItem
import com.owenlejeune.tvtime.utils.SessionManager
import com.owenlejeune.tvtime.utils.TmdbUtils import com.owenlejeune.tvtime.utils.TmdbUtils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.text.DecimalFormat
@Composable @Composable
fun DetailView( fun DetailView(
@@ -65,7 +72,7 @@ fun DetailView(
.verticalScroll(state = scrollState) .verticalScroll(state = scrollState)
) { ) {
val ( val (
backButton, backdropImage, posterImage, titleText, contentColumn backButton, backdropImage, posterImage, titleText, contentColumn, ratingsView
) = createRefs() ) = createRefs()
Backdrop( Backdrop(
@@ -96,6 +103,27 @@ fun DetailView(
title = mediaItem.value?.title ?: "", 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( BackButton(
modifier = Modifier.constrainAs(backButton) { modifier = Modifier.constrainAs(backButton) {
top.linkTo(parent.top)//, 8.dp) top.linkTo(parent.top)//, 8.dp)
@@ -372,6 +400,8 @@ private fun ContentColumn(
MiscTvDetails(mediaItem = mediaItem, service as TvService) MiscTvDetails(mediaItem = mediaItem, service as TvService)
} }
ActionsView(itemId = itemId, type = mediaType)
if (mediaItem.value?.overview?.isNotEmpty() == true) { if (mediaItem.value?.overview?.isNotEmpty() == true) {
OverviewCard(mediaItem = mediaItem) 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 @Composable
private fun OverviewCard(mediaItem: MutableState<DetailedItem?>, modifier: Modifier = Modifier) { private fun OverviewCard(mediaItem: MutableState<DetailedItem?>, modifier: Modifier = Modifier) {
ContentCard( 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="updated_at_label">Updated at: %1$s</string>
<string name="no_reviews_label">No reviews</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 --> <!-- preferences -->
<string name="preference_heading_search">Search</string> <string name="preference_heading_search">Search</string>
<string name="preferences_persistent_search_title">Persistent search bar</string> <string name="preferences_persistent_search_title">Persistent search bar</string>
@@ -45,4 +50,8 @@
<string name="video_type_featureette">Featurettes</string> <string name="video_type_featureette">Featurettes</string>
<string name="content_description_back_button">Back</string> <string name="content_description_back_button">Back</string>
<string name="search_icon_content_descriptor">Search Icon</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> </resources>