From 28674704eb389470ff30bf47afdcc53ad7a2e683 Mon Sep 17 00:00:00 2001
From: tapframe <85391825+tapframe@users.noreply.github.com>
Date: Tue, 7 Apr 2026 02:07:30 +0530
Subject: [PATCH] feat: custom bottom nav in parity with tv icons
---
.../core/ui/NuvioPlatformInsets.android.kt | 1 +
.../drawable/sidebar_home.xml | 18 ++
.../drawable/sidebar_library.xml | 17 ++
.../drawable/sidebar_search.xml | 11 ++
.../drawable/sidebar_settings.xml | 11 ++
.../commonMain/kotlin/com/nuvio/app/App.kt | 94 +++++----
.../nuvio/app/core/ui/NuvioNavigationBar.kt | 178 ++++++++++++++++++
.../nuvio/app/core/ui/NuvioPlatformInsets.kt | 1 +
.../features/profiles/ProfileSwitcherTab.kt | 18 +-
.../app/core/ui/NuvioPlatformInsets.ios.kt | 8 +-
10 files changed, 301 insertions(+), 56 deletions(-)
create mode 100644 composeApp/src/commonMain/composeResources/drawable/sidebar_home.xml
create mode 100644 composeApp/src/commonMain/composeResources/drawable/sidebar_library.xml
create mode 100644 composeApp/src/commonMain/composeResources/drawable/sidebar_search.xml
create mode 100644 composeApp/src/commonMain/composeResources/drawable/sidebar_settings.xml
create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioNavigationBar.kt
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)