From 216457c148799754d983e76383ce1960b1d541f2 Mon Sep 17 00:00:00 2001 From: Owen LeJeune Date: Fri, 4 Nov 2022 15:42:28 -0400 Subject: [PATCH] redesign account tab media views --- .../com/owenlejeune/tvtime/MainActivity.kt | 4 - .../api/tmdb/api/v3/model/AuthorDetails.kt | 2 +- .../tvtime/ui/components/Widgets.kt | 24 +-- .../tvtime/ui/navigation/AccountTabNavItem.kt | 32 ++-- .../tvtime/ui/navigation/Routes.kt | 1 + .../tvtime/ui/screens/main/AccountTab.kt | 181 +++++++++++++----- .../tvtime/ui/screens/main/MediaDetailView.kt | 13 +- .../tvtime/utils/SessionManager.kt | 14 +- 8 files changed, 167 insertions(+), 104 deletions(-) diff --git a/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt b/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt index 2050131..7fb1356 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt @@ -338,10 +338,6 @@ class MainActivity : MonetCompatActivity() { val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route -// if (currentRoute in searchableScreens) { -// SearchBar(appBarTitle.value) -// } - MainNavGraph( activity = this@MainActivity, appNavController = appNavController, diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/AuthorDetails.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/AuthorDetails.kt index 896185d..4bfd5ae 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/AuthorDetails.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/AuthorDetails.kt @@ -6,5 +6,5 @@ class AuthorDetails( @SerializedName("name") val name: String, @SerializedName("username") val username: String, @SerializedName("avatar_path") val avatarPath: String?, - @SerializedName("rating") val rating: Int + @SerializedName("rating") val rating: Float ) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt index 9181b99..225ca14 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt @@ -456,8 +456,6 @@ fun RatingRing( Box( modifier = modifier .size(size) -// .size(60.dp) -// .padding(8.dp) ) { CircularProgressIndicator( modifier = Modifier.fillMaxSize(), @@ -716,22 +714,6 @@ fun AvatarImage( size = size, character = text ) -// Box( -// modifier = Modifier -// .clip(CircleShape) -// .size(size) -// .background(color = MaterialTheme.colorScheme.tertiary) -// ) { -// Text( -// modifier = Modifier -// .fillMaxSize() -// .padding(top = size / 5), -// 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 -// ) -// } } } @@ -739,8 +721,7 @@ fun AvatarImage( fun RoundedLetterImage( size: Dp, character: Char, - modifier: Modifier = Modifier, - topPadding: Dp = size / 5 + modifier: Modifier = Modifier ) { Box( modifier = modifier @@ -750,8 +731,7 @@ fun RoundedLetterImage( ) { Text( modifier = Modifier - .fillMaxSize() - .padding(top = topPadding), + .align(Alignment.Center), text = character.uppercase(), color = MaterialTheme.colorScheme.onTertiary, textAlign = TextAlign.Center, diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/AccountTabNavItem.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/AccountTabNavItem.kt index 6a5aa1e..71883cd 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/AccountTabNavItem.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/AccountTabNavItem.kt @@ -11,24 +11,34 @@ import com.owenlejeune.tvtime.utils.SessionManager import org.koin.core.component.inject import kotlin.reflect.KClass -sealed class AccountTabNavItem(stringRes: Int, route: String, val mediaType: MediaViewType, val screen: AccountNavComposableFun, val listFetchFun: ListFetchFun, val listType: KClass<*>): TabNavItem(route) { +sealed class AccountTabNavItem( + stringRes: Int, + route: String, + val mediaType: MediaViewType, + val screen: AccountNavComposableFun, + val listFetchFun: ListFetchFun, + val listType: KClass<*>, + val ordinal: Int +): TabNavItem(route) { private val resourceUtils: ResourceUtils by inject() override val name = resourceUtils.getString(stringRes) companion object { - val GuestItems = listOf(RatedMovies, RatedTvShows, RatedTvEpisodes) - val AuthorizedItems = listOf(RatedMovies, RatedTvShows, RatedTvEpisodes, FavoriteMovies, FavoriteTvShows, MovieWatchlist, TvWatchlist) + val GuestItems + get() = listOf(RatedMovies, RatedTvShows, RatedTvEpisodes) + + val AuthorizedItems + get() = listOf(RatedMovies, RatedTvShows, RatedTvEpisodes, FavoriteMovies, FavoriteTvShows, MovieWatchlist, TvWatchlist).filter { it.ordinal > -1 }.sortedBy { it.ordinal } } - object RatedMovies: AccountTabNavItem(R.string.nav_rated_movies_title, "rated_movies_route", MediaViewType.MOVIE, screenContent, { SessionManager.currentSession?.ratedMovies ?: emptyList() }, RatedMovie::class) - object RatedTvShows: AccountTabNavItem(R.string.nav_rated_shows_title, "rated_shows_route", MediaViewType.TV, screenContent, { SessionManager.currentSession?.ratedTvShows ?: emptyList() }, RatedTv::class) - object RatedTvEpisodes: AccountTabNavItem(R.string.nav_rated_episodes_title, "rated_episodes_route", MediaViewType.EPISODE, screenContent, { SessionManager.currentSession?.ratedTvEpisodes ?: emptyList() }, RatedEpisode::class) -// object Lists - object FavoriteMovies: AccountTabNavItem(R.string.nav_favorite_movies_title, "favorite_movies_route", MediaViewType.MOVIE, screenContent, { SessionManager.currentSession?.favoriteMovies ?: emptyList() }, FavoriteMovie::class) - object FavoriteTvShows: AccountTabNavItem(R.string.nav_favorite_tv_show_title, "favorite_shows_route", MediaViewType.TV, screenContent, { SessionManager.currentSession?.favoriteTvShows ?: emptyList() }, FavoriteTvSeries::class) - object MovieWatchlist: AccountTabNavItem(R.string.nav_movie_watchlist_title, "movie_watchlist_route", MediaViewType.MOVIE, screenContent, { SessionManager.currentSession?.movieWatchlist ?: emptyList() }, WatchlistMovie::class) - object TvWatchlist: AccountTabNavItem(R.string.nav_tv_watchlist_title, "tv_watchlist_route", MediaViewType.TV, screenContent, { SessionManager.currentSession?.tvWatchlist ?: emptyList() }, WatchlistTvSeries::class) + object RatedMovies: AccountTabNavItem(R.string.nav_rated_movies_title, "rated_movies_route", MediaViewType.MOVIE, screenContent, { SessionManager.currentSession?.ratedMovies ?: emptyList() }, RatedMovie::class, 0) + object RatedTvShows: AccountTabNavItem(R.string.nav_rated_shows_title, "rated_shows_route", MediaViewType.TV, screenContent, { SessionManager.currentSession?.ratedTvShows ?: emptyList() }, RatedTv::class, 1) + object RatedTvEpisodes: AccountTabNavItem(R.string.nav_rated_episodes_title, "rated_episodes_route", MediaViewType.EPISODE, screenContent, { SessionManager.currentSession?.ratedTvEpisodes ?: emptyList() }, RatedEpisode::class, 2) + object FavoriteMovies: AccountTabNavItem(R.string.nav_favorite_movies_title, "favorite_movies_route", MediaViewType.MOVIE, screenContent, { SessionManager.currentSession?.favoriteMovies ?: emptyList() }, FavoriteMovie::class, 3) + object FavoriteTvShows: AccountTabNavItem(R.string.nav_favorite_tv_show_title, "favorite_shows_route", MediaViewType.TV, screenContent, { SessionManager.currentSession?.favoriteTvShows ?: emptyList() }, FavoriteTvSeries::class, 4) + object MovieWatchlist: AccountTabNavItem(R.string.nav_movie_watchlist_title, "movie_watchlist_route", MediaViewType.MOVIE, screenContent, { SessionManager.currentSession?.movieWatchlist ?: emptyList() }, WatchlistMovie::class, 5) + object TvWatchlist: AccountTabNavItem(R.string.nav_tv_watchlist_title, "tv_watchlist_route", MediaViewType.TV, screenContent, { SessionManager.currentSession?.tvWatchlist ?: emptyList() }, WatchlistTvSeries::class, 6) } private val screenContent: AccountNavComposableFun = { appNavController, mediaViewType, listFetchFun, clazz -> diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt index d00a7a2..a80d837 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt @@ -47,6 +47,7 @@ fun MainNavGraph( } composable(BottomNavItem.Account.route) { AccountTab(appBarTitle = appBarTitle, appNavController = appNavController, appBarActions = appBarActions) + fab.value = {} } composable(BottomNavItem.People.route) { appBarActions.value = {} diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/AccountTab.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/AccountTab.kt index cb7205b..ee70819 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/AccountTab.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/AccountTab.kt @@ -1,13 +1,14 @@ package com.owenlejeune.tvtime.ui.screens.main import android.content.Context +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle -import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -15,13 +16,19 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale 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 +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension import androidx.navigation.NavHostController import coil.compose.AsyncImage import com.google.accompanist.pager.ExperimentalPagerApi @@ -43,7 +50,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.koin.java.KoinJavaComponent import org.koin.java.KoinJavaComponent.get import kotlin.reflect.KClass @@ -184,9 +190,11 @@ fun AccountTabContent( mediaViewType = mediaViewType, id = item.id, posterPath = TmdbUtils.getFullPosterPath(item.posterPath), + backdropPath = TmdbUtils.getFullBackdropPath(item.backdropPath), name = item.name, date = item.releaseDate, - rating = item.rating + rating = item.rating, + description = item.overview ) } RatedEpisode::class -> { @@ -197,7 +205,8 @@ fun AccountTabContent( id = item.id, name = item.name, date = item.releaseDate, - rating = item.rating + rating = item.rating, + description = item.overview ) } FavoriteMovie::class, FavoriteTvSeries::class -> { @@ -207,8 +216,10 @@ fun AccountTabContent( mediaViewType = mediaViewType, id = item.id, posterPath = TmdbUtils.getFullPosterPath(item.posterPath), + backdropPath = TmdbUtils.getFullBackdropPath(item.backdropPath), name = item.title, - date = item.releaseDate + date = item.releaseDate, + description = item.overview ) } WatchlistMovie::class, WatchlistTvSeries::class -> { @@ -218,8 +229,10 @@ fun AccountTabContent( mediaViewType = mediaViewType, id = item.id, posterPath = TmdbUtils.getFullPosterPath(item.posterPath), + backdropPath = TmdbUtils.getFullBackdropPath(item.backdropPath), name = item.title, - date = item.releaseDate + date = item.releaseDate, + description = item.overview ) } } @@ -228,6 +241,7 @@ fun AccountTabContent( } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun MediaItemRow( appNavController: NavHostController, @@ -235,46 +249,109 @@ private fun MediaItemRow( id: Int, name: String, date: String, + description: String, posterPath: String? = null, + backdropPath: String? = null, rating: Float? = null ) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.clickable( - onClick = { - appNavController.navigate( - "${MainNavItem.DetailView.route}/${mediaViewType}/${id}" + Card( + shape = RoundedCornerShape(10.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 10.dp), + modifier = Modifier + .background(color = MaterialTheme.colorScheme.surface) + .fillMaxWidth() + .clickable( + onClick = { + appNavController.navigate( + "${MainNavItem.DetailView.route}/${mediaViewType}/${id}" + ) + } + ) + ) { + Box( + modifier = Modifier + .height(112.dp) + ) { + AsyncImage( + model = backdropPath, + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .blur(radius = 10.dp) + .fillMaxWidth() + .wrapContentHeight() + ) + + backdropPath?.let { + Box(modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.7f)) + .blur(radius = 10.dp) ) } - ) - ) { - AsyncImage( - modifier = Modifier - .size(width = 60.dp, height = 80.dp), - model = posterPath, - contentDescription = "" - ) - Column( - modifier = Modifier.height(80.dp), - verticalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = name, - color = MaterialTheme.colorScheme.onBackground, - fontSize = 18.sp - ) + ConstraintLayout( + modifier = Modifier + .padding(8.dp) + .fillMaxSize() + ) { + val (poster, content, ratingView) = createRefs() - Text( - text = date, - color = MaterialTheme.colorScheme.onBackground - ) - - if (rating != null) { - Text( - text = stringResource(id = R.string.rating_test, (rating * 10).toInt()), - color = MaterialTheme.colorScheme.onBackground + AsyncImage( + modifier = Modifier + .constrainAs(poster) { + start.linkTo(parent.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + height = Dimension.fillToConstraints + } + .clip(RoundedCornerShape(10.dp)), + model = posterPath, + contentDescription = "", ) + + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .constrainAs(content) { + end.linkTo( + rating?.let { ratingView.start } ?: parent.end, + margin = 8.dp + ) + start.linkTo(poster.end, margin = 8.dp) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + width = Dimension.fillToConstraints + height = Dimension.fillToConstraints + } + ) { + val releaseYear = date.split("-")[0] + Text( + text = "$name (${releaseYear})", + color = MaterialTheme.colorScheme.onBackground, + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + + Text( + text = description, + color = MaterialTheme.colorScheme.onBackground, + fontSize = 14.sp, + overflow = TextOverflow.Ellipsis + ) + } + + rating?.let { + RatingView( + progress = rating/10, + modifier = Modifier + .constrainAs(ratingView) { + end.linkTo(parent.end) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) + } } } } @@ -290,7 +367,6 @@ private fun AccountDropdownMenu( IconButton( onClick = { expanded.value = true } ) { -// Icon(imageVector = Icons.Filled.MoreVert, contentDescription = null) Icon(imageVector = Icons.Filled.AccountCircle, contentDescription = stringResource(id = R.string.nav_account_title)) } @@ -372,18 +448,17 @@ private fun NoSessionMenuItems( } } - if (!preferences.useV4Api) { - DropdownMenuItem( - text = { Text(text = stringResource(id = R.string.action_sign_in)) }, - onClick = { showSignInDialog.value = true } - ) - } else { - val context = LocalContext.current - DropdownMenuItem( - text = { Text(text = stringResource(id = R.string.action_sign_in)) }, - onClick = { v4SignInPart1(context) } - ) - } + val context = LocalContext.current + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_sign_in)) }, + onClick = { + if (preferences.useV4Api) { + v4SignInPart1(context) + } else { + showSignInDialog.value = true + } + } + ) DropdownMenuItem( text = { Text(text = stringResource(id = R.string.action_sign_in_as_guest)) }, @@ -414,7 +489,7 @@ private fun v4SignInPart2(lastSelectedOption: MutableState) { @Composable private fun GuestSessionIcon() { val guestName = stringResource(id = R.string.account_name_guest) - RoundedLetterImage(size = 60.dp, character = guestName[0], topPadding = 60.dp / 4) + RoundedLetterImage(size = 60.dp, character = guestName[0]) } @Composable @@ -435,7 +510,7 @@ private fun AuthorizedSessionIcon() { Box(modifier = Modifier.padding(start = 12.dp)) { if (accountDetails == null || avatarUrl == null) { val accLetter = (accountDetails?.name?.ifEmpty { accountDetails.username } ?: " ")[0] - RoundedLetterImage(size = 60.dp, character = accLetter, topPadding = 60.dp / 4) + RoundedLetterImage(size = 60.dp, character = accLetter) } else { Box(modifier = Modifier.size(60.dp)) { AsyncImage( diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaDetailView.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaDetailView.kt index 3327d83..9764e8a 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaDetailView.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaDetailView.kt @@ -75,7 +75,7 @@ fun MediaDetailView( val decayAnimationSpec = rememberSplineBasedDecay() val topAppBarScrollState = rememberTopAppBarScrollState() val scrollBehavior = remember(decayAnimationSpec) { - TopAppBarDefaults.exitUntilCollapsedScrollBehavior(decayAnimationSpec, topAppBarScrollState) + TopAppBarDefaults.pinnedScrollBehavior(topAppBarScrollState) } Scaffold( @@ -84,7 +84,7 @@ fun MediaDetailView( SmallTopAppBar( scrollBehavior = scrollBehavior, colors = TopAppBarDefaults - .largeTopAppBarColors( + .smallTopAppBarColors( scrolledContainerColor = MaterialTheme.colorScheme.background, titleContentColor = MaterialTheme.colorScheme.primary ), @@ -902,8 +902,15 @@ private fun ReviewsCard( textColor = MaterialTheme.colorScheme.onSecondary ) + val context = LocalContext.current CircleBackgroundColorImage( - modifier = Modifier.align(Alignment.CenterVertically), + modifier = Modifier + .align(Alignment.CenterVertically) + .clickable ( + onClick = { + Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show() + } + ), size = 40.dp, backgroundColor = MaterialTheme.colorScheme.tertiary, image = Icons.Filled.Send, diff --git a/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt b/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt index 8bd041f..4403128 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt @@ -51,7 +51,6 @@ object SessionManager: KoinComponent { _currentSession = null preferences.guestSessionId = "" preferences.authorizedSessionValues = null -// preferences.authorizedSessionId = "" } onResponse(deleteResponse.isSuccessful) } @@ -68,7 +67,6 @@ object SessionManager: KoinComponent { _currentSession = null preferences.guestSessionId = "" preferences.authorizedSessionValues = null -// preferences.authorizedSessionId = "" } onResponse(deleteResponse.isSuccessful) } @@ -314,14 +312,10 @@ object SessionManager: KoinComponent { if (changed.contains(Changed.AccountDetails)) { service.getAccountDetails().apply { if (isSuccessful) { -// withContext(Dispatchers.Main) { - _accountDetails = body() ?: _accountDetails - accountDetails?.let { -// CoroutineScope(Dispatchers.IO).launch { - refreshWithAccountId(it.id, changed) -// } - } -// } + _accountDetails = body() ?: _accountDetails + accountDetails?.let { + refreshWithAccountId(it.id, changed) + } } } } else if (accountDetails != null) {