Release v2026.05.08.1 — DRY pass + cover-art HTTP base #31
+6
-5
@@ -73,15 +73,16 @@ class ArtistDetailViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pull every track across the artist's albums and start playing.
|
* Pull every track across the artist's albums, shuffle, and start
|
||||||
* Network round-trip per click; fine because the user explicitly
|
* playing. Mirrors Flutter's artist Play button — pressing Play on
|
||||||
* tapped Play and a cache lookup wouldn't be complete anyway
|
* an artist is implicitly "shuffle this artist", not "play album by
|
||||||
* until per-album hydration finishes.
|
* album in tracklist order". Network round-trip per click; fine
|
||||||
|
* because the user explicitly tapped Play.
|
||||||
*/
|
*/
|
||||||
fun playArtist() {
|
fun playArtist() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val tracks = repository.fetchArtistTracks(artistId)
|
val tracks = repository.fetchArtistTracks(artistId).shuffled()
|
||||||
if (tracks.isNotEmpty()) {
|
if (tracks.isNotEmpty()) {
|
||||||
player.setQueue(
|
player.setQueue(
|
||||||
tracks = tracks,
|
tracks = tracks,
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ import com.fabledsword.minstrel.player.RepeatMode
|
|||||||
import com.fabledsword.minstrel.nav.AlbumDetail
|
import com.fabledsword.minstrel.nav.AlbumDetail
|
||||||
import com.fabledsword.minstrel.nav.ArtistDetail
|
import com.fabledsword.minstrel.nav.ArtistDetail
|
||||||
import com.fabledsword.minstrel.nav.Queue
|
import com.fabledsword.minstrel.nav.Queue
|
||||||
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
|
||||||
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton
|
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton
|
||||||
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel
|
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel
|
||||||
import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
|
import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
|
||||||
@@ -63,6 +62,7 @@ private const val TRANSPORT_ICON_DP = 36
|
|||||||
private const val PLAY_PAUSE_ICON_DP = 56
|
private const val PLAY_PAUSE_ICON_DP = 56
|
||||||
private const val MS_PER_SECOND = 1000L
|
private const val MS_PER_SECOND = 1000L
|
||||||
private const val SECONDS_PER_MINUTE = 60L
|
private const val SECONDS_PER_MINUTE = 60L
|
||||||
|
private const val POP_GRACE_MS = 500L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full-screen player. Shows the cover (placeholder until AlbumRef join
|
* Full-screen player. Shows the cover (placeholder until AlbumRef join
|
||||||
@@ -85,10 +85,17 @@ fun NowPlayingScreen(
|
|||||||
}
|
}
|
||||||
val track = state.currentTrack
|
val track = state.currentTrack
|
||||||
if (track == null) {
|
if (track == null) {
|
||||||
EmptyState(
|
// Session torn down (queue finished + auto-stop, or user cleared
|
||||||
title = "Nothing playing",
|
// the queue from elsewhere). Pop back to whichever shell screen
|
||||||
body = "Pick a track from the library to start.",
|
// launched NowPlaying rather than stranding the user on an
|
||||||
)
|
// EmptyState with no escape. A short delay swallows the
|
||||||
|
// momentary null during MediaController IPC bind on cold-mount.
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
kotlinx.coroutines.delay(POP_GRACE_MS)
|
||||||
|
if (viewModel.uiState.value.currentTrack == null) {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|||||||
@@ -76,9 +76,22 @@ class SearchViewModel @Inject constructor(
|
|||||||
setQuery("")
|
setQuery("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tapping a search result builds a queue from the full visible
|
||||||
|
* track-results list starting at the tapped entry, matching Flutter.
|
||||||
|
* Falls back to single-track playback when the loaded state isn't
|
||||||
|
* a Success (shouldn't happen in normal flow but keeps the call
|
||||||
|
* safe).
|
||||||
|
*/
|
||||||
fun playTrack(track: TrackRef) {
|
fun playTrack(track: TrackRef) {
|
||||||
if (track.streamUrl.isEmpty()) return
|
if (track.streamUrl.isEmpty()) return
|
||||||
player.setQueue(listOf(track), initialIndex = 0, source = "search")
|
val loaded = internal.value.results as? SearchResultsState.Loaded
|
||||||
|
val playable = loaded?.response?.tracks
|
||||||
|
?.filter { it.streamUrl.isNotEmpty() }
|
||||||
|
?.takeIf { it.isNotEmpty() }
|
||||||
|
?: listOf(track)
|
||||||
|
val startIndex = playable.indexOfFirst { it.id == track.id }.coerceAtLeast(0)
|
||||||
|
player.setQueue(playable, initialIndex = startIndex, source = "search")
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun runSearch(q: String) {
|
private suspend fun runSearch(q: String) {
|
||||||
|
|||||||
Reference in New Issue
Block a user