From 28feebcec35a95d3058c5934e5c0b456411a72af Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Fri, 20 May 2022 18:33:36 -0600 Subject: [PATCH] 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. --- .../java/org/oxycblt/auxio/IntegerTable.kt | 8 +- .../java/org/oxycblt/auxio/MainActivity.kt | 2 +- .../auxio/detail/AlbumDetailFragment.kt | 6 +- .../auxio/detail/ArtistDetailFragment.kt | 6 +- .../auxio/detail/GenreDetailFragment.kt | 6 +- .../detail/recycler/AlbumDetailAdapter.kt | 8 +- .../detail/recycler/GenreDetailAdapter.kt | 5 +- .../org/oxycblt/auxio/home/HomeFragment.kt | 24 +++-- .../org/oxycblt/auxio/home/HomeViewModel.kt | 3 + .../auxio/home/list/AlbumListFragment.kt | 7 ++ .../auxio/home/list/ArtistListFragment.kt | 23 ++++- .../auxio/home/list/GenreListFragment.kt | 23 ++++- .../auxio/home/list/SongListFragment.kt | 11 ++- .../java/org/oxycblt/auxio/music/Indexer.kt | 2 +- .../java/org/oxycblt/auxio/music/Music.kt | 21 ++-- .../auxio/music/excluded/ExcludedDialog.kt | 3 +- .../auxio/playback/PlaybackBarFragment.kt | 6 +- .../auxio/playback/PlaybackPanelFragment.kt | 8 +- .../auxio/playback/PlaybackViewModel.kt | 34 +++---- .../playback/state/PlaybackStateManager.kt | 2 +- .../playback/system/MediaSessionComponent.kt | 2 +- .../oxycblt/auxio/search/SearchFragment.kt | 2 +- .../oxycblt/auxio/settings/AboutFragment.kt | 3 +- .../java/org/oxycblt/auxio/ui/ActionMenu.kt | 12 +-- .../main/java/org/oxycblt/auxio/ui/Sort.kt | 96 +++++++++++++++++-- app/src/main/res/menu/menu_home.xml | 6 ++ app/src/main/res/values/dimens.xml | 2 + app/src/main/res/values/strings.xml | 4 +- 28 files changed, 242 insertions(+), 93 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt index 172dcb89f..89b101a6b 100644 --- a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt +++ b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 1fb87fe9f..6e7f8906f 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -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) } } } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index d88a9c10a..764541284 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -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) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 533dffa6e..63a6e96b1 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -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) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 40d9d0bc5..9aa9b8074 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -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) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt index c342790c7..1042780c0 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt @@ -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() { override fun areItemsTheSame(oldItem: Song, newItem: Song) = - oldItem.rawName == newItem.rawName && oldItem.duration == newItem.duration + oldItem.rawName == newItem.rawName && oldItem.durationMs == newItem.durationMs } } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt index 5abd610e0..f9000854d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt @@ -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 } } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index 27ab09d8e..5ce957f90 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -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(), Toolbar.OnMenuItemClickListener { private val playbackModel: PlaybackViewModel by activityViewModels() @@ -79,6 +77,7 @@ class HomeFragment : ViewBindingFragment(), 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(), 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(), 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(), 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(), Toolbar.OnMenuI private fun updateSortMenu( item: MenuItem, displayMode: DisplayMode, - isVisible: (Int) -> Boolean = { true } + isVisible: (Int) -> Boolean ) { val toHighlight = homeModel.getSortForDisplay(displayMode) diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index 102a20093..adffe50dd 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index 42f11cfd7..ca136c5d6 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -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() { // 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 } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt index 4e703076c..48790b9d3 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt @@ -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() { 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) diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt index 28a34211b..cca2ba115 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt @@ -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() { 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) diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 1218c8aa9..443dc3d94 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -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() { // 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) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/Indexer.kt index 203ad5249..954faf033 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Indexer.kt @@ -260,7 +260,7 @@ object Indexer { it._mediaStoreArtistName to it._mediaStoreAlbumArtistName to it.track to - it.duration + it.durationMs } .toMutableList() diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 3ec1dca0d..cee9aa73c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -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 + + /** 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) 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) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt index 4c4076b06..af63b4d16 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt @@ -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 } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt index b0cb9d25b..de58a4cc3 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -78,11 +78,11 @@ class PlaybackBarFragment : ViewBindingFragment() { } } - 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() { 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() } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 0fd2f082b..d49a7f62c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index ea3366d28..f81678d91 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -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) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 03cb52825..82e28cf80 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -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() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt index ffcad28d4..b261391c9 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt @@ -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()) diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index 1b644b54e..226b284e5 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -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) } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt index d50de6a02..bd889b6cf 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt @@ -65,7 +65,8 @@ class AboutFragment : ViewBindingFragment() { 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 -> diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt b/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt index 1ccc025bc..77cd80115 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt @@ -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 -> {} } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt b/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt index 1a49fcf59..cbcbd1f4b 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt @@ -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) { + songs.sortWith( + MultiComparator( + compareByDynamic { it.durationSecs }, compareBy(NameComparator()) { it })) + } + + override fun albumsInPlace(albums: MutableList) { + albums.sortWith( + MultiComparator( + compareByDynamic { it.durationSecs }, compareBy(NameComparator()) { it })) + } + + override fun artistsInPlace(artists: MutableList) { + artists.sortWith( + MultiComparator( + compareByDynamic { it.durationSecs }, compareBy(NameComparator()) { it })) + } + + override fun genresInPlace(genres: MutableList) { + 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) { + albums.sortWith( + MultiComparator( + compareByDynamic { it.songs.size }, compareBy(NameComparator()) { it })) + } + + override fun artistsInPlace(artists: MutableList) { + artists.sortWith( + MultiComparator( + compareByDynamic { it.songs.size }, compareBy(NameComparator()) { it })) + } + + override fun genresInPlace(genres: MutableList) { + 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 > compareByDynamic( + crossinline selector: (T) -> K + ): Comparator { + return if (isAscending) { + compareBy(selector) + } else { + compareByDescending(selector) + } + } + class NameComparator : Comparator { 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 } } diff --git a/app/src/main/res/menu/menu_home.xml b/app/src/main/res/menu/menu_home.xml index 43e693123..690e11511 100644 --- a/app/src/main/res/menu/menu_home.xml +++ b/app/src/main/res/menu/menu_home.xml @@ -27,6 +27,12 @@ + + 6dp 16dp + 6dp + 10dp 88dp 128dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8e7b5afa5..1040d8501 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,6 +24,8 @@ Artist Album Year + Duration + Song Count Disc Track Ascending @@ -114,7 +116,7 @@ Music loading failed Auxio needs permission to read your music library No app can open this link - This directory is not supported + This folder is not supported Auxio does not support this window size