From 8424140d60e65a6a20352928db4dd71a191c7fca Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 27 May 2026 02:51:36 +0000 Subject: [PATCH] chore(music): @Preview composables for every new UI surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds previews wherever there's something visual worth previewing: - MusicTrack.kt: RenderMusicTrack (full event with cover + meta), RenderMusicTrackExplicit (explicit-badge variant), MusicTrackCover (no cover fallback), ExplicitBadge, TopicChip. Uses the same Event-constructor + LocalCache.justConsume + LoadNote pattern Attestation and Wiki previews already use. - MusicPlaylist.kt: RenderMusicPlaylist (with three resolved track refs), RenderMusicPlaylistCollaborativePrivate (chips visible), MusicPlaylistCover, MissingPlaylistTrackRow, TrackCoverPlaceholder, PlaylistTag. - MusicTracksScreen.kt: full feed scaffold via mockAccountViewModel(). - MusicTracksTopBar.kt: the static-title top bar. - NewMusicTrackButton.kt: the FAB on its own. - NewMusicTrackScreen.kt: the composer form. - AddToMusicPlaylistSheet.kt: NewMusicPlaylistDialog leaf, and the empty state of the full sheet. - MusicPlaylistManagementItem.kt: four variants — not-in / in / empty (zero tracks) / untitled. Skipped MusicTracksFeedLoaded — FeedState.Loaded can't be easily mocked and no existing feed-loaded previews exist in the codebase. The screen preview already covers the scaffold around it. URLs in the constructed events point at example.invalid so MyAsyncImage falls back to the deterministic DefaultImageHeader robohash and VideoView shows its tap-to-load thumbnail state — both of which are the real first-paint state users see before tapping anything. --- .../amethyst/ui/note/types/MusicPlaylist.kt | 209 ++++++++++++++++++ .../amethyst/ui/note/types/MusicTrack.kt | 164 ++++++++++++++ .../loggedIn/music/AddToMusicPlaylistSheet.kt | 26 +++ .../music/MusicPlaylistManagementItem.kt | 58 +++++ .../loggedIn/music/MusicTracksScreen.kt | 17 ++ .../loggedIn/music/MusicTracksTopBar.kt | 12 + .../loggedIn/music/NewMusicTrackButton.kt | 12 + .../loggedIn/music/NewMusicTrackScreen.kt | 16 ++ 8 files changed, 514 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt index ae829eddc9..db8d7fa852 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt @@ -40,6 +40,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -49,27 +50,36 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size5dp +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext private val PLAYLIST_COVER_ASPECT_RATIO = 1f private const val MAX_PREVIEW_TRACKS = 25 @@ -451,3 +461,202 @@ private fun formatTrackDuration(seconds: Int): String { val secs = seconds % 60 return "%d:%02d".format(minutes, secs) } + +// --------------------------------------------------------------------------- +// @Preview composables. Constructs a MusicPlaylistEvent plus a handful of +// MusicTrackEvents it references, pushes them through LocalCache, then +// renders the same code path the production feed uses. The cover image URL +// is unreachable on purpose so MyAsyncImage falls back to the deterministic +// DefaultImageHeader robohash — that's what users see while the real cover +// is still loading. +// --------------------------------------------------------------------------- + +private const val PREVIEW_PLAYLIST_PUBKEY = "989c3734c46abac7ce3ce229971581a5a6ee39cdd6aa7261a55823fa7f8c4799" +private val PREVIEW_PLAYLIST_SIG = "0".repeat(128) + +private fun previewPlaylistTrack( + dTag: String, + title: String, + artist: String, + duration: Int, +): MusicTrackEvent = + MusicTrackEvent( + id = "ptrack_${dTag}_${title.hashCode()}".padEnd(64, '0').take(64), + pubKey = PREVIEW_PLAYLIST_PUBKEY, + createdAt = 1_730_000_000L, + tags = + arrayOf( + arrayOf("d", dTag), + arrayOf("title", title), + arrayOf("artist", artist), + arrayOf("url", "https://example.invalid/$dTag.mp3"), + arrayOf("duration", duration.toString()), + arrayOf("t", "music"), + ), + content = "", + sig = PREVIEW_PLAYLIST_SIG, + ) + +private fun previewPlaylistEvent( + dTag: String, + title: String, + description: String, + tracks: List, + image: String? = "https://example.invalid/playlist.jpg", + isCollaborative: Boolean = false, + isPrivate: Boolean = false, + content: String = "", +): MusicPlaylistEvent { + val tags = + buildList { + add(arrayOf("d", dTag)) + add(arrayOf("title", title)) + add(arrayOf("description", description)) + image?.let { add(arrayOf("image", it)) } + add(arrayOf("t", "playlist")) + tracks.forEach { track -> + add(arrayOf("a", "${MusicTrackEvent.KIND}:${track.pubKey}:${track.dTag()}")) + } + if (isPrivate) add(arrayOf("private", "true")) else add(arrayOf("public", "true")) + if (isCollaborative) add(arrayOf("collaborative", "true")) + }.toTypedArray() + + return MusicPlaylistEvent( + id = "pl_${dTag}_${title.hashCode()}".padEnd(64, '0').take(64), + pubKey = PREVIEW_PLAYLIST_PUBKEY, + createdAt = 1_730_000_000L, + tags = tags, + content = content, + sig = PREVIEW_PLAYLIST_SIG, + ) +} + +@Preview +@Composable +private fun RenderMusicPlaylistPreview() { + val tracks = + listOf( + previewPlaylistTrack("summer-nights-2024", "Summer Nights", "The Midnight Collective", 245), + previewPlaylistTrack("sunset-dreams", "Sunset Dreams", "Pacific Wave", 198), + previewPlaylistTrack("ocean-breeze", "Ocean Breeze", "The Midnight Collective", 312), + ) + val event = + previewPlaylistEvent( + dTag = "summer-vibes-2024", + title = "Summer Vibes 2024", + description = "Chill electronic tracks for summer", + tracks = tracks, + content = "My favorite summer vibes from 2024", + ) + + runBlocking { + withContext(Dispatchers.IO) { + tracks.forEach { LocalCache.justConsume(it, null, true) } + LocalCache.justConsume(event, null, true) + } + } + + ThemeComparisonColumn { + LoadNote(baseNoteHex = event.address().toValue(), accountViewModel = mockAccountViewModel()) { note -> + note?.let { + RenderMusicPlaylist( + note = it, + makeItShort = false, + canPreview = true, + backgroundColor = remember { mutableStateOf(Color.Transparent) }, + accountViewModel = mockAccountViewModel(), + nav = EmptyNav(), + ) + } + } + } +} + +@Preview +@Composable +private fun RenderMusicPlaylistCollaborativePrivatePreview() { + val event = + previewPlaylistEvent( + dTag = "friends-collab", + title = "Friends Collab", + description = "Everyone adds their picks", + tracks = emptyList(), + isPrivate = true, + isCollaborative = true, + content = "", + ) + + runBlocking { withContext(Dispatchers.IO) { LocalCache.justConsume(event, null, true) } } + + ThemeComparisonColumn { + LoadNote(baseNoteHex = event.address().toValue(), accountViewModel = mockAccountViewModel()) { note -> + note?.let { + RenderMusicPlaylist( + note = it, + makeItShort = false, + canPreview = true, + backgroundColor = remember { mutableStateOf(Color.Transparent) }, + accountViewModel = mockAccountViewModel(), + nav = EmptyNav(), + ) + } + } + } +} + +@Preview +@Composable +private fun MusicPlaylistCoverPreview() { + val event = + previewPlaylistEvent( + dTag = "cover-only", + title = "Cover Only", + description = "", + tracks = emptyList(), + image = null, + ) + + runBlocking { withContext(Dispatchers.IO) { LocalCache.justConsume(event, null, true) } } + + ThemeComparisonColumn { + LoadNote(baseNoteHex = event.address().toValue(), accountViewModel = mockAccountViewModel()) { note -> + note?.let { + MusicPlaylistCover( + image = null, + note = it, + trackCount = 7, + accountViewModel = mockAccountViewModel(), + ) + } + } + } +} + +@Preview +@Composable +private fun MissingPlaylistTrackRowPreview() { + ThemeComparisonColumn { + MissingPlaylistTrackRow(position = 4) + } +} + +@Preview +@Composable +private fun TrackCoverPlaceholderPreview() { + ThemeComparisonColumn { + Row(modifier = Modifier.padding(8.dp)) { + TrackCoverPlaceholder() + } + } +} + +@Preview +@Composable +private fun PlaylistTagPreview() { + ThemeComparisonColumn { + Row(modifier = Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) { + PlaylistTag(text = "Collaborative") + PlaylistTag(text = "Private") + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt index 56a0657fe0..91c68d34e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt @@ -39,6 +39,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -47,29 +48,38 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.playback.composable.LoadThumbAndThenVideoView import com.vitorpamplona.amethyst.service.playback.composable.VideoView +import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Font10SP import com.vitorpamplona.amethyst.ui.theme.Size5dp +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext private val COVER_ASPECT_RATIO = 1f @@ -350,3 +360,157 @@ private fun formatDuration(seconds: Int): String { val secs = seconds % 60 return "%d:%02d".format(minutes, secs) } + +// --------------------------------------------------------------------------- +// @Preview composables. These wire a constructed MusicTrackEvent through +// LocalCache so the renderer pulls the real Note path it would in production. +// The audio/image URLs are intentionally unreachable — VideoView falls back to +// its tap-to-load thumbnail state, and MyAsyncImage falls back to the +// DefaultImageHeader robohash. That's the same first-paint state real users +// see for a track they haven't tapped to download yet, which is what the +// preview is meant to show. +// --------------------------------------------------------------------------- + +private const val PREVIEW_TRACK_PUBKEY = "989c3734c46abac7ce3ce229971581a5a6ee39cdd6aa7261a55823fa7f8c4799" +private val PREVIEW_TRACK_SIG = "0".repeat(128) + +private fun previewMusicTrackEvent( + dTag: String, + title: String, + artist: String, + album: String? = null, + duration: Int? = null, + released: String? = null, + trackNumber: Int? = null, + explicit: Boolean = false, + image: String? = "https://example.invalid/cover.jpg", + url: String = "https://example.invalid/track.mp3", + content: String = "", +): MusicTrackEvent { + val tags = + buildList { + add(arrayOf("d", dTag)) + add(arrayOf("title", title)) + add(arrayOf("artist", artist)) + add(arrayOf("url", url)) + add(arrayOf("t", "music")) + image?.let { add(arrayOf("image", it)) } + album?.let { add(arrayOf("album", it)) } + trackNumber?.let { add(arrayOf("track_number", it.toString())) } + released?.let { add(arrayOf("released", it)) } + duration?.let { add(arrayOf("duration", it.toString())) } + if (explicit) add(arrayOf("explicit", "true")) + }.toTypedArray() + + return MusicTrackEvent( + id = "track_${dTag}_${title.hashCode()}".padEnd(64, '0').take(64), + pubKey = PREVIEW_TRACK_PUBKEY, + createdAt = 1_730_000_000L, + tags = tags, + content = content, + sig = PREVIEW_TRACK_SIG, + ) +} + +@Preview +@Composable +private fun RenderMusicTrackPreview() { + val event = + previewMusicTrackEvent( + dTag = "summer-nights-2024", + title = "Summer Nights", + artist = "The Midnight Collective", + album = "Endless Summer", + duration = 245, + released = "2024-06-15", + trackNumber = 3, + explicit = false, + content = "Lyrics:\n[Verse 1]\nWalking through the city lights...\n\nProducer: John Doe", + ) + + runBlocking { withContext(Dispatchers.IO) { LocalCache.justConsume(event, null, true) } } + + ThemeComparisonColumn { + LoadNote(baseNoteHex = event.address().toValue(), accountViewModel = mockAccountViewModel()) { note -> + note?.let { + RenderMusicTrack( + note = it, + makeItShort = false, + canPreview = true, + backgroundColor = remember { mutableStateOf(Color.Transparent) }, + accountViewModel = mockAccountViewModel(), + nav = EmptyNav(), + ) + } + } + } +} + +@Preview +@Composable +private fun RenderMusicTrackExplicitPreview() { + val event = + previewMusicTrackEvent( + dTag = "loud-and-clear", + title = "Loud and Clear", + artist = "Static Riot", + album = "Distortion", + duration = 187, + explicit = true, + content = "", + ) + + runBlocking { withContext(Dispatchers.IO) { LocalCache.justConsume(event, null, true) } } + + ThemeComparisonColumn { + LoadNote(baseNoteHex = event.address().toValue(), accountViewModel = mockAccountViewModel()) { note -> + note?.let { + RenderMusicTrack( + note = it, + makeItShort = false, + canPreview = true, + backgroundColor = remember { mutableStateOf(Color.Transparent) }, + accountViewModel = mockAccountViewModel(), + nav = EmptyNav(), + ) + } + } + } +} + +@Preview +@Composable +private fun MusicTrackCoverPreview() { + val event = previewMusicTrackEvent(dTag = "cover-only", title = "Cover Only", artist = "Anon") + runBlocking { withContext(Dispatchers.IO) { LocalCache.justConsume(event, null, true) } } + + ThemeComparisonColumn { + LoadNote(baseNoteHex = event.address().toValue(), accountViewModel = mockAccountViewModel()) { note -> + note?.let { MusicTrackCover(image = null, note = it, accountViewModel = mockAccountViewModel()) } + } + } +} + +@Preview +@Composable +private fun ExplicitBadgePreview() { + ThemeComparisonColumn { + Row(modifier = Modifier.padding(8.dp)) { + ExplicitBadge() + } + } +} + +@Preview +@Composable +private fun TopicChipPreview() { + ThemeComparisonColumn { + Row(modifier = Modifier.padding(8.dp)) { + TopicChip("electronic") + Spacer(Modifier.padding(start = 4.dp)) + TopicChip("synthwave") + Spacer(Modifier.padding(start = 4.dp)) + TopicChip("80s") + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/AddToMusicPlaylistSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/AddToMusicPlaylistSheet.kt index 7b916d4b45..14bd8510fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/AddToMusicPlaylistSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/AddToMusicPlaylistSheet.kt @@ -44,16 +44,20 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.navigation.bottombars.FabBottomBarPadded +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.NewListButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn @Composable fun AddToMusicPlaylistSheet( @@ -184,3 +188,25 @@ private fun NewMusicPlaylistDialog( }, ) } + +@Preview +@Composable +private fun NewMusicPlaylistDialogPreview() { + ThemeComparisonColumn { + NewMusicPlaylistDialog(onDismiss = {}, onCreate = {}) + } +} + +@Preview +@Composable +private fun AddToMusicPlaylistSheetEmptyPreview() { + ThemeComparisonColumn { + AddToMusicPlaylistSheet( + // No matching addressable in LocalCache → the VM resolves zero playlists, + // which is the empty-state path we want to capture. + trackAddress = "36787:0000000000000000000000000000000000000000000000000000000000000000:preview-empty", + accountViewModel = mockAccountViewModel(), + nav = EmptyNav(), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicPlaylistManagementItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicPlaylistManagementItem.kt index b2e956c0e2..dab3da0491 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicPlaylistManagementItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicPlaylistManagementItem.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -45,6 +46,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size15Modifier import com.vitorpamplona.amethyst.ui.theme.Size50Modifier import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn /** * One row in the Add-to-Playlist sheet. Mirrors `BookmarkGroupManagementItem` so the visual @@ -176,3 +178,59 @@ private fun PlaylistToggleButton( } } } + +@Preview +@Composable +private fun MusicPlaylistManagementItemNotInPreview() { + ThemeComparisonColumn { + MusicPlaylistManagementItem( + playlistTitle = "Summer Vibes 2024", + isTrackInPlaylist = false, + totalTracks = 12, + onClick = {}, + onToggle = {}, + ) + } +} + +@Preview +@Composable +private fun MusicPlaylistManagementItemInPreview() { + ThemeComparisonColumn { + MusicPlaylistManagementItem( + playlistTitle = "Late-night Coding Beats", + isTrackInPlaylist = true, + totalTracks = 47, + onClick = {}, + onToggle = {}, + ) + } +} + +@Preview +@Composable +private fun MusicPlaylistManagementItemEmptyPreview() { + ThemeComparisonColumn { + MusicPlaylistManagementItem( + playlistTitle = "Fresh Picks", + isTrackInPlaylist = false, + totalTracks = 0, + onClick = {}, + onToggle = {}, + ) + } +} + +@Preview +@Composable +private fun MusicPlaylistManagementItemUntitledPreview() { + ThemeComparisonColumn { + MusicPlaylistManagementItem( + playlistTitle = "", + isTrackInPlaylist = false, + totalTracks = 3, + onClick = {}, + onToggle = {}, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksScreen.kt index fe330bf9f6..c5c4b1b636 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksScreen.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.music import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox @@ -33,9 +34,12 @@ import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.bottombars.FabBottomBarPadded +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn @Composable fun MusicTracksScreen( @@ -118,3 +122,16 @@ fun WatchAccountForMusicTracksScreen( musicFeedState.checkKeysInvalidateDataAndSendToTop() } } + +@Preview +@Composable +private fun MusicTracksScreenPreview() { + val accountViewModel = mockAccountViewModel() + ThemeComparisonColumn { + MusicTracksScreen( + musicTracksFeedContentState = accountViewModel.feedStates.musicTracksFeed, + accountViewModel = accountViewModel, + nav = EmptyNav(), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksTopBar.kt index 5270525b7f..00501e7f91 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksTopBar.kt @@ -24,11 +24,15 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn @Composable fun MusicTracksTopBar( @@ -46,3 +50,11 @@ fun MusicTracksTopBar( ) } } + +@Preview +@Composable +private fun MusicTracksTopBarPreview() { + ThemeComparisonColumn { + MusicTracksTopBar(accountViewModel = mockAccountViewModel(), nav = EmptyNav()) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackButton.kt index 558c59d61e..9d05b125dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackButton.kt @@ -25,15 +25,19 @@ import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size26Modifier import com.vitorpamplona.amethyst.ui.theme.Size55Modifier +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow @Composable fun NewMusicTrackButton( @@ -54,3 +58,11 @@ fun NewMusicTrackButton( ) } } + +@Preview +@Composable +private fun NewMusicTrackButtonPreview() { + ThemeComparisonRow { + NewMusicTrackButton(accountViewModel = mockAccountViewModel(), nav = EmptyNav()) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt index cc45a021e7..f28c72fecb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt @@ -46,13 +46,17 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -214,3 +218,15 @@ private fun DeleteMusicTrackRow( ) } } + +@Preview +@Composable +private fun NewMusicTrackScreenPreview() { + ThemeComparisonColumn { + NewMusicTrackScreen( + editDTag = null, + accountViewModel = mockAccountViewModel(), + nav = EmptyNav(), + ) + } +}