fix(android): silent-breakage cluster — search/artist/nowplaying
Three bugs from the v2 parity audit: * Search: tapping a track result built a single-track queue (auto-advance died on track end). Now builds a queue from the full visible Loaded tracks list starting at the tapped row, mirroring Flutter. * ArtistDetail: Play button played albums in tracklist order. Flutter shuffles. .shuffled() on the fetched list. * NowPlaying: when the session tore down (queue finished, queue cleared from elsewhere) the screen stranded the user on an EmptyState with no escape. Replaced with a 500ms-debounced popBackStack so the brief null during MediaController IPC bind doesn't bounce the user, but a genuine session-end pops them back to wherever they came from. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+6
-5
@@ -73,15 +73,16 @@ class ArtistDetailViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull every track across the artist's albums and start playing.
|
||||
* Network round-trip per click; fine because the user explicitly
|
||||
* tapped Play and a cache lookup wouldn't be complete anyway
|
||||
* until per-album hydration finishes.
|
||||
* Pull every track across the artist's albums, shuffle, and start
|
||||
* playing. Mirrors Flutter's artist Play button — pressing Play on
|
||||
* an artist is implicitly "shuffle this artist", not "play album by
|
||||
* album in tracklist order". Network round-trip per click; fine
|
||||
* because the user explicitly tapped Play.
|
||||
*/
|
||||
fun playArtist() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val tracks = repository.fetchArtistTracks(artistId)
|
||||
val tracks = repository.fetchArtistTracks(artistId).shuffled()
|
||||
if (tracks.isNotEmpty()) {
|
||||
player.setQueue(
|
||||
tracks = tracks,
|
||||
|
||||
@@ -52,7 +52,6 @@ import com.fabledsword.minstrel.player.RepeatMode
|
||||
import com.fabledsword.minstrel.nav.AlbumDetail
|
||||
import com.fabledsword.minstrel.nav.ArtistDetail
|
||||
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.TrackActionsViewModel
|
||||
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 MS_PER_SECOND = 1000L
|
||||
private const val SECONDS_PER_MINUTE = 60L
|
||||
private const val POP_GRACE_MS = 500L
|
||||
|
||||
/**
|
||||
* Full-screen player. Shows the cover (placeholder until AlbumRef join
|
||||
@@ -85,10 +85,17 @@ fun NowPlayingScreen(
|
||||
}
|
||||
val track = state.currentTrack
|
||||
if (track == null) {
|
||||
EmptyState(
|
||||
title = "Nothing playing",
|
||||
body = "Pick a track from the library to start.",
|
||||
)
|
||||
// Session torn down (queue finished + auto-stop, or user cleared
|
||||
// the queue from elsewhere). Pop back to whichever shell screen
|
||||
// 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
|
||||
}
|
||||
Scaffold(
|
||||
|
||||
@@ -76,9 +76,22 @@ class SearchViewModel @Inject constructor(
|
||||
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) {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user