From 42a231e17934bd0c14734caf0bc30f6526020874 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 28 May 2026 00:47:49 -0400 Subject: [PATCH] =?UTF-8?q?feat(android):=20variant=20pill=20+=20mini-cove?= =?UTF-8?q?r=20crossfade=20+=20clipboard=20API=20(audit=20v3=20=C2=A74.10/?= =?UTF-8?q?=C2=A74.16=20tail)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §4.10/§4.18 — PlaylistCard now overlays a small primary-tinted pill on system-playlist covers showing the variant ("For You" / "Discover" / "Today's mix" / …) instead of relying on the subtitle line. The subtitle now prefers track count, so system tiles show variant pill + track count rather than just the label. §4.16 — MiniPlayer cover wraps its content in a Crossfade keyed on coverUrl so artwork dissolves into the next track instead of hard- swapping. Pairs with the CoverPrefetcher cache-warming so the next cover is usually ready to fade in. Cleanup — migrated the AdminUsers invite Copy-token button off the deprecated LocalClipboardManager.setText to LocalClipboard.setClipEntry (suspend, via rememberCoroutineScope + ClipData/ClipEntry), clearing the build warning. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../minstrel/admin/ui/AdminUsersScreen.kt | 13 +++++--- .../minstrel/player/ui/MiniPlayer.kt | 31 +++++++++++-------- .../playlists/widgets/PlaylistCard.kt | 30 +++++++++++++++++- 3 files changed, 56 insertions(+), 18 deletions(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminUsersScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminUsersScreen.kt index 16f96beb..c8d77543 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminUsersScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminUsersScreen.kt @@ -31,14 +31,16 @@ import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.text.AnnotatedString +import android.content.ClipData +import androidx.compose.ui.platform.ClipEntry +import androidx.compose.ui.platform.LocalClipboard import com.fabledsword.minstrel.models.Invite import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -52,6 +54,7 @@ import com.fabledsword.minstrel.models.AdminUserRef import com.fabledsword.minstrel.nav.AdminUsers import com.fabledsword.minstrel.shared.widgets.MainAppBarActions import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -328,7 +331,8 @@ private fun GenerateInviteDialog(onDismiss: () -> Unit, onConfirm: (String?) -> @Composable private fun GeneratedInviteDialog(invite: Invite, onDismiss: () -> Unit) { - val clipboard = LocalClipboardManager.current + val clipboard = LocalClipboard.current + val scope = rememberCoroutineScope() AlertDialog( onDismissRequest = onDismiss, title = { Text("Invite created") }, @@ -350,7 +354,8 @@ private fun GeneratedInviteDialog(invite: Invite, onDismiss: () -> Unit) { confirmButton = { Button( onClick = { - clipboard.setText(AnnotatedString(invite.token)) + val clip = ClipData.newPlainText("Minstrel invite", invite.token) + scope.launch { clipboard.setClipEntry(ClipEntry(clip)) } onDismiss() }, ) { diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/MiniPlayer.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/MiniPlayer.kt index e5733ff3..806030bf 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/MiniPlayer.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/MiniPlayer.kt @@ -1,5 +1,6 @@ package com.fabledsword.minstrel.player.ui +import androidx.compose.animation.Crossfade import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -75,19 +76,23 @@ private fun MiniCover(coverUrl: String, contentDescription: String) { .background(MaterialTheme.colorScheme.surfaceVariant), contentAlignment = Alignment.Center, ) { - if (coverUrl.isEmpty()) { - Icon( - imageVector = Lucide.Music, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } else { - AsyncImage( - model = coverUrl, - contentDescription = contentDescription, - modifier = Modifier.size(COVER_SIZE_DP.dp), - contentScale = ContentScale.Crop, - ) + // Cross-fade the artwork when the track changes so the cover + // dissolves into the next instead of hard-swapping. + Crossfade(targetState = coverUrl, label = "mini-cover") { url -> + if (url.isEmpty()) { + Icon( + imageVector = Lucide.Music, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } else { + AsyncImage( + model = url, + contentDescription = contentDescription, + modifier = Modifier.size(COVER_SIZE_DP.dp), + contentScale = ContentScale.Crop, + ) + } } } } diff --git a/android/app/src/main/java/com/fabledsword/minstrel/playlists/widgets/PlaylistCard.kt b/android/app/src/main/java/com/fabledsword/minstrel/playlists/widgets/PlaylistCard.kt index 28ceadad..c46e3938 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/playlists/widgets/PlaylistCard.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/playlists/widgets/PlaylistCard.kt @@ -76,6 +76,14 @@ fun PlaylistCard( contentScale = ContentScale.Crop, ) } + if (playlist.isSystem) { + VariantPill( + label = systemLabelFor(playlist.systemVariant), + modifier = Modifier + .align(Alignment.TopStart) + .padding(6.dp), + ) + } } Spacer(Modifier.height(8.dp)) Text( @@ -96,9 +104,29 @@ fun PlaylistCard( } } +/** Small label chip overlaid on a system playlist's cover. */ +@Composable +private fun VariantPill(label: String, modifier: Modifier = Modifier) { + if (label.isEmpty()) return + Surface( + modifier = modifier, + shape = RoundedCornerShape(FabledSwordFlatTokens.radiusSm), + color = MaterialTheme.colorScheme.primary.copy(alpha = PILL_BG_ALPHA), + ) { + Text( + text = label, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onPrimary, + ) + } +} + +private const val PILL_BG_ALPHA = 0.85f + private fun subtitleFor(playlist: PlaylistRef): String = when { - playlist.isSystem -> systemLabelFor(playlist.systemVariant) playlist.trackCount > 0 -> "${playlist.trackCount} tracks" + playlist.isSystem -> systemLabelFor(playlist.systemVariant) else -> " " }