move account and sign in/out options to overlay menu

This commit is contained in:
Owen LeJeune
2023-06-05 10:59:42 -04:00
parent 1aee028cb7
commit 88f6932140
15 changed files with 572 additions and 363 deletions

View File

@@ -1,20 +1,18 @@
package com.owenlejeune.tvtime
import android.os.Bundle
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Scaffold
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
@@ -23,7 +21,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
@@ -31,11 +28,17 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import androidx.navigation.navDeepLink
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.kieronquinn.monetcompat.app.MonetCompatActivity
import com.owenlejeune.tvtime.extensions.WindowSizeClass
import com.owenlejeune.tvtime.extensions.navigateInBottomBar
import com.owenlejeune.tvtime.extensions.rememberWindowSizeClass
import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.components.AccountIcon
import com.owenlejeune.tvtime.ui.components.ProfileMenuContainer
import com.owenlejeune.tvtime.ui.components.ProfileMenuDefaults
import com.owenlejeune.tvtime.ui.components.ProfileMenuOverlay
import com.owenlejeune.tvtime.ui.navigation.BottomNavItem
import com.owenlejeune.tvtime.ui.navigation.MainNavGraph
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
@@ -89,8 +92,6 @@ class MainActivity : MonetCompatActivity() {
preferences: AppPreferences = get(AppPreferences::class.java)
) {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
val topAppBarScrollState = rememberTopAppBarScrollState()
@@ -102,38 +103,58 @@ class MainActivity : MonetCompatActivity() {
val appBarActions = remember { mutableStateOf<@Composable RowScope.() -> Unit>({}) }
val fab = remember { mutableStateOf<@Composable () -> Unit>({}) }
Scaffold (
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
if (windowSize != WindowSizeClass.Expanded) {
TopBar(
val showProfileMenuOverlay = remember { mutableStateOf(false) }
val navigationIcon = @Composable {
AccountIcon(
modifier = Modifier.padding(horizontal = 12.dp),
size = 32.dp,
onClick = { showProfileMenuOverlay.value = true }
)
}
val defaultNavBarColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.08f).compositeOver(background = MaterialTheme.colorScheme.surface)
ProfileMenuContainer(
appNavController = appNavController,
visible = showProfileMenuOverlay.value,
onDismissRequest = { showProfileMenuOverlay.value = false },
colors = ProfileMenuDefaults.systemBarColors(navBarColor = defaultNavBarColor)
) {
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
if (windowSize != WindowSizeClass.Expanded) {
TopBar(
appNavController = appNavController,
title = appBarTitle.value,
scrollBehavior = scrollBehavior,
appBarActions = appBarActions,
navigationIcon = navigationIcon
)
}
},
floatingActionButton = {
fab.value()
},
bottomBar = {
if (windowSize != WindowSizeClass.Expanded) {
BottomNavBar(navController = navController)
}
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
MainContent(
windowSize = windowSize,
appNavController = appNavController,
title = appBarTitle.value,
scrollBehavior = scrollBehavior,
appBarActions = appBarActions
navController = navController,
fab = fab,
appBarTitle = appBarTitle,
appBarActions = appBarActions,
topBarScrollBehaviour = scrollBehavior,
mainNavStartRoute = mainNavStartRoute,
navigationIcon = navigationIcon
)
}
},
floatingActionButton = {
fab.value()
},
bottomBar = {
if (windowSize != WindowSizeClass.Expanded) {
BottomNavBar(navController = navController)
}
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
MainContent(
windowSize = windowSize,
appNavController = appNavController,
navController = navController,
fab = fab,
appBarTitle = appBarTitle,
appBarActions = appBarActions,
topBarScrollBehaviour = scrollBehavior,
mainNavStartRoute = mainNavStartRoute
)
}
}
}
@@ -143,17 +164,9 @@ class MainActivity : MonetCompatActivity() {
appNavController: NavHostController,
title: @Composable () -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({})
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}),
navigationIcon: @Composable () -> Unit = {}
) {
val defaultAppBarActions: @Composable RowScope.() -> Unit = {
IconButton(
onClick = {
appNavController.navigate(MainNavItem.SettingsView.route)
}
) {
Icon(imageVector = Icons.Filled.Settings, contentDescription = stringResource(id = R.string.nav_settings_title))
}
}
LargeTopAppBar(
title = title,
scrollBehavior = scrollBehavior,
@@ -162,9 +175,9 @@ class MainActivity : MonetCompatActivity() {
scrolledContainerColor = MaterialTheme.colorScheme.background
),
actions = {
defaultAppBarActions()
appBarActions.value(this)
}
},
navigationIcon = navigationIcon
)
}
@@ -208,6 +221,7 @@ class MainActivity : MonetCompatActivity() {
topBarScrollBehaviour: TopAppBarScrollBehavior,
appBarTitle: MutableState<@Composable () -> Unit>,
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}),
navigationIcon: @Composable () -> Unit = {},
mainNavStartRoute: String = BottomNavItem.SortedItems[0].route
) {
if (windowSize == WindowSizeClass.Expanded) {
@@ -218,7 +232,8 @@ class MainActivity : MonetCompatActivity() {
appBarTitle = appBarTitle,
appBarActions = appBarActions,
topBarScrollBehaviour = topBarScrollBehaviour,
mainNavStartRoute = mainNavStartRoute
mainNavStartRoute = mainNavStartRoute,
navigationIcon = navigationIcon
)
} else {
SingleColumnMainContent(
@@ -259,6 +274,7 @@ class MainActivity : MonetCompatActivity() {
topBarScrollBehaviour: TopAppBarScrollBehavior,
appBarTitle: MutableState<@Composable () -> Unit>,
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}),
navigationIcon: @Composable () -> Unit = {},
mainNavStartRoute: String = BottomNavItem.SortedItems[0].route,
preferences: AppPreferences = get(AppPreferences::class.java)
) {
@@ -291,7 +307,8 @@ class MainActivity : MonetCompatActivity() {
appNavController = appNavController,
title = appBarTitle.value,
scrollBehavior = topBarScrollBehaviour,
appBarActions = appBarActions
appBarActions = appBarActions,
navigationIcon = navigationIcon
)
MainMediaView(
appNavController = appNavController,
@@ -435,6 +452,18 @@ class MainActivity : MonetCompatActivity() {
WebLinkView(url = url, appNavController = appNavController)
}
}
composable(
route = MainNavItem.AccountView.route,
deepLinks = listOf(
navDeepLink { uriPattern = "app://tvtime.auth.{${NavConstants.ACCOUNT_KEY}}" }
)
) {
val deepLink = it.arguments?.getString(NavConstants.ACCOUNT_KEY)
AccountView(
appNavController = appNavController,
doSignInPartTwo = deepLink == NavConstants.AUTH_REDIRECT_PAGE
)
}
}
}

View File

@@ -1,141 +1,282 @@
package com.owenlejeune.tvtime.ui.components
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.DropdownMenu
import androidx.compose.material3.Divider
import androidx.compose.material3.IconButton
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Login
import androidx.compose.material.icons.outlined.Logout
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material.icons.outlined.PersonAdd
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
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.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
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.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.owenlejeune.tvtime.BuildConfig
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
import com.owenlejeune.tvtime.utils.SessionManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
private const val ALPHA = 0.7f
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TopAppBarDropdownMenu(
icon: @Composable () -> Unit = {},
content: @Composable ColumnScope.(expanded: MutableState<Boolean>) -> Unit = {}
fun ProfileMenuOverlay(
appNavController: NavController,
visible: Boolean,
onDismissRequest: () -> Unit
) {
val expanded = remember { mutableStateOf(false) }
Box(
modifier = Modifier.wrapContentSize(Alignment.TopEnd)
) {
IconButton(
onClick = {
expanded.value = true
}
) {
icon()
}
}
DropdownMenu(
modifier = Modifier.background(color = MaterialTheme.colorScheme.background),
expanded = expanded.value,
onDismissRequest = { expanded.value = false }
) {
content(this, expanded)
}
}
@Composable
fun CustomTopAppBarDropdownMenu(
icon: @Composable () -> Unit = {},
content: @Composable ColumnScope.(expanded: MutableState<Boolean>) -> Unit = {}
) {
val expanded = remember { mutableStateOf(false) }
Box(modifier = Modifier.wrapContentSize(Alignment.TopEnd).padding(end = 12.dp)) {
IconButton(onClick = { expanded.value = true }) {
icon()
}
}
DropdownMenu(
expanded = expanded.value,
onDismissRequest = { expanded.value = false},
modifier = Modifier
.background(color = MaterialTheme.colorScheme.background)
.shadow(elevation = 0.dp),
offset = DpOffset(16.dp, 0.dp)
) {
content(this, expanded)
}
}
@Composable
fun CustomMenuItem(
text: String,
onClick: () -> Unit
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(30.dp))
.fillMaxWidth()
.background(color = MaterialTheme.colorScheme.primary)
) {
Text(
text = text,
color = MaterialTheme.colorScheme.background,
if (visible) {
val context = LocalContext.current
Box(
modifier = Modifier
.padding(horizontal = 15.dp, vertical = 10.dp)
.clickable(onClick = onClick)
.fillMaxWidth(),
textAlign = TextAlign.Center
)
}
}
@Composable
fun CustomMenuDivider() {
Divider(color = Color.Transparent, modifier = Modifier.padding(vertical = 2.dp))
}
@Composable
fun TopAppBarDialogMenu(
icon: @Composable () -> Unit = {},
content: @Composable (showing: MutableState<Boolean>) -> Unit = {}
) {
val expanded = remember { mutableStateOf(false) }
Box(
modifier = Modifier.wrapContentSize(Alignment.TopEnd)
) {
IconButton(
onClick = {
expanded.value = true
}
.fillMaxSize()
.background(color = Color.Black.copy(alpha = ALPHA))
.clickable(onClick = onDismissRequest)
) {
icon()
Card(
modifier = Modifier
.align(Alignment.TopCenter)
.padding(vertical = 100.dp, horizontal = 12.dp)
.fillMaxWidth()
.wrapContentHeight(),
colors = CardDefaults.cardColors(
containerColor = Color.Black.copy(alpha = 0.6f).compositeOver(MaterialTheme.colorScheme.primaryContainer),
contentColor = Color.White.copy(alpha = 0.8f).compositeOver(MaterialTheme.colorScheme.onPrimaryContainer)
)
) {
Column(
modifier = Modifier.padding(vertical = 12.dp)
) {
val currentSessionState = remember { SessionManager.currentSession }
val currentSession = currentSessionState.value
currentSession?.let {
ProfileMenuItem {
AccountIcon(
size = 48.dp,
enabled = false
)
Text(
text = currentSession.accountDetails.value?.name ?: "",
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
}
MenuDivider()
ProfileMenuItem(
onClick = {
Toast.makeText(context, "Under construction", Toast.LENGTH_SHORT).show()
}
) {
Icon(
imageVector = Icons.Outlined.PersonAdd,
contentDescription = null
)
Text(
text = stringResource(id = R.string.account_add),
fontSize = 16.sp
)
}
} ?: run {
ProfileMenuItem {
Text(
text = stringResource(id = R.string.account_not_logged_in),
fontSize = 16.sp
)
}
}
MenuDivider()
currentSession?.let {
ProfileMenuItem(
onClick = {
onDismissRequest()
appNavController.navigate(MainNavItem.AccountView.route)
}
) {
Icon(
imageVector = Icons.Outlined.Person,
contentDescription = null
)
Text(
text = stringResource(id = R.string.nav_account_title),
fontSize = 16.sp
)
}
}
ProfileMenuItem(
onClick = {
onDismissRequest()
appNavController.navigate(MainNavItem.SettingsView.route)
}
) {
Icon(
imageVector = Icons.Outlined.Settings,
contentDescription = null
)
Text(
text = stringResource(id = R.string.nav_settings_title),
fontSize = 16.sp
)
}
MenuDivider()
ProfileMenuItem(
onClick = {
onDismissRequest()
CoroutineScope(Dispatchers.IO).launch {
if (currentSession != null) {
SessionManager.clearSession()
} else {
SessionManager.signInPart1(context) {
appNavController.navigate(
MainNavItem.WebLinkView.route.plus("/$it")
)
}
}
}
}
) {
if (currentSession != null) {
Icon(
imageVector = Icons.Outlined.Logout,
contentDescription = null
)
Text(
text = stringResource(id = R.string.action_sign_out),
fontSize = 16.sp
)
} else {
Icon(
imageVector = Icons.Outlined.Login,
contentDescription = null
)
Text(
text = stringResource(id = R.string.action_sign_in),
fontSize = 16.sp
)
}
}
MenuDivider()
Text(
text = "${stringResource(id = R.string.app_name)} v${BuildConfig.VERSION_NAME}",
fontSize = 10.sp,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
}
if (expanded.value) {
Dialog(
onDismissRequest = { expanded.value = false },
content = { content(expanded) }
@Composable
private fun MenuDivider() {
MyDivider(modifier = Modifier.padding(vertical = 9.dp))
}
@Composable
private fun ProfileMenuItem(
onClick: (() -> Unit)? = null,
content: @Composable RowScope.() -> Unit
) {
Box(
modifier = Modifier
.clickable(
enabled = onClick != null,
onClick = onClick ?: {}
)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(18.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(horizontal = 24.dp, vertical = 9.dp)
.fillMaxWidth(),
content = content
)
}
}
class ProfileMenuColors internal constructor(val statusBarColor: Color, val navBarColor: Color)
object ProfileMenuDefaults {
@Composable
fun systemBarColors(
statusBarColor: Color = MaterialTheme.colorScheme.background,
navBarColor: Color = MaterialTheme.colorScheme.background
) = ProfileMenuColors(statusBarColor, navBarColor)
}
@Composable
fun ProfileMenuContainer(
colors: ProfileMenuColors = ProfileMenuDefaults.systemBarColors(),
appNavController: NavController,
visible: Boolean,
onDismissRequest: () -> Unit,
content: @Composable BoxScope.() -> Unit
) {
val statusBarColor = if (visible) {
Color.Black.copy(alpha = ALPHA).compositeOver(background = colors.statusBarColor)
} else {
colors.statusBarColor
}
val navBarColor = if (visible) {
Color.Black.copy(alpha = ALPHA).compositeOver(background = colors.navBarColor)
} else {
colors.navBarColor
}
val systemUiController = rememberSystemUiController()
systemUiController.setStatusBarColor(statusBarColor)
systemUiController.setNavigationBarColor(navBarColor)
Box {
content()
ProfileMenuOverlay(
visible = visible,
onDismissRequest = onDismissRequest,
appNavController = appNavController
)
// AlertDialog(
// modifier = Modifier
// .fillMaxWidth()
// .wrapContentHeight(),
// backgroundColor = MaterialTheme.colorScheme.background,
// onDismissRequest = { expanded.value = false },
// text = { content(expanded) },
// buttons = {}
// )
}
}

View File

@@ -247,13 +247,16 @@ fun PosterItem(
colors = CardDefaults.cardColors(containerColor = Color.Transparent)
) {
var backgroundColor by remember { mutableStateOf(Color.Gray) }
val m = if (backgroundColor == Color.Transparent) {
Modifier.wrapContentHeight()
} else {
Modifier.height(POSTER_HEIGHT)
}
Box(
modifier = Modifier
modifier = m
.width(width = width)
.height(height = POSTER_HEIGHT)
.background(color = backgroundColor)
.clip(RoundedCornerShape(5.dp))
.onGloballyPositioned { sizeImage = it.size }
) {
var bgIcon by remember { mutableStateOf(placeholder) }
Icon(
@@ -286,7 +289,7 @@ fun PosterItem(
},
model = url,
contentDescription = title,
contentScale = ContentScale.FillWidth,
contentScale = ContentScale.FillBounds,
onSuccess = { backgroundColor = Color.Transparent }
)

View File

@@ -17,12 +17,14 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.ArrowDropUp
import androidx.compose.material.icons.filled.Error
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.outlined.AccountCircle
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@@ -65,9 +67,11 @@ import coil.compose.rememberAsyncImagePainter
import com.google.accompanist.flowlayout.FlowRow
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AuthorDetails
import com.owenlejeune.tvtime.extensions.unlessEmpty
import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
import com.owenlejeune.tvtime.utils.SessionManager
import com.owenlejeune.tvtime.utils.TmdbUtils
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -696,34 +700,90 @@ fun AvatarImage(
contentDescription = ""
)
} else {
val text = if (author.name.isNotEmpty()) author.name[0] else author.username[0]
RoundedLetterImage(
size = size,
character = text
val name = author.name.unlessEmpty(author.username)
UserInitials(size = size, name = name)
}
}
@Composable
fun UserInitials(
size: Dp,
name: String,
fontSize: TextUnit = 16.sp
) {
val sanitizedName = name.replace("\\s+".toRegex(), " ")
val userName = if(sanitizedName.contains(" ")) {
sanitizedName.split(" ")[0][0].toString() + sanitizedName.split(" ")[1][0].toString()
} else {
if (sanitizedName.length < 3) name else { sanitizedName.substring(0, 2) }
}
Box(
modifier = Modifier
.clip(CircleShape)
.size(size)
.background(color = MaterialTheme.colorScheme.secondary)
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = userName.uppercase(),
textAlign = TextAlign.Center,
style = TextStyle(
color = MaterialTheme.colorScheme.background,
fontSize = fontSize
)
)
}
}
@Composable
fun RoundedLetterImage(
size: Dp,
character: Char,
modifier: Modifier = Modifier
fun AccountIcon(
modifier: Modifier = Modifier,
size: Dp = 60.dp,
onClick: () -> Unit = {},
enabled: Boolean = true
) {
Box(
modifier = modifier
.clip(CircleShape)
.size(size)
.background(color = MaterialTheme.colorScheme.tertiary)
) {
Text(
modifier = Modifier
.align(Alignment.Center),
text = character.uppercase(),
color = MaterialTheme.colorScheme.onTertiary,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge
val accountDetails = SessionManager.currentSession.value?.accountDetails?.value
val avatarUrl = accountDetails?.let {
when {
accountDetails.avatar.tmdb?.avatarPath?.isNotEmpty() == true -> {
TmdbUtils.getAccountAvatarUrl(accountDetails)
}
accountDetails.avatar.gravatar?.isDefault() == false -> {
TmdbUtils.getAccountGravatarUrl(accountDetails)
}
else -> null
}
}
Box(modifier = modifier
.clip(CircleShape)
.clickable(
enabled = enabled,
onClick = onClick
)
) {
if (accountDetails == null) {
Icon(
imageVector = Icons.Outlined.AccountCircle,
contentDescription = null,
modifier = Modifier.size(size),
tint = MaterialTheme.colorScheme.secondary
)
} else if (avatarUrl == null) {
val name = accountDetails.name.ifEmpty { accountDetails.username }
UserInitials(size = size, name = name)
} else {
Box(modifier = Modifier.size(size)) {
AsyncImage(
model = avatarUrl,
contentDescription = "",
modifier = Modifier
.size(60.dp)
.clip(CircleShape),
contentScale = ContentScale.Fit
)
}
}
}
}
@@ -1035,4 +1095,9 @@ fun SearchBar(
)
}
)
}
@Composable
fun MyDivider(modifier: Modifier = Modifier) {
Divider(thickness = 0.5.dp, modifier = modifier, color = MaterialTheme.colorScheme.secondaryContainer)
}

View File

@@ -3,6 +3,7 @@ package com.owenlejeune.tvtime.ui.navigation
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.utils.ResourceUtils
import com.owenlejeune.tvtime.utils.SessionManager
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -41,9 +42,39 @@ sealed class BottomNavItem(
}
}
object Movies: BottomNavItem(R.string.nav_movies_title, R.drawable.ic_movie, "movies_route", { it.moviesTabPosition }, { p, i -> p.moviesTabPosition = i } )
object TV: BottomNavItem(R.string.nav_tv_title, R.drawable.ic_tv, "tv_route", { it.tvTabPosition }, { p, i -> p.tvTabPosition = i } )
object Account: BottomNavItem(R.string.nav_account_title, R.drawable.ic_person, "account_route", { it.accountTabPosition }, { p, i -> p.accountTabPosition = i } )
object People: BottomNavItem(R.string.nav_people_title, R.drawable.ic_face, "people_route", { it.peopleTabPosition }, { p, i -> p.peopleTabPosition = i } )
object Movies: BottomNavItem(
R.string.nav_movies_title,
R.drawable.ic_movie,
"movies_route",
{ it.moviesTabPosition },
{ p, i -> p.moviesTabPosition = i }
)
object TV: BottomNavItem(
R.string.nav_tv_title,
R.drawable.ic_tv,
"tv_route",
{ it.tvTabPosition },
{ p, i -> p.tvTabPosition = i }
)
object Account: BottomNavItem(
R.string.nav_account_title,
R.drawable.ic_person,
"account_route",
{
// if (SessionManager.currentSession.value?.isAuthorized == true) {
// it.accountTabPosition
// } else {
-2
// }
},
{ p, i -> p.accountTabPosition = i }
)
object People: BottomNavItem(
R.string.nav_people_title,
R.drawable.ic_face,
"people_route",
{ it.peopleTabPosition },
{ p, i -> p.peopleTabPosition = i }
)
}

View File

@@ -12,4 +12,6 @@ sealed class MainNavItem(val route: String) {
object SearchView: MainNavItem("search_route")
object WebLinkView: MainNavItem("web_link_route")
object AccountView: MainNavItem("account_route")
}

View File

@@ -1,16 +1,13 @@
package com.owenlejeune.tvtime.ui.navigation
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.RowScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import androidx.navigation.navDeepLink
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.*
@@ -45,19 +42,8 @@ fun MainNavGraph(
fab = fab
)
}
composable(
route = BottomNavItem.Account.route,
deepLinks = listOf(
navDeepLink { uriPattern = "app://tvtime.auth.{${NavConstants.ACCOUNT_KEY}}" }
)
) {
val deepLink = it.arguments?.getString(NavConstants.ACCOUNT_KEY)
AccountTab(
appBarTitle = appBarTitle,
appNavController = appNavController,
appBarActions = appBarActions,
doSignInPartTwo = deepLink == NavConstants.AUTH_REDIRECT_PAGE
)
composable(route = BottomNavItem.Account.route) {
AccountViewContent(appNavController = appNavController)
fab.value = {}
}
composable(BottomNavItem.People.route) {

View File

@@ -1,29 +1,25 @@
package com.owenlejeune.tvtime.ui.screens.main
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import coil.compose.AsyncImage
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.PagerState
@@ -31,63 +27,104 @@ import com.google.accompanist.pager.rememberPagerState
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.V4AccountList
import com.owenlejeune.tvtime.api.tmdb.viewmodel.RecommendedMediaViewModel
import com.owenlejeune.tvtime.extensions.unlessEmpty
import com.owenlejeune.tvtime.ui.components.AccountIcon
import com.owenlejeune.tvtime.ui.components.PagingPosterGrid
import com.owenlejeune.tvtime.ui.components.RoundedLetterImage
import com.owenlejeune.tvtime.ui.components.ProfileMenuContainer
import com.owenlejeune.tvtime.ui.components.ProfileMenuOverlay
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.main.tabs.top.ScrollableTabs
import com.owenlejeune.tvtime.api.tmdb.viewmodel.RecommendedMediaViewModel
import com.owenlejeune.tvtime.utils.SessionManager
import com.owenlejeune.tvtime.utils.TmdbUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.reflect.KClass
@OptIn(ExperimentalPagerApi::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AccountTab(
fun AccountView(
appNavController: NavHostController,
appBarTitle: MutableState<@Composable () -> Unit>,
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}),
doSignInPartTwo: Boolean = false
) {
val currentSessionState = remember { SessionManager.currentSession }
val currentSession = currentSessionState.value
val showProfileMenuOverlay = remember { mutableStateOf(false) }
ProfileMenuContainer(
appNavController = appNavController,
visible = showProfileMenuOverlay.value,
onDismissRequest = { showProfileMenuOverlay.value = false }
) {
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
val topAppBarScrollState = rememberTopAppBarScrollState()
val scrollBehavior = remember(decayAnimationSpec) {
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(decayAnimationSpec, topAppBarScrollState)
}
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
LargeTopAppBar(
scrollBehavior = scrollBehavior,
navigationIcon = {
IconButton(
onClick = { appNavController.popBackStack() }
) {
Icon(
Icons.Filled.ArrowBack,
contentDescription = null
)
}
},
title = {
if (currentSession?.isAuthorized == false) {
Text(text = stringResource(id = R.string.account_not_logged_in))
} else {
val accountDetails = remember { currentSession!!.accountDetails }
Text(text = getAccountName(accountDetails.value))
}
},
colors = TopAppBarDefaults.largeTopAppBarColors(scrolledContainerColor = MaterialTheme.colorScheme.background),
actions = {
AccountIcon(
modifier = Modifier.padding(start = 12.dp),
size = 32.dp,
onClick = { showProfileMenuOverlay.value = true }
)
Spacer(modifier = Modifier.width(8.dp))
}
)
}
) {
Box(modifier = Modifier.padding(it)) {
AccountViewContent(appNavController = appNavController, doSignInPartTwo = doSignInPartTwo)
}
}
}
}
@OptIn(ExperimentalPagerApi::class)
@Composable
fun AccountViewContent(
appNavController: NavHostController,
doSignInPartTwo: Boolean = false
) {
val currentSessionState = remember { SessionManager.currentSession }
val currentSession = currentSessionState.value
val scope = rememberCoroutineScope()
if (currentSession?.isAuthorized == false) {
appBarTitle.value = { Text(text = stringResource(id = R.string.account_not_logged_in)) }
if (doSignInPartTwo) {
AccountLoadingView()
LaunchedEffect(Unit) {
scope.launch {
SessionManager.singInPart2()
}
if (currentSession?.isAuthorized != true && doSignInPartTwo) {
AccountLoadingView()
LaunchedEffect(Unit) {
scope.launch {
SessionManager.signInPart2()
}
}
} else {
if (currentSession?.isAuthorized == true) {
val accountDetails = remember { currentSession.accountDetails }
appBarTitle.value = { Text(text = stringResource(id = R.string.account_header_title_formatted, getAccountName(accountDetails.value))) }
} else {
appBarTitle.value = { Text(text = stringResource(id = R.string.account_not_logged_in)) }
}
appBarActions.value = {
AccountDropdownMenu(
session = currentSession,
appNavController = appNavController
)
}
currentSession?.let {
Column {
AuthorizedSessionIcon()
val tabs = AccountTabNavItem.AuthorizedItems
val pagerState = rememberPagerState()
ScrollableTabs(tabs = tabs, pagerState = pagerState)
@@ -288,95 +325,6 @@ private fun MediaItemRow(
)
}
@Composable
private fun AccountDropdownMenu(session: SessionManager.Session?, appNavController: NavHostController) {
val expanded = remember { mutableStateOf(false) }
IconButton(
onClick = { expanded.value = true }
) {
Icon(imageVector = Icons.Filled.AccountCircle, contentDescription = stringResource(id = R.string.nav_account_title))
}
DropdownMenu(
expanded = expanded.value,
onDismissRequest = { expanded.value = false }
) {
if(session?.isAuthorized == true) {
AuthorizedSessionMenuItems(expanded = expanded)
} else {
NoSessionMenuItems(expanded = expanded, appNavController = appNavController)
}
}
}
@Composable
private fun AuthorizedSessionMenuItems(expanded: MutableState<Boolean>) {
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.action_sign_out)) },
onClick = {
signOut()
expanded.value = false
}
)
}
@Composable
private fun NoSessionMenuItems(expanded: MutableState<Boolean>, appNavController: NavHostController) {
val context = LocalContext.current
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.action_sign_in)) },
onClick = {
CoroutineScope(Dispatchers.IO).launch {
SessionManager.signInPart1(context) {
appNavController.navigate(MainNavItem.WebLinkView.route.plus("/$it"))
}
}
expanded.value = false
}
)
}
@Composable
private fun AuthorizedSessionIcon() {
val accountDetails = SessionManager.currentSession.value?.accountDetails?.value
val avatarUrl = accountDetails?.let {
when {
accountDetails.avatar.tmdb?.avatarPath?.isNotEmpty() == true -> {
TmdbUtils.getAccountAvatarUrl(accountDetails)
}
accountDetails.avatar.gravatar?.isDefault() == false -> {
TmdbUtils.getAccountGravatarUrl(accountDetails)
}
else -> null
}
}
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)
} else {
Box(modifier = Modifier.size(60.dp)) {
AsyncImage(
model = avatarUrl,
contentDescription = "",
modifier = Modifier
.size(60.dp)
.clip(CircleShape),
contentScale = ContentScale.Fit
)
}
}
}
}
private fun signOut() {
CoroutineScope(Dispatchers.IO).launch {
SessionManager.clearSession()
}
}
@OptIn(ExperimentalPagerApi::class)
@Composable

View File

@@ -2,7 +2,7 @@ package com.owenlejeune.tvtime.ui.screens.main
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
@@ -22,7 +22,7 @@ fun PeopleTab(
appNavController: NavHostController,
fab: MutableState<@Composable () -> Unit>
) {
val titleText = stringResource(id = R.string.nav_people_title)
val titleText = stringResource(id = R.string.popular_today_header)
appBarTitle.value = { Text(text = titleText) }
Column {
@@ -38,10 +38,10 @@ fun PeopleTab(
PagingPeoplePosterGrid(
lazyPagingItems = peopleList,
header = {
Text(
text = stringResource(R.string.popular_today_header),
modifier = Modifier.padding(start = 8.dp)
)
// Text(
// text = stringResource(R.string.popular_today_header),
// modifier = Modifier.padding(start = 8.dp)
// )
},
onClick = { id ->
appNavController.navigate(

View File

@@ -382,7 +382,7 @@ private fun HomeScreenPreferences(
SwitchPreference(
titleText = stringResource(R.string.preference_show_poster_titles_title),
subtitleText = stringResource(R.string.preference_show_poster_titles_subtitle),
checkState = showTabLabels.value,
checkState = showPosterTitles.value,
onCheckedChange = { isChecked ->
showPosterTitles.value = isChecked
preferences.showPosterTitles = isChecked

View File

@@ -22,6 +22,7 @@ import androidx.navigation.NavController
import com.google.accompanist.web.AccompanistWebViewClient
import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewState
import com.owenlejeune.tvtime.utils.SessionManager
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -37,7 +38,12 @@ fun WebLinkView(
title = {},
navigationIcon = {
IconButton(
onClick = { appNavController.popBackStack() }
onClick = {
if (url.contains("auth")) {
SessionManager.cancelSignIn()
}
appNavController.popBackStack()
}
) {
Icon(
imageVector = Icons.Filled.Close,
@@ -47,8 +53,8 @@ fun WebLinkView(
}
)
}
) {
Box(modifier = Modifier.padding(it)) {
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
val webViewState = rememberWebViewState(url = url)
WebView(
state = webViewState,

View File

@@ -115,13 +115,7 @@ fun TVTimeTheme(
content = {
val systemUiController = rememberSystemUiController()
systemUiController.setStatusBarColor(color = androidx.compose.material3.MaterialTheme.colorScheme.background)
systemUiController.setNavigationBarColor(
color = androidx.compose.material3.MaterialTheme.colorScheme.primary.copy(
alpha = 0.08f
).compositeOver(
background = androidx.compose.material3.MaterialTheme.colorScheme.surface
)
)
systemUiController.setNavigationBarColor(color = androidx.compose.material3.MaterialTheme.colorScheme.background)
content()
}

View File

@@ -33,7 +33,7 @@ class HomeTabRecyclerAdapter: RecyclerView.Adapter<HomeTabRecyclerAdapter.TabVie
init {
val visiblePages = BottomNavItem.Items.filter { it.order > -1 }.sortedBy { it.order }
val hiddenPages = BottomNavItem.Items.filter { it.order < 0 }
val hiddenPages = BottomNavItem.Items.filter { it.order == -1 }
pages = ArrayList<BottomNavItem?>().apply {
addAll(visiblePages)
add(null)

View File

@@ -1,8 +1,6 @@
package com.owenlejeune.tvtime.utils
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
@@ -18,7 +16,6 @@ import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AuthDeleteBody
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AuthRequestBody
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.V4AccountList
import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.navigation.AccountTabNavItem
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -59,6 +56,12 @@ object SessionManager: KoinComponent {
}
}
fun cancelSignIn() {
if (currentSession.value is InProgressSession) {
currentSession.value = null
}
}
suspend fun initialize() {
preferences.authorizedSessionValues?.let { values ->
val session = AuthorizedSession(
@@ -79,17 +82,17 @@ object SessionManager: KoinComponent {
val requestTokenResponse = service.createRequestToken(AuthRequestBody(redirect = "app://tvtime.auth.return"))
if (requestTokenResponse.isSuccessful) {
requestTokenResponse.body()?.let { ctr ->
currentSession.value = InProgressSession(ctr.requestToken)
val url = context.getString(R.string.tmdb_auth_url, ctr.requestToken)
val encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8.toString())
withContext(Dispatchers.Main) {
onRedirect(encodedUrl)
}
currentSession.value = InProgressSession(ctr.requestToken)
}
}
}
suspend fun singInPart2(
suspend fun signInPart2(
context: Context = get(Context::class.java)
) {
if (currentSession.value is InProgressSession) {

View File

@@ -114,6 +114,7 @@
<string name="account_header_title_formatted">Hello, %1$s!</string>
<string name="account_name_guest">Guest</string>
<string name="account_not_logged_in">Not logged in</string>
<string name="account_add">Add Account</string>
<string name="no_rated_content_message">No rated content</string>
<string name="rating_test">Rating: %1$d</string>