mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-22 11:40:54 -05:00
add ability to rate as guest and see rated content
This commit is contained in:
@@ -17,4 +17,8 @@ interface DetailService {
|
||||
|
||||
suspend fun getReviews(id: Int): Response<ReviewResponse>
|
||||
|
||||
suspend fun postRating(id: Int, ratingBody: RatingBody): Response<RatingResponse>
|
||||
|
||||
suspend fun deleteRating(id: Int): Response<RatingResponse>
|
||||
|
||||
}
|
||||
@@ -1,18 +1,13 @@
|
||||
package com.owenlejeune.tvtime.api.tmdb
|
||||
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.*
|
||||
import com.owenlejeune.tvtime.preferences.AppPreferences
|
||||
import com.owenlejeune.tvtime.utils.SessionManager
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import retrofit2.Response
|
||||
|
||||
class MoviesService: KoinComponent, DetailService, HomePageService {
|
||||
|
||||
private val preferences: AppPreferences by inject()
|
||||
|
||||
private val movieService by lazy { TmdbClient().createMovieService() }
|
||||
private val authService by lazy { TmdbClient().createAuthenticationService() }
|
||||
|
||||
override suspend fun getPopular(page: Int): Response<out HomePageResponse> {
|
||||
return movieService.getPopularMovies(page)
|
||||
@@ -58,7 +53,7 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
|
||||
return movieService.getReviews(id)
|
||||
}
|
||||
|
||||
suspend fun postRating(id: Int, rating: RatingBody): Response<RatingResponse> {
|
||||
override suspend fun postRating(id: Int, rating: RatingBody): Response<RatingResponse> {
|
||||
val session = SessionManager.currentSession
|
||||
return if (session.isGuest) {
|
||||
movieService.postMovieRatingAsGuest(id, session.sessionId, rating)
|
||||
@@ -67,7 +62,7 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteRating(id: Int, rating: RatingBody): Response<RatingResponse> {
|
||||
override suspend fun deleteRating(id: Int): Response<RatingResponse> {
|
||||
val session = SessionManager.currentSession
|
||||
return if (session.isGuest) {
|
||||
movieService.deleteMovieReviewAsGuest(id, session.sessionId)
|
||||
|
||||
@@ -15,8 +15,6 @@ class TmdbClient: KoinComponent {
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://api.themoviedb.org/3/"
|
||||
|
||||
private val SUPPORTED_LANGUAGES = listOf("en", "fr")
|
||||
}
|
||||
|
||||
private val client: Client by inject { parametersOf(BASE_URL) }
|
||||
@@ -50,12 +48,8 @@ class TmdbClient: KoinComponent {
|
||||
val apiParam = QueryParam("api_key", BuildConfig.TMDB_ApiKey)
|
||||
|
||||
val locale = Locale.current
|
||||
val languageParam = if (SUPPORTED_LANGUAGES.contains(locale.language)) {
|
||||
val languageCode = "${locale.language}-${locale.region}"
|
||||
QueryParam("language", languageCode)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val languageParam = QueryParam("language", languageCode)
|
||||
|
||||
val request = chain.addQueryParams(apiParam, languageParam)
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@ package com.owenlejeune.tvtime.api.tmdb
|
||||
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.*
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
import retrofit2.http.*
|
||||
|
||||
interface TvApi {
|
||||
|
||||
@@ -38,7 +36,33 @@ interface TvApi {
|
||||
@GET("tv/{id}/videos")
|
||||
suspend fun getVideos(@Path("id") id: Int): Response<VideoResponse>
|
||||
|
||||
@GET("movie/{id}/reviews")
|
||||
@GET("tv/{id}/reviews")
|
||||
suspend fun getReviews(@Path("id") id: Int): Response<ReviewResponse>
|
||||
|
||||
@POST("tv/{id}/rating")
|
||||
suspend fun postTvRatingAsGuest(
|
||||
@Path("id") id: Int,
|
||||
@Query("guest_session_id") guestSessionId: String,
|
||||
@Body ratingBody: RatingBody
|
||||
): Response<RatingResponse>
|
||||
|
||||
@POST("tv/{id}/rating")
|
||||
suspend fun postTvRatingAsUser(
|
||||
@Path("id") id: Int,
|
||||
@Query("session_id") sessionId: String,
|
||||
@Body ratingBody: RatingBody
|
||||
): Response<RatingResponse>
|
||||
|
||||
@DELETE("tv/{id}/rating")
|
||||
suspend fun deleteTvReviewAsGuest(
|
||||
@Path("id") id: Int,
|
||||
@Query("guest_session_id") guestSessionId: String
|
||||
): Response<RatingResponse>
|
||||
|
||||
@DELETE("tv/{id}/rating")
|
||||
suspend fun deleteTvReviewAsUser(
|
||||
@Path("id") id: Int,
|
||||
@Query("session_id") sessionId: String
|
||||
): Response<RatingResponse>
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.owenlejeune.tvtime.api.tmdb
|
||||
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.*
|
||||
import com.owenlejeune.tvtime.utils.SessionManager
|
||||
import org.koin.core.component.KoinComponent
|
||||
import retrofit2.Response
|
||||
|
||||
@@ -52,4 +53,22 @@ class TvService: KoinComponent, DetailService, HomePageService {
|
||||
return service.getReviews(id)
|
||||
}
|
||||
|
||||
override suspend fun postRating(id: Int, rating: RatingBody): Response<RatingResponse> {
|
||||
val session = SessionManager.currentSession
|
||||
return if (session.isGuest) {
|
||||
service.postTvRatingAsGuest(id, session.sessionId, rating)
|
||||
} else {
|
||||
service.postTvRatingAsUser(id, session.sessionId, rating)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteRating(id: Int): Response<RatingResponse> {
|
||||
val session = SessionManager.currentSession
|
||||
return if (session.isGuest) {
|
||||
service.deleteTvReviewAsGuest(id, session.sessionId)
|
||||
} else {
|
||||
service.deleteTvReviewAsUser(id, session.sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,25 @@ package com.owenlejeune.tvtime.api.tmdb.model
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class RatedMedia(
|
||||
@SerializedName("name", alternate = ["title"]) val title: String,
|
||||
@SerializedName("id") val id: Int,
|
||||
@SerializedName("poster_path") val posterPath: String?,
|
||||
@SerializedName("backdrop_path") val backdropPath: String?,
|
||||
@SerializedName("release_date", alternate = ["first_air_date", "air_date"]) val releaseDate: String,
|
||||
@SerializedName("rating") val rating: Float,
|
||||
@SerializedName("genre_ids") val genreIds: List<Int>,
|
||||
@SerializedName("original_language") val originalLanguage: String,
|
||||
@SerializedName("original_title") val originalTitle: String,
|
||||
@SerializedName("overview") val overview: String,
|
||||
@SerializedName("popularity") val popularity: Float,
|
||||
@SerializedName("vote_average") val voteAverage: Float,
|
||||
@SerializedName("vote_count") val voteCount: Int,
|
||||
// only for movies
|
||||
@SerializedName("adult") val isAdult: Boolean?,
|
||||
@SerializedName("video") val isVideo: Boolean?,
|
||||
// only for tv
|
||||
@SerializedName("origin_country") val originCountries: List<String>?,
|
||||
|
||||
var type: Type
|
||||
) {
|
||||
|
||||
|
||||
@@ -3,5 +3,8 @@ package com.owenlejeune.tvtime.api.tmdb.model
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class RatedMediaResponse(
|
||||
@SerializedName("results") val results: List<RatedMedia>
|
||||
@SerializedName("page") val page: Int,
|
||||
@SerializedName("results") val results: List<RatedMedia>,
|
||||
@SerializedName("total_pages") val totalPages: Int,
|
||||
@SerializedName("total_results") val totalResults: Int
|
||||
)
|
||||
|
||||
@@ -19,8 +19,8 @@ fun SliderWithLabel(
|
||||
valueRange: ClosedFloatingPointRange<Float>,
|
||||
onValueChanged: (Float) -> Unit,
|
||||
sliderLabel: String,
|
||||
step: Int = 0,
|
||||
labelMinWidth: Dp = 24.dp
|
||||
steps: Int = 0,
|
||||
labelMinWidth: Dp = 36.dp
|
||||
) {
|
||||
Column {
|
||||
BoxWithConstraints(
|
||||
@@ -32,15 +32,13 @@ fun SliderWithLabel(
|
||||
value = value,
|
||||
valueRange = valueRange,
|
||||
boxWidth = maxWidth,
|
||||
labelWidth = labelMinWidth + 8.dp // Since we use a padding of 4.dp on either sides of the SliderLabel, we need to account for this in our calculation
|
||||
labelWidth = labelMinWidth + 8.dp
|
||||
)
|
||||
|
||||
// if (value > valueRange.start) {
|
||||
SliderLabel(
|
||||
label = sliderLabel, minWidth = labelMinWidth, modifier = Modifier
|
||||
.padding(start = offset)
|
||||
)
|
||||
// }
|
||||
}
|
||||
|
||||
Slider(
|
||||
@@ -48,7 +46,7 @@ fun SliderWithLabel(
|
||||
onValueChange = onValueChanged,
|
||||
valueRange = valueRange,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
|
||||
steps = step,
|
||||
steps = steps,
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = MaterialTheme.colorScheme.primary,
|
||||
activeTrackColor = MaterialTheme.colorScheme.primary
|
||||
|
||||
@@ -597,7 +597,7 @@ fun AvatarImage(
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxSize().padding(top = size/5),
|
||||
text = author.name[0].uppercase(),
|
||||
text = if (author.name.isNotEmpty()) author.name[0].uppercase() else author.username[0].toString(),
|
||||
color = MaterialTheme.colorScheme.onTertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.owenlejeune.tvtime.ui.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.NavHost
|
||||
import androidx.navigation.NavHostController
|
||||
import com.owenlejeune.tvtime.R
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.RatedMedia
|
||||
import com.owenlejeune.tvtime.ui.screens.MediaViewType
|
||||
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.AccountTabContent
|
||||
import com.owenlejeune.tvtime.utils.ResourceUtils
|
||||
import com.owenlejeune.tvtime.utils.SessionManager
|
||||
import org.koin.core.component.inject
|
||||
|
||||
sealed class AccountTabNavItem(stringRes: Int, route: String, val mediaType: MediaViewType, val screen: AccountNavComposableFun, val listFetchFun: ListFetchFun): TabNavItem(route) {
|
||||
private val resourceUtils: ResourceUtils by inject()
|
||||
|
||||
override val name = resourceUtils.getString(stringRes)
|
||||
|
||||
companion object {
|
||||
val GuestItems = listOf(RatedMovies, RatedTvShows)//, RatedTvEpisodes)
|
||||
}
|
||||
|
||||
object RatedMovies: AccountTabNavItem(R.string.nav_rated_movies_title, "rated_movies_route", MediaViewType.MOVIE, screenContent, { SessionManager.currentSession.ratedMovies } )
|
||||
object RatedTvShows: AccountTabNavItem(R.string.nav_rated_shows_title, "rated_shows_route", MediaViewType.TV, screenContent, { SessionManager.currentSession.ratedTvShows } )
|
||||
object RatedTvEpisodes: AccountTabNavItem(R.string.nav_rated_episodes_title, "rated_episodes_route", MediaViewType.EPISODE, screenContent, { SessionManager.currentSession.ratedTvEpisodes } )
|
||||
}
|
||||
|
||||
private val screenContent: AccountNavComposableFun = { appNavController, mediaViewType, listFetchFun ->
|
||||
AccountTabContent(appNavController = appNavController, mediaViewType = mediaViewType, listFetchFun = listFetchFun)
|
||||
}
|
||||
|
||||
typealias ListFetchFun = () -> List<RatedMedia>
|
||||
|
||||
typealias AccountNavComposableFun = @Composable (NavHostController, MediaViewType, ListFetchFun) -> Unit
|
||||
@@ -1,51 +0,0 @@
|
||||
package com.owenlejeune.tvtime.ui.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.NavHostController
|
||||
import com.owenlejeune.tvtime.R
|
||||
import com.owenlejeune.tvtime.api.tmdb.HomePageService
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.HomePageResponse
|
||||
import com.owenlejeune.tvtime.ui.screens.MediaViewType
|
||||
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.MediaTabContent
|
||||
import com.owenlejeune.tvtime.utils.ResourceUtils
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import retrofit2.Response
|
||||
|
||||
typealias NavComposableFun = @Composable (NavHostController, MediaViewType, MediaFetchFun) -> Unit
|
||||
|
||||
private val screenContent: NavComposableFun = { appNavController, mediaViewType, mediaFetchFun ->
|
||||
MediaTabContent(appNavController = appNavController, mediaType = mediaViewType, mediaFetchFun = mediaFetchFun)
|
||||
}
|
||||
|
||||
typealias MediaFetchFun = suspend (service: HomePageService, page: Int) -> Response<out HomePageResponse>
|
||||
|
||||
abstract class TabNavItem(val route: String, val screen: NavComposableFun, val mediaFetchFun: MediaFetchFun): KoinComponent {
|
||||
abstract val name: String
|
||||
}
|
||||
|
||||
sealed class MainTabNavItem(stringRes: Int, route: String, screen: NavComposableFun, mediaFetchFun: MediaFetchFun)
|
||||
: TabNavItem(route, screen, mediaFetchFun)
|
||||
{
|
||||
private val resourceUtils: ResourceUtils by inject()
|
||||
|
||||
override val name = resourceUtils.getString(stringRes)
|
||||
|
||||
companion object {
|
||||
val MovieItems = listOf(Popular, TopRated, NowPlaying, Upcoming)
|
||||
val TvItems = listOf(Popular, TopRated, AiringToday, OnTheAir)
|
||||
|
||||
private val Items = listOf(NowPlaying, Popular, TopRated, Upcoming, AiringToday, OnTheAir)
|
||||
|
||||
fun getByRoute(route: String?): MainTabNavItem? {
|
||||
return Items.firstOrNull { it.route == route }
|
||||
}
|
||||
}
|
||||
|
||||
object Popular: MainTabNavItem(R.string.nav_popular_title, "popular_route", screenContent, { s, p -> s.getPopular(p) } )
|
||||
object TopRated: MainTabNavItem(R.string.nav_top_rated_title, "top_rated_route", screenContent, { s, p -> s.getTopRated(p) } )
|
||||
object NowPlaying: MainTabNavItem(R.string.nav_now_playing_title, "now_playing_route", screenContent, { s, p -> s.getNowPlaying(p) } )
|
||||
object Upcoming: MainTabNavItem(R.string.nav_upcoming_title, "upcoming_route", screenContent, { s, p -> s.getUpcoming(p) } )
|
||||
object AiringToday: MainTabNavItem(R.string.nav_tv_airing_today_title, "airing_today_route", screenContent, { s, p -> s.getNowPlaying(p) } )
|
||||
object OnTheAir: MainTabNavItem(R.string.nav_tv_on_the_air, "on_the_air_route", screenContent, { s, p -> s.getUpcoming(p) } )
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.owenlejeune.tvtime.ui.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.NavHostController
|
||||
import com.owenlejeune.tvtime.R
|
||||
import com.owenlejeune.tvtime.api.tmdb.HomePageService
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.HomePageResponse
|
||||
import com.owenlejeune.tvtime.ui.screens.MediaViewType
|
||||
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.MediaTabContent
|
||||
import com.owenlejeune.tvtime.utils.ResourceUtils
|
||||
import org.koin.core.component.inject
|
||||
import retrofit2.Response
|
||||
|
||||
sealed class MediaTabNavItem(stringRes: Int, route: String, val screen: MediaNavComposableFun, val mediaFetchFun: MediaFetchFun): TabNavItem(route) {
|
||||
private val resourceUtils: ResourceUtils by inject()
|
||||
|
||||
override val name = resourceUtils.getString(stringRes)
|
||||
|
||||
companion object {
|
||||
val MovieItems = listOf(Popular, TopRated, NowPlaying, Upcoming)
|
||||
val TvItems = listOf(Popular, TopRated, AiringToday, OnTheAir)
|
||||
|
||||
private val Items = listOf(NowPlaying, Popular, TopRated, Upcoming, AiringToday, OnTheAir)
|
||||
|
||||
fun getByRoute(route: String?): MediaTabNavItem? {
|
||||
return Items.firstOrNull { it.route == route }
|
||||
}
|
||||
}
|
||||
|
||||
object Popular: MediaTabNavItem(R.string.nav_popular_title, "popular_route", screenContent, { s, p -> s.getPopular(p) } )
|
||||
object TopRated: MediaTabNavItem(R.string.nav_top_rated_title, "top_rated_route", screenContent, { s, p -> s.getTopRated(p) } )
|
||||
object NowPlaying: MediaTabNavItem(R.string.nav_now_playing_title, "now_playing_route", screenContent, { s, p -> s.getNowPlaying(p) } )
|
||||
object Upcoming: MediaTabNavItem(R.string.nav_upcoming_title, "upcoming_route", screenContent, { s, p -> s.getUpcoming(p) } )
|
||||
object AiringToday: MediaTabNavItem(R.string.nav_tv_airing_today_title, "airing_today_route", screenContent, { s, p -> s.getNowPlaying(p) } )
|
||||
object OnTheAir: MediaTabNavItem(R.string.nav_tv_on_the_air, "on_the_air_route", screenContent, { s, p -> s.getUpcoming(p) } )
|
||||
}
|
||||
|
||||
private val screenContent: MediaNavComposableFun = { appNavController, mediaViewType, mediaFetchFun ->
|
||||
MediaTabContent(appNavController = appNavController, mediaType = mediaViewType, mediaFetchFun = mediaFetchFun)
|
||||
}
|
||||
|
||||
typealias MediaNavComposableFun = @Composable (NavHostController, MediaViewType, MediaFetchFun) -> Unit
|
||||
|
||||
typealias MediaFetchFun = suspend (service: HomePageService, page: Int) -> Response<out HomePageResponse>
|
||||
@@ -58,7 +58,8 @@ fun MainNavigationRoutes(navController: NavHostController, displayUnderStatusBar
|
||||
@Composable
|
||||
fun BottomNavigationRoutes(
|
||||
appNavController: NavHostController,
|
||||
navController: NavHostController
|
||||
navController: NavHostController,
|
||||
appBarTitle: MutableState<String>
|
||||
) {
|
||||
NavHost(navController = navController, startDestination = BottomNavItem.Movies.route) {
|
||||
composable(BottomNavItem.Movies.route) {
|
||||
@@ -68,7 +69,7 @@ fun BottomNavigationRoutes(
|
||||
MediaTab(appNavController = appNavController, mediaType = MediaViewType.TV)
|
||||
}
|
||||
composable(BottomNavItem.Account.route) {
|
||||
AccountTab()
|
||||
AccountTab(appBarTitle = appBarTitle, appNavController = appNavController)
|
||||
}
|
||||
composable(BottomNavItem.Favourites.route) {
|
||||
FavouritesTab()
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.owenlejeune.tvtime.ui.navigation
|
||||
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
||||
abstract class TabNavItem(val route: String): KoinComponent {
|
||||
abstract val name: String
|
||||
}
|
||||
@@ -42,6 +42,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
import java.text.DecimalFormat
|
||||
|
||||
@Composable
|
||||
@@ -400,7 +401,7 @@ private fun ContentColumn(
|
||||
MiscTvDetails(mediaItem = mediaItem, service as TvService)
|
||||
}
|
||||
|
||||
ActionsView(itemId = itemId, type = mediaType)
|
||||
ActionsView(itemId = itemId, type = mediaType, service = service)
|
||||
|
||||
if (mediaItem.value?.overview?.isNotEmpty() == true) {
|
||||
OverviewCard(mediaItem = mediaItem)
|
||||
@@ -500,9 +501,9 @@ private fun MiscDetails(
|
||||
private fun ActionsView(
|
||||
itemId: Int?,
|
||||
type: MediaViewType,
|
||||
service: DetailService,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
itemId?.let {
|
||||
val session = SessionManager.currentSession
|
||||
Row(
|
||||
@@ -510,23 +511,13 @@ private fun ActionsView(
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
val itemIsRated = if (type == MediaViewType.MOVIE) {
|
||||
session.hasRatedMovie(itemId)
|
||||
} else {
|
||||
session.hasRatedTvShow(itemId)
|
||||
}
|
||||
val showRatingDialog = remember { mutableStateOf(false) }
|
||||
ActionButton(
|
||||
RateButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = if (itemIsRated) stringResource(R.string.delete_rating_action_label) else stringResource(R.string.rate_action_label),
|
||||
onClick = {
|
||||
showRatingDialog.value = true
|
||||
}
|
||||
itemId = itemId,
|
||||
type = type,
|
||||
service = service
|
||||
)
|
||||
RatingDialog(showDialog = showRatingDialog, onValueConfirmed = { rating ->
|
||||
// todo post rating
|
||||
Toast.makeText(context, "Rating :${rating}", Toast.LENGTH_SHORT).show()
|
||||
})
|
||||
|
||||
if (!session.isGuest) {
|
||||
ActionButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
@@ -555,11 +546,69 @@ private fun ActionButton(modifier: Modifier, text: String, onClick: () -> Unit)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RateButton(
|
||||
itemId: Int,
|
||||
type: MediaViewType,
|
||||
service: DetailService,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val session = SessionManager.currentSession
|
||||
val context = LocalContext.current
|
||||
|
||||
var itemIsRated by remember {
|
||||
mutableStateOf(
|
||||
if (type == MediaViewType.MOVIE) {
|
||||
session.hasRatedMovie(itemId)
|
||||
} else {
|
||||
session.hasRatedTvShow(itemId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val showRatingDialog = remember { mutableStateOf(false) }
|
||||
ActionButton(
|
||||
modifier = modifier,
|
||||
text = if (itemIsRated) stringResource(R.string.delete_rating_action_label) else stringResource(R.string.rate_action_label),
|
||||
onClick = {
|
||||
if (!itemIsRated) {
|
||||
showRatingDialog.value = true
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val response = service.deleteRating(itemId)
|
||||
if (response.isSuccessful) {
|
||||
withContext(Dispatchers.Main) {
|
||||
itemIsRated = false
|
||||
}
|
||||
}
|
||||
SessionManager.currentSession.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
RatingDialog(showDialog = showRatingDialog, onValueConfirmed = { rating ->
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val response = service.postRating(itemId, RatingBody(rating = rating))
|
||||
if (response.isSuccessful) {
|
||||
SessionManager.currentSession.refresh()
|
||||
withContext(Dispatchers.Main) {
|
||||
itemIsRated = true
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
val errorObj = JSONObject(response.errorBody().toString())
|
||||
Toast.makeText(context, "Error: ${errorObj.getString("status_message")}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RatingDialog(showDialog: MutableState<Boolean>, onValueConfirmed: (Float) -> Unit) {
|
||||
|
||||
fun formatPosition(position: Float): String {
|
||||
return DecimalFormat("#.#").format(position/10f)
|
||||
return DecimalFormat("#.#").format(position.toInt()*5/10f)
|
||||
}
|
||||
|
||||
if (showDialog.value) {
|
||||
@@ -592,9 +641,11 @@ private fun RatingDialog(showDialog: MutableState<Boolean>, onValueConfirmed: (F
|
||||
text = {
|
||||
SliderWithLabel(
|
||||
value = sliderPosition,
|
||||
valueRange = 0f..100f,
|
||||
onValueChanged = { sliderPosition = it },
|
||||
sliderLabel = formatPosition(sliderPosition)
|
||||
valueRange = 0f..20f,
|
||||
onValueChanged = {
|
||||
sliderPosition = it
|
||||
},
|
||||
sliderLabel = "${sliderPosition.toInt() * 5}%",
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -86,7 +86,7 @@ fun MainAppView(appNavController: NavHostController, preferences: AppPreferences
|
||||
}
|
||||
) { innerPadding ->
|
||||
Box(modifier = Modifier.padding(innerPadding)) {
|
||||
BottomNavigationRoutes(appNavController = appNavController, navController = navController)
|
||||
BottomNavigationRoutes(appNavController = appNavController, navController = navController, appBarTitle = appBarTitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,9 +173,7 @@ private fun BottomNavBar(navController: NavController, appBarTitle: MutableState
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = navBackStackEntry?.destination?.route
|
||||
|
||||
NavigationBar(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
) {
|
||||
NavigationBar {
|
||||
BottomNavItem.Items.forEach { item ->
|
||||
NavigationBarItem(
|
||||
icon = { Icon(painter = painterResource(id = item.icon), contentDescription = null) },
|
||||
@@ -187,17 +185,10 @@ private fun BottomNavBar(navController: NavController, appBarTitle: MutableState
|
||||
appBarTitle = appBarTitle,
|
||||
item = item
|
||||
)
|
||||
},
|
||||
colors = NavigationBarItemDefaults
|
||||
.colors(
|
||||
selectedIconColor = MaterialTheme.colorScheme.secondary,
|
||||
indicatorColor = MaterialTheme.colorScheme.onSecondary
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
appBarTitle.value = BottomNavItem.getByRoute(currentRoute)?.name ?: ""
|
||||
}
|
||||
|
||||
private fun onBottomAppBarItemClicked(
|
||||
|
||||
@@ -7,5 +7,6 @@ enum class MediaViewType {
|
||||
MOVIE,
|
||||
@SerializedName("tv")
|
||||
TV,
|
||||
PERSON
|
||||
PERSON,
|
||||
EPISODE
|
||||
}
|
||||
@@ -1,24 +1,109 @@
|
||||
package com.owenlejeune.tvtime.ui.screens.tabs.bottom
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavHostController
|
||||
import coil.compose.rememberImagePainter
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.PagerState
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import com.owenlejeune.tvtime.ui.navigation.AccountTabNavItem
|
||||
import com.owenlejeune.tvtime.ui.navigation.ListFetchFun
|
||||
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
|
||||
import com.owenlejeune.tvtime.ui.screens.MediaViewType
|
||||
import com.owenlejeune.tvtime.ui.screens.tabs.top.Tabs
|
||||
import com.owenlejeune.tvtime.utils.SessionManager
|
||||
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun AccountTab() {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.wrapContentSize(Alignment.Center)
|
||||
) {
|
||||
Text(
|
||||
text = "Account",
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
fun AccountTab(appNavController: NavHostController, appBarTitle: MutableState<String>) {
|
||||
if (SessionManager.currentSession.isGuest) {
|
||||
appBarTitle.value = "Hello, Guest"
|
||||
}
|
||||
|
||||
val tabs = if (SessionManager.currentSession.isGuest) {
|
||||
AccountTabNavItem.GuestItems
|
||||
} else {
|
||||
AccountTabNavItem.GuestItems
|
||||
}
|
||||
|
||||
Column {
|
||||
val pagerState = rememberPagerState()
|
||||
Tabs(tabs = tabs, pagerState = pagerState)
|
||||
AccountTabs(
|
||||
appNavController = appNavController,
|
||||
tabs = tabs,
|
||||
pagerState = pagerState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AccountTabContent(
|
||||
appNavController: NavHostController,
|
||||
mediaViewType: MediaViewType,
|
||||
listFetchFun: ListFetchFun
|
||||
) {
|
||||
val contentItems = listFetchFun()
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth().padding(12.dp)) {
|
||||
items(contentItems.size) { i ->
|
||||
val ratedItem = contentItems[i]
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.clickable(
|
||||
onClick = {
|
||||
appNavController.navigate(
|
||||
"${MainNavItem.DetailView.route}/${mediaViewType}/${ratedItem.id}"
|
||||
)
|
||||
}
|
||||
)
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.width(60.dp)
|
||||
.height(80.dp),
|
||||
painter = rememberImagePainter(
|
||||
data = TmdbUtils.getFullPosterPath(ratedItem.posterPath)
|
||||
),
|
||||
contentDescription = ""
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.height(80.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(text = ratedItem.title, color = MaterialTheme.colorScheme.onBackground, fontSize = 18.sp)
|
||||
|
||||
Text(text = ratedItem.releaseDate, color = MaterialTheme.colorScheme.onBackground)
|
||||
|
||||
Text(text = "Rating: ${(ratedItem.rating*10).toInt()}%", color = MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun AccountTabs(
|
||||
tabs: List<AccountTabNavItem>,
|
||||
pagerState: PagerState,
|
||||
appNavController: NavHostController
|
||||
) {
|
||||
HorizontalPager(count = tabs.size, state = pagerState) { page ->
|
||||
tabs[page].screen(appNavController, tabs[page].mediaType, tabs[page].listFetchFun)
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,22 @@ package com.owenlejeune.tvtime.ui.screens.tabs.bottom
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.PagerState
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import com.owenlejeune.tvtime.api.tmdb.HomePageService
|
||||
import com.owenlejeune.tvtime.api.tmdb.MoviesService
|
||||
import com.owenlejeune.tvtime.api.tmdb.TvService
|
||||
import com.owenlejeune.tvtime.ui.components.PosterGrid
|
||||
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
|
||||
import com.owenlejeune.tvtime.ui.navigation.MainTabNavItem
|
||||
import com.owenlejeune.tvtime.ui.navigation.MediaFetchFun
|
||||
import com.owenlejeune.tvtime.ui.navigation.MediaTabNavItem
|
||||
import com.owenlejeune.tvtime.ui.screens.MediaViewType
|
||||
import com.owenlejeune.tvtime.ui.screens.tabs.top.Tabs
|
||||
import com.owenlejeune.tvtime.ui.screens.tabs.top.TabsContent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -25,13 +28,13 @@ import kotlinx.coroutines.withContext
|
||||
fun MediaTab(appNavController: NavHostController, mediaType: MediaViewType) {
|
||||
Column {
|
||||
val tabs = when (mediaType) {
|
||||
MediaViewType.MOVIE -> MainTabNavItem.MovieItems
|
||||
MediaViewType.TV -> MainTabNavItem.TvItems
|
||||
MediaViewType.MOVIE -> MediaTabNavItem.MovieItems
|
||||
MediaViewType.TV -> MediaTabNavItem.TvItems
|
||||
else -> throw IllegalArgumentException("Media type given: ${mediaType}, \n expected one of MediaViewType.MOVIE, MediaViewType.TV") // shouldn't happen
|
||||
}
|
||||
val pagerState = rememberPagerState()
|
||||
Tabs(tabs = tabs, pagerState = pagerState)
|
||||
TabsContent(
|
||||
MediaTabs(
|
||||
tabs = tabs,
|
||||
pagerState = pagerState,
|
||||
appNavController = appNavController,
|
||||
@@ -66,6 +69,28 @@ fun MediaTabContent(appNavController: NavHostController, mediaType: MediaViewTyp
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun MediaTabs(
|
||||
tabs: List<MediaTabNavItem>,
|
||||
pagerState: PagerState,
|
||||
mediaViewType: MediaViewType,
|
||||
appNavController: NavHostController = rememberNavController()
|
||||
) {
|
||||
HorizontalPager(count = tabs.size, state = pagerState) { page ->
|
||||
tabs[page].screen(appNavController, mediaViewType, tabs[page].mediaFetchFun)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun MediaTabsPreview() {
|
||||
val tabs = MediaTabNavItem.MovieItems
|
||||
val pagerState = rememberPagerState()
|
||||
MediaTabs(tabs = tabs, pagerState = pagerState, MediaViewType.MOVIE)
|
||||
}
|
||||
|
||||
// val moviesViewModel = viewModel(PopularMovieViewModel::class.java)
|
||||
// val moviesList = moviesViewModel.moviePage
|
||||
// val movieListItems: LazyPagingItems<PopularMovie> = moviesList.collectAsLazyPagingItems()
|
||||
@@ -5,8 +5,8 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ScrollableTabRow
|
||||
import androidx.compose.material.Tab
|
||||
import androidx.compose.material.TabRow
|
||||
import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@@ -17,15 +17,11 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.PagerState
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import com.owenlejeune.tvtime.ui.navigation.MainTabNavItem
|
||||
import com.owenlejeune.tvtime.ui.navigation.MediaTabNavItem
|
||||
import com.owenlejeune.tvtime.ui.navigation.TabNavItem
|
||||
import com.owenlejeune.tvtime.ui.screens.MediaViewType
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@@ -43,12 +39,11 @@ fun Tabs(
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
ScrollableTabRow(
|
||||
TabRow(
|
||||
modifier = modifier,
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
backgroundColor = backgroundColor,
|
||||
contentColor = contentColor,
|
||||
edgePadding = 8.dp,
|
||||
indicator = { tabPositions ->
|
||||
SmallTabIndicator(
|
||||
modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]),
|
||||
@@ -93,30 +88,8 @@ private fun SmallTabIndicator(
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun TabsPreview() {
|
||||
val tabs = MainTabNavItem.MovieItems
|
||||
val tabs = MediaTabNavItem.MovieItems
|
||||
val pagerState = rememberPagerState()
|
||||
Tabs(tabs = tabs, pagerState = pagerState)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun TabsContent(
|
||||
tabs: List<TabNavItem>,
|
||||
pagerState: PagerState,
|
||||
mediaViewType: MediaViewType,
|
||||
appNavController: NavHostController = rememberNavController()
|
||||
) {
|
||||
HorizontalPager(count = tabs.size, state = pagerState) { page ->
|
||||
tabs[page].screen(appNavController, mediaViewType, tabs[page].mediaFetchFun)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun TabsContentPreview() {
|
||||
val tabs = MainTabNavItem.MovieItems
|
||||
val pagerState = rememberPagerState()
|
||||
TabsContent(tabs = tabs, pagerState = pagerState, MediaViewType.MOVIE)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.owenlejeune.tvtime.utils
|
||||
|
||||
import com.owenlejeune.tvtime.api.tmdb.GuestSessionApi
|
||||
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.RatedMedia
|
||||
import com.owenlejeune.tvtime.preferences.AppPreferences
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
@@ -61,6 +64,8 @@ object SessionManager: KoinComponent {
|
||||
}
|
||||
|
||||
abstract suspend fun initialize()
|
||||
|
||||
abstract suspend fun refresh()
|
||||
}
|
||||
|
||||
private class GuestSession: Session(preferences.guestSessionId, true) {
|
||||
@@ -68,21 +73,33 @@ object SessionManager: KoinComponent {
|
||||
override var _ratedTvEpisodes: List<RatedMedia> = emptyList()
|
||||
override var _ratedTvShows: List<RatedMedia> = emptyList()
|
||||
|
||||
private lateinit var service: GuestSessionApi
|
||||
|
||||
override suspend fun initialize() {
|
||||
val service = TmdbClient().createGuestSessionService()
|
||||
service = TmdbClient().createGuestSessionService()
|
||||
refresh()
|
||||
}
|
||||
|
||||
override suspend fun refresh() {
|
||||
service.getRatedMovies(sessionId).apply {
|
||||
if (isSuccessful) {
|
||||
_ratedMovies = body()?.results ?: emptyList()
|
||||
withContext(Dispatchers.Main) {
|
||||
_ratedMovies = body()?.results ?: _ratedMovies
|
||||
}
|
||||
}
|
||||
}
|
||||
service.getRatedTvShows(sessionId).apply {
|
||||
if (isSuccessful) {
|
||||
_ratedTvShows = body()?.results ?: emptyList()
|
||||
withContext(Dispatchers.Main) {
|
||||
_ratedTvShows = body()?.results ?: _ratedTvShows
|
||||
}
|
||||
}
|
||||
}
|
||||
service.getRatedTvEpisodes(sessionId).apply {
|
||||
if (isSuccessful) {
|
||||
_ratedTvEpisodes = body()?.results ?: emptyList()
|
||||
withContext(Dispatchers.Main) {
|
||||
_ratedTvEpisodes = body()?.results ?: _ratedTvEpisodes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,4 +54,7 @@
|
||||
<string name="rating_dialog_confirm">Submit rating</string>
|
||||
<string name="action_cancel">Cancel</string>
|
||||
<string name="nav_account_title">Account</string>
|
||||
<string name="nav_rated_movies_title">Rated Movies</string>
|
||||
<string name="nav_rated_shows_title">Rated TV Shows</string>
|
||||
<string name="nav_rated_episodes_title">Rated TV Episodes</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user