From d7b333e51962a6035bbb1c03273aa9048fe763a8 Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:42:26 -0500 Subject: [PATCH] Adjust color & size of the nav drawer (#228) The navigation drawer now appear more subtle when closed using a neutral background and dimmer, slightly more compact icons. When open, it looks much like it did before. The profile button stays fixed at the top since it is the only indication for which user & server. Closes #176 Closed drawer_closed Large Open drawer_open Large --- .../damontecres/wholphin/ui/main/HomePage.kt | 3 +- .../damontecres/wholphin/ui/nav/NavDrawer.kt | 346 +++++++++++------- 2 files changed, 210 insertions(+), 139 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index 4d7f4566..e5729620 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color @@ -290,7 +291,7 @@ fun HomePageContent( top = 0.dp, bottom = Cards.height2x3, ), - modifier = Modifier, + modifier = Modifier.focusRestorer(), ) { itemsIndexed(homeRows) { rowIndex, row -> when (val r = row) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt index e41db910..41a6b88e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt @@ -2,16 +2,20 @@ package com.github.damontecres.wholphin.ui.nav import android.content.Context import androidx.activity.compose.BackHandler +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.focusGroup import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState @@ -34,6 +38,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester @@ -51,6 +56,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.findViewTreeViewModelStoreOwner import androidx.lifecycle.viewModelScope +import androidx.tv.material3.DrawerState import androidx.tv.material3.DrawerValue import androidx.tv.material3.Icon import androidx.tv.material3.LocalContentColor @@ -214,6 +220,7 @@ fun NavDrawer( val context = LocalContext.current val drawerFocusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() } + val searchFocusRequester = remember { FocusRequester() } // If the user presses back while on the home page, open the nav drawer, another back press will quit the app BackHandler(enabled = (drawerState.currentValue == DrawerValue.Closed && destination is Destination.Home)) { @@ -285,6 +292,18 @@ fun NavDrawer( } } + val drawerWidth by animateDpAsState(if (drawerState.isOpen) 260.dp else 40.dp) + val drawerPadding by animateDpAsState(if (drawerState.isOpen) 0.dp else 8.dp) + val drawerBackground by animateColorAsState( + if (drawerState.isOpen) { + MaterialTheme.colorScheme.surfaceColorAtElevation( + 1.dp, + ) + } else { + MaterialTheme.colorScheme.surface + }, + ) + val spacedBy = 4.dp NavigationDrawer( modifier = modifier @@ -292,149 +311,175 @@ fun NavDrawer( drawerState = drawerState, drawerContent = { ProvideTextStyle(MaterialTheme.typography.labelMedium) { - LazyColumn( - state = listState, - contentPadding = PaddingValues(0.dp), + Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedByWithFooter(8.dp), + verticalArrangement = Arrangement.spacedBy(spacedBy), modifier = Modifier - .focusGroup() - .focusProperties { - onEnter = { - focusRequester.tryRequestFocus() - } - }.fillMaxHeight() - .background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)), + .fillMaxHeight() + .width(drawerWidth) + .background(drawerBackground), ) { - item { - // Even though some must be clicked, focusing on it should clear other focused items - val interactionSource = remember { MutableInteractionSource() } - val focused by interactionSource.collectIsFocusedAsState() - LaunchedEffect(focused) { if (focused) focusedIndex = Int.MIN_VALUE } - IconNavItem( - text = user?.name ?: "", - subtext = server?.name ?: server?.url, - icon = Icons.Default.AccountCircle, - selected = false, - interactionSource = interactionSource, - onClick = { - viewModel.navigationManager.navigateToFromDrawer( - Destination.UserList( - server, - ), - ) - }, - modifier = Modifier.animateItem(), - ) - } - item { - val interactionSource = remember { MutableInteractionSource() } - val focused by interactionSource.collectIsFocusedAsState() - LaunchedEffect(focused) { if (focused) focusedIndex = -2 } - IconNavItem( - text = stringResource(R.string.search), - icon = Icons.Default.Search, - selected = selectedIndex == -2, - interactionSource = interactionSource, - onClick = { - viewModel.setIndex(-2) - viewModel.navigationManager.navigateToFromDrawer(Destination.Search) - }, - modifier = - Modifier - .ifElse( - selectedIndex == -2, - Modifier.focusRequester(focusRequester), - ).animateItem(), - ) - } - item { - val interactionSource = remember { MutableInteractionSource() } - val focused by interactionSource.collectIsFocusedAsState() - LaunchedEffect(focused) { if (focused) focusedIndex = -1 } - IconNavItem( - text = stringResource(R.string.home), - icon = Icons.Default.Home, - selected = selectedIndex == -1, - interactionSource = interactionSource, - onClick = { - viewModel.setIndex(-1) - if (destination is Destination.Home) { - viewModel.navigationManager.reloadHome() - } else { - viewModel.navigationManager.goToHome() - } - }, - modifier = - Modifier - .ifElse( - selectedIndex == -1, - Modifier.focusRequester(focusRequester), - ).animateItem(), - ) - } - itemsIndexed(libraries) { index, it -> - val interactionSource = remember { MutableInteractionSource() } - val focused by interactionSource.collectIsFocusedAsState() - LaunchedEffect(focused) { if (focused) focusedIndex = index } - NavItem( - library = it, - selected = selectedIndex == index, - moreExpanded = showMore, - interactionSource = interactionSource, - onClick = { - onClick.invoke(index, it) - if (it !is NavDrawerItem.More) setShowMore(false) - }, - modifier = - Modifier - .ifElse( - selectedIndex == index, - Modifier.focusRequester(focusRequester), - ).animateItem(), - ) - } - if (showMore) { - itemsIndexed(moreLibraries) { index, it -> - val adjustedIndex = (index + libraries.size + 1) + // Even though some must be clicked, focusing on it should clear other focused items + val interactionSource = remember { MutableInteractionSource() } + val focused by interactionSource.collectIsFocusedAsState() + LaunchedEffect(focused) { if (focused) focusedIndex = Int.MIN_VALUE } + IconNavItem( + text = user?.name ?: "", + subtext = server?.name ?: server?.url, + icon = Icons.Default.AccountCircle, + selected = false, + drawerOpen = drawerState.isOpen, + interactionSource = interactionSource, + onClick = { + viewModel.navigationManager.navigateToFromDrawer( + Destination.UserList( + server, + ), + ) + }, + modifier = Modifier.padding(start = drawerPadding), + ) + LazyColumn( + state = listState, + contentPadding = PaddingValues(0.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedByWithFooter(spacedBy), + modifier = + Modifier + .focusGroup() + .focusProperties { + onEnter = { + if (requestedFocusDirection == FocusDirection.Down) { + searchFocusRequester.tryRequestFocus() + } else { + focusRequester.tryRequestFocus() + } + } + }.fillMaxHeight() + .padding(start = drawerPadding), + ) { + item { val interactionSource = remember { MutableInteractionSource() } val focused by interactionSource.collectIsFocusedAsState() - LaunchedEffect(focused) { if (focused) focusedIndex = adjustedIndex } - NavItem( - library = it, - selected = selectedIndex == adjustedIndex, - moreExpanded = showMore, - onClick = { onClick.invoke(adjustedIndex, it) }, - containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp), + LaunchedEffect(focused) { if (focused) focusedIndex = -2 } + IconNavItem( + text = stringResource(R.string.search), + icon = Icons.Default.Search, + selected = selectedIndex == -2, + drawerOpen = drawerState.isOpen, interactionSource = interactionSource, + onClick = { + viewModel.setIndex(-2) + viewModel.navigationManager.navigateToFromDrawer(Destination.Search) + }, modifier = Modifier + .focusRequester(searchFocusRequester) .ifElse( - selectedIndex == adjustedIndex, + selectedIndex == -2, Modifier.focusRequester(focusRequester), ).animateItem(), ) } - } - item { - val interactionSource = remember { MutableInteractionSource() } - val focused by interactionSource.collectIsFocusedAsState() - LaunchedEffect(focused) { if (focused) focusedIndex = Int.MIN_VALUE } - IconNavItem( - text = stringResource(R.string.settings), - icon = Icons.Default.Settings, - selected = false, - interactionSource = interactionSource, - onClick = { - viewModel.navigationManager.navigateTo( - Destination.Settings( - PreferenceScreenOption.BASIC, - ), + item { + val interactionSource = remember { MutableInteractionSource() } + val focused by interactionSource.collectIsFocusedAsState() + LaunchedEffect(focused) { if (focused) focusedIndex = -1 } + IconNavItem( + text = stringResource(R.string.home), + icon = Icons.Default.Home, + selected = selectedIndex == -1, + drawerOpen = drawerState.isOpen, + interactionSource = interactionSource, + onClick = { + viewModel.setIndex(-1) + if (destination is Destination.Home) { + viewModel.navigationManager.reloadHome() + } else { + viewModel.navigationManager.goToHome() + } + }, + modifier = + Modifier + .ifElse( + selectedIndex == -1, + Modifier.focusRequester(focusRequester), + ).animateItem(), + ) + } + itemsIndexed(libraries) { index, it -> + val interactionSource = remember { MutableInteractionSource() } + val focused by interactionSource.collectIsFocusedAsState() + LaunchedEffect(focused) { if (focused) focusedIndex = index } + NavItem( + library = it, + selected = selectedIndex == index, + moreExpanded = showMore, + drawerOpen = drawerState.isOpen, + interactionSource = interactionSource, + onClick = { + onClick.invoke(index, it) + if (it !is NavDrawerItem.More) setShowMore(false) + }, + modifier = + Modifier + .ifElse( + selectedIndex == index, + Modifier.focusRequester(focusRequester), + ).animateItem(), + ) + } + if (showMore) { + itemsIndexed(moreLibraries) { index, it -> + val adjustedIndex = (index + libraries.size + 1) + val interactionSource = remember { MutableInteractionSource() } + val focused by interactionSource.collectIsFocusedAsState() + LaunchedEffect(focused) { + if (focused) focusedIndex = adjustedIndex + } + NavItem( + library = it, + selected = selectedIndex == adjustedIndex, + moreExpanded = showMore, + drawerOpen = drawerState.isOpen, + onClick = { onClick.invoke(adjustedIndex, it) }, + containerColor = + if (drawerState.isOpen) { + MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp) + } else { + Color.Unspecified + }, + interactionSource = interactionSource, + modifier = + Modifier + .ifElse( + selectedIndex == adjustedIndex, + Modifier.focusRequester(focusRequester), + ).animateItem(), ) - }, - modifier = Modifier.animateItem(), - ) + } + } + item { + val interactionSource = remember { MutableInteractionSource() } + val focused by interactionSource.collectIsFocusedAsState() + LaunchedEffect(focused) { if (focused) focusedIndex = Int.MIN_VALUE } + IconNavItem( + text = stringResource(R.string.settings), + icon = Icons.Default.Settings, + selected = false, + drawerOpen = drawerState.isOpen, + interactionSource = interactionSource, + onClick = { + viewModel.navigationManager.navigateTo( + Destination.Settings( + PreferenceScreenOption.BASIC, + ), + ) + }, + modifier = Modifier.animateItem(), + ) + } } } } @@ -448,7 +493,9 @@ fun NavDrawer( destination = destination, preferences = preferences, deviceProfile = deviceProfile, - modifier = Modifier.fillMaxSize(), + modifier = + Modifier + .fillMaxSize(), ) if (preferences.appPreferences.interfacePreferences.showClock) { var now by remember { mutableStateOf(LocalTime.now()) } @@ -479,26 +526,23 @@ fun NavigationDrawerScope.IconNavItem( icon: ImageVector, onClick: () -> Unit, selected: Boolean, + drawerOpen: Boolean, modifier: Modifier = Modifier, subtext: String? = null, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { - val isFocused = interactionSource.collectIsFocusedAsState().value + val focused by interactionSource.collectIsFocusedAsState() NavigationDrawerItem( modifier = modifier, selected = false, onClick = onClick, leadingContent = { - val color = - when { - selected -> MaterialTheme.colorScheme.border - isFocused -> LocalContentColor.current - else -> LocalContentColor.current - } + val color = navItemColor(selected, focused, drawerOpen) Icon( icon, contentDescription = null, tint = color, + modifier = Modifier.padding(0.dp), ) }, supportingContent = @@ -526,6 +570,7 @@ fun NavigationDrawerScope.NavItem( onClick: () -> Unit, selected: Boolean, moreExpanded: Boolean, + drawerOpen: Boolean, modifier: Modifier = Modifier, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, containerColor: Color = Color.Unspecified, @@ -549,6 +594,7 @@ fun NavigationDrawerScope.NavItem( else -> R.string.fa_film } } + val focused by interactionSource.collectIsFocusedAsState() NavigationDrawerItem( modifier = modifier, selected = false, @@ -558,6 +604,7 @@ fun NavigationDrawerScope.NavItem( containerColor = containerColor, ), leadingContent = { + val color = navItemColor(selected, focused, drawerOpen) Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { if (useFont) { Text( @@ -565,13 +612,15 @@ fun NavigationDrawerScope.NavItem( textAlign = TextAlign.Center, fontSize = 16.sp, fontFamily = FontAwesome, - color = if (selected) MaterialTheme.colorScheme.border else LocalContentColor.current, + color = color, + modifier = Modifier, ) } else { Icon( painter = painterResource(icon), contentDescription = null, - tint = if (selected) MaterialTheme.colorScheme.border else LocalContentColor.current, + tint = color, + modifier = Modifier, ) } } @@ -593,3 +642,24 @@ fun NavigationDrawerScope.NavItem( ) } } + +@Composable +fun navItemColor( + selected: Boolean, + focused: Boolean, + drawerOpen: Boolean, +): Color { + val alpha = + when { + drawerOpen -> .75f + selected && !drawerOpen -> .5f + else -> .2f + } + return when { + selected -> MaterialTheme.colorScheme.border + focused -> LocalContentColor.current + else -> MaterialTheme.colorScheme.onSurface + }.copy(alpha = alpha) +} + +val DrawerState.isOpen: Boolean get() = this.currentValue == DrawerValue.Open