From 86ec117cfd5df23b966afebddf618c67b80380b8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 25 Sep 2026 21:07:19 +0600 Subject: [PATCH] feat(navigation): refactor sidebar and bottombar navigation structure with new tab and group models --- .../kotlin/dev/krtirtho/spotube/App.kt | 94 ++++++--- .../spotube/modules/artist/ArtistScreen.kt | 56 ++++-- .../spotube/modules/lyrics/LyricsScreen.kt | 82 ++++---- .../spotube/modules/shell/AppBottombar.kt | 80 ++++---- .../spotube/modules/shell/AppSidebar.kt | 190 ++++++++++-------- .../iconsax/IconsaxMirrorScreenRegular.kt | 89 ++++++++ .../spotube/resources/iconsax/IconsaxSound.kt | 29 +-- .../resources/iconsax/IconsaxSoundTwotone.kt | 97 +++++++++ gradle/gradle-daemon-jvm.properties | 4 +- 9 files changed, 483 insertions(+), 238 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/resources/iconsax/IconsaxMirrorScreenRegular.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/resources/iconsax/IconsaxSoundTwotone.kt diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt index ee165e9b..ae862050 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt @@ -40,56 +40,91 @@ import dev.krtirtho.spotube.core.navigation.rememberNavigationState import dev.krtirtho.spotube.core.navigation.toEntries import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope 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.UserSettings import dev.krtirtho.spotube.modules.shell.AppShell import dev.krtirtho.spotube.modules.webview.WebViewScreen 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.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.IconsaxMusicLibraryOutline import dev.krtirtho.spotube.resources.iconsax.IconsaxSearch import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken import dev.krtirtho.spotube.resources.iconsax.IconsaxSetting2 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 org.koin.compose.koinInject import org.koin.compose.navigation3.koinEntryProvider import org.koin.core.annotation.KoinExperimentalAPI -data class TabItem( - val title: String, - val icon: ImageVector, - val activeIcon: ImageVector, - val route: Routes +interface NavigationItem { + data class Tab( + val title: String, val icon: ImageVector, val route: Routes, + val data: T? = null + ) : NavigationItem + + data class Group( + val title: String, val items: List> + ) : NavigationItem +} + +private val tabs = listOf( + NavigationItem.Tab("Home", Iconsax.IconsaxHome, Routes.Home), + NavigationItem.Tab("Search", Iconsax.IconsaxSearch, Routes.Search), ) -val tabs = listOf( - TabItem( - "Home", - Iconsax.IconsaxHomeBroken, - Iconsax.IconsaxHome, - Routes.Home - ), - TabItem( - "Search", - Iconsax.IconsaxSearchBroken, - Iconsax.IconsaxSearch, - Routes.Search - ), - TabItem( +val sidebarTabs = tabs + listOf( + NavigationItem.Group( "Library", - Iconsax.IconsaxMusicLibraryOutline, - Iconsax.IconsaxMusicLibrary, - Routes.Library + items = listOf( + NavigationItem.Tab( + "Playlists", + Iconsax.IconsaxMusicDashboard, + Routes.Library, + LibraryTab.Playlists + ), + NavigationItem.Tab("Albums", Iconsax.IconsaxCd, Routes.Library, LibraryTab.Albums), + NavigationItem.Tab("Artists", Iconsax.User, Routes.Library, LibraryTab.Artists), + ) ), - TabItem( - "Settings", - 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("Library", Iconsax.IconsaxMusicLibrary, Routes.Library), + NavigationItem.Tab("Settings", Iconsax.IconsaxSetting2, Routes.Settings) ) @OptIn( @@ -105,8 +140,7 @@ fun App( val userSettings by settingsRepository.userSettings.collectAsStateWithLifecycle(initialValue = UserSettings()) val navigationState = rememberNavigationState( - startRoute = Routes.Home, - topLevelRoutes = TOP_LEVEL_ROUTES + startRoute = Routes.Home, topLevelRoutes = TOP_LEVEL_ROUTES ) val navigator = remember { Navigator(navigationState) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistScreen.kt index a38c36e6..ca2d0458 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistScreen.kt @@ -38,10 +38,16 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape 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.PlainTooltip import androidx.compose.material3.Scaffold 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.LaunchedEffect 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.misc.SkeletonTree 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.IconsaxAddSquare import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay @@ -134,7 +141,8 @@ fun ArtistScreen( state = listState, modifier = Modifier .fillMaxSize() - .padding(innerPadding), + .padding(innerPadding) + .padding(bottom = LocalAppShellBottomInset.current + 24.dp), verticalArrangement = Arrangement.spacedBy(24.dp), ) { item { @@ -495,6 +503,7 @@ private fun ArtistMeta( } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ArtistHeaderActions( isSaved: Boolean, @@ -508,7 +517,12 @@ private fun ArtistHeaderActions( ) { if (isSaved) { SecondaryButton(onClick = onFollowClick) { - Text("Following", modifier = Modifier.width(65.dp), textAlign = TextAlign.Center) + Text( + "Following", + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) } } else { PrimaryButton(onClick = onFollowClick) { @@ -516,20 +530,30 @@ private fun ArtistHeaderActions( } } - if (isBlacklisted) { - SecondaryIconButton(onClick = onBlacklistClick) { - Icon( - imageVector = Iconsax.IconsaxUserRemove, - contentDescription = "Remove from blacklist", - tint = MaterialTheme.colorScheme.error, - ) - } - } else { - SecondaryIconButton(onClick = onBlacklistClick) { - Icon( - imageVector = Iconsax.IconsaxUserRemove, - contentDescription = "Add to blacklist", - ) + TooltipBox( + positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above), + tooltip = { + PlainTooltip { + Text(if (isBlacklisted) "Remove from blacklist" else "Add to blacklist") + } + }, + state = rememberTooltipState() + ) { + if (isBlacklisted) { + SecondaryIconButton(onClick = onBlacklistClick) { + Icon( + imageVector = Iconsax.IconsaxUserRemove, + contentDescription = "Remove from blacklist", + tint = MaterialTheme.colorScheme.error, + ) + } + } else { + SecondaryIconButton(onClick = onBlacklistClick) { + Icon( + imageVector = Iconsax.IconsaxUserRemove, + contentDescription = "Add to blacklist", + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/lyrics/LyricsScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/lyrics/LyricsScreen.kt index f93008e2..b1a0cbd4 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/lyrics/LyricsScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/lyrics/LyricsScreen.kt @@ -54,6 +54,8 @@ import androidx.compose.ui.unit.sp import compose.icons.FeatherIcons import compose.icons.feathericons.X 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.IconsaxArrowDown4 import kotlinx.coroutines.launch @@ -70,6 +72,47 @@ fun LyricsScreen( val scope = rememberCoroutineScope() Scaffold( + topBar = { + ApplicationMainBar( + backButton = onClose == null, + title = { + Box( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + ) { + if (onClose != null) { + IconButton( + onClick = onClose, + modifier = Modifier.align(Alignment.CenterStart) + ) { + Icon( + imageVector = Iconsax.IconsaxArrowDown4, + contentDescription = "Close" + ) + } + } + Row( + modifier = Modifier + .align(Alignment.Center) + .clip(RoundedCornerShape(50.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + .padding(10.dp, 4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + LyricsModeTab( + label = "Synced", + isSelected = uiState.mode == LyricType.SYNCED, + onClick = { viewModel.setMode(LyricType.SYNCED) } + ) + LyricsModeTab( + label = "Plain", + isSelected = uiState.mode == LyricType.STATIC, + onClick = { viewModel.setMode(LyricType.STATIC) } + ) + } + } + } + ) + }, modifier = modifier, ) { innerPadding -> Column( @@ -77,46 +120,9 @@ fun LyricsScreen( .fillMaxSize() .padding(innerPadding) .padding(horizontal = 16.dp) + .padding(bottom = LocalAppShellBottomInset.current + 24.dp) ) { Spacer(modifier = Modifier.height(16.dp)) - - Box( - modifier = Modifier.fillMaxWidth(), - ) { - if (onClose != null) { - IconButton( - onClick = onClose, - modifier = Modifier.align(Alignment.CenterStart) - ) { - Icon( - imageVector = Iconsax.IconsaxArrowDown4, - contentDescription = "Close" - ) - } - } - Row( - modifier = Modifier - .align(Alignment.Center) - .clip(RoundedCornerShape(50.dp)) - .background(MaterialTheme.colorScheme.surfaceContainerHighest) - .padding(10.dp, 4.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - LyricsModeTab( - label = "Synced", - isSelected = uiState.mode == LyricType.SYNCED, - onClick = { viewModel.setMode(LyricType.SYNCED) } - ) - LyricsModeTab( - label = "Plain", - isSelected = uiState.mode == LyricType.STATIC, - onClick = { viewModel.setMode(LyricType.STATIC) } - ) - } - } - - Spacer(modifier = Modifier.height(24.dp)) - when { uiState.isLoading -> { Box( diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppBottombar.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppBottombar.kt index 4603e3e7..eaabb5e9 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppBottombar.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppBottombar.kt @@ -42,11 +42,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight 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.Navigator import dev.krtirtho.spotube.core.navigation.Routes import dev.krtirtho.spotube.modules.downloads.DownloadBadgeIndicator -import dev.krtirtho.spotube.tabs @Composable fun AppBottombar( @@ -68,46 +69,47 @@ fun AppBottombar( horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically, ) { - tabs.forEach { (label, icon, activeIcon, screen) -> - val selected = navigationState.topLevelRoute == screen - val labelColor = if (selected) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - } - - Column( - modifier = Modifier - .clip(CircleShape) - .clickable { navigator.navigate(screen) } - .padding(8.dp) - .size(56.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Box { - Icon( - imageVector = activeIcon, - contentDescription = label, - tint = if (selected) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp) - ) - if (screen == Routes.Library) { - DownloadBadgeIndicator( - modifier = Modifier.align(Alignment.TopEnd) - ) - } + bottombarTabs.filterIsInstance>() + .forEach { tab -> + val selected = navigationState.topLevelRoute == tab.route + val labelColor = if (selected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + + Column( + modifier = Modifier + .clip(CircleShape) + .clickable { navigator.navigate(tab.route) } + .padding(8.dp) + .size(56.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Box { + Icon( + imageVector = tab.icon, + contentDescription = tab.title, + tint = if (selected) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp) + ) + if (tab.route == Routes.Library) { + DownloadBadgeIndicator( + modifier = Modifier.align(Alignment.TopEnd) + ) + } + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = tab.title, + style = MaterialTheme.typography.labelSmall, + fontWeight = if (selected) FontWeight.Medium else FontWeight.Normal, + color = labelColor, + ) } - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = label, - style = MaterialTheme.typography.labelSmall, - fontWeight = if (selected) FontWeight.Medium else FontWeight.Normal, - color = labelColor, - ) } - } } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt index 0c4a3c78..36ede564 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt @@ -29,12 +29,16 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size 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.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.LocalTextStyle 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.blurEffect import dev.chrisbanes.haze.hazeEffect +import dev.krtirtho.spotube.NavigationItem import dev.krtirtho.spotube.core.navigation.NavigationState import dev.krtirtho.spotube.core.navigation.Navigator 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.copyPadding 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.library.LibraryState import dev.krtirtho.spotube.modules.library.LibraryTab 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.IconsaxSidebarRightBroken -import dev.krtirtho.spotube.resources.iconsax.User -import dev.krtirtho.spotube.tabs +import dev.krtirtho.spotube.sidebarTabs import org.jetbrains.compose.resources.Font import org.koin.compose.koinInject 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 currentLibraryTab by libraryState.currentTab.collectAsState() val surfaceTint = MaterialTheme.colorScheme.surface.copy(alpha = 0.85f) + val listState = rememberLazyListState() Column( modifier = modifier @@ -106,95 +112,107 @@ fun AppSidebar( horizontalAlignment = Alignment.CenterHorizontally ) { Column(modifier = Modifier.weight(1f)) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(14.dp), - horizontalArrangement = if (expanded) Arrangement.SpaceBetween else Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - AnimatedVisibility(visible = expanded, enter = fadeIn(), exit = fadeOut()) { - Text( - text = "Spotube", - style = MaterialTheme.typography.headlineLarge.copy( - fontFamily = FontFamily( - listOf( - Font(Res.font.cookie_regular, weight = FontWeight.Normal) - ) - ) - ), - color = MaterialTheme.colorScheme.onSurface - ) - } - GhostIconButton( - onClick = { expanded = !expanded } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + horizontalArrangement = if (expanded) Arrangement.SpaceBetween else Arrangement.Center, + verticalAlignment = Alignment.CenterVertically ) { - Icon( - imageVector = if (expanded) Iconsax.IconsaxSidebarLeftBroken else Iconsax.IconsaxSidebarRightBroken, - contentDescription = if (expanded) "Collapse sidebar" else "Expand sidebar", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - - Spacer(modifier = Modifier.height(8.dp)) - - tabs.forEach { (label, icon, activeIcon, screen) -> - val selected = navigationState.topLevelRoute == screen - - if (screen == Routes.Library) { - AnimatedVisibility(visible = expanded) { + AnimatedVisibility(visible = expanded, enter = fadeIn(), exit = fadeOut()) { Text( - text = "LIBRARY", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 8.dp) + text = "Spotube", + style = MaterialTheme.typography.headlineLarge.copy( + fontFamily = FontFamily( + listOf( + Font(Res.font.cookie_regular, weight = FontWeight.Normal) + ) + ) + ), + color = MaterialTheme.colorScheme.onSurface ) } - - LibraryTab.entries.forEach { tab -> - val isSubSelected = selected && currentLibraryTab == tab - - SidebarItem( - label = tab.title, - activeIcon = tab.icon, - onClick = { - libraryState.currentTab.value = tab - navigator.navigate(screen) - }, - selected = isSubSelected, - expanded = expanded, - showDownloadBadge = tab == LibraryTab.Downloads, + GhostIconButton( + onClick = { expanded = !expanded } + ) { + Icon( + imageVector = if (expanded) Iconsax.IconsaxSidebarLeftBroken else Iconsax.IconsaxSidebarRightBroken, + contentDescription = if (expanded) "Collapse sidebar" else "Expand sidebar", + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } - } else { - SidebarItem( - label = label, - activeIcon = activeIcon, - onClick = { navigator.navigate(screen) }, - selected = selected, - expanded = expanded, + } + + Spacer(modifier = Modifier.height(8.dp)) + + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn(state = listState) { + items(sidebarTabs.size) { index -> + when (val tab = sidebarTabs[index]) { + is NavigationItem.Group<*> -> { + androidx.compose.animation.AnimatedVisibility(visible = expanded) { + Text( + text = tab.title.uppercase(), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.fillMaxWidth() + .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 + ) + } + + } + + is NavigationItem.Tab<*> -> { + val selected = navigationState.topLevelRoute == tab.route + SidebarItem( + label = tab.title, + activeIcon = tab.icon, + onClick = { + navigator.navigate(tab.route) + }, + selected = selected, + expanded = expanded, + ) + } + } + } + } + VerticalScrollbar( + listState = listState, + modifier = Modifier + .align(Alignment.CenterEnd) + .fillMaxHeight() ) } } - } - + HorizontalDivider() SidebarItem( - label = "Devices", - activeIcon = Iconsax.IconsaxMirroringScreen, - onClick = { navigator.navigate(Routes.Devices) }, - selected = false, + label = "Settings", + activeIcon = Iconsax.IconsaxSetting2, + onClick = { + navigator.navigate(Routes.Settings) + }, + selected = navigationState.topLevelRoute == Routes.Settings, expanded = expanded, ) - SidebarItem( - label = "Group Jam", - activeIcon = Iconsax.User, - onClick = { navigator.navigate(Routes.Jam) }, - selected = false, - expanded = expanded, - ) - Spacer(modifier = Modifier.height(120.dp)) } } @@ -209,7 +227,7 @@ fun SidebarItem( showDownloadBadge: Boolean = false, ) { val itemContent: @Composable RowScope.() -> Unit = { - Box(modifier = Modifier.size(20.dp)) { + Box(modifier = Modifier.size(16.dp)) { Icon( imageVector = activeIcon, contentDescription = label, @@ -231,7 +249,7 @@ fun SidebarItem( maxLines = 1, softWrap = false, style = LocalTextStyle.current.copy( - fontSize = MaterialTheme.typography.labelLarge.fontSize, + fontSize = MaterialTheme.typography.labelMedium.fontSize, fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium, color = if (selected) { MaterialTheme.colorScheme.onSecondaryContainer @@ -246,8 +264,8 @@ fun SidebarItem( val buttonModifier = Modifier .fillMaxWidth() - .padding(horizontal = 10.dp, vertical = 4.dp) - val contentPadding = PaddingValues(horizontal = 7.dp, vertical = 6.dp) + .padding(horizontal = 7.dp, vertical = 2.dp) + val contentPadding = PaddingValues(horizontal = 5.dp) if (selected) { SecondaryButton( @@ -255,7 +273,7 @@ fun SidebarItem( modifier = buttonModifier, theme = LocalBaseUITheme.current.buttons.secondary .copyPadding(contentPadding) - .copyShape(RoundedCornerShape(8.dp)), + .copyShape(RoundedCornerShape(5.dp)), content = itemContent, ) } else { @@ -264,7 +282,7 @@ fun SidebarItem( modifier = buttonModifier, theme = LocalBaseUITheme.current.buttons.outline .copyPadding(contentPadding) - .copyShape(RoundedCornerShape(8.dp)), + .copyShape(RoundedCornerShape(5.dp)), hoverOnly = true, content = itemContent, ) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/resources/iconsax/IconsaxMirrorScreenRegular.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/resources/iconsax/IconsaxMirrorScreenRegular.kt new file mode 100644 index 00000000..df99a0eb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/resources/iconsax/IconsaxMirrorScreenRegular.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/resources/iconsax/IconsaxSound.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/resources/iconsax/IconsaxSound.kt index 89e3d481..6bb517c5 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/resources/iconsax/IconsaxSound.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/resources/iconsax/IconsaxSound.kt @@ -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 . - */ - package dev.krtirtho.spotube.resources.iconsax 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) close() } - path( - fill = SolidColor(Color.White), - fillAlpha = 0.4f, - strokeAlpha = 0.4f - ) { + path(fill = SolidColor(Color.White)) { moveTo(7.5f, 19f) curveTo(7.09f, 19f, 6.75f, 18.66f, 6.75f, 18.25f) verticalLineTo(5.75f) @@ -80,11 +59,7 @@ val Iconsax.IconsaxSound: ImageVector curveTo(12.75f, 21.16f, 12.41f, 21.5f, 12f, 21.5f) close() } - path( - fill = SolidColor(Color.White), - fillAlpha = 0.4f, - strokeAlpha = 0.4f - ) { + path(fill = SolidColor(Color.White)) { moveTo(16.5f, 19f) curveTo(16.09f, 19f, 15.75f, 18.66f, 15.75f, 18.25f) verticalLineTo(5.75f) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/resources/iconsax/IconsaxSoundTwotone.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/resources/iconsax/IconsaxSoundTwotone.kt new file mode 100644 index 00000000..4021e59a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/resources/iconsax/IconsaxSoundTwotone.kt @@ -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 diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties index 1d5bfcc0..55147520 100644 --- a/gradle/gradle-daemon-jvm.properties +++ b/gradle/gradle-daemon-jvm.properties @@ -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.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 -toolchainVendor=ADOPTIUM -toolchainVersion=21 +toolchainVendor=JETBRAINS +toolchainVersion=21 \ No newline at end of file