mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-11-10 05:04:01 +00:00
Compare commits
1 Commits
50905080b8
...
30394a923c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30394a923c |
@ -336,14 +336,6 @@ android {
|
||||
|
||||
}
|
||||
|
||||
// ditchoom's socket-quic-quiche ships libquiche_jni.so with 4 KB LOAD segment alignment
|
||||
// (not 16 KB compatible), even in the latest release. It is only an optional QUIC transport
|
||||
// pulled in transitively by mqtt-client, so drop it on Android until upstream fixes it.
|
||||
configurations.matching { it.name.startsWith("debug") || it.name.startsWith("release") }.configureEach {
|
||||
exclude(group = "com.ditchoom", module = "socket-quic-quiche")
|
||||
exclude(group = "com.ditchoom", module = "socket-quic-quiche-android")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
debugImplementation(libs.compose.uiTooling)
|
||||
|
||||
@ -68,8 +68,6 @@
|
||||
<string name="library_local_permission_description">Allow audio access to browse music on your device.</string>
|
||||
<string name="library_local_grant_access">Grant access</string>
|
||||
<string name="library_local_empty_folders">No local folders found</string>
|
||||
<string name="library_local_empty_folders_description">Add a folder with music to play it right here. No plugin needed.</string>
|
||||
<string name="library_local_add_folder">Add folder</string>
|
||||
<string name="library_local_empty_tracks">No tracks in this folder</string>
|
||||
<string name="library_local_no_results">No matching folders or tracks</string>
|
||||
<string name="library_local_refresh_failed">Failed to refresh local media</string>
|
||||
@ -265,24 +263,10 @@
|
||||
<string name="plugin_error_download_failed">Failed to download plugin</string>
|
||||
<string name="plugin_empty_title">No plugins installed yet</string>
|
||||
<string name="plugin_empty_subtitle">Paste a download URL or pick a file above.</string>
|
||||
<string name="no_metadata_plugin_title">No metadata plugin selected</string>
|
||||
<string name="no_metadata_plugin_description_install">Spotube needs a metadata plugin to browse, search and show your music. Install one to get started.</string>
|
||||
<string name="no_metadata_plugin_description_select">You already have plugins installed, but none is set as your metadata source. Pick one to start browsing.</string>
|
||||
<string name="no_metadata_plugin_step_discover">Discover a metadata plugin</string>
|
||||
<string name="no_metadata_plugin_step_install">Review its permissions and install it</string>
|
||||
<string name="no_metadata_plugin_step_select">Select it as your metadata source</string>
|
||||
<string name="no_metadata_plugin_action_install">Install a plugin</string>
|
||||
<string name="no_metadata_plugin_action_select">Choose a plugin</string>
|
||||
<string name="no_metadata_plugin_action_local">Play local music</string>
|
||||
<string name="no_metadata_plugin_local_hint">Your local tracks and downloads work without a plugin.</string>
|
||||
<plurals name="plugin_installed_plugins_count">
|
||||
<item quantity="one">%1$d plugin installed</item>
|
||||
<item quantity="other">%1$d plugins installed</item>
|
||||
</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_url_placeholder">https://example.com/plugin.smplug</string>
|
||||
<string name="plugin_action_download">Download</string>
|
||||
@ -576,7 +560,6 @@
|
||||
<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_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_searching">Searching</string>
|
||||
<string name="search_loading_tracks">Loading tracks...</string>
|
||||
@ -630,12 +613,6 @@
|
||||
<string name="media_user">User</string>
|
||||
<string name="media_playlist">Playlist</string>
|
||||
|
||||
<!-- Login required -->
|
||||
<string name="login_required_title">Sign in to continue</string>
|
||||
<string name="login_required_description">This part of Spotube needs you to be signed in with your music provider. Sign in once and everything will show up here.</string>
|
||||
<string name="login_required_description_plugin">%1$s needs you to sign in before it can show this. Sign in once and everything will show up here.</string>
|
||||
<string name="login_required_action">Sign in</string>
|
||||
|
||||
<!-- Error display -->
|
||||
<string name="error_title">We\'re Truly Sorry</string>
|
||||
<string name="error_description">Something isn\'t working right now. Your experience matters to us, and we\'re doing our best to fix this. Please try again or check the error details below.</string>
|
||||
|
||||
@ -54,7 +54,6 @@ import dev.krtirtho.spotube.modules.downloads.DownloadManager
|
||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
||||
import dev.krtirtho.spotube.modules.home.HomeScreenRepository
|
||||
import dev.krtirtho.spotube.modules.home.HomeScreenViewModel
|
||||
import dev.krtirtho.spotube.modules.plugin.MetadataAuthViewModel
|
||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
||||
import dev.krtirtho.spotube.modules.library.LibraryState
|
||||
import dev.krtirtho.spotube.modules.library.album.LibraryAlbumsViewModel
|
||||
@ -71,7 +70,6 @@ import dev.krtirtho.spotube.modules.plugin.AudioPluginSource
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginManager
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginProvider
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginViewModel
|
||||
import dev.krtirtho.spotube.modules.plugin.guard.MetadataPluginGuardViewModel
|
||||
import dev.krtirtho.spotube.modules.plugin.ScrobblePluginSource
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksRepository
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel
|
||||
@ -117,7 +115,6 @@ val sharedModules = module {
|
||||
// Home
|
||||
singleOf(::HomeScreenRepository)
|
||||
viewModelOf(::HomeScreenViewModel)
|
||||
viewModelOf(::MetadataAuthViewModel)
|
||||
|
||||
// Search
|
||||
singleOf(::SearchRepository)
|
||||
@ -141,7 +138,6 @@ val sharedModules = module {
|
||||
bind<ScrobblePluginSource>()
|
||||
}
|
||||
viewModelOf(::PluginViewModel)
|
||||
viewModelOf(::MetadataPluginGuardViewModel)
|
||||
|
||||
// Settings
|
||||
singleOf(::SettingsRepository) { bind<UserSettingsSource>() }
|
||||
|
||||
@ -29,7 +29,6 @@ import dev.krtirtho.spotube.modules.library.LibraryScreen
|
||||
import dev.krtirtho.spotube.modules.lyrics.LyricsScreen
|
||||
import dev.krtirtho.spotube.modules.playlist.PlaylistScreen
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginScreen
|
||||
import dev.krtirtho.spotube.modules.plugin.guard.MetadataPluginGuard
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksScreen
|
||||
import dev.krtirtho.spotube.modules.search.SearchScreen
|
||||
@ -102,17 +101,12 @@ sealed interface Routes : NavKey {
|
||||
val navigationModule = module {
|
||||
singleOf(::NavigationCommands)
|
||||
navigation<Routes.Home> {
|
||||
MetadataPluginGuard {
|
||||
HomeScreen(viewModel = koinViewModel())
|
||||
}
|
||||
}
|
||||
navigation<Routes.Search> {
|
||||
MetadataPluginGuard {
|
||||
SearchScreen()
|
||||
}
|
||||
}
|
||||
navigation<Routes.Library> {
|
||||
// Not guarded as a whole: Local tracks and Downloads work without a plugin.
|
||||
LibraryScreen()
|
||||
}
|
||||
navigation<Routes.Settings> {
|
||||
@ -123,7 +117,6 @@ val navigationModule = module {
|
||||
}
|
||||
navigation<Routes.WebView> {}
|
||||
navigation<Routes.Playlist> {
|
||||
MetadataPluginGuard(backButton = true) {
|
||||
PlaylistScreen(
|
||||
playlistId = it.playlistId,
|
||||
viewModel = koinViewModel(
|
||||
@ -134,9 +127,7 @@ val navigationModule = module {
|
||||
audioPlayer = get(),
|
||||
navigationCommands = get())
|
||||
}
|
||||
}
|
||||
navigation<Routes.Artist> {
|
||||
MetadataPluginGuard(backButton = true) {
|
||||
ArtistScreen(
|
||||
viewModel = koinViewModel(
|
||||
key = it.artistId,
|
||||
@ -146,9 +137,7 @@ val navigationModule = module {
|
||||
audioPlayer = get(),
|
||||
navigationCommands = get())
|
||||
}
|
||||
}
|
||||
navigation<Routes.Album> {
|
||||
MetadataPluginGuard(backButton = true) {
|
||||
AlbumScreen(
|
||||
albumId = it.albumId,
|
||||
viewModel = koinViewModel(
|
||||
@ -159,12 +148,10 @@ val navigationModule = module {
|
||||
audioPlayer = get(),
|
||||
navigationCommands = get())
|
||||
}
|
||||
}
|
||||
navigation<Routes.Lyrics> {
|
||||
LyricsScreen(viewModel = koinViewModel())
|
||||
}
|
||||
navigation<Routes.SavedTracks> {
|
||||
MetadataPluginGuard(backButton = true) {
|
||||
SavedTracksScreen(
|
||||
viewModel = koinViewModel(
|
||||
key = SAVED_TRACKS_COLLECTION_ID,
|
||||
@ -174,7 +161,6 @@ val navigationModule = module {
|
||||
audioPlayer = get(),
|
||||
navigationCommands = get())
|
||||
}
|
||||
}
|
||||
navigation<Routes.Blacklist> {
|
||||
BlacklistScreen()
|
||||
}
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
|
||||
package dev.krtirtho.spotube.core.remote
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumAlertDialog
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@ -35,6 +34,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.spotube.millennium.composables.MillenniumButton
|
||||
import org.spotube.millennium.composables.MillenniumButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumDialog
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.koinInject
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
@ -56,25 +56,21 @@ fun ConnectionRequestDialogHost() {
|
||||
}
|
||||
|
||||
pendingRequest?.let { request ->
|
||||
MillenniumAlertDialog(
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = {
|
||||
handler.resolveConnectionRequest(request.sessionId, ConnectionRequestResponse.Deny)
|
||||
pendingRequest = null
|
||||
},
|
||||
title = { Text(stringResource(Res.string.remote_request_title)) },
|
||||
description = {
|
||||
Text(stringResource(Res.string.remote_request_message, request.deviceName))
|
||||
},
|
||||
) {
|
||||
MillenniumButton(
|
||||
onClick = {
|
||||
handler.resolveConnectionRequest(request.sessionId, ConnectionRequestResponse.AllowAlways)
|
||||
pendingRequest = null
|
||||
},
|
||||
style = MillenniumButtonStyle.Quiet) {
|
||||
Text(stringResource(Res.string.remote_request_allow_always))
|
||||
}
|
||||
title = stringResource(Res.string.remote_request_title)) {
|
||||
Text(
|
||||
text = stringResource(Res.string.remote_request_message, request.deviceName),
|
||||
style = AppTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Start)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
MillenniumButton(
|
||||
onClick = {
|
||||
handler.resolveConnectionRequest(request.sessionId, ConnectionRequestResponse.Deny)
|
||||
@ -90,6 +86,15 @@ fun ConnectionRequestDialogHost() {
|
||||
}) {
|
||||
Text(stringResource(Res.string.remote_request_allow))
|
||||
}
|
||||
MillenniumButton(
|
||||
onClick = {
|
||||
handler.resolveConnectionRequest(request.sessionId, ConnectionRequestResponse.AllowAlways)
|
||||
pendingRequest = null
|
||||
},
|
||||
style = MillenniumButtonStyle.Quiet) {
|
||||
Text(stringResource(Res.string.remote_request_allow_always))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,15 +19,6 @@ package dev.krtirtho.spotube.core.ui.component
|
||||
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumDialog
|
||||
import org.spotube.millennium.composables.MillenniumDialogBody
|
||||
import org.spotube.millennium.composables.MillenniumDialogClose
|
||||
import org.spotube.millennium.composables.MillenniumDialogFooter
|
||||
import org.spotube.millennium.composables.MillenniumDialogHeader
|
||||
import org.spotube.millennium.composables.MillenniumDialogTitle
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import dev.krtirtho.spotube.core.ui.theming.AppTheme
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.composables.MillenniumIconButton
|
||||
import org.spotube.millennium.composables.MillenniumIconButtonStyle
|
||||
@ -40,6 +31,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import com.composeunstyled.SheetDetent
|
||||
import com.composeunstyled.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -58,9 +50,6 @@ import spotube.composeapp.generated.resources.*
|
||||
fun AdaptiveDialogBottomSheet(
|
||||
onDismiss: () -> Unit,
|
||||
title: @Composable (() -> Unit)? = null,
|
||||
// Plain-text title for the large-screen dialog's own titlebar; when set it replaces [title]
|
||||
// there so the titlebar isn't left empty above a duplicated in-body title.
|
||||
dialogTitle: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
actions: @Composable (() -> Unit)? = null,
|
||||
content: @Composable () -> Unit) {
|
||||
@ -70,19 +59,14 @@ fun AdaptiveDialogBottomSheet(
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = onDismiss,
|
||||
modifier = modifier,
|
||||
paneTitle = dialogTitle ?: "") {
|
||||
MillenniumDialogHeader {
|
||||
if (dialogTitle != null) {
|
||||
MillenniumDialogTitle(text = dialogTitle, textStyle = AppTheme.typography.titleMedium)
|
||||
} else {
|
||||
MillenniumDialogTitle { title?.invoke() }
|
||||
title = "",
|
||||
modifier = modifier) {
|
||||
title?.let { it() }
|
||||
content()
|
||||
actions?.let {
|
||||
MillenniumSeparator()
|
||||
it()
|
||||
}
|
||||
MillenniumDialogClose(
|
||||
contentDescription = stringResource(Res.string.common_close_dialog))
|
||||
}
|
||||
MillenniumDialogBody { content() }
|
||||
actions?.let { MillenniumDialogFooter(content = { it() }) }
|
||||
}
|
||||
} else {
|
||||
val sheetState = rememberModalBottomSheetState(initialDetent = SheetDetent.FullyExpanded)
|
||||
@ -93,7 +77,10 @@ fun AdaptiveDialogBottomSheet(
|
||||
}
|
||||
MillenniumModalBottomSheet(state = sheetState) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 16.dp),
|
||||
horizontalAlignment = Alignment.Start) {
|
||||
title?.let {
|
||||
Row(
|
||||
@ -112,13 +99,7 @@ fun AdaptiveDialogBottomSheet(
|
||||
}
|
||||
Spacer(Modifier.height(12.dp))
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 560.dp)
|
||||
.verticalScroll(rememberScrollState())) {
|
||||
content()
|
||||
}
|
||||
actions?.let {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
MillenniumSeparator()
|
||||
@ -126,6 +107,7 @@ fun AdaptiveDialogBottomSheet(
|
||||
it()
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,18 +22,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@ -41,7 +30,6 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.foundation.text.input.clearText
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@ -53,7 +41,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import com.composeunstyled.currentWindowWidthBreakpoint
|
||||
import org.spotube.millennium.theme.MillenniumTheme
|
||||
import org.spotube.millennium.adaptive.WindowBreakpoints
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
@ -70,14 +57,6 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxShare
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.spotube.millennium.composables.MillenniumDropdownMenu
|
||||
import org.spotube.millennium.composables.MillenniumDropdownMenuContent
|
||||
import org.spotube.millennium.composables.MillenniumDropdownMenuEmpty
|
||||
import org.spotube.millennium.composables.MillenniumDropdownMenuFilter
|
||||
import org.spotube.millennium.composables.MillenniumDropdownMenuHeader
|
||||
import org.spotube.millennium.composables.MillenniumDropdownMenuItem
|
||||
import org.spotube.millennium.composables.MillenniumDropdownMenuItemIcon
|
||||
import org.spotube.millennium.composables.MillenniumDropdownMenuSeparator
|
||||
import com.composeunstyled.Text
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.composables.MillenniumIconButton
|
||||
import org.spotube.millennium.composables.MillenniumIconButtonStyle
|
||||
@ -88,7 +67,6 @@ import org.spotube.millennium.composables.MillenniumText
|
||||
import org.spotube.millennium.composables.MillenniumTextField
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
import spotube.composeapp.generated.resources.accessibility_selected
|
||||
import spotube.composeapp.generated.resources.common_no_results
|
||||
import spotube.composeapp.generated.resources.search_filter_placeholder
|
||||
|
||||
data class AdaptiveMenuItem(
|
||||
@ -121,13 +99,17 @@ fun AdaptiveDropdownBottomSheet(
|
||||
|
||||
Box(modifier = modifier) {
|
||||
if (isLargeScreen) {
|
||||
DesktopDropdownMenu(
|
||||
MillenniumDropdownMenu(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it },
|
||||
items = items,
|
||||
menuMinWidth = menuMinWidth,
|
||||
filter = filter,
|
||||
trigger = { trigger { expanded = true } })
|
||||
items = items.map { it.label },
|
||||
onItemClick = { label ->
|
||||
items.firstOrNull { it.label == label && it.enabled }?.onClick?.invoke()
|
||||
expanded = false
|
||||
},
|
||||
minimumWidth = menuMinWidth,
|
||||
anchor = { trigger { expanded = true } }
|
||||
)
|
||||
} else {
|
||||
trigger { expanded = true }
|
||||
if (expanded) {
|
||||
@ -152,7 +134,6 @@ private fun AdaptiveBottomSheetContent(
|
||||
header: @Composable (() -> Unit)?,
|
||||
filter: ((AdaptiveMenuItem, String) -> Boolean)? = null) {
|
||||
val hasSelection = items.any { it.selected }
|
||||
val hasLeading = items.any { it.icon != null || it.leadingContent != null }
|
||||
val queryState = rememberTextFieldState()
|
||||
val query = queryState.text.toString()
|
||||
val filteredItems = if (filter != null && query.isNotBlank()) {
|
||||
@ -166,10 +147,7 @@ private fun AdaptiveBottomSheetContent(
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
MillenniumModalBottomSheet(
|
||||
state = sheetState,
|
||||
contentPadding = PaddingValues(top = 10.dp, bottom = 8.dp),
|
||||
contentSpacing = 4.dp) {
|
||||
MillenniumModalBottomSheet(state = sheetState) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()) {
|
||||
header?.invoke()
|
||||
@ -197,45 +175,25 @@ private fun AdaptiveBottomSheetContent(
|
||||
if (item.dividerBefore) {
|
||||
MillenniumSeparator(modifier = Modifier.padding(vertical = 4.dp))
|
||||
}
|
||||
AdaptiveMenuTile(
|
||||
item = item,
|
||||
hasSelection = hasSelection,
|
||||
hasLeading = hasLeading,
|
||||
textStyle = AppTheme.typography.bodyLarge,
|
||||
minimumHeight = 56.dp,
|
||||
onClick = {
|
||||
item.onClick()
|
||||
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) },
|
||||
title = {
|
||||
MillenniumText(
|
||||
item.label,
|
||||
style = AppTheme.typography.bodyLarge,
|
||||
color = if (item.enabled) {
|
||||
AppTheme.colors.onSurface
|
||||
} else {
|
||||
AppTheme.colors.onSurface.copy(alpha = 0.38f)
|
||||
}
|
||||
)
|
||||
},
|
||||
subtitle = item.subtitle?.let { subtitle ->
|
||||
{
|
||||
MillenniumText(
|
||||
subtitle,
|
||||
style = AppTheme.typography.bodySmall,
|
||||
color = AppTheme.colors.onSurfaceVariant)
|
||||
color = AppTheme.colors.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
leading = when {
|
||||
@ -245,8 +203,8 @@ private fun AdaptiveMenuTile(
|
||||
MillenniumIcon(
|
||||
imageVector = item.icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(22.dp),
|
||||
tint = contentColor)
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
item.selected -> {
|
||||
@ -254,10 +212,11 @@ private fun AdaptiveMenuTile(
|
||||
MillenniumIcon(
|
||||
imageVector = FeatherIcons.Check,
|
||||
contentDescription = stringResource(Res.string.accessibility_selected),
|
||||
modifier = Modifier.size(22.dp))
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
hasSelection || hasLeading -> {{ Spacer(modifier = Modifier.size(22.dp)) }}
|
||||
hasSelection -> {{ Spacer(modifier = Modifier.size(22.dp)) }}
|
||||
else -> null
|
||||
},
|
||||
trailing = if (item.selected && (item.icon != null || item.leadingContent != null)) {
|
||||
@ -265,66 +224,20 @@ private fun AdaptiveMenuTile(
|
||||
MillenniumIcon(
|
||||
imageVector = FeatherIcons.Check,
|
||||
contentDescription = stringResource(Res.string.accessibility_selected),
|
||||
modifier = Modifier.size(22.dp))
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
selected = item.selected,
|
||||
enabled = item.enabled,
|
||||
onClick = onClick,
|
||||
minimumHeight = minimumHeight)
|
||||
}
|
||||
|
||||
/** Desktop popup built from Millennium's dropdown bricks: native icons, descriptions,
|
||||
* selection indicator, separators and an optional search header. */
|
||||
@Composable
|
||||
private fun DesktopDropdownMenu(
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
items: List<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)
|
||||
onClick = {
|
||||
item.onClick()
|
||||
onDismiss()
|
||||
},
|
||||
minimumHeight = 56.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,11 +18,6 @@
|
||||
package dev.krtirtho.spotube.core.ui.component
|
||||
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumDialogHeader
|
||||
import org.spotube.millennium.composables.MillenniumDialogTitle
|
||||
import org.spotube.millennium.composables.MillenniumDialogClose
|
||||
import org.spotube.millennium.composables.MillenniumDialogBody
|
||||
import org.spotube.millennium.composables.MillenniumDialogFooter
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@ -50,9 +45,6 @@ import dev.krtirtho.spotube.core.ui.theming.AppTheme
|
||||
import com.composeunstyled.Text
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.krtirtho.spotube.modules.plugin.MetadataAuthViewModel
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@ -80,16 +72,7 @@ import spotube.composeapp.generated.resources.*
|
||||
fun ErrorDisplay(
|
||||
errorMessage: String,
|
||||
onRetry: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
authViewModel: MetadataAuthViewModel = koinViewModel()) {
|
||||
val authState by authViewModel.uiState.collectAsStateWithLifecycle()
|
||||
if (authState.loginRequired) {
|
||||
LoginRequiredDisplay(
|
||||
pluginName = authState.pluginName,
|
||||
onLogin = authViewModel::login,
|
||||
modifier = modifier)
|
||||
return
|
||||
}
|
||||
modifier: Modifier = Modifier) {
|
||||
var showFullLog by remember { mutableStateOf(false) }
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
@ -208,11 +191,19 @@ private fun ErrorLogDialog(
|
||||
onDismiss: () -> Unit) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
MillenniumDialog(visible = true, onDismissRequest = onDismiss) {
|
||||
MillenniumDialogHeader {
|
||||
MillenniumDialogTitle(
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = onDismiss,
|
||||
title = "") {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = stringResource(Res.string.error_full_log_title),
|
||||
textStyle = AppTheme.typography.titleMedium)
|
||||
style = AppTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold)
|
||||
|
||||
MillenniumIconButton(
|
||||
onClick = {
|
||||
clipboardManager.setText(AnnotatedString(errorMessage))
|
||||
@ -222,9 +213,7 @@ private fun ErrorLogDialog(
|
||||
imageVector = FeatherIcons.Copy,
|
||||
contentDescription = stringResource(Res.string.error_copy_to_clipboard))
|
||||
}
|
||||
MillenniumDialogClose()
|
||||
}
|
||||
MillenniumDialogBody(scrollable = false) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@ -243,17 +232,28 @@ private fun ErrorLogDialog(
|
||||
.padding(12.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
MillenniumDialogFooter {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End) {
|
||||
MillenniumButton(
|
||||
onClick = onDismiss,
|
||||
style = MillenniumButtonStyle.Secondary) {
|
||||
style = MillenniumButtonStyle.Quiet) {
|
||||
Text(stringResource(Res.string.common_close))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ErrorDisplayPreview() {
|
||||
Box(modifier = Modifier.fillMaxSize().background(AppTheme.colors.background)) {
|
||||
ErrorDisplay(
|
||||
errorMessage = "This is an error message",
|
||||
onRetry = {})
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ErrorLogDialogPreview() {
|
||||
|
||||
@ -1,101 +0,0 @@
|
||||
/*
|
||||
* 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.core.ui.component
|
||||
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.composeunstyled.Text
|
||||
import compose.icons.FeatherIcons
|
||||
import compose.icons.feathericons.Lock
|
||||
import compose.icons.feathericons.LogIn
|
||||
import dev.krtirtho.spotube.core.ui.theming.AppTheme
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.spotube.millennium.composables.MillenniumButton
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import spotube.composeapp.generated.resources.*
|
||||
|
||||
/** Friendly "sign in first" state for screens whose metadata plugin needs an account. */
|
||||
@Composable
|
||||
fun LoginRequiredDisplay(
|
||||
pluginName: String?,
|
||||
onLogin: () -> Unit,
|
||||
modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp, vertical = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(88.dp)
|
||||
.clip(CircleShape)
|
||||
.background(AppTheme.colors.primary.copy(alpha = 0.14f)),
|
||||
contentAlignment = Alignment.Center) {
|
||||
MillenniumIcon(
|
||||
imageVector = FeatherIcons.Lock,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(40.dp),
|
||||
tint = AppTheme.colors.primary)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.login_required_title),
|
||||
style = AppTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = AppTheme.colors.onSurface,
|
||||
textAlign = TextAlign.Center)
|
||||
|
||||
Text(
|
||||
text = if (pluginName.isNullOrBlank()) {
|
||||
stringResource(Res.string.login_required_description)
|
||||
} else {
|
||||
stringResource(Res.string.login_required_description_plugin, pluginName)
|
||||
},
|
||||
style = AppTheme.typography.bodyMedium,
|
||||
color = AppTheme.colors.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center)
|
||||
|
||||
MillenniumButton(
|
||||
onClick = onLogin,
|
||||
modifier = Modifier.height(44.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
MillenniumIcon(
|
||||
imageVector = FeatherIcons.LogIn,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp).size(18.dp))
|
||||
Text(stringResource(Res.string.login_required_action))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -28,9 +28,7 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
|
||||
@ -145,10 +143,7 @@ fun TrackOptionsBottomSheet(
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
MillenniumModalBottomSheet(
|
||||
state = sheetState,
|
||||
contentPadding = PaddingValues(top = 10.dp, bottom = 8.dp),
|
||||
contentSpacing = 4.dp) {
|
||||
MillenniumModalBottomSheet(state = sheetState) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
TrackOptionsSheetHeader(track = track)
|
||||
|
||||
@ -189,7 +184,7 @@ fun TrackOptionsBottomSheet(
|
||||
} else {
|
||||
AppTheme.colors.onSurface.copy(alpha = 0.38f)
|
||||
})
|
||||
} ?: Spacer(modifier = Modifier.size(22.dp))
|
||||
}
|
||||
Text(
|
||||
text = item.label,
|
||||
style = AppTheme.typography.bodyLarge,
|
||||
|
||||
@ -42,8 +42,6 @@ import androidx.compose.ui.unit.TextUnit
|
||||
import com.valentinilk.shimmer.ShimmerBounds
|
||||
import com.valentinilk.shimmer.rememberShimmer
|
||||
import com.valentinilk.shimmer.shimmer
|
||||
import org.spotube.millennium.theme.MillenniumTheme
|
||||
import org.spotube.millennium.theme.millenniumBevelBrush
|
||||
|
||||
val LocalSkeletonLoading = compositionLocalOf { false }
|
||||
|
||||
@ -82,16 +80,11 @@ fun SkeletonTree(
|
||||
}
|
||||
|
||||
fun Modifier.shimmerApply(
|
||||
color: Color? = null // defaults to Millennium's recessed skeleton fill
|
||||
color: Color = Color.LightGray.copy(alpha = 0.5f) // Adjust alpha/color to match your theme
|
||||
): Modifier = composed {
|
||||
val isTreeLoading = LocalSkeletonLoading.current
|
||||
val skeletonBrush = millenniumBevelBrush(MillenniumTheme.colors, sunken = true)
|
||||
|
||||
when {
|
||||
!isTreeLoading -> this
|
||||
color != null -> this.background(color)
|
||||
else -> this.background(skeletonBrush)
|
||||
}
|
||||
if (isTreeLoading) this.background(color) else this
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@ -18,7 +18,6 @@
|
||||
package dev.krtirtho.spotube.core.ui.theming
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
@ -36,11 +35,9 @@ import androidx.compose.ui.unit.sp
|
||||
|
||||
import dev.krtirtho.spotube.modules.settings.Theme
|
||||
import dev.krtirtho.spotube.modules.settings.UserSettings
|
||||
import org.spotube.millennium.composables.MillenniumModalBottomSheetTheme
|
||||
import org.spotube.millennium.composables.MillenniumTooltipHost
|
||||
import org.spotube.millennium.composables.MillenniumTopAppBarTheme
|
||||
import org.spotube.millennium.composables.MillenniumTopAppBarThemeProperties
|
||||
import org.spotube.millennium.composables.defaultMillenniumModalBottomSheetTheme
|
||||
import org.spotube.millennium.composables.defaultMillenniumTopAppBarTheme
|
||||
import org.spotube.millennium.theme.MillenniumColors
|
||||
import org.spotube.millennium.theme.MillenniumDarkThemeComponents
|
||||
@ -221,16 +218,14 @@ fun SpotubeTheme(
|
||||
properties = darkTheme.topAppBarTheme.properties.copy(
|
||||
separatorColor = Color.Transparent
|
||||
)
|
||||
),
|
||||
modalBottomSheetTheme = compactModalBottomSheetTheme()
|
||||
)
|
||||
),
|
||||
light = lightTheme.copy(
|
||||
topAppBarTheme = MillenniumTopAppBarTheme(
|
||||
properties = lightTheme.topAppBarTheme.properties.copy(
|
||||
separatorColor = Color.Transparent
|
||||
)
|
||||
),
|
||||
modalBottomSheetTheme = compactModalBottomSheetTheme()
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
@ -246,23 +241,6 @@ fun SpotubeTheme(
|
||||
}
|
||||
}
|
||||
|
||||
/** Library default is 24dp padding + 16dp spacing, which is far too roomy on phones. */
|
||||
private fun compactModalBottomSheetTheme(): MillenniumModalBottomSheetTheme {
|
||||
val padding = PaddingValues(start = 16.dp, end = 16.dp, top = 10.dp, bottom = 8.dp)
|
||||
fun org.spotube.millennium.composables.MillenniumModalBottomSheetThemeProperties.compact() =
|
||||
copy(contentPadding = padding, contentSpacing = 8.dp)
|
||||
val states = defaultMillenniumModalBottomSheetTheme().states
|
||||
return MillenniumModalBottomSheetTheme(
|
||||
states = org.spotube.millennium.theme.MillenniumControlStateProperty(
|
||||
default = states.default.compact(),
|
||||
hovered = states.hovered.compact(),
|
||||
focused = states.focused.compact(),
|
||||
pressed = states.pressed.compact(),
|
||||
disabled = states.disabled.compact(),
|
||||
selected = states.selected.compact(),
|
||||
error = states.error.compact()))
|
||||
}
|
||||
|
||||
/** Whether the active Millennium appearance is light. */
|
||||
@Composable
|
||||
fun isLightAppearance(): Boolean = MillenniumTheme.colors.background.luminance() > 0.5f
|
||||
|
||||
@ -84,6 +84,7 @@ import dev.krtirtho.spotube.core.ui.component.cards.PlayableCard
|
||||
import dev.krtirtho.spotube.core.ui.component.dragScrollable
|
||||
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
||||
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
||||
import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay
|
||||
@ -142,7 +143,8 @@ fun ArtistScreen(
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
.fillMaxSize()
|
||||
.padding(bottom = LocalAppShellBottomInset.current + 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
item {
|
||||
|
||||
@ -18,10 +18,10 @@
|
||||
package dev.krtirtho.spotube.modules.devices
|
||||
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumSpokeSpinner
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.composables.MillenniumButton
|
||||
import org.spotube.millennium.composables.MillenniumButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
|
||||
import org.spotube.millennium.theme.MillenniumTheme
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@ -103,9 +103,10 @@ fun DevicesScreen(
|
||||
title = { Text(stringResource(Res.string.devices_title)) },
|
||||
actions = {
|
||||
if (isDiscovering && connectingToDevice == null) {
|
||||
MillenniumSpokeSpinner(
|
||||
size = 24.dp,
|
||||
modifier = Modifier.padding(end = 8.dp))
|
||||
MillenniumIndeterminateProgress(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.padding(end = 8.dp))
|
||||
} else {
|
||||
MillenniumIcon(
|
||||
imageVector = Iconsax.IconsaxRefreshRight,
|
||||
@ -176,7 +177,8 @@ fun DevicesScreen(
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
MillenniumSpokeSpinner(size = 24.dp)
|
||||
MillenniumIndeterminateProgress(
|
||||
modifier = Modifier.size(24.dp))
|
||||
Text(
|
||||
text = stringResource(Res.string.devices_connecting),
|
||||
style = AppTheme.typography.bodyMedium)
|
||||
@ -248,7 +250,8 @@ private fun DeviceRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
if (isConnecting) {
|
||||
MillenniumSpokeSpinner(size = 24.dp)
|
||||
MillenniumIndeterminateProgress(
|
||||
modifier = Modifier.size(24.dp))
|
||||
} else {
|
||||
MillenniumIcon(
|
||||
imageVector = Iconsax.IconsaxMirroringScreen,
|
||||
|
||||
@ -18,11 +18,6 @@
|
||||
package dev.krtirtho.spotube.modules.devices
|
||||
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumDialogHeader
|
||||
import org.spotube.millennium.composables.MillenniumDialogTitle
|
||||
import org.spotube.millennium.composables.MillenniumDialogClose
|
||||
import org.spotube.millennium.composables.MillenniumDialogBody
|
||||
import org.spotube.millennium.composables.MillenniumDialogFooter
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.composables.MillenniumDialog
|
||||
import org.spotube.millennium.composables.MillenniumListTile
|
||||
@ -87,12 +82,10 @@ fun PlayDestinationPickerHost() {
|
||||
PlaybackDestinationAction.PlayNext -> stringResource(Res.string.devices_destination_action_play_next)
|
||||
}
|
||||
|
||||
MillenniumDialog(visible = true, onDismissRequest = controller::dismissPicker) {
|
||||
MillenniumDialogHeader {
|
||||
MillenniumDialogTitle(stringResource(Res.string.devices_destination_title))
|
||||
MillenniumDialogClose()
|
||||
}
|
||||
MillenniumDialogBody {
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = controller::dismissPicker,
|
||||
title = stringResource(Res.string.devices_destination_title)) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
@ -145,9 +138,6 @@ fun PlayDestinationPickerHost() {
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
MillenniumDialogFooter {
|
||||
MillenniumButton(
|
||||
onClick = controller::dismissPicker,
|
||||
style = MillenniumButtonStyle.Quiet) {
|
||||
|
||||
@ -18,8 +18,6 @@
|
||||
package dev.krtirtho.spotube.modules.devices
|
||||
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumDropdownMenuItem
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemove
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.composables.MillenniumIconButton
|
||||
import org.spotube.millennium.composables.MillenniumIconButtonStyle
|
||||
@ -606,6 +604,11 @@ private fun RemoteQueueItemRow(
|
||||
MillenniumDropdownMenu(
|
||||
expanded = showMenu,
|
||||
onExpandedChange = { showMenu = it },
|
||||
items = listOf(stringResource(Res.string.devices_remote_remove_from_queue)),
|
||||
onItemClick = {
|
||||
onRemoveClick()
|
||||
showMenu = false
|
||||
},
|
||||
anchor = {
|
||||
MillenniumIconButton(
|
||||
onClick = { showMenu = true },
|
||||
@ -617,17 +620,8 @@ private fun RemoteQueueItemRow(
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}) {
|
||||
MillenniumDropdownMenuItem(
|
||||
label = stringResource(Res.string.devices_remote_remove_from_queue),
|
||||
icon = Iconsax.IconsaxMusicSquareRemove,
|
||||
destructive = true,
|
||||
onClick = {
|
||||
onRemoveClick()
|
||||
showMenu = false
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
*/
|
||||
package dev.krtirtho.spotube.modules.downloads
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumStripedProgress
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@ -43,6 +42,7 @@ import org.spotube.millennium.composables.MillenniumButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.composables.MillenniumIconButton
|
||||
import org.spotube.millennium.composables.MillenniumIconButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumProgress
|
||||
import org.spotube.millennium.theme.MillenniumTheme
|
||||
import org.spotube.millennium.theme.millenniumPanelBrush
|
||||
import org.spotube.millennium.theme.millenniumSurface
|
||||
@ -225,10 +225,9 @@ internal fun DownloadListRow(
|
||||
},
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (item.status == DownloadStatus.Downloading) MillenniumStripedProgress(
|
||||
progress = item.progress,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
height = 6.dp
|
||||
if (item.status == DownloadStatus.Downloading) MillenniumProgress(
|
||||
item.progress,
|
||||
Modifier.fillMaxWidth().height(4.dp)
|
||||
)
|
||||
}
|
||||
MillenniumIconButton(
|
||||
|
||||
@ -17,9 +17,6 @@
|
||||
|
||||
package dev.krtirtho.spotube.modules.jam
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumDropdownMenuItem
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxUserRemove
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxForbidden
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@ -424,27 +421,25 @@ internal fun SessionPanel(
|
||||
)
|
||||
}
|
||||
if (participant.canModerate) {
|
||||
val removeLabel = stringResource(Res.string.jam_participants_remove)
|
||||
val banLabel = stringResource(Res.string.jam_participants_ban)
|
||||
// Keep destructive moderation out of the participant's primary row.
|
||||
org.spotube.millennium.composables.MillenniumDropdownMenu(
|
||||
expanded = participant.menuExpanded,
|
||||
onExpandedChange = { expanded -> onMenuChange(if (expanded) participant.id else null) },
|
||||
items = listOf(removeLabel, banLabel),
|
||||
onItemClick = { label ->
|
||||
if (label == removeLabel) onKick(participant.id) else onBan(
|
||||
participant.id
|
||||
)
|
||||
},
|
||||
anchor = {
|
||||
MillenniumButton(
|
||||
onClick = { onMenuChange(participant.id) },
|
||||
style = MillenniumButtonStyle.Secondary
|
||||
) { Text(stringResource(Res.string.jam_participants_manage)) }
|
||||
}
|
||||
) {
|
||||
MillenniumDropdownMenuItem(
|
||||
label = stringResource(Res.string.jam_participants_remove),
|
||||
icon = Iconsax.IconsaxUserRemove,
|
||||
onClick = { onKick(participant.id) })
|
||||
MillenniumDropdownMenuItem(
|
||||
label = stringResource(Res.string.jam_participants_ban),
|
||||
icon = Iconsax.IconsaxForbidden,
|
||||
destructive = true,
|
||||
onClick = { onBan(participant.id) })
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
|
||||
package dev.krtirtho.spotube.modules.jam
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumAlertDialog
|
||||
import dev.krtirtho.spotube.core.ui.theming.AppTheme
|
||||
import com.composeunstyled.Text
|
||||
import org.spotube.millennium.composables.MillenniumButton
|
||||
@ -48,11 +47,14 @@ fun PlayDestinationPicker(
|
||||
onSuggestToJam: () -> Unit) {
|
||||
if (!visible) return
|
||||
|
||||
MillenniumAlertDialog(
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(Res.string.jam_destination_title)) },
|
||||
description = { Text(stringResource(Res.string.jam_destination_message)) }) {
|
||||
title = stringResource(Res.string.jam_destination_title)) {
|
||||
Text(
|
||||
text = stringResource(Res.string.jam_destination_message),
|
||||
style = AppTheme.typography.bodyMedium)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
MillenniumButton(
|
||||
onClick = onDismiss,
|
||||
style = MillenniumButtonStyle.Quiet) {
|
||||
@ -60,11 +62,12 @@ fun PlayDestinationPicker(
|
||||
}
|
||||
MillenniumButton(
|
||||
onClick = onPlayLocally,
|
||||
style = MillenniumButtonStyle.Secondary) {
|
||||
style = MillenniumButtonStyle.Quiet) {
|
||||
Text(stringResource(Res.string.jam_destination_play_here))
|
||||
}
|
||||
MillenniumButton(onClick = onSuggestToJam) {
|
||||
Text(stringResource(Res.string.jam_destination_suggest))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,7 +57,6 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxCloseSquare
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxRefreshRight
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import dev.krtirtho.spotube.modules.plugin.guard.MetadataPluginGuard
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.koinInject
|
||||
import org.spotube.millennium.adaptive.WindowBreakpoints
|
||||
@ -117,20 +116,14 @@ fun LibraryScreen(libraryState: LibraryState = koinInject()) {
|
||||
}
|
||||
Column(Modifier.weight(1f).padding(horizontal = 12.dp)) {
|
||||
when (state.currentTab) {
|
||||
LibraryTab.Playlists -> MetadataPluginGuard(showTopBar = false, requireLogin = true) {
|
||||
LibraryPlaylistsScreen(
|
||||
LibraryTab.Playlists -> LibraryPlaylistsScreen(
|
||||
showCreatePlaylist = state.showCreatePlaylist,
|
||||
onDismissCreatePlaylist = libraryState::dismissCreatePlaylist,
|
||||
viewMode = viewMode
|
||||
)
|
||||
}
|
||||
|
||||
LibraryTab.Albums -> MetadataPluginGuard(showTopBar = false, requireLogin = true) {
|
||||
LibraryAlbumsScreen(viewMode = viewMode)
|
||||
}
|
||||
LibraryTab.Artists -> MetadataPluginGuard(showTopBar = false, requireLogin = true) {
|
||||
LibraryArtistsScreen(viewMode = viewMode)
|
||||
}
|
||||
LibraryTab.Albums -> LibraryAlbumsScreen(viewMode = viewMode)
|
||||
LibraryTab.Artists -> LibraryArtistsScreen(viewMode = viewMode)
|
||||
LibraryTab.LocalTracks -> LibraryLocalTracksScreen()
|
||||
LibraryTab.Downloads -> DownloadsScreen()
|
||||
}
|
||||
|
||||
@ -3,8 +3,6 @@
|
||||
*/
|
||||
package dev.krtirtho.spotube.modules.library.local_tracks
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumCircularProgress
|
||||
import org.spotube.millennium.composables.MillenniumSpokeSpinner
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@ -38,9 +36,6 @@ import dev.krtirtho.spotube.core.ui.component.cards.MediaEmptyState
|
||||
import dev.krtirtho.spotube.core.ui.component.cards.PlayableListTile
|
||||
import dev.krtirtho.spotube.core.ui.theming.AppTheme
|
||||
import dev.krtirtho.spotube.modules.library.local_tracks.media.rememberLocalMediaPermissionState
|
||||
import dev.krtirtho.spotube.modules.settings.LocalMediaFoldersViewModel
|
||||
import dev.krtirtho.spotube.modules.settings.components.LocalMediaFoldersDialogHost
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxFolderAdd
|
||||
import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxFolderOpen
|
||||
@ -53,6 +48,7 @@ import org.spotube.millennium.composables.MillenniumButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.composables.MillenniumIconButton
|
||||
import org.spotube.millennium.composables.MillenniumIconButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
|
||||
import org.spotube.millennium.composables.MillenniumListTile
|
||||
import org.spotube.millennium.theme.MillenniumTheme
|
||||
import org.spotube.millennium.theme.millenniumPanelBrush
|
||||
@ -62,8 +58,7 @@ import spotube.composeapp.generated.resources.*
|
||||
@Composable
|
||||
fun LibraryLocalTracksScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: LibraryLocalTracksViewModel = koinViewModel(),
|
||||
foldersViewModel: LocalMediaFoldersViewModel = koinViewModel()
|
||||
viewModel: LibraryLocalTracksViewModel = koinViewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val permission = rememberLocalMediaPermissionState()
|
||||
@ -71,10 +66,8 @@ fun LibraryLocalTracksScreen(
|
||||
LaunchedEffect(permission.isGranted) { viewModel.onPermissionChanged(permission.isGranted) }
|
||||
LocalTracksContent(
|
||||
state, viewModel::openFolder, viewModel::playTrack, viewModel::navigateUp,
|
||||
viewModel::playCurrentFolder, permission.requestPermission, modifier,
|
||||
onManageFolders = foldersViewModel::open
|
||||
viewModel::playCurrentFolder, permission.requestPermission, modifier
|
||||
)
|
||||
LocalMediaFoldersDialogHost(foldersViewModel)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -85,15 +78,14 @@ internal fun LocalTracksContent(
|
||||
onNavigateUp: () -> Unit,
|
||||
onPlayFolder: () -> Unit,
|
||||
onRequestPermission: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onManageFolders: () -> Unit = {}
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val inset = LocalAppShellBottomInset.current
|
||||
Box(modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
|
||||
Column(
|
||||
Modifier.widthIn(max = 960.dp).fillMaxSize()
|
||||
) {
|
||||
LocalTracksBreadcrumb(state, onNavigateUp, onPlayFolder, onManageFolders = onManageFolders)
|
||||
LocalTracksBreadcrumb(state, onNavigateUp, onPlayFolder)
|
||||
when {
|
||||
!state.canAccessMedia -> MediaEmptyState(
|
||||
Iconsax.IconsaxFolderOpen,
|
||||
@ -114,7 +106,7 @@ internal fun LocalTracksContent(
|
||||
Modifier.fillMaxSize().padding(bottom = inset),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
MillenniumCircularProgress(size = 40.dp)
|
||||
MillenniumIndeterminateProgress()
|
||||
}
|
||||
|
||||
state.currentFolderPath == null && state.visibleFolders.isNotEmpty() -> LazyColumn(
|
||||
@ -151,9 +143,7 @@ internal fun LocalTracksContent(
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
val noFolders = state.currentFolderPath == null && state.query.isBlank()
|
||||
MediaEmptyState(
|
||||
else -> MediaEmptyState(
|
||||
Iconsax.IconsaxFolderOpen,
|
||||
state.error ?: stringResource(
|
||||
when {
|
||||
@ -161,25 +151,8 @@ internal fun LocalTracksContent(
|
||||
state.currentFolderPath == null -> Res.string.library_local_empty_folders
|
||||
else -> Res.string.library_local_empty_tracks
|
||||
}
|
||||
), Modifier.padding(bottom = inset),
|
||||
description = if (noFolders && state.error == null)
|
||||
stringResource(Res.string.library_local_empty_folders_description) else null,
|
||||
action = if (noFolders) ({
|
||||
MillenniumButton(onClick = onManageFolders) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
MillenniumIcon(
|
||||
Iconsax.IconsaxFolderAdd, null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
), Modifier.padding(bottom = inset)
|
||||
)
|
||||
Text(stringResource(Res.string.library_local_add_folder))
|
||||
}
|
||||
}
|
||||
}) else null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -190,8 +163,7 @@ internal fun LocalTracksBreadcrumb(
|
||||
state: LibraryLocalTracksState,
|
||||
onNavigateUp: () -> Unit,
|
||||
onPlayFolder: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onManageFolders: (() -> Unit)? = null
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val backDescription = stringResource(Res.string.library_local_back)
|
||||
Row(
|
||||
@ -265,17 +237,7 @@ internal fun LocalTracksBreadcrumb(
|
||||
style = AppTheme.typography.labelSmall, color = AppTheme.colors.onSurfaceVariant,
|
||||
maxLines = 1
|
||||
)
|
||||
if (state.isRefreshing) MillenniumSpokeSpinner(size = 14.dp)
|
||||
if (state.currentFolderPath == null && onManageFolders != null) MillenniumIconButton(
|
||||
onClick = onManageFolders,
|
||||
modifier = Modifier.size(28.dp), style = MillenniumIconButtonStyle.Quiet
|
||||
) {
|
||||
MillenniumIcon(
|
||||
Iconsax.IconsaxFolderAdd,
|
||||
stringResource(Res.string.library_local_add_folder),
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
}
|
||||
if (state.isRefreshing) MillenniumIndeterminateProgress(Modifier.size(14.dp))
|
||||
if (state.currentFolderPath != null) MillenniumIconButton(
|
||||
onClick = onPlayFolder, enabled = state.canPlayFolder,
|
||||
modifier = Modifier.size(28.dp), style = MillenniumIconButtonStyle.Quiet
|
||||
|
||||
@ -18,7 +18,6 @@
|
||||
package dev.krtirtho.spotube.modules.library.playlist
|
||||
|
||||
|
||||
import dev.krtirtho.spotube.core.ui.component.AdaptiveDialogBottomSheet
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.composables.MillenniumButton
|
||||
import org.spotube.millennium.composables.MillenniumButtonStyle
|
||||
@ -155,27 +154,36 @@ fun AddToPlaylistPicker(
|
||||
}
|
||||
}
|
||||
|
||||
val pickerTitle = stringResource(Res.string.library_add_to_playlist_title)
|
||||
AdaptiveDialogBottomSheet(
|
||||
onDismiss = onDismiss,
|
||||
modifier = modifier,
|
||||
dialogTitle = pickerTitle,
|
||||
title = { Text(pickerTitle, style = AppTheme.typography.headlineSmall) },
|
||||
actions = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End) {
|
||||
if (isLargeScreen) {
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = onDismiss,
|
||||
title = stringResource(Res.string.library_add_to_playlist_title),
|
||||
modifier = modifier) {
|
||||
Box(modifier = Modifier.fillMaxWidth().height(400.dp)) {
|
||||
content()
|
||||
}
|
||||
MillenniumButton(
|
||||
onClick = onDismiss,
|
||||
style = MillenniumButtonStyle.Secondary) {
|
||||
Text(stringResource(Res.string.common_cancel))
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Box(modifier = Modifier.fillMaxWidth().height(400.dp)) {
|
||||
} else {
|
||||
val sheetState = rememberModalBottomSheetState(initialDetent = SheetDetent.FullyExpanded)
|
||||
MillenniumModalBottomSheet(state = sheetState) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(Res.string.library_add_to_playlist_title),
|
||||
style = AppTheme.typography.headlineSmall,
|
||||
modifier = Modifier.padding(bottom = 16.dp))
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
|
||||
package dev.krtirtho.spotube.modules.library.playlist
|
||||
|
||||
import dev.krtirtho.spotube.core.ui.component.AdaptiveDialogBottomSheet
|
||||
import androidx.compose.foundation.clickable
|
||||
import org.spotube.millennium.composables.MillenniumButton
|
||||
import org.spotube.millennium.composables.MillenniumButtonStyle
|
||||
@ -163,32 +162,13 @@ fun PlaylistFormSheet(
|
||||
|
||||
val handleImagePick = { imagePicker.launch() }
|
||||
|
||||
val formTitle = stringResource(
|
||||
if (isEditing) Res.string.library_playlist_form_edit_title else Res.string.library_playlist_form_create_title)
|
||||
AdaptiveDialogBottomSheet(
|
||||
onDismiss = onDismiss,
|
||||
modifier = modifier,
|
||||
dialogTitle = formTitle,
|
||||
title = { Text(formTitle, style = AppTheme.typography.headlineSmall) },
|
||||
actions = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End)) {
|
||||
MillenniumButton(
|
||||
onClick = onDismiss,
|
||||
style = MillenniumButtonStyle.Secondary) {
|
||||
Text(stringResource(Res.string.common_cancel))
|
||||
}
|
||||
MillenniumButton(
|
||||
onClick = {
|
||||
onSubmit(currentData)
|
||||
onDismiss()
|
||||
},
|
||||
enabled = name.isNotBlank()) {
|
||||
Text(stringResource(if (isEditing) Res.string.common_save else Res.string.library_playlist_form_create))
|
||||
}
|
||||
}
|
||||
}) {
|
||||
if (isLargeScreen) {
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = onDismiss,
|
||||
title = stringResource(if (isEditing) Res.string.library_playlist_form_edit_title else Res.string.library_playlist_form_create_title),
|
||||
modifier = modifier
|
||||
) {
|
||||
PlaylistFormFields(
|
||||
nameState = nameState,
|
||||
descriptionState = descriptionState,
|
||||
@ -198,7 +178,74 @@ fun PlaylistFormSheet(
|
||||
onIsCollaboratingChange = { isCollaborating = it },
|
||||
imageBase64 = imageBase64,
|
||||
imagePreviewUrl = imagePreviewUrl,
|
||||
onPickImage = handleImagePick)
|
||||
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(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End)
|
||||
) {
|
||||
MillenniumButton(
|
||||
onClick = onDismiss,
|
||||
style = MillenniumButtonStyle.Secondary
|
||||
) {
|
||||
Text(stringResource(Res.string.common_cancel))
|
||||
}
|
||||
MillenniumButton(
|
||||
onClick = {
|
||||
onSubmit(currentData)
|
||||
onDismiss()
|
||||
},
|
||||
enabled = name.isNotBlank()
|
||||
) {
|
||||
Text(stringResource(if (isEditing) Res.string.common_save else Res.string.library_playlist_form_create))
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -18,10 +18,10 @@
|
||||
package dev.krtirtho.spotube.modules.lyrics
|
||||
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumBarsLoader
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.composables.MillenniumIconButton
|
||||
import org.spotube.millennium.composables.MillenniumIconButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@ -138,7 +138,7 @@ fun LyricsScreen(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
MillenniumBarsLoader(size = 48.dp)
|
||||
MillenniumIndeterminateProgress(modifier = Modifier.fillMaxWidth(0.4f))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -18,15 +18,13 @@
|
||||
package dev.krtirtho.spotube.modules.playlist
|
||||
|
||||
|
||||
import dev.krtirtho.spotube.core.ui.component.AdaptiveDialogBottomSheet
|
||||
import org.spotube.millennium.composables.MillenniumCircularProgress
|
||||
import org.spotube.millennium.composables.MillenniumSpokeSpinner
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.composables.MillenniumButton
|
||||
import org.spotube.millennium.composables.MillenniumButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumDialog
|
||||
import org.spotube.millennium.composables.MillenniumIconButton
|
||||
import org.spotube.millennium.composables.MillenniumIconButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
|
||||
import org.spotube.millennium.composables.MillenniumModalBottomSheet
|
||||
import org.spotube.millennium.composables.MillenniumTextField
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@ -171,7 +169,7 @@ fun AddTracksToPlaylistDialog(
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
contentAlignment = Alignment.Center) {
|
||||
MillenniumCircularProgress(size = 40.dp)
|
||||
MillenniumIndeterminateProgress()
|
||||
}
|
||||
}
|
||||
hasSearched && searchResults.isEmpty() -> {
|
||||
@ -211,25 +209,35 @@ fun AddTracksToPlaylistDialog(
|
||||
}
|
||||
}
|
||||
|
||||
val dialogTitle = stringResource(Res.string.playlist_add_tracks_title)
|
||||
AdaptiveDialogBottomSheet(
|
||||
onDismiss = onDismiss,
|
||||
modifier = modifier,
|
||||
dialogTitle = dialogTitle,
|
||||
title = { Text(dialogTitle, style = AppTheme.typography.headlineSmall) },
|
||||
actions = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End) {
|
||||
if (isLargeScreen) {
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = onDismiss,
|
||||
title = stringResource(Res.string.playlist_add_tracks_title),
|
||||
modifier = modifier) {
|
||||
Box(modifier = Modifier.fillMaxWidth().height(500.dp)) {
|
||||
content()
|
||||
}
|
||||
MillenniumButton(onClick = onDismiss) {
|
||||
Text(stringResource(Res.string.playlist_add_tracks_done))
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Box(modifier = Modifier.fillMaxWidth().height(500.dp)) {
|
||||
} else {
|
||||
val sheetState = rememberModalBottomSheetState(initialDetent = SheetDetent.FullyExpanded)
|
||||
MillenniumModalBottomSheet(state = sheetState) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.height(600.dp)) {
|
||||
Text(
|
||||
text = stringResource(Res.string.playlist_add_tracks_title),
|
||||
style = AppTheme.typography.headlineSmall,
|
||||
modifier = Modifier.padding(bottom = 16.dp))
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -289,7 +297,8 @@ private fun TrackSearchResultRow(
|
||||
modifier = Modifier.size(24.dp))
|
||||
}
|
||||
isAdding -> {
|
||||
MillenniumSpokeSpinner(size = 24.dp)
|
||||
MillenniumIndeterminateProgress(
|
||||
modifier = Modifier.size(24.dp))
|
||||
}
|
||||
else -> {
|
||||
MillenniumButton(
|
||||
|
||||
@ -1,80 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dev.krtirtho.spotube.core.di.injectLogger
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
||||
data class MetadataAuthUiState(
|
||||
/** True when the selected metadata plugin needs an account and the user isn't signed in. */
|
||||
val loginRequired: Boolean = false,
|
||||
val pluginName: String? = null)
|
||||
|
||||
/**
|
||||
* Tells screens whether the selected metadata plugin is waiting for a login, so they can show a
|
||||
* friendly sign-in prompt instead of surfacing the plugin's raw authorization exception.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MetadataAuthViewModel(
|
||||
private val pluginManager: PluginManager) : ViewModel(), KoinComponent {
|
||||
private val logger by injectLogger<MetadataAuthViewModel>()
|
||||
|
||||
val uiState: StateFlow<MetadataAuthUiState> = combine(
|
||||
pluginManager.selectedMetadataPlugin.flatMapLatest { service ->
|
||||
if (service == null) flowOf(false) else loginRequiredFlow(service)
|
||||
},
|
||||
pluginManager.state) { loginRequired, managerState ->
|
||||
val name = (managerState as? PluginManagerStates.Data)
|
||||
?.selectedPlugins?.get(PluginAbility.METADATA)?.name
|
||||
MetadataAuthUiState(loginRequired = loginRequired, pluginName = name)
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), MetadataAuthUiState())
|
||||
|
||||
private fun loginRequiredFlow(service: dev.krtirtho.spotube.core.zipline.PluginService): Flow<Boolean> = flow {
|
||||
val requiresAuth = try {
|
||||
service.use { coreAPI.requiresAuthentication }
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
false
|
||||
}
|
||||
if (!requiresAuth) {
|
||||
emit(false)
|
||||
} else {
|
||||
service.loggedInFlow.collect { emit(!it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun login() {
|
||||
val service = pluginManager.selectedMetadataPlugin.value ?: return
|
||||
pluginManager.launchTask {
|
||||
runCatching { service.use { coreAPI.login() } }
|
||||
.onFailure { logger.e(it) { "Metadata plugin login failed" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -18,15 +18,13 @@
|
||||
package dev.krtirtho.spotube.modules.plugin
|
||||
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumCircularProgress
|
||||
import org.spotube.millennium.composables.MillenniumDotsLoader
|
||||
import org.spotube.millennium.composables.MillenniumSpokeSpinner
|
||||
import org.spotube.millennium.composables.MillenniumButton
|
||||
import org.spotube.millennium.composables.MillenniumButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumDialog
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.composables.MillenniumIconButton
|
||||
import org.spotube.millennium.composables.MillenniumIconButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
|
||||
import org.spotube.millennium.composables.MillenniumSeparator
|
||||
import org.spotube.millennium.composables.MillenniumSegmentedButton
|
||||
import org.spotube.millennium.composables.MillenniumSegmentedButtonDefaults
|
||||
@ -87,22 +85,6 @@ import dev.krtirtho.spotube.core.ui.component.HeaderDisplayMode
|
||||
import dev.krtirtho.spotube.getPlatform
|
||||
import dev.krtirtho.spotube.modules.plugin.components.DefaultAbilityPluginGrid
|
||||
import dev.krtirtho.spotube.modules.plugin.components.PluginCard
|
||||
import dev.krtirtho.spotube.modules.plugin.components.PluginDialogActions
|
||||
import dev.krtirtho.spotube.modules.plugin.components.PluginDialogBody
|
||||
import dev.krtirtho.spotube.modules.plugin.components.PluginDialogTitle
|
||||
import dev.krtirtho.spotube.modules.plugin.components.PluginIconTile
|
||||
import dev.krtirtho.spotube.modules.plugin.components.PluginLogoTile
|
||||
import dev.krtirtho.spotube.modules.plugin.components.PluginPanel
|
||||
import dev.krtirtho.spotube.modules.plugin.components.PluginPill
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxBoxAdd
|
||||
import dev.krtirtho.spotube.resources.iconsax.User
|
||||
import compose.icons.FeatherIcons
|
||||
import compose.icons.feathericons.ChevronRight
|
||||
import spotube.composeapp.generated.resources.plugin_install_sheet_subtitle
|
||||
import spotube.composeapp.generated.resources.plugin_install_file_hint
|
||||
import spotube.composeapp.generated.resources.plugin_install_or
|
||||
import spotube.composeapp.generated.resources.plugin_info_dialog_title
|
||||
import spotube.composeapp.generated.resources.plugin_version_label
|
||||
import dev.krtirtho.spotube.modules.plugin.components.PluginInstallDialog
|
||||
import dev.krtirtho.spotube.modules.plugin.components.PluginLogo
|
||||
import dev.krtirtho.spotube.modules.plugin.components.PluginPermissionDialog
|
||||
@ -285,7 +267,7 @@ internal fun PluginScreenContent(
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) { MillenniumCircularProgress(size = 40.dp) }
|
||||
) { MillenniumIndeterminateProgress(modifier = Modifier.widthIn(max = 160.dp)) }
|
||||
}
|
||||
|
||||
is PluginUiState.Data -> {
|
||||
@ -510,7 +492,7 @@ internal fun PluginScreenContent(
|
||||
Modifier.fillMaxWidth().padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
MillenniumDotsLoader()
|
||||
MillenniumIndeterminateProgress(Modifier.widthIn(max = 120.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -542,7 +524,7 @@ internal fun PluginScreenContent(
|
||||
.padding(vertical = 16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
MillenniumDotsLoader()
|
||||
MillenniumIndeterminateProgress(modifier = Modifier.widthIn(max = 120.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -624,17 +606,24 @@ private fun InstallSheet(
|
||||
AdaptiveDialogBottomSheet(
|
||||
onDismiss = onDismiss,
|
||||
title = {
|
||||
PluginDialogTitle(
|
||||
title = stringResource(Res.string.plugin_install_section_title),
|
||||
icon = Iconsax.IconsaxBoxAdd)
|
||||
},
|
||||
dialogTitle = stringResource(Res.string.plugin_install_section_title)) {
|
||||
PluginDialogBody {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
MillenniumIcon(
|
||||
Iconsax.IconsaxImportArrow2Bulk,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = AppTheme.colors.primary
|
||||
)
|
||||
Text(
|
||||
stringResource(Res.string.plugin_install_sheet_subtitle),
|
||||
style = AppTheme.typography.bodySmall,
|
||||
color = AppTheme.colors.onSurfaceVariant)
|
||||
|
||||
stringResource(Res.string.plugin_install_section_title),
|
||||
style = AppTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
val urlState = rememberTextFieldState(urlInput)
|
||||
LaunchedEffect(urlInput) {
|
||||
if (urlState.text.toString() != urlInput) {
|
||||
@ -647,82 +636,51 @@ private fun InstallSheet(
|
||||
}
|
||||
}
|
||||
|
||||
PluginPanel {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(14.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
PluginIconTile(Iconsax.IconsaxLink)
|
||||
Text(
|
||||
stringResource(Res.string.plugin_section_url_title),
|
||||
style = AppTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
MillenniumTextField(
|
||||
state = urlState,
|
||||
label = "",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = stringResource(Res.string.plugin_section_url_title),
|
||||
modifier = Modifier.weight(1f),
|
||||
placeholder = stringResource(Res.string.plugin_url_placeholder),
|
||||
error = urlError?.text())
|
||||
MillenniumButton(
|
||||
MillenniumIconButton(
|
||||
onClick = onSubmitUrl,
|
||||
enabled = !isLoadingUrl,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = MillenniumButtonStyle.Primary) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
style = MillenniumIconButtonStyle.Secondary) {
|
||||
if (isLoadingUrl) {
|
||||
MillenniumSpokeSpinner(size = 16.dp, color = MillenniumTheme.colors.onAccent)
|
||||
MillenniumIndeterminateProgress(modifier = Modifier.size(16.dp))
|
||||
} else {
|
||||
MillenniumIcon(
|
||||
Iconsax.IconsaxImportArrow2Bulk,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp))
|
||||
}
|
||||
Text(stringResource(Res.string.plugin_action_download))
|
||||
}
|
||||
contentDescription = stringResource(Res.string.plugin_action_download))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
MillenniumSeparator()
|
||||
|
||||
Text(
|
||||
stringResource(Res.string.plugin_section_file_title),
|
||||
style = AppTheme.typography.labelLarge,
|
||||
color = AppTheme.colors.onSurfaceVariant
|
||||
)
|
||||
MillenniumButton(
|
||||
onClick = onPickFile,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
MillenniumSeparator(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
stringResource(Res.string.plugin_install_or),
|
||||
style = AppTheme.typography.labelSmall,
|
||||
color = AppTheme.colors.onSurfaceVariant)
|
||||
MillenniumSeparator(modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
PluginPanel(modifier = Modifier.clickable(onClick = onPickFile)) {
|
||||
style = MillenniumButtonStyle.Secondary) {
|
||||
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)
|
||||
}
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
MillenniumIcon(
|
||||
FeatherIcons.ChevronRight,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = AppTheme.colors.onSurfaceVariant)
|
||||
Iconsax.IconsaxExportArrowBulk,
|
||||
contentDescription = stringResource(Res.string.plugin_action_install_from_file)
|
||||
)
|
||||
Text(stringResource(Res.string.plugin_action_install_from_file))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -734,80 +692,82 @@ private fun PluginInfoDialog(
|
||||
plugin: PluginEntry,
|
||||
logoPath: Path?,
|
||||
onDismiss: () -> Unit) {
|
||||
AdaptiveDialogBottomSheet(
|
||||
onDismiss = onDismiss,
|
||||
title = { PluginDialogTitle(stringResource(Res.string.plugin_info_dialog_title)) },
|
||||
dialogTitle = stringResource(Res.string.plugin_info_dialog_title),
|
||||
actions = {
|
||||
PluginDialogActions(
|
||||
secondaryLabel = stringResource(Res.string.common_close),
|
||||
onSecondary = onDismiss)
|
||||
}) {
|
||||
PluginDialogBody {
|
||||
PluginPanel {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||
PluginLogoTile(plugin = plugin, logoPath = logoPath)
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = onDismiss,
|
||||
title = plugin.name) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(AppTheme.colors.primary.copy(alpha = 0.12f))
|
||||
) {
|
||||
PluginLogo(
|
||||
plugin = plugin,
|
||||
logoPath = logoPath,
|
||||
contentDescription = plugin.name,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
fallback = {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
MillenniumIcon(
|
||||
Iconsax.IconsaxBox,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = AppTheme.colors.primary
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(
|
||||
plugin.name,
|
||||
style = AppTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
PluginPill(plugin.author, icon = Iconsax.User)
|
||||
PluginPill(
|
||||
stringResource(Res.string.plugin_version_label, plugin.version),
|
||||
tint = AppTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (plugin.description.isNotBlank()) {
|
||||
Text(
|
||||
plugin.description,
|
||||
modifier = Modifier.padding(start = 14.dp, end = 14.dp, bottom = 14.dp),
|
||||
style = AppTheme.typography.bodySmall,
|
||||
color = AppTheme.colors.onSurfaceVariant)
|
||||
}
|
||||
style = AppTheme.typography.bodyMedium,
|
||||
color = AppTheme.colors.onSurface
|
||||
)
|
||||
}
|
||||
|
||||
PluginPanel {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(14.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
DetailRow(stringResource(Res.string.plugin_info_version), plugin.version)
|
||||
DetailRow(stringResource(Res.string.plugin_info_api_version), plugin.apiVersion)
|
||||
DetailRow(stringResource(Res.string.plugin_info_author), plugin.author)
|
||||
if (plugin.license.isNotBlank()) {
|
||||
DetailRow(stringResource(Res.string.plugin_info_license), plugin.license)
|
||||
}
|
||||
|
||||
if (plugin.capabilities.isNotEmpty()) {
|
||||
DetailChipsRow(
|
||||
stringResource(Res.string.plugin_info_capabilities),
|
||||
plugin.capabilities.map { it.name })
|
||||
}
|
||||
|
||||
if (plugin.abilities.isNotEmpty()) {
|
||||
DetailChipsRow(
|
||||
stringResource(Res.string.plugin_info_abilities),
|
||||
plugin.abilities.map { ability -> ability.displayLabel() })
|
||||
}
|
||||
|
||||
if (plugin.repository.isNotBlank()) {
|
||||
ClickableDetailRow(label = stringResource(Res.string.plugin_info_repository), value = plugin.repository) {
|
||||
openUrlInBrowser(plugin.repository)
|
||||
}
|
||||
}
|
||||
|
||||
if (plugin.contact.isNotBlank()) {
|
||||
DetailRow(stringResource(Res.string.plugin_info_contact), plugin.contact)
|
||||
}
|
||||
|
||||
if (plugin.bugs.isNotBlank()) {
|
||||
ClickableDetailRow(label = stringResource(Res.string.plugin_info_report_bugs), value = plugin.bugs) {
|
||||
openUrlInBrowser(plugin.bugs)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
MillenniumButton(onClick = onDismiss) {
|
||||
Text(stringResource(Res.string.common_close))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -819,30 +779,27 @@ private fun SupportDialog(
|
||||
supportText: String?,
|
||||
isLoading: Boolean,
|
||||
onDismiss: () -> Unit) {
|
||||
AdaptiveDialogBottomSheet(
|
||||
onDismiss = onDismiss,
|
||||
title = { PluginDialogTitle(stringResource(Res.string.plugin_support_dialog_title, plugin.name)) },
|
||||
dialogTitle = stringResource(Res.string.plugin_support_dialog_title, plugin.name),
|
||||
actions = {
|
||||
PluginDialogActions(
|
||||
secondaryLabel = stringResource(Res.string.common_close),
|
||||
onSecondary = onDismiss)
|
||||
}) {
|
||||
PluginDialogBody {
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = onDismiss,
|
||||
title = stringResource(Res.string.plugin_support_dialog_title, plugin.name)) {
|
||||
if (isLoading) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp),
|
||||
contentAlignment = Alignment.Center) {
|
||||
MillenniumCircularProgress(size = 36.dp)
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
MillenniumIndeterminateProgress(modifier = Modifier.widthIn(max = 160.dp))
|
||||
}
|
||||
} else {
|
||||
PluginPanel {
|
||||
Text(
|
||||
supportText ?: "",
|
||||
modifier = Modifier.padding(14.dp),
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1015,7 +972,7 @@ private fun DiscoverRepoCard(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
if (isInstalling) {
|
||||
MillenniumSpokeSpinner(size = 16.dp)
|
||||
MillenniumIndeterminateProgress(modifier = Modifier.size(16.dp))
|
||||
} else {
|
||||
MillenniumIcon(
|
||||
Iconsax.IconsaxAdd,
|
||||
|
||||
@ -1,249 +0,0 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
package dev.krtirtho.spotube.modules.plugin.components
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumCircularProgress
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@ -32,6 +32,10 @@ import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
|
||||
import dev.krtirtho.spotube.core.ui.theming.AppTheme
|
||||
|
||||
import com.composeunstyled.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -48,22 +52,19 @@ import coil3.compose.AsyncImage
|
||||
import coil3.compose.LocalPlatformContext
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
import com.composeunstyled.Text
|
||||
import dev.krtirtho.spotube.core.ui.component.AdaptiveDialogBottomSheet
|
||||
import dev.krtirtho.spotube.core.ui.theming.AppTheme
|
||||
import dev.krtirtho.spotube.modules.plugin.GitHubRelease
|
||||
import dev.krtirtho.spotube.modules.plugin.GitHubRepo
|
||||
import dev.krtirtho.spotube.modules.plugin.GitHubRelease
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxBox
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.spotube.millennium.composables.MillenniumButton
|
||||
import org.spotube.millennium.composables.MillenniumButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumDialog
|
||||
import org.spotube.millennium.composables.MillenniumDropdownMenu
|
||||
import org.spotube.millennium.composables.MillenniumDropdownMenuContent
|
||||
import org.spotube.millennium.composables.MillenniumDropdownMenuItem
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
import spotube.composeapp.generated.resources.common_cancel
|
||||
import spotube.composeapp.generated.resources.plugin_action_install
|
||||
import spotube.composeapp.generated.resources.plugin_install_no_releases
|
||||
import spotube.composeapp.generated.resources.plugin_install_prerelease
|
||||
@ -87,24 +88,21 @@ fun PluginInstallDialog(
|
||||
}
|
||||
}
|
||||
|
||||
AdaptiveDialogBottomSheet(
|
||||
onDismiss = onDismiss,
|
||||
title = { PluginDialogTitle(stringResource(Res.string.plugin_action_install)) },
|
||||
dialogTitle = stringResource(Res.string.plugin_action_install),
|
||||
actions = {
|
||||
PluginDialogActions(
|
||||
secondaryLabel = stringResource(Res.string.common_cancel),
|
||||
onSecondary = onDismiss,
|
||||
primaryLabel = stringResource(Res.string.plugin_action_install),
|
||||
onPrimary = { selectedRelease?.let(onInstall) },
|
||||
primaryEnabled = selectedRelease != null && !isLoadingReleases)
|
||||
}) {
|
||||
PluginDialogBody {
|
||||
PluginPanel {
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = onDismiss,
|
||||
title = repo.fullName) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(AppTheme.colors.primary.copy(alpha = 0.12f))
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val platformContext = LocalPlatformContext.current
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(platformContext)
|
||||
@ -112,100 +110,132 @@ fun PluginInstallDialog(
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = repo.owner.login,
|
||||
modifier = Modifier.size(56.dp).clip(RoundedCornerShape(16.dp)))
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
repo.fullName,
|
||||
style = AppTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis)
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (!repo.description.isNullOrBlank()) {
|
||||
Text(
|
||||
repo.description,
|
||||
style = AppTheme.typography.bodySmall,
|
||||
color = AppTheme.colors.onSurfaceVariant,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoadingReleases) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().height(96.dp),
|
||||
contentAlignment = Alignment.Center) {
|
||||
MillenniumCircularProgress(size = 36.dp)
|
||||
modifier = Modifier.fillMaxWidth().height(120.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
MillenniumIndeterminateProgress(modifier = Modifier.widthIn(max = 160.dp))
|
||||
}
|
||||
} else if (releases.isEmpty()) {
|
||||
PluginCallout(stringResource(Res.string.plugin_install_no_releases), isWarning = true)
|
||||
Text(
|
||||
stringResource(Res.string.plugin_install_no_releases),
|
||||
style = AppTheme.typography.bodyMedium,
|
||||
color = AppTheme.colors.onSurfaceVariant
|
||||
)
|
||||
} else {
|
||||
PluginSectionLabel(stringResource(Res.string.plugin_install_select_release_label))
|
||||
Text(
|
||||
stringResource(Res.string.plugin_install_select_release_label),
|
||||
style = AppTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = AppTheme.colors.onSurface
|
||||
)
|
||||
|
||||
MillenniumDropdownMenu(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it },
|
||||
items = releases.map { it.tagName },
|
||||
onItemClick = { tagName ->
|
||||
selectedRelease = releases.firstOrNull { it.tagName == tagName }
|
||||
expanded = false
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
anchor = {
|
||||
MillenniumButton(
|
||||
onClick = { expanded = true },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = MillenniumButtonStyle.Secondary) {
|
||||
style = MillenniumButtonStyle.Secondary
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
selectedRelease?.tagName
|
||||
?: stringResource(Res.string.plugin_install_select_release_placeholder),
|
||||
style = AppTheme.typography.bodyMedium)
|
||||
style = AppTheme.typography.bodyMedium
|
||||
)
|
||||
MillenniumIcon(
|
||||
imageVector = Iconsax.IconsaxArrowDown4,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp))
|
||||
}
|
||||
}
|
||||
}) {
|
||||
MillenniumDropdownMenuContent {
|
||||
releases.forEach { release ->
|
||||
MillenniumDropdownMenuItem(
|
||||
label = release.tagName,
|
||||
selected = release.tagName == selectedRelease?.tagName,
|
||||
description = if (release.prerelease) {
|
||||
stringResource(Res.string.plugin_install_prerelease)
|
||||
} else null,
|
||||
onClick = {
|
||||
selectedRelease = release
|
||||
expanded = false
|
||||
})
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
selectedRelease?.let { release ->
|
||||
if (release.prerelease) {
|
||||
PluginCallout(stringResource(Res.string.plugin_install_prerelease), isWarning = true)
|
||||
Text(
|
||||
stringResource(Res.string.plugin_install_prerelease),
|
||||
style = AppTheme.typography.labelSmall,
|
||||
color = AppTheme.colors.error
|
||||
)
|
||||
}
|
||||
if (!release.body.isNullOrBlank()) {
|
||||
PluginSectionLabel(stringResource(Res.string.plugin_install_release_notes))
|
||||
PluginPanel {
|
||||
Text(
|
||||
stringResource(Res.string.plugin_install_release_notes),
|
||||
style = AppTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = AppTheme.colors.onSurface
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(AppTheme.colors.surfaceVariant.copy(alpha = 0.5f))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 200.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(12.dp)) {
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Text(
|
||||
release.body,
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,39 +17,47 @@
|
||||
|
||||
package dev.krtirtho.spotube.modules.plugin.components
|
||||
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumButton
|
||||
import org.spotube.millennium.composables.MillenniumButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumCard
|
||||
import org.spotube.millennium.composables.MillenniumDialog
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
||||
import dev.krtirtho.spotube.core.ui.theming.AppTheme
|
||||
import com.composeunstyled.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import 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 okio.Path
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginCapability
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginEntry
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckCircle
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxBoxAdd
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxEye
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxWifiSquare
|
||||
import dev.krtirtho.spotube.resources.iconsax.PhosphorDatabaseDuotone
|
||||
import dev.krtirtho.spotube.resources.iconsax.User
|
||||
import okio.Path
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.composables.MillenniumSeparator
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
import spotube.composeapp.generated.resources.common_cancel
|
||||
import spotube.composeapp.generated.resources.common_close
|
||||
import spotube.composeapp.generated.resources.plugin_permissions_api_diff
|
||||
import spotube.composeapp.generated.resources.plugin_permissions_author_version
|
||||
import spotube.composeapp.generated.resources.plugin_permissions_capability_network_desc
|
||||
import spotube.composeapp.generated.resources.plugin_permissions_capability_network_title
|
||||
import spotube.composeapp.generated.resources.plugin_permissions_capability_storage_desc
|
||||
@ -62,6 +70,8 @@ import spotube.composeapp.generated.resources.plugin_permissions_none
|
||||
import spotube.composeapp.generated.resources.plugin_permissions_requested_title
|
||||
import spotube.composeapp.generated.resources.plugin_permissions_supplied_label
|
||||
import spotube.composeapp.generated.resources.plugin_version_label
|
||||
import spotube.composeapp.generated.resources.common_cancel
|
||||
import spotube.composeapp.generated.resources.common_close
|
||||
|
||||
@Composable
|
||||
fun PluginPermissionDialog(
|
||||
@ -73,139 +83,187 @@ fun PluginPermissionDialog(
|
||||
logoPath: Path? = null,
|
||||
onConfirm: (() -> Unit)? = null,
|
||||
onDismiss: () -> Unit) {
|
||||
AdaptiveDialogBottomSheet(
|
||||
onDismiss = onDismiss,
|
||||
title = { PluginDialogTitle(title) },
|
||||
dialogTitle = title,
|
||||
actions = {
|
||||
PluginDialogActions(
|
||||
secondaryLabel = stringResource(
|
||||
if (confirmLabel == null) Res.string.common_close else Res.string.common_cancel),
|
||||
onSecondary = onDismiss,
|
||||
primaryLabel = confirmLabel,
|
||||
onPrimary = onConfirm)
|
||||
}) {
|
||||
PluginDialogBody {
|
||||
PluginPanel {
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = onDismiss,
|
||||
title = title) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||
PluginLogoTile(plugin = pluginInfo, logoPath = logoPath)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(AppTheme.colors.primary.copy(alpha = 0.12f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
PluginLogo(
|
||||
plugin = pluginInfo,
|
||||
logoPath = logoPath,
|
||||
contentDescription = pluginInfo.name,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
fallback = {
|
||||
MillenniumIcon(
|
||||
imageVector = Iconsax.IconsaxBoxAdd,
|
||||
contentDescription = null,
|
||||
tint = AppTheme.colors.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
})
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
pluginInfo.name,
|
||||
style = AppTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis)
|
||||
Row(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()) {
|
||||
Text(
|
||||
pluginInfo.description,
|
||||
modifier = Modifier.padding(start = 14.dp, end = 14.dp, bottom = 14.dp),
|
||||
style = AppTheme.typography.bodySmall,
|
||||
color = AppTheme.colors.onSurfaceVariant,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
PluginCallout(text = message, isWarning = confirmLabel == null)
|
||||
MillenniumCard(contentPadding = PaddingValues(14.dp)) {
|
||||
Text(
|
||||
message,
|
||||
style = AppTheme.typography.bodySmall,
|
||||
color = AppTheme.colors.onSurface
|
||||
)
|
||||
}
|
||||
|
||||
existingPlugin?.let {
|
||||
InstalledComparisonPanel(installedPlugin = it, incomingPlugin = pluginInfo)
|
||||
InstalledComparisonCard(
|
||||
installedPlugin = it,
|
||||
incomingPlugin = pluginInfo
|
||||
)
|
||||
}
|
||||
|
||||
PluginSectionLabel(
|
||||
Text(
|
||||
stringResource(Res.string.plugin_permissions_requested_title),
|
||||
badge = pluginInfo.capabilities.size.takeIf { it > 0 }?.toString())
|
||||
style = AppTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = AppTheme.colors.onSurface
|
||||
)
|
||||
|
||||
PluginPanel {
|
||||
if (pluginInfo.capabilities.isEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
PluginIconTile(Iconsax.IconsaxCheckCircle)
|
||||
Text(
|
||||
stringResource(Res.string.plugin_permissions_none),
|
||||
style = AppTheme.typography.bodySmall,
|
||||
color = AppTheme.colors.onSurfaceVariant)
|
||||
}
|
||||
color = AppTheme.colors.onSurfaceVariant
|
||||
)
|
||||
} else {
|
||||
pluginInfo.capabilities.forEachIndexed { index, capability ->
|
||||
if (index > 0) MillenniumSeparator()
|
||||
pluginInfo.capabilities.forEach { capability ->
|
||||
CapabilityRow(capability)
|
||||
}
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
MillenniumButton(
|
||||
onClick = onDismiss,
|
||||
style = MillenniumButtonStyle.Secondary) {
|
||||
Text(
|
||||
if (confirmLabel == null) {
|
||||
stringResource(Res.string.common_close)
|
||||
} else {
|
||||
stringResource(Res.string.common_cancel)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (confirmLabel != null && onConfirm != null) {
|
||||
MillenniumButton(onClick = onConfirm) {
|
||||
Text(confirmLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InstalledComparisonPanel(
|
||||
private fun InstalledComparisonCard(
|
||||
installedPlugin: PluginEntry,
|
||||
incomingPlugin: PluginEntry) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
PluginSectionLabel(stringResource(Res.string.plugin_permissions_compare_title))
|
||||
PluginPanel {
|
||||
MillenniumCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(14.dp)) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(14.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(Res.string.plugin_permissions_compare_title),
|
||||
style = AppTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.SemiBold)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
VersionStat(
|
||||
label = stringResource(Res.string.plugin_permissions_installed_label),
|
||||
value = installedPlugin.version)
|
||||
MillenniumIcon(
|
||||
imageVector = FeatherIcons.ArrowRight,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = AppTheme.colors.onSurfaceVariant)
|
||||
VersionStat(
|
||||
label = stringResource(Res.string.plugin_permissions_supplied_label),
|
||||
value = incomingPlugin.version,
|
||||
highlighted = true)
|
||||
value = incomingPlugin.version)
|
||||
}
|
||||
Text(
|
||||
stringResource(
|
||||
Res.string.plugin_permissions_api_diff,
|
||||
installedPlugin.apiVersion,
|
||||
incomingPlugin.apiVersion),
|
||||
incomingPlugin.apiVersion
|
||||
),
|
||||
style = AppTheme.typography.labelSmall,
|
||||
color = AppTheme.colors.onSurfaceVariant)
|
||||
}
|
||||
color = AppTheme.colors.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VersionStat(label: String, value: String, highlighted: Boolean = false) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
private fun VersionStat(label: String, value: String) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(
|
||||
label,
|
||||
style = AppTheme.typography.labelSmall,
|
||||
color = AppTheme.colors.onSurfaceVariant)
|
||||
color = AppTheme.colors.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
stringResource(Res.string.plugin_version_label, value),
|
||||
style = AppTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = if (highlighted) AppTheme.colors.primary else AppTheme.colors.onSurface)
|
||||
style = AppTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,31 +273,54 @@ private fun CapabilityRow(capability: PluginCapability) {
|
||||
PluginCapability.PERSISTENT_STORAGE -> Triple(
|
||||
Iconsax.PhosphorDatabaseDuotone,
|
||||
stringResource(Res.string.plugin_permissions_capability_storage_title),
|
||||
stringResource(Res.string.plugin_permissions_capability_storage_desc))
|
||||
stringResource(Res.string.plugin_permissions_capability_storage_desc)
|
||||
)
|
||||
PluginCapability.NETWORK_REQUESTS -> Triple(
|
||||
Iconsax.IconsaxWifiSquare,
|
||||
stringResource(Res.string.plugin_permissions_capability_network_title),
|
||||
stringResource(Res.string.plugin_permissions_capability_network_desc))
|
||||
stringResource(Res.string.plugin_permissions_capability_network_desc)
|
||||
)
|
||||
PluginCapability.WEBVIEW -> Triple(
|
||||
Iconsax.IconsaxEye,
|
||||
stringResource(Res.string.plugin_permissions_capability_webview_title),
|
||||
stringResource(Res.string.plugin_permissions_capability_webview_desc))
|
||||
stringResource(Res.string.plugin_permissions_capability_webview_desc)
|
||||
)
|
||||
}
|
||||
|
||||
MillenniumCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(12.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
PluginIconTile(icon)
|
||||
Column(verticalArrangement = Arrangement.spacedBy(1.dp)) {
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(34.dp)
|
||||
.clip(CircleShape)
|
||||
.background(AppTheme.colors.errorContainer.copy(alpha = 0.6f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
MillenniumIcon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = AppTheme.colors.error
|
||||
)
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
label,
|
||||
style = AppTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium)
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Text(
|
||||
description,
|
||||
style = AppTheme.typography.labelSmall,
|
||||
color = AppTheme.colors.onSurfaceVariant)
|
||||
color = AppTheme.colors.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,269 +0,0 @@
|
||||
/*
|
||||
* 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.guard
|
||||
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.composeunstyled.Text
|
||||
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
||||
import dev.krtirtho.spotube.core.ui.component.LoginRequiredDisplay
|
||||
import dev.krtirtho.spotube.modules.plugin.MetadataAuthViewModel
|
||||
import dev.krtirtho.spotube.core.ui.theming.AppTheme
|
||||
import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAdd
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxBoxAdd
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckCircle
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicLibrary
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.spotube.millennium.composables.MillenniumButton
|
||||
import org.spotube.millennium.composables.MillenniumButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumCard
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.theme.MillenniumTheme
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
import spotube.composeapp.generated.resources.no_metadata_plugin_action_install
|
||||
import spotube.composeapp.generated.resources.no_metadata_plugin_action_local
|
||||
import spotube.composeapp.generated.resources.no_metadata_plugin_local_hint
|
||||
import spotube.composeapp.generated.resources.no_metadata_plugin_action_select
|
||||
import spotube.composeapp.generated.resources.no_metadata_plugin_description_install
|
||||
import spotube.composeapp.generated.resources.no_metadata_plugin_description_select
|
||||
import spotube.composeapp.generated.resources.no_metadata_plugin_step_discover
|
||||
import spotube.composeapp.generated.resources.no_metadata_plugin_step_install
|
||||
import spotube.composeapp.generated.resources.no_metadata_plugin_step_select
|
||||
import spotube.composeapp.generated.resources.no_metadata_plugin_title
|
||||
|
||||
/**
|
||||
* Renders [content] only while a metadata plugin is selected. Otherwise shows a guided
|
||||
* "no plugin" state with a call to action leading to the plugin manager, instead of letting
|
||||
* the screen sit on an endless loading skeleton.
|
||||
*
|
||||
* @param backButton whether the placeholder's top bar shows a back button (for pushed screens).
|
||||
* @param requireLogin also swap [content] for a friendly sign-in prompt while the plugin needs an
|
||||
* account and the user isn't signed in (for screens that only make sense when logged in).
|
||||
* @param showTopBar false when the host screen already draws its own app bar (e.g. Library tabs).
|
||||
*/
|
||||
@Composable
|
||||
fun MetadataPluginGuard(
|
||||
backButton: Boolean = false,
|
||||
showTopBar: Boolean = true,
|
||||
requireLogin: Boolean = false,
|
||||
viewModel: MetadataPluginGuardViewModel = koinViewModel(),
|
||||
authViewModel: MetadataAuthViewModel = koinViewModel(),
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val authState by authViewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
when (val s = state) {
|
||||
is MetadataPluginGuardState.Available ->
|
||||
if (requireLogin && authState.loginRequired) {
|
||||
Column(Modifier.fillMaxSize().background(MillenniumTheme.colors.background)) {
|
||||
if (showTopBar) ApplicationMainBar(backButton = backButton)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(bottom = LocalAppShellBottomInset.current),
|
||||
contentAlignment = Alignment.Center) {
|
||||
LoginRequiredDisplay(
|
||||
pluginName = authState.pluginName,
|
||||
onLogin = authViewModel::login,
|
||||
modifier = Modifier.widthIn(max = 420.dp))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content()
|
||||
}
|
||||
is MetadataPluginGuardState.Loading -> Box(
|
||||
Modifier.fillMaxSize().background(MillenniumTheme.colors.background))
|
||||
is MetadataPluginGuardState.Missing -> NoMetadataPluginScreen(
|
||||
hasInstalledPlugins = s.hasInstalledPlugins,
|
||||
backButton = backButton,
|
||||
showTopBar = showTopBar,
|
||||
onOpenPlugins = viewModel::openPlugins,
|
||||
onOpenLocalTracks = viewModel::openLocalTracks)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoMetadataPluginScreen(
|
||||
hasInstalledPlugins: Boolean,
|
||||
backButton: Boolean,
|
||||
showTopBar: Boolean,
|
||||
onOpenPlugins: () -> Unit,
|
||||
onOpenLocalTracks: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MillenniumTheme.colors.background)) {
|
||||
if (showTopBar) ApplicationMainBar(backButton = backButton)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = LocalAppShellBottomInset.current),
|
||||
contentAlignment = Alignment.Center) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 420.dp)
|
||||
.padding(vertical = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
HeroBadge()
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.no_metadata_plugin_title),
|
||||
style = AppTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = AppTheme.colors.onSurface,
|
||||
textAlign = TextAlign.Center)
|
||||
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (hasInstalledPlugins) Res.string.no_metadata_plugin_description_select
|
||||
else Res.string.no_metadata_plugin_description_install),
|
||||
style = AppTheme.typography.bodyMedium,
|
||||
color = AppTheme.colors.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center)
|
||||
|
||||
if (!hasInstalledPlugins) {
|
||||
MillenniumCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Step(1, Res.string.no_metadata_plugin_step_discover)
|
||||
Step(2, Res.string.no_metadata_plugin_step_install)
|
||||
Step(3, Res.string.no_metadata_plugin_step_select)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MillenniumButton(
|
||||
onClick = onOpenPlugins,
|
||||
style = MillenniumButtonStyle.Primary) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
MillenniumIcon(
|
||||
if (hasInstalledPlugins) Iconsax.IconsaxCheckCircle else Iconsax.IconsaxAdd,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp))
|
||||
Text(
|
||||
stringResource(
|
||||
if (hasInstalledPlugins) Res.string.no_metadata_plugin_action_select
|
||||
else Res.string.no_metadata_plugin_action_install))
|
||||
}
|
||||
}
|
||||
|
||||
MillenniumButton(
|
||||
onClick = onOpenLocalTracks,
|
||||
style = MillenniumButtonStyle.Secondary) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
MillenniumIcon(
|
||||
Iconsax.IconsaxMusicLibrary,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp))
|
||||
Text(stringResource(Res.string.no_metadata_plugin_action_local))
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.no_metadata_plugin_local_hint),
|
||||
style = AppTheme.typography.labelSmall,
|
||||
color = AppTheme.colors.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HeroBadge() {
|
||||
val primary = AppTheme.colors.primary
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(104.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Brush.radialGradient(listOf(primary.copy(alpha = 0.22f), primary.copy(alpha = 0f)))),
|
||||
contentAlignment = Alignment.Center) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(72.dp)
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(primary.copy(alpha = 0.12f)),
|
||||
contentAlignment = Alignment.Center) {
|
||||
MillenniumIcon(
|
||||
Iconsax.IconsaxBoxAdd,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(36.dp),
|
||||
tint = primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Step(index: Int, label: StringResource) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(26.dp)
|
||||
.clip(CircleShape)
|
||||
.background(AppTheme.colors.primary.copy(alpha = 0.14f)),
|
||||
contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = index.toString(),
|
||||
style = AppTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = AppTheme.colors.primary)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(label),
|
||||
style = AppTheme.typography.bodyMedium,
|
||||
color = AppTheme.colors.onSurface)
|
||||
}
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
/*
|
||||
* 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.guard
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||
import dev.krtirtho.spotube.core.navigation.Routes
|
||||
import dev.krtirtho.spotube.modules.library.LibraryState
|
||||
import dev.krtirtho.spotube.modules.library.LibraryTab
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginAbility
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginManager
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginManagerStates
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
sealed interface MetadataPluginGuardState {
|
||||
data object Loading : MetadataPluginGuardState
|
||||
data object Available : MetadataPluginGuardState
|
||||
|
||||
/** No metadata plugin is selected. [hasInstalledPlugins] tells whether the user only needs
|
||||
* to pick one (true) or has to install one first (false). */
|
||||
data class Missing(val hasInstalledPlugins: Boolean) : MetadataPluginGuardState
|
||||
}
|
||||
|
||||
class MetadataPluginGuardViewModel(
|
||||
pluginManager: PluginManager,
|
||||
private val navigationCommands: NavigationCommands,
|
||||
private val libraryState: LibraryState
|
||||
) : ViewModel() {
|
||||
val uiState: StateFlow<MetadataPluginGuardState> = pluginManager.state
|
||||
.map { state ->
|
||||
when (state) {
|
||||
is PluginManagerStates.Loading -> MetadataPluginGuardState.Loading
|
||||
is PluginManagerStates.Data ->
|
||||
if (PluginAbility.METADATA in state.selectedPlugins) {
|
||||
MetadataPluginGuardState.Available
|
||||
} else {
|
||||
MetadataPluginGuardState.Missing(
|
||||
hasInstalledPlugins = state.plugins.any { PluginAbility.METADATA in it.abilities })
|
||||
}
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = MetadataPluginGuardState.Loading)
|
||||
|
||||
fun openPlugins() {
|
||||
navigationCommands.navigateTo(Routes.Plugins)
|
||||
}
|
||||
|
||||
/** Local tracks are a plugin-free player, so offer them as the way forward. */
|
||||
fun openLocalTracks() {
|
||||
libraryState.onTabSelected(LibraryTab.LocalTracks)
|
||||
navigationCommands.navigateTo(Routes.Library)
|
||||
}
|
||||
}
|
||||
@ -18,10 +18,10 @@
|
||||
package dev.krtirtho.spotube.modules.search
|
||||
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumMarqueeProgress
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.composables.MillenniumButton
|
||||
import org.spotube.millennium.composables.MillenniumButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
|
||||
import org.spotube.millennium.composables.MillenniumListTile
|
||||
import org.spotube.millennium.composables.MillenniumAutocomplete
|
||||
import org.spotube.millennium.composables.MillenniumText
|
||||
@ -1155,7 +1155,7 @@ private fun SearchLoadingIndicator(
|
||||
translationY = bounce
|
||||
})
|
||||
}
|
||||
MillenniumMarqueeProgress(
|
||||
MillenniumIndeterminateProgress(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp, vertical = 16.dp)
|
||||
|
||||
@ -18,10 +18,10 @@
|
||||
package dev.krtirtho.spotube.modules.settings
|
||||
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumCircularProgress
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.composables.MillenniumIconButton
|
||||
import org.spotube.millennium.composables.MillenniumIconButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
|
||||
import org.spotube.millennium.composables.MillenniumSeparator
|
||||
import org.spotube.millennium.composables.MillenniumSidebarItem
|
||||
import org.spotube.millennium.theme.MillenniumTheme
|
||||
@ -288,7 +288,7 @@ private fun SettingsTabContent(
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center) {
|
||||
MillenniumCircularProgress(size = 40.dp)
|
||||
MillenniumIndeterminateProgress()
|
||||
}
|
||||
} else {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
|
||||
@ -18,12 +18,6 @@
|
||||
package dev.krtirtho.spotube.modules.settings.components
|
||||
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumDialogHeader
|
||||
import org.spotube.millennium.composables.MillenniumDialogTitle
|
||||
import org.spotube.millennium.composables.MillenniumDialogClose
|
||||
import org.spotube.millennium.composables.MillenniumDialogBody
|
||||
import org.spotube.millennium.composables.MillenniumDialogFooter
|
||||
import org.spotube.millennium.composables.MillenniumDialogDescription
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import com.composeunstyled.currentWindowWidthBreakpoint
|
||||
import org.spotube.millennium.adaptive.WindowBreakpoints
|
||||
@ -244,14 +238,17 @@ internal fun TextInputSettingCard(
|
||||
val normalizedValue = normalize(draft)
|
||||
val errorMessage = validate(normalizedValue)
|
||||
|
||||
MillenniumDialog(visible = true, onDismissRequest = { isDialogOpen = false }) {
|
||||
MillenniumDialogHeader {
|
||||
MillenniumDialogTitle(dialogTitle)
|
||||
MillenniumDialogClose()
|
||||
}
|
||||
MillenniumDialogBody {
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = { isDialogOpen = false },
|
||||
title = dialogTitle
|
||||
) {
|
||||
if (dialogDescription != null) {
|
||||
MillenniumDialogDescription(dialogDescription)
|
||||
Text(
|
||||
text = dialogDescription,
|
||||
style = AppTheme.typography.bodySmall,
|
||||
color = AppTheme.colors.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
MillenniumTextField(
|
||||
state = dialogState,
|
||||
@ -260,14 +257,7 @@ internal fun TextInputSettingCard(
|
||||
placeholder = placeholder,
|
||||
error = errorMessage
|
||||
)
|
||||
}
|
||||
MillenniumDialogFooter {
|
||||
MillenniumButton(
|
||||
onClick = { isDialogOpen = false },
|
||||
style = MillenniumButtonStyle.Secondary
|
||||
) {
|
||||
Text(stringResource(Res.string.common_cancel))
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
MillenniumButton(
|
||||
onClick = {
|
||||
if (errorMessage == null) {
|
||||
@ -279,6 +269,12 @@ internal fun TextInputSettingCard(
|
||||
) {
|
||||
Text(stringResource(Res.string.common_save))
|
||||
}
|
||||
MillenniumButton(
|
||||
onClick = { isDialogOpen = false },
|
||||
style = MillenniumButtonStyle.Secondary
|
||||
) {
|
||||
Text(stringResource(Res.string.common_cancel))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,12 +18,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.krtirtho.spotube.modules.settings.LocalMediaFoldersViewModel
|
||||
import io.github.vinceglb.filekit.dialogs.compose.rememberDirectoryPickerLauncher
|
||||
import io.github.vinceglb.filekit.path
|
||||
import org.spotube.millennium.composables.MillenniumDialog
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
@ -261,24 +255,3 @@ private fun LocalMediaFolderEditorRow(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Renders the "manage local folders" dialog (folder picker included) while [viewModel] has it open.
|
||||
* Shared by Settings and the Local Tracks screen; callers trigger it via [LocalMediaFoldersViewModel.open]. */
|
||||
@Composable
|
||||
fun LocalMediaFoldersDialogHost(viewModel: LocalMediaFoldersViewModel) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val pickerLauncher =
|
||||
rememberDirectoryPickerLauncher { directory -> viewModel.addFolder(directory?.path) }
|
||||
if (state.isOpen) {
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = viewModel::dismiss,
|
||||
title = stringResource(Res.string.settings_local_media_folders_title)
|
||||
) {
|
||||
LocalMediaFoldersEditorContent(
|
||||
state, { pickerLauncher.launch() }, viewModel::removeFolder,
|
||||
viewModel::save, viewModel::dismiss
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,11 +17,6 @@
|
||||
|
||||
package dev.krtirtho.spotube.modules.settings.sections
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumDialogHeader
|
||||
import org.spotube.millennium.composables.MillenniumDialogTitle
|
||||
import org.spotube.millennium.composables.MillenniumDialogClose
|
||||
import org.spotube.millennium.composables.MillenniumDialogBody
|
||||
import org.spotube.millennium.composables.MillenniumDialogFooter
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
@ -156,12 +151,10 @@ private fun AppearanceSettingCard(
|
||||
mutableStateOf(selectedPalette)
|
||||
}
|
||||
|
||||
MillenniumDialog(visible = true, onDismissRequest = { isDialogOpen = false }) {
|
||||
MillenniumDialogHeader {
|
||||
MillenniumDialogTitle(stringResource(Res.string.settings_palette_dialog_title))
|
||||
MillenniumDialogClose()
|
||||
}
|
||||
MillenniumDialogBody {
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = { isDialogOpen = false },
|
||||
title = stringResource(Res.string.settings_palette_dialog_title)) {
|
||||
MillenniumText(
|
||||
stringResource(Res.string.settings_palette_dialog_description),
|
||||
color = AppTheme.colors.onSurfaceVariant)
|
||||
@ -181,13 +174,9 @@ private fun AppearanceSettingCard(
|
||||
}
|
||||
|
||||
PalettePreview(draftPalette)
|
||||
}
|
||||
MillenniumDialogFooter {
|
||||
MillenniumButton(
|
||||
onClick = { isDialogOpen = false },
|
||||
style = MillenniumButtonStyle.Secondary) {
|
||||
MillenniumText(stringResource(Res.string.common_cancel))
|
||||
}
|
||||
|
||||
MillenniumSeparator()
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
MillenniumButton(
|
||||
onClick = {
|
||||
onSaved(draftPalette)
|
||||
@ -195,6 +184,11 @@ private fun AppearanceSettingCard(
|
||||
}) {
|
||||
MillenniumText(stringResource(Res.string.common_save))
|
||||
}
|
||||
MillenniumButton(
|
||||
onClick = { isDialogOpen = false },
|
||||
style = MillenniumButtonStyle.Secondary) {
|
||||
MillenniumText(stringResource(Res.string.common_cancel))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ package dev.krtirtho.spotube.modules.settings.sections
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumButton
|
||||
import org.spotube.millennium.composables.MillenniumButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumDialog
|
||||
import org.spotube.millennium.composables.MillenniumIconButton
|
||||
import org.spotube.millennium.composables.MillenniumIconButtonStyle
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
@ -30,7 +31,8 @@ import androidx.compose.runtime.getValue
|
||||
import dev.krtirtho.spotube.modules.settings.SettingsViewModel
|
||||
import dev.krtirtho.spotube.modules.settings.UserSettings
|
||||
import dev.krtirtho.spotube.modules.settings.LocalMediaFoldersViewModel
|
||||
import dev.krtirtho.spotube.modules.settings.components.LocalMediaFoldersDialogHost
|
||||
import dev.krtirtho.spotube.modules.settings.LocalMediaFoldersUiState
|
||||
import dev.krtirtho.spotube.modules.settings.components.LocalMediaFoldersEditorContent
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import dev.krtirtho.spotube.modules.settings.components.SelectionSettingCard
|
||||
@ -183,6 +185,8 @@ private fun LocalMediaFoldersSettingCard(
|
||||
viewModel: LocalMediaFoldersViewModel = koinViewModel()
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val pickerLauncher =
|
||||
rememberDirectoryPickerLauncher { directory -> viewModel.addFolder(directory?.path) }
|
||||
val subtitlePreview = state.singleConfiguredPath ?: if (state.configuredCount == 0) {
|
||||
stringResource(Res.string.settings_local_media_folders_subtitle_empty)
|
||||
} else stringResource(
|
||||
@ -210,5 +214,30 @@ private fun LocalMediaFoldersSettingCard(
|
||||
onClick = viewModel::open
|
||||
)
|
||||
|
||||
LocalMediaFoldersDialogHost(viewModel)
|
||||
if (state.isOpen) {
|
||||
LocalMediaFoldersDialog(
|
||||
state = state,
|
||||
onAddFolder = { pickerLauncher.launch() },
|
||||
onRemoveFolder = viewModel::removeFolder,
|
||||
onSave = viewModel::save,
|
||||
onDismiss = viewModel::dismiss
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LocalMediaFoldersDialog(
|
||||
state: LocalMediaFoldersUiState,
|
||||
onAddFolder: () -> Unit,
|
||||
onRemoveFolder: (String) -> Unit,
|
||||
onSave: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = onDismiss,
|
||||
title = stringResource(Res.string.settings_local_media_folders_title)
|
||||
) {
|
||||
LocalMediaFoldersEditorContent(state, onAddFolder, onRemoveFolder, onSave, onDismiss)
|
||||
}
|
||||
}
|
||||
|
||||
@ -289,8 +289,7 @@ fun AppExpandedPlayer(
|
||||
if (playerOptionsUiState.isMoreOptionsSheetOpen) {
|
||||
MillenniumModalBottomSheet(
|
||||
state = moreOptionsSheetState,
|
||||
contentPadding = PaddingValues(horizontal = 0.dp, vertical = 8.dp),
|
||||
contentSpacing = 4.dp
|
||||
contentPadding = PaddingValues(horizontal = 0.dp, vertical = 8.dp)
|
||||
) {
|
||||
// Grid of Options
|
||||
LazyVerticalGrid(
|
||||
|
||||
@ -18,10 +18,6 @@
|
||||
package dev.krtirtho.spotube.modules.shell
|
||||
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumDropdownMenuItem
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxCd
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMoon
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxInformation
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@ -440,6 +436,27 @@ fun AppLargePlayer(
|
||||
onExpandedChange = { expanded ->
|
||||
if (!expanded) playerOptionsViewModel.dismissMoreOptionsMenu()
|
||||
},
|
||||
items = listOf(goToAlbumLabel, sleepTimerLabel, trackDetailsLabel),
|
||||
onItemClick = { label ->
|
||||
when (label) {
|
||||
goToAlbumLabel -> {
|
||||
playerOptionsViewModel.dismissMoreOptionsMenu()
|
||||
currentTrack?.album?.id?.let { albumId ->
|
||||
navigationCommands.navigateTo(Routes.Album(albumId))
|
||||
}
|
||||
}
|
||||
|
||||
sleepTimerLabel -> {
|
||||
playerOptionsViewModel.dismissMoreOptionsMenu()
|
||||
playerOptionsViewModel.showSleepTimerDialog()
|
||||
}
|
||||
|
||||
trackDetailsLabel -> {
|
||||
playerOptionsViewModel.dismissMoreOptionsMenu()
|
||||
playerOptionsViewModel.showTrackDetails()
|
||||
}
|
||||
}
|
||||
},
|
||||
side = AnchorSide.Top,
|
||||
anchor = {
|
||||
MillenniumIconButton(
|
||||
@ -454,31 +471,7 @@ fun AppLargePlayer(
|
||||
contentDescription = stringResource(Res.string.player_more_options_content_description)
|
||||
)
|
||||
}
|
||||
}) {
|
||||
MillenniumDropdownMenuItem(
|
||||
label = goToAlbumLabel,
|
||||
icon = Iconsax.IconsaxCd,
|
||||
onClick = {
|
||||
playerOptionsViewModel.dismissMoreOptionsMenu()
|
||||
currentTrack?.album?.id?.let { albumId ->
|
||||
navigationCommands.navigateTo(Routes.Album(albumId))
|
||||
}
|
||||
})
|
||||
MillenniumDropdownMenuItem(
|
||||
label = sleepTimerLabel,
|
||||
icon = Iconsax.IconsaxMoon,
|
||||
onClick = {
|
||||
playerOptionsViewModel.dismissMoreOptionsMenu()
|
||||
playerOptionsViewModel.showSleepTimerDialog()
|
||||
})
|
||||
MillenniumDropdownMenuItem(
|
||||
label = trackDetailsLabel,
|
||||
icon = Iconsax.IconsaxInformation,
|
||||
onClick = {
|
||||
playerOptionsViewModel.dismissMoreOptionsMenu()
|
||||
playerOptionsViewModel.showTrackDetails()
|
||||
})
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
MillenniumIconButton(
|
||||
|
||||
@ -21,7 +21,6 @@ import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@ -167,11 +166,7 @@ fun AppSidebar(
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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
|
||||
) {
|
||||
MillenniumSidebarFooter {
|
||||
val supportLabel = stringResource(Res.string.support_button_label)
|
||||
MillenniumSidebarItem(
|
||||
label = supportLabel,
|
||||
|
||||
@ -18,16 +18,11 @@
|
||||
package dev.krtirtho.spotube.modules.shell
|
||||
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumDialogHeader
|
||||
import org.spotube.millennium.composables.MillenniumDialogTitle
|
||||
import org.spotube.millennium.composables.MillenniumDialogClose
|
||||
import org.spotube.millennium.composables.MillenniumDialogBody
|
||||
import org.spotube.millennium.composables.MillenniumDialogFooter
|
||||
import org.spotube.millennium.composables.MillenniumMarqueeProgress
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.composables.MillenniumButton
|
||||
import org.spotube.millennium.composables.MillenniumButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumDialog
|
||||
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
|
||||
import org.spotube.millennium.composables.MillenniumTextField
|
||||
import org.spotube.millennium.theme.millenniumSurface
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
@ -133,12 +128,11 @@ private fun SleepTimerDialog(
|
||||
onStart: () -> Unit,
|
||||
onCancelTimer: () -> Unit,
|
||||
onDismiss: () -> Unit) {
|
||||
MillenniumDialog(visible = true, onDismissRequest = onDismiss) {
|
||||
MillenniumDialogHeader {
|
||||
MillenniumDialogTitle(stringResource(Res.string.player_sleep_timer))
|
||||
MillenniumDialogClose()
|
||||
}
|
||||
MillenniumDialogBody {
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = onDismiss,
|
||||
title = stringResource(Res.string.player_sleep_timer)) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
if (remainingMs != null) {
|
||||
Text(
|
||||
text = stringResource(Res.string.player_sleep_timer_remaining, formatRemainingTime(remainingMs)),
|
||||
@ -158,12 +152,12 @@ private fun SleepTimerDialog(
|
||||
label = stringResource(Res.string.player_sleep_timer_minutes_label),
|
||||
placeholder = stringResource(Res.string.player_sleep_timer_minutes_placeholder),
|
||||
modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
MillenniumDialogFooter {
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
if (remainingMs != null) {
|
||||
MillenniumButton(
|
||||
onClick = onCancelTimer,
|
||||
style = MillenniumButtonStyle.Quiet) { Text(stringResource(Res.string.player_sleep_timer_cancel)) }
|
||||
style = MillenniumButtonStyle.Secondary) { Text(stringResource(Res.string.player_sleep_timer_cancel)) }
|
||||
}
|
||||
MillenniumButton(
|
||||
onClick = onDismiss,
|
||||
@ -175,18 +169,18 @@ private fun SleepTimerDialog(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackDetailsDialog(
|
||||
details: PlayerTrackDetails,
|
||||
onDismiss: () -> Unit) {
|
||||
MillenniumDialog(visible = true, onDismissRequest = onDismiss) {
|
||||
MillenniumDialogHeader {
|
||||
MillenniumDialogTitle(stringResource(Res.string.player_track_details))
|
||||
MillenniumDialogClose()
|
||||
}
|
||||
MillenniumDialogBody(contentSpacing = 14.dp) {
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = onDismiss,
|
||||
title = stringResource(Res.string.player_track_details)) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||
Text(
|
||||
details.track.title,
|
||||
style = AppTheme.typography.bodyMedium,
|
||||
@ -195,7 +189,8 @@ private fun TrackDetailsDialog(
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 20.dp),
|
||||
horizontalArrangement = Arrangement.Center) {
|
||||
MillenniumMarqueeProgress(modifier = Modifier.fillMaxWidth())
|
||||
MillenniumIndeterminateProgress(
|
||||
modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
|
||||
@ -320,8 +315,6 @@ private fun TrackDetailsDialog(
|
||||
DetailRow(stringResource(Res.string.player_track_details_active_url), activeUrl)
|
||||
}
|
||||
|
||||
}
|
||||
MillenniumDialogFooter {
|
||||
MillenniumButton(
|
||||
onClick = onDismiss,
|
||||
style = MillenniumButtonStyle.Secondary) { Text(stringResource(Res.string.common_close)) }
|
||||
|
||||
@ -18,8 +18,8 @@
|
||||
package dev.krtirtho.spotube.modules.shell.alternative_track
|
||||
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumSpokeSpinner
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
|
||||
import org.spotube.millennium.composables.MillenniumListTile
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@ -98,7 +98,8 @@ fun AlternativeTrackContent(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
MillenniumSpokeSpinner(size = 24.dp)
|
||||
MillenniumIndeterminateProgress(
|
||||
modifier = Modifier.size(24.dp))
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = stringResource(Res.string.alternative_track_searching),
|
||||
|
||||
@ -18,7 +18,6 @@
|
||||
package dev.krtirtho.spotube.modules.shell.player_queue
|
||||
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumDropdownMenuItem
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@ -420,6 +419,11 @@ private fun QueueItemRow(
|
||||
MillenniumDropdownMenu(
|
||||
expanded = showMenu,
|
||||
onExpandedChange = { showMenu = it },
|
||||
items = listOf(stringResource(Res.string.queue_remove_from_queue)),
|
||||
onItemClick = {
|
||||
onRemoveClick()
|
||||
showMenu = false
|
||||
},
|
||||
anchor = {
|
||||
MillenniumIconButton(
|
||||
onClick = { showMenu = true },
|
||||
@ -430,17 +434,8 @@ private fun QueueItemRow(
|
||||
contentDescription = stringResource(Res.string.player_more_options_content_description)
|
||||
)
|
||||
}
|
||||
}) {
|
||||
MillenniumDropdownMenuItem(
|
||||
label = stringResource(Res.string.queue_remove_from_queue),
|
||||
icon = Iconsax.IconsaxMusicSquareRemove,
|
||||
destructive = true,
|
||||
onClick = {
|
||||
onRemoveClick()
|
||||
showMenu = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -99,8 +99,7 @@ internal fun BottomSheetContent(
|
||||
}
|
||||
MillenniumModalBottomSheet(
|
||||
state = sheetState,
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
|
||||
contentSpacing = 8.dp
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
|
||||
@ -17,12 +17,6 @@
|
||||
|
||||
package dev.krtirtho.spotube.modules.support
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumDialogHeader
|
||||
import org.spotube.millennium.composables.MillenniumDialogTitle
|
||||
import org.spotube.millennium.composables.MillenniumDialogClose
|
||||
import org.spotube.millennium.composables.MillenniumDialogBody
|
||||
import org.spotube.millennium.composables.MillenniumDialogDescription
|
||||
import org.spotube.millennium.composables.MillenniumDialogFooter
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@ -109,13 +103,14 @@ fun SupportDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
isReminder: Boolean = false) {
|
||||
if (!visible) return
|
||||
MillenniumDialog(visible = true, onDismissRequest = onDismissRequest) {
|
||||
MillenniumDialogHeader {
|
||||
MillenniumDialogTitle(stringResource(Res.string.support_dialog_title))
|
||||
MillenniumDialogClose()
|
||||
}
|
||||
MillenniumDialogBody {
|
||||
MillenniumDialogDescription(stringResource(Res.string.support_dialog_message))
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = stringResource(Res.string.support_dialog_title)) {
|
||||
Text(
|
||||
stringResource(Res.string.support_dialog_message),
|
||||
style = AppTheme.typography.bodyMedium,
|
||||
color = AppTheme.colors.onSurfaceVariant)
|
||||
if (isReminder) {
|
||||
Text(
|
||||
stringResource(Res.string.support_dialog_reminder_hint),
|
||||
@ -146,13 +141,11 @@ fun SupportDialog(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MillenniumDialogFooter {
|
||||
MillenniumButton(
|
||||
onClick = onDismissRequest,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = MillenniumButtonStyle.Quiet) {
|
||||
Text(stringResource(Res.string.support_dialog_dismiss))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,14 +17,19 @@
|
||||
|
||||
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.runtime.Composable
|
||||
import com.composeunstyled.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mikepenz.markdown.m3.Markdown
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.spotube.millennium.composables.MillenniumAlertDialog
|
||||
import org.spotube.millennium.composables.MillenniumButton
|
||||
import org.spotube.millennium.composables.MillenniumButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumDialog
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
import spotube.composeapp.generated.resources.app_update_dialog_no_notes
|
||||
import spotube.composeapp.generated.resources.app_update_dialog_title
|
||||
@ -42,19 +47,20 @@ fun AppUpdateDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onIgnore: () -> Unit,
|
||||
onUpdate: () -> Unit) {
|
||||
MillenniumAlertDialog(
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(Res.string.app_update_dialog_title, update.tag)) },
|
||||
content = {
|
||||
title = stringResource(Res.string.app_update_dialog_title, update.tag)) {
|
||||
if (update.releaseNotesMarkdown.isNotBlank()) {
|
||||
SelectionContainer {
|
||||
Markdown(content = update.releaseNotesMarkdown)
|
||||
Markdown(
|
||||
content = update.releaseNotesMarkdown,
|
||||
modifier = Modifier.padding(bottom = 8.dp))
|
||||
}
|
||||
} else {
|
||||
Text(stringResource(Res.string.app_update_dialog_no_notes))
|
||||
}
|
||||
}) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
MillenniumButton(
|
||||
onClick = onIgnore,
|
||||
style = MillenniumButtonStyle.Secondary) {
|
||||
@ -64,4 +70,5 @@ fun AppUpdateDialog(
|
||||
Text(stringResource(Res.string.update_action_update))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
|
||||
package dev.krtirtho.spotube.modules.update
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumAlertDialog
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@ -31,6 +30,7 @@ import com.mikepenz.markdown.m3.Markdown
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.spotube.millennium.composables.MillenniumButton
|
||||
import org.spotube.millennium.composables.MillenniumButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumDialog
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
import spotube.composeapp.generated.resources.plugin_update_dialog_current_version
|
||||
import spotube.composeapp.generated.resources.plugin_update_dialog_no_notes
|
||||
@ -50,20 +50,17 @@ fun PluginUpdateDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onIgnore: () -> Unit,
|
||||
onUpdate: () -> Unit) {
|
||||
MillenniumAlertDialog(
|
||||
MillenniumDialog(
|
||||
visible = true,
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(
|
||||
title = stringResource(
|
||||
Res.string.plugin_update_dialog_title,
|
||||
update.pluginName,
|
||||
update.info.latestVersion))
|
||||
},
|
||||
description = {
|
||||
Text(stringResource(Res.string.plugin_update_dialog_current_version, update.currentVersion))
|
||||
},
|
||||
content = {
|
||||
update.info.latestVersion)) {
|
||||
Text(
|
||||
text = stringResource(Res.string.plugin_update_dialog_current_version, update.currentVersion),
|
||||
color = AppTheme.colors.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 8.dp))
|
||||
if (update.info.releaseNotes.isNotBlank()) {
|
||||
SelectionContainer {
|
||||
Markdown(content = update.info.releaseNotes)
|
||||
@ -71,7 +68,7 @@ fun PluginUpdateDialog(
|
||||
} else {
|
||||
Text(stringResource(Res.string.plugin_update_dialog_no_notes))
|
||||
}
|
||||
}) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
MillenniumButton(
|
||||
onClick = onIgnore,
|
||||
style = MillenniumButtonStyle.Secondary) {
|
||||
@ -81,4 +78,5 @@ fun PluginUpdateDialog(
|
||||
Text(stringResource(Res.string.update_action_update))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user