add ability to rate as guest and see rated content

This commit is contained in:
Owen LeJeune
2022-02-28 19:19:26 -05:00
parent 11a1b5c68d
commit be23d58b1d
22 changed files with 408 additions and 172 deletions

View File

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

View File

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

View File

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

View File

@@ -2,9 +2,7 @@ package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.*
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
import retrofit2.http.*
interface 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>
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}%",
)
}
)

View File

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

View File

@@ -7,5 +7,6 @@ enum class MediaViewType {
MOVIE,
@SerializedName("tv")
TV,
PERSON
PERSON,
EPISODE
}

View File

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

View File

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

View File

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

View File

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

View File

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