feat(ui): replace IndeterminateProgress with new loading components and enhance dialog structures

This commit is contained in:
Kingkor Roy Tirtho 2026-10-16 11:49:48 +06:00
parent daf54b74f4
commit c57e33fe0d
35 changed files with 1304 additions and 1025 deletions

View File

@ -279,6 +279,10 @@
<item quantity="one">%1$d plugin installed</item> <item quantity="one">%1$d plugin installed</item>
<item quantity="other">%1$d plugins installed</item> <item quantity="other">%1$d plugins installed</item>
</plurals> </plurals>
<string name="plugin_install_sheet_subtitle">Add a plugin from a link or a file on your device.</string>
<string name="plugin_install_file_hint">Pick a .smplug file from your device</string>
<string name="plugin_install_or">or</string>
<string name="plugin_info_dialog_title">Plugin details</string>
<string name="plugin_install_section_title">Install a Plugin</string> <string name="plugin_install_section_title">Install a Plugin</string>
<string name="plugin_url_placeholder">https://example.com/plugin.smplug</string> <string name="plugin_url_placeholder">https://example.com/plugin.smplug</string>
<string name="plugin_action_download">Download</string> <string name="plugin_action_download">Download</string>
@ -572,6 +576,7 @@
<string name="search_start_typing">Start typing to search</string> <string name="search_start_typing">Start typing to search</string>
<string name="search_start_typing_tracks">Start typing to search tracks</string> <string name="search_start_typing_tracks">Start typing to search tracks</string>
<string name="search_no_results">No results found</string> <string name="search_no_results">No results found</string>
<string name="common_no_results">No results</string>
<string name="search_see_all">See all</string> <string name="search_see_all">See all</string>
<string name="search_searching">Searching</string> <string name="search_searching">Searching</string>
<string name="search_loading_tracks">Loading tracks...</string> <string name="search_loading_tracks">Loading tracks...</string>

View File

@ -17,6 +17,7 @@
package dev.krtirtho.spotube.core.remote package dev.krtirtho.spotube.core.remote
import org.spotube.millennium.composables.MillenniumAlertDialog
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -34,7 +35,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.spotube.millennium.composables.MillenniumButton import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle import org.spotube.millennium.composables.MillenniumButtonStyle
import org.spotube.millennium.composables.MillenniumDialog
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import org.koin.compose.koinInject import org.koin.compose.koinInject
import spotube.composeapp.generated.resources.Res import spotube.composeapp.generated.resources.Res
@ -56,21 +56,25 @@ fun ConnectionRequestDialogHost() {
} }
pendingRequest?.let { request -> pendingRequest?.let { request ->
MillenniumDialog( MillenniumAlertDialog(
visible = true, visible = true,
onDismissRequest = { onDismissRequest = {
handler.resolveConnectionRequest(request.sessionId, ConnectionRequestResponse.Deny) handler.resolveConnectionRequest(request.sessionId, ConnectionRequestResponse.Deny)
pendingRequest = null pendingRequest = null
}, },
title = stringResource(Res.string.remote_request_title)) { title = { Text(stringResource(Res.string.remote_request_title)) },
Text( description = {
text = stringResource(Res.string.remote_request_message, request.deviceName), Text(stringResource(Res.string.remote_request_message, request.deviceName))
style = AppTheme.typography.bodyMedium, },
textAlign = TextAlign.Start) ) {
Row( MillenniumButton(
modifier = Modifier.fillMaxWidth(), onClick = {
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End), handler.resolveConnectionRequest(request.sessionId, ConnectionRequestResponse.AllowAlways)
verticalAlignment = Alignment.CenterVertically) { pendingRequest = null
},
style = MillenniumButtonStyle.Quiet) {
Text(stringResource(Res.string.remote_request_allow_always))
}
MillenniumButton( MillenniumButton(
onClick = { onClick = {
handler.resolveConnectionRequest(request.sessionId, ConnectionRequestResponse.Deny) handler.resolveConnectionRequest(request.sessionId, ConnectionRequestResponse.Deny)
@ -86,15 +90,6 @@ fun ConnectionRequestDialogHost() {
}) { }) {
Text(stringResource(Res.string.remote_request_allow)) 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))
}
}
} }
} }
} }

View File

