release v2026.05.13.1: player + Discover hotfix #44

Merged
bvandeusen merged 262 commits from dev into main 2026-05-14 11:17:47 -04:00
6 changed files with 15 additions and 46 deletions
Showing only changes of commit 3e35127284 - Show all commits
@@ -11,6 +11,8 @@ import com.fabledsword.minstrel.models.wire.AlbumWire
import com.fabledsword.minstrel.models.wire.ArtistDetailWire import com.fabledsword.minstrel.models.wire.ArtistDetailWire
import com.fabledsword.minstrel.models.wire.ArtistWire import com.fabledsword.minstrel.models.wire.ArtistWire
import com.fabledsword.minstrel.models.wire.TrackWire import com.fabledsword.minstrel.models.wire.TrackWire
import com.fabledsword.minstrel.shared.millisToSeconds
import com.fabledsword.minstrel.shared.secondsToMillis
// Wire to Entity — used after a successful sync/fetch to persist into Room. // Wire to Entity — used after a successful sync/fetch to persist into Room.
// Entity fields are a subset of wire fields (display names + sort keys); // Entity fields are a subset of wire fields (display names + sort keys);
@@ -37,7 +39,7 @@ fun TrackWire.toEntity(): CachedTrackEntity =
albumId = albumId, albumId = albumId,
artistId = artistId, artistId = artistId,
title = title, title = title,
durationMs = durationSec * MILLIS_PER_SECOND, durationMs = durationSec.secondsToMillis(),
trackNumber = trackNumber, trackNumber = trackNumber,
discNumber = discNumber, discNumber = discNumber,
) )
@@ -94,7 +96,7 @@ fun CachedTrackEntity.toDomain(
artistName = artistName, artistName = artistName,
trackNumber = trackNumber, trackNumber = trackNumber,
discNumber = discNumber, discNumber = discNumber,
durationSec = durationMs / MILLIS_PER_SECOND, durationSec = durationMs.millisToSeconds(),
) )
// Wire to Domain — for fresh server responses that bypass the cache // Wire to Domain — for fresh server responses that bypass the cache
@@ -136,5 +138,3 @@ fun AlbumWire.toDomain(): AlbumRef =
durationSec = durationSec, durationSec = durationSec,
coverUrl = coverUrl, coverUrl = coverUrl,
) )
private const val MILLIS_PER_SECOND = 1000
@@ -55,6 +55,7 @@ import com.fabledsword.minstrel.models.AlbumRef
import com.fabledsword.minstrel.models.TrackRef import com.fabledsword.minstrel.models.TrackRef
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.shared.formatDuration
import com.fabledsword.minstrel.shared.widgets.CachedDot import com.fabledsword.minstrel.shared.widgets.CachedDot
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.LikeButton import com.fabledsword.minstrel.shared.widgets.LikeButton
@@ -352,7 +353,7 @@ private fun TrackRow(
} }
CachedDot(track.id) CachedDot(track.id)
Text( Text(
text = formatDuration(track.durationSec), text = formatDuration(track.durationSec, zero = "--:--"),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 12.dp), modifier = Modifier.padding(start = 12.dp),
@@ -421,12 +422,3 @@ private fun SeededAlbumLoading(seed: AlbumRef) {
items(SKELETON_TRACK_ROWS) { SkeletonTrackRow() } items(SKELETON_TRACK_ROWS) { SkeletonTrackRow() }
} }
} }
private const val SECONDS_PER_MINUTE = 60
private fun formatDuration(seconds: Int): String {
if (seconds <= 0) return "--:--"
val m = seconds / SECONDS_PER_MINUTE
val s = seconds % SECONDS_PER_MINUTE
return "$m:${s.toString().padStart(2, '0')}"
}
@@ -69,6 +69,8 @@ import com.fabledsword.minstrel.nav.LocalAnimatedContentScope
import com.fabledsword.minstrel.nav.LocalSharedTransitionScope import com.fabledsword.minstrel.nav.LocalSharedTransitionScope
import com.fabledsword.minstrel.nav.NowPlaying import com.fabledsword.minstrel.nav.NowPlaying
import com.fabledsword.minstrel.nav.Queue import com.fabledsword.minstrel.nav.Queue
import com.fabledsword.minstrel.shared.formatDuration
import com.fabledsword.minstrel.shared.millisToSeconds
import com.fabledsword.minstrel.shared.widgets.LikeButton import com.fabledsword.minstrel.shared.widgets.LikeButton
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
@@ -78,8 +80,6 @@ import com.fabledsword.minstrel.theme.LocalActionColors
private const val COVER_MAX_WIDTH_DP = 320 private const val COVER_MAX_WIDTH_DP = 320
private const val TRANSPORT_ICON_DP = 36 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 SECONDS_PER_MINUTE = 60L
private const val POP_GRACE_MS = 500L private const val POP_GRACE_MS = 500L
// Vertical drag-down threshold (in pixels) past which the gesture // Vertical drag-down threshold (in pixels) past which the gesture
@@ -420,12 +420,12 @@ private fun ScrubberRow(positionMs: Long, durationMs: Long, onSeek: (Long) -> Un
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Text( Text(
text = formatMillis(positionMs), text = formatDuration(positionMs.millisToSeconds()),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
Text( Text(
text = formatMillis(durationMs), text = formatDuration(durationMs.millisToSeconds()),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
@@ -471,11 +471,3 @@ private fun TransportRow(
} }
} }
} }
private fun formatMillis(ms: Long): String {
if (ms <= 0) return "0:00"
val totalSeconds = ms / MS_PER_SECOND
val minutes = totalSeconds / SECONDS_PER_MINUTE
val seconds = totalSeconds % SECONDS_PER_MINUTE
return "%d:%02d".format(minutes, seconds)
}
@@ -34,6 +34,7 @@ import com.composables.icons.lucide.ArrowLeft
import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Volume2 import com.composables.icons.lucide.Volume2
import com.fabledsword.minstrel.models.TrackRef import com.fabledsword.minstrel.models.TrackRef
import com.fabledsword.minstrel.shared.formatDuration
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -149,12 +150,4 @@ private fun queueSubtitle(track: TrackRef): String = listOf(track.artistName, tr
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
.joinToString(" · ") .joinToString(" · ")
private const val SECONDS_PER_MINUTE = 60
private fun formatDuration(seconds: Int): String {
val mins = seconds / SECONDS_PER_MINUTE
val secs = seconds % SECONDS_PER_MINUTE
return "%d:%02d".format(mins, secs)
}
private const val HIGHLIGHT_ALPHA = 0.12f private const val HIGHLIGHT_ALPHA = 0.12f
@@ -72,6 +72,7 @@ import com.fabledsword.minstrel.player.PlayerController
import com.fabledsword.minstrel.likes.data.LikesRepository import com.fabledsword.minstrel.likes.data.LikesRepository
import com.fabledsword.minstrel.playlists.data.PlaylistDetailRef import com.fabledsword.minstrel.playlists.data.PlaylistDetailRef
import com.fabledsword.minstrel.playlists.data.PlaylistsRepository import com.fabledsword.minstrel.playlists.data.PlaylistsRepository
import com.fabledsword.minstrel.shared.formatDuration
import com.fabledsword.minstrel.shared.widgets.CachedDot import com.fabledsword.minstrel.shared.widgets.CachedDot
import com.fabledsword.minstrel.shared.widgets.LikeButton import com.fabledsword.minstrel.shared.widgets.LikeButton
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
@@ -531,7 +532,7 @@ private fun TrackRow(
} }
row.trackId?.let { CachedDot(it) } row.trackId?.let { CachedDot(it) }
Text( Text(
text = formatDuration(row.durationSec), text = formatDuration(row.durationSec, zero = "--:--"),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = rowAlpha), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = rowAlpha),
modifier = Modifier.padding(start = 12.dp), modifier = Modifier.padding(start = 12.dp),
@@ -632,16 +633,8 @@ private fun ErrorBlock(message: String, onRetry: () -> Unit) {
// ─── Helpers ───────────────────────────────────────────────────────── // ─── Helpers ─────────────────────────────────────────────────────────
private const val SECONDS_PER_MINUTE = 60
private const val UNAVAILABLE_ALPHA = 0.4f private const val UNAVAILABLE_ALPHA = 0.4f
private fun formatDuration(seconds: Int): String {
if (seconds <= 0) return "--:--"
val m = seconds / SECONDS_PER_MINUTE
val s = seconds % SECONDS_PER_MINUTE
return "$m:${s.toString().padStart(2, '0')}"
}
/** /**
* Lossy mapping: PlaylistTrackRef → TrackRef so the player can take it. * Lossy mapping: PlaylistTrackRef → TrackRef so the player can take it.
* Caller must filter `isAvailable && streamUrl != null` first. * Caller must filter `isAvailable && streamUrl != null` first.
@@ -8,6 +8,7 @@ import com.fabledsword.minstrel.cache.mutations.MutationQueue
import com.fabledsword.minstrel.models.QuarantineRef import com.fabledsword.minstrel.models.QuarantineRef
import com.fabledsword.minstrel.models.TrackRef import com.fabledsword.minstrel.models.TrackRef
import com.fabledsword.minstrel.models.wire.QuarantineMineWire import com.fabledsword.minstrel.models.wire.QuarantineMineWire
import com.fabledsword.minstrel.shared.secondsToMillis
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@@ -16,8 +17,6 @@ import retrofit2.create
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
private const val MS_PER_SECOND = 1000
/** /**
* Outcome of an `unflag` call — mirrors LikesRepository.toggleLike's * Outcome of an `unflag` call — mirrors LikesRepository.toggleLike's
* RequestOutcome flavor. ACCEPTED = server confirmed; QUEUED = the * RequestOutcome flavor. ACCEPTED = server confirmed; QUEUED = the
@@ -142,7 +141,7 @@ private fun TrackRef.toQuarantineEntity(reason: String, notes: String): CachedQu
notes = notes.ifEmpty { null }, notes = notes.ifEmpty { null },
createdAt = Clock.System.now().toString(), createdAt = Clock.System.now().toString(),
trackTitle = title, trackTitle = title,
trackDurationMs = durationSec * MS_PER_SECOND, trackDurationMs = durationSec.secondsToMillis(),
albumId = albumId, albumId = albumId,
albumTitle = albumTitle, albumTitle = albumTitle,
albumCoverArtPath = null, albumCoverArtPath = null,