diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.android.kt index 10e8d5f8..a1820c50 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.android.kt @@ -8,5 +8,6 @@ import androidx.compose.ui.unit.dp internal actual val nuvioPlatformExtraTopPadding: Dp = 0.dp internal actual val nuvioPlatformExtraBottomPadding: Dp = 0.dp +internal actual val nuvioBottomNavigationExtraVerticalPadding: Dp = 6.dp @Composable internal actual fun nuvioBottomNavigationBarInsets(): WindowInsets = WindowInsets.navigationBars diff --git a/composeApp/src/commonMain/composeResources/drawable/sidebar_home.xml b/composeApp/src/commonMain/composeResources/drawable/sidebar_home.xml new file mode 100644 index 00000000..e6c5a0be --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/sidebar_home.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/drawable/sidebar_library.xml b/composeApp/src/commonMain/composeResources/drawable/sidebar_library.xml new file mode 100644 index 00000000..945764f0 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/sidebar_library.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/drawable/sidebar_search.xml b/composeApp/src/commonMain/composeResources/drawable/sidebar_search.xml new file mode 100644 index 00000000..5afc6e04 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/sidebar_search.xml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/drawable/sidebar_settings.xml b/composeApp/src/commonMain/composeResources/drawable/sidebar_settings.xml new file mode 100644 index 00000000..5eb15e5d --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/sidebar_settings.xml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index 610bf5d2..68414ba8 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -27,9 +27,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Home -import androidx.compose.material.icons.rounded.Search -import androidx.compose.material.icons.rounded.VideoLibrary +import androidx.compose.material.icons.filled.Home import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -37,9 +35,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.NavigationBarItemDefaults + import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -75,7 +71,7 @@ import com.nuvio.app.core.auth.AuthState import com.nuvio.app.core.deeplink.AppDeepLink import com.nuvio.app.core.deeplink.AppDeepLinkRepository import com.nuvio.app.core.sync.SyncManager -import com.nuvio.app.core.ui.nuvioBottomNavigationBarInsets +import com.nuvio.app.core.ui.NuvioNavigationBar import com.nuvio.app.core.ui.NuvioPosterActionSheet import com.nuvio.app.core.ui.PlatformBackHandler import com.nuvio.app.core.ui.TraktListPickerDialog @@ -145,6 +141,9 @@ import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import nuvio.composeapp.generated.resources.Res import nuvio.composeapp.generated.resources.app_logo_wordmark +import nuvio.composeapp.generated.resources.sidebar_library +import nuvio.composeapp.generated.resources.sidebar_search +import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.painterResource @Serializable @@ -556,52 +555,36 @@ private fun MainAppContent( contentWindowInsets = WindowInsets(0), bottomBar = { if (!isTabletLayout) { - val navigationItemColors = NavigationBarItemDefaults.colors( - selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer, - selectedTextColor = MaterialTheme.colorScheme.onPrimaryContainer, - indicatorColor = MaterialTheme.colorScheme.primaryContainer, - unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, - unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, - ) - NavigationBar( - containerColor = MaterialTheme.colorScheme.surfaceVariant, - windowInsets = nuvioBottomNavigationBarInsets(), - ) { - NavigationBarItem( + NuvioNavigationBar { + NavItem( selected = selectedTab == AppScreenTab.Home, onClick = { selectedTab = AppScreenTab.Home }, - icon = { Icon(Icons.Rounded.Home, contentDescription = null) }, - label = { Text("Home") }, - colors = navigationItemColors, + icon = Icons.Filled.Home, + contentDescription = "Home", ) - NavigationBarItem( + NavItem( selected = selectedTab == AppScreenTab.Search, onClick = { selectedTab = AppScreenTab.Search }, - icon = { Icon(Icons.Rounded.Search, contentDescription = null) }, - label = { Text("Search") }, - colors = navigationItemColors, + icon = Res.drawable.sidebar_search, + contentDescription = "Search", ) - NavigationBarItem( + NavItem( selected = selectedTab == AppScreenTab.Library, onClick = { selectedTab = AppScreenTab.Library }, - icon = { Icon(Icons.Rounded.VideoLibrary, contentDescription = null) }, - label = { Text("Library") }, - colors = navigationItemColors, + icon = Res.drawable.sidebar_library, + contentDescription = "Library", ) - NavigationBarItem( + NavItem( selected = selectedTab == AppScreenTab.Settings, onClick = { selectedTab = AppScreenTab.Settings }, - icon = { - ProfileSwitcherTab( - selected = selectedTab == AppScreenTab.Settings, - onClick = { selectedTab = AppScreenTab.Settings }, - onProfileSelected = onProfileSelected, - onAddProfileRequested = onSwitchProfile, - ) - }, - label = { Text("Profile") }, - colors = navigationItemColors, - ) + ) { + ProfileSwitcherTab( + selected = selectedTab == AppScreenTab.Settings, + onClick = { selectedTab = AppScreenTab.Settings }, + onProfileSelected = onProfileSelected, + onAddProfileRequested = onSwitchProfile, + ) + } } } }, @@ -1430,9 +1413,14 @@ private fun TabletFloatingTopBar( onClick = { onTabSelected(AppScreenTab.Home) }, icon = { Icon( - imageVector = Icons.Rounded.Home, - contentDescription = null, + imageVector = Icons.Filled.Home, + contentDescription = "Home", modifier = Modifier.size(18.dp), + tint = if (selectedTab == AppScreenTab.Home) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, ) }, ) @@ -1442,9 +1430,14 @@ private fun TabletFloatingTopBar( onClick = { onTabSelected(AppScreenTab.Search) }, icon = { Icon( - imageVector = Icons.Rounded.Search, - contentDescription = null, + painter = painterResource(Res.drawable.sidebar_search), + contentDescription = "Search", modifier = Modifier.size(18.dp), + tint = if (selectedTab == AppScreenTab.Search) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, ) }, ) @@ -1454,9 +1447,14 @@ private fun TabletFloatingTopBar( onClick = { onTabSelected(AppScreenTab.Library) }, icon = { Icon( - imageVector = Icons.Rounded.VideoLibrary, - contentDescription = null, + painter = painterResource(Res.drawable.sidebar_library), + contentDescription = "Library", modifier = Modifier.size(18.dp), + tint = if (selectedTab == AppScreenTab.Library) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, ) }, ) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioNavigationBar.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioNavigationBar.kt new file mode 100644 index 00000000..5176b6a2 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioNavigationBar.kt @@ -0,0 +1,178 @@ +package com.nuvio.app.core.ui + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.painterResource + +@Composable +fun NuvioNavigationBar( + modifier: Modifier = Modifier, + content: @Composable NuvioNavigationBarScope.() -> Unit, +) { + Column(modifier.fillMaxWidth()) { + HorizontalDivider( + thickness = 0.5.dp, + color = MaterialTheme.colorScheme.outlineVariant, + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(nuvioBottomNavigationBarInsets().asPaddingValues()) + .padding(horizontal = 4.dp, vertical = nuvioBottomNavigationExtraVerticalPadding), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), + ) { + NuvioNavigationBarScopeImpl(this).content() + } + } +} + +interface NuvioNavigationBarScope { + @Composable + fun NavItem( + selected: Boolean, + onClick: () -> Unit, + icon: ImageVector, + contentDescription: String?, + modifier: Modifier = Modifier, + ) + + @Composable + fun NavItem( + selected: Boolean, + onClick: () -> Unit, + icon: DrawableResource, + contentDescription: String?, + modifier: Modifier = Modifier, + ) + + @Composable + fun NavItem( + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, + ) +} + +private class NuvioNavigationBarScopeImpl( + private val rowScope: androidx.compose.foundation.layout.RowScope, +) : NuvioNavigationBarScope { + + @Composable + override fun NavItem( + selected: Boolean, + onClick: () -> Unit, + icon: ImageVector, + contentDescription: String?, + modifier: Modifier, + ) { + val iconColor by animateColorAsState( + targetValue = if (selected) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant, + ) + with(rowScope) { + Icon( + modifier = modifier + .widthIn(max = 150.dp) + .fillMaxWidth() + .weight(1f, fill = false) + .clip(RoundedCornerShape(16.dp)) + .selectable( + selected = selected, + enabled = true, + role = Role.Tab, + onClick = onClick, + ) + .padding(10.dp) + .size(28.dp), + imageVector = icon, + contentDescription = contentDescription, + tint = iconColor, + ) + } + } + + @Composable + override fun NavItem( + selected: Boolean, + onClick: () -> Unit, + icon: DrawableResource, + contentDescription: String?, + modifier: Modifier, + ) { + val iconColor by animateColorAsState( + targetValue = if (selected) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant, + ) + with(rowScope) { + Icon( + modifier = modifier + .widthIn(max = 150.dp) + .fillMaxWidth() + .weight(1f, fill = false) + .clip(RoundedCornerShape(16.dp)) + .selectable( + selected = selected, + enabled = true, + role = Role.Tab, + onClick = onClick, + ) + .padding(10.dp) + .size(28.dp), + painter = painterResource(icon), + contentDescription = contentDescription, + tint = iconColor, + ) + } + } + + @Composable + override fun NavItem( + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier, + content: @Composable () -> Unit, + ) { + with(rowScope) { + Box( + modifier = modifier + .widthIn(max = 150.dp) + .fillMaxWidth() + .weight(1f, fill = false) + .clip(RoundedCornerShape(16.dp)) + .selectable( + selected = selected, + enabled = true, + role = Role.Tab, + onClick = onClick, + ) + .padding(10.dp), + contentAlignment = Alignment.Center, + ) { + content() + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.kt index 22751960..945063a0 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.kt @@ -6,5 +6,6 @@ import androidx.compose.ui.unit.Dp internal expect val nuvioPlatformExtraTopPadding: Dp internal expect val nuvioPlatformExtraBottomPadding: Dp +internal expect val nuvioBottomNavigationExtraVerticalPadding: Dp @Composable internal expect fun nuvioBottomNavigationBarInsets(): WindowInsets diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt index a7b93288..9f1fe758 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt @@ -73,6 +73,7 @@ fun ProfileSwitcherTab( onClick: () -> Unit, onProfileSelected: (NuvioProfile) -> Unit, onAddProfileRequested: () -> Unit, + triggerContent: (@Composable (selected: Boolean) -> Unit)? = null, modifier: Modifier = Modifier, ) { val profileState by ProfileRepository.state.collectAsStateWithLifecycle() @@ -144,13 +145,16 @@ fun ProfileSwitcherTab( }, contentAlignment = Alignment.Center, ) { - // The avatar icon for the bottom navigation - ActiveProfileMiniAvatar( - profile = activeProfile, - avatars = avatars, - selected = selected, - size = 28, - ) + if (triggerContent != null) { + triggerContent(selected) + } else { + ActiveProfileMiniAvatar( + profile = activeProfile, + avatars = avatars, + selected = selected, + size = 28, + ) + } // Floating profile popup (stays composed during exit animation) if (popupVisible && profiles.isNotEmpty()) { diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.ios.kt index fa82abde..5042b4a4 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.ios.kt @@ -1,11 +1,17 @@ package com.nuvio.app.core.ui import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.safeDrawing import androidx.compose.runtime.Composable import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp internal actual val nuvioPlatformExtraTopPadding: Dp = 0.dp internal actual val nuvioPlatformExtraBottomPadding: Dp = 0.dp +internal actual val nuvioBottomNavigationExtraVerticalPadding: Dp = 0.dp + @Composable -internal actual fun nuvioBottomNavigationBarInsets(): WindowInsets = WindowInsets(0) +internal actual fun nuvioBottomNavigationBarInsets(): WindowInsets = + WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)