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)) } } }