redesign account tab media views

This commit is contained in:
Owen LeJeune
2022-11-04 15:42:28 -04:00
parent f8b6b67816
commit 216457c148
8 changed files with 167 additions and 104 deletions

View File

@@ -338,10 +338,6 @@ class MainActivity : MonetCompatActivity() {
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route val currentRoute = navBackStackEntry?.destination?.route
// if (currentRoute in searchableScreens) {
// SearchBar(appBarTitle.value)
// }
MainNavGraph( MainNavGraph(
activity = this@MainActivity, activity = this@MainActivity,
appNavController = appNavController, appNavController = appNavController,

View File

@@ -6,5 +6,5 @@ class AuthorDetails(
@SerializedName("name") val name: String, @SerializedName("name") val name: String,
@SerializedName("username") val username: String, @SerializedName("username") val username: String,
@SerializedName("avatar_path") val avatarPath: String?, @SerializedName("avatar_path") val avatarPath: String?,
@SerializedName("rating") val rating: Int @SerializedName("rating") val rating: Float
) )

View File

@@ -456,8 +456,6 @@ fun RatingRing(
Box( Box(
modifier = modifier modifier = modifier
.size(size) .size(size)
// .size(60.dp)
// .padding(8.dp)
) { ) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -716,22 +714,6 @@ fun AvatarImage(
size = size, size = size,
character = text 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( fun RoundedLetterImage(
size: Dp, size: Dp,
character: Char, character: Char,
modifier: Modifier = Modifier, modifier: Modifier = Modifier
topPadding: Dp = size / 5
) { ) {
Box( Box(
modifier = modifier modifier = modifier
@@ -750,8 +731,7 @@ fun RoundedLetterImage(
) { ) {
Text( Text(
modifier = Modifier modifier = Modifier
.fillMaxSize() .align(Alignment.Center),
.padding(top = topPadding),
text = character.uppercase(), text = character.uppercase(),
color = MaterialTheme.colorScheme.onTertiary, color = MaterialTheme.colorScheme.onTertiary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,

View File

@@ -11,24 +11,34 @@ import com.owenlejeune.tvtime.utils.SessionManager
import org.koin.core.component.inject import org.koin.core.component.inject
import kotlin.reflect.KClass 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() private val resourceUtils: ResourceUtils by inject()
override val name = resourceUtils.getString(stringRes) override val name = resourceUtils.getString(stringRes)
companion object { companion object {
val GuestItems = listOf(RatedMovies, RatedTvShows, RatedTvEpisodes) val GuestItems
val AuthorizedItems = listOf(RatedMovies, RatedTvShows, RatedTvEpisodes, FavoriteMovies, FavoriteTvShows, MovieWatchlist, TvWatchlist) 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 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) 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) object RatedTvEpisodes: AccountTabNavItem(R.string.nav_rated_episodes_title, "rated_episodes_route", MediaViewType.EPISODE, screenContent, { SessionManager.currentSession?.ratedTvEpisodes ?: emptyList() }, RatedEpisode::class, 2)
// object Lists object FavoriteMovies: AccountTabNavItem(R.string.nav_favorite_movies_title, "favorite_movies_route", MediaViewType.MOVIE, screenContent, { SessionManager.currentSession?.favoriteMovies ?: emptyList() }, FavoriteMovie::class, 3)
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, 4)
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, 5)
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, 6)
object TvWatchlist: AccountTabNavItem(R.string.nav_tv_watchlist_title, "tv_watchlist_route", MediaViewType.TV, screenContent, { SessionManager.currentSession?.tvWatchlist ?: emptyList() }, WatchlistTvSeries::class)
} }
private val screenContent: AccountNavComposableFun = { appNavController, mediaViewType, listFetchFun, clazz -> private val screenContent: AccountNavComposableFun = { appNavController, mediaViewType, listFetchFun, clazz ->

View File

@@ -47,6 +47,7 @@ fun MainNavGraph(
} }
composable(BottomNavItem.Account.route) { composable(BottomNavItem.Account.route) {
AccountTab(appBarTitle = appBarTitle, appNavController = appNavController, appBarActions = appBarActions) AccountTab(appBarTitle = appBarTitle, appNavController = appNavController, appBarActions = appBarActions)
fab.value = {}
} }
composable(BottomNavItem.People.route) { composable(BottomNavItem.People.route) {
appBarActions.value = {} appBarActions.value = {}

View File

@@ -1,13 +1,14 @@
package com.owenlejeune.tvtime.ui.screens.main package com.owenlejeune.tvtime.ui.screens.main
import android.content.Context import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
@@ -15,13 +16,19 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
@@ -43,7 +50,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.java.KoinJavaComponent
import org.koin.java.KoinJavaComponent.get import org.koin.java.KoinJavaComponent.get
import kotlin.reflect.KClass import kotlin.reflect.KClass
@@ -184,9 +190,11 @@ fun <T: Any> AccountTabContent(
mediaViewType = mediaViewType, mediaViewType = mediaViewType,
id = item.id, id = item.id,
posterPath = TmdbUtils.getFullPosterPath(item.posterPath), posterPath = TmdbUtils.getFullPosterPath(item.posterPath),
backdropPath = TmdbUtils.getFullBackdropPath(item.backdropPath),
name = item.name, name = item.name,
date = item.releaseDate, date = item.releaseDate,
rating = item.rating rating = item.rating,
description = item.overview
) )
} }
RatedEpisode::class -> { RatedEpisode::class -> {
@@ -197,7 +205,8 @@ fun <T: Any> AccountTabContent(
id = item.id, id = item.id,
name = item.name, name = item.name,
date = item.releaseDate, date = item.releaseDate,
rating = item.rating rating = item.rating,
description = item.overview
) )
} }
FavoriteMovie::class, FavoriteTvSeries::class -> { FavoriteMovie::class, FavoriteTvSeries::class -> {
@@ -207,8 +216,10 @@ fun <T: Any> AccountTabContent(
mediaViewType = mediaViewType, mediaViewType = mediaViewType,
id = item.id, id = item.id,
posterPath = TmdbUtils.getFullPosterPath(item.posterPath), posterPath = TmdbUtils.getFullPosterPath(item.posterPath),
backdropPath = TmdbUtils.getFullBackdropPath(item.backdropPath),
name = item.title, name = item.title,
date = item.releaseDate date = item.releaseDate,
description = item.overview
) )
} }
WatchlistMovie::class, WatchlistTvSeries::class -> { WatchlistMovie::class, WatchlistTvSeries::class -> {
@@ -218,8 +229,10 @@ fun <T: Any> AccountTabContent(
mediaViewType = mediaViewType, mediaViewType = mediaViewType,
id = item.id, id = item.id,
posterPath = TmdbUtils.getFullPosterPath(item.posterPath), posterPath = TmdbUtils.getFullPosterPath(item.posterPath),
backdropPath = TmdbUtils.getFullBackdropPath(item.backdropPath),
name = item.title, name = item.title,
date = item.releaseDate date = item.releaseDate,
description = item.overview
) )
} }
} }
@@ -228,6 +241,7 @@ fun <T: Any> AccountTabContent(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun MediaItemRow( private fun MediaItemRow(
appNavController: NavHostController, appNavController: NavHostController,
@@ -235,12 +249,18 @@ private fun MediaItemRow(
id: Int, id: Int,
name: String, name: String,
date: String, date: String,
description: String,
posterPath: String? = null, posterPath: String? = null,
backdropPath: String? = null,
rating: Float? = null rating: Float? = null
) { ) {
Row( Card(
horizontalArrangement = Arrangement.spacedBy(8.dp), shape = RoundedCornerShape(10.dp),
modifier = Modifier.clickable( elevation = CardDefaults.cardElevation(defaultElevation = 10.dp),
modifier = Modifier
.background(color = MaterialTheme.colorScheme.surface)
.fillMaxWidth()
.clickable(
onClick = { onClick = {
appNavController.navigate( appNavController.navigate(
"${MainNavItem.DetailView.route}/${mediaViewType}/${id}" "${MainNavItem.DetailView.route}/${mediaViewType}/${id}"
@@ -248,36 +268,93 @@ private fun MediaItemRow(
} }
) )
) { ) {
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)
)
}
ConstraintLayout(
modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
val (poster, content, ratingView) = createRefs()
AsyncImage( AsyncImage(
modifier = Modifier modifier = Modifier
.size(width = 60.dp, height = 80.dp), .constrainAs(poster) {
start.linkTo(parent.start)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
height = Dimension.fillToConstraints
}
.clip(RoundedCornerShape(10.dp)),
model = posterPath, model = posterPath,
contentDescription = "" contentDescription = "",
) )
Column( Column(
modifier = Modifier.height(80.dp), verticalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.SpaceBetween 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(
text = name, text = "$name (${releaseYear})",
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
fontSize = 18.sp fontSize = 18.sp,
fontWeight = FontWeight.Bold
) )
Text( Text(
text = date, text = description,
color = MaterialTheme.colorScheme.onBackground color = MaterialTheme.colorScheme.onBackground,
fontSize = 14.sp,
overflow = TextOverflow.Ellipsis
) )
}
if (rating != null) { rating?.let {
Text( RatingView(
text = stringResource(id = R.string.rating_test, (rating * 10).toInt()), progress = rating/10,
color = MaterialTheme.colorScheme.onBackground modifier = Modifier
.constrainAs(ratingView) {
end.linkTo(parent.end)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
}
) )
} }
} }
} }
}
} }
@Composable @Composable
@@ -290,7 +367,6 @@ private fun AccountDropdownMenu(
IconButton( IconButton(
onClick = { expanded.value = true } onClick = { expanded.value = true }
) { ) {
// Icon(imageVector = Icons.Filled.MoreVert, contentDescription = null)
Icon(imageVector = Icons.Filled.AccountCircle, contentDescription = stringResource(id = R.string.nav_account_title)) 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 val context = LocalContext.current
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.action_sign_in)) }, text = { Text(text = stringResource(id = R.string.action_sign_in)) },
onClick = { v4SignInPart1(context) } onClick = {
) if (preferences.useV4Api) {
v4SignInPart1(context)
} else {
showSignInDialog.value = true
} }
}
)
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.action_sign_in_as_guest)) }, text = { Text(text = stringResource(id = R.string.action_sign_in_as_guest)) },
@@ -414,7 +489,7 @@ private fun v4SignInPart2(lastSelectedOption: MutableState<String>) {
@Composable @Composable
private fun GuestSessionIcon() { private fun GuestSessionIcon() {
val guestName = stringResource(id = R.string.account_name_guest) 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 @Composable
@@ -435,7 +510,7 @@ private fun AuthorizedSessionIcon() {
Box(modifier = Modifier.padding(start = 12.dp)) { Box(modifier = Modifier.padding(start = 12.dp)) {
if (accountDetails == null || avatarUrl == null) { if (accountDetails == null || avatarUrl == null) {
val accLetter = (accountDetails?.name?.ifEmpty { accountDetails.username } ?: " ")[0] 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 { } else {
Box(modifier = Modifier.size(60.dp)) { Box(modifier = Modifier.size(60.dp)) {
AsyncImage( AsyncImage(

View File

@@ -75,7 +75,7 @@ fun MediaDetailView(
val decayAnimationSpec = rememberSplineBasedDecay<Float>() val decayAnimationSpec = rememberSplineBasedDecay<Float>()
val topAppBarScrollState = rememberTopAppBarScrollState() val topAppBarScrollState = rememberTopAppBarScrollState()
val scrollBehavior = remember(decayAnimationSpec) { val scrollBehavior = remember(decayAnimationSpec) {
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(decayAnimationSpec, topAppBarScrollState) TopAppBarDefaults.pinnedScrollBehavior(topAppBarScrollState)
} }
Scaffold( Scaffold(
@@ -84,7 +84,7 @@ fun MediaDetailView(
SmallTopAppBar( SmallTopAppBar(
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults colors = TopAppBarDefaults
.largeTopAppBarColors( .smallTopAppBarColors(
scrolledContainerColor = MaterialTheme.colorScheme.background, scrolledContainerColor = MaterialTheme.colorScheme.background,
titleContentColor = MaterialTheme.colorScheme.primary titleContentColor = MaterialTheme.colorScheme.primary
), ),
@@ -902,8 +902,15 @@ private fun ReviewsCard(
textColor = MaterialTheme.colorScheme.onSecondary textColor = MaterialTheme.colorScheme.onSecondary
) )
val context = LocalContext.current
CircleBackgroundColorImage( CircleBackgroundColorImage(
modifier = Modifier.align(Alignment.CenterVertically), modifier = Modifier
.align(Alignment.CenterVertically)
.clickable (
onClick = {
Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show()
}
),
size = 40.dp, size = 40.dp,
backgroundColor = MaterialTheme.colorScheme.tertiary, backgroundColor = MaterialTheme.colorScheme.tertiary,
image = Icons.Filled.Send, image = Icons.Filled.Send,

View File

@@ -51,7 +51,6 @@ object SessionManager: KoinComponent {
_currentSession = null _currentSession = null
preferences.guestSessionId = "" preferences.guestSessionId = ""
preferences.authorizedSessionValues = null preferences.authorizedSessionValues = null
// preferences.authorizedSessionId = ""
} }
onResponse(deleteResponse.isSuccessful) onResponse(deleteResponse.isSuccessful)
} }
@@ -68,7 +67,6 @@ object SessionManager: KoinComponent {
_currentSession = null _currentSession = null
preferences.guestSessionId = "" preferences.guestSessionId = ""
preferences.authorizedSessionValues = null preferences.authorizedSessionValues = null
// preferences.authorizedSessionId = ""
} }
onResponse(deleteResponse.isSuccessful) onResponse(deleteResponse.isSuccessful)
} }
@@ -314,14 +312,10 @@ object SessionManager: KoinComponent {
if (changed.contains(Changed.AccountDetails)) { if (changed.contains(Changed.AccountDetails)) {
service.getAccountDetails().apply { service.getAccountDetails().apply {
if (isSuccessful) { if (isSuccessful) {
// withContext(Dispatchers.Main) {
_accountDetails = body() ?: _accountDetails _accountDetails = body() ?: _accountDetails
accountDetails?.let { accountDetails?.let {
// CoroutineScope(Dispatchers.IO).launch {
refreshWithAccountId(it.id, changed) refreshWithAccountId(it.id, changed)
// }
} }
// }
} }
} }
} else if (accountDetails != null) { } else if (accountDetails != null) {