playback: add smooth seeking
Switch position math to rely on deciseconds (1/10th of a second) instead of full seconds. This makes seeking and position management much smoother, with minimal performance cost. In the future I may try to migrate the playback state so that the position calculations are done on the UI end, but this works for now.
This commit is contained in:
parent
1ed6d75121
commit
13793fdfe2
16 changed files with 99 additions and 63 deletions
|
@ -2,6 +2,12 @@
|
|||
|
||||
## dev
|
||||
|
||||
#### What's New
|
||||
- Made the SeekBar much smoother and better to use
|
||||
|
||||
#### What's Fixed
|
||||
- Fixed issue where fast scroller popup would not appear
|
||||
|
||||
## 2.6.2
|
||||
|
||||
#### What's New
|
||||
|
|
|
@ -29,7 +29,7 @@ import org.oxycblt.auxio.databinding.DialogSongDetailBinding
|
|||
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.formatDuration
|
||||
import org.oxycblt.auxio.util.formatDurationMs
|
||||
|
||||
/**
|
||||
* A dialog displayed when "View properties" is selected on a song, showing more information about
|
||||
|
@ -70,7 +70,7 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
|
|||
binding.detailRelativeDir.setText(song.song.path.parent.resolveName(context))
|
||||
binding.detailFormat.setText(song.info.resolvedMimeType.resolveName(context))
|
||||
binding.detailSize.setText(Formatter.formatFileSize(context, song.song.size))
|
||||
binding.detailDuration.setText(song.song.durationSecs.formatDuration(true))
|
||||
binding.detailDuration.setText(song.song.durationMs.formatDurationMs(true))
|
||||
|
||||
if (song.info.bitrateKbps != null) {
|
||||
binding.detailBitrate.setText(
|
||||
|
|
|
@ -33,7 +33,7 @@ import org.oxycblt.auxio.ui.recycler.Item
|
|||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.formatDuration
|
||||
import org.oxycblt.auxio.util.formatDurationMs
|
||||
import org.oxycblt.auxio.util.getPlural
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
|
@ -131,7 +131,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
|
|||
|
||||
val songCount = context.getPlural(R.plurals.fmt_song_count, item.songs.size)
|
||||
|
||||
val duration = item.durationSecs.formatDuration(true)
|
||||
val duration = item.durationMs.formatDurationMs(true)
|
||||
|
||||
text = context.getString(R.string.fmt_three, date, songCount, duration)
|
||||
}
|
||||
|
@ -157,7 +157,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
|
|||
oldItem.artist.rawName == newItem.artist.rawName &&
|
||||
oldItem.date == newItem.date &&
|
||||
oldItem.songs.size == newItem.songs.size &&
|
||||
oldItem.durationSecs == newItem.durationSecs &&
|
||||
oldItem.durationMs == newItem.durationMs &&
|
||||
oldItem.releaseType == newItem.releaseType
|
||||
}
|
||||
}
|
||||
|
@ -207,7 +207,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
|
|||
}
|
||||
|
||||
binding.songName.text = item.resolveName(binding.context)
|
||||
binding.songDuration.text = item.durationSecs.formatDuration(false)
|
||||
binding.songDuration.text = item.durationMs.formatDurationMs(false)
|
||||
|
||||
// binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||
binding.root.setOnLongClickListener {
|
||||
|
|
|
@ -29,7 +29,7 @@ import org.oxycblt.auxio.ui.recycler.Item
|
|||
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
||||
import org.oxycblt.auxio.ui.recycler.SongViewHolder
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.formatDuration
|
||||
import org.oxycblt.auxio.util.formatDurationMs
|
||||
import org.oxycblt.auxio.util.getPlural
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
|
@ -106,7 +106,7 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
|
|||
binding.detailName.text = item.resolveName(binding.context)
|
||||
binding.detailSubhead.text =
|
||||
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size)
|
||||
binding.detailInfo.text = item.durationSecs.formatDuration(false)
|
||||
binding.detailInfo.text = item.durationMs.formatDurationMs(false)
|
||||
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
|
||||
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
|
|||
override fun areItemsTheSame(oldItem: Genre, newItem: Genre) =
|
||||
oldItem.rawName == newItem.rawName &&
|
||||
oldItem.songs.size == newItem.songs.size &&
|
||||
oldItem.durationSecs == newItem.durationSecs
|
||||
oldItem.durationMs == newItem.durationMs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,8 +33,9 @@ import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
|||
import org.oxycblt.auxio.ui.recycler.MonoAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.SyncBackingData
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.formatDuration
|
||||
import org.oxycblt.auxio.util.formatDurationMs
|
||||
import org.oxycblt.auxio.util.logEOrThrow
|
||||
import org.oxycblt.auxio.util.secsToMs
|
||||
|
||||
/**
|
||||
* A [HomeListFragment] for showing a list of [Album]s.
|
||||
|
@ -71,14 +72,14 @@ class AlbumListFragment : HomeListFragment<Album>() {
|
|||
is Sort.Mode.ByYear -> album.date?.resolveYear(requireContext())
|
||||
|
||||
// Duration -> Use formatted duration
|
||||
is Sort.Mode.ByDuration -> album.durationSecs.formatDuration(false)
|
||||
is Sort.Mode.ByDuration -> album.durationMs.formatDurationMs(false)
|
||||
|
||||
// Count -> Use song count
|
||||
is Sort.Mode.ByCount -> album.songs.size.toString()
|
||||
|
||||
// Last added -> Format as date
|
||||
is Sort.Mode.ByDateAdded -> {
|
||||
val dateAddedMillis = album.dateAdded * 1000
|
||||
val dateAddedMillis = album.dateAdded.secsToMs()
|
||||
formatterSb.setLength(0)
|
||||
DateUtils.formatDateRange(
|
||||
context,
|
||||
|
|
|
@ -31,7 +31,7 @@ import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
|||
import org.oxycblt.auxio.ui.recycler.MonoAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.SyncBackingData
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.formatDuration
|
||||
import org.oxycblt.auxio.util.formatDurationMs
|
||||
import org.oxycblt.auxio.util.logEOrThrow
|
||||
|
||||
/**
|
||||
|
@ -61,7 +61,7 @@ class ArtistListFragment : HomeListFragment<Artist>() {
|
|||
is Sort.Mode.ByName -> artist.sortName?.run { first().uppercase() }
|
||||
|
||||
// Duration -> Use formatted duration
|
||||
is Sort.Mode.ByDuration -> artist.durationSecs.formatDuration(false)
|
||||
is Sort.Mode.ByDuration -> artist.durationMs.formatDurationMs(false)
|
||||
|
||||
// Count -> Use song count
|
||||
is Sort.Mode.ByCount -> artist.songs.size.toString()
|
||||
|
|
|
@ -31,7 +31,7 @@ import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
|||
import org.oxycblt.auxio.ui.recycler.MonoAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.SyncBackingData
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.formatDuration
|
||||
import org.oxycblt.auxio.util.formatDurationMs
|
||||
import org.oxycblt.auxio.util.logEOrThrow
|
||||
|
||||
/**
|
||||
|
@ -61,7 +61,7 @@ class GenreListFragment : HomeListFragment<Genre>() {
|
|||
is Sort.Mode.ByName -> genre.sortName?.run { first().uppercase() }
|
||||
|
||||
// Duration -> Use formatted duration
|
||||
is Sort.Mode.ByDuration -> genre.durationSecs.formatDuration(false)
|
||||
is Sort.Mode.ByDuration -> genre.durationMs.formatDurationMs(false)
|
||||
|
||||
// Count -> Use song count
|
||||
is Sort.Mode.ByCount -> genre.songs.size.toString()
|
||||
|
|
|
@ -34,8 +34,9 @@ import org.oxycblt.auxio.ui.recycler.SongViewHolder
|
|||
import org.oxycblt.auxio.ui.recycler.SyncBackingData
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.formatDuration
|
||||
import org.oxycblt.auxio.util.formatDurationMs
|
||||
import org.oxycblt.auxio.util.logEOrThrow
|
||||
import org.oxycblt.auxio.util.secsToMs
|
||||
|
||||
/**
|
||||
* A [HomeListFragment] for showing a list of [Song]s.
|
||||
|
@ -78,11 +79,11 @@ class SongListFragment : HomeListFragment<Song>() {
|
|||
is Sort.Mode.ByYear -> song.album.date?.resolveYear(requireContext())
|
||||
|
||||
// Duration -> Use formatted duration
|
||||
is Sort.Mode.ByDuration -> song.durationSecs.formatDuration(false)
|
||||
is Sort.Mode.ByDuration -> song.durationMs.formatDurationMs(false)
|
||||
|
||||
// Last added -> Format as date
|
||||
is Sort.Mode.ByDateAdded -> {
|
||||
val dateAddedMillis = song.dateAdded * 1000
|
||||
val dateAddedMillis = song.dateAdded.secsToMs()
|
||||
formatterSb.setLength(0)
|
||||
DateUtils.formatDateRange(
|
||||
context,
|
||||
|
|
|
@ -62,10 +62,6 @@ sealed class Music : Item() {
|
|||
sealed class MusicParent : Music() {
|
||||
/** The songs that this parent owns. */
|
||||
abstract val songs: List<Song>
|
||||
|
||||
/** The formatted total duration of this genre */
|
||||
val durationSecs: Long
|
||||
get() = songs.sumOf { it.durationSecs }
|
||||
}
|
||||
|
||||
/** The data object for a song. */
|
||||
|
@ -122,9 +118,6 @@ data class Song(
|
|||
|
||||
override fun resolveName(context: Context) = rawName
|
||||
|
||||
/** The duration of this song, in seconds (rounded down) */
|
||||
val durationSecs = durationMs / 1000
|
||||
|
||||
private var _album: Album? = null
|
||||
/** The album of this song. */
|
||||
val album: Album
|
||||
|
@ -236,6 +229,9 @@ data class Album(
|
|||
/** The earliest date a song in this album was added. */
|
||||
val dateAdded = songs.minOf { it.dateAdded }
|
||||
|
||||
val durationMs: Long
|
||||
get() = songs.sumOf { it.durationMs }
|
||||
|
||||
/** Internal field. Do not use. */
|
||||
val _artistGroupingId: Long
|
||||
get() = _artistGroupingName.toMusicId()
|
||||
|
@ -273,6 +269,9 @@ data class Artist(
|
|||
|
||||
/** The songs of this artist. */
|
||||
override val songs = albums.flatMap { it.songs }
|
||||
|
||||
val durationMs: Long
|
||||
get() = songs.sumOf { it.durationMs }
|
||||
}
|
||||
|
||||
/** The data object for a genre. */
|
||||
|
@ -291,6 +290,9 @@ data class Genre(override val rawName: String?, override val songs: List<Song>)
|
|||
get() = rawName.toMusicId()
|
||||
|
||||
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_genre)
|
||||
|
||||
val durationMs: Long
|
||||
get() = songs.sumOf { it.durationMs }
|
||||
}
|
||||
|
||||
private fun String?.toMusicId(): Long {
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.oxycblt.auxio.playback
|
|||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import kotlin.math.max
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding
|
||||
|
@ -33,6 +34,7 @@ import org.oxycblt.auxio.util.androidActivityViewModels
|
|||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.getAttrColorCompat
|
||||
import org.oxycblt.auxio.util.getColorCompat
|
||||
import org.oxycblt.auxio.util.msToDs
|
||||
|
||||
/**
|
||||
* A fragment showing the current playback state in a compact manner. Used as the bar for the
|
||||
|
@ -104,7 +106,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
|
|||
|
||||
collectImmediately(playbackModel.song, ::updateSong)
|
||||
collectImmediately(playbackModel.isPlaying, ::updateIsPlaying)
|
||||
collectImmediately(playbackModel.positionSecs, ::updatePosition)
|
||||
collectImmediately(playbackModel.positionDs, ::updatePosition)
|
||||
}
|
||||
|
||||
override fun onDestroyBinding(binding: FragmentPlaybackBarBinding) {
|
||||
|
@ -119,7 +121,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
|
|||
binding.playbackCover.bind(song)
|
||||
binding.playbackSong.text = song.resolveName(context)
|
||||
binding.playbackInfo.text = song.resolveIndividualArtistName(context)
|
||||
binding.playbackProgressBar.max = song.durationSecs.toInt()
|
||||
binding.playbackProgressBar.max = song.durationMs.msToDs().toInt()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,8 +140,8 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
|
|||
requireBinding().playbackSecondaryAction.isActivated = isShuffled
|
||||
}
|
||||
|
||||
private fun updatePosition(position: Long) {
|
||||
requireBinding().playbackProgressBar.progress = position.toInt()
|
||||
private fun updatePosition(positionDs: Long) {
|
||||
requireBinding().playbackProgressBar.progress = positionDs.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode
|
|||
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||
import org.oxycblt.auxio.ui.fragment.MenuFragment
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.msToDs
|
||||
import org.oxycblt.auxio.util.showToast
|
||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||
|
||||
|
@ -109,7 +110,7 @@ class PlaybackPanelFragment :
|
|||
|
||||
collectImmediately(playbackModel.song, ::updateSong)
|
||||
collectImmediately(playbackModel.parent, ::updateParent)
|
||||
collectImmediately(playbackModel.positionSecs, ::updatePosition)
|
||||
collectImmediately(playbackModel.positionDs, ::updatePosition)
|
||||
collectImmediately(playbackModel.repeatMode, ::updateRepeat)
|
||||
collectImmediately(playbackModel.isPlaying, ::updatePlaying)
|
||||
collectImmediately(playbackModel.isShuffled, ::updateShuffled)
|
||||
|
@ -142,8 +143,8 @@ class PlaybackPanelFragment :
|
|||
}
|
||||
}
|
||||
|
||||
override fun seekTo(positionSecs: Long) {
|
||||
playbackModel.seekTo(positionSecs)
|
||||
override fun seekTo(positionDs: Long) {
|
||||
playbackModel.seekTo(positionDs)
|
||||
}
|
||||
|
||||
private fun updateSong(song: Song?) {
|
||||
|
@ -155,7 +156,7 @@ class PlaybackPanelFragment :
|
|||
binding.playbackSong.text = song.resolveName(context)
|
||||
binding.playbackArtist.text = song.resolveIndividualArtistName(context)
|
||||
binding.playbackAlbum.text = song.album.resolveName(context)
|
||||
binding.playbackSeekBar.durationSecs = song.durationSecs
|
||||
binding.playbackSeekBar.durationDs = song.durationMs.msToDs()
|
||||
}
|
||||
|
||||
private fun updateParent(parent: MusicParent?) {
|
||||
|
@ -166,8 +167,8 @@ class PlaybackPanelFragment :
|
|||
parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs)
|
||||
}
|
||||
|
||||
private fun updatePosition(positionSecs: Long) {
|
||||
requireBinding().playbackSeekBar.positionSecs = positionSecs
|
||||
private fun updatePosition(positionDs: Long) {
|
||||
requireBinding().playbackSeekBar.positionDs = positionDs
|
||||
}
|
||||
|
||||
private fun updateRepeat(repeatMode: RepeatMode) {
|
||||
|
|
|
@ -35,7 +35,9 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
|||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.application
|
||||
import org.oxycblt.auxio.util.dsToMs
|
||||
import org.oxycblt.auxio.util.logE
|
||||
import org.oxycblt.auxio.util.msToDs
|
||||
|
||||
/**
|
||||
* The ViewModel that provides a UI frontend for [PlaybackStateManager].
|
||||
|
@ -60,10 +62,10 @@ class PlaybackViewModel(application: Application) :
|
|||
private val _isPlaying = MutableStateFlow(false)
|
||||
val isPlaying: StateFlow<Boolean>
|
||||
get() = _isPlaying
|
||||
private val _positionSecs = MutableStateFlow(0L)
|
||||
/** The current playback position, in seconds */
|
||||
val positionSecs: StateFlow<Long>
|
||||
get() = _positionSecs
|
||||
private val _positionDs = MutableStateFlow(0L)
|
||||
/** The current playback position, in *deci-seconds* */
|
||||
val positionDs: StateFlow<Long>
|
||||
get() = _positionDs
|
||||
|
||||
private val _repeatMode = MutableStateFlow(RepeatMode.NONE)
|
||||
/** The current repeat mode, see [RepeatMode] for more information */
|
||||
|
@ -147,8 +149,8 @@ class PlaybackViewModel(application: Application) :
|
|||
// --- PLAYER FUNCTIONS ---
|
||||
|
||||
/** Update the position and push it to [PlaybackStateManager] */
|
||||
fun seekTo(positionSecs: Long) {
|
||||
playbackManager.seekTo(positionSecs * 1000)
|
||||
fun seekTo(positionDs: Long) {
|
||||
playbackManager.seekTo(positionDs.dsToMs())
|
||||
}
|
||||
|
||||
// --- QUEUE FUNCTIONS ---
|
||||
|
@ -267,7 +269,7 @@ class PlaybackViewModel(application: Application) :
|
|||
}
|
||||
|
||||
override fun onPositionChanged(positionMs: Long) {
|
||||
_positionSecs.value = positionMs / 1000
|
||||
_positionDs.value = positionMs.msToDs()
|
||||
}
|
||||
|
||||
override fun onPlayingChanged(isPlaying: Boolean) {
|
||||
|
|
|
@ -22,7 +22,7 @@ import android.util.AttributeSet
|
|||
import com.google.android.material.slider.Slider
|
||||
import kotlin.math.max
|
||||
import org.oxycblt.auxio.databinding.ViewSeekBarBinding
|
||||
import org.oxycblt.auxio.util.formatDuration
|
||||
import org.oxycblt.auxio.util.formatDurationDs
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
|
@ -66,18 +66,18 @@ constructor(
|
|||
* The current position, in seconds. This is the current value of the SeekBar and is indicated
|
||||
* by the start TextView in the layout.
|
||||
*/
|
||||
var positionSecs: Long
|
||||
var positionDs: Long
|
||||
get() = binding.seekBarSlider.value.toLong()
|
||||
set(value) {
|
||||
// Sanity check: Ensure that this value is within the duration and will not crash
|
||||
// the app, and that the user is not currently seeking (which would cause the SeekBar
|
||||
// to jump around).
|
||||
if (value <= durationSecs && !isActivated) {
|
||||
if (value <= durationDs && !isActivated) {
|
||||
binding.seekBarSlider.value = value.toFloat()
|
||||
|
||||
// We would want to keep this in the callback, but the callback only fires when
|
||||
// a value changes completely, and sometimes that does not happen with this view.
|
||||
binding.seekBarPosition.text = value.formatDuration(true)
|
||||
binding.seekBarPosition.text = value.formatDurationDs(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,7 @@ constructor(
|
|||
* The current duration, in seconds. This is the end value of the SeekBar and is indicated by
|
||||
* the end TextView in the layout.
|
||||
*/
|
||||
var durationSecs: Long
|
||||
var durationDs: Long
|
||||
get() = binding.seekBarSlider.valueTo.toLong()
|
||||
set(value) {
|
||||
// Sanity check 1: If this is a value so low that it effectively rounds down to
|
||||
|
@ -95,13 +95,13 @@ constructor(
|
|||
|
||||
// Sanity check 2: If the current value exceeds the new duration value, clamp it
|
||||
// down so that we don't crash and instead have an annoying visual flicker.
|
||||
if (positionSecs > to) {
|
||||
logD("Clamping invalid position [current: $positionSecs new max: $to]")
|
||||
if (positionDs > to) {
|
||||
logD("Clamping invalid position [current: $positionDs new max: $to]")
|
||||
binding.seekBarSlider.value = to.toFloat()
|
||||
}
|
||||
|
||||
binding.seekBarSlider.valueTo = to.toFloat()
|
||||
binding.seekBarDuration.text = value.formatDuration(false)
|
||||
binding.seekBarDuration.text = value.formatDurationDs(false)
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(slider: Slider) {
|
||||
|
@ -119,13 +119,13 @@ constructor(
|
|||
}
|
||||
|
||||
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
||||
binding.seekBarPosition.text = value.toLong().formatDuration(true)
|
||||
binding.seekBarPosition.text = value.toLong().formatDurationDs(true)
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
/**
|
||||
* Called when a seek event was completed and the new position must be seeked to by the app.
|
||||
*/
|
||||
fun seekTo(positionSecs: Long)
|
||||
fun seekTo(positionDs: Long)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ import org.oxycblt.auxio.music.Song
|
|||
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.formatDuration
|
||||
import org.oxycblt.auxio.util.formatDurationMs
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.showToast
|
||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||
|
@ -84,7 +84,7 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
|
|||
binding.aboutTotalDuration.text =
|
||||
getString(
|
||||
R.string.fmt_lib_total_duration,
|
||||
songs.sumOf { it.durationSecs }.formatDuration(false))
|
||||
songs.sumOf { it.durationMs }.formatDurationMs(false))
|
||||
}
|
||||
|
||||
private fun updateAlbumCount(albums: List<Album>) {
|
||||
|
|
|
@ -205,23 +205,20 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
|||
|
||||
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
|
||||
MultiComparator(
|
||||
compareByDynamic(ascending) { it.durationSecs },
|
||||
compareBy(BasicComparator.SONG))
|
||||
compareByDynamic(ascending) { it.durationMs }, compareBy(BasicComparator.SONG))
|
||||
|
||||
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
|
||||
MultiComparator(
|
||||
compareByDynamic(ascending) { it.durationSecs },
|
||||
compareBy(BasicComparator.ALBUM))
|
||||
compareByDynamic(ascending) { it.durationMs }, compareBy(BasicComparator.ALBUM))
|
||||
|
||||
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
|
||||
MultiComparator(
|
||||
compareByDynamic(ascending) { it.durationSecs },
|
||||
compareByDynamic(ascending) { it.durationMs },
|
||||
compareBy(BasicComparator.ARTIST))
|
||||
|
||||
override fun getGenreComparator(ascending: Boolean): Comparator<Genre> =
|
||||
MultiComparator(
|
||||
compareByDynamic(ascending) { it.durationSecs },
|
||||
compareBy(BasicComparator.GENRE))
|
||||
compareByDynamic(ascending) { it.durationMs }, compareBy(BasicComparator.GENRE))
|
||||
}
|
||||
|
||||
/** Sort by the amount of songs. Only applicable to music parents. */
|
||||
|
|
|
@ -47,12 +47,36 @@ fun Int.nonZeroOrNull() = if (this > 0) this else null
|
|||
|
||||
fun Int.inRangeOrNull(range: IntRange) = if (range.contains(this)) this else null
|
||||
|
||||
fun Long.msToDs() = floorDiv(100)
|
||||
|
||||
fun Long.msToSecs() = floorDiv(1000)
|
||||
|
||||
fun Long.dsToMs() = times(100)
|
||||
|
||||
fun Long.dsToSecs() = floorDiv(10)
|
||||
|
||||
fun Long.secsToMs() = times(1000)
|
||||
|
||||
/**
|
||||
* Convert a [Long] of milliseconds into a string duration.
|
||||
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
|
||||
* will be returned if the second value is 0.
|
||||
*/
|
||||
fun Long.formatDurationMs(isElapsed: Boolean) = msToSecs().formatDurationSecs(isElapsed)
|
||||
|
||||
/**
|
||||
* Convert a [Long] of deci-seconds into a string duration.
|
||||
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
|
||||
* will be returned if the second value is 0.
|
||||
*/
|
||||
fun Long.formatDurationDs(isElapsed: Boolean) = dsToSecs().formatDurationSecs(isElapsed)
|
||||
|
||||
/**
|
||||
* Convert a [Long] of seconds into a string duration.
|
||||
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
|
||||
* will be returned if the second value is 0.
|
||||
*/
|
||||
fun Long.formatDuration(isElapsed: Boolean): String {
|
||||
fun Long.formatDurationSecs(isElapsed: Boolean): String {
|
||||
if (!isElapsed && this == 0L) {
|
||||
logD("Non-elapsed duration is zero, using --:--")
|
||||
return "--:--"
|
||||
|
|
Loading…
Reference in a new issue