@ -19,6 +19,15 @@ package dev.krtirtho.spotube.core.ui.component
import org.spotube.millennium.composables.MillenniumDialog 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.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle import org.spotube.millennium.composables.MillenniumIconButtonStyle
@ -50,6 +59,9 @@ import spotube.composeapp.generated.resources.*
fun AdaptiveDialogBottomSheet( fun AdaptiveDialogBottomSheet(
onDismiss: () -> Unit, onDismiss: () -> Unit,
title: @Composable (() -> Unit)? = null, 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, modifier: Modifier = Modifier,
actions: @Composable (() -> Unit)? = null, actions: @Composable (() -> Unit)? = null,
content: @Composable () -> Unit) { content: @Composable () -> Unit) {
@ -59,14 +71,19 @@ fun AdaptiveDialogBottomSheet(
MillenniumDialog( MillenniumDialog(
visible = true, visible = true,
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = "", modifier = modifier,
modifier = modifier) { paneTitle = dialogTitle ?: "") {
title?.let { it() } MillenniumDialogHeader {
content() if (dialogTitle != null) {
actions?.let { MillenniumDialogTitle(text = dialogTitle, textStyle = AppTheme.typography.titleMedium)
MillenniumSeparator() } else {
it() MillenniumDialogTitle { title?.invoke() }
} }
MillenniumDialogClose(
contentDescription = stringResource(Res.string.common_close_dialog))
}
MillenniumDialogBody { content() }
actions?.let { MillenniumDialogFooter(content = { it() }) }
} }
} else { } else {
val sheetState = rememberModalBottomSheetState(initialDetent = SheetDetent.FullyExpanded) val sheetState = rememberModalBottomSheetState(initialDetent = SheetDetent.FullyExpanded)
@ -99,7 +116,13 @@ fun AdaptiveDialogBottomSheet(
} }
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
} }
Column(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 560.dp)
.verticalScroll(rememberScrollState())) {
content() content()
}
actions?.let { actions?.let {
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
MillenniumSeparator() MillenniumSeparator()

View File

@ -23,6 +23,16 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -41,6 +52,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import com.composeunstyled.currentWindowWidthBreakpoint import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.theme.MillenniumTheme
import org.spotube.millennium.adaptive.WindowBreakpoints import org.spotube.millennium.adaptive.WindowBreakpoints
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp 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 dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import org.spotube.millennium.composables.MillenniumDropdownMenu 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.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle import org.spotube.millennium.composables.MillenniumIconButtonStyle
@ -67,6 +87,7 @@ import org.spotube.millennium.composables.MillenniumText
import org.spotube.millennium.composables.MillenniumTextField import org.spotube.millennium.composables.MillenniumTextField
import spotube.composeapp.generated.resources.Res import spotube.composeapp.generated.resources.Res
import spotube.composeapp.generated.resources.accessibility_selected import spotube.composeapp.generated.resources.accessibility_selected
import spotube.composeapp.generated.resources.common_no_results
import spotube.composeapp.generated.resources.search_filter_placeholder import spotube.composeapp.generated.resources.search_filter_placeholder
data class AdaptiveMenuItem( data class AdaptiveMenuItem(
@ -99,17 +120,13 @@ fun AdaptiveDropdownBottomSheet(
Box(modifier = modifier) { Box(modifier = modifier) {
if (isLargeScreen) { if (isLargeScreen) {
MillenniumDropdownMenu( DesktopDropdownMenu(
expanded = expanded, expanded = expanded,
onExpandedChange = { expanded = it }, onExpandedChange = { expanded = it },
items = items.map { it.label }, items = items,
onItemClick = { label -> menuMinWidth = menuMinWidth,
items.firstOrNull { it.label == label && it.enabled }?.onClick?.invoke() filter = filter,
expanded = false trigger = { trigger { expanded = true } })
},
minimumWidth = menuMinWidth,
anchor = { trigger { expanded = true } }
)
} else { } else {
trigger { expanded = true } trigger { expanded = true }
if (expanded) { if (expanded) {
@ -134,6 +151,7 @@ private fun AdaptiveBottomSheetContent(
header: @Composable (() -> Unit)?, header: @Composable (() -> Unit)?,
filter: ((AdaptiveMenuItem, String) -> Boolean)? = null) { filter: ((AdaptiveMenuItem, String) -> Boolean)? = null) {
val hasSelection = items.any { it.selected } val hasSelection = items.any { it.selected }
val hasLeading = items.any { it.icon != null || it.leadingContent != null }
val queryState = rememberTextFieldState() val queryState = rememberTextFieldState()
val query = queryState.text.toString() val query = queryState.text.toString()
val filteredItems = if (filter != null && query.isNotBlank()) { val filteredItems = if (filter != null && query.isNotBlank()) {
@ -175,25 +193,45 @@ private fun AdaptiveBottomSheetContent(
if (item.dividerBefore) { if (item.dividerBefore) {
MillenniumSeparator(modifier = Modifier.padding(vertical = 4.dp)) MillenniumSeparator(modifier = Modifier.padding(vertical = 4.dp))
} }
MillenniumListTile( AdaptiveMenuTile(
title = { item = item,
MillenniumText( hasSelection = hasSelection,
item.label, hasLeading = hasLeading,
style = AppTheme.typography.bodyLarge, textStyle = AppTheme.typography.bodyLarge,
color = if (item.enabled) { minimumHeight = 56.dp,
AppTheme.colors.onSurface onClick = {
} else { item.onClick()
AppTheme.colors.onSurface.copy(alpha = 0.38f) onDismiss()
})
} }
) }
}, }
}
}
/** 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 -> subtitle = item.subtitle?.let { subtitle ->
{ {
MillenniumText( MillenniumText(
subtitle, subtitle,
style = AppTheme.typography.bodySmall, style = AppTheme.typography.bodySmall,
color = AppTheme.colors.onSurfaceVariant color = AppTheme.colors.onSurfaceVariant)
)
} }
}, },
leading = when { leading = when {
@ -203,8 +241,8 @@ private fun AdaptiveBottomSheetContent(
MillenniumIcon( MillenniumIcon(
imageVector = item.icon, imageVector = item.icon,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(22.dp) modifier = Modifier.size(22.dp),
) tint = contentColor)
} }
} }
item.selected -> { item.selected -> {
@ -212,11 +250,10 @@ private fun AdaptiveBottomSheetContent(
MillenniumIcon( MillenniumIcon(
imageVector = FeatherIcons.Check, imageVector = FeatherIcons.Check,
contentDescription = stringResource(Res.string.accessibility_selected), contentDescription = stringResource(Res.string.accessibility_selected),
modifier = Modifier.size(22.dp) modifier = Modifier.size(22.dp))
)
} }
} }
hasSelection -> {{ Spacer(modifier = Modifier.size(22.dp)) }} hasSelection || hasLeading -> {{ Spacer(modifier = Modifier.size(22.dp)) }}
else -> null else -> null
}, },
trailing = if (item.selected && (item.icon != null || item.leadingContent != null)) { trailing = if (item.selected && (item.icon != null || item.leadingContent != null)) {
@ -224,20 +261,66 @@ private fun AdaptiveBottomSheetContent(
MillenniumIcon( MillenniumIcon(
imageVector = FeatherIcons.Check, imageVector = FeatherIcons.Check,
contentDescription = stringResource(Res.string.accessibility_selected), contentDescription = stringResource(Res.string.accessibility_selected),
modifier = Modifier.size(22.dp) modifier = Modifier.size(22.dp))
)
} }
} else { } else {
null null
}, },
selected = item.selected, selected = item.selected,
enabled = item.enabled, enabled = item.enabled,
onClick = { onClick = onClick,
item.onClick() minimumHeight = minimumHeight)
onDismiss() }
},
minimumHeight = 56.dp /** 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<AdaptiveMenuItem>,
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)
} }
} }
} }

View File

@ -18,6 +18,11 @@
package dev.krtirtho.spotube.core.ui.component 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.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -191,19 +196,11 @@ private fun ErrorLogDialog(
onDismiss: () -> Unit) { onDismiss: () -> Unit) {
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
MillenniumDialog( MillenniumDialog(visible = true, onDismissRequest = onDismiss) {
visible = true, MillenniumDialogHeader {
onDismissRequest = onDismiss, MillenniumDialogTitle(
title = "") {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResource(Res.string.error_full_log_title), text = stringResource(Res.string.error_full_log_title),
style = AppTheme.typography.titleLarge, textStyle = AppTheme.typography.titleMedium)
fontWeight = FontWeight.Bold)
MillenniumIconButton( MillenniumIconButton(
onClick = { onClick = {
clipboardManager.setText(AnnotatedString(errorMessage)) clipboardManager.setText(AnnotatedString(errorMessage))
@ -213,7 +210,9 @@ private fun ErrorLogDialog(
imageVector = FeatherIcons.Copy, imageVector = FeatherIcons.Copy,
contentDescription = stringResource(Res.string.error_copy_to_clipboard)) contentDescription = stringResource(Res.string.error_copy_to_clipboard))
} }
MillenniumDialogClose()
} }
MillenniumDialogBody(scrollable = false) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -232,12 +231,11 @@ private fun ErrorLogDialog(
.padding(12.dp)) .padding(12.dp))
} }
} }
Row( }
modifier = Modifier.fillMaxWidth(), MillenniumDialogFooter {
horizontalArrangement = Arrangement.End) {
MillenniumButton( MillenniumButton(
onClick = onDismiss, onClick = onDismiss,
style = MillenniumButtonStyle.Quiet) { style = MillenniumButtonStyle.Secondary) {
Text(stringResource(Res.string.common_close)) Text(stringResource(Res.string.common_close))
} }
} }

View File

@ -28,6 +28,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@ -184,7 +185,7 @@ fun TrackOptionsBottomSheet(
} else { } else {
AppTheme.colors.onSurface.copy(alpha = 0.38f) AppTheme.colors.onSurface.copy(alpha = 0.38f)
}) })
} } ?: Spacer(modifier = Modifier.size(22.dp))
Text( Text(
text = item.label, text = item.label,
style = AppTheme.typography.bodyLarge, style = AppTheme.typography.bodyLarge,

View File

@ -42,6 +42,8 @@ import androidx.compose.ui.unit.TextUnit
import com.valentinilk.shimmer.ShimmerBounds import com.valentinilk.shimmer.ShimmerBounds
import com.valentinilk.shimmer.rememberShimmer import com.valentinilk.shimmer.rememberShimmer
import com.valentinilk.shimmer.shimmer import com.valentinilk.shimmer.shimmer
import org.spotube.millennium.theme.MillenniumTheme
import org.spotube.millennium.theme.millenniumBevelBrush
val LocalSkeletonLoading = compositionLocalOf { false } val LocalSkeletonLoading = compositionLocalOf { false }
@ -80,11 +82,16 @@ fun SkeletonTree(
} }
fun Modifier.shimmerApply( 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 { ): Modifier = composed {
val isTreeLoading = LocalSkeletonLoading.current 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 @Composable

View File

@ -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.component.dragScrollable
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker 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.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay
@ -143,8 +142,7 @@ fun ArtistScreen(
LazyColumn( LazyColumn(
state = listState, state = listState,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize(),
.padding(bottom = LocalAppShellBottomInset.current + 24.dp),
verticalArrangement = Arrangement.spacedBy(24.dp) verticalArrangement = Arrangement.spacedBy(24.dp)
) { ) {
item { item {

View File

@ -18,10 +18,10 @@
package dev.krtirtho.spotube.modules.devices package dev.krtirtho.spotube.modules.devices
import org.spotube.millennium.composables.MillenniumSpokeSpinner
import org.spotube.millennium.composables.MillenniumIcon import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumButton import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle import org.spotube.millennium.composables.MillenniumButtonStyle
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.theme.MillenniumTheme import org.spotube.millennium.theme.MillenniumTheme
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@ -103,10 +103,9 @@ fun DevicesScreen(
title = { Text(stringResource(Res.string.devices_title)) }, title = { Text(stringResource(Res.string.devices_title)) },
actions = { actions = {
if (isDiscovering && connectingToDevice == null) { if (isDiscovering && connectingToDevice == null) {
MillenniumIndeterminateProgress( MillenniumSpokeSpinner(
modifier = Modifier size = 24.dp,
.size(24.dp) modifier = Modifier.padding(end = 8.dp))
.padding(end = 8.dp))
} else { } else {
MillenniumIcon( MillenniumIcon(
imageVector = Iconsax.IconsaxRefreshRight, imageVector = Iconsax.IconsaxRefreshRight,
@ -177,8 +176,7 @@ fun DevicesScreen(
Row( Row(
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically) { verticalAlignment = Alignment.CenterVertically) {
MillenniumIndeterminateProgress( MillenniumSpokeSpinner(size = 24.dp)
modifier = Modifier.size(24.dp))
Text( Text(
text = stringResource(Res.string.devices_connecting), text = stringResource(Res.string.devices_connecting),
style = AppTheme.typography.bodyMedium) style = AppTheme.typography.bodyMedium)
@ -250,8 +248,7 @@ private fun DeviceRow(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)) { horizontalArrangement = Arrangement.spacedBy(12.dp)) {
if (isConnecting) { if (isConnecting) {
MillenniumIndeterminateProgress( MillenniumSpokeSpinner(size = 24.dp)
modifier = Modifier.size(24.dp))
} else { } else {
MillenniumIcon( MillenniumIcon(
imageVector = Iconsax.IconsaxMirroringScreen, imageVector = Iconsax.IconsaxMirroringScreen,

View File

@ -18,6 +18,11 @@
package dev.krtirtho.spotube.modules.devices 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.MillenniumIcon
import org.spotube.millennium.composables.MillenniumDialog import org.spotube.millennium.composables.MillenniumDialog
import org.spotube.millennium.composables.MillenniumListTile import org.spotube.millennium.composables.MillenniumListTile
@ -82,10 +87,12 @@ fun PlayDestinationPickerHost() {
PlaybackDestinationAction.PlayNext -> stringResource(Res.string.devices_destination_action_play_next) PlaybackDestinationAction.PlayNext -> stringResource(Res.string.devices_destination_action_play_next)
} }
MillenniumDialog( MillenniumDialog(visible = true, onDismissRequest = controller::dismissPicker) {
visible = true, MillenniumDialogHeader {
onDismissRequest = controller::dismissPicker, MillenniumDialogTitle(stringResource(Res.string.devices_destination_title))
title = stringResource(Res.string.devices_destination_title)) { MillenniumDialogClose()
}
MillenniumDialogBody {
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)) { verticalArrangement = Arrangement.spacedBy(8.dp)) {
@ -138,6 +145,9 @@ fun PlayDestinationPickerHost() {
}) })
} }
}
}
MillenniumDialogFooter {
MillenniumButton( MillenniumButton(
onClick = controller::dismissPicker, onClick = controller::dismissPicker,
style = MillenniumButtonStyle.Quiet) { style = MillenniumButtonStyle.Quiet) {

View File

@ -18,6 +18,8 @@
package dev.krtirtho.spotube.modules.devices 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.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle import org.spotube.millennium.composables.MillenniumIconButtonStyle
@ -604,11 +606,6 @@ private fun RemoteQueueItemRow(
MillenniumDropdownMenu( MillenniumDropdownMenu(
expanded = showMenu, expanded = showMenu,
onExpandedChange = { showMenu = it }, onExpandedChange = { showMenu = it },
items = listOf(stringResource(Res.string.devices_remote_remove_from_queue)),
onItemClick = {
onRemoveClick()
showMenu = false
},
anchor = { anchor = {
MillenniumIconButton( MillenniumIconButton(
onClick = { showMenu = true }, onClick = { showMenu = true },
@ -620,8 +617,17 @@ private fun RemoteQueueItemRow(
modifier = Modifier.size(18.dp) modifier = Modifier.size(18.dp)
) )
} }
}) {
MillenniumDropdownMenuItem(
label = stringResource(Res.string.devices_remote_remove_from_queue),
icon = Iconsax.IconsaxMusicSquareRemove,
destructive = true,
onClick = {
onRemoveClick()
showMenu = false
}) })
} }
}
) )
} }

View File

@ -3,6 +3,7 @@
*/ */
package dev.krtirtho.spotube.modules.downloads package dev.krtirtho.spotube.modules.downloads
import org.spotube.millennium.composables.MillenniumStripedProgress
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle import org.spotube.millennium.composables.MillenniumIconButtonStyle
import org.spotube.millennium.composables.MillenniumProgress
import org.spotube.millennium.theme.MillenniumTheme import org.spotube.millennium.theme.MillenniumTheme
import org.spotube.millennium.theme.millenniumPanelBrush import org.spotube.millennium.theme.millenniumPanelBrush
import org.spotube.millennium.theme.millenniumSurface import org.spotube.millennium.theme.millenniumSurface
@ -225,9 +225,10 @@ internal fun DownloadListRow(
}, },
maxLines = 1, overflow = TextOverflow.Ellipsis maxLines = 1, overflow = TextOverflow.Ellipsis
) )
if (item.status == DownloadStatus.Downloading) MillenniumProgress( if (item.status == DownloadStatus.Downloading) MillenniumStripedProgress(
item.progress, progress = item.progress,
Modifier.fillMaxWidth().height(4.dp) modifier = Modifier.fillMaxWidth(),
height = 6.dp
) )
} }
MillenniumIconButton( MillenniumIconButton(

View File

@ -17,6 +17,9 @@
package dev.krtirtho.spotube.modules.jam 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.Canvas
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -421,25 +424,27 @@ internal fun SessionPanel(
) )
} }
if (participant.canModerate) { 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. // Keep destructive moderation out of the participant's primary row.
org.spotube.millennium.composables.MillenniumDropdownMenu( org.spotube.millennium.composables.MillenniumDropdownMenu(
expanded = participant.menuExpanded, expanded = participant.menuExpanded,
onExpandedChange = { expanded -> onMenuChange(if (expanded) participant.id else null) }, 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 = { anchor = {
MillenniumButton( MillenniumButton(
onClick = { onMenuChange(participant.id) }, onClick = { onMenuChange(participant.id) },
style = MillenniumButtonStyle.Secondary style = MillenniumButtonStyle.Secondary
) { Text(stringResource(Res.string.jam_participants_manage)) } ) { 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) })
}
} }
} }
} }

View File

@ -17,6 +17,7 @@
package dev.krtirtho.spotube.modules.jam package dev.krtirtho.spotube.modules.jam
import org.spotube.millennium.composables.MillenniumAlertDialog
import dev.krtirtho.spotube.core.ui.theming.AppTheme import dev.krtirtho.spotube.core.ui.theming.AppTheme
import com.composeunstyled.Text import com.composeunstyled.Text
import org.spotube.millennium.composables.MillenniumButton import org.spotube.millennium.composables.MillenniumButton
@ -47,14 +48,11 @@ fun PlayDestinationPicker(
onSuggestToJam: () -> Unit) { onSuggestToJam: () -> Unit) {
if (!visible) return if (!visible) return
MillenniumDialog( MillenniumAlertDialog(
visible = true, visible = true,
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = stringResource(Res.string.jam_destination_title)) { title = { Text(stringResource(Res.string.jam_destination_title)) },
Text( description = { Text(stringResource(Res.string.jam_destination_message)) }) {
text = stringResource(Res.string.jam_destination_message),
style = AppTheme.typography.bodyMedium)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
MillenniumButton( MillenniumButton(
onClick = onDismiss, onClick = onDismiss,
style = MillenniumButtonStyle.Quiet) { style = MillenniumButtonStyle.Quiet) {
@ -62,7 +60,7 @@ fun PlayDestinationPicker(
} }
MillenniumButton( MillenniumButton(
onClick = onPlayLocally, onClick = onPlayLocally,
style = MillenniumButtonStyle.Quiet) { style = MillenniumButtonStyle.Secondary) {
Text(stringResource(Res.string.jam_destination_play_here)) Text(stringResource(Res.string.jam_destination_play_here))
} }
MillenniumButton(onClick = onSuggestToJam) { MillenniumButton(onClick = onSuggestToJam) {
@ -70,4 +68,3 @@ fun PlayDestinationPicker(
} }
} }
} }
}

View File

@ -3,6 +3,8 @@
*/ */
package dev.krtirtho.spotube.modules.library.local_tracks 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle import org.spotube.millennium.composables.MillenniumIconButtonStyle
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.composables.MillenniumListTile import org.spotube.millennium.composables.MillenniumListTile
import org.spotube.millennium.theme.MillenniumTheme import org.spotube.millennium.theme.MillenniumTheme
import org.spotube.millennium.theme.millenniumPanelBrush import org.spotube.millennium.theme.millenniumPanelBrush
@ -113,7 +114,7 @@ internal fun LocalTracksContent(
Modifier.fillMaxSize().padding(bottom = inset), Modifier.fillMaxSize().padding(bottom = inset),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
MillenniumIndeterminateProgress() MillenniumCircularProgress(size = 40.dp)
} }
state.currentFolderPath == null && state.visibleFolders.isNotEmpty() -> LazyColumn( state.currentFolderPath == null && state.visibleFolders.isNotEmpty() -> LazyColumn(
@ -264,7 +265,7 @@ internal fun LocalTracksBreadcrumb(
style = AppTheme.typography.labelSmall, color = AppTheme.colors.onSurfaceVariant, style = AppTheme.typography.labelSmall, color = AppTheme.colors.onSurfaceVariant,
maxLines = 1 maxLines = 1
) )
if (state.isRefreshing) MillenniumIndeterminateProgress(Modifier.size(14.dp)) if (state.isRefreshing) MillenniumSpokeSpinner(size = 14.dp)
if (state.currentFolderPath == null && onManageFolders != null) MillenniumIconButton( if (state.currentFolderPath == null && onManageFolders != null) MillenniumIconButton(
onClick = onManageFolders, onClick = onManageFolders,
modifier = Modifier.size(28.dp), style = MillenniumIconButtonStyle.Quiet modifier = Modifier.size(28.dp), style = MillenniumIconButtonStyle.Quiet

View File

@ -18,6 +18,7 @@
package dev.krtirtho.spotube.modules.library.playlist 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.MillenniumIcon
import org.spotube.millennium.composables.MillenniumButton import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle import org.spotube.millennium.composables.MillenniumButtonStyle
@ -154,37 +155,28 @@ fun AddToPlaylistPicker(
} }
} }
if (isLargeScreen) { val pickerTitle = stringResource(Res.string.library_add_to_playlist_title)
MillenniumDialog( AdaptiveDialogBottomSheet(
visible = true, onDismiss = onDismiss,
onDismissRequest = onDismiss, modifier = modifier,
title = stringResource(Res.string.library_add_to_playlist_title), dialogTitle = pickerTitle,
modifier = modifier) { title = { Text(pickerTitle, style = AppTheme.typography.headlineSmall) },
Box(modifier = Modifier.fillMaxWidth().height(400.dp)) { actions = {
content() Row(
} modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End) {
MillenniumButton( MillenniumButton(
onClick = onDismiss, onClick = onDismiss,
style = MillenniumButtonStyle.Secondary) { style = MillenniumButtonStyle.Secondary) {
Text(stringResource(Res.string.common_cancel)) Text(stringResource(Res.string.common_cancel))
} }
} }
} else { }) {
val sheetState = rememberModalBottomSheetState(initialDetent = SheetDetent.FullyExpanded) Box(modifier = Modifier.fillMaxWidth().height(400.dp)) {
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() content()
} }
} }
} }
}
@Composable @Composable
private fun PlaylistPickerContent( private fun PlaylistPickerContent(

View File

@ -17,6 +17,7 @@
package dev.krtirtho.spotube.modules.library.playlist package dev.krtirtho.spotube.modules.library.playlist
import dev.krtirtho.spotube.core.ui.component.AdaptiveDialogBottomSheet
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import org.spotube.millennium.composables.MillenniumButton import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle import org.spotube.millennium.composables.MillenniumButtonStyle
@ -162,75 +163,20 @@ fun PlaylistFormSheet(
val handleImagePick = { imagePicker.launch() } val handleImagePick = { imagePicker.launch() }
if (isLargeScreen) { val formTitle = stringResource(
MillenniumDialog( if (isEditing) Res.string.library_playlist_form_edit_title else Res.string.library_playlist_form_create_title)
visible = true, AdaptiveDialogBottomSheet(
onDismissRequest = onDismiss, onDismiss = onDismiss,
title = stringResource(if (isEditing) Res.string.library_playlist_form_edit_title else Res.string.library_playlist_form_create_title), modifier = modifier,
modifier = modifier dialogTitle = formTitle,
) { title = { Text(formTitle, style = AppTheme.typography.headlineSmall) },
PlaylistFormFields( actions = {
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)) {
MillenniumButton(
onClick = {
onSubmit(currentData)
onDismiss()
},
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( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End) horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End)) {
) {
MillenniumButton( MillenniumButton(
onClick = onDismiss, onClick = onDismiss,
style = MillenniumButtonStyle.Secondary style = MillenniumButtonStyle.Secondary) {
) {
Text(stringResource(Res.string.common_cancel)) Text(stringResource(Res.string.common_cancel))
} }
MillenniumButton( MillenniumButton(
@ -238,14 +184,21 @@ fun PlaylistFormSheet(
onSubmit(currentData) onSubmit(currentData)
onDismiss() onDismiss()
}, },
enabled = name.isNotBlank() enabled = name.isNotBlank()) {
) {
Text(stringResource(if (isEditing) Res.string.common_save else Res.string.library_playlist_form_create)) 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)
} }
} }

View File

@ -18,10 +18,10 @@
package dev.krtirtho.spotube.modules.lyrics package dev.krtirtho.spotube.modules.lyrics
import org.spotube.millennium.composables.MillenniumBarsLoader
import org.spotube.millennium.composables.MillenniumIcon import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle import org.spotube.millennium.composables.MillenniumIconButtonStyle
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -138,7 +138,7 @@ fun LyricsScreen(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
MillenniumIndeterminateProgress(modifier = Modifier.fillMaxWidth(0.4f)) MillenniumBarsLoader(size = 48.dp)
} }
} }

View File

@ -18,13 +18,15 @@
package dev.krtirtho.spotube.modules.playlist 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.MillenniumIcon
import org.spotube.millennium.composables.MillenniumButton import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle import org.spotube.millennium.composables.MillenniumButtonStyle
import org.spotube.millennium.composables.MillenniumDialog import org.spotube.millennium.composables.MillenniumDialog
import org.spotube.millennium.composables.MillenniumIconButton import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle import org.spotube.millennium.composables.MillenniumIconButtonStyle
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.composables.MillenniumModalBottomSheet import org.spotube.millennium.composables.MillenniumModalBottomSheet
import org.spotube.millennium.composables.MillenniumTextField import org.spotube.millennium.composables.MillenniumTextField
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -169,7 +171,7 @@ fun AddTracksToPlaylistDialog(
.fillMaxWidth() .fillMaxWidth()
.weight(1f), .weight(1f),
contentAlignment = Alignment.Center) { contentAlignment = Alignment.Center) {
MillenniumIndeterminateProgress() MillenniumCircularProgress(size = 40.dp)
} }
} }
hasSearched && searchResults.isEmpty() -> { hasSearched && searchResults.isEmpty() -> {
@ -209,36 +211,26 @@ fun AddTracksToPlaylistDialog(
} }
} }
if (isLargeScreen) { val dialogTitle = stringResource(Res.string.playlist_add_tracks_title)
MillenniumDialog( AdaptiveDialogBottomSheet(
visible = true, onDismiss = onDismiss,
onDismissRequest = onDismiss, modifier = modifier,
title = stringResource(Res.string.playlist_add_tracks_title), dialogTitle = dialogTitle,
modifier = modifier) { title = { Text(dialogTitle, style = AppTheme.typography.headlineSmall) },
Box(modifier = Modifier.fillMaxWidth().height(500.dp)) { actions = {
content() Row(
} modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End) {
MillenniumButton(onClick = onDismiss) { MillenniumButton(onClick = onDismiss) {
Text(stringResource(Res.string.playlist_add_tracks_done)) Text(stringResource(Res.string.playlist_add_tracks_done))
} }
} }
} else { }) {
val sheetState = rememberModalBottomSheetState(initialDetent = SheetDetent.FullyExpanded) Box(modifier = Modifier.fillMaxWidth().height(500.dp)) {
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() content()
} }
} }
} }
}
@Composable @Composable
private fun TrackSearchResultRow( private fun TrackSearchResultRow(
@ -297,8 +289,7 @@ private fun TrackSearchResultRow(
modifier = Modifier.size(24.dp)) modifier = Modifier.size(24.dp))
} }
isAdding -> { isAdding -> {
MillenniumIndeterminateProgress( MillenniumSpokeSpinner(size = 24.dp)
modifier = Modifier.size(24.dp))
} }
else -> { else -> {
MillenniumButton( MillenniumButton(

View File

@ -18,13 +18,15 @@
package dev.krtirtho.spotube.modules.plugin 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.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle import org.spotube.millennium.composables.MillenniumButtonStyle
import org.spotube.millennium.composables.MillenniumDialog import org.spotube.millennium.composables.MillenniumDialog
import org.spotube.millennium.composables.MillenniumIcon import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle import org.spotube.millennium.composables.MillenniumIconButtonStyle
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.composables.MillenniumSeparator import org.spotube.millennium.composables.MillenniumSeparator
import org.spotube.millennium.composables.MillenniumSegmentedButton import org.spotube.millennium.composables.MillenniumSegmentedButton
import org.spotube.millennium.composables.MillenniumSegmentedButtonDefaults 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.getPlatform
import dev.krtirtho.spotube.modules.plugin.components.DefaultAbilityPluginGrid import dev.krtirtho.spotube.modules.plugin.components.DefaultAbilityPluginGrid
import dev.krtirtho.spotube.modules.plugin.components.PluginCard 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.PluginInstallDialog
import dev.krtirtho.spotube.modules.plugin.components.PluginLogo import dev.krtirtho.spotube.modules.plugin.components.PluginLogo
import dev.krtirtho.spotube.modules.plugin.components.PluginPermissionDialog import dev.krtirtho.spotube.modules.plugin.components.PluginPermissionDialog
@ -267,7 +285,7 @@ internal fun PluginScreenContent(
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { MillenniumIndeterminateProgress(modifier = Modifier.widthIn(max = 160.dp)) } ) { MillenniumCircularProgress(size = 40.dp) }
} }
is PluginUiState.Data -> { is PluginUiState.Data -> {
@ -492,7 +510,7 @@ internal fun PluginScreenContent(
Modifier.fillMaxWidth().padding(16.dp), Modifier.fillMaxWidth().padding(16.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
MillenniumIndeterminateProgress(Modifier.widthIn(max = 120.dp)) MillenniumDotsLoader()
} }
} }
} }
@ -524,7 +542,7 @@ internal fun PluginScreenContent(
.padding(vertical = 16.dp), .padding(vertical = 16.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
MillenniumIndeterminateProgress(modifier = Modifier.widthIn(max = 120.dp)) MillenniumDotsLoader()
} }
} }
} }
@ -606,24 +624,17 @@ private fun InstallSheet(
AdaptiveDialogBottomSheet( AdaptiveDialogBottomSheet(
onDismiss = onDismiss, onDismiss = onDismiss,
title = { title = {
Row( PluginDialogTitle(
verticalAlignment = Alignment.CenterVertically, title = stringResource(Res.string.plugin_install_section_title),
horizontalArrangement = Arrangement.spacedBy(8.dp) icon = Iconsax.IconsaxBoxAdd)
) { },
MillenniumIcon( dialogTitle = stringResource(Res.string.plugin_install_section_title)) {
Iconsax.IconsaxImportArrow2Bulk, PluginDialogBody {
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = AppTheme.colors.primary
)
Text( Text(
stringResource(Res.string.plugin_install_section_title), stringResource(Res.string.plugin_install_sheet_subtitle),
style = AppTheme.typography.titleSmall, style = AppTheme.typography.bodySmall,
fontWeight = FontWeight.SemiBold color = AppTheme.colors.onSurfaceVariant)
)
}
}) {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
val urlState = rememberTextFieldState(urlInput) val urlState = rememberTextFieldState(urlInput)
LaunchedEffect(urlInput) { LaunchedEffect(urlInput) {
if (urlState.text.toString() != urlInput) { if (urlState.text.toString() != urlInput) {
@ -636,51 +647,82 @@ private fun InstallSheet(
} }
} }
PluginPanel {
Column(
modifier = Modifier.fillMaxWidth().padding(14.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row( Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(12.dp)) {
) { PluginIconTile(Iconsax.IconsaxLink)
Text(
stringResource(Res.string.plugin_section_url_title),
style = AppTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold)
}
MillenniumTextField( MillenniumTextField(
state = urlState, state = urlState,
label = stringResource(Res.string.plugin_section_url_title), label = "",
modifier = Modifier.weight(1f), modifier = Modifier.fillMaxWidth(),
placeholder = stringResource(Res.string.plugin_url_placeholder), placeholder = stringResource(Res.string.plugin_url_placeholder),
error = urlError?.text()) error = urlError?.text())
MillenniumIconButton( MillenniumButton(
onClick = onSubmitUrl, onClick = onSubmitUrl,
enabled = !isLoadingUrl, enabled = !isLoadingUrl,
style = MillenniumIconButtonStyle.Secondary) { modifier = Modifier.fillMaxWidth(),
style = MillenniumButtonStyle.Primary) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)) {
if (isLoadingUrl) { if (isLoadingUrl) {
MillenniumIndeterminateProgress(modifier = Modifier.size(16.dp)) MillenniumSpokeSpinner(size = 16.dp, color = MillenniumTheme.colors.onAccent)
} else { } else {
MillenniumIcon( MillenniumIcon(
Iconsax.IconsaxImportArrow2Bulk, Iconsax.IconsaxImportArrow2Bulk,
contentDescription = stringResource(Res.string.plugin_action_download)) 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,
modifier = Modifier.fillMaxWidth(),
style = MillenniumButtonStyle.Secondary) {
Row( Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp) 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(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( MillenniumIcon(
Iconsax.IconsaxExportArrowBulk, FeatherIcons.ChevronRight,
contentDescription = stringResource(Res.string.plugin_action_install_from_file) contentDescription = null,
) modifier = Modifier.size(18.dp),
Text(stringResource(Res.string.plugin_action_install_from_file)) tint = AppTheme.colors.onSurfaceVariant)
} }
} }
} }
@ -692,82 +734,80 @@ private fun PluginInfoDialog(
plugin: PluginEntry, plugin: PluginEntry,
logoPath: Path?, logoPath: Path?,
onDismiss: () -> Unit) { onDismiss: () -> Unit) {
MillenniumDialog( AdaptiveDialogBottomSheet(
visible = true, onDismiss = onDismiss,
onDismissRequest = onDismiss, title = { PluginDialogTitle(stringResource(Res.string.plugin_info_dialog_title)) },
title = plugin.name) { dialogTitle = stringResource(Res.string.plugin_info_dialog_title),
Box( actions = {
modifier = Modifier PluginDialogActions(
.size(48.dp) secondaryLabel = stringResource(Res.string.common_close),
.clip(RoundedCornerShape(12.dp)) onSecondary = onDismiss)
.background(AppTheme.colors.primary.copy(alpha = 0.12f)) }) {
) { PluginDialogBody {
PluginLogo( PluginPanel {
plugin = plugin, Row(
logoPath = logoPath, modifier = Modifier.fillMaxWidth().padding(14.dp),
contentDescription = plugin.name, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.spacedBy(14.dp)) {
fallback = { PluginLogoTile(plugin = plugin, logoPath = logoPath)
Box(contentAlignment = Alignment.Center) {
MillenniumIcon(
Iconsax.IconsaxBox,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = AppTheme.colors.primary
)
}
})
}
Column( Column(
verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.weight(1f),
modifier = Modifier.fillMaxWidth() 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)
}
}
}
if (plugin.description.isNotBlank()) { if (plugin.description.isNotBlank()) {
Text( Text(
plugin.description, plugin.description,
style = AppTheme.typography.bodyMedium, modifier = Modifier.padding(start = 14.dp, end = 14.dp, bottom = 14.dp),
color = AppTheme.colors.onSurface style = AppTheme.typography.bodySmall,
) color = AppTheme.colors.onSurfaceVariant)
}
} }
DetailRow(stringResource(Res.string.plugin_info_version), plugin.version) PluginPanel {
Column(
modifier = Modifier.fillMaxWidth().padding(14.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)) {
DetailRow(stringResource(Res.string.plugin_info_api_version), plugin.apiVersion) DetailRow(stringResource(Res.string.plugin_info_api_version), plugin.apiVersion)
DetailRow(stringResource(Res.string.plugin_info_author), plugin.author)
if (plugin.license.isNotBlank()) { if (plugin.license.isNotBlank()) {
DetailRow(stringResource(Res.string.plugin_info_license), plugin.license) DetailRow(stringResource(Res.string.plugin_info_license), plugin.license)
} }
if (plugin.capabilities.isNotEmpty()) { if (plugin.capabilities.isNotEmpty()) {
DetailChipsRow( DetailChipsRow(
stringResource(Res.string.plugin_info_capabilities), stringResource(Res.string.plugin_info_capabilities),
plugin.capabilities.map { it.name }) plugin.capabilities.map { it.name })
} }
if (plugin.abilities.isNotEmpty()) { if (plugin.abilities.isNotEmpty()) {
DetailChipsRow( DetailChipsRow(
stringResource(Res.string.plugin_info_abilities), stringResource(Res.string.plugin_info_abilities),
plugin.abilities.map { ability -> ability.displayLabel() }) plugin.abilities.map { ability -> ability.displayLabel() })
} }
if (plugin.repository.isNotBlank()) { if (plugin.repository.isNotBlank()) {
ClickableDetailRow(label = stringResource(Res.string.plugin_info_repository), value = plugin.repository) { ClickableDetailRow(label = stringResource(Res.string.plugin_info_repository), value = plugin.repository) {
openUrlInBrowser(plugin.repository) openUrlInBrowser(plugin.repository)
} }
} }
if (plugin.contact.isNotBlank()) { if (plugin.contact.isNotBlank()) {
DetailRow(stringResource(Res.string.plugin_info_contact), plugin.contact) DetailRow(stringResource(Res.string.plugin_info_contact), plugin.contact)
} }
if (plugin.bugs.isNotBlank()) { if (plugin.bugs.isNotBlank()) {
ClickableDetailRow(label = stringResource(Res.string.plugin_info_report_bugs), value = plugin.bugs) { ClickableDetailRow(label = stringResource(Res.string.plugin_info_report_bugs), value = plugin.bugs) {
openUrlInBrowser(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?, supportText: String?,
isLoading: Boolean, isLoading: Boolean,
onDismiss: () -> Unit) { onDismiss: () -> Unit) {
MillenniumDialog( AdaptiveDialogBottomSheet(
visible = true, onDismiss = onDismiss,
onDismissRequest = onDismiss, title = { PluginDialogTitle(stringResource(Res.string.plugin_support_dialog_title, plugin.name)) },
title = 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) { if (isLoading) {
Box( Box(
modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp), modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center) {
) { MillenniumCircularProgress(size = 36.dp)
MillenniumIndeterminateProgress(modifier = Modifier.widthIn(max = 160.dp))
} }
} else { } else {
PluginPanel {
Text( Text(
supportText ?: "", supportText ?: "",
modifier = Modifier.padding(14.dp),
style = AppTheme.typography.bodyMedium, style = AppTheme.typography.bodyMedium,
color = AppTheme.colors.onSurface color = AppTheme.colors.onSurface)
)
} }
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
MillenniumButton(onClick = onDismiss) {
Text(stringResource(Res.string.common_close))
} }
} }
} }
@ -972,7 +1015,7 @@ private fun DiscoverRepoCard(
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
if (isInstalling) { if (isInstalling) {
MillenniumIndeterminateProgress(modifier = Modifier.size(16.dp)) MillenniumSpokeSpinner(size = 16.dp)
} else { } else {
MillenniumIcon( MillenniumIcon(
Iconsax.IconsaxAdd, Iconsax.IconsaxAdd,

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}
}
}

View File

@ -17,7 +17,7 @@
package dev.krtirtho.spotube.modules.plugin.components 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -52,19 +48,22 @@ import coil3.compose.AsyncImage
import coil3.compose.LocalPlatformContext import coil3.compose.LocalPlatformContext
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.crossfade 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.GitHubRelease
import dev.krtirtho.spotube.modules.plugin.GitHubRepo
import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4 import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4
import dev.krtirtho.spotube.resources.iconsax.IconsaxBox
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import org.spotube.millennium.composables.MillenniumButton import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle import org.spotube.millennium.composables.MillenniumButtonStyle
import org.spotube.millennium.composables.MillenniumDialog
import org.spotube.millennium.composables.MillenniumDropdownMenu 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.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import spotube.composeapp.generated.resources.Res 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_action_install
import spotube.composeapp.generated.resources.plugin_install_no_releases import spotube.composeapp.generated.resources.plugin_install_no_releases
import spotube.composeapp.generated.resources.plugin_install_prerelease import spotube.composeapp.generated.resources.plugin_install_prerelease
@ -88,21 +87,24 @@ fun PluginInstallDialog(
} }
} }
MillenniumDialog( AdaptiveDialogBottomSheet(
visible = true, onDismiss = onDismiss,
onDismissRequest = onDismiss, title = { PluginDialogTitle(stringResource(Res.string.plugin_action_install)) },
title = repo.fullName) { 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( Row(
modifier = Modifier.fillMaxWidth().padding(14.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp) horizontalArrangement = Arrangement.spacedBy(14.dp)) {
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(12.dp))
.background(AppTheme.colors.primary.copy(alpha = 0.12f))
) {
Box(contentAlignment = Alignment.Center) {
val platformContext = LocalPlatformContext.current val platformContext = LocalPlatformContext.current
AsyncImage( AsyncImage(
model = ImageRequest.Builder(platformContext) model = ImageRequest.Builder(platformContext)
@ -110,132 +112,100 @@ fun PluginInstallDialog(
.crossfade(true) .crossfade(true)
.build(), .build(),
contentDescription = repo.owner.login, contentDescription = repo.owner.login,
modifier = Modifier.fillMaxSize() modifier = Modifier.size(56.dp).clip(RoundedCornerShape(16.dp)))
) Column(
} modifier = Modifier.weight(1f),
} verticalArrangement = Arrangement.spacedBy(4.dp)) {
Column(modifier = Modifier.weight(1f)) {
Text( Text(
repo.fullName, repo.fullName,
style = AppTheme.typography.titleMedium, style = AppTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis)
)
if (!repo.description.isNullOrBlank()) { if (!repo.description.isNullOrBlank()) {
Text( Text(
repo.description, repo.description,
style = AppTheme.typography.bodySmall, style = AppTheme.typography.bodySmall,
color = AppTheme.colors.onSurfaceVariant, color = AppTheme.colors.onSurfaceVariant,
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis)
) }
} }
} }
} }
if (isLoadingReleases) { if (isLoadingReleases) {
Box( Box(
modifier = Modifier.fillMaxWidth().height(120.dp), modifier = Modifier.fillMaxWidth().height(96.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center) {
) { MillenniumCircularProgress(size = 36.dp)
MillenniumIndeterminateProgress(modifier = Modifier.widthIn(max = 160.dp))
} }
} else if (releases.isEmpty()) { } else if (releases.isEmpty()) {
Text( PluginCallout(stringResource(Res.string.plugin_install_no_releases), isWarning = true)
stringResource(Res.string.plugin_install_no_releases),
style = AppTheme.typography.bodyMedium,
color = AppTheme.colors.onSurfaceVariant
)
} else { } else {
Text( PluginSectionLabel(stringResource(Res.string.plugin_install_select_release_label))
stringResource(Res.string.plugin_install_select_release_label),
style = AppTheme.typography.labelMedium,
fontWeight = FontWeight.SemiBold,
color = AppTheme.colors.onSurface
)
MillenniumDropdownMenu( MillenniumDropdownMenu(
expanded = expanded, expanded = expanded,
onExpandedChange = { expanded = it }, onExpandedChange = { expanded = it },
items = releases.map { it.tagName },
onItemClick = { tagName ->
selectedRelease = releases.firstOrNull { it.tagName == tagName }
expanded = false
},
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
anchor = { anchor = {
MillenniumButton( MillenniumButton(
onClick = { expanded = true }, onClick = { expanded = true },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
style = MillenniumButtonStyle.Secondary style = MillenniumButtonStyle.Secondary) {
) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween) {
) {
Text( Text(
selectedRelease?.tagName selectedRelease?.tagName
?: stringResource(Res.string.plugin_install_select_release_placeholder), ?: stringResource(Res.string.plugin_install_select_release_placeholder),
style = AppTheme.typography.bodyMedium style = AppTheme.typography.bodyMedium)
)
MillenniumIcon( MillenniumIcon(
imageVector = Iconsax.IconsaxArrowDown4, imageVector = Iconsax.IconsaxArrowDown4,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(16.dp) 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 -> selectedRelease?.let { release ->
if (release.prerelease) { if (release.prerelease) {
Text( PluginCallout(stringResource(Res.string.plugin_install_prerelease), isWarning = true)
stringResource(Res.string.plugin_install_prerelease),
style = AppTheme.typography.labelSmall,
color = AppTheme.colors.error
)
} }
if (!release.body.isNullOrBlank()) { if (!release.body.isNullOrBlank()) {
Text( PluginSectionLabel(stringResource(Res.string.plugin_install_release_notes))
stringResource(Res.string.plugin_install_release_notes), PluginPanel {
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( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(max = 200.dp) .heightIn(max = 200.dp)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(12.dp) .padding(12.dp)) {
) {
Text( Text(
release.body, release.body,
style = AppTheme.typography.bodySmall, style = AppTheme.typography.bodySmall,
color = AppTheme.colors.onSurfaceVariant color = AppTheme.colors.onSurfaceVariant)
)
} }
} }
} }
} }
} }
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
MillenniumButton(
onClick = { selectedRelease?.let { onInstall(it) } },
enabled = selectedRelease != null && !isLoadingReleases
) {
Text(stringResource(Res.string.plugin_action_install))
}
} }
} }
} }

View File

@ -17,47 +17,39 @@
package dev.krtirtho.spotube.modules.plugin.components 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.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size 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.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp 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.PluginCapability
import dev.krtirtho.spotube.modules.plugin.PluginEntry import dev.krtirtho.spotube.modules.plugin.PluginEntry
import dev.krtirtho.spotube.resources.iconsax.Iconsax 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.IconsaxEye
import dev.krtirtho.spotube.resources.iconsax.IconsaxWifiSquare import dev.krtirtho.spotube.resources.iconsax.IconsaxWifiSquare
import dev.krtirtho.spotube.resources.iconsax.PhosphorDatabaseDuotone import dev.krtirtho.spotube.resources.iconsax.PhosphorDatabaseDuotone
import dev.krtirtho.spotube.resources.iconsax.User import dev.krtirtho.spotube.resources.iconsax.User
import okio.Path
import org.jetbrains.compose.resources.stringResource 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.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_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_desc
import spotube.composeapp.generated.resources.plugin_permissions_capability_network_title import spotube.composeapp.generated.resources.plugin_permissions_capability_network_title
import spotube.composeapp.generated.resources.plugin_permissions_capability_storage_desc 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_requested_title
import spotube.composeapp.generated.resources.plugin_permissions_supplied_label import spotube.composeapp.generated.resources.plugin_permissions_supplied_label
import spotube.composeapp.generated.resources.plugin_version_label import spotube.composeapp.generated.resources.plugin_version_label
import spotube.composeapp.generated.resources.common_cancel
import spotube.composeapp.generated.resources.common_close
@Composable @Composable
fun PluginPermissionDialog( fun PluginPermissionDialog(
@ -83,187 +73,139 @@ fun PluginPermissionDialog(
logoPath: Path? = null, logoPath: Path? = null,
onConfirm: (() -> Unit)? = null, onConfirm: (() -> Unit)? = null,
onDismiss: () -> Unit) { onDismiss: () -> Unit) {
MillenniumDialog( AdaptiveDialogBottomSheet(
visible = true, onDismiss = onDismiss,
onDismissRequest = onDismiss, title = { PluginDialogTitle(title) },
title = 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( Row(
modifier = Modifier.fillMaxWidth().padding(14.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(14.dp) horizontalArrangement = Arrangement.spacedBy(14.dp)) {
) { PluginLogoTile(plugin = pluginInfo, logoPath = logoPath)
Box( Column(
modifier = Modifier modifier = Modifier.weight(1f),
.size(48.dp) verticalArrangement = Arrangement.spacedBy(6.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( Text(
pluginInfo.name, pluginInfo.name,
style = AppTheme.typography.titleMedium, style = AppTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis) 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)
} }
} }
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()) { if (pluginInfo.description.isNotBlank()) {
Text( Text(
pluginInfo.description, pluginInfo.description,
modifier = Modifier.padding(start = 14.dp, end = 14.dp, bottom = 14.dp),
style = AppTheme.typography.bodySmall, style = AppTheme.typography.bodySmall,
color = AppTheme.colors.onSurfaceVariant, color = AppTheme.colors.onSurfaceVariant,
maxLines = 3, maxLines = 3,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis)
) }
} }
MillenniumCard(contentPadding = PaddingValues(14.dp)) { PluginCallout(text = message, isWarning = confirmLabel == null)
Text(
message,
style = AppTheme.typography.bodySmall,
color = AppTheme.colors.onSurface
)
}
existingPlugin?.let { existingPlugin?.let {
InstalledComparisonCard( InstalledComparisonPanel(installedPlugin = it, incomingPlugin = pluginInfo)
installedPlugin = it,
incomingPlugin = pluginInfo
)
} }
Text( PluginSectionLabel(
stringResource(Res.string.plugin_permissions_requested_title), stringResource(Res.string.plugin_permissions_requested_title),
style = AppTheme.typography.labelMedium, badge = pluginInfo.capabilities.size.takeIf { it > 0 }?.toString())
fontWeight = FontWeight.SemiBold,
color = AppTheme.colors.onSurface
)
PluginPanel {
if (pluginInfo.capabilities.isEmpty()) { if (pluginInfo.capabilities.isEmpty()) {
Row(
modifier = Modifier.fillMaxWidth().padding(14.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)) {
PluginIconTile(Iconsax.IconsaxCheckCircle)
Text( Text(
stringResource(Res.string.plugin_permissions_none), stringResource(Res.string.plugin_permissions_none),
style = AppTheme.typography.bodySmall, style = AppTheme.typography.bodySmall,
color = AppTheme.colors.onSurfaceVariant color = AppTheme.colors.onSurfaceVariant)
) }
} else { } else {
pluginInfo.capabilities.forEach { capability -> pluginInfo.capabilities.forEachIndexed { index, capability ->
if (index > 0) MillenniumSeparator()
CapabilityRow(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)
}
)
}
if (confirmLabel != null && onConfirm != null) {
MillenniumButton(onClick = onConfirm) {
Text(confirmLabel)
}
} }
} }
} }
} }
@Composable @Composable
private fun InstalledComparisonCard( private fun InstalledComparisonPanel(
installedPlugin: PluginEntry, installedPlugin: PluginEntry,
incomingPlugin: PluginEntry) { incomingPlugin: PluginEntry) {
MillenniumCard( Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
modifier = Modifier.fillMaxWidth(), PluginSectionLabel(stringResource(Res.string.plugin_permissions_compare_title))
contentPadding = PaddingValues(14.dp)) { PluginPanel {
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth().padding(14.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(10.dp)) {
) {
Text(
stringResource(Res.string.plugin_permissions_compare_title),
style = AppTheme.typography.labelMedium,
fontWeight = FontWeight.SemiBold)
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween verticalAlignment = Alignment.CenterVertically,
) { horizontalArrangement = Arrangement.SpaceEvenly) {
VersionStat( VersionStat(
label = stringResource(Res.string.plugin_permissions_installed_label), label = stringResource(Res.string.plugin_permissions_installed_label),
value = installedPlugin.version) value = installedPlugin.version)
MillenniumIcon(
imageVector = FeatherIcons.ArrowRight,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = AppTheme.colors.onSurfaceVariant)
VersionStat( VersionStat(
label = stringResource(Res.string.plugin_permissions_supplied_label), label = stringResource(Res.string.plugin_permissions_supplied_label),
value = incomingPlugin.version) value = incomingPlugin.version,
highlighted = true)
} }
Text( Text(
stringResource( stringResource(
Res.string.plugin_permissions_api_diff, Res.string.plugin_permissions_api_diff,
installedPlugin.apiVersion, installedPlugin.apiVersion,
incomingPlugin.apiVersion incomingPlugin.apiVersion),
),
style = AppTheme.typography.labelSmall, style = AppTheme.typography.labelSmall,
color = AppTheme.colors.onSurfaceVariant color = AppTheme.colors.onSurfaceVariant)
) }
} }
} }
} }
@Composable @Composable
private fun VersionStat(label: String, value: String) { private fun VersionStat(label: String, value: String, highlighted: Boolean = false) {
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text( Text(
label, label,
style = AppTheme.typography.labelSmall, style = AppTheme.typography.labelSmall,
color = AppTheme.colors.onSurfaceVariant color = AppTheme.colors.onSurfaceVariant)
)
Text( Text(
stringResource(Res.string.plugin_version_label, value), stringResource(Res.string.plugin_version_label, value),
style = AppTheme.typography.bodyMedium, style = AppTheme.typography.titleSmall,
fontWeight = FontWeight.Medium 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( PluginCapability.PERSISTENT_STORAGE -> Triple(
Iconsax.PhosphorDatabaseDuotone, Iconsax.PhosphorDatabaseDuotone,
stringResource(Res.string.plugin_permissions_capability_storage_title), 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( PluginCapability.NETWORK_REQUESTS -> Triple(
Iconsax.IconsaxWifiSquare, Iconsax.IconsaxWifiSquare,
stringResource(Res.string.plugin_permissions_capability_network_title), 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( PluginCapability.WEBVIEW -> Triple(
Iconsax.IconsaxEye, Iconsax.IconsaxEye,
stringResource(Res.string.plugin_permissions_capability_webview_title), 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( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(14.dp) horizontalArrangement = Arrangement.spacedBy(12.dp)) {
) { PluginIconTile(icon)
Box( Column(verticalArrangement = Arrangement.spacedBy(1.dp)) {
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( Text(
label, label,
style = AppTheme.typography.bodyMedium, style = AppTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium)
)
Text( Text(
description, description,
style = AppTheme.typography.labelSmall, style = AppTheme.typography.labelSmall,
color = AppTheme.colors.onSurfaceVariant color = AppTheme.colors.onSurfaceVariant)
)
}
} }
} }
} }

View File

@ -18,10 +18,10 @@
package dev.krtirtho.spotube.modules.search package dev.krtirtho.spotube.modules.search
import org.spotube.millennium.composables.MillenniumMarqueeProgress
import org.spotube.millennium.composables.MillenniumIcon import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumButton import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle import org.spotube.millennium.composables.MillenniumButtonStyle
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.composables.MillenniumListTile import org.spotube.millennium.composables.MillenniumListTile
import org.spotube.millennium.composables.MillenniumAutocomplete import org.spotube.millennium.composables.MillenniumAutocomplete
import org.spotube.millennium.composables.MillenniumText import org.spotube.millennium.composables.MillenniumText
@ -1155,7 +1155,7 @@ private fun SearchLoadingIndicator(
translationY = bounce translationY = bounce
}) })
} }
MillenniumIndeterminateProgress( MillenniumMarqueeProgress(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 32.dp, vertical = 16.dp) .padding(horizontal = 32.dp, vertical = 16.dp)

View File

@ -18,10 +18,10 @@
package dev.krtirtho.spotube.modules.settings package dev.krtirtho.spotube.modules.settings
import org.spotube.millennium.composables.MillenniumCircularProgress
import org.spotube.millennium.composables.MillenniumIcon import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle import org.spotube.millennium.composables.MillenniumIconButtonStyle
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.composables.MillenniumSeparator import org.spotube.millennium.composables.MillenniumSeparator
import org.spotube.millennium.composables.MillenniumSidebarItem import org.spotube.millennium.composables.MillenniumSidebarItem
import org.spotube.millennium.theme.MillenniumTheme import org.spotube.millennium.theme.MillenniumTheme
@ -288,7 +288,7 @@ private fun SettingsTabContent(
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center) { contentAlignment = Alignment.Center) {
MillenniumIndeterminateProgress() MillenniumCircularProgress(size = 40.dp)
} }
} else { } else {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {

View File

@ -18,6 +18,12 @@
package dev.krtirtho.spotube.modules.settings.components 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 androidx.compose.foundation.layout.Box
import com.composeunstyled.currentWindowWidthBreakpoint import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.adaptive.WindowBreakpoints import org.spotube.millennium.adaptive.WindowBreakpoints
@ -238,17 +244,14 @@ internal fun TextInputSettingCard(
val normalizedValue = normalize(draft) val normalizedValue = normalize(draft)
val errorMessage = validate(normalizedValue) val errorMessage = validate(normalizedValue)
MillenniumDialog( MillenniumDialog(visible = true, onDismissRequest = { isDialogOpen = false }) {
visible = true, MillenniumDialogHeader {
onDismissRequest = { isDialogOpen = false }, MillenniumDialogTitle(dialogTitle)
title = dialogTitle MillenniumDialogClose()
) { }
MillenniumDialogBody {
if (dialogDescription != null) { if (dialogDescription != null) {
Text( MillenniumDialogDescription(dialogDescription)
text = dialogDescription,
style = AppTheme.typography.bodySmall,
color = AppTheme.colors.onSurfaceVariant
)
} }
MillenniumTextField( MillenniumTextField(
state = dialogState, state = dialogState,
@ -257,7 +260,14 @@ internal fun TextInputSettingCard(
placeholder = placeholder, placeholder = placeholder,
error = errorMessage error = errorMessage
) )
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { }
MillenniumDialogFooter {
MillenniumButton(
onClick = { isDialogOpen = false },
style = MillenniumButtonStyle.Secondary
) {
Text(stringResource(Res.string.common_cancel))
}
MillenniumButton( MillenniumButton(
onClick = { onClick = {
if (errorMessage == null) { if (errorMessage == null) {
@ -269,12 +279,6 @@ internal fun TextInputSettingCard(
) { ) {
Text(stringResource(Res.string.common_save)) Text(stringResource(Res.string.common_save))
} }
MillenniumButton(
onClick = { isDialogOpen = false },
style = MillenniumButtonStyle.Secondary
) {
Text(stringResource(Res.string.common_cancel))
}
} }
} }
} }

View File

@ -17,6 +17,11 @@
package dev.krtirtho.spotube.modules.settings.sections 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.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@ -151,10 +156,12 @@ private fun AppearanceSettingCard(
mutableStateOf(selectedPalette) mutableStateOf(selectedPalette)
} }
MillenniumDialog( MillenniumDialog(visible = true, onDismissRequest = { isDialogOpen = false }) {
visible = true, MillenniumDialogHeader {
onDismissRequest = { isDialogOpen = false }, MillenniumDialogTitle(stringResource(Res.string.settings_palette_dialog_title))
title = stringResource(Res.string.settings_palette_dialog_title)) { MillenniumDialogClose()
}
MillenniumDialogBody {
MillenniumText( MillenniumText(
stringResource(Res.string.settings_palette_dialog_description), stringResource(Res.string.settings_palette_dialog_description),
color = AppTheme.colors.onSurfaceVariant) color = AppTheme.colors.onSurfaceVariant)
@ -174,9 +181,13 @@ private fun AppearanceSettingCard(
} }
PalettePreview(draftPalette) PalettePreview(draftPalette)
}
MillenniumSeparator() MillenniumDialogFooter {
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { MillenniumButton(
onClick = { isDialogOpen = false },
style = MillenniumButtonStyle.Secondary) {
MillenniumText(stringResource(Res.string.common_cancel))
}
MillenniumButton( MillenniumButton(
onClick = { onClick = {
onSaved(draftPalette) onSaved(draftPalette)
@ -184,11 +195,6 @@ private fun AppearanceSettingCard(
}) { }) {
MillenniumText(stringResource(Res.string.common_save)) MillenniumText(stringResource(Res.string.common_save))
} }
MillenniumButton(
onClick = { isDialogOpen = false },
style = MillenniumButtonStyle.Secondary) {
MillenniumText(stringResource(Res.string.common_cancel))
}
} }
} }
} }

View File

@ -18,6 +18,10 @@
package dev.krtirtho.spotube.modules.shell 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.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -436,27 +440,6 @@ fun AppLargePlayer(
onExpandedChange = { expanded -> onExpandedChange = { expanded ->
if (!expanded) playerOptionsViewModel.dismissMoreOptionsMenu() 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, side = AnchorSide.Top,
anchor = { anchor = {
MillenniumIconButton( MillenniumIconButton(
@ -471,7 +454,31 @@ fun AppLargePlayer(
contentDescription = stringResource(Res.string.player_more_options_content_description) 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) { Row(verticalAlignment = Alignment.CenterVertically) {
MillenniumIconButton( MillenniumIconButton(

View File

@ -21,6 +21,7 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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) val supportLabel = stringResource(Res.string.support_button_label)
MillenniumSidebarItem( MillenniumSidebarItem(
label = supportLabel, label = supportLabel,

View File

@ -18,11 +18,16 @@
package dev.krtirtho.spotube.modules.shell 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.MillenniumIcon
import org.spotube.millennium.composables.MillenniumButton import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle import org.spotube.millennium.composables.MillenniumButtonStyle
import org.spotube.millennium.composables.MillenniumDialog import org.spotube.millennium.composables.MillenniumDialog
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.composables.MillenniumTextField import org.spotube.millennium.composables.MillenniumTextField
import org.spotube.millennium.theme.millenniumSurface import org.spotube.millennium.theme.millenniumSurface
import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState
@ -128,11 +133,12 @@ private fun SleepTimerDialog(
onStart: () -> Unit, onStart: () -> Unit,
onCancelTimer: () -> Unit, onCancelTimer: () -> Unit,
onDismiss: () -> Unit) { onDismiss: () -> Unit) {
MillenniumDialog( MillenniumDialog(visible = true, onDismissRequest = onDismiss) {
visible = true, MillenniumDialogHeader {
onDismissRequest = onDismiss, MillenniumDialogTitle(stringResource(Res.string.player_sleep_timer))
title = stringResource(Res.string.player_sleep_timer)) { MillenniumDialogClose()
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { }
MillenniumDialogBody {
if (remainingMs != null) { if (remainingMs != null) {
Text( Text(
text = stringResource(Res.string.player_sleep_timer_remaining, formatRemainingTime(remainingMs)), text = stringResource(Res.string.player_sleep_timer_remaining, formatRemainingTime(remainingMs)),
@ -152,12 +158,12 @@ private fun SleepTimerDialog(
label = stringResource(Res.string.player_sleep_timer_minutes_label), label = stringResource(Res.string.player_sleep_timer_minutes_label),
placeholder = stringResource(Res.string.player_sleep_timer_minutes_placeholder), placeholder = stringResource(Res.string.player_sleep_timer_minutes_placeholder),
modifier = Modifier.fillMaxWidth()) modifier = Modifier.fillMaxWidth())
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { MillenniumDialogFooter {
if (remainingMs != null) { if (remainingMs != null) {
MillenniumButton( MillenniumButton(
onClick = onCancelTimer, onClick = onCancelTimer,
style = MillenniumButtonStyle.Secondary) { Text(stringResource(Res.string.player_sleep_timer_cancel)) } style = MillenniumButtonStyle.Quiet) { Text(stringResource(Res.string.player_sleep_timer_cancel)) }
} }
MillenniumButton( MillenniumButton(
onClick = onDismiss, onClick = onDismiss,
@ -170,17 +176,17 @@ private fun SleepTimerDialog(
} }
} }
} }
}
@Composable @Composable
private fun TrackDetailsDialog( private fun TrackDetailsDialog(
details: PlayerTrackDetails, details: PlayerTrackDetails,
onDismiss: () -> Unit) { onDismiss: () -> Unit) {
MillenniumDialog( MillenniumDialog(visible = true, onDismissRequest = onDismiss) {
visible = true, MillenniumDialogHeader {
onDismissRequest = onDismiss, MillenniumDialogTitle(stringResource(Res.string.player_track_details))
title = stringResource(Res.string.player_track_details)) { MillenniumDialogClose()
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { }
MillenniumDialogBody(contentSpacing = 14.dp) {
Text( Text(
details.track.title, details.track.title,
style = AppTheme.typography.bodyMedium, style = AppTheme.typography.bodyMedium,
@ -189,8 +195,7 @@ private fun TrackDetailsDialog(
Row( Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 20.dp), modifier = Modifier.fillMaxWidth().padding(vertical = 20.dp),
horizontalArrangement = Arrangement.Center) { horizontalArrangement = Arrangement.Center) {
MillenniumIndeterminateProgress( MillenniumMarqueeProgress(modifier = Modifier.fillMaxWidth())
modifier = Modifier.fillMaxWidth())
} }
} }
@ -315,6 +320,8 @@ private fun TrackDetailsDialog(
DetailRow(stringResource(Res.string.player_track_details_active_url), activeUrl) DetailRow(stringResource(Res.string.player_track_details_active_url), activeUrl)
} }
}
MillenniumDialogFooter {
MillenniumButton( MillenniumButton(
onClick = onDismiss, onClick = onDismiss,
style = MillenniumButtonStyle.Secondary) { Text(stringResource(Res.string.common_close)) } style = MillenniumButtonStyle.Secondary) { Text(stringResource(Res.string.common_close)) }

View File

@ -18,8 +18,8 @@
package dev.krtirtho.spotube.modules.shell.alternative_track 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.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.composables.MillenniumListTile import org.spotube.millennium.composables.MillenniumListTile
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@ -98,8 +98,7 @@ fun AlternativeTrackContent(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically) { verticalAlignment = Alignment.CenterVertically) {
MillenniumIndeterminateProgress( MillenniumSpokeSpinner(size = 24.dp)
modifier = Modifier.size(24.dp))
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
Text( Text(
text = stringResource(Res.string.alternative_track_searching), text = stringResource(Res.string.alternative_track_searching),

View File

@ -18,6 +18,7 @@
package dev.krtirtho.spotube.modules.shell.player_queue package dev.krtirtho.spotube.modules.shell.player_queue
import org.spotube.millennium.composables.MillenniumDropdownMenuItem
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -419,11 +420,6 @@ private fun QueueItemRow(
MillenniumDropdownMenu( MillenniumDropdownMenu(
expanded = showMenu, expanded = showMenu,
onExpandedChange = { showMenu = it }, onExpandedChange = { showMenu = it },
items = listOf(stringResource(Res.string.queue_remove_from_queue)),
onItemClick = {
onRemoveClick()
showMenu = false
},
anchor = { anchor = {
MillenniumIconButton( MillenniumIconButton(
onClick = { showMenu = true }, onClick = { showMenu = true },
@ -434,8 +430,17 @@ private fun QueueItemRow(
contentDescription = stringResource(Res.string.player_more_options_content_description) 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
}) })
} }
} }
}
) )
} }

View File

@ -17,6 +17,12 @@
package dev.krtirtho.spotube.modules.support 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.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -103,14 +109,13 @@ fun SupportDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
isReminder: Boolean = false) { isReminder: Boolean = false) {
if (!visible) return if (!visible) return
MillenniumDialog( MillenniumDialog(visible = true, onDismissRequest = onDismissRequest) {
visible = true, MillenniumDialogHeader {
onDismissRequest = onDismissRequest, MillenniumDialogTitle(stringResource(Res.string.support_dialog_title))
title = stringResource(Res.string.support_dialog_title)) { MillenniumDialogClose()
Text( }
stringResource(Res.string.support_dialog_message), MillenniumDialogBody {
style = AppTheme.typography.bodyMedium, MillenniumDialogDescription(stringResource(Res.string.support_dialog_message))
color = AppTheme.colors.onSurfaceVariant)
if (isReminder) { if (isReminder) {
Text( Text(
stringResource(Res.string.support_dialog_reminder_hint), stringResource(Res.string.support_dialog_reminder_hint),
@ -141,11 +146,13 @@ fun SupportDialog(
} }
} }
} }
}
MillenniumDialogFooter {
MillenniumButton( MillenniumButton(
onClick = onDismissRequest, onClick = onDismissRequest,
modifier = Modifier.fillMaxWidth(),
style = MillenniumButtonStyle.Quiet) { style = MillenniumButtonStyle.Quiet) {
Text(stringResource(Res.string.support_dialog_dismiss)) Text(stringResource(Res.string.support_dialog_dismiss))
} }
} }
} }
}

View File

@ -17,19 +17,14 @@
package dev.krtirtho.spotube.modules.update 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 androidx.compose.foundation.text.selection.SelectionContainer
import com.composeunstyled.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import com.composeunstyled.Text
import androidx.compose.ui.unit.dp
import com.mikepenz.markdown.m3.Markdown import com.mikepenz.markdown.m3.Markdown
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import org.spotube.millennium.composables.MillenniumAlertDialog
import org.spotube.millennium.composables.MillenniumButton import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle import org.spotube.millennium.composables.MillenniumButtonStyle
import org.spotube.millennium.composables.MillenniumDialog
import spotube.composeapp.generated.resources.Res import spotube.composeapp.generated.resources.Res
import spotube.composeapp.generated.resources.app_update_dialog_no_notes import spotube.composeapp.generated.resources.app_update_dialog_no_notes
import spotube.composeapp.generated.resources.app_update_dialog_title import spotube.composeapp.generated.resources.app_update_dialog_title
@ -47,20 +42,19 @@ fun AppUpdateDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onIgnore: () -> Unit, onIgnore: () -> Unit,
onUpdate: () -> Unit) { onUpdate: () -> Unit) {
MillenniumDialog( MillenniumAlertDialog(
visible = true, visible = true,
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = stringResource(Res.string.app_update_dialog_title, update.tag)) { title = { Text(stringResource(Res.string.app_update_dialog_title, update.tag)) },
content = {
if (update.releaseNotesMarkdown.isNotBlank()) { if (update.releaseNotesMarkdown.isNotBlank()) {
SelectionContainer { SelectionContainer {
Markdown( Markdown(content = update.releaseNotesMarkdown)
content = update.releaseNotesMarkdown,
modifier = Modifier.padding(bottom = 8.dp))
} }
} else { } else {
Text(stringResource(Res.string.app_update_dialog_no_notes)) Text(stringResource(Res.string.app_update_dialog_no_notes))
} }
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { }) {
MillenniumButton( MillenniumButton(
onClick = onIgnore, onClick = onIgnore,
style = MillenniumButtonStyle.Secondary) { style = MillenniumButtonStyle.Secondary) {
@ -71,4 +65,3 @@ fun AppUpdateDialog(
} }
} }
} }
}

View File

@ -17,6 +17,7 @@
package dev.krtirtho.spotube.modules.update package dev.krtirtho.spotube.modules.update
import org.spotube.millennium.composables.MillenniumAlertDialog
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -30,7 +31,6 @@ import com.mikepenz.markdown.m3.Markdown
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import org.spotube.millennium.composables.MillenniumButton import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle import org.spotube.millennium.composables.MillenniumButtonStyle
import org.spotube.millennium.composables.MillenniumDialog
import spotube.composeapp.generated.resources.Res import spotube.composeapp.generated.resources.Res
import spotube.composeapp.generated.resources.plugin_update_dialog_current_version import spotube.composeapp.generated.resources.plugin_update_dialog_current_version
import spotube.composeapp.generated.resources.plugin_update_dialog_no_notes import spotube.composeapp.generated.resources.plugin_update_dialog_no_notes
@ -50,17 +50,20 @@ fun PluginUpdateDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onIgnore: () -> Unit, onIgnore: () -> Unit,
onUpdate: () -> Unit) { onUpdate: () -> Unit) {
MillenniumDialog( MillenniumAlertDialog(
visible = true, visible = true,
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = stringResource( title = {
Text(
stringResource(
Res.string.plugin_update_dialog_title, Res.string.plugin_update_dialog_title,
update.pluginName, update.pluginName,
update.info.latestVersion)) { update.info.latestVersion))
Text( },
text = stringResource(Res.string.plugin_update_dialog_current_version, update.currentVersion), description = {
color = AppTheme.colors.onSurfaceVariant, Text(stringResource(Res.string.plugin_update_dialog_current_version, update.currentVersion))
modifier = Modifier.padding(bottom = 8.dp)) },
content = {
if (update.info.releaseNotes.isNotBlank()) { if (update.info.releaseNotes.isNotBlank()) {
SelectionContainer { SelectionContainer {
Markdown(content = update.info.releaseNotes) Markdown(content = update.info.releaseNotes)
@ -68,7 +71,7 @@ fun PluginUpdateDialog(
} else { } else {
Text(stringResource(Res.string.plugin_update_dialog_no_notes)) Text(stringResource(Res.string.plugin_update_dialog_no_notes))
} }
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { }) {
MillenniumButton( MillenniumButton(
onClick = onIgnore, onClick = onIgnore,
style = MillenniumButtonStyle.Secondary) { style = MillenniumButtonStyle.Secondary) {
@ -79,4 +82,3 @@ fun PluginUpdateDialog(
} }
} }
} }
}