feat(navigation): refactor sidebar and bottombar navigation structure with new tab and group models

This commit is contained in:
Kingkor Roy Tirtho 2026-09-25 21:07:19 +06:00
parent 78168d269c
commit 86ec117cfd
9 changed files with 483 additions and 238 deletions

View File

@ -40,56 +40,91 @@ import dev.krtirtho.spotube.core.navigation.rememberNavigationState
import dev.krtirtho.spotube.core.navigation.toEntries import dev.krtirtho.spotube.core.navigation.toEntries
import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope
import dev.krtirtho.spotube.core.ui.theming.SpotubeTheme import dev.krtirtho.spotube.core.ui.theming.SpotubeTheme
import dev.krtirtho.spotube.modules.library.LibraryTab
import dev.krtirtho.spotube.modules.settings.SettingsRepository import dev.krtirtho.spotube.modules.settings.SettingsRepository
import dev.krtirtho.spotube.modules.settings.UserSettings import dev.krtirtho.spotube.modules.settings.UserSettings
import dev.krtirtho.spotube.modules.shell.AppShell import dev.krtirtho.spotube.modules.shell.AppShell
import dev.krtirtho.spotube.modules.webview.WebViewScreen import dev.krtirtho.spotube.modules.webview.WebViewScreen
import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxCd
import dev.krtirtho.spotube.resources.iconsax.IconsaxDirectboxReceive
import dev.krtirtho.spotube.resources.iconsax.IconsaxHome import dev.krtirtho.spotube.resources.iconsax.IconsaxHome
import dev.krtirtho.spotube.resources.iconsax.IconsaxHomeBroken import dev.krtirtho.spotube.resources.iconsax.IconsaxHomeBroken
import dev.krtirtho.spotube.resources.iconsax.IconsaxMirrorScreenRegular
import dev.krtirtho.spotube.resources.iconsax.IconsaxMirroringScreen
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicDashboard
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicLibrary import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicLibrary
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicLibraryOutline import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicLibraryOutline
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearch import dev.krtirtho.spotube.resources.iconsax.IconsaxSearch
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
import dev.krtirtho.spotube.resources.iconsax.IconsaxSetting2 import dev.krtirtho.spotube.resources.iconsax.IconsaxSetting2
import dev.krtirtho.spotube.resources.iconsax.IconsaxSettingTwotone import dev.krtirtho.spotube.resources.iconsax.IconsaxSettingTwotone
import dev.krtirtho.spotube.resources.iconsax.IconsaxSound
import dev.krtirtho.spotube.resources.iconsax.IconsaxSoundTwotone
import dev.krtirtho.spotube.resources.iconsax.User
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.koin.compose.koinInject import org.koin.compose.koinInject
import org.koin.compose.navigation3.koinEntryProvider import org.koin.compose.navigation3.koinEntryProvider
import org.koin.core.annotation.KoinExperimentalAPI import org.koin.core.annotation.KoinExperimentalAPI
data class TabItem( interface NavigationItem {
val title: String, data class Tab<T : Any>(
val icon: ImageVector, val title: String, val icon: ImageVector, val route: Routes,
val activeIcon: ImageVector, val data: T? = null
val route: Routes ) : NavigationItem
data class Group<T : Any>(
val title: String, val items: List<Tab<T>>
) : NavigationItem
}
private val tabs = listOf<NavigationItem>(
NavigationItem.Tab<Any>("Home", Iconsax.IconsaxHome, Routes.Home),
NavigationItem.Tab<Any>("Search", Iconsax.IconsaxSearch, Routes.Search),
) )
val tabs = listOf( val sidebarTabs = tabs + listOf(
TabItem( NavigationItem.Group(
"Home",
Iconsax.IconsaxHomeBroken,
Iconsax.IconsaxHome,
Routes.Home
),
TabItem(
"Search",
Iconsax.IconsaxSearchBroken,
Iconsax.IconsaxSearch,
Routes.Search
),
TabItem(
"Library", "Library",
Iconsax.IconsaxMusicLibraryOutline, items = listOf(
Iconsax.IconsaxMusicLibrary, NavigationItem.Tab(
Routes.Library "Playlists",
Iconsax.IconsaxMusicDashboard,
Routes.Library,
LibraryTab.Playlists
), ),
TabItem( NavigationItem.Tab("Albums", Iconsax.IconsaxCd, Routes.Library, LibraryTab.Albums),
"Settings", NavigationItem.Tab("Artists", Iconsax.User, Routes.Library, LibraryTab.Artists),
Iconsax.IconsaxSettingTwotone,
Iconsax.IconsaxSetting2,
Routes.Settings
) )
),
NavigationItem.Group(
"On Device",
items = listOf(
NavigationItem.Tab(
"Local Tracks",
Iconsax.IconsaxMusicLibrary,
Routes.Library,
LibraryTab.LocalTracks
),
NavigationItem.Tab(
"Downloads",
Iconsax.IconsaxDirectboxReceive,
Routes.Library,
LibraryTab.Downloads
),
)
),
NavigationItem.Group(
"Connect",
items = listOf(
NavigationItem.Tab("Devices", Iconsax.IconsaxMirrorScreenRegular, Routes.Devices),
NavigationItem.Tab("Group Jam", Iconsax.IconsaxSoundTwotone, Routes.Jam),
),
),
)
val bottombarTabs = tabs + listOf(
NavigationItem.Tab<Any>("Library", Iconsax.IconsaxMusicLibrary, Routes.Library),
NavigationItem.Tab("Settings", Iconsax.IconsaxSetting2, Routes.Settings)
) )
@OptIn( @OptIn(
@ -105,8 +140,7 @@ fun App(
val userSettings by settingsRepository.userSettings.collectAsStateWithLifecycle(initialValue = UserSettings()) val userSettings by settingsRepository.userSettings.collectAsStateWithLifecycle(initialValue = UserSettings())
val navigationState = rememberNavigationState( val navigationState = rememberNavigationState(
startRoute = Routes.Home, startRoute = Routes.Home, topLevelRoutes = TOP_LEVEL_ROUTES
topLevelRoutes = TOP_LEVEL_ROUTES
) )
val navigator = remember { val navigator = remember {
Navigator(navigationState) Navigator(navigationState)

View File

@ -38,10 +38,16 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -81,6 +87,7 @@ import dev.krtirtho.spotube.core.ui.component.cards.PlayableCard
import dev.krtirtho.spotube.core.ui.component.dragScrollable import dev.krtirtho.spotube.core.ui.component.dragScrollable
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset
import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay
@ -134,7 +141,8 @@ fun ArtistScreen(
state = listState, state = listState,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(innerPadding), .padding(innerPadding)
.padding(bottom = LocalAppShellBottomInset.current + 24.dp),
verticalArrangement = Arrangement.spacedBy(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp),
) { ) {
item { item {
@ -495,6 +503,7 @@ private fun ArtistMeta(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun ArtistHeaderActions( private fun ArtistHeaderActions(
isSaved: Boolean, isSaved: Boolean,
@ -508,7 +517,12 @@ private fun ArtistHeaderActions(
) { ) {
if (isSaved) { if (isSaved) {
SecondaryButton(onClick = onFollowClick) { SecondaryButton(onClick = onFollowClick) {
Text("Following", modifier = Modifier.width(65.dp), textAlign = TextAlign.Center) Text(
"Following",
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
} }
} else { } else {
PrimaryButton(onClick = onFollowClick) { PrimaryButton(onClick = onFollowClick) {
@ -516,6 +530,15 @@ private fun ArtistHeaderActions(
} }
} }
TooltipBox(
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
tooltip = {
PlainTooltip {
Text(if (isBlacklisted) "Remove from blacklist" else "Add to blacklist")
}
},
state = rememberTooltipState()
) {
if (isBlacklisted) { if (isBlacklisted) {
SecondaryIconButton(onClick = onBlacklistClick) { SecondaryIconButton(onClick = onBlacklistClick) {
Icon( Icon(
@ -534,6 +557,7 @@ private fun ArtistHeaderActions(
} }
} }
} }
}
@Composable @Composable
private fun TopTracksHeader( private fun TopTracksHeader(

View File

@ -54,6 +54,8 @@ import androidx.compose.ui.unit.sp
import compose.icons.FeatherIcons import compose.icons.FeatherIcons
import compose.icons.feathericons.X import compose.icons.feathericons.X
import dev.krtirtho.plugin_interfaces.plugin_apis.lyrics.LyricsLine import dev.krtirtho.plugin_interfaces.plugin_apis.lyrics.LyricsLine
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset
import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4 import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -70,18 +72,12 @@ fun LyricsScreen(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
Scaffold( Scaffold(
modifier = modifier, topBar = {
) { innerPadding -> ApplicationMainBar(
Column( backButton = onClose == null,
modifier = Modifier title = {
.fillMaxSize()
.padding(innerPadding)
.padding(horizontal = 16.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
Box( Box(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
) { ) {
if (onClose != null) { if (onClose != null) {
IconButton( IconButton(
@ -114,9 +110,19 @@ fun LyricsScreen(
) )
} }
} }
}
Spacer(modifier = Modifier.height(24.dp)) )
},
modifier = modifier,
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(horizontal = 16.dp)
.padding(bottom = LocalAppShellBottomInset.current + 24.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
when { when {
uiState.isLoading -> { uiState.isLoading -> {
Box( Box(

View File

@ -42,11 +42,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.krtirtho.spotube.NavigationItem
import dev.krtirtho.spotube.bottombarTabs
import dev.krtirtho.spotube.core.navigation.NavigationState import dev.krtirtho.spotube.core.navigation.NavigationState
import dev.krtirtho.spotube.core.navigation.Navigator import dev.krtirtho.spotube.core.navigation.Navigator
import dev.krtirtho.spotube.core.navigation.Routes import dev.krtirtho.spotube.core.navigation.Routes
import dev.krtirtho.spotube.modules.downloads.DownloadBadgeIndicator import dev.krtirtho.spotube.modules.downloads.DownloadBadgeIndicator
import dev.krtirtho.spotube.tabs
@Composable @Composable
fun AppBottombar( fun AppBottombar(
@ -68,8 +69,9 @@ fun AppBottombar(
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
tabs.forEach { (label, icon, activeIcon, screen) -> bottombarTabs.filterIsInstance<NavigationItem.Tab<*>>()
val selected = navigationState.topLevelRoute == screen .forEach { tab ->
val selected = navigationState.topLevelRoute == tab.route
val labelColor = if (selected) { val labelColor = if (selected) {
MaterialTheme.colorScheme.primary MaterialTheme.colorScheme.primary
} else { } else {
@ -79,7 +81,7 @@ fun AppBottombar(
Column( Column(
modifier = Modifier modifier = Modifier
.clip(CircleShape) .clip(CircleShape)
.clickable { navigator.navigate(screen) } .clickable { navigator.navigate(tab.route) }
.padding(8.dp) .padding(8.dp)
.size(56.dp), .size(56.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@ -87,13 +89,13 @@ fun AppBottombar(
) { ) {
Box { Box {
Icon( Icon(
imageVector = activeIcon, imageVector = tab.icon,
contentDescription = label, contentDescription = tab.title,
tint = if (selected) MaterialTheme.colorScheme.primary tint = if (selected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant, else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
if (screen == Routes.Library) { if (tab.route == Routes.Library) {
DownloadBadgeIndicator( DownloadBadgeIndicator(
modifier = Modifier.align(Alignment.TopEnd) modifier = Modifier.align(Alignment.TopEnd)
) )
@ -101,7 +103,7 @@ fun AppBottombar(
} }
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text( Text(
text = label, text = tab.title,
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
fontWeight = if (selected) FontWeight.Medium else FontWeight.Normal, fontWeight = if (selected) FontWeight.Medium else FontWeight.Normal,
color = labelColor, color = labelColor,

View File

@ -29,12 +29,16 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -55,6 +59,7 @@ import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.blur.HazeColorEffect import dev.chrisbanes.haze.blur.HazeColorEffect
import dev.chrisbanes.haze.blur.blurEffect import dev.chrisbanes.haze.blur.blurEffect
import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeEffect
import dev.krtirtho.spotube.NavigationItem
import dev.krtirtho.spotube.core.navigation.NavigationState import dev.krtirtho.spotube.core.navigation.NavigationState
import dev.krtirtho.spotube.core.navigation.Navigator import dev.krtirtho.spotube.core.navigation.Navigator
import dev.krtirtho.spotube.core.navigation.Routes import dev.krtirtho.spotube.core.navigation.Routes
@ -64,15 +69,15 @@ import dev.krtirtho.spotube.core.ui.base.OutlineButton
import dev.krtirtho.spotube.core.ui.base.SecondaryButton import dev.krtirtho.spotube.core.ui.base.SecondaryButton
import dev.krtirtho.spotube.core.ui.base.copyPadding import dev.krtirtho.spotube.core.ui.base.copyPadding
import dev.krtirtho.spotube.core.ui.base.copyShape import dev.krtirtho.spotube.core.ui.base.copyShape
import dev.krtirtho.spotube.core.ui.component.VerticalScrollbar
import dev.krtirtho.spotube.modules.downloads.DownloadBadgeIndicator import dev.krtirtho.spotube.modules.downloads.DownloadBadgeIndicator
import dev.krtirtho.spotube.modules.library.LibraryState import dev.krtirtho.spotube.modules.library.LibraryState
import dev.krtirtho.spotube.modules.library.LibraryTab import dev.krtirtho.spotube.modules.library.LibraryTab
import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxMirroringScreen import dev.krtirtho.spotube.resources.iconsax.IconsaxSetting2
import dev.krtirtho.spotube.resources.iconsax.IconsaxSidebarLeftBroken import dev.krtirtho.spotube.resources.iconsax.IconsaxSidebarLeftBroken
import dev.krtirtho.spotube.resources.iconsax.IconsaxSidebarRightBroken import dev.krtirtho.spotube.resources.iconsax.IconsaxSidebarRightBroken
import dev.krtirtho.spotube.resources.iconsax.User import dev.krtirtho.spotube.sidebarTabs
import dev.krtirtho.spotube.tabs
import org.jetbrains.compose.resources.Font import org.jetbrains.compose.resources.Font
import org.koin.compose.koinInject import org.koin.compose.koinInject
import spotube.composeapp.generated.resources.Res import spotube.composeapp.generated.resources.Res
@ -90,6 +95,7 @@ fun AppSidebar(
val width by animateDpAsState(targetValue = if (expanded) 236.dp else 86.dp) val width by animateDpAsState(targetValue = if (expanded) 236.dp else 86.dp)
val currentLibraryTab by libraryState.currentTab.collectAsState() val currentLibraryTab by libraryState.currentTab.collectAsState()
val surfaceTint = MaterialTheme.colorScheme.surface.copy(alpha = 0.85f) val surfaceTint = MaterialTheme.colorScheme.surface.copy(alpha = 0.85f)
val listState = rememberLazyListState()
Column( Column(
modifier = modifier modifier = modifier
@ -139,62 +145,74 @@ fun AppSidebar(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
tabs.forEach { (label, icon, activeIcon, screen) -> Box(modifier = Modifier.fillMaxSize()) {
val selected = navigationState.topLevelRoute == screen LazyColumn(state = listState) {
items(sidebarTabs.size) { index ->
if (screen == Routes.Library) { when (val tab = sidebarTabs[index]) {
AnimatedVisibility(visible = expanded) { is NavigationItem.Group<*> -> {
androidx.compose.animation.AnimatedVisibility(visible = expanded) {
Text( Text(
text = "LIBRARY", text = tab.title.uppercase(),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 8.dp) .padding(horizontal = 24.dp, vertical = 8.dp)
) )
} }
tab.items.forEach {
val selected = navigationState.topLevelRoute == it.route
SidebarItem(
label = it.title,
activeIcon = it.icon,
onClick = {
if (it.route == Routes.Library) {
libraryState.currentTab.value =
it.data as LibraryTab
}
navigator.navigate(it.route)
},
selected = if (it.route == Routes.Library) selected && currentLibraryTab == it.data else selected,
expanded = expanded,
showDownloadBadge = it.route == Routes.Library
&& it.data == LibraryTab.Downloads
)
}
LibraryTab.entries.forEach { tab -> }
val isSubSelected = selected && currentLibraryTab == tab
is NavigationItem.Tab<*> -> {
val selected = navigationState.topLevelRoute == tab.route
SidebarItem( SidebarItem(
label = tab.title, label = tab.title,
activeIcon = tab.icon, activeIcon = tab.icon,
onClick = { onClick = {
libraryState.currentTab.value = tab navigator.navigate(tab.route)
navigator.navigate(screen)
}, },
selected = isSubSelected,
expanded = expanded,
showDownloadBadge = tab == LibraryTab.Downloads,
)
}
} else {
SidebarItem(
label = label,
activeIcon = activeIcon,
onClick = { navigator.navigate(screen) },
selected = selected, selected = selected,
expanded = expanded, expanded = expanded,
) )
} }
} }
} }
}
VerticalScrollbar(
listState = listState,
modifier = Modifier
.align(Alignment.CenterEnd)
.fillMaxHeight()
)
}
}
HorizontalDivider()
SidebarItem( SidebarItem(
label = "Devices", label = "Settings",
activeIcon = Iconsax.IconsaxMirroringScreen, activeIcon = Iconsax.IconsaxSetting2,
onClick = { navigator.navigate(Routes.Devices) }, onClick = {
selected = false, navigator.navigate(Routes.Settings)
},
selected = navigationState.topLevelRoute == Routes.Settings,
expanded = expanded, expanded = expanded,
) )
SidebarItem(
label = "Group Jam",
activeIcon = Iconsax.User,
onClick = { navigator.navigate(Routes.Jam) },
selected = false,
expanded = expanded,
)
Spacer(modifier = Modifier.height(120.dp)) Spacer(modifier = Modifier.height(120.dp))
} }
} }
@ -209,7 +227,7 @@ fun SidebarItem(
showDownloadBadge: Boolean = false, showDownloadBadge: Boolean = false,
) { ) {
val itemContent: @Composable RowScope.() -> Unit = { val itemContent: @Composable RowScope.() -> Unit = {
Box(modifier = Modifier.size(20.dp)) { Box(modifier = Modifier.size(16.dp)) {
Icon( Icon(
imageVector = activeIcon, imageVector = activeIcon,
contentDescription = label, contentDescription = label,
@ -231,7 +249,7 @@ fun SidebarItem(
maxLines = 1, maxLines = 1,
softWrap = false, softWrap = false,
style = LocalTextStyle.current.copy( style = LocalTextStyle.current.copy(
fontSize = MaterialTheme.typography.labelLarge.fontSize, fontSize = MaterialTheme.typography.labelMedium.fontSize,
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium, fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium,
color = if (selected) { color = if (selected) {
MaterialTheme.colorScheme.onSecondaryContainer MaterialTheme.colorScheme.onSecondaryContainer
@ -246,8 +264,8 @@ fun SidebarItem(
val buttonModifier = Modifier val buttonModifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 4.dp) .padding(horizontal = 7.dp, vertical = 2.dp)
val contentPadding = PaddingValues(horizontal = 7.dp, vertical = 6.dp) val contentPadding = PaddingValues(horizontal = 5.dp)
if (selected) { if (selected) {
SecondaryButton( SecondaryButton(
@ -255,7 +273,7 @@ fun SidebarItem(
modifier = buttonModifier, modifier = buttonModifier,
theme = LocalBaseUITheme.current.buttons.secondary theme = LocalBaseUITheme.current.buttons.secondary
.copyPadding(contentPadding) .copyPadding(contentPadding)
.copyShape(RoundedCornerShape(8.dp)), .copyShape(RoundedCornerShape(5.dp)),
content = itemContent, content = itemContent,
) )
} else { } else {
@ -264,7 +282,7 @@ fun SidebarItem(
modifier = buttonModifier, modifier = buttonModifier,
theme = LocalBaseUITheme.current.buttons.outline theme = LocalBaseUITheme.current.buttons.outline
.copyPadding(contentPadding) .copyPadding(contentPadding)
.copyShape(RoundedCornerShape(8.dp)), .copyShape(RoundedCornerShape(5.dp)),
hoverOnly = true, hoverOnly = true,
content = itemContent, content = itemContent,
) )

View File

@ -0,0 +1,89 @@
package dev.krtirtho.spotube.resources.iconsax
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.PathData
import androidx.compose.ui.graphics.vector.group
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val Iconsax.IconsaxMirrorScreenRegular: ImageVector
get() {
if (_IconsaxMirrorScreenRegular != null) {
return _IconsaxMirrorScreenRegular!!
}
_IconsaxMirrorScreenRegular = ImageVector.Builder(
name = "IconsaxMirrorScreenRegular",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
group(
clipPathData = PathData {
moveTo(0f, 0f)
horizontalLineToRelative(24f)
verticalLineToRelative(24f)
horizontalLineToRelative(-24f)
close()
}
) {
path(
stroke = SolidColor(Color.White),
strokeLineWidth = 1.5f,
strokeLineCap = StrokeCap.Round,
strokeLineJoin = StrokeJoin.Round
) {
moveTo(2f, 9f)
verticalLineTo(8f)
curveTo(2f, 5f, 4f, 3f, 7f, 3f)
horizontalLineTo(17f)
curveTo(20f, 3f, 22f, 5f, 22f, 8f)
verticalLineTo(16f)
curveTo(22f, 19f, 20f, 21f, 17f, 21f)
horizontalLineTo(16f)
}
path(
fillAlpha = 0.4f,
stroke = SolidColor(Color.White),
strokeAlpha = 0.4f,
strokeLineWidth = 1.5f,
strokeLineCap = StrokeCap.Round,
strokeLineJoin = StrokeJoin.Round
) {
moveTo(3.69f, 11.711f)
curveTo(8.31f, 12.301f, 11.7f, 15.701f, 12.3f, 20.321f)
}
path(
fillAlpha = 0.4f,
stroke = SolidColor(Color.White),
strokeAlpha = 0.4f,
strokeLineWidth = 1.5f,
strokeLineCap = StrokeCap.Round,
strokeLineJoin = StrokeJoin.Round
) {
moveTo(2.62f, 15.07f)
curveTo(6.01f, 15.5f, 8.5f, 18f, 8.94f, 21.39f)
}
path(
fillAlpha = 0.4f,
stroke = SolidColor(Color.White),
strokeAlpha = 0.4f,
strokeLineWidth = 1.5f,
strokeLineCap = StrokeCap.Round,
strokeLineJoin = StrokeJoin.Round
) {
moveTo(1.98f, 18.859f)
curveTo(3.67f, 19.079f, 4.92f, 20.319f, 5.14f, 22.019f)
}
}
}.build()
return _IconsaxMirrorScreenRegular!!
}
@Suppress("ObjectPropertyName")
private var _IconsaxMirrorScreenRegular: ImageVector? = null

View File

@ -1,20 +1,3 @@
/*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package dev.krtirtho.spotube.resources.iconsax package dev.krtirtho.spotube.resources.iconsax
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -56,11 +39,7 @@ val Iconsax.IconsaxSound: ImageVector
curveTo(3.75f, 16.16f, 3.41f, 16.5f, 3f, 16.5f) curveTo(3.75f, 16.16f, 3.41f, 16.5f, 3f, 16.5f)
close() close()
} }
path( path(fill = SolidColor(Color.White)) {
fill = SolidColor(Color.White),
fillAlpha = 0.4f,
strokeAlpha = 0.4f
) {
moveTo(7.5f, 19f) moveTo(7.5f, 19f)
curveTo(7.09f, 19f, 6.75f, 18.66f, 6.75f, 18.25f) curveTo(7.09f, 19f, 6.75f, 18.66f, 6.75f, 18.25f)
verticalLineTo(5.75f) verticalLineTo(5.75f)
@ -80,11 +59,7 @@ val Iconsax.IconsaxSound: ImageVector
curveTo(12.75f, 21.16f, 12.41f, 21.5f, 12f, 21.5f) curveTo(12.75f, 21.16f, 12.41f, 21.5f, 12f, 21.5f)
close() close()
} }
path( path(fill = SolidColor(Color.White)) {
fill = SolidColor(Color.White),
fillAlpha = 0.4f,
strokeAlpha = 0.4f
) {
moveTo(16.5f, 19f) moveTo(16.5f, 19f)
curveTo(16.09f, 19f, 15.75f, 18.66f, 15.75f, 18.25f) curveTo(16.09f, 19f, 15.75f, 18.66f, 15.75f, 18.25f)
verticalLineTo(5.75f) verticalLineTo(5.75f)

View File

@ -0,0 +1,97 @@
package dev.krtirtho.spotube.resources.iconsax
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.PathData
import androidx.compose.ui.graphics.vector.group
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val Iconsax.IconsaxSoundTwotone: ImageVector
get() {
if (_IconsaxSoundTwotone != null) {
return _IconsaxSoundTwotone!!
}
_IconsaxSoundTwotone = ImageVector.Builder(
name = "IconsaxSoundTwotone",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
group(
clipPathData = PathData {
moveTo(0f, 0f)
horizontalLineToRelative(24f)
verticalLineToRelative(24f)
horizontalLineToRelative(-24f)
close()
}
) {
path(fill = SolidColor(Color.White)) {
moveTo(3f, 16.5f)
curveTo(2.59f, 16.5f, 2.25f, 16.16f, 2.25f, 15.75f)
verticalLineTo(8.25f)
curveTo(2.25f, 7.84f, 2.59f, 7.5f, 3f, 7.5f)
curveTo(3.41f, 7.5f, 3.75f, 7.84f, 3.75f, 8.25f)
verticalLineTo(15.75f)
curveTo(3.75f, 16.16f, 3.41f, 16.5f, 3f, 16.5f)
close()
}
path(
fill = SolidColor(Color.White),
fillAlpha = 0.4f,
strokeAlpha = 0.4f
) {
moveTo(7.5f, 19f)
curveTo(7.09f, 19f, 6.75f, 18.66f, 6.75f, 18.25f)
verticalLineTo(5.75f)
curveTo(6.75f, 5.34f, 7.09f, 5f, 7.5f, 5f)
curveTo(7.91f, 5f, 8.25f, 5.34f, 8.25f, 5.75f)
verticalLineTo(18.25f)
curveTo(8.25f, 18.66f, 7.91f, 19f, 7.5f, 19f)
close()
}
path(fill = SolidColor(Color.White)) {
moveTo(12f, 21.5f)
curveTo(11.59f, 21.5f, 11.25f, 21.16f, 11.25f, 20.75f)
verticalLineTo(3.25f)
curveTo(11.25f, 2.84f, 11.59f, 2.5f, 12f, 2.5f)
curveTo(12.41f, 2.5f, 12.75f, 2.84f, 12.75f, 3.25f)
verticalLineTo(20.75f)
curveTo(12.75f, 21.16f, 12.41f, 21.5f, 12f, 21.5f)
close()
}
path(
fill = SolidColor(Color.White),
fillAlpha = 0.4f,
strokeAlpha = 0.4f
) {
moveTo(16.5f, 19f)
curveTo(16.09f, 19f, 15.75f, 18.66f, 15.75f, 18.25f)
verticalLineTo(5.75f)
curveTo(15.75f, 5.34f, 16.09f, 5f, 16.5f, 5f)
curveTo(16.91f, 5f, 17.25f, 5.34f, 17.25f, 5.75f)
verticalLineTo(18.25f)
curveTo(17.25f, 18.66f, 16.91f, 19f, 16.5f, 19f)
close()
}
path(fill = SolidColor(Color.White)) {
moveTo(21f, 16.5f)
curveTo(20.59f, 16.5f, 20.25f, 16.16f, 20.25f, 15.75f)
verticalLineTo(8.25f)
curveTo(20.25f, 7.84f, 20.59f, 7.5f, 21f, 7.5f)
curveTo(21.41f, 7.5f, 21.75f, 7.84f, 21.75f, 8.25f)
verticalLineTo(15.75f)
curveTo(21.75f, 16.16f, 21.41f, 16.5f, 21f, 16.5f)
close()
}
}
}.build()
return _IconsaxSoundTwotone!!
}
@Suppress("ObjectPropertyName")
private var _IconsaxSoundTwotone: ImageVector? = null

View File

@ -26,5 +26,5 @@ toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/402983f310a88ac6
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/e50b80b5a11d194a898bc3e6211b7c4b/redirect toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/e50b80b5a11d194a898bc3e6211b7c4b/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/476d3c08f4989328dee56d22e202d98d/redirect toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/476d3c08f4989328dee56d22e202d98d/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/5a88b04b5e582b332d2e6bc12b45f1b9/redirect toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/5a88b04b5e582b332d2e6bc12b45f1b9/redirect
toolchainVendor=ADOPTIUM toolchainVendor=JETBRAINS
toolchainVersion=21 toolchainVersion=21