feat: replace FeatherIcons with Iconsax in window controls and update sidebar with better design

This commit is contained in:
Kingkor Roy Tirtho 2026-07-20 11:58:52 +06:00
parent a09a7c8976
commit 12dea49c2c
11 changed files with 259 additions and 39 deletions

View File

@ -19,8 +19,6 @@ package dev.krtirtho.spotube.modules.library
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@ -34,19 +32,17 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.krtirtho.spotube.core.ui.base.ChipTab
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
import dev.krtirtho.spotube.modules.downloads.DownloadsScreen
import dev.krtirtho.spotube.modules.library.album.LibraryAlbumsScreen
import dev.krtirtho.spotube.modules.library.artist.LibraryArtistsScreen
import dev.krtirtho.spotube.modules.library.local_tracks.LibraryLocalTracksScreen
import dev.krtirtho.spotube.modules.library.playlist.LibraryPlaylistsScreen
import dev.krtirtho.spotube.modules.downloads.DownloadsScreen
import dev.krtirtho.spotube.modules.shell.AppShellViewModel
import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset
import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
@ -94,11 +90,12 @@ fun LibraryScreen(
) {
items(LibraryTab.entries.size) { index ->
val tab = LibraryTab.entries[index]
FilterChip(
label = { Text(tab.title) },
ChipTab(
selected = currentTab == tab,
onClick = { libraryState.onTabSelected(tab) },
)
) {
Text(tab.title)
}
}
}
Spacer(modifier = Modifier.height(12.dp))

View File

@ -173,7 +173,6 @@ fun AppLargePlayer(
Surface(
modifier = modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.78f),
tonalElevation = 0.dp,
shadowElevation = 0.dp
) {

View File

@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
@ -36,7 +37,9 @@ import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.SheetValue
import androidx.compose.material3.VerticalDivider
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.material3.rememberStandardBottomSheetState
import androidx.compose.runtime.Composable
@ -119,7 +122,11 @@ fun AppShell(
AppSidebar(
navigator = navigator, navigationState = navigationState
)
VerticalDivider(
modifier = Modifier.fillMaxHeight(),
color = Color.Gray.copy(alpha = 0.2f),
thickness = 1.dp,
)
Box(modifier = Modifier.weight(1f)) {
content()
}
@ -167,6 +174,11 @@ fun AppShell(
)
}
}
HorizontalDivider(
modifier = Modifier.fillMaxWidth(),
color = Color.Gray.copy(alpha = 0.2f),
thickness = 1.dp,
)
AppLargePlayer(
modifier = Modifier.fillMaxWidth(),
onQueue = queueViewModel::toggleQueueVisibility,

View File

@ -22,6 +22,7 @@ import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -35,7 +36,9 @@ 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.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -59,6 +62,7 @@ import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
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.modules.downloads.DownloadBadgeIndicator
import dev.krtirtho.spotube.modules.library.LibraryState
import dev.krtirtho.spotube.modules.library.LibraryTab
@ -85,8 +89,7 @@ fun AppSidebar(
Column(
modifier = modifier
.fillMaxHeight()
.width(width)
.background(MaterialTheme.colorScheme.surfaceContainer),
.width(width),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
@ -174,12 +177,12 @@ fun SidebarItem(
showDownloadBadge: Boolean = false,
) {
val itemContent: @Composable RowScope.() -> Unit = {
Box(modifier = Modifier.size(24.dp)) {
Box(modifier = Modifier.size(20.dp)) {
Icon(
imageVector = activeIcon,
contentDescription = label,
tint = if (selected) {
MaterialTheme.colorScheme.onSecondaryContainer
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
@ -195,11 +198,15 @@ fun SidebarItem(
text = label,
maxLines = 1,
softWrap = false,
style = LocalTextStyle.current.copy(
fontSize = MaterialTheme.typography.labelLarge.fontSize,
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium,
color = if (selected) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
}
)
)
}
Spacer(modifier = if (expanded) Modifier.weight(1f) else Modifier)
@ -208,20 +215,24 @@ fun SidebarItem(
val buttonModifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 4.dp)
val contentPadding = PaddingValues(horizontal = 14.dp, vertical = 12.dp)
val contentPadding = PaddingValues(horizontal = 7.dp, vertical = 6.dp)
if (selected) {
SecondaryButton(
onClick = onClick,
modifier = buttonModifier,
theme = LocalBaseUITheme.current.buttons.secondary.copyPadding(contentPadding),
theme = LocalBaseUITheme.current.buttons.secondary
.copyPadding(contentPadding)
.copyShape(RoundedCornerShape(8.dp)),
content = itemContent,
)
} else {
OutlineButton(
onClick = onClick,
modifier = buttonModifier,
theme = LocalBaseUITheme.current.buttons.outline.copyPadding(contentPadding),
theme = LocalBaseUITheme.current.buttons.outline
.copyPadding(contentPadding)
.copyShape(RoundedCornerShape(8.dp)),
hoverOnly = true,
content = itemContent,
)

View File

@ -0,0 +1,51 @@
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.path
import androidx.compose.ui.unit.dp
val Iconsax.FluentDismiss: ImageVector
get() {
if (_FluentDismiss != null) {
return _FluentDismiss!!
}
_FluentDismiss = ImageVector.Builder(
name = "FluentDismiss",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
path(fill = SolidColor(Color(0xFF212121))) {
moveToRelative(4.397f, 4.554f)
lineToRelative(0.073f, -0.084f)
arcToRelative(0.75f, 0.75f, 0f, isMoreThanHalf = false, isPositiveArc = true, 0.976f, -0.073f)
lineToRelative(0.084f, 0.073f)
lineTo(12f, 10.939f)
lineToRelative(6.47f, -6.47f)
arcToRelative(0.75f, 0.75f, 0f, isMoreThanHalf = true, isPositiveArc = true, 1.06f, 1.061f)
lineTo(13.061f, 12f)
lineToRelative(6.47f, 6.47f)
arcToRelative(0.75f, 0.75f, 0f, isMoreThanHalf = false, isPositiveArc = true, 0.072f, 0.976f)
lineToRelative(-0.073f, 0.084f)
arcToRelative(0.75f, 0.75f, 0f, isMoreThanHalf = false, isPositiveArc = true, -0.976f, 0.073f)
lineToRelative(-0.084f, -0.073f)
lineTo(12f, 13.061f)
lineToRelative(-6.47f, 6.47f)
arcToRelative(0.75f, 0.75f, 0f, isMoreThanHalf = false, isPositiveArc = true, -1.06f, -1.061f)
lineTo(10.939f, 12f)
lineToRelative(-6.47f, -6.47f)
arcToRelative(0.75f, 0.75f, 0f, isMoreThanHalf = false, isPositiveArc = true, -0.072f, -0.976f)
lineToRelative(0.073f, -0.084f)
lineToRelative(-0.073f, 0.084f)
close()
}
}.build()
return _FluentDismiss!!
}
@Suppress("ObjectPropertyName")
private var _FluentDismiss: ImageVector? = null

View File

@ -0,0 +1,49 @@
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.path
import androidx.compose.ui.unit.dp
val Iconsax.FluentMaximize: ImageVector
get() {
if (_FluentMaximize != null) {
return _FluentMaximize!!
}
_FluentMaximize = ImageVector.Builder(
name = "FluentMaximize",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
path(fill = SolidColor(Color(0xFF212121))) {
moveTo(5.75f, 3f)
horizontalLineToRelative(12.5f)
arcTo(2.75f, 2.75f, 0f, isMoreThanHalf = false, isPositiveArc = true, 21f, 5.75f)
verticalLineToRelative(12.5f)
arcTo(2.75f, 2.75f, 0f, isMoreThanHalf = false, isPositiveArc = true, 18.25f, 21f)
lineTo(5.75f, 21f)
arcTo(2.75f, 2.75f, 0f, isMoreThanHalf = false, isPositiveArc = true, 3f, 18.25f)
lineTo(3f, 5.75f)
arcTo(2.75f, 2.75f, 0f, isMoreThanHalf = false, isPositiveArc = true, 5.75f, 3f)
close()
moveTo(5.75f, 4.5f)
curveToRelative(-0.69f, 0f, -1.25f, 0.56f, -1.25f, 1.25f)
verticalLineToRelative(12.5f)
curveToRelative(0f, 0.69f, 0.56f, 1.25f, 1.25f, 1.25f)
horizontalLineToRelative(12.5f)
curveToRelative(0.69f, 0f, 1.25f, -0.56f, 1.25f, -1.25f)
lineTo(19.5f, 5.75f)
curveToRelative(0f, -0.69f, -0.56f, -1.25f, -1.25f, -1.25f)
lineTo(5.75f, 4.5f)
close()
}
}.build()
return _FluentMaximize!!
}
@Suppress("ObjectPropertyName")
private var _FluentMaximize: ImageVector? = null

View File

@ -0,0 +1,35 @@
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.path
import androidx.compose.ui.unit.dp
val Iconsax.FluentMinus: ImageVector
get() {
if (_FluentMinus != null) {
return _FluentMinus!!
}
_FluentMinus = ImageVector.Builder(
name = "FluentMinus",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
path(fill = SolidColor(Color(0xFF212121))) {
moveTo(3.755f, 12.5f)
horizontalLineToRelative(16.492f)
arcToRelative(0.75f, 0.75f, 0f, isMoreThanHalf = false, isPositiveArc = false, 0f, -1.5f)
horizontalLineTo(3.755f)
arcToRelative(0.75f, 0.75f, 0f, isMoreThanHalf = false, isPositiveArc = false, 0f, 1.5f)
close()
}
}.build()
return _FluentMinus!!
}
@Suppress("ObjectPropertyName")
private var _FluentMinus: ImageVector? = null

View File

@ -0,0 +1,63 @@
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.path
import androidx.compose.ui.unit.dp
val Iconsax.FluentSquareMultiple: ImageVector
get() {
if (_FluentSquareMultiple != null) {
return _FluentSquareMultiple!!
}
_FluentSquareMultiple = ImageVector.Builder(
name = "FluentSquareMultiple",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
path(fill = SolidColor(Color(0xFF212121))) {
moveTo(7.518f, 5f)
horizontalLineTo(6.009f)
arcToRelative(3.25f, 3.25f, 0f, isMoreThanHalf = false, isPositiveArc = true, 3.24f, -3f)
horizontalLineToRelative(8.001f)
arcTo(4.75f, 4.75f, 0f, isMoreThanHalf = false, isPositiveArc = true, 22f, 6.75f)
verticalLineToRelative(8f)
arcToRelative(3.25f, 3.25f, 0f, isMoreThanHalf = false, isPositiveArc = true, -3f, 3.24f)
verticalLineToRelative(-1.508f)
arcToRelative(1.75f, 1.75f, 0f, isMoreThanHalf = false, isPositiveArc = false, 1.5f, -1.732f)
verticalLineToRelative(-8f)
arcToRelative(3.25f, 3.25f, 0f, isMoreThanHalf = false, isPositiveArc = false, -3.25f, -3.25f)
horizontalLineToRelative(-8f)
arcTo(1.75f, 1.75f, 0f, isMoreThanHalf = false, isPositiveArc = false, 7.518f, 5f)
close()
moveTo(5.25f, 6f)
arcTo(3.25f, 3.25f, 0f, isMoreThanHalf = false, isPositiveArc = false, 2f, 9.25f)
verticalLineToRelative(9.5f)
arcTo(3.25f, 3.25f, 0f, isMoreThanHalf = false, isPositiveArc = false, 5.25f, 22f)
horizontalLineToRelative(9.5f)
arcTo(3.25f, 3.25f, 0f, isMoreThanHalf = false, isPositiveArc = false, 18f, 18.75f)
verticalLineToRelative(-9.5f)
arcTo(3.25f, 3.25f, 0f, isMoreThanHalf = false, isPositiveArc = false, 14.75f, 6f)
horizontalLineToRelative(-9.5f)
close()
moveTo(3.5f, 9.25f)
curveToRelative(0f, -0.966f, 0.784f, -1.75f, 1.75f, -1.75f)
horizontalLineToRelative(9.5f)
curveToRelative(0.967f, 0f, 1.75f, 0.784f, 1.75f, 1.75f)
verticalLineToRelative(9.5f)
arcToRelative(1.75f, 1.75f, 0f, isMoreThanHalf = false, isPositiveArc = true, -1.75f, 1.75f)
horizontalLineToRelative(-9.5f)
arcToRelative(1.75f, 1.75f, 0f, isMoreThanHalf = false, isPositiveArc = true, -1.75f, -1.75f)
verticalLineToRelative(-9.5f)
close()
}
}.build()
return _FluentSquareMultiple!!
}
@Suppress("ObjectPropertyName")
private var _FluentSquareMultiple: ImageVector? = null

View File

@ -22,8 +22,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.window.WindowDraggableArea
@ -36,13 +34,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.ApplicationScope
import androidx.compose.ui.window.WindowPlacement
import compose.icons.FeatherIcons
import compose.icons.feathericons.Maximize2
import compose.icons.feathericons.Minimize2
import compose.icons.feathericons.Minus
import compose.icons.feathericons.X
import dev.krtirtho.spotube.resources.iconsax.FluentDismiss
import dev.krtirtho.spotube.resources.iconsax.FluentMaximize
import dev.krtirtho.spotube.resources.iconsax.FluentMinus
import dev.krtirtho.spotube.resources.iconsax.FluentSquareMultiple
import dev.krtirtho.spotube.resources.iconsax.Iconsax
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@ -79,7 +76,7 @@ actual fun ApplicationMainBar(
@Composable
private fun WindowButtons(modifier: Modifier = Modifier) {
val window = LocalWindowScope.current.window
val windowState = LocalWindowState.current
val applicationScope = LocalApplicationScope.current
Row(
@ -88,27 +85,27 @@ private fun WindowButtons(modifier: Modifier = Modifier) {
) {
IconButton(
onClick = {
window.isMinimized = true
windowState.isMinimized = true
}) {
Icon(
FeatherIcons.Minus, "Minimize", modifier = Modifier.size(14.dp)
Iconsax.FluentMinus, "Minimize", modifier = Modifier.size(14.dp)
)
}
IconButton(
onClick = {
window.placement = if (window.placement == WindowPlacement.Maximized) {
windowState.placement = if (windowState.placement == WindowPlacement.Maximized) {
WindowPlacement.Floating
} else {
WindowPlacement.Maximized
}
}) {
if (window.placement == WindowPlacement.Floating) {
if (windowState.placement == WindowPlacement.Floating) {
Icon(
FeatherIcons.Maximize2, "Maximize", modifier = Modifier.size(14.dp)
Iconsax.FluentMaximize, "Maximize", modifier = Modifier.size(14.dp)
)
} else {
Icon(
FeatherIcons.Minimize2, "Restore", modifier = Modifier.size(14.dp)
Iconsax.FluentSquareMultiple, "Restore", modifier = Modifier.size(14.dp)
)
}
}
@ -117,7 +114,7 @@ private fun WindowButtons(modifier: Modifier = Modifier) {
applicationScope.exitApplication()
}) {
Icon(
FeatherIcons.X, "Close", modifier = Modifier.size(14.dp)
Iconsax.FluentDismiss, "Close", modifier = Modifier.size(14.dp)
)
}
}

View File

@ -19,8 +19,12 @@ package dev.krtirtho.spotube.core.ui.component
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.window.FrameWindowScope
import androidx.compose.ui.window.WindowState
// LocalWindow is a LocalComposition that provides access to the current Window instance in the composition hierarchy.
val LocalWindowScope = compositionLocalOf<FrameWindowScope> {
error("No Window found in LocalWindowScope")
}
val LocalWindowState = compositionLocalOf<WindowState> {
error("No WindowState found in LocalWindowState")
}

View File

@ -29,6 +29,7 @@ import dev.krtirtho.spotube.core.newpipe.NewPipeDownloader
import dev.krtirtho.spotube.core.paths.Paths
import dev.krtirtho.spotube.core.ui.component.LocalApplicationScope
import dev.krtirtho.spotube.core.ui.component.LocalWindowScope
import dev.krtirtho.spotube.core.ui.component.LocalWindowState
import io.github.vinceglb.filekit.FileKit
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -66,6 +67,7 @@ fun main() {
CompositionLocalProvider(
LocalApplicationScope provides this@application,
LocalWindowScope provides this@Window,
LocalWindowState provides windowState
) {
App()
}