mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-16 16:50:55 -05:00
basis for session manager and adding rating
This commit is contained in:
@@ -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) }
|
||||
|
||||
@@ -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>
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.owenlejeune.tvtime.api.tmdb.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class DeleteSessionResponse(
|
||||
@SerializedName("success") val success: Boolean
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.owenlejeune.tvtime.api.tmdb.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class RatingBody(
|
||||
@SerializedName("value") val rating: Float
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user