diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index 296ddb3f..a98cb2e0 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -279,6 +279,10 @@
- %1$d plugin installed
- %1$d plugins installed
+ Add a plugin from a link or a file on your device.
+ Pick a .smplug file from your device
+ or
+ Plugin details
Install a Plugin
https://example.com/plugin.smplug
Download
@@ -572,6 +576,7 @@
Start typing to search
Start typing to search tracks
No results found
+ No results
See all
Searching
Loading tracks...
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/ConnectionRequestDialog.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/ConnectionRequestDialog.kt
index 68e1d03b..311169db 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/ConnectionRequestDialog.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/ConnectionRequestDialog.kt
@@ -17,6 +17,7 @@
package dev.krtirtho.spotube.core.remote
+import org.spotube.millennium.composables.MillenniumAlertDialog
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@@ -34,7 +35,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle
-import org.spotube.millennium.composables.MillenniumDialog
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.koinInject
import spotube.composeapp.generated.resources.Res
@@ -56,21 +56,25 @@ fun ConnectionRequestDialogHost() {
}
pendingRequest?.let { request ->
- MillenniumDialog(
+ MillenniumAlertDialog(
visible = true,
onDismissRequest = {
handler.resolveConnectionRequest(request.sessionId, ConnectionRequestResponse.Deny)
pendingRequest = null
},
- title = stringResource(Res.string.remote_request_title)) {
- Text(
- text = stringResource(Res.string.remote_request_message, request.deviceName),
- style = AppTheme.typography.bodyMedium,
- textAlign = TextAlign.Start)
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
- verticalAlignment = Alignment.CenterVertically) {
+ title = { Text(stringResource(Res.string.remote_request_title)) },
+ description = {
+ Text(stringResource(Res.string.remote_request_message, request.deviceName))
+ },
+ ) {
+ MillenniumButton(
+ onClick = {
+ handler.resolveConnectionRequest(request.sessionId, ConnectionRequestResponse.AllowAlways)
+ pendingRequest = null
+ },
+ style = MillenniumButtonStyle.Quiet) {
+ Text(stringResource(Res.string.remote_request_allow_always))
+ }
MillenniumButton(
onClick = {
handler.resolveConnectionRequest(request.sessionId, ConnectionRequestResponse.Deny)
@@ -86,15 +90,6 @@ fun ConnectionRequestDialogHost() {
}) {
Text(stringResource(Res.string.remote_request_allow))
}
- MillenniumButton(
- onClick = {
- handler.resolveConnectionRequest(request.sessionId, ConnectionRequestResponse.AllowAlways)
- pendingRequest = null
- },
- style = MillenniumButtonStyle.Quiet) {
- Text(stringResource(Res.string.remote_request_allow_always))
- }
}
- }
}
}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDialogBottomSheet.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDialogBottomSheet.kt
index 24bda126..d35a8c38 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDialogBottomSheet.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDialogBottomSheet.kt
@@ -19,6 +19,15 @@ package dev.krtirtho.spotube.core.ui.component
import org.spotube.millennium.composables.MillenniumDialog
+import org.spotube.millennium.composables.MillenniumDialogBody
+import org.spotube.millennium.composables.MillenniumDialogClose
+import org.spotube.millennium.composables.MillenniumDialogFooter
+import org.spotube.millennium.composables.MillenniumDialogHeader
+import org.spotube.millennium.composables.MillenniumDialogTitle
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import dev.krtirtho.spotube.core.ui.theming.AppTheme
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle
@@ -50,6 +59,9 @@ import spotube.composeapp.generated.resources.*
fun AdaptiveDialogBottomSheet(
onDismiss: () -> Unit,
title: @Composable (() -> Unit)? = null,
+ // Plain-text title for the large-screen dialog's own titlebar; when set it replaces [title]
+ // there so the titlebar isn't left empty above a duplicated in-body title.
+ dialogTitle: String? = null,
modifier: Modifier = Modifier,
actions: @Composable (() -> Unit)? = null,
content: @Composable () -> Unit) {
@@ -59,14 +71,19 @@ fun AdaptiveDialogBottomSheet(
MillenniumDialog(
visible = true,
onDismissRequest = onDismiss,
- title = "",
- modifier = modifier) {
- title?.let { it() }
- content()
- actions?.let {
- MillenniumSeparator()
- it()
+ modifier = modifier,
+ paneTitle = dialogTitle ?: "") {
+ MillenniumDialogHeader {
+ if (dialogTitle != null) {
+ MillenniumDialogTitle(text = dialogTitle, textStyle = AppTheme.typography.titleMedium)
+ } else {
+ MillenniumDialogTitle { title?.invoke() }
+ }
+ MillenniumDialogClose(
+ contentDescription = stringResource(Res.string.common_close_dialog))
}
+ MillenniumDialogBody { content() }
+ actions?.let { MillenniumDialogFooter(content = { it() }) }
}
} else {
val sheetState = rememberModalBottomSheetState(initialDetent = SheetDetent.FullyExpanded)
@@ -99,7 +116,13 @@ fun AdaptiveDialogBottomSheet(
}
Spacer(Modifier.height(12.dp))
}
- content()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(max = 560.dp)
+ .verticalScroll(rememberScrollState())) {
+ content()
+ }
actions?.let {
Spacer(Modifier.height(8.dp))
MillenniumSeparator()
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDropdownBottomSheet.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDropdownBottomSheet.kt
index 5c7ec032..d66044b2 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDropdownBottomSheet.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDropdownBottomSheet.kt
@@ -23,6 +23,16 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -30,6 +40,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.draw.clip
+import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -41,6 +52,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import com.composeunstyled.currentWindowWidthBreakpoint
+import org.spotube.millennium.theme.MillenniumTheme
import org.spotube.millennium.adaptive.WindowBreakpoints
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
@@ -57,6 +69,14 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxShare
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
import org.jetbrains.compose.resources.stringResource
import org.spotube.millennium.composables.MillenniumDropdownMenu
+import org.spotube.millennium.composables.MillenniumDropdownMenuContent
+import org.spotube.millennium.composables.MillenniumDropdownMenuEmpty
+import org.spotube.millennium.composables.MillenniumDropdownMenuFilter
+import org.spotube.millennium.composables.MillenniumDropdownMenuHeader
+import org.spotube.millennium.composables.MillenniumDropdownMenuItem
+import org.spotube.millennium.composables.MillenniumDropdownMenuItemIcon
+import org.spotube.millennium.composables.MillenniumDropdownMenuSeparator
+import com.composeunstyled.Text
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle
@@ -67,6 +87,7 @@ import org.spotube.millennium.composables.MillenniumText
import org.spotube.millennium.composables.MillenniumTextField
import spotube.composeapp.generated.resources.Res
import spotube.composeapp.generated.resources.accessibility_selected
+import spotube.composeapp.generated.resources.common_no_results
import spotube.composeapp.generated.resources.search_filter_placeholder
data class AdaptiveMenuItem(
@@ -99,17 +120,13 @@ fun AdaptiveDropdownBottomSheet(
Box(modifier = modifier) {
if (isLargeScreen) {
- MillenniumDropdownMenu(
+ DesktopDropdownMenu(
expanded = expanded,
onExpandedChange = { expanded = it },
- items = items.map { it.label },
- onItemClick = { label ->
- items.firstOrNull { it.label == label && it.enabled }?.onClick?.invoke()
- expanded = false
- },
- minimumWidth = menuMinWidth,
- anchor = { trigger { expanded = true } }
- )
+ items = items,
+ menuMinWidth = menuMinWidth,
+ filter = filter,
+ trigger = { trigger { expanded = true } })
} else {
trigger { expanded = true }
if (expanded) {
@@ -134,6 +151,7 @@ private fun AdaptiveBottomSheetContent(
header: @Composable (() -> Unit)?,
filter: ((AdaptiveMenuItem, String) -> Boolean)? = null) {
val hasSelection = items.any { it.selected }
+ val hasLeading = items.any { it.icon != null || it.leadingContent != null }
val queryState = rememberTextFieldState()
val query = queryState.text.toString()
val filteredItems = if (filter != null && query.isNotBlank()) {
@@ -175,69 +193,134 @@ private fun AdaptiveBottomSheetContent(
if (item.dividerBefore) {
MillenniumSeparator(modifier = Modifier.padding(vertical = 4.dp))
}
- MillenniumListTile(
- title = {
- MillenniumText(
- item.label,
- style = AppTheme.typography.bodyLarge,
- color = if (item.enabled) {
- AppTheme.colors.onSurface
- } else {
- AppTheme.colors.onSurface.copy(alpha = 0.38f)
- }
- )
- },
- subtitle = item.subtitle?.let { subtitle ->
- {
- MillenniumText(
- subtitle,
- style = AppTheme.typography.bodySmall,
- color = AppTheme.colors.onSurfaceVariant
- )
- }
- },
- leading = when {
- item.leadingContent != null -> item.leadingContent
- item.icon != null -> {
- {
- MillenniumIcon(
- imageVector = item.icon,
- contentDescription = null,
- modifier = Modifier.size(22.dp)
- )
- }
- }
- item.selected -> {
- {
- MillenniumIcon(
- imageVector = FeatherIcons.Check,
- contentDescription = stringResource(Res.string.accessibility_selected),
- modifier = Modifier.size(22.dp)
- )
- }
- }
- hasSelection -> {{ Spacer(modifier = Modifier.size(22.dp)) }}
- else -> null
- },
- trailing = if (item.selected && (item.icon != null || item.leadingContent != null)) {
- {
- MillenniumIcon(
- imageVector = FeatherIcons.Check,
- contentDescription = stringResource(Res.string.accessibility_selected),
- modifier = Modifier.size(22.dp)
- )
- }
- } else {
- null
- },
- selected = item.selected,
- enabled = item.enabled,
+ AdaptiveMenuTile(
+ item = item,
+ hasSelection = hasSelection,
+ hasLeading = hasLeading,
+ textStyle = AppTheme.typography.bodyLarge,
+ minimumHeight = 56.dp,
onClick = {
item.onClick()
onDismiss()
- },
- minimumHeight = 56.dp
- )
+ })
+ }
+ }
+ }
+ }
+}
+
+/** One menu row. The leading slot is always reserved when any sibling has an icon/leading content
+ * (or a selection), so labels and icons stay aligned in a column. */
+@Composable
+private fun AdaptiveMenuTile(
+ item: AdaptiveMenuItem,
+ hasSelection: Boolean,
+ hasLeading: Boolean,
+ textStyle: androidx.compose.ui.text.TextStyle,
+ minimumHeight: Dp,
+ onClick: () -> Unit) {
+ if (item.dividerBefore) {
+ MillenniumSeparator(modifier = Modifier.padding(vertical = 4.dp))
+ }
+ val contentColor = if (item.enabled) AppTheme.colors.onSurface
+ else AppTheme.colors.onSurface.copy(alpha = 0.38f)
+ MillenniumListTile(
+ title = { MillenniumText(item.label, style = textStyle, color = contentColor) },
+ subtitle = item.subtitle?.let { subtitle ->
+ {
+ MillenniumText(
+ subtitle,
+ style = AppTheme.typography.bodySmall,
+ color = AppTheme.colors.onSurfaceVariant)
+ }
+ },
+ leading = when {
+ item.leadingContent != null -> item.leadingContent
+ item.icon != null -> {
+ {
+ MillenniumIcon(
+ imageVector = item.icon,
+ contentDescription = null,
+ modifier = Modifier.size(22.dp),
+ tint = contentColor)
+ }
+ }
+ item.selected -> {
+ {
+ MillenniumIcon(
+ imageVector = FeatherIcons.Check,
+ contentDescription = stringResource(Res.string.accessibility_selected),
+ modifier = Modifier.size(22.dp))
+ }
+ }
+ hasSelection || hasLeading -> {{ Spacer(modifier = Modifier.size(22.dp)) }}
+ else -> null
+ },
+ trailing = if (item.selected && (item.icon != null || item.leadingContent != null)) {
+ {
+ MillenniumIcon(
+ imageVector = FeatherIcons.Check,
+ contentDescription = stringResource(Res.string.accessibility_selected),
+ modifier = Modifier.size(22.dp))
+ }
+ } else {
+ null
+ },
+ selected = item.selected,
+ enabled = item.enabled,
+ onClick = onClick,
+ minimumHeight = minimumHeight)
+}
+
+/** Desktop popup built from Millennium's dropdown bricks: native icons, descriptions,
+ * selection indicator, separators and an optional search header. */
+@Composable
+private fun DesktopDropdownMenu(
+ expanded: Boolean,
+ onExpandedChange: (Boolean) -> Unit,
+ items: List,
+ menuMinWidth: Dp,
+ filter: ((AdaptiveMenuItem, String) -> Boolean)?,
+ trigger: @Composable () -> Unit) {
+ val queryState = rememberTextFieldState()
+ LaunchedEffect(expanded) { if (!expanded) queryState.clearText() }
+ val query = queryState.text.toString()
+ val visibleItems = if (filter != null && query.isNotBlank()) {
+ items.filter { filter(it, query) }
+ } else {
+ items
+ }
+ val hasLeading = items.any { it.icon != null || it.leadingContent != null }
+ val iconSize = MillenniumTheme.dropdownMenuTheme.states.default.iconSize
+
+ MillenniumDropdownMenu(
+ expanded = expanded,
+ onExpandedChange = onExpandedChange,
+ minimumWidth = menuMinWidth,
+ width = menuMinWidth,
+ anchor = trigger) {
+ if (filter != null) {
+ MillenniumDropdownMenuHeader(showDivider = false) {
+ MillenniumDropdownMenuFilter(
+ state = queryState,
+ placeholder = stringResource(Res.string.search_filter_placeholder))
+ }
+ }
+ MillenniumDropdownMenuContent {
+ if (visibleItems.isEmpty()) {
+ MillenniumDropdownMenuEmpty(stringResource(Res.string.common_no_results))
+ }
+ visibleItems.forEach { item ->
+ if (item.dividerBefore) MillenniumDropdownMenuSeparator()
+ MillenniumDropdownMenuItem(
+ onClick = item.onClick,
+ enabled = item.enabled,
+ selected = item.selected,
+ leading = item.leadingContent
+ ?: item.icon?.let { icon -> { MillenniumDropdownMenuItemIcon(icon) } }
+ ?: if (hasLeading) ({ Spacer(Modifier.size(iconSize)) }) else null,
+ description = item.subtitle?.let { subtitle -> { Text(subtitle) } }) {
+ Text(item.label)
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/ErrorDisplay.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/ErrorDisplay.kt
index 398cd494..9a7dc563 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/ErrorDisplay.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/ErrorDisplay.kt
@@ -18,6 +18,11 @@
package dev.krtirtho.spotube.core.ui.component
+import org.spotube.millennium.composables.MillenniumDialogHeader
+import org.spotube.millennium.composables.MillenniumDialogTitle
+import org.spotube.millennium.composables.MillenniumDialogClose
+import org.spotube.millennium.composables.MillenniumDialogBody
+import org.spotube.millennium.composables.MillenniumDialogFooter
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -191,19 +196,11 @@ private fun ErrorLogDialog(
onDismiss: () -> Unit) {
val clipboardManager = LocalClipboardManager.current
- MillenniumDialog(
- visible = true,
- onDismissRequest = onDismiss,
- title = "") {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically) {
- Text(
+ MillenniumDialog(visible = true, onDismissRequest = onDismiss) {
+ MillenniumDialogHeader {
+ MillenniumDialogTitle(
text = stringResource(Res.string.error_full_log_title),
- style = AppTheme.typography.titleLarge,
- fontWeight = FontWeight.Bold)
-
+ textStyle = AppTheme.typography.titleMedium)
MillenniumIconButton(
onClick = {
clipboardManager.setText(AnnotatedString(errorMessage))
@@ -213,31 +210,32 @@ private fun ErrorLogDialog(
imageVector = FeatherIcons.Copy,
contentDescription = stringResource(Res.string.error_copy_to_clipboard))
}
+ MillenniumDialogClose()
}
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .height(400.dp)
- .clip(AppTheme.shapes.small)
- .background(AppTheme.colors.surfaceContainerHighest.copy(alpha = 0.3f))) {
- SelectionContainer {
- Text(
- text = errorMessage,
- style = AppTheme.typography.bodySmall.copy(
- fontFamily = FontFamily.Monospace),
- color = AppTheme.colors.onSurfaceVariant,
- modifier = Modifier
- .fillMaxSize()
- .verticalScroll(rememberScrollState())
- .padding(12.dp))
+ MillenniumDialogBody(scrollable = false) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(400.dp)
+ .clip(AppTheme.shapes.small)
+ .background(AppTheme.colors.surfaceContainerHighest.copy(alpha = 0.3f))) {
+ SelectionContainer {
+ Text(
+ text = errorMessage,
+ style = AppTheme.typography.bodySmall.copy(
+ fontFamily = FontFamily.Monospace),
+ color = AppTheme.colors.onSurfaceVariant,
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(12.dp))
+ }
}
}
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.End) {
+ MillenniumDialogFooter {
MillenniumButton(
onClick = onDismiss,
- style = MillenniumButtonStyle.Quiet) {
+ style = MillenniumButtonStyle.Secondary) {
Text(stringResource(Res.string.common_close))
}
}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackOptions.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackOptions.kt
index e17d0d66..775d726a 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackOptions.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackOptions.kt
@@ -28,6 +28,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -184,7 +185,7 @@ fun TrackOptionsBottomSheet(
} else {
AppTheme.colors.onSurface.copy(alpha = 0.38f)
})
- }
+ } ?: Spacer(modifier = Modifier.size(22.dp))
Text(
text = item.label,
style = AppTheme.typography.bodyLarge,
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/misc/Shimmer.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/misc/Shimmer.kt
index 39795062..30954e17 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/misc/Shimmer.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/misc/Shimmer.kt
@@ -42,6 +42,8 @@ import androidx.compose.ui.unit.TextUnit
import com.valentinilk.shimmer.ShimmerBounds
import com.valentinilk.shimmer.rememberShimmer
import com.valentinilk.shimmer.shimmer
+import org.spotube.millennium.theme.MillenniumTheme
+import org.spotube.millennium.theme.millenniumBevelBrush
val LocalSkeletonLoading = compositionLocalOf { false }
@@ -80,11 +82,16 @@ fun SkeletonTree(
}
fun Modifier.shimmerApply(
- color: Color = Color.LightGray.copy(alpha = 0.5f) // Adjust alpha/color to match your theme
+ color: Color? = null // defaults to Millennium's recessed skeleton fill
): Modifier = composed {
val isTreeLoading = LocalSkeletonLoading.current
+ val skeletonBrush = millenniumBevelBrush(MillenniumTheme.colors, sunken = true)
- if (isTreeLoading) this.background(color) else this
+ when {
+ !isTreeLoading -> this
+ color != null -> this.background(color)
+ else -> this.background(skeletonBrush)
+ }
}
@Composable
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 5092a82b..992cae40 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
@@ -84,7 +84,6 @@ 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
@@ -143,8 +142,7 @@ fun ArtistScreen(
LazyColumn(
state = listState,
modifier = Modifier
- .fillMaxSize()
- .padding(bottom = LocalAppShellBottomInset.current + 24.dp),
+ .fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
item {
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesScreen.kt
index a6b029b7..e494ff27 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesScreen.kt
@@ -18,10 +18,10 @@
package dev.krtirtho.spotube.modules.devices
+import org.spotube.millennium.composables.MillenniumSpokeSpinner
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle
-import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.theme.MillenniumTheme
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -103,10 +103,9 @@ fun DevicesScreen(
title = { Text(stringResource(Res.string.devices_title)) },
actions = {
if (isDiscovering && connectingToDevice == null) {
- MillenniumIndeterminateProgress(
- modifier = Modifier
- .size(24.dp)
- .padding(end = 8.dp))
+ MillenniumSpokeSpinner(
+ size = 24.dp,
+ modifier = Modifier.padding(end = 8.dp))
} else {
MillenniumIcon(
imageVector = Iconsax.IconsaxRefreshRight,
@@ -177,8 +176,7 @@ fun DevicesScreen(
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically) {
- MillenniumIndeterminateProgress(
- modifier = Modifier.size(24.dp))
+ MillenniumSpokeSpinner(size = 24.dp)
Text(
text = stringResource(Res.string.devices_connecting),
style = AppTheme.typography.bodyMedium)
@@ -250,8 +248,7 @@ private fun DeviceRow(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)) {
if (isConnecting) {
- MillenniumIndeterminateProgress(
- modifier = Modifier.size(24.dp))
+ MillenniumSpokeSpinner(size = 24.dp)
} else {
MillenniumIcon(
imageVector = Iconsax.IconsaxMirroringScreen,
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/PlayDestinationPicker.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/PlayDestinationPicker.kt
index 2aec64e1..95554726 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/PlayDestinationPicker.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/PlayDestinationPicker.kt
@@ -18,6 +18,11 @@
package dev.krtirtho.spotube.modules.devices
+import org.spotube.millennium.composables.MillenniumDialogHeader
+import org.spotube.millennium.composables.MillenniumDialogTitle
+import org.spotube.millennium.composables.MillenniumDialogClose
+import org.spotube.millennium.composables.MillenniumDialogBody
+import org.spotube.millennium.composables.MillenniumDialogFooter
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumDialog
import org.spotube.millennium.composables.MillenniumListTile
@@ -82,10 +87,12 @@ fun PlayDestinationPickerHost() {
PlaybackDestinationAction.PlayNext -> stringResource(Res.string.devices_destination_action_play_next)
}
- MillenniumDialog(
- visible = true,
- onDismissRequest = controller::dismissPicker,
- title = stringResource(Res.string.devices_destination_title)) {
+ MillenniumDialog(visible = true, onDismissRequest = controller::dismissPicker) {
+ MillenniumDialogHeader {
+ MillenniumDialogTitle(stringResource(Res.string.devices_destination_title))
+ MillenniumDialogClose()
+ }
+ MillenniumDialogBody {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)) {
@@ -138,6 +145,9 @@ fun PlayDestinationPickerHost() {
})
}
+ }
+ }
+ MillenniumDialogFooter {
MillenniumButton(
onClick = controller::dismissPicker,
style = MillenniumButtonStyle.Quiet) {
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt
index 136c3408..fe3bc3be 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt
@@ -18,6 +18,8 @@
package dev.krtirtho.spotube.modules.devices
+import org.spotube.millennium.composables.MillenniumDropdownMenuItem
+import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemove
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle
@@ -604,11 +606,6 @@ private fun RemoteQueueItemRow(
MillenniumDropdownMenu(
expanded = showMenu,
onExpandedChange = { showMenu = it },
- items = listOf(stringResource(Res.string.devices_remote_remove_from_queue)),
- onItemClick = {
- onRemoveClick()
- showMenu = false
- },
anchor = {
MillenniumIconButton(
onClick = { showMenu = true },
@@ -620,7 +617,16 @@ private fun RemoteQueueItemRow(
modifier = Modifier.size(18.dp)
)
}
- })
+ }) {
+ MillenniumDropdownMenuItem(
+ label = stringResource(Res.string.devices_remote_remove_from_queue),
+ icon = Iconsax.IconsaxMusicSquareRemove,
+ destructive = true,
+ onClick = {
+ onRemoveClick()
+ showMenu = false
+ })
+ }
}
)
}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/downloads/DownloadsScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/downloads/DownloadsScreen.kt
index df59bd4b..0cfa15c7 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/downloads/DownloadsScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/downloads/DownloadsScreen.kt
@@ -3,6 +3,7 @@
*/
package dev.krtirtho.spotube.modules.downloads
+import org.spotube.millennium.composables.MillenniumStripedProgress
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -42,7 +43,6 @@ import org.spotube.millennium.composables.MillenniumButtonStyle
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle
-import org.spotube.millennium.composables.MillenniumProgress
import org.spotube.millennium.theme.MillenniumTheme
import org.spotube.millennium.theme.millenniumPanelBrush
import org.spotube.millennium.theme.millenniumSurface
@@ -225,9 +225,10 @@ internal fun DownloadListRow(
},
maxLines = 1, overflow = TextOverflow.Ellipsis
)
- if (item.status == DownloadStatus.Downloading) MillenniumProgress(
- item.progress,
- Modifier.fillMaxWidth().height(4.dp)
+ if (item.status == DownloadStatus.Downloading) MillenniumStripedProgress(
+ progress = item.progress,
+ modifier = Modifier.fillMaxWidth(),
+ height = 6.dp
)
}
MillenniumIconButton(
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/jam/JamScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/jam/JamScreen.kt
index 9180c0f8..7fa57b17 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/jam/JamScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/jam/JamScreen.kt
@@ -17,6 +17,9 @@
package dev.krtirtho.spotube.modules.jam
+import org.spotube.millennium.composables.MillenniumDropdownMenuItem
+import dev.krtirtho.spotube.resources.iconsax.IconsaxUserRemove
+import dev.krtirtho.spotube.resources.iconsax.IconsaxForbidden
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@@ -421,25 +424,27 @@ internal fun SessionPanel(
)
}
if (participant.canModerate) {
- val removeLabel = stringResource(Res.string.jam_participants_remove)
- val banLabel = stringResource(Res.string.jam_participants_ban)
// Keep destructive moderation out of the participant's primary row.
org.spotube.millennium.composables.MillenniumDropdownMenu(
expanded = participant.menuExpanded,
onExpandedChange = { expanded -> onMenuChange(if (expanded) participant.id else null) },
- items = listOf(removeLabel, banLabel),
- onItemClick = { label ->
- if (label == removeLabel) onKick(participant.id) else onBan(
- participant.id
- )
- },
anchor = {
MillenniumButton(
onClick = { onMenuChange(participant.id) },
style = MillenniumButtonStyle.Secondary
) { Text(stringResource(Res.string.jam_participants_manage)) }
}
- )
+ ) {
+ MillenniumDropdownMenuItem(
+ label = stringResource(Res.string.jam_participants_remove),
+ icon = Iconsax.IconsaxUserRemove,
+ onClick = { onKick(participant.id) })
+ MillenniumDropdownMenuItem(
+ label = stringResource(Res.string.jam_participants_ban),
+ icon = Iconsax.IconsaxForbidden,
+ destructive = true,
+ onClick = { onBan(participant.id) })
+ }
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/jam/PlayDestinationPicker.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/jam/PlayDestinationPicker.kt
index adff1af3..357bad74 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/jam/PlayDestinationPicker.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/jam/PlayDestinationPicker.kt
@@ -17,6 +17,7 @@
package dev.krtirtho.spotube.modules.jam
+import org.spotube.millennium.composables.MillenniumAlertDialog
import dev.krtirtho.spotube.core.ui.theming.AppTheme
import com.composeunstyled.Text
import org.spotube.millennium.composables.MillenniumButton
@@ -47,27 +48,23 @@ fun PlayDestinationPicker(
onSuggestToJam: () -> Unit) {
if (!visible) return
- MillenniumDialog(
+ MillenniumAlertDialog(
visible = true,
onDismissRequest = onDismiss,
- title = stringResource(Res.string.jam_destination_title)) {
- Text(
- text = stringResource(Res.string.jam_destination_message),
- style = AppTheme.typography.bodyMedium)
- Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
- MillenniumButton(
- onClick = onDismiss,
- style = MillenniumButtonStyle.Quiet) {
- Text(stringResource(Res.string.common_cancel))
- }
- MillenniumButton(
- onClick = onPlayLocally,
- style = MillenniumButtonStyle.Quiet) {
- Text(stringResource(Res.string.jam_destination_play_here))
- }
- MillenniumButton(onClick = onSuggestToJam) {
- Text(stringResource(Res.string.jam_destination_suggest))
- }
+ title = { Text(stringResource(Res.string.jam_destination_title)) },
+ description = { Text(stringResource(Res.string.jam_destination_message)) }) {
+ MillenniumButton(
+ onClick = onDismiss,
+ style = MillenniumButtonStyle.Quiet) {
+ Text(stringResource(Res.string.common_cancel))
+ }
+ MillenniumButton(
+ onClick = onPlayLocally,
+ style = MillenniumButtonStyle.Secondary) {
+ Text(stringResource(Res.string.jam_destination_play_here))
+ }
+ MillenniumButton(onClick = onSuggestToJam) {
+ Text(stringResource(Res.string.jam_destination_suggest))
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/local_tracks/LibraryLocalTracksScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/local_tracks/LibraryLocalTracksScreen.kt
index c2f3b405..4c22b818 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/local_tracks/LibraryLocalTracksScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/local_tracks/LibraryLocalTracksScreen.kt
@@ -3,6 +3,8 @@
*/
package dev.krtirtho.spotube.modules.library.local_tracks
+import org.spotube.millennium.composables.MillenniumCircularProgress
+import org.spotube.millennium.composables.MillenniumSpokeSpinner
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -51,7 +53,6 @@ import org.spotube.millennium.composables.MillenniumButtonStyle
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle
-import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.composables.MillenniumListTile
import org.spotube.millennium.theme.MillenniumTheme
import org.spotube.millennium.theme.millenniumPanelBrush
@@ -113,7 +114,7 @@ internal fun LocalTracksContent(
Modifier.fillMaxSize().padding(bottom = inset),
contentAlignment = Alignment.Center
) {
- MillenniumIndeterminateProgress()
+ MillenniumCircularProgress(size = 40.dp)
}
state.currentFolderPath == null && state.visibleFolders.isNotEmpty() -> LazyColumn(
@@ -264,7 +265,7 @@ internal fun LocalTracksBreadcrumb(
style = AppTheme.typography.labelSmall, color = AppTheme.colors.onSurfaceVariant,
maxLines = 1
)
- if (state.isRefreshing) MillenniumIndeterminateProgress(Modifier.size(14.dp))
+ if (state.isRefreshing) MillenniumSpokeSpinner(size = 14.dp)
if (state.currentFolderPath == null && onManageFolders != null) MillenniumIconButton(
onClick = onManageFolders,
modifier = Modifier.size(28.dp), style = MillenniumIconButtonStyle.Quiet
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/AddToPlaylistPicker.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/AddToPlaylistPicker.kt
index 6c84fd81..62819b97 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/AddToPlaylistPicker.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/AddToPlaylistPicker.kt
@@ -18,6 +18,7 @@
package dev.krtirtho.spotube.modules.library.playlist
+import dev.krtirtho.spotube.core.ui.component.AdaptiveDialogBottomSheet
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle
@@ -154,34 +155,25 @@ fun AddToPlaylistPicker(
}
}
- if (isLargeScreen) {
- MillenniumDialog(
- visible = true,
- onDismissRequest = onDismiss,
- title = stringResource(Res.string.library_add_to_playlist_title),
- modifier = modifier) {
- Box(modifier = Modifier.fillMaxWidth().height(400.dp)) {
- content()
- }
- MillenniumButton(
- onClick = onDismiss,
- style = MillenniumButtonStyle.Secondary) {
- Text(stringResource(Res.string.common_cancel))
- }
- }
- } else {
- val sheetState = rememberModalBottomSheetState(initialDetent = SheetDetent.FullyExpanded)
- MillenniumModalBottomSheet(state = sheetState) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 8.dp)) {
- Text(
- text = stringResource(Res.string.library_add_to_playlist_title),
- style = AppTheme.typography.headlineSmall,
- modifier = Modifier.padding(bottom = 16.dp))
- content()
+ val pickerTitle = stringResource(Res.string.library_add_to_playlist_title)
+ AdaptiveDialogBottomSheet(
+ onDismiss = onDismiss,
+ modifier = modifier,
+ dialogTitle = pickerTitle,
+ title = { Text(pickerTitle, style = AppTheme.typography.headlineSmall) },
+ actions = {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.End) {
+ MillenniumButton(
+ onClick = onDismiss,
+ style = MillenniumButtonStyle.Secondary) {
+ Text(stringResource(Res.string.common_cancel))
+ }
}
+ }) {
+ Box(modifier = Modifier.fillMaxWidth().height(400.dp)) {
+ content()
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/PlaylistFormSheet.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/PlaylistFormSheet.kt
index 7551a157..38c4eb9b 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/PlaylistFormSheet.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/PlaylistFormSheet.kt
@@ -17,6 +17,7 @@
package dev.krtirtho.spotube.modules.library.playlist
+import dev.krtirtho.spotube.core.ui.component.AdaptiveDialogBottomSheet
import androidx.compose.foundation.clickable
import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle
@@ -162,90 +163,42 @@ fun PlaylistFormSheet(
val handleImagePick = { imagePicker.launch() }
- if (isLargeScreen) {
- MillenniumDialog(
- visible = true,
- onDismissRequest = onDismiss,
- title = stringResource(if (isEditing) Res.string.library_playlist_form_edit_title else Res.string.library_playlist_form_create_title),
- modifier = modifier
- ) {
- PlaylistFormFields(
- nameState = nameState,
- descriptionState = descriptionState,
- isPublic = isPublic,
- onIsPublicChange = { isPublic = it },
- isCollaborating = isCollaborating,
- onIsCollaboratingChange = { isCollaborating = it },
- imageBase64 = imageBase64,
- imagePreviewUrl = imagePreviewUrl,
- onPickImage = handleImagePick
- )
- Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ val formTitle = stringResource(
+ if (isEditing) Res.string.library_playlist_form_edit_title else Res.string.library_playlist_form_create_title)
+ AdaptiveDialogBottomSheet(
+ onDismiss = onDismiss,
+ modifier = modifier,
+ dialogTitle = formTitle,
+ title = { Text(formTitle, style = AppTheme.typography.headlineSmall) },
+ actions = {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End)) {
+ MillenniumButton(
+ onClick = onDismiss,
+ style = MillenniumButtonStyle.Secondary) {
+ Text(stringResource(Res.string.common_cancel))
+ }
MillenniumButton(
onClick = {
onSubmit(currentData)
onDismiss()
},
- enabled = name.isNotBlank()
- ) {
+ enabled = name.isNotBlank()) {
Text(stringResource(if (isEditing) Res.string.common_save else Res.string.library_playlist_form_create))
}
- MillenniumButton(
- onClick = onDismiss,
- style = MillenniumButtonStyle.Secondary
- ) {
- Text(stringResource(Res.string.common_cancel))
- }
}
- }
- } else {
- val sheetState = rememberModalBottomSheetState(initialDetent = SheetDetent.FullyExpanded)
- MillenniumModalBottomSheet(state = sheetState) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 8.dp)
- ) {
- Text(
- text = stringResource(if (isEditing) Res.string.library_playlist_form_edit_title else Res.string.library_playlist_form_create_title),
- style = AppTheme.typography.headlineSmall,
- modifier = Modifier.padding(bottom = 16.dp)
- )
- PlaylistFormFields(
- nameState = nameState,
- descriptionState = descriptionState,
- isPublic = isPublic,
- onIsPublicChange = { isPublic = it },
- isCollaborating = isCollaborating,
- onIsCollaboratingChange = { isCollaborating = it },
- imageBase64 = imageBase64,
- imagePreviewUrl = imagePreviewUrl,
- onPickImage = handleImagePick
- )
- Spacer(modifier = Modifier.height(12.dp))
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End)
- ) {
- MillenniumButton(
- onClick = onDismiss,
- style = MillenniumButtonStyle.Secondary
- ) {
- Text(stringResource(Res.string.common_cancel))
- }
- MillenniumButton(
- onClick = {
- onSubmit(currentData)
- onDismiss()
- },
- enabled = name.isNotBlank()
- ) {
- Text(stringResource(if (isEditing) Res.string.common_save else Res.string.library_playlist_form_create))
- }
- }
- Spacer(modifier = Modifier.height(8.dp))
- }
- }
+ }) {
+ PlaylistFormFields(
+ nameState = nameState,
+ descriptionState = descriptionState,
+ isPublic = isPublic,
+ onIsPublicChange = { isPublic = it },
+ isCollaborating = isCollaborating,
+ onIsCollaboratingChange = { isCollaborating = it },
+ imageBase64 = imageBase64,
+ imagePreviewUrl = imagePreviewUrl,
+ onPickImage = handleImagePick)
}
}
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 5bc23b20..24285b5b 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
@@ -18,10 +18,10 @@
package dev.krtirtho.spotube.modules.lyrics
+import org.spotube.millennium.composables.MillenniumBarsLoader
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle
-import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -138,7 +138,7 @@ fun LyricsScreen(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
- MillenniumIndeterminateProgress(modifier = Modifier.fillMaxWidth(0.4f))
+ MillenniumBarsLoader(size = 48.dp)
}
}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/AddTracksToPlaylistDialog.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/AddTracksToPlaylistDialog.kt
index dfe87d1d..f50481df 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/AddTracksToPlaylistDialog.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/AddTracksToPlaylistDialog.kt
@@ -18,13 +18,15 @@
package dev.krtirtho.spotube.modules.playlist
+import dev.krtirtho.spotube.core.ui.component.AdaptiveDialogBottomSheet
+import org.spotube.millennium.composables.MillenniumCircularProgress
+import org.spotube.millennium.composables.MillenniumSpokeSpinner
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle
import org.spotube.millennium.composables.MillenniumDialog
import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle
-import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.composables.MillenniumModalBottomSheet
import org.spotube.millennium.composables.MillenniumTextField
import androidx.compose.foundation.layout.Arrangement
@@ -169,7 +171,7 @@ fun AddTracksToPlaylistDialog(
.fillMaxWidth()
.weight(1f),
contentAlignment = Alignment.Center) {
- MillenniumIndeterminateProgress()
+ MillenniumCircularProgress(size = 40.dp)
}
}
hasSearched && searchResults.isEmpty() -> {
@@ -209,33 +211,23 @@ fun AddTracksToPlaylistDialog(
}
}
- if (isLargeScreen) {
- MillenniumDialog(
- visible = true,
- onDismissRequest = onDismiss,
- title = stringResource(Res.string.playlist_add_tracks_title),
- modifier = modifier) {
- Box(modifier = Modifier.fillMaxWidth().height(500.dp)) {
- content()
- }
- MillenniumButton(onClick = onDismiss) {
- Text(stringResource(Res.string.playlist_add_tracks_done))
- }
- }
- } else {
- val sheetState = rememberModalBottomSheetState(initialDetent = SheetDetent.FullyExpanded)
- MillenniumModalBottomSheet(state = sheetState) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 8.dp)
- .height(600.dp)) {
- Text(
- text = stringResource(Res.string.playlist_add_tracks_title),
- style = AppTheme.typography.headlineSmall,
- modifier = Modifier.padding(bottom = 16.dp))
- content()
+ val dialogTitle = stringResource(Res.string.playlist_add_tracks_title)
+ AdaptiveDialogBottomSheet(
+ onDismiss = onDismiss,
+ modifier = modifier,
+ dialogTitle = dialogTitle,
+ title = { Text(dialogTitle, style = AppTheme.typography.headlineSmall) },
+ actions = {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.End) {
+ MillenniumButton(onClick = onDismiss) {
+ Text(stringResource(Res.string.playlist_add_tracks_done))
+ }
}
+ }) {
+ Box(modifier = Modifier.fillMaxWidth().height(500.dp)) {
+ content()
}
}
}
@@ -297,8 +289,7 @@ private fun TrackSearchResultRow(
modifier = Modifier.size(24.dp))
}
isAdding -> {
- MillenniumIndeterminateProgress(
- modifier = Modifier.size(24.dp))
+ MillenniumSpokeSpinner(size = 24.dp)
}
else -> {
MillenniumButton(
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginScreen.kt
index 5ddffeb0..ec1a862f 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginScreen.kt
@@ -18,13 +18,15 @@
package dev.krtirtho.spotube.modules.plugin
+import org.spotube.millennium.composables.MillenniumCircularProgress
+import org.spotube.millennium.composables.MillenniumDotsLoader
+import org.spotube.millennium.composables.MillenniumSpokeSpinner
import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle
import org.spotube.millennium.composables.MillenniumDialog
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle
-import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.composables.MillenniumSeparator
import org.spotube.millennium.composables.MillenniumSegmentedButton
import org.spotube.millennium.composables.MillenniumSegmentedButtonDefaults
@@ -85,6 +87,22 @@ import dev.krtirtho.spotube.core.ui.component.HeaderDisplayMode
import dev.krtirtho.spotube.getPlatform
import dev.krtirtho.spotube.modules.plugin.components.DefaultAbilityPluginGrid
import dev.krtirtho.spotube.modules.plugin.components.PluginCard
+import dev.krtirtho.spotube.modules.plugin.components.PluginDialogActions
+import dev.krtirtho.spotube.modules.plugin.components.PluginDialogBody
+import dev.krtirtho.spotube.modules.plugin.components.PluginDialogTitle
+import dev.krtirtho.spotube.modules.plugin.components.PluginIconTile
+import dev.krtirtho.spotube.modules.plugin.components.PluginLogoTile
+import dev.krtirtho.spotube.modules.plugin.components.PluginPanel
+import dev.krtirtho.spotube.modules.plugin.components.PluginPill
+import dev.krtirtho.spotube.resources.iconsax.IconsaxBoxAdd
+import dev.krtirtho.spotube.resources.iconsax.User
+import compose.icons.FeatherIcons
+import compose.icons.feathericons.ChevronRight
+import spotube.composeapp.generated.resources.plugin_install_sheet_subtitle
+import spotube.composeapp.generated.resources.plugin_install_file_hint
+import spotube.composeapp.generated.resources.plugin_install_or
+import spotube.composeapp.generated.resources.plugin_info_dialog_title
+import spotube.composeapp.generated.resources.plugin_version_label
import dev.krtirtho.spotube.modules.plugin.components.PluginInstallDialog
import dev.krtirtho.spotube.modules.plugin.components.PluginLogo
import dev.krtirtho.spotube.modules.plugin.components.PluginPermissionDialog
@@ -267,7 +285,7 @@ internal fun PluginScreenContent(
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
- ) { MillenniumIndeterminateProgress(modifier = Modifier.widthIn(max = 160.dp)) }
+ ) { MillenniumCircularProgress(size = 40.dp) }
}
is PluginUiState.Data -> {
@@ -492,7 +510,7 @@ internal fun PluginScreenContent(
Modifier.fillMaxWidth().padding(16.dp),
contentAlignment = Alignment.Center
) {
- MillenniumIndeterminateProgress(Modifier.widthIn(max = 120.dp))
+ MillenniumDotsLoader()
}
}
}
@@ -524,7 +542,7 @@ internal fun PluginScreenContent(
.padding(vertical = 16.dp),
contentAlignment = Alignment.Center
) {
- MillenniumIndeterminateProgress(modifier = Modifier.widthIn(max = 120.dp))
+ MillenniumDotsLoader()
}
}
}
@@ -606,24 +624,17 @@ private fun InstallSheet(
AdaptiveDialogBottomSheet(
onDismiss = onDismiss,
title = {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- MillenniumIcon(
- Iconsax.IconsaxImportArrow2Bulk,
- contentDescription = null,
- modifier = Modifier.size(18.dp),
- tint = AppTheme.colors.primary
- )
- Text(
- stringResource(Res.string.plugin_install_section_title),
- style = AppTheme.typography.titleSmall,
- fontWeight = FontWeight.SemiBold
- )
- }
- }) {
- Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ PluginDialogTitle(
+ title = stringResource(Res.string.plugin_install_section_title),
+ icon = Iconsax.IconsaxBoxAdd)
+ },
+ dialogTitle = stringResource(Res.string.plugin_install_section_title)) {
+ PluginDialogBody {
+ Text(
+ stringResource(Res.string.plugin_install_sheet_subtitle),
+ style = AppTheme.typography.bodySmall,
+ color = AppTheme.colors.onSurfaceVariant)
+
val urlState = rememberTextFieldState(urlInput)
LaunchedEffect(urlInput) {
if (urlState.text.toString() != urlInput) {
@@ -636,51 +647,82 @@ private fun InstallSheet(
}
}
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- MillenniumTextField(
- state = urlState,
- label = stringResource(Res.string.plugin_section_url_title),
- modifier = Modifier.weight(1f),
- placeholder = stringResource(Res.string.plugin_url_placeholder),
- error = urlError?.text())
- MillenniumIconButton(
- onClick = onSubmitUrl,
- enabled = !isLoadingUrl,
- style = MillenniumIconButtonStyle.Secondary) {
- if (isLoadingUrl) {
- MillenniumIndeterminateProgress(modifier = Modifier.size(16.dp))
- } else {
- MillenniumIcon(
- Iconsax.IconsaxImportArrow2Bulk,
- contentDescription = stringResource(Res.string.plugin_action_download))
+ PluginPanel {
+ Column(
+ modifier = Modifier.fillMaxWidth().padding(14.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ PluginIconTile(Iconsax.IconsaxLink)
+ Text(
+ stringResource(Res.string.plugin_section_url_title),
+ style = AppTheme.typography.titleSmall,
+ fontWeight = FontWeight.SemiBold)
+ }
+ MillenniumTextField(
+ state = urlState,
+ label = "",
+ modifier = Modifier.fillMaxWidth(),
+ placeholder = stringResource(Res.string.plugin_url_placeholder),
+ error = urlError?.text())
+ MillenniumButton(
+ onClick = onSubmitUrl,
+ enabled = !isLoadingUrl,
+ modifier = Modifier.fillMaxWidth(),
+ style = MillenniumButtonStyle.Primary) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ if (isLoadingUrl) {
+ MillenniumSpokeSpinner(size = 16.dp, color = MillenniumTheme.colors.onAccent)
+ } else {
+ MillenniumIcon(
+ Iconsax.IconsaxImportArrow2Bulk,
+ contentDescription = null,
+ modifier = Modifier.size(18.dp))
+ }
+ Text(stringResource(Res.string.plugin_action_download))
+ }
}
}
}
- MillenniumSeparator()
-
- Text(
- stringResource(Res.string.plugin_section_file_title),
- style = AppTheme.typography.labelLarge,
- color = AppTheme.colors.onSurfaceVariant
- )
- MillenniumButton(
- onClick = onPickFile,
+ Row(
modifier = Modifier.fillMaxWidth(),
- style = MillenniumButtonStyle.Secondary) {
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ MillenniumSeparator(modifier = Modifier.weight(1f))
+ Text(
+ stringResource(Res.string.plugin_install_or),
+ style = AppTheme.typography.labelSmall,
+ color = AppTheme.colors.onSurfaceVariant)
+ MillenniumSeparator(modifier = Modifier.weight(1f))
+ }
+
+ PluginPanel(modifier = Modifier.clickable(onClick = onPickFile)) {
Row(
+ modifier = Modifier.fillMaxWidth().padding(14.dp),
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
+ horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ PluginIconTile(Iconsax.IconsaxExportArrowBulk)
+ Column(
+ modifier = Modifier.weight(1f),
+ verticalArrangement = Arrangement.spacedBy(2.dp)) {
+ Text(
+ stringResource(Res.string.plugin_action_install_from_file),
+ style = AppTheme.typography.titleSmall,
+ fontWeight = FontWeight.SemiBold)
+ Text(
+ stringResource(Res.string.plugin_install_file_hint),
+ style = AppTheme.typography.labelSmall,
+ color = AppTheme.colors.onSurfaceVariant)
+ }
MillenniumIcon(
- Iconsax.IconsaxExportArrowBulk,
- contentDescription = stringResource(Res.string.plugin_action_install_from_file)
- )
- Text(stringResource(Res.string.plugin_action_install_from_file))
+ FeatherIcons.ChevronRight,
+ contentDescription = null,
+ modifier = Modifier.size(18.dp),
+ tint = AppTheme.colors.onSurfaceVariant)
}
}
}
@@ -692,84 +734,82 @@ private fun PluginInfoDialog(
plugin: PluginEntry,
logoPath: Path?,
onDismiss: () -> Unit) {
- MillenniumDialog(
- visible = true,
- onDismissRequest = onDismiss,
- title = plugin.name) {
- Box(
- modifier = Modifier
- .size(48.dp)
- .clip(RoundedCornerShape(12.dp))
- .background(AppTheme.colors.primary.copy(alpha = 0.12f))
- ) {
- PluginLogo(
- plugin = plugin,
- logoPath = logoPath,
- contentDescription = plugin.name,
- modifier = Modifier.fillMaxSize(),
- fallback = {
- Box(contentAlignment = Alignment.Center) {
- MillenniumIcon(
- Iconsax.IconsaxBox,
- contentDescription = null,
- modifier = Modifier.size(24.dp),
- tint = AppTheme.colors.primary
- )
+ AdaptiveDialogBottomSheet(
+ onDismiss = onDismiss,
+ title = { PluginDialogTitle(stringResource(Res.string.plugin_info_dialog_title)) },
+ dialogTitle = stringResource(Res.string.plugin_info_dialog_title),
+ actions = {
+ PluginDialogActions(
+ secondaryLabel = stringResource(Res.string.common_close),
+ onSecondary = onDismiss)
+ }) {
+ PluginDialogBody {
+ PluginPanel {
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(14.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(14.dp)) {
+ PluginLogoTile(plugin = plugin, logoPath = logoPath)
+ Column(
+ modifier = Modifier.weight(1f),
+ verticalArrangement = Arrangement.spacedBy(6.dp)) {
+ Text(
+ plugin.name,
+ style = AppTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis)
+ Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ PluginPill(plugin.author, icon = Iconsax.User)
+ PluginPill(
+ stringResource(Res.string.plugin_version_label, plugin.version),
+ tint = AppTheme.colors.primary)
+ }
}
- })
- }
- Column(
- verticalArrangement = Arrangement.spacedBy(12.dp),
- modifier = Modifier.fillMaxWidth()
- ) {
- if (plugin.description.isNotBlank()) {
- Text(
- plugin.description,
- style = AppTheme.typography.bodyMedium,
- color = AppTheme.colors.onSurface
- )
- }
-
- DetailRow(stringResource(Res.string.plugin_info_version), plugin.version)
- DetailRow(stringResource(Res.string.plugin_info_api_version), plugin.apiVersion)
- DetailRow(stringResource(Res.string.plugin_info_author), plugin.author)
- if (plugin.license.isNotBlank()) {
- DetailRow(stringResource(Res.string.plugin_info_license), plugin.license)
- }
-
- if (plugin.capabilities.isNotEmpty()) {
- DetailChipsRow(
- stringResource(Res.string.plugin_info_capabilities),
- plugin.capabilities.map { it.name })
- }
-
- if (plugin.abilities.isNotEmpty()) {
- DetailChipsRow(
- stringResource(Res.string.plugin_info_abilities),
- plugin.abilities.map { ability -> ability.displayLabel() })
- }
-
- if (plugin.repository.isNotBlank()) {
- ClickableDetailRow(label = stringResource(Res.string.plugin_info_repository), value = plugin.repository) {
- openUrlInBrowser(plugin.repository)
+ }
+ if (plugin.description.isNotBlank()) {
+ Text(
+ plugin.description,
+ modifier = Modifier.padding(start = 14.dp, end = 14.dp, bottom = 14.dp),
+ style = AppTheme.typography.bodySmall,
+ color = AppTheme.colors.onSurfaceVariant)
}
}
- if (plugin.contact.isNotBlank()) {
- DetailRow(stringResource(Res.string.plugin_info_contact), plugin.contact)
- }
-
- if (plugin.bugs.isNotBlank()) {
- ClickableDetailRow(label = stringResource(Res.string.plugin_info_report_bugs), value = plugin.bugs) {
- openUrlInBrowser(plugin.bugs)
+ PluginPanel {
+ Column(
+ modifier = Modifier.fillMaxWidth().padding(14.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)) {
+ DetailRow(stringResource(Res.string.plugin_info_api_version), plugin.apiVersion)
+ if (plugin.license.isNotBlank()) {
+ DetailRow(stringResource(Res.string.plugin_info_license), plugin.license)
+ }
+ if (plugin.capabilities.isNotEmpty()) {
+ DetailChipsRow(
+ stringResource(Res.string.plugin_info_capabilities),
+ plugin.capabilities.map { it.name })
+ }
+ if (plugin.abilities.isNotEmpty()) {
+ DetailChipsRow(
+ stringResource(Res.string.plugin_info_abilities),
+ plugin.abilities.map { ability -> ability.displayLabel() })
+ }
+ if (plugin.repository.isNotBlank()) {
+ ClickableDetailRow(label = stringResource(Res.string.plugin_info_repository), value = plugin.repository) {
+ openUrlInBrowser(plugin.repository)
+ }
+ }
+ if (plugin.contact.isNotBlank()) {
+ DetailRow(stringResource(Res.string.plugin_info_contact), plugin.contact)
+ }
+ if (plugin.bugs.isNotBlank()) {
+ ClickableDetailRow(label = stringResource(Res.string.plugin_info_report_bugs), value = plugin.bugs) {
+ openUrlInBrowser(plugin.bugs)
+ }
+ }
}
}
}
- Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
- MillenniumButton(onClick = onDismiss) {
- Text(stringResource(Res.string.common_close))
- }
- }
}
}
@@ -779,27 +819,30 @@ private fun SupportDialog(
supportText: String?,
isLoading: Boolean,
onDismiss: () -> Unit) {
- MillenniumDialog(
- visible = true,
- onDismissRequest = onDismiss,
- title = stringResource(Res.string.plugin_support_dialog_title, plugin.name)) {
- if (isLoading) {
- Box(
- modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp),
- contentAlignment = Alignment.Center
- ) {
- MillenniumIndeterminateProgress(modifier = Modifier.widthIn(max = 160.dp))
- }
- } else {
- Text(
- supportText ?: "",
- style = AppTheme.typography.bodyMedium,
- color = AppTheme.colors.onSurface
- )
- }
- Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
- MillenniumButton(onClick = onDismiss) {
- Text(stringResource(Res.string.common_close))
+ AdaptiveDialogBottomSheet(
+ onDismiss = onDismiss,
+ title = { PluginDialogTitle(stringResource(Res.string.plugin_support_dialog_title, plugin.name)) },
+ dialogTitle = stringResource(Res.string.plugin_support_dialog_title, plugin.name),
+ actions = {
+ PluginDialogActions(
+ secondaryLabel = stringResource(Res.string.common_close),
+ onSecondary = onDismiss)
+ }) {
+ PluginDialogBody {
+ if (isLoading) {
+ Box(
+ modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp),
+ contentAlignment = Alignment.Center) {
+ MillenniumCircularProgress(size = 36.dp)
+ }
+ } else {
+ PluginPanel {
+ Text(
+ supportText ?: "",
+ modifier = Modifier.padding(14.dp),
+ style = AppTheme.typography.bodyMedium,
+ color = AppTheme.colors.onSurface)
+ }
}
}
}
@@ -972,7 +1015,7 @@ private fun DiscoverRepoCard(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
if (isInstalling) {
- MillenniumIndeterminateProgress(modifier = Modifier.size(16.dp))
+ MillenniumSpokeSpinner(size = 16.dp)
} else {
MillenniumIcon(
Iconsax.IconsaxAdd,
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginDialogParts.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginDialogParts.kt
new file mode 100644
index 00000000..dd114789
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginDialogParts.kt
@@ -0,0 +1,249 @@
+/*
+ * 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.modules.plugin.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.composeunstyled.Text
+import dev.krtirtho.spotube.core.ui.theming.AppTheme
+import dev.krtirtho.spotube.modules.plugin.PluginEntry
+import dev.krtirtho.spotube.resources.iconsax.Iconsax
+import dev.krtirtho.spotube.resources.iconsax.IconsaxBoxAdd
+import dev.krtirtho.spotube.resources.iconsax.IconsaxInformation
+import okio.Path
+import org.jetbrains.compose.resources.stringResource
+import org.spotube.millennium.composables.MillenniumButton
+import org.spotube.millennium.composables.MillenniumButtonStyle
+import org.spotube.millennium.composables.MillenniumIcon
+import org.spotube.millennium.theme.MillenniumTheme
+import org.spotube.millennium.theme.millenniumPanelBrush
+import org.spotube.millennium.theme.millenniumSurface
+
+// Shared building blocks for the plugin dialogs/sheets so they all read as one family and
+// match the panel/tile language used by the plugin cards and the rest of the app.
+
+/** Body column for plugin dialogs; the adaptive dialog/sheet around it handles scrolling. */
+@Composable
+internal fun PluginDialogBody(content: @Composable ColumnScope.() -> Unit) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(14.dp),
+ content = content)
+}
+
+/** Title for the phone bottom sheet; the desktop dialog shows the same text in its titlebar. */
+@Composable
+internal fun PluginDialogTitle(
+ title: String,
+ icon: ImageVector? = null) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ if (icon != null) {
+ PluginIconTile(icon, size = 40.dp)
+ }
+ Text(
+ title,
+ style = AppTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold,
+ color = AppTheme.colors.onSurface)
+ }
+}
+
+/** Rounded, softly bordered panel — the same surface the plugin cards use. */
+@Composable
+internal fun PluginPanel(
+ modifier: Modifier = Modifier,
+ content: @Composable ColumnScope.() -> Unit) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .millenniumSurface(
+ background = millenniumPanelBrush(MillenniumTheme.colors),
+ shape = RoundedCornerShape(14.dp),
+ borderColor = AppTheme.colors.outlineVariant,
+ borderWidth = 1.dp),
+ content = content)
+}
+
+@Composable
+internal fun PluginIconTile(
+ icon: ImageVector,
+ modifier: Modifier = Modifier,
+ size: Dp = 38.dp,
+ tint: Color = AppTheme.colors.primary) {
+ Box(
+ modifier = modifier
+ .size(size)
+ .clip(RoundedCornerShape(size / 3.4f))
+ .background(tint.copy(alpha = 0.14f)),
+ contentAlignment = Alignment.Center) {
+ MillenniumIcon(
+ imageVector = icon,
+ contentDescription = null,
+ modifier = Modifier.size(size * 0.5f),
+ tint = tint)
+ }
+}
+
+@Composable
+internal fun PluginLogoTile(
+ plugin: PluginEntry,
+ logoPath: Path?,
+ size: Dp = 56.dp) {
+ Box(
+ modifier = Modifier
+ .size(size)
+ .clip(RoundedCornerShape(size / 3.4f))
+ .background(AppTheme.colors.primary.copy(alpha = 0.12f)),
+ contentAlignment = Alignment.Center) {
+ PluginLogo(
+ plugin = plugin,
+ logoPath = logoPath,
+ contentDescription = plugin.name,
+ modifier = Modifier.fillMaxSize(),
+ fallback = {
+ MillenniumIcon(
+ imageVector = Iconsax.IconsaxBoxAdd,
+ contentDescription = null,
+ tint = AppTheme.colors.primary,
+ modifier = Modifier.size(size * 0.5f))
+ })
+ }
+}
+
+@Composable
+internal fun PluginPill(
+ text: String,
+ icon: ImageVector? = null,
+ tint: Color = AppTheme.colors.onSurfaceVariant) {
+ Row(
+ modifier = Modifier
+ .clip(RoundedCornerShape(50))
+ .background(tint.copy(alpha = 0.12f))
+ .padding(horizontal = 8.dp, vertical = 3.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(4.dp)) {
+ if (icon != null) {
+ MillenniumIcon(
+ imageVector = icon,
+ contentDescription = null,
+ modifier = Modifier.size(12.dp),
+ tint = tint)
+ }
+ Text(
+ text,
+ style = AppTheme.typography.labelSmall,
+ fontWeight = FontWeight.Medium,
+ color = tint,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis)
+ }
+}
+
+/** Tinted info strip; [isWarning] switches it to the error tone. */
+@Composable
+internal fun PluginCallout(text: String, isWarning: Boolean = false) {
+ val tint = if (isWarning) AppTheme.colors.error else AppTheme.colors.primary
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(12.dp))
+ .background(tint.copy(alpha = 0.10f))
+ .padding(12.dp),
+ verticalAlignment = Alignment.Top,
+ horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ MillenniumIcon(
+ imageVector = Iconsax.IconsaxInformation,
+ contentDescription = null,
+ modifier = Modifier.size(18.dp),
+ tint = tint)
+ Text(
+ text,
+ style = AppTheme.typography.bodySmall,
+ color = AppTheme.colors.onSurface)
+ }
+}
+
+@Composable
+internal fun PluginSectionLabel(text: String, badge: String? = null) {
+ Row(
+ modifier = Modifier.padding(horizontal = 2.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ Text(
+ text,
+ style = AppTheme.typography.labelLarge,
+ fontWeight = FontWeight.SemiBold,
+ color = AppTheme.colors.onSurface)
+ if (badge != null) {
+ PluginPill(badge, tint = AppTheme.colors.primary)
+ }
+ }
+}
+
+/** Equal-width secondary + primary action pair. */
+@Composable
+internal fun PluginDialogActions(
+ secondaryLabel: String,
+ onSecondary: () -> Unit,
+ primaryLabel: String? = null,
+ onPrimary: (() -> Unit)? = null,
+ primaryEnabled: Boolean = true) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ MillenniumButton(
+ onClick = onSecondary,
+ modifier = Modifier.weight(1f),
+ style = if (primaryLabel != null) MillenniumButtonStyle.Secondary else MillenniumButtonStyle.Primary) {
+ Text(secondaryLabel)
+ }
+ if (primaryLabel != null && onPrimary != null) {
+ MillenniumButton(
+ onClick = onPrimary,
+ enabled = primaryEnabled,
+ modifier = Modifier.weight(1f),
+ style = MillenniumButtonStyle.Primary) {
+ Text(primaryLabel)
+ }
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginInstallDialog.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginInstallDialog.kt
index f598b5c6..e8eebdef 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginInstallDialog.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginInstallDialog.kt
@@ -17,7 +17,7 @@
package dev.krtirtho.spotube.modules.plugin.components
-import androidx.compose.foundation.background
+import org.spotube.millennium.composables.MillenniumCircularProgress
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -32,10 +32,6 @@ import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
-
-import dev.krtirtho.spotube.core.ui.theming.AppTheme
-
-import com.composeunstyled.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -52,19 +48,22 @@ import coil3.compose.AsyncImage
import coil3.compose.LocalPlatformContext
import coil3.request.ImageRequest
import coil3.request.crossfade
-import dev.krtirtho.spotube.modules.plugin.GitHubRepo
+import com.composeunstyled.Text
+import dev.krtirtho.spotube.core.ui.component.AdaptiveDialogBottomSheet
+import dev.krtirtho.spotube.core.ui.theming.AppTheme
import dev.krtirtho.spotube.modules.plugin.GitHubRelease
+import dev.krtirtho.spotube.modules.plugin.GitHubRepo
import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4
-import dev.krtirtho.spotube.resources.iconsax.IconsaxBox
import org.jetbrains.compose.resources.stringResource
import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle
-import org.spotube.millennium.composables.MillenniumDialog
import org.spotube.millennium.composables.MillenniumDropdownMenu
+import org.spotube.millennium.composables.MillenniumDropdownMenuContent
+import org.spotube.millennium.composables.MillenniumDropdownMenuItem
import org.spotube.millennium.composables.MillenniumIcon
-import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import spotube.composeapp.generated.resources.Res
+import spotube.composeapp.generated.resources.common_cancel
import spotube.composeapp.generated.resources.plugin_action_install
import spotube.composeapp.generated.resources.plugin_install_no_releases
import spotube.composeapp.generated.resources.plugin_install_prerelease
@@ -88,21 +87,24 @@ fun PluginInstallDialog(
}
}
- MillenniumDialog(
- visible = true,
- onDismissRequest = onDismiss,
- title = repo.fullName) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- Box(
- modifier = Modifier
- .size(48.dp)
- .clip(RoundedCornerShape(12.dp))
- .background(AppTheme.colors.primary.copy(alpha = 0.12f))
- ) {
- Box(contentAlignment = Alignment.Center) {
+ AdaptiveDialogBottomSheet(
+ onDismiss = onDismiss,
+ title = { PluginDialogTitle(stringResource(Res.string.plugin_action_install)) },
+ dialogTitle = stringResource(Res.string.plugin_action_install),
+ actions = {
+ PluginDialogActions(
+ secondaryLabel = stringResource(Res.string.common_cancel),
+ onSecondary = onDismiss,
+ primaryLabel = stringResource(Res.string.plugin_action_install),
+ onPrimary = { selectedRelease?.let(onInstall) },
+ primaryEnabled = selectedRelease != null && !isLoadingReleases)
+ }) {
+ PluginDialogBody {
+ PluginPanel {
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(14.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(14.dp)) {
val platformContext = LocalPlatformContext.current
AsyncImage(
model = ImageRequest.Builder(platformContext)
@@ -110,131 +112,99 @@ fun PluginInstallDialog(
.crossfade(true)
.build(),
contentDescription = repo.owner.login,
- modifier = Modifier.fillMaxSize()
- )
- }
- }
- Column(modifier = Modifier.weight(1f)) {
- Text(
- repo.fullName,
- style = AppTheme.typography.titleMedium,
- fontWeight = FontWeight.SemiBold,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- if (!repo.description.isNullOrBlank()) {
- Text(
- repo.description,
- style = AppTheme.typography.bodySmall,
- color = AppTheme.colors.onSurfaceVariant,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis
- )
- }
- }
- }
-
- if (isLoadingReleases) {
- Box(
- modifier = Modifier.fillMaxWidth().height(120.dp),
- contentAlignment = Alignment.Center
- ) {
- MillenniumIndeterminateProgress(modifier = Modifier.widthIn(max = 160.dp))
- }
- } else if (releases.isEmpty()) {
- Text(
- stringResource(Res.string.plugin_install_no_releases),
- style = AppTheme.typography.bodyMedium,
- color = AppTheme.colors.onSurfaceVariant
- )
- } else {
- Text(
- stringResource(Res.string.plugin_install_select_release_label),
- style = AppTheme.typography.labelMedium,
- fontWeight = FontWeight.SemiBold,
- color = AppTheme.colors.onSurface
- )
-
- MillenniumDropdownMenu(
- expanded = expanded,
- onExpandedChange = { expanded = it },
- items = releases.map { it.tagName },
- onItemClick = { tagName ->
- selectedRelease = releases.firstOrNull { it.tagName == tagName }
- expanded = false
- },
- modifier = Modifier.fillMaxWidth(),
- anchor = {
- MillenniumButton(
- onClick = { expanded = true },
- modifier = Modifier.fillMaxWidth(),
- style = MillenniumButtonStyle.Secondary
- ) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
+ modifier = Modifier.size(56.dp).clip(RoundedCornerShape(16.dp)))
+ Column(
+ modifier = Modifier.weight(1f),
+ verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ Text(
+ repo.fullName,
+ style = AppTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis)
+ if (!repo.description.isNullOrBlank()) {
Text(
- selectedRelease?.tagName
- ?: stringResource(Res.string.plugin_install_select_release_placeholder),
- style = AppTheme.typography.bodyMedium
- )
- MillenniumIcon(
- imageVector = Iconsax.IconsaxArrowDown4,
- contentDescription = null,
- modifier = Modifier.size(16.dp)
- )
- }
- }
- }
- )
-
- selectedRelease?.let { release ->
- if (release.prerelease) {
- Text(
- stringResource(Res.string.plugin_install_prerelease),
- style = AppTheme.typography.labelSmall,
- color = AppTheme.colors.error
- )
- }
- if (!release.body.isNullOrBlank()) {
- Text(
- stringResource(Res.string.plugin_install_release_notes),
- style = AppTheme.typography.labelMedium,
- fontWeight = FontWeight.SemiBold,
- color = AppTheme.colors.onSurface
- )
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(8.dp))
- .background(AppTheme.colors.surfaceVariant.copy(alpha = 0.5f))
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .heightIn(max = 200.dp)
- .verticalScroll(rememberScrollState())
- .padding(12.dp)
- ) {
- Text(
- release.body,
+ repo.description,
style = AppTheme.typography.bodySmall,
- color = AppTheme.colors.onSurfaceVariant
- )
+ color = AppTheme.colors.onSurfaceVariant,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis)
}
}
}
}
- }
- Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
- MillenniumButton(
- onClick = { selectedRelease?.let { onInstall(it) } },
- enabled = selectedRelease != null && !isLoadingReleases
- ) {
- Text(stringResource(Res.string.plugin_action_install))
+ if (isLoadingReleases) {
+ Box(
+ modifier = Modifier.fillMaxWidth().height(96.dp),
+ contentAlignment = Alignment.Center) {
+ MillenniumCircularProgress(size = 36.dp)
+ }
+ } else if (releases.isEmpty()) {
+ PluginCallout(stringResource(Res.string.plugin_install_no_releases), isWarning = true)
+ } else {
+ PluginSectionLabel(stringResource(Res.string.plugin_install_select_release_label))
+ MillenniumDropdownMenu(
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ modifier = Modifier.fillMaxWidth(),
+ anchor = {
+ MillenniumButton(
+ onClick = { expanded = true },
+ modifier = Modifier.fillMaxWidth(),
+ style = MillenniumButtonStyle.Secondary) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween) {
+ Text(
+ selectedRelease?.tagName
+ ?: stringResource(Res.string.plugin_install_select_release_placeholder),
+ style = AppTheme.typography.bodyMedium)
+ MillenniumIcon(
+ imageVector = Iconsax.IconsaxArrowDown4,
+ contentDescription = null,
+ modifier = Modifier.size(16.dp))
+ }
+ }
+ }) {
+ MillenniumDropdownMenuContent {
+ releases.forEach { release ->
+ MillenniumDropdownMenuItem(
+ label = release.tagName,
+ selected = release.tagName == selectedRelease?.tagName,
+ description = if (release.prerelease) {
+ stringResource(Res.string.plugin_install_prerelease)
+ } else null,
+ onClick = {
+ selectedRelease = release
+ expanded = false
+ })
+ }
+ }
+ }
+
+ selectedRelease?.let { release ->
+ if (release.prerelease) {
+ PluginCallout(stringResource(Res.string.plugin_install_prerelease), isWarning = true)
+ }
+ if (!release.body.isNullOrBlank()) {
+ PluginSectionLabel(stringResource(Res.string.plugin_install_release_notes))
+ PluginPanel {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(max = 200.dp)
+ .verticalScroll(rememberScrollState())
+ .padding(12.dp)) {
+ Text(
+ release.body,
+ style = AppTheme.typography.bodySmall,
+ color = AppTheme.colors.onSurfaceVariant)
+ }
+ }
+ }
+ }
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginPermissionsDialog.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginPermissionsDialog.kt
index 5c385c41..bf2754d9 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginPermissionsDialog.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginPermissionsDialog.kt
@@ -17,47 +17,39 @@
package dev.krtirtho.spotube.modules.plugin.components
-
-import org.spotube.millennium.composables.MillenniumButton
-import org.spotube.millennium.composables.MillenniumButtonStyle
-import org.spotube.millennium.composables.MillenniumCard
-import org.spotube.millennium.composables.MillenniumDialog
-import org.spotube.millennium.composables.MillenniumIcon
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-
-import dev.krtirtho.spotube.core.ui.theming.AppTheme
-import com.composeunstyled.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
-import okio.Path
+import com.composeunstyled.Text
+import compose.icons.FeatherIcons
+import compose.icons.feathericons.ArrowRight
+import dev.krtirtho.spotube.core.ui.component.AdaptiveDialogBottomSheet
+import dev.krtirtho.spotube.core.ui.theming.AppTheme
import dev.krtirtho.spotube.modules.plugin.PluginCapability
import dev.krtirtho.spotube.modules.plugin.PluginEntry
import dev.krtirtho.spotube.resources.iconsax.Iconsax
-import dev.krtirtho.spotube.resources.iconsax.IconsaxBoxAdd
+import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckCircle
import dev.krtirtho.spotube.resources.iconsax.IconsaxEye
import dev.krtirtho.spotube.resources.iconsax.IconsaxWifiSquare
import dev.krtirtho.spotube.resources.iconsax.PhosphorDatabaseDuotone
import dev.krtirtho.spotube.resources.iconsax.User
+import okio.Path
import org.jetbrains.compose.resources.stringResource
+import org.spotube.millennium.composables.MillenniumIcon
+import org.spotube.millennium.composables.MillenniumSeparator
import spotube.composeapp.generated.resources.Res
+import spotube.composeapp.generated.resources.common_cancel
+import spotube.composeapp.generated.resources.common_close
import spotube.composeapp.generated.resources.plugin_permissions_api_diff
-import spotube.composeapp.generated.resources.plugin_permissions_author_version
import spotube.composeapp.generated.resources.plugin_permissions_capability_network_desc
import spotube.composeapp.generated.resources.plugin_permissions_capability_network_title
import spotube.composeapp.generated.resources.plugin_permissions_capability_storage_desc
@@ -70,8 +62,6 @@ import spotube.composeapp.generated.resources.plugin_permissions_none
import spotube.composeapp.generated.resources.plugin_permissions_requested_title
import spotube.composeapp.generated.resources.plugin_permissions_supplied_label
import spotube.composeapp.generated.resources.plugin_version_label
-import spotube.composeapp.generated.resources.common_cancel
-import spotube.composeapp.generated.resources.common_close
@Composable
fun PluginPermissionDialog(
@@ -83,129 +73,80 @@ fun PluginPermissionDialog(
logoPath: Path? = null,
onConfirm: (() -> Unit)? = null,
onDismiss: () -> Unit) {
- MillenniumDialog(
- visible = true,
- onDismissRequest = onDismiss,
- title = title) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(14.dp)
- ) {
- Box(
- modifier = Modifier
- .size(48.dp)
- .clip(RoundedCornerShape(12.dp))
- .background(AppTheme.colors.primary.copy(alpha = 0.12f)),
- contentAlignment = Alignment.Center
- ) {
- PluginLogo(
- plugin = pluginInfo,
- logoPath = logoPath,
- contentDescription = pluginInfo.name,
- modifier = Modifier.fillMaxSize(),
- fallback = {
- MillenniumIcon(
- imageVector = Iconsax.IconsaxBoxAdd,
- contentDescription = null,
- tint = AppTheme.colors.primary,
- modifier = Modifier.size(24.dp)
- )
- })
- }
- Column(modifier = Modifier.weight(1f)) {
- Text(
- pluginInfo.name,
- style = AppTheme.typography.titleMedium,
- fontWeight = FontWeight.SemiBold,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis)
- }
- }
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(6.dp)
- ) {
- MillenniumIcon(
- Iconsax.User,
- contentDescription = null,
- modifier = Modifier.size(13.dp),
- tint = AppTheme.colors.onSurfaceVariant
- )
- Text(
- pluginInfo.author,
- style = AppTheme.typography.labelSmall,
- color = AppTheme.colors.onSurfaceVariant
- )
- Text(
- stringResource(
- Res.string.plugin_permissions_author_version,
- stringResource(Res.string.plugin_version_label, pluginInfo.version)
- ),
- style = AppTheme.typography.labelSmall,
- color = AppTheme.colors.onSurfaceVariant
- )
- }
-
- if (pluginInfo.description.isNotBlank()) {
- Text(
- pluginInfo.description,
- style = AppTheme.typography.bodySmall,
- color = AppTheme.colors.onSurfaceVariant,
- maxLines = 3,
- overflow = TextOverflow.Ellipsis
- )
- }
-
- MillenniumCard(contentPadding = PaddingValues(14.dp)) {
- Text(
- message,
- style = AppTheme.typography.bodySmall,
- color = AppTheme.colors.onSurface
- )
- }
-
- existingPlugin?.let {
- InstalledComparisonCard(
- installedPlugin = it,
- incomingPlugin = pluginInfo
- )
- }
-
- Text(
- stringResource(Res.string.plugin_permissions_requested_title),
- style = AppTheme.typography.labelMedium,
- fontWeight = FontWeight.SemiBold,
- color = AppTheme.colors.onSurface
- )
-
- if (pluginInfo.capabilities.isEmpty()) {
- Text(
- stringResource(Res.string.plugin_permissions_none),
- style = AppTheme.typography.bodySmall,
- color = AppTheme.colors.onSurfaceVariant
- )
- } else {
- pluginInfo.capabilities.forEach { capability ->
- CapabilityRow(capability)
- }
- }
-
- Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
- MillenniumButton(
- onClick = onDismiss,
- style = MillenniumButtonStyle.Secondary) {
- Text(
- if (confirmLabel == null) {
- stringResource(Res.string.common_close)
- } else {
- stringResource(Res.string.common_cancel)
+ AdaptiveDialogBottomSheet(
+ onDismiss = onDismiss,
+ title = { PluginDialogTitle(title) },
+ dialogTitle = title,
+ actions = {
+ PluginDialogActions(
+ secondaryLabel = stringResource(
+ if (confirmLabel == null) Res.string.common_close else Res.string.common_cancel),
+ onSecondary = onDismiss,
+ primaryLabel = confirmLabel,
+ onPrimary = onConfirm)
+ }) {
+ PluginDialogBody {
+ PluginPanel {
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(14.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(14.dp)) {
+ PluginLogoTile(plugin = pluginInfo, logoPath = logoPath)
+ Column(
+ modifier = Modifier.weight(1f),
+ verticalArrangement = Arrangement.spacedBy(6.dp)) {
+ Text(
+ pluginInfo.name,
+ style = AppTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis)
+ Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ PluginPill(pluginInfo.author, icon = Iconsax.User)
+ PluginPill(
+ stringResource(Res.string.plugin_version_label, pluginInfo.version),
+ tint = AppTheme.colors.primary)
+ }
}
- )
+ }
+ if (pluginInfo.description.isNotBlank()) {
+ Text(
+ pluginInfo.description,
+ modifier = Modifier.padding(start = 14.dp, end = 14.dp, bottom = 14.dp),
+ style = AppTheme.typography.bodySmall,
+ color = AppTheme.colors.onSurfaceVariant,
+ maxLines = 3,
+ overflow = TextOverflow.Ellipsis)
+ }
}
- if (confirmLabel != null && onConfirm != null) {
- MillenniumButton(onClick = onConfirm) {
- Text(confirmLabel)
+
+ PluginCallout(text = message, isWarning = confirmLabel == null)
+
+ existingPlugin?.let {
+ InstalledComparisonPanel(installedPlugin = it, incomingPlugin = pluginInfo)
+ }
+
+ PluginSectionLabel(
+ stringResource(Res.string.plugin_permissions_requested_title),
+ badge = pluginInfo.capabilities.size.takeIf { it > 0 }?.toString())
+
+ PluginPanel {
+ if (pluginInfo.capabilities.isEmpty()) {
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(14.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ PluginIconTile(Iconsax.IconsaxCheckCircle)
+ Text(
+ stringResource(Res.string.plugin_permissions_none),
+ style = AppTheme.typography.bodySmall,
+ color = AppTheme.colors.onSurfaceVariant)
+ }
+ } else {
+ pluginInfo.capabilities.forEachIndexed { index, capability ->
+ if (index > 0) MillenniumSeparator()
+ CapabilityRow(capability)
+ }
}
}
}
@@ -213,57 +154,58 @@ fun PluginPermissionDialog(
}
@Composable
-private fun InstalledComparisonCard(
+private fun InstalledComparisonPanel(
installedPlugin: PluginEntry,
incomingPlugin: PluginEntry) {
- MillenniumCard(
- modifier = Modifier.fillMaxWidth(),
- contentPadding = PaddingValues(14.dp)) {
- Column(
- modifier = Modifier.fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- Text(
- stringResource(Res.string.plugin_permissions_compare_title),
- style = AppTheme.typography.labelMedium,
- fontWeight = FontWeight.SemiBold)
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- VersionStat(
- label = stringResource(Res.string.plugin_permissions_installed_label),
- value = installedPlugin.version)
- VersionStat(
- label = stringResource(Res.string.plugin_permissions_supplied_label),
- value = incomingPlugin.version)
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ PluginSectionLabel(stringResource(Res.string.plugin_permissions_compare_title))
+ PluginPanel {
+ Column(
+ modifier = Modifier.fillMaxWidth().padding(14.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceEvenly) {
+ VersionStat(
+ label = stringResource(Res.string.plugin_permissions_installed_label),
+ value = installedPlugin.version)
+ MillenniumIcon(
+ imageVector = FeatherIcons.ArrowRight,
+ contentDescription = null,
+ modifier = Modifier.size(18.dp),
+ tint = AppTheme.colors.onSurfaceVariant)
+ VersionStat(
+ label = stringResource(Res.string.plugin_permissions_supplied_label),
+ value = incomingPlugin.version,
+ highlighted = true)
+ }
+ Text(
+ stringResource(
+ Res.string.plugin_permissions_api_diff,
+ installedPlugin.apiVersion,
+ incomingPlugin.apiVersion),
+ style = AppTheme.typography.labelSmall,
+ color = AppTheme.colors.onSurfaceVariant)
}
- Text(
- stringResource(
- Res.string.plugin_permissions_api_diff,
- installedPlugin.apiVersion,
- incomingPlugin.apiVersion
- ),
- style = AppTheme.typography.labelSmall,
- color = AppTheme.colors.onSurfaceVariant
- )
}
}
}
@Composable
-private fun VersionStat(label: String, value: String) {
- Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
+private fun VersionStat(label: String, value: String, highlighted: Boolean = false) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
label,
style = AppTheme.typography.labelSmall,
- color = AppTheme.colors.onSurfaceVariant
- )
+ color = AppTheme.colors.onSurfaceVariant)
Text(
stringResource(Res.string.plugin_version_label, value),
- style = AppTheme.typography.bodyMedium,
- fontWeight = FontWeight.Medium
- )
+ style = AppTheme.typography.titleSmall,
+ fontWeight = FontWeight.SemiBold,
+ color = if (highlighted) AppTheme.colors.primary else AppTheme.colors.onSurface)
}
}
@@ -273,54 +215,31 @@ private fun CapabilityRow(capability: PluginCapability) {
PluginCapability.PERSISTENT_STORAGE -> Triple(
Iconsax.PhosphorDatabaseDuotone,
stringResource(Res.string.plugin_permissions_capability_storage_title),
- stringResource(Res.string.plugin_permissions_capability_storage_desc)
- )
+ stringResource(Res.string.plugin_permissions_capability_storage_desc))
PluginCapability.NETWORK_REQUESTS -> Triple(
Iconsax.IconsaxWifiSquare,
stringResource(Res.string.plugin_permissions_capability_network_title),
- stringResource(Res.string.plugin_permissions_capability_network_desc)
- )
+ stringResource(Res.string.plugin_permissions_capability_network_desc))
PluginCapability.WEBVIEW -> Triple(
Iconsax.IconsaxEye,
stringResource(Res.string.plugin_permissions_capability_webview_title),
- stringResource(Res.string.plugin_permissions_capability_webview_desc)
- )
+ stringResource(Res.string.plugin_permissions_capability_webview_desc))
}
- MillenniumCard(
- modifier = Modifier.fillMaxWidth(),
- contentPadding = PaddingValues(12.dp)) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(14.dp)
- ) {
- Box(
- modifier = Modifier
- .size(34.dp)
- .clip(CircleShape)
- .background(AppTheme.colors.errorContainer.copy(alpha = 0.6f)),
- contentAlignment = Alignment.Center
- ) {
- MillenniumIcon(
- imageVector = icon,
- contentDescription = null,
- modifier = Modifier.size(16.dp),
- tint = AppTheme.colors.error
- )
- }
- Column {
- Text(
- label,
- style = AppTheme.typography.bodyMedium,
- fontWeight = FontWeight.Medium
- )
- Text(
- description,
- style = AppTheme.typography.labelSmall,
- color = AppTheme.colors.onSurfaceVariant
- )
- }
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ PluginIconTile(icon)
+ Column(verticalArrangement = Arrangement.spacedBy(1.dp)) {
+ Text(
+ label,
+ style = AppTheme.typography.bodyMedium,
+ fontWeight = FontWeight.Medium)
+ Text(
+ description,
+ style = AppTheme.typography.labelSmall,
+ color = AppTheme.colors.onSurfaceVariant)
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreen.kt
index 24d466c6..7352a399 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreen.kt
@@ -18,10 +18,10 @@
package dev.krtirtho.spotube.modules.search
+import org.spotube.millennium.composables.MillenniumMarqueeProgress
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle
-import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.composables.MillenniumListTile
import org.spotube.millennium.composables.MillenniumAutocomplete
import org.spotube.millennium.composables.MillenniumText
@@ -1155,7 +1155,7 @@ private fun SearchLoadingIndicator(
translationY = bounce
})
}
- MillenniumIndeterminateProgress(
+ MillenniumMarqueeProgress(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp, vertical = 16.dp)
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsSidebarLayout.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsSidebarLayout.kt
index 40095a64..1316530f 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsSidebarLayout.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsSidebarLayout.kt
@@ -18,10 +18,10 @@
package dev.krtirtho.spotube.modules.settings
+import org.spotube.millennium.composables.MillenniumCircularProgress
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle
-import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.composables.MillenniumSeparator
import org.spotube.millennium.composables.MillenniumSidebarItem
import org.spotube.millennium.theme.MillenniumTheme
@@ -288,7 +288,7 @@ private fun SettingsTabContent(
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center) {
- MillenniumIndeterminateProgress()
+ MillenniumCircularProgress(size = 40.dp)
}
} else {
Box(modifier = Modifier.fillMaxSize()) {
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/components/InteractiveSettingCards.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/components/InteractiveSettingCards.kt
index abdecb8c..ba54f3bf 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/components/InteractiveSettingCards.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/components/InteractiveSettingCards.kt
@@ -18,6 +18,12 @@
package dev.krtirtho.spotube.modules.settings.components
+import org.spotube.millennium.composables.MillenniumDialogHeader
+import org.spotube.millennium.composables.MillenniumDialogTitle
+import org.spotube.millennium.composables.MillenniumDialogClose
+import org.spotube.millennium.composables.MillenniumDialogBody
+import org.spotube.millennium.composables.MillenniumDialogFooter
+import org.spotube.millennium.composables.MillenniumDialogDescription
import androidx.compose.foundation.layout.Box
import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.adaptive.WindowBreakpoints
@@ -238,26 +244,30 @@ internal fun TextInputSettingCard(
val normalizedValue = normalize(draft)
val errorMessage = validate(normalizedValue)
- MillenniumDialog(
- visible = true,
- onDismissRequest = { isDialogOpen = false },
- title = dialogTitle
- ) {
- if (dialogDescription != null) {
- Text(
- text = dialogDescription,
- style = AppTheme.typography.bodySmall,
- color = AppTheme.colors.onSurfaceVariant
+ MillenniumDialog(visible = true, onDismissRequest = { isDialogOpen = false }) {
+ MillenniumDialogHeader {
+ MillenniumDialogTitle(dialogTitle)
+ MillenniumDialogClose()
+ }
+ MillenniumDialogBody {
+ if (dialogDescription != null) {
+ MillenniumDialogDescription(dialogDescription)
+ }
+ MillenniumTextField(
+ state = dialogState,
+ label = title,
+ modifier = Modifier.fillMaxWidth(),
+ placeholder = placeholder,
+ error = errorMessage
)
}
- MillenniumTextField(
- state = dialogState,
- label = title,
- modifier = Modifier.fillMaxWidth(),
- placeholder = placeholder,
- error = errorMessage
- )
- Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ MillenniumDialogFooter {
+ MillenniumButton(
+ onClick = { isDialogOpen = false },
+ style = MillenniumButtonStyle.Secondary
+ ) {
+ Text(stringResource(Res.string.common_cancel))
+ }
MillenniumButton(
onClick = {
if (errorMessage == null) {
@@ -269,12 +279,6 @@ internal fun TextInputSettingCard(
) {
Text(stringResource(Res.string.common_save))
}
- MillenniumButton(
- onClick = { isDialogOpen = false },
- style = MillenniumButtonStyle.Secondary
- ) {
- Text(stringResource(Res.string.common_cancel))
- }
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/AppearanceSection.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/AppearanceSection.kt
index 7337ad09..b3d4d0e6 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/AppearanceSection.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/AppearanceSection.kt
@@ -17,6 +17,11 @@
package dev.krtirtho.spotube.modules.settings.sections
+import org.spotube.millennium.composables.MillenniumDialogHeader
+import org.spotube.millennium.composables.MillenniumDialogTitle
+import org.spotube.millennium.composables.MillenniumDialogClose
+import org.spotube.millennium.composables.MillenniumDialogBody
+import org.spotube.millennium.composables.MillenniumDialogFooter
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@@ -151,10 +156,12 @@ private fun AppearanceSettingCard(
mutableStateOf(selectedPalette)
}
- MillenniumDialog(
- visible = true,
- onDismissRequest = { isDialogOpen = false },
- title = stringResource(Res.string.settings_palette_dialog_title)) {
+ MillenniumDialog(visible = true, onDismissRequest = { isDialogOpen = false }) {
+ MillenniumDialogHeader {
+ MillenniumDialogTitle(stringResource(Res.string.settings_palette_dialog_title))
+ MillenniumDialogClose()
+ }
+ MillenniumDialogBody {
MillenniumText(
stringResource(Res.string.settings_palette_dialog_description),
color = AppTheme.colors.onSurfaceVariant)
@@ -174,9 +181,13 @@ private fun AppearanceSettingCard(
}
PalettePreview(draftPalette)
-
- MillenniumSeparator()
- Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ }
+ MillenniumDialogFooter {
+ MillenniumButton(
+ onClick = { isDialogOpen = false },
+ style = MillenniumButtonStyle.Secondary) {
+ MillenniumText(stringResource(Res.string.common_cancel))
+ }
MillenniumButton(
onClick = {
onSaved(draftPalette)
@@ -184,11 +195,6 @@ private fun AppearanceSettingCard(
}) {
MillenniumText(stringResource(Res.string.common_save))
}
- MillenniumButton(
- onClick = { isDialogOpen = false },
- style = MillenniumButtonStyle.Secondary) {
- MillenniumText(stringResource(Res.string.common_cancel))
- }
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppLargePlayer.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppLargePlayer.kt
index 1586083d..09f28848 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppLargePlayer.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppLargePlayer.kt
@@ -18,6 +18,10 @@
package dev.krtirtho.spotube.modules.shell
+import org.spotube.millennium.composables.MillenniumDropdownMenuItem
+import dev.krtirtho.spotube.resources.iconsax.IconsaxCd
+import dev.krtirtho.spotube.resources.iconsax.IconsaxMoon
+import dev.krtirtho.spotube.resources.iconsax.IconsaxInformation
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -436,27 +440,6 @@ fun AppLargePlayer(
onExpandedChange = { expanded ->
if (!expanded) playerOptionsViewModel.dismissMoreOptionsMenu()
},
- items = listOf(goToAlbumLabel, sleepTimerLabel, trackDetailsLabel),
- onItemClick = { label ->
- when (label) {
- goToAlbumLabel -> {
- playerOptionsViewModel.dismissMoreOptionsMenu()
- currentTrack?.album?.id?.let { albumId ->
- navigationCommands.navigateTo(Routes.Album(albumId))
- }
- }
-
- sleepTimerLabel -> {
- playerOptionsViewModel.dismissMoreOptionsMenu()
- playerOptionsViewModel.showSleepTimerDialog()
- }
-
- trackDetailsLabel -> {
- playerOptionsViewModel.dismissMoreOptionsMenu()
- playerOptionsViewModel.showTrackDetails()
- }
- }
- },
side = AnchorSide.Top,
anchor = {
MillenniumIconButton(
@@ -471,7 +454,31 @@ fun AppLargePlayer(
contentDescription = stringResource(Res.string.player_more_options_content_description)
)
}
- })
+ }) {
+ MillenniumDropdownMenuItem(
+ label = goToAlbumLabel,
+ icon = Iconsax.IconsaxCd,
+ onClick = {
+ playerOptionsViewModel.dismissMoreOptionsMenu()
+ currentTrack?.album?.id?.let { albumId ->
+ navigationCommands.navigateTo(Routes.Album(albumId))
+ }
+ })
+ MillenniumDropdownMenuItem(
+ label = sleepTimerLabel,
+ icon = Iconsax.IconsaxMoon,
+ onClick = {
+ playerOptionsViewModel.dismissMoreOptionsMenu()
+ playerOptionsViewModel.showSleepTimerDialog()
+ })
+ MillenniumDropdownMenuItem(
+ label = trackDetailsLabel,
+ icon = Iconsax.IconsaxInformation,
+ onClick = {
+ playerOptionsViewModel.dismissMoreOptionsMenu()
+ playerOptionsViewModel.showTrackDetails()
+ })
+ }
}
Row(verticalAlignment = Alignment.CenterVertically) {
MillenniumIconButton(
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 e89b40bb..e613d25b 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
@@ -21,6 +21,7 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -166,7 +167,11 @@ fun AppSidebar(
}
}
}
- MillenniumSidebarFooter {
+ // Compact the footer chrome only (padding + gap between items); items keep their size.
+ MillenniumSidebarFooter(
+ contentPadding = PaddingValues(horizontal = 6.dp, vertical = 4.dp),
+ contentSpacing = 2.dp
+ ) {
val supportLabel = stringResource(Res.string.support_button_label)
MillenniumSidebarItem(
label = supportLabel,
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerOptionDialogs.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerOptionDialogs.kt
index 3529f6ab..d8098f6d 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerOptionDialogs.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerOptionDialogs.kt
@@ -18,11 +18,16 @@
package dev.krtirtho.spotube.modules.shell
+import org.spotube.millennium.composables.MillenniumDialogHeader
+import org.spotube.millennium.composables.MillenniumDialogTitle
+import org.spotube.millennium.composables.MillenniumDialogClose
+import org.spotube.millennium.composables.MillenniumDialogBody
+import org.spotube.millennium.composables.MillenniumDialogFooter
+import org.spotube.millennium.composables.MillenniumMarqueeProgress
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle
import org.spotube.millennium.composables.MillenniumDialog
-import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.composables.MillenniumTextField
import org.spotube.millennium.theme.millenniumSurface
import androidx.compose.foundation.text.input.rememberTextFieldState
@@ -128,11 +133,12 @@ private fun SleepTimerDialog(
onStart: () -> Unit,
onCancelTimer: () -> Unit,
onDismiss: () -> Unit) {
- MillenniumDialog(
- visible = true,
- onDismissRequest = onDismiss,
- title = stringResource(Res.string.player_sleep_timer)) {
- Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ MillenniumDialog(visible = true, onDismissRequest = onDismiss) {
+ MillenniumDialogHeader {
+ MillenniumDialogTitle(stringResource(Res.string.player_sleep_timer))
+ MillenniumDialogClose()
+ }
+ MillenniumDialogBody {
if (remainingMs != null) {
Text(
text = stringResource(Res.string.player_sleep_timer_remaining, formatRemainingTime(remainingMs)),
@@ -152,21 +158,20 @@ private fun SleepTimerDialog(
label = stringResource(Res.string.player_sleep_timer_minutes_label),
placeholder = stringResource(Res.string.player_sleep_timer_minutes_placeholder),
modifier = Modifier.fillMaxWidth())
-
- Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
- if (remainingMs != null) {
- MillenniumButton(
- onClick = onCancelTimer,
- style = MillenniumButtonStyle.Secondary) { Text(stringResource(Res.string.player_sleep_timer_cancel)) }
- }
+ }
+ MillenniumDialogFooter {
+ if (remainingMs != null) {
MillenniumButton(
- onClick = onDismiss,
- style = MillenniumButtonStyle.Secondary) { Text(stringResource(Res.string.common_close)) }
- MillenniumButton(
- onClick = onStart,
- enabled = minutes.toLongOrNull()?.let { it in 1L..600L } == true) {
- Text(stringResource(Res.string.player_sleep_timer_start))
- }
+ onClick = onCancelTimer,
+ style = MillenniumButtonStyle.Quiet) { Text(stringResource(Res.string.player_sleep_timer_cancel)) }
+ }
+ MillenniumButton(
+ onClick = onDismiss,
+ style = MillenniumButtonStyle.Secondary) { Text(stringResource(Res.string.common_close)) }
+ MillenniumButton(
+ onClick = onStart,
+ enabled = minutes.toLongOrNull()?.let { it in 1L..600L } == true) {
+ Text(stringResource(Res.string.player_sleep_timer_start))
}
}
}
@@ -176,11 +181,12 @@ private fun SleepTimerDialog(
private fun TrackDetailsDialog(
details: PlayerTrackDetails,
onDismiss: () -> Unit) {
- MillenniumDialog(
- visible = true,
- onDismissRequest = onDismiss,
- title = stringResource(Res.string.player_track_details)) {
- Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
+ MillenniumDialog(visible = true, onDismissRequest = onDismiss) {
+ MillenniumDialogHeader {
+ MillenniumDialogTitle(stringResource(Res.string.player_track_details))
+ MillenniumDialogClose()
+ }
+ MillenniumDialogBody(contentSpacing = 14.dp) {
Text(
details.track.title,
style = AppTheme.typography.bodyMedium,
@@ -189,8 +195,7 @@ private fun TrackDetailsDialog(
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 20.dp),
horizontalArrangement = Arrangement.Center) {
- MillenniumIndeterminateProgress(
- modifier = Modifier.fillMaxWidth())
+ MillenniumMarqueeProgress(modifier = Modifier.fillMaxWidth())
}
}
@@ -315,6 +320,8 @@ private fun TrackDetailsDialog(
DetailRow(stringResource(Res.string.player_track_details_active_url), activeUrl)
}
+ }
+ MillenniumDialogFooter {
MillenniumButton(
onClick = onDismiss,
style = MillenniumButtonStyle.Secondary) { Text(stringResource(Res.string.common_close)) }
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/alternative_track/AlternativeTrackContent.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/alternative_track/AlternativeTrackContent.kt
index 129b6ea1..d1303959 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/alternative_track/AlternativeTrackContent.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/alternative_track/AlternativeTrackContent.kt
@@ -18,8 +18,8 @@
package dev.krtirtho.spotube.modules.shell.alternative_track
+import org.spotube.millennium.composables.MillenniumSpokeSpinner
import org.spotube.millennium.composables.MillenniumIcon
-import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.composables.MillenniumListTile
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -98,8 +98,7 @@ fun AlternativeTrackContent(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically) {
- MillenniumIndeterminateProgress(
- modifier = Modifier.size(24.dp))
+ MillenniumSpokeSpinner(size = 24.dp)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(Res.string.alternative_track_searching),
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/player_queue/PlayerQueueContent.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/player_queue/PlayerQueueContent.kt
index 67fbf043..9c4c7685 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/player_queue/PlayerQueueContent.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/player_queue/PlayerQueueContent.kt
@@ -18,6 +18,7 @@
package dev.krtirtho.spotube.modules.shell.player_queue
+import org.spotube.millennium.composables.MillenniumDropdownMenuItem
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -419,11 +420,6 @@ private fun QueueItemRow(
MillenniumDropdownMenu(
expanded = showMenu,
onExpandedChange = { showMenu = it },
- items = listOf(stringResource(Res.string.queue_remove_from_queue)),
- onItemClick = {
- onRemoveClick()
- showMenu = false
- },
anchor = {
MillenniumIconButton(
onClick = { showMenu = true },
@@ -434,7 +430,16 @@ private fun QueueItemRow(
contentDescription = stringResource(Res.string.player_more_options_content_description)
)
}
- })
+ }) {
+ MillenniumDropdownMenuItem(
+ label = stringResource(Res.string.queue_remove_from_queue),
+ icon = Iconsax.IconsaxMusicSquareRemove,
+ destructive = true,
+ onClick = {
+ onRemoveClick()
+ showMenu = false
+ })
+ }
}
}
)
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/support/Support.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/support/Support.kt
index fc1c59a8..51486546 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/support/Support.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/support/Support.kt
@@ -17,6 +17,12 @@
package dev.krtirtho.spotube.modules.support
+import org.spotube.millennium.composables.MillenniumDialogHeader
+import org.spotube.millennium.composables.MillenniumDialogTitle
+import org.spotube.millennium.composables.MillenniumDialogClose
+import org.spotube.millennium.composables.MillenniumDialogBody
+import org.spotube.millennium.composables.MillenniumDialogDescription
+import org.spotube.millennium.composables.MillenniumDialogFooter
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@@ -103,49 +109,50 @@ fun SupportDialog(
onDismissRequest: () -> Unit,
isReminder: Boolean = false) {
if (!visible) return
- MillenniumDialog(
- visible = true,
- onDismissRequest = onDismissRequest,
- title = stringResource(Res.string.support_dialog_title)) {
- Text(
- stringResource(Res.string.support_dialog_message),
- style = AppTheme.typography.bodyMedium,
- color = AppTheme.colors.onSurfaceVariant)
- if (isReminder) {
- Text(
- stringResource(Res.string.support_dialog_reminder_hint),
- style = AppTheme.typography.bodySmall,
- color = AppTheme.colors.onSurfaceVariant)
+ MillenniumDialog(visible = true, onDismissRequest = onDismissRequest) {
+ MillenniumDialogHeader {
+ MillenniumDialogTitle(stringResource(Res.string.support_dialog_title))
+ MillenniumDialogClose()
}
- donationOptions.forEach { option ->
- MillenniumButton(
- onClick = {
- openDonation(option)
- onDismissRequest()
- },
- modifier = Modifier.fillMaxWidth(),
- style = MillenniumButtonStyle.Secondary) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(8.dp)) {
- MillenniumIcon(
- imageVector = option.icon,
- contentDescription = null,
- modifier = Modifier.size(20.dp),
- tint = if (option.id == "patreon") {
- AppTheme.colors.onSurface
- } else {
- Color.Unspecified
- })
- Text(stringResource(option.title))
+ MillenniumDialogBody {
+ MillenniumDialogDescription(stringResource(Res.string.support_dialog_message))
+ if (isReminder) {
+ Text(
+ stringResource(Res.string.support_dialog_reminder_hint),
+ style = AppTheme.typography.bodySmall,
+ color = AppTheme.colors.onSurfaceVariant)
+ }
+ donationOptions.forEach { option ->
+ MillenniumButton(
+ onClick = {
+ openDonation(option)
+ onDismissRequest()
+ },
+ modifier = Modifier.fillMaxWidth(),
+ style = MillenniumButtonStyle.Secondary) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ MillenniumIcon(
+ imageVector = option.icon,
+ contentDescription = null,
+ modifier = Modifier.size(20.dp),
+ tint = if (option.id == "patreon") {
+ AppTheme.colors.onSurface
+ } else {
+ Color.Unspecified
+ })
+ Text(stringResource(option.title))
+ }
}
}
}
- MillenniumButton(
- onClick = onDismissRequest,
- modifier = Modifier.fillMaxWidth(),
- style = MillenniumButtonStyle.Quiet) {
- Text(stringResource(Res.string.support_dialog_dismiss))
+ MillenniumDialogFooter {
+ MillenniumButton(
+ onClick = onDismissRequest,
+ style = MillenniumButtonStyle.Quiet) {
+ Text(stringResource(Res.string.support_dialog_dismiss))
+ }
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/update/AppUpdateDialog.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/update/AppUpdateDialog.kt
index 6cd60adf..d894ef08 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/update/AppUpdateDialog.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/update/AppUpdateDialog.kt
@@ -17,19 +17,14 @@
package dev.krtirtho.spotube.modules.update
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.selection.SelectionContainer
-import com.composeunstyled.Text
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
+import com.composeunstyled.Text
import com.mikepenz.markdown.m3.Markdown
import org.jetbrains.compose.resources.stringResource
+import org.spotube.millennium.composables.MillenniumAlertDialog
import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle
-import org.spotube.millennium.composables.MillenniumDialog
import spotube.composeapp.generated.resources.Res
import spotube.composeapp.generated.resources.app_update_dialog_no_notes
import spotube.composeapp.generated.resources.app_update_dialog_title
@@ -47,28 +42,26 @@ fun AppUpdateDialog(
onDismiss: () -> Unit,
onIgnore: () -> Unit,
onUpdate: () -> Unit) {
- MillenniumDialog(
+ MillenniumAlertDialog(
visible = true,
onDismissRequest = onDismiss,
- title = stringResource(Res.string.app_update_dialog_title, update.tag)) {
- if (update.releaseNotesMarkdown.isNotBlank()) {
- SelectionContainer {
- Markdown(
- content = update.releaseNotesMarkdown,
- modifier = Modifier.padding(bottom = 8.dp))
+ title = { Text(stringResource(Res.string.app_update_dialog_title, update.tag)) },
+ content = {
+ if (update.releaseNotesMarkdown.isNotBlank()) {
+ SelectionContainer {
+ Markdown(content = update.releaseNotesMarkdown)
+ }
+ } else {
+ Text(stringResource(Res.string.app_update_dialog_no_notes))
}
- } else {
- Text(stringResource(Res.string.app_update_dialog_no_notes))
+ }) {
+ MillenniumButton(
+ onClick = onIgnore,
+ style = MillenniumButtonStyle.Secondary) {
+ Text(stringResource(Res.string.update_action_ignore))
}
- Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
- MillenniumButton(
- onClick = onIgnore,
- style = MillenniumButtonStyle.Secondary) {
- Text(stringResource(Res.string.update_action_ignore))
- }
- MillenniumButton(onClick = onUpdate) {
- Text(stringResource(Res.string.update_action_update))
- }
+ MillenniumButton(onClick = onUpdate) {
+ Text(stringResource(Res.string.update_action_update))
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/update/PluginUpdateDialog.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/update/PluginUpdateDialog.kt
index 0e1e9f50..8b303d20 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/update/PluginUpdateDialog.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/update/PluginUpdateDialog.kt
@@ -17,6 +17,7 @@
package dev.krtirtho.spotube.modules.update
+import org.spotube.millennium.composables.MillenniumAlertDialog
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
@@ -30,7 +31,6 @@ import com.mikepenz.markdown.m3.Markdown
import org.jetbrains.compose.resources.stringResource
import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle
-import org.spotube.millennium.composables.MillenniumDialog
import spotube.composeapp.generated.resources.Res
import spotube.composeapp.generated.resources.plugin_update_dialog_current_version
import spotube.composeapp.generated.resources.plugin_update_dialog_no_notes
@@ -50,33 +50,35 @@ fun PluginUpdateDialog(
onDismiss: () -> Unit,
onIgnore: () -> Unit,
onUpdate: () -> Unit) {
- MillenniumDialog(
+ MillenniumAlertDialog(
visible = true,
onDismissRequest = onDismiss,
- title = stringResource(
- Res.string.plugin_update_dialog_title,
- update.pluginName,
- update.info.latestVersion)) {
- Text(
- text = stringResource(Res.string.plugin_update_dialog_current_version, update.currentVersion),
- color = AppTheme.colors.onSurfaceVariant,
- modifier = Modifier.padding(bottom = 8.dp))
- if (update.info.releaseNotes.isNotBlank()) {
- SelectionContainer {
- Markdown(content = update.info.releaseNotes)
+ title = {
+ Text(
+ stringResource(
+ Res.string.plugin_update_dialog_title,
+ update.pluginName,
+ update.info.latestVersion))
+ },
+ description = {
+ Text(stringResource(Res.string.plugin_update_dialog_current_version, update.currentVersion))
+ },
+ content = {
+ if (update.info.releaseNotes.isNotBlank()) {
+ SelectionContainer {
+ Markdown(content = update.info.releaseNotes)
+ }
+ } else {
+ Text(stringResource(Res.string.plugin_update_dialog_no_notes))
}
- } else {
- Text(stringResource(Res.string.plugin_update_dialog_no_notes))
+ }) {
+ MillenniumButton(
+ onClick = onIgnore,
+ style = MillenniumButtonStyle.Secondary) {
+ Text(stringResource(Res.string.update_action_ignore))
}
- Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
- MillenniumButton(
- onClick = onIgnore,
- style = MillenniumButtonStyle.Secondary) {
- Text(stringResource(Res.string.update_action_ignore))
- }
- MillenniumButton(onClick = onUpdate) {
- Text(stringResource(Res.string.update_action_update))
- }
+ MillenniumButton(onClick = onUpdate) {
+ Text(stringResource(Res.string.update_action_update))
}
}
}