Release: web UX overhaul + Android native port + server polish #59
@@ -69,6 +69,7 @@ import com.fabledsword.minstrel.nav.ArtistDetail
|
||||
import com.fabledsword.minstrel.nav.Home
|
||||
import com.fabledsword.minstrel.nav.PlaylistDetail
|
||||
import com.fabledsword.minstrel.playlists.data.PlaylistsRepository
|
||||
import com.fabledsword.minstrel.playlists.data.toPlayableTrackRefs
|
||||
import com.fabledsword.minstrel.playlists.widgets.OfflinePoolCard
|
||||
import com.fabledsword.minstrel.playlists.widgets.PlaylistCard
|
||||
import com.fabledsword.minstrel.playlists.widgets.PlaylistPlaceholderCard
|
||||
@@ -274,20 +275,10 @@ class HomeViewModel @Inject constructor(
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
val tracks = detail.tracks.mapNotNull { row ->
|
||||
row.trackId?.let { trackId ->
|
||||
TrackRef(
|
||||
id = trackId,
|
||||
title = row.title,
|
||||
albumId = row.albumId.orEmpty(),
|
||||
albumTitle = row.albumTitle,
|
||||
artistId = row.artistId.orEmpty(),
|
||||
artistName = row.artistName,
|
||||
durationSec = row.durationSec,
|
||||
streamUrl = row.streamUrl.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
// Shared with PlaylistDetailViewModel.play - filters out
|
||||
// unplayable rows (missing trackId or empty streamUrl) so the
|
||||
// queue can't end up with tracks Media3 silently rejects.
|
||||
val tracks = detail.tracks.toPlayableTrackRefs()
|
||||
if (tracks.isEmpty()) {
|
||||
poolMessages.trySend("Mix isn't ready yet - try again in a moment")
|
||||
return@launch
|
||||
|
||||
+26
@@ -12,6 +12,7 @@ import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistTrackEntity
|
||||
import com.fabledsword.minstrel.cache.mutations.MutationQueue
|
||||
import com.fabledsword.minstrel.models.PlaylistRef
|
||||
import com.fabledsword.minstrel.models.PlaylistTrackRef
|
||||
import com.fabledsword.minstrel.models.TrackRef
|
||||
import com.fabledsword.minstrel.models.wire.PlaylistDetailWire
|
||||
import com.fabledsword.minstrel.models.wire.PlaylistTrackWire
|
||||
import com.fabledsword.minstrel.models.wire.PlaylistWire
|
||||
@@ -204,6 +205,31 @@ data class PlaylistDetailRef(
|
||||
val tracks: List<PlaylistTrackRef>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Convert each [PlaylistTrackRef] to a player-ready [TrackRef], dropping
|
||||
* any row that isn't actually playable (trackId missing OR streamUrl
|
||||
* missing). The two call sites (PlaylistDetailViewModel.play and
|
||||
* HomeViewModel.playPlaylist) used to hand-roll this conversion and
|
||||
* differed on whether they applied the filter, which let unplayable
|
||||
* tracks reach `player.setQueue` and silently no-op inside Media3
|
||||
* (setUri("") never errors). Centralise both the conversion AND the
|
||||
* defensive filter so the queue is always playable.
|
||||
*/
|
||||
fun List<PlaylistTrackRef>.toPlayableTrackRefs(): List<TrackRef> =
|
||||
filter { it.isAvailable && !it.streamUrl.isNullOrEmpty() }
|
||||
.map { row ->
|
||||
TrackRef(
|
||||
id = row.trackId.orEmpty(),
|
||||
title = row.title,
|
||||
albumId = row.albumId.orEmpty(),
|
||||
artistId = row.artistId.orEmpty(),
|
||||
albumTitle = row.albumTitle,
|
||||
artistName = row.artistName,
|
||||
durationSec = row.durationSec,
|
||||
streamUrl = row.streamUrl.orEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
// ── Mappers (internal — keep Room + wire types out of the UI layer) ──
|
||||
|
||||
private fun CachedPlaylistEntity.toDomain(): PlaylistRef =
|
||||
|
||||
+3
-3
@@ -69,6 +69,7 @@ import com.fabledsword.minstrel.player.PlayerController
|
||||
import com.fabledsword.minstrel.likes.data.LikesRepository
|
||||
import com.fabledsword.minstrel.playlists.data.PlaylistDetailRef
|
||||
import com.fabledsword.minstrel.playlists.data.PlaylistsRepository
|
||||
import com.fabledsword.minstrel.playlists.data.toPlayableTrackRefs
|
||||
import com.fabledsword.minstrel.shared.formatDuration
|
||||
import com.fabledsword.minstrel.shared.widgets.TrackRow
|
||||
import com.fabledsword.minstrel.shared.widgets.LikeButton
|
||||
@@ -223,9 +224,8 @@ class PlaylistDetailViewModel @Inject constructor(
|
||||
|
||||
/** Play the available tracks starting at [startTrackId] (or first available). */
|
||||
fun play(tracks: List<PlaylistTrackRef>, startTrackId: String?) {
|
||||
val playable = tracks.filter { it.isAvailable && !it.streamUrl.isNullOrEmpty() }
|
||||
if (playable.isEmpty()) return
|
||||
val refs = playable.map { it.toTrackRef() }
|
||||
val refs = tracks.toPlayableTrackRefs()
|
||||
if (refs.isEmpty()) return
|
||||
val startIndex = refs.indexOfFirst { it.id == startTrackId }.coerceAtLeast(0)
|
||||
player.setQueue(refs, initialIndex = startIndex, source = "playlist:$playlistId")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user