diff --git a/android/app/src/main/java/com/fabledsword/minstrel/library/data/LibraryMappers.kt b/android/app/src/main/java/com/fabledsword/minstrel/library/data/LibraryMappers.kt index 4c3c214a..a6cf3982 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/library/data/LibraryMappers.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/library/data/LibraryMappers.kt @@ -11,6 +11,8 @@ import com.fabledsword.minstrel.models.wire.AlbumWire import com.fabledsword.minstrel.models.wire.ArtistDetailWire import com.fabledsword.minstrel.models.wire.ArtistWire 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. // Entity fields are a subset of wire fields (display names + sort keys); @@ -37,7 +39,7 @@ fun TrackWire.toEntity(): CachedTrackEntity = albumId = albumId, artistId = artistId, title = title, - durationMs = durationSec * MILLIS_PER_SECOND, + durationMs = durationSec.secondsToMillis(), trackNumber = trackNumber, discNumber = discNumber, ) @@ -94,7 +96,7 @@ fun CachedTrackEntity.toDomain( artistName = artistName, trackNumber = trackNumber, discNumber = discNumber, - durationSec = durationMs / MILLIS_PER_SECOND, + durationSec = durationMs.millisToSeconds(), ) // Wire to Domain — for fresh server responses that bypass the cache @@ -136,5 +138,3 @@ fun AlbumWire.toDomain(): AlbumRef = durationSec = durationSec, coverUrl = coverUrl, ) - -private const val MILLIS_PER_SECOND = 1000 diff --git a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/AlbumDetailScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/AlbumDetailScreen.kt index 7fcaf2c9..02bab424 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/AlbumDetailScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/AlbumDetailScreen.kt @@ -55,6 +55,7 @@ import com.fabledsword.minstrel.models.AlbumRef import com.fabledsword.minstrel.models.TrackRef import com.fabledsword.minstrel.nav.AlbumDetail 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.EmptyState import com.fabledsword.minstrel.shared.widgets.LikeButton @@ -352,7 +353,7 @@ private fun TrackRow( } CachedDot(track.id) Text( - text = formatDuration(track.durationSec), + text = formatDuration(track.durationSec, zero = "--:--"), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 12.dp), @@ -421,12 +422,3 @@ private fun SeededAlbumLoading(seed: AlbumRef) { 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')}" -} diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/NowPlayingScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/NowPlayingScreen.kt index 3a770dfc..af6bef5c 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/NowPlayingScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/NowPlayingScreen.kt @@ -69,6 +69,8 @@ import com.fabledsword.minstrel.nav.LocalAnimatedContentScope import com.fabledsword.minstrel.nav.LocalSharedTransitionScope import com.fabledsword.minstrel.nav.NowPlaying 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.trackactions.TrackActionsButton 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 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 // 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, ) { Text( - text = formatMillis(positionMs), + text = formatDuration(positionMs.millisToSeconds()), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) Text( - text = formatMillis(durationMs), + text = formatDuration(durationMs.millisToSeconds()), style = MaterialTheme.typography.labelSmall, 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) -} diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/QueueScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/QueueScreen.kt index 04ab26e3..71e2f64d 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/QueueScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/QueueScreen.kt @@ -34,6 +34,7 @@ import com.composables.icons.lucide.ArrowLeft import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.Volume2 import com.fabledsword.minstrel.models.TrackRef +import com.fabledsword.minstrel.shared.formatDuration import com.fabledsword.minstrel.shared.widgets.EmptyState @OptIn(ExperimentalMaterial3Api::class) @@ -149,12 +150,4 @@ private fun queueSubtitle(track: TrackRef): String = listOf(track.artistName, tr .filter { it.isNotEmpty() } .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 diff --git a/android/app/src/main/java/com/fabledsword/minstrel/playlists/ui/PlaylistDetailScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/playlists/ui/PlaylistDetailScreen.kt index d9033345..83000c9f 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/playlists/ui/PlaylistDetailScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/playlists/ui/PlaylistDetailScreen.kt @@ -72,6 +72,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.shared.formatDuration import com.fabledsword.minstrel.shared.widgets.CachedDot import com.fabledsword.minstrel.shared.widgets.LikeButton import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold @@ -531,7 +532,7 @@ private fun TrackRow( } row.trackId?.let { CachedDot(it) } Text( - text = formatDuration(row.durationSec), + text = formatDuration(row.durationSec, zero = "--:--"), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = rowAlpha), modifier = Modifier.padding(start = 12.dp), @@ -632,16 +633,8 @@ private fun ErrorBlock(message: String, onRetry: () -> Unit) { // ─── Helpers ───────────────────────────────────────────────────────── -private const val SECONDS_PER_MINUTE = 60 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. * Caller must filter `isAvailable && streamUrl != null` first. diff --git a/android/app/src/main/java/com/fabledsword/minstrel/quarantine/data/QuarantineRepository.kt b/android/app/src/main/java/com/fabledsword/minstrel/quarantine/data/QuarantineRepository.kt index d8d4ac80..f67b0d28 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/quarantine/data/QuarantineRepository.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/quarantine/data/QuarantineRepository.kt @@ -8,6 +8,7 @@ import com.fabledsword.minstrel.cache.mutations.MutationQueue import com.fabledsword.minstrel.models.QuarantineRef import com.fabledsword.minstrel.models.TrackRef import com.fabledsword.minstrel.models.wire.QuarantineMineWire +import com.fabledsword.minstrel.shared.secondsToMillis import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.datetime.Clock @@ -16,8 +17,6 @@ import retrofit2.create import javax.inject.Inject import javax.inject.Singleton -private const val MS_PER_SECOND = 1000 - /** * Outcome of an `unflag` call — mirrors LikesRepository.toggleLike's * RequestOutcome flavor. ACCEPTED = server confirmed; QUEUED = the @@ -142,7 +141,7 @@ private fun TrackRef.toQuarantineEntity(reason: String, notes: String): CachedQu notes = notes.ifEmpty { null }, createdAt = Clock.System.now().toString(), trackTitle = title, - trackDurationMs = durationSec * MS_PER_SECOND, + trackDurationMs = durationSec.secondsToMillis(), albumId = albumId, albumTitle = albumTitle, albumCoverArtPath = null,