sort: add duration and count sorts

Add sorting modes for duration and song count.

This was requested previously in the now-closed UI/UX changes
megathread, however I have only gotten to it now.
This commit is contained in:
OxygenCobalt 2022-05-20 18:33:36 -06:00
parent 059652d2f1
commit 28feebcec3
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
28 changed files with 242 additions and 93 deletions

View file

@ -92,10 +92,14 @@ object IntegerTable {
const val SORT_BY_ALBUM = 0xA10E
/** Sort.ByYear */
const val SORT_BY_YEAR = 0xA10F
/** Sort.ByDuration */
const val SORT_BY_DURATION = 0xA114
/** Sort.ByCount */
const val SORT_BY_COUNT = 0xA115
/** Sort.ByDisc */
const val SORT_BY_DISC = 0xA114
const val SORT_BY_DISC = 0xA116
/** Sort.ByTrack */
const val SORT_BY_TRACK = 0xA115
const val SORT_BY_TRACK = 0xA117
/** ReplayGainMode.Off */
const val REPLAY_GAIN_MODE_OFF = 0xA110

View file

@ -86,7 +86,7 @@ class MainActivity : AppCompatActivity() {
if (action == Intent.ACTION_VIEW && !isConsumed) {
// Mark the intent as used so this does not fire again
intent.putExtra(KEY_INTENT_USED, true)
intent.data?.let { fileUri -> playbackModel.playWithUri(fileUri, this) }
intent.data?.let { fileUri -> playbackModel.play(fileUri, this) }
}
}
}

View file

@ -89,7 +89,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
override fun onItemClick(item: Item) {
if (item is Song) {
playbackModel.playSong(item, PlaybackMode.IN_ALBUM)
playbackModel.play(item, PlaybackMode.IN_ALBUM)
}
}
@ -98,11 +98,11 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
}
override fun onPlayParent() {
playbackModel.playAlbum(unlikelyToBeNull(detailModel.currentAlbum.value), false)
playbackModel.play(unlikelyToBeNull(detailModel.currentAlbum.value), false)
}
override fun onShuffleParent() {
playbackModel.playAlbum(unlikelyToBeNull(detailModel.currentAlbum.value), true)
playbackModel.play(unlikelyToBeNull(detailModel.currentAlbum.value), true)
}
override fun onShowSortMenu(anchor: View) {

View file

@ -74,7 +74,7 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
override fun onItemClick(item: Item) {
when (item) {
is Song -> playbackModel.playSong(item, PlaybackMode.IN_ARTIST)
is Song -> playbackModel.play(item, PlaybackMode.IN_ARTIST)
is Album -> navModel.exploreNavigateTo(item)
}
}
@ -84,11 +84,11 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
}
override fun onPlayParent() {
playbackModel.playArtist(unlikelyToBeNull(detailModel.currentArtist.value), false)
playbackModel.play(unlikelyToBeNull(detailModel.currentArtist.value), false)
}
override fun onShuffleParent() {
playbackModel.playArtist(unlikelyToBeNull(detailModel.currentArtist.value), true)
playbackModel.play(unlikelyToBeNull(detailModel.currentArtist.value), true)
}
override fun onShowSortMenu(anchor: View) {

View file

@ -71,7 +71,7 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
override fun onItemClick(item: Item) {
when (item) {
is Song -> playbackModel.playSong(item, PlaybackMode.IN_GENRE)
is Song -> playbackModel.play(item, PlaybackMode.IN_GENRE)
is Album ->
findNavController()
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.id))
@ -83,11 +83,11 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
}
override fun onPlayParent() {
playbackModel.playGenre(unlikelyToBeNull(detailModel.currentGenre.value), false)
playbackModel.play(unlikelyToBeNull(detailModel.currentGenre.value), false)
}
override fun onShuffleParent() {
playbackModel.playGenre(unlikelyToBeNull(detailModel.currentGenre.value), true)
playbackModel.play(unlikelyToBeNull(detailModel.currentGenre.value), true)
}
override fun onShowSortMenu(anchor: View) {

View file

@ -137,7 +137,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
R.string.fmt_three,
item.year?.toString() ?: context.getString(R.string.def_date),
context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size),
item.totalDuration)
item.durationSecs.formatDuration(false))
}
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
@ -161,7 +161,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
oldItem.artist.rawName == newItem.artist.rawName &&
oldItem.year == newItem.year &&
oldItem.songs.size == newItem.songs.size &&
oldItem.totalDuration == newItem.totalDuration
oldItem.durationSecs == newItem.durationSecs
}
}
}
@ -215,7 +215,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
}
binding.songName.textSafe = item.resolveName(binding.context)
binding.songDuration.textSafe = item.seconds.formatDuration(false)
binding.songDuration.textSafe = item.durationSecs.formatDuration(false)
binding.root.apply {
setOnClickListener { listener.onItemClick(item) }
@ -245,7 +245,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
val DIFFER =
object : SimpleItemCallback<Song>() {
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
oldItem.rawName == newItem.rawName && oldItem.duration == newItem.duration
oldItem.rawName == newItem.rawName && oldItem.durationMs == newItem.durationMs
}
}
}

View file

@ -33,6 +33,7 @@ import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.SimpleItemCallback
import org.oxycblt.auxio.ui.SongViewHolder
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.getPluralSafe
import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.textSafe
@ -117,7 +118,7 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
binding.detailName.textSafe = item.resolveName(binding.context)
binding.detailSubhead.textSafe =
binding.context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size)
binding.detailInfo.textSafe = item.totalDuration
binding.detailInfo.textSafe = item.durationSecs.formatDuration(false)
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
}
@ -137,7 +138,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.totalDuration == newItem.totalDuration
oldItem.durationSecs == newItem.durationSecs
}
}
}

View file

@ -58,8 +58,6 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* @author OxygenCobalt
*
* TODO: Make tabs invisible when there is only one
*
* TODO: Add duration and song count sorts
*/
class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuItemClickListener {
private val playbackModel: PlaybackViewModel by activityViewModels()
@ -79,6 +77,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homePager.apply {
adapter = HomePagerAdapter()
// We know that there will only be a fixed amount of tabs, so we manually set this
// limit to that. This also prevents the appbar lift state from being confused during
// page transitions.
@ -138,9 +137,8 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
.getSortForDisplay(unlikelyToBeNull(homeModel.currentTab.value))
.ascending(item.isChecked)))
}
// Sorting option was selected, mark it as selected and update the mode
else -> {
// Sorting option was selected, mark it as selected and update the mode
item.isChecked = true
homeModel.updateCurrentSort(
unlikelyToBeNull(
@ -175,7 +173,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
val binding = requireBinding()
when (tab) {
DisplayMode.SHOW_SONGS -> {
updateSortMenu(sortItem, tab)
updateSortMenu(sortItem, tab) { id -> id != R.id.option_sort_count }
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_song_list
}
DisplayMode.SHOW_ALBUMS -> {
@ -183,11 +181,21 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_album_list
}
DisplayMode.SHOW_ARTISTS -> {
updateSortMenu(sortItem, tab) { id -> id == R.id.option_sort_asc }
updateSortMenu(sortItem, tab) { id ->
id == R.id.option_sort_asc ||
id == R.id.option_sort_name ||
id == R.id.option_sort_count ||
id == R.id.option_sort_duration
}
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_artist_list
}
DisplayMode.SHOW_GENRES -> {
updateSortMenu(sortItem, tab) { id -> id == R.id.option_sort_asc }
updateSortMenu(sortItem, tab) { id ->
id == R.id.option_sort_asc ||
id == R.id.option_sort_name ||
id == R.id.option_sort_count ||
id == R.id.option_sort_duration
}
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_genre_list
}
}
@ -196,7 +204,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
private fun updateSortMenu(
item: MenuItem,
displayMode: DisplayMode,
isVisible: (Int) -> Boolean = { true }
isVisible: (Int) -> Boolean
) {
val toHighlight = homeModel.getSortForDisplay(displayMode)

View file

@ -91,6 +91,9 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
_shouldRecreateTabs.value = false
}
/**
* Get the specific sort for the given [DisplayMode].
*/
fun getSortForDisplay(displayMode: DisplayMode): Sort {
return when (displayMode) {
DisplayMode.SHOW_SONGS -> settingsManager.libSongSort

View file

@ -30,6 +30,7 @@ import org.oxycblt.auxio.ui.MonoAdapter
import org.oxycblt.auxio.ui.PrimitiveBackingData
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@ -62,6 +63,12 @@ class AlbumListFragment : HomeListFragment<Album>() {
// Year -> Use Full Year
is Sort.ByYear -> album.year?.toString()
// Duration -> Use formatted duration
is Sort.ByDuration -> album.durationSecs.formatDuration(false)
// Count -> Use song count
is Sort.ByCount -> album.songs.size.toString()
// Unsupported sort, error gracefully
else -> null
}

View file

@ -23,11 +23,14 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.ui.ArtistViewHolder
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.MonoAdapter
import org.oxycblt.auxio.ui.PrimitiveBackingData
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@ -46,8 +49,24 @@ class ArtistListFragment : HomeListFragment<Artist>() {
homeModel.artists.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) }
}
override fun getPopup(pos: Int) =
unlikelyToBeNull(homeModel.artists.value)[pos].sortName?.run { first().uppercase() }
override fun getPopup(pos: Int): String? {
val artist = unlikelyToBeNull(homeModel.artists.value)[pos]
// Change how we display the popup depending on the mode.
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_ARTISTS)) {
// By Name -> Use Name
is Sort.ByName -> artist.sortName?.run { first().uppercase() }
// Duration -> Use formatted duration
is Sort.ByDuration -> artist.durationSecs.formatDuration(false)
// Count -> Use song count
is Sort.ByCount -> artist.songs.size.toString()
// Unsupported sort, error gracefully
else -> null
}
}
override fun onItemClick(item: Item) {
check(item is Music)

View file

@ -22,12 +22,15 @@ import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.GenreViewHolder
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.MonoAdapter
import org.oxycblt.auxio.ui.PrimitiveBackingData
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@ -46,8 +49,24 @@ class GenreListFragment : HomeListFragment<Genre>() {
homeModel.genres.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) }
}
override fun getPopup(pos: Int) =
unlikelyToBeNull(homeModel.genres.value)[pos].sortName?.run { first().uppercase() }
override fun getPopup(pos: Int): String? {
val genre = unlikelyToBeNull(homeModel.genres.value)[pos]
// Change how we display the popup depending on the mode.
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_GENRES)) {
// By Name -> Use Name
is Sort.ByName -> genre.sortName?.run { first().uppercase() }
// Duration -> Use formatted duration
is Sort.ByDuration -> genre.durationSecs.formatDuration(false)
// Count -> Use song count
is Sort.ByCount -> genre.songs.size.toString()
// Unsupported sort, error gracefully
else -> null
}
}
override fun onItemClick(item: Item) {
check(item is Music)

View file

@ -29,6 +29,7 @@ import org.oxycblt.auxio.ui.PrimitiveBackingData
import org.oxycblt.auxio.ui.SongViewHolder
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@ -66,15 +67,17 @@ class SongListFragment : HomeListFragment<Song>() {
// Year -> Use Full Year
is Sort.ByYear -> song.album.year?.toString()
// Unreachable state
is Sort.ByDisc,
is Sort.ByTrack -> null
// Duration -> Use formatted duration
is Sort.ByDuration -> song.durationSecs.formatDuration(false)
// Unsupported sort, error gracefully
else -> null
}
}
override fun onItemClick(item: Item) {
check(item is Song)
playbackModel.playSong(item)
playbackModel.play(item)
}
override fun onOpenMenu(item: Item, anchor: View) {

View file

@ -260,7 +260,7 @@ object Indexer {
it._mediaStoreArtistName to
it._mediaStoreAlbumArtistName to
it.track to
it.duration
it.durationMs
}
.toMutableList()

View file

@ -26,7 +26,6 @@ import android.provider.MediaStore
import androidx.core.text.isDigitsOnly
import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.unlikelyToBeNull
// --- MUSIC MODELS ---
@ -53,6 +52,10 @@ 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. */
@ -61,7 +64,7 @@ data class Song(
/** The file name of this song, excluding the full path. */
val fileName: String,
/** The total duration of this song, in millis. */
val duration: Long,
val durationMs: Long,
/** The track number of this song, null if there isn't any. */
val track: Int?,
/** The disc number of this song, null if there isn't any. */
@ -85,7 +88,7 @@ data class Song(
result = 31 * result + album.rawName.hashCode()
result = 31 * result + album.artist.rawName.hashCode()
result = 31 * result + (track ?: 0)
result = 31 * result + duration.hashCode()
result = 31 * result + durationMs.hashCode()
return result
}
@ -99,8 +102,8 @@ data class Song(
get() =
ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, _mediaStoreId)
/** The duration of this song, in seconds (rounded down) */
val seconds: Long
get() = duration / 1000
val durationSecs: Long
get() = durationMs / 1000
private var _album: Album? = null
/** The album of this song. */
@ -190,10 +193,6 @@ data class Album(
override fun resolveName(context: Context) = rawName
/** The formatted total duration of this album */
val totalDuration: String
get() = songs.sumOf { it.seconds }.formatDuration(false)
private var _artist: Artist? = null
/** The parent artist of this album. */
val artist: Artist
@ -256,10 +255,6 @@ data class Genre(override val rawName: String?, override val songs: List<Song>)
override fun resolveName(context: Context) =
rawName?.genreNameCompat ?: context.getString(R.string.def_genre)
/** The formatted total duration of this genre */
val totalDuration: String
get() = songs.sumOf { it.seconds }.formatDuration(false)
}
/**

View file

@ -34,6 +34,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.hardRestart
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.showToast
/**
@ -144,7 +145,7 @@ class ExcludedDialog :
return getRootPath() + "/" + typeAndPath.last()
}
logD("Unsupported volume ${typeAndPath[0]}")
logW("Unsupported volume ${typeAndPath[0]}")
return null
}

View file

@ -78,11 +78,11 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
}
}
binding.playbackSkipPrev?.setOnClickListener { playbackModel.skipPrev() }
binding.playbackSkipPrev?.setOnClickListener { playbackModel.prev() }
binding.playbackPlayPause.setOnClickListener { playbackModel.invertPlaying() }
binding.playbackSkipNext?.setOnClickListener { playbackModel.skipNext() }
binding.playbackSkipNext?.setOnClickListener { playbackModel.next() }
// Deliberately override the progress bar color [in a Lollipop-friendly way] so that
// we use colorSecondary instead of colorSurfaceVariant. This is because
@ -107,7 +107,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
binding.playbackCover.bindAlbumCover(song)
binding.playbackSong.textSafe = song.resolveName(context)
binding.playbackInfo.textSafe = song.resolveIndividualArtistName(context)
binding.playbackProgressBar.max = song.seconds.toInt()
binding.playbackProgressBar.max = song.durationSecs.toInt()
}
}

View file

@ -108,7 +108,7 @@ class PlaybackPanelFragment :
}
binding.playbackRepeat.setOnClickListener { playbackModel.incrementRepeatMode() }
binding.playbackSkipPrev.setOnClickListener { playbackModel.skipPrev() }
binding.playbackSkipPrev.setOnClickListener { playbackModel.prev() }
binding.playbackPlayPause.apply {
// Abuse the play/pause FAB (see style definition for more info)
@ -116,7 +116,7 @@ class PlaybackPanelFragment :
setOnClickListener { playbackModel.invertPlaying() }
}
binding.playbackSkipNext.setOnClickListener { playbackModel.skipNext() }
binding.playbackSkipNext.setOnClickListener { playbackModel.next() }
binding.playbackShuffle.setOnClickListener { playbackModel.invertShuffled() }
binding.playbackSeekBar.apply {}
@ -162,7 +162,7 @@ class PlaybackPanelFragment :
override fun onStopTrackingTouch(slider: Slider) {
requireBinding().playbackPosition.isActivated = false
playbackModel.setPosition(slider.value.toLong())
playbackModel.seekTo(slider.value.toLong())
}
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
@ -182,7 +182,7 @@ class PlaybackPanelFragment :
binding.playbackAlbum.textSafe = song.album.resolveName(context)
// Normally if a song had a duration
val seconds = song.seconds
val seconds = song.durationSecs
binding.playbackDuration.textSafe = seconds.formatDuration(false)
binding.playbackSeekBar.apply {
isEnabled = seconds > 0L

View file

@ -99,7 +99,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
* Play a [song] with the [mode] specified. [mode] will default to the preferred song playback
* mode of the user if not specified.
*/
fun playSong(song: Song, mode: PlaybackMode = settingsManager.songPlaybackMode) {
fun play(song: Song, mode: PlaybackMode = settingsManager.songPlaybackMode) {
playbackManager.play(song, mode)
}
@ -107,10 +107,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
* Play an [album].
* @param shuffled Whether to shuffle the new queue
*/
fun playAlbum(album: Album, shuffled: Boolean) {
fun play(album: Album, shuffled: Boolean) {
if (album.songs.isEmpty()) {
logE("Album is empty, Not playing")
return
}
@ -121,10 +120,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
* Play an [artist].
* @param shuffled Whether to shuffle the new queue
*/
fun playArtist(artist: Artist, shuffled: Boolean) {
fun play(artist: Artist, shuffled: Boolean) {
if (artist.songs.isEmpty()) {
logE("Artist is empty, Not playing")
return
}
@ -135,10 +133,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
* Play a [genre].
* @param shuffled Whether to shuffle the new queue
*/
fun playGenre(genre: Genre, shuffled: Boolean) {
fun play(genre: Genre, shuffled: Boolean) {
if (genre.songs.isEmpty()) {
logE("Genre is empty, Not playing")
return
}
@ -148,22 +145,21 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
/**
* Play using a file [uri]. This will not play instantly during the initial startup sequence.
*/
fun playWithUri(uri: Uri, context: Context) {
fun play(uri: Uri, context: Context) {
// Check if everything is already running to run the URI play
if (playbackManager.isInitialized && musicStore.library != null) {
playWithUriInternal(uri, context)
playUriImpl(uri, context)
} else {
logD("Cant play this URI right now, waiting")
intentUri = uri
}
}
/** Play with a file URI. This is called after [playWithUri] once its deemed safe to do so. */
private fun playWithUriInternal(uri: Uri, context: Context) {
/** Play with a file URI. This is called after [play] once its deemed safe to do so. */
private fun playUriImpl(uri: Uri, context: Context) {
logD("Playing with uri $uri")
val library = musicStore.library ?: return
library.findSongForUri(uri, context.contentResolver)?.let { song -> playSong(song) }
library.findSongForUri(uri, context.contentResolver)?.let { song -> play(song) }
}
/** Shuffle all songs */
@ -174,19 +170,19 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
// --- POSITION FUNCTIONS ---
/** Update the position and push it to [PlaybackStateManager] */
fun setPosition(progress: Long) {
playbackManager.seekTo(progress * 1000)
fun seekTo(positionSecs: Long) {
playbackManager.seekTo(positionSecs * 1000)
}
// --- QUEUE FUNCTIONS ---
/** Skip to the next song. */
fun skipNext() {
fun next() {
playbackManager.next()
}
/** Skip to the previous song. */
fun skipPrev() {
fun prev() {
playbackManager.prev()
}
@ -271,14 +267,14 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
/**
* Restore playback on startup. This can do one of two things:
* - Play a file intent that was given by MainActivity in [playWithUri]
* - Play a file intent that was given by MainActivity in [play]
* - Restore the last playback state if there is no active file intent.
*/
fun setupPlayback(context: Context) {
val intentUri = intentUri
if (intentUri != null) {
playWithUriInternal(intentUri, context)
playUriImpl(intentUri, context)
// Remove the uri after finishing the calls so that this does not fire again.
this.intentUri = null
} else if (!playbackManager.isInitialized) {

View file

@ -277,7 +277,7 @@ class PlaybackStateManager private constructor() {
*/
fun synchronizePosition(positionMs: Long) {
// Don't accept any bugged positions that are over the duration of the song.
val maxDuration = song?.duration ?: -1
val maxDuration = song?.durationMs ?: -1
if (positionMs <= maxDuration) {
this.positionMs = positionMs
notifyPositionChanged()

View file

@ -100,7 +100,7 @@ class MediaSessionComponent(private val context: Context, private val player: Pl
.putText(MediaMetadataCompat.METADATA_KEY_GENRE, song.genre.resolveName(context))
.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, song.track?.toLong() ?: 0L)
.putText(MediaMetadataCompat.METADATA_KEY_DATE, song.album.year?.toString())
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs)
.putText(
MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI,
song.album.albumCoverUri.toString())

View file

@ -134,7 +134,7 @@ class SearchFragment :
override fun onItemClick(item: Item) {
when (item) {
is Song -> playbackModel.playSong(item)
is Song -> playbackModel.play(item)
is MusicParent -> navModel.exploreNavigateTo(item)
}
}

View file

@ -65,7 +65,8 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
binding.aboutSongCount.textSafe = getString(R.string.fmt_songs_loaded, songs.size)
binding.aboutTotalDuration.textSafe =
getString(
R.string.fmt_total_duration, songs.sumOf { it.seconds }.formatDuration(false))
R.string.fmt_total_duration,
songs.sumOf { it.durationSecs }.formatDuration(false))
}
homeModel.albums.observe(viewLifecycleOwner) { albums ->

View file

@ -129,17 +129,17 @@ class ActionMenu(
when (id) {
R.id.action_play -> {
when (data) {
is Album -> playbackModel.playAlbum(data, false)
is Artist -> playbackModel.playArtist(data, false)
is Genre -> playbackModel.playGenre(data, false)
is Album -> playbackModel.play(data, false)
is Artist -> playbackModel.play(data, false)
is Genre -> playbackModel.play(data, false)
else -> {}
}
}
R.id.action_shuffle -> {
when (data) {
is Album -> playbackModel.playAlbum(data, true)
is Artist -> playbackModel.playArtist(data, true)
is Genre -> playbackModel.playGenre(data, true)
is Album -> playbackModel.play(data, true)
is Artist -> playbackModel.play(data, true)
is Genre -> playbackModel.play(data, true)
else -> {}
}
}

View file

@ -205,6 +205,74 @@ sealed class Sort(open val isAscending: Boolean) {
}
}
/** Sort by the duration of the item. Supports all items. */
class ByDuration(override val isAscending: Boolean) : Sort(isAscending) {
override val sortIntCode: Int
get() = IntegerTable.SORT_BY_DURATION
override val itemId: Int
get() = R.id.option_sort_duration
override fun songsInPlace(songs: MutableList<Song>) {
songs.sortWith(
MultiComparator(
compareByDynamic { it.durationSecs }, compareBy(NameComparator()) { it }))
}
override fun albumsInPlace(albums: MutableList<Album>) {
albums.sortWith(
MultiComparator(
compareByDynamic { it.durationSecs }, compareBy(NameComparator()) { it }))
}
override fun artistsInPlace(artists: MutableList<Artist>) {
artists.sortWith(
MultiComparator(
compareByDynamic { it.durationSecs }, compareBy(NameComparator()) { it }))
}
override fun genresInPlace(genres: MutableList<Genre>) {
genres.sortWith(
MultiComparator(
compareByDynamic { it.durationSecs }, compareBy(NameComparator()) { it }))
}
override fun ascending(newIsAscending: Boolean): Sort {
return ByDuration(newIsAscending)
}
}
/** Sort by the amount of songs. Only applicable to music parents. */
class ByCount(override val isAscending: Boolean) : Sort(isAscending) {
override val sortIntCode: Int
get() = IntegerTable.SORT_BY_COUNT
override val itemId: Int
get() = R.id.option_sort_count
override fun albumsInPlace(albums: MutableList<Album>) {
albums.sortWith(
MultiComparator(
compareByDynamic { it.songs.size }, compareBy(NameComparator()) { it }))
}
override fun artistsInPlace(artists: MutableList<Artist>) {
artists.sortWith(
MultiComparator(
compareByDynamic { it.songs.size }, compareBy(NameComparator()) { it }))
}
override fun genresInPlace(genres: MutableList<Genre>) {
genres.sortWith(
MultiComparator(
compareByDynamic { it.songs.size }, compareBy(NameComparator()) { it }))
}
override fun ascending(newIsAscending: Boolean): Sort {
return ByCount(newIsAscending)
}
}
/**
* Sort by the disc, and then track number of an item. Only supported by [Song]. Do not use this
* in a main sorting view, as it is not assigned to a particular item ID
@ -267,6 +335,8 @@ sealed class Sort(open val isAscending: Boolean) {
R.id.option_sort_artist -> ByArtist(isAscending)
R.id.option_sort_album -> ByAlbum(isAscending)
R.id.option_sort_year -> ByYear(isAscending)
R.id.option_sort_duration -> ByDuration(isAscending)
R.id.option_sort_count -> ByCount(isAscending)
R.id.option_sort_disc -> ByDisc(isAscending)
R.id.option_sort_track -> ByTrack(isAscending)
else -> null
@ -284,6 +354,16 @@ sealed class Sort(open val isAscending: Boolean) {
}
}
protected inline fun <T : Music, K : Comparable<K>> compareByDynamic(
crossinline selector: (T) -> K
): Comparator<T> {
return if (isAscending) {
compareBy(selector)
} else {
compareByDescending(selector)
}
}
class NameComparator<T : Music> : Comparator<T> {
override fun compare(a: T, b: T): Int {
val aSortName = a.sortName
@ -341,15 +421,17 @@ sealed class Sort(open val isAscending: Boolean) {
* @return A [Sort] instance, null if the data is malformed.
*/
fun fromIntCode(value: Int): Sort? {
val ascending = (value and 1) == 1
val isAscending = (value and 1) == 1
return when (value.shr(1)) {
IntegerTable.SORT_BY_NAME -> ByName(ascending)
IntegerTable.SORT_BY_ARTIST -> ByArtist(ascending)
IntegerTable.SORT_BY_ALBUM -> ByAlbum(ascending)
IntegerTable.SORT_BY_YEAR -> ByYear(ascending)
IntegerTable.SORT_BY_DISC -> ByDisc(ascending)
IntegerTable.SORT_BY_TRACK -> ByTrack(ascending)
IntegerTable.SORT_BY_NAME -> ByName(isAscending)
IntegerTable.SORT_BY_ARTIST -> ByArtist(isAscending)
IntegerTable.SORT_BY_ALBUM -> ByAlbum(isAscending)
IntegerTable.SORT_BY_YEAR -> ByYear(isAscending)
IntegerTable.SORT_BY_DURATION -> ByDuration(isAscending)
IntegerTable.SORT_BY_COUNT -> ByCount(isAscending)
IntegerTable.SORT_BY_DISC -> ByDisc(isAscending)
IntegerTable.SORT_BY_TRACK -> ByTrack(isAscending)
else -> null
}
}

View file

@ -27,6 +27,12 @@
<item
android:id="@+id/option_sort_year"
android:title="@string/lbl_sort_year" />
<item
android:id="@+id/option_sort_duration"
android:title="@string/lbl_sort_duration" />
<item
android:id="@+id/option_sort_count"
android:title="@string/lbl_sort_count" />
</group>
<group android:checkableBehavior="all">
<item

View file

@ -41,6 +41,8 @@
<dimen name="slider_thumb_radius">6dp</dimen>
<dimen name="slider_halo_radius">16dp</dimen>
<dimen name="slider_thumb_radius_collapsed">6dp</dimen>
<dimen name="slider_thumb_radius_expanded">10dp</dimen>
<dimen name="recycler_fab_space_normal">88dp</dimen>
<dimen name="recycler_fab_space_large">128dp</dimen>

View file

@ -24,6 +24,8 @@
<string name="lbl_sort_artist">Artist</string>
<string name="lbl_sort_album">Album</string>
<string name="lbl_sort_year">Year</string>
<string name="lbl_sort_duration">Duration</string>
<string name="lbl_sort_count">Song Count</string>
<string name="lbl_sort_disc">Disc</string>
<string name="lbl_sort_track">Track</string>
<string name="lbl_sort_asc">Ascending</string>
@ -114,7 +116,7 @@
<string name="err_load_failed">Music loading failed</string>
<string name="err_no_perms">Auxio needs permission to read your music library</string>
<string name="err_no_app">No app can open this link</string>
<string name="err_bad_dir">This directory is not supported</string>
<string name="err_bad_dir">This folder is not supported</string>
<string name="err_too_small">Auxio does not support this window size</string>
<!-- Hint Namespace | EditText Hints -->