mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-11-04 10:13:59 +00:00
feat(navigation): refactor sidebar and bottombar navigation structure with new tab and group models
This commit is contained in:
parent
78168d269c
commit
86ec117cfd
@ -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<T : Any>(
|
||||
val title: String, val icon: ImageVector, val route: Routes,
|
||||
val data: T? = null
|
||||
) : 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(
|
||||
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<Any>("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)
|
||||
|
||||
@ -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",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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<NavigationItem.Tab<*>>()
|
||||
.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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
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)
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
Loading…
Reference in New Issue
Block a user