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:
parent
059652d2f1
commit
28feebcec3
28 changed files with 242 additions and 93 deletions
|
@ -92,10 +92,14 @@ object IntegerTable {
|
||||||
const val SORT_BY_ALBUM = 0xA10E
|
const val SORT_BY_ALBUM = 0xA10E
|
||||||
/** Sort.ByYear */
|
/** Sort.ByYear */
|
||||||
const val SORT_BY_YEAR = 0xA10F
|
const val SORT_BY_YEAR = 0xA10F
|
||||||
|
/** Sort.ByDuration */
|
||||||
|
const val SORT_BY_DURATION = 0xA114
|
||||||
|
/** Sort.ByCount */
|
||||||
|
const val SORT_BY_COUNT = 0xA115
|
||||||
/** Sort.ByDisc */
|
/** Sort.ByDisc */
|
||||||
const val SORT_BY_DISC = 0xA114
|
const val SORT_BY_DISC = 0xA116
|
||||||
/** Sort.ByTrack */
|
/** Sort.ByTrack */
|
||||||
const val SORT_BY_TRACK = 0xA115
|
const val SORT_BY_TRACK = 0xA117
|
||||||
|
|
||||||
/** ReplayGainMode.Off */
|
/** ReplayGainMode.Off */
|
||||||
const val REPLAY_GAIN_MODE_OFF = 0xA110
|
const val REPLAY_GAIN_MODE_OFF = 0xA110
|
||||||
|
|
|
@ -86,7 +86,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
if (action == Intent.ACTION_VIEW && !isConsumed) {
|
if (action == Intent.ACTION_VIEW && !isConsumed) {
|
||||||
// Mark the intent as used so this does not fire again
|
// Mark the intent as used so this does not fire again
|
||||||
intent.putExtra(KEY_INTENT_USED, true)
|
intent.putExtra(KEY_INTENT_USED, true)
|
||||||
intent.data?.let { fileUri -> playbackModel.playWithUri(fileUri, this) }
|
intent.data?.let { fileUri -> playbackModel.play(fileUri, this) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,7 +89,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
||||||
|
|
||||||
override fun onItemClick(item: Item) {
|
override fun onItemClick(item: Item) {
|
||||||
if (item is Song) {
|
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() {
|
override fun onPlayParent() {
|
||||||
playbackModel.playAlbum(unlikelyToBeNull(detailModel.currentAlbum.value), false)
|
playbackModel.play(unlikelyToBeNull(detailModel.currentAlbum.value), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShuffleParent() {
|
override fun onShuffleParent() {
|
||||||
playbackModel.playAlbum(unlikelyToBeNull(detailModel.currentAlbum.value), true)
|
playbackModel.play(unlikelyToBeNull(detailModel.currentAlbum.value), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShowSortMenu(anchor: View) {
|
override fun onShowSortMenu(anchor: View) {
|
||||||
|
|
|
@ -74,7 +74,7 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
||||||
|
|
||||||
override fun onItemClick(item: Item) {
|
override fun onItemClick(item: Item) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> playbackModel.playSong(item, PlaybackMode.IN_ARTIST)
|
is Song -> playbackModel.play(item, PlaybackMode.IN_ARTIST)
|
||||||
is Album -> navModel.exploreNavigateTo(item)
|
is Album -> navModel.exploreNavigateTo(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,11 +84,11 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayParent() {
|
override fun onPlayParent() {
|
||||||
playbackModel.playArtist(unlikelyToBeNull(detailModel.currentArtist.value), false)
|
playbackModel.play(unlikelyToBeNull(detailModel.currentArtist.value), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShuffleParent() {
|
override fun onShuffleParent() {
|
||||||
playbackModel.playArtist(unlikelyToBeNull(detailModel.currentArtist.value), true)
|
playbackModel.play(unlikelyToBeNull(detailModel.currentArtist.value), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShowSortMenu(anchor: View) {
|
override fun onShowSortMenu(anchor: View) {
|
||||||
|
|
|
@ -71,7 +71,7 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
||||||
|
|
||||||
override fun onItemClick(item: Item) {
|
override fun onItemClick(item: Item) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> playbackModel.playSong(item, PlaybackMode.IN_GENRE)
|
is Song -> playbackModel.play(item, PlaybackMode.IN_GENRE)
|
||||||
is Album ->
|
is Album ->
|
||||||
findNavController()
|
findNavController()
|
||||||
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.id))
|
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.id))
|
||||||
|
@ -83,11 +83,11 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayParent() {
|
override fun onPlayParent() {
|
||||||
playbackModel.playGenre(unlikelyToBeNull(detailModel.currentGenre.value), false)
|
playbackModel.play(unlikelyToBeNull(detailModel.currentGenre.value), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShuffleParent() {
|
override fun onShuffleParent() {
|
||||||
playbackModel.playGenre(unlikelyToBeNull(detailModel.currentGenre.value), true)
|
playbackModel.play(unlikelyToBeNull(detailModel.currentGenre.value), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShowSortMenu(anchor: View) {
|
override fun onShowSortMenu(anchor: View) {
|
||||||
|
|
|
@ -137,7 +137,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
|
||||||
R.string.fmt_three,
|
R.string.fmt_three,
|
||||||
item.year?.toString() ?: context.getString(R.string.def_date),
|
item.year?.toString() ?: context.getString(R.string.def_date),
|
||||||
context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size),
|
context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size),
|
||||||
item.totalDuration)
|
item.durationSecs.formatDuration(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
|
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.artist.rawName == newItem.artist.rawName &&
|
||||||
oldItem.year == newItem.year &&
|
oldItem.year == newItem.year &&
|
||||||
oldItem.songs.size == newItem.songs.size &&
|
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.songName.textSafe = item.resolveName(binding.context)
|
||||||
binding.songDuration.textSafe = item.seconds.formatDuration(false)
|
binding.songDuration.textSafe = item.durationSecs.formatDuration(false)
|
||||||
|
|
||||||
binding.root.apply {
|
binding.root.apply {
|
||||||
setOnClickListener { listener.onItemClick(item) }
|
setOnClickListener { listener.onItemClick(item) }
|
||||||
|
@ -245,7 +245,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
|
||||||
val DIFFER =
|
val DIFFER =
|
||||||
object : SimpleItemCallback<Song>() {
|
object : SimpleItemCallback<Song>() {
|
||||||
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
|
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
|
||||||
oldItem.rawName == newItem.rawName && oldItem.duration == newItem.duration
|
oldItem.rawName == newItem.rawName && oldItem.durationMs == newItem.durationMs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.oxycblt.auxio.ui.MenuItemListener
|
||||||
import org.oxycblt.auxio.ui.SimpleItemCallback
|
import org.oxycblt.auxio.ui.SimpleItemCallback
|
||||||
import org.oxycblt.auxio.ui.SongViewHolder
|
import org.oxycblt.auxio.ui.SongViewHolder
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
import org.oxycblt.auxio.util.getPluralSafe
|
import org.oxycblt.auxio.util.getPluralSafe
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
import org.oxycblt.auxio.util.textSafe
|
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.detailName.textSafe = item.resolveName(binding.context)
|
||||||
binding.detailSubhead.textSafe =
|
binding.detailSubhead.textSafe =
|
||||||
binding.context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size)
|
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.detailPlayButton.setOnClickListener { listener.onPlayParent() }
|
||||||
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
|
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) =
|
override fun areItemsTheSame(oldItem: Genre, newItem: Genre) =
|
||||||
oldItem.rawName == newItem.rawName &&
|
oldItem.rawName == newItem.rawName &&
|
||||||
oldItem.songs.size == newItem.songs.size &&
|
oldItem.songs.size == newItem.songs.size &&
|
||||||
oldItem.totalDuration == newItem.totalDuration
|
oldItem.durationSecs == newItem.durationSecs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,8 +58,6 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*
|
*
|
||||||
* TODO: Make tabs invisible when there is only one
|
* TODO: Make tabs invisible when there is only one
|
||||||
*
|
|
||||||
* TODO: Add duration and song count sorts
|
|
||||||
*/
|
*/
|
||||||
class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuItemClickListener {
|
class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuItemClickListener {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
@ -79,6 +77,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
|
|
||||||
binding.homePager.apply {
|
binding.homePager.apply {
|
||||||
adapter = HomePagerAdapter()
|
adapter = HomePagerAdapter()
|
||||||
|
|
||||||
// We know that there will only be a fixed amount of tabs, so we manually set this
|
// 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
|
// limit to that. This also prevents the appbar lift state from being confused during
|
||||||
// page transitions.
|
// page transitions.
|
||||||
|
@ -138,9 +137,8 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
.getSortForDisplay(unlikelyToBeNull(homeModel.currentTab.value))
|
.getSortForDisplay(unlikelyToBeNull(homeModel.currentTab.value))
|
||||||
.ascending(item.isChecked)))
|
.ascending(item.isChecked)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sorting option was selected, mark it as selected and update the mode
|
|
||||||
else -> {
|
else -> {
|
||||||
|
// Sorting option was selected, mark it as selected and update the mode
|
||||||
item.isChecked = true
|
item.isChecked = true
|
||||||
homeModel.updateCurrentSort(
|
homeModel.updateCurrentSort(
|
||||||
unlikelyToBeNull(
|
unlikelyToBeNull(
|
||||||
|
@ -175,7 +173,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
when (tab) {
|
when (tab) {
|
||||||
DisplayMode.SHOW_SONGS -> {
|
DisplayMode.SHOW_SONGS -> {
|
||||||
updateSortMenu(sortItem, tab)
|
updateSortMenu(sortItem, tab) { id -> id != R.id.option_sort_count }
|
||||||
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_song_list
|
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_song_list
|
||||||
}
|
}
|
||||||
DisplayMode.SHOW_ALBUMS -> {
|
DisplayMode.SHOW_ALBUMS -> {
|
||||||
|
@ -183,11 +181,21 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_album_list
|
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_album_list
|
||||||
}
|
}
|
||||||
DisplayMode.SHOW_ARTISTS -> {
|
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
|
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_artist_list
|
||||||
}
|
}
|
||||||
DisplayMode.SHOW_GENRES -> {
|
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
|
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_genre_list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -196,7 +204,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
private fun updateSortMenu(
|
private fun updateSortMenu(
|
||||||
item: MenuItem,
|
item: MenuItem,
|
||||||
displayMode: DisplayMode,
|
displayMode: DisplayMode,
|
||||||
isVisible: (Int) -> Boolean = { true }
|
isVisible: (Int) -> Boolean
|
||||||
) {
|
) {
|
||||||
val toHighlight = homeModel.getSortForDisplay(displayMode)
|
val toHighlight = homeModel.getSortForDisplay(displayMode)
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,9 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
|
||||||
_shouldRecreateTabs.value = false
|
_shouldRecreateTabs.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the specific sort for the given [DisplayMode].
|
||||||
|
*/
|
||||||
fun getSortForDisplay(displayMode: DisplayMode): Sort {
|
fun getSortForDisplay(displayMode: DisplayMode): Sort {
|
||||||
return when (displayMode) {
|
return when (displayMode) {
|
||||||
DisplayMode.SHOW_SONGS -> settingsManager.libSongSort
|
DisplayMode.SHOW_SONGS -> settingsManager.libSongSort
|
||||||
|
|
|
@ -30,6 +30,7 @@ import org.oxycblt.auxio.ui.MonoAdapter
|
||||||
import org.oxycblt.auxio.ui.PrimitiveBackingData
|
import org.oxycblt.auxio.ui.PrimitiveBackingData
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -62,6 +63,12 @@ class AlbumListFragment : HomeListFragment<Album>() {
|
||||||
// Year -> Use Full Year
|
// Year -> Use Full Year
|
||||||
is Sort.ByYear -> album.year?.toString()
|
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
|
// Unsupported sort, error gracefully
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,11 +23,14 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.ui.ArtistViewHolder
|
import org.oxycblt.auxio.ui.ArtistViewHolder
|
||||||
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.ui.Item
|
import org.oxycblt.auxio.ui.Item
|
||||||
import org.oxycblt.auxio.ui.MenuItemListener
|
import org.oxycblt.auxio.ui.MenuItemListener
|
||||||
import org.oxycblt.auxio.ui.MonoAdapter
|
import org.oxycblt.auxio.ui.MonoAdapter
|
||||||
import org.oxycblt.auxio.ui.PrimitiveBackingData
|
import org.oxycblt.auxio.ui.PrimitiveBackingData
|
||||||
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,8 +49,24 @@ class ArtistListFragment : HomeListFragment<Artist>() {
|
||||||
homeModel.artists.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) }
|
homeModel.artists.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPopup(pos: Int) =
|
override fun getPopup(pos: Int): String? {
|
||||||
unlikelyToBeNull(homeModel.artists.value)[pos].sortName?.run { first().uppercase() }
|
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) {
|
override fun onItemClick(item: Item) {
|
||||||
check(item is Music)
|
check(item is Music)
|
||||||
|
|
|
@ -22,12 +22,15 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.ui.GenreViewHolder
|
import org.oxycblt.auxio.ui.GenreViewHolder
|
||||||
import org.oxycblt.auxio.ui.Item
|
import org.oxycblt.auxio.ui.Item
|
||||||
import org.oxycblt.auxio.ui.MenuItemListener
|
import org.oxycblt.auxio.ui.MenuItemListener
|
||||||
import org.oxycblt.auxio.ui.MonoAdapter
|
import org.oxycblt.auxio.ui.MonoAdapter
|
||||||
import org.oxycblt.auxio.ui.PrimitiveBackingData
|
import org.oxycblt.auxio.ui.PrimitiveBackingData
|
||||||
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,8 +49,24 @@ class GenreListFragment : HomeListFragment<Genre>() {
|
||||||
homeModel.genres.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) }
|
homeModel.genres.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPopup(pos: Int) =
|
override fun getPopup(pos: Int): String? {
|
||||||
unlikelyToBeNull(homeModel.genres.value)[pos].sortName?.run { first().uppercase() }
|
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) {
|
override fun onItemClick(item: Item) {
|
||||||
check(item is Music)
|
check(item is Music)
|
||||||
|
|
|
@ -29,6 +29,7 @@ import org.oxycblt.auxio.ui.PrimitiveBackingData
|
||||||
import org.oxycblt.auxio.ui.SongViewHolder
|
import org.oxycblt.auxio.ui.SongViewHolder
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,15 +67,17 @@ class SongListFragment : HomeListFragment<Song>() {
|
||||||
// Year -> Use Full Year
|
// Year -> Use Full Year
|
||||||
is Sort.ByYear -> song.album.year?.toString()
|
is Sort.ByYear -> song.album.year?.toString()
|
||||||
|
|
||||||
// Unreachable state
|
// Duration -> Use formatted duration
|
||||||
is Sort.ByDisc,
|
is Sort.ByDuration -> song.durationSecs.formatDuration(false)
|
||||||
is Sort.ByTrack -> null
|
|
||||||
|
// Unsupported sort, error gracefully
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: Item) {
|
override fun onItemClick(item: Item) {
|
||||||
check(item is Song)
|
check(item is Song)
|
||||||
playbackModel.playSong(item)
|
playbackModel.play(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Item, anchor: View) {
|
override fun onOpenMenu(item: Item, anchor: View) {
|
||||||
|
|
|
@ -260,7 +260,7 @@ object Indexer {
|
||||||
it._mediaStoreArtistName to
|
it._mediaStoreArtistName to
|
||||||
it._mediaStoreAlbumArtistName to
|
it._mediaStoreAlbumArtistName to
|
||||||
it.track to
|
it.track to
|
||||||
it.duration
|
it.durationMs
|
||||||
}
|
}
|
||||||
.toMutableList()
|
.toMutableList()
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,6 @@ import android.provider.MediaStore
|
||||||
import androidx.core.text.isDigitsOnly
|
import androidx.core.text.isDigitsOnly
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.ui.Item
|
import org.oxycblt.auxio.ui.Item
|
||||||
import org.oxycblt.auxio.util.formatDuration
|
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
// --- MUSIC MODELS ---
|
// --- MUSIC MODELS ---
|
||||||
|
@ -53,6 +52,10 @@ sealed class Music : Item() {
|
||||||
sealed class MusicParent : Music() {
|
sealed class MusicParent : Music() {
|
||||||
/** The songs that this parent owns. */
|
/** The songs that this parent owns. */
|
||||||
abstract val songs: List<Song>
|
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. */
|
/** The data object for a song. */
|
||||||
|
@ -61,7 +64,7 @@ data class Song(
|
||||||
/** The file name of this song, excluding the full path. */
|
/** The file name of this song, excluding the full path. */
|
||||||
val fileName: String,
|
val fileName: String,
|
||||||
/** The total duration of this song, in millis. */
|
/** 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. */
|
/** The track number of this song, null if there isn't any. */
|
||||||
val track: Int?,
|
val track: Int?,
|
||||||
/** The disc number of this song, null if there isn't any. */
|
/** 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.rawName.hashCode()
|
||||||
result = 31 * result + album.artist.rawName.hashCode()
|
result = 31 * result + album.artist.rawName.hashCode()
|
||||||
result = 31 * result + (track ?: 0)
|
result = 31 * result + (track ?: 0)
|
||||||
result = 31 * result + duration.hashCode()
|
result = 31 * result + durationMs.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,8 +102,8 @@ data class Song(
|
||||||
get() =
|
get() =
|
||||||
ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, _mediaStoreId)
|
ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, _mediaStoreId)
|
||||||
/** The duration of this song, in seconds (rounded down) */
|
/** The duration of this song, in seconds (rounded down) */
|
||||||
val seconds: Long
|
val durationSecs: Long
|
||||||
get() = duration / 1000
|
get() = durationMs / 1000
|
||||||
|
|
||||||
private var _album: Album? = null
|
private var _album: Album? = null
|
||||||
/** The album of this song. */
|
/** The album of this song. */
|
||||||
|
@ -190,10 +193,6 @@ data class Album(
|
||||||
|
|
||||||
override fun resolveName(context: Context) = rawName
|
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
|
private var _artist: Artist? = null
|
||||||
/** The parent artist of this album. */
|
/** The parent artist of this album. */
|
||||||
val artist: Artist
|
val artist: Artist
|
||||||
|
@ -256,10 +255,6 @@ data class Genre(override val rawName: String?, override val songs: List<Song>)
|
||||||
|
|
||||||
override fun resolveName(context: Context) =
|
override fun resolveName(context: Context) =
|
||||||
rawName?.genreNameCompat ?: context.getString(R.string.def_genre)
|
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -34,6 +34,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.hardRestart
|
import org.oxycblt.auxio.util.hardRestart
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
import org.oxycblt.auxio.util.logW
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -144,7 +145,7 @@ class ExcludedDialog :
|
||||||
return getRootPath() + "/" + typeAndPath.last()
|
return getRootPath() + "/" + typeAndPath.last()
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Unsupported volume ${typeAndPath[0]}")
|
logW("Unsupported volume ${typeAndPath[0]}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -78,11 +78,11 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.playbackSkipPrev?.setOnClickListener { playbackModel.skipPrev() }
|
binding.playbackSkipPrev?.setOnClickListener { playbackModel.prev() }
|
||||||
|
|
||||||
binding.playbackPlayPause.setOnClickListener { playbackModel.invertPlaying() }
|
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
|
// Deliberately override the progress bar color [in a Lollipop-friendly way] so that
|
||||||
// we use colorSecondary instead of colorSurfaceVariant. This is because
|
// we use colorSecondary instead of colorSurfaceVariant. This is because
|
||||||
|
@ -107,7 +107,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
|
||||||
binding.playbackCover.bindAlbumCover(song)
|
binding.playbackCover.bindAlbumCover(song)
|
||||||
binding.playbackSong.textSafe = song.resolveName(context)
|
binding.playbackSong.textSafe = song.resolveName(context)
|
||||||
binding.playbackInfo.textSafe = song.resolveIndividualArtistName(context)
|
binding.playbackInfo.textSafe = song.resolveIndividualArtistName(context)
|
||||||
binding.playbackProgressBar.max = song.seconds.toInt()
|
binding.playbackProgressBar.max = song.durationSecs.toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -108,7 +108,7 @@ class PlaybackPanelFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.playbackRepeat.setOnClickListener { playbackModel.incrementRepeatMode() }
|
binding.playbackRepeat.setOnClickListener { playbackModel.incrementRepeatMode() }
|
||||||
binding.playbackSkipPrev.setOnClickListener { playbackModel.skipPrev() }
|
binding.playbackSkipPrev.setOnClickListener { playbackModel.prev() }
|
||||||
|
|
||||||
binding.playbackPlayPause.apply {
|
binding.playbackPlayPause.apply {
|
||||||
// Abuse the play/pause FAB (see style definition for more info)
|
// Abuse the play/pause FAB (see style definition for more info)
|
||||||
|
@ -116,7 +116,7 @@ class PlaybackPanelFragment :
|
||||||
setOnClickListener { playbackModel.invertPlaying() }
|
setOnClickListener { playbackModel.invertPlaying() }
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.playbackSkipNext.setOnClickListener { playbackModel.skipNext() }
|
binding.playbackSkipNext.setOnClickListener { playbackModel.next() }
|
||||||
binding.playbackShuffle.setOnClickListener { playbackModel.invertShuffled() }
|
binding.playbackShuffle.setOnClickListener { playbackModel.invertShuffled() }
|
||||||
|
|
||||||
binding.playbackSeekBar.apply {}
|
binding.playbackSeekBar.apply {}
|
||||||
|
@ -162,7 +162,7 @@ class PlaybackPanelFragment :
|
||||||
|
|
||||||
override fun onStopTrackingTouch(slider: Slider) {
|
override fun onStopTrackingTouch(slider: Slider) {
|
||||||
requireBinding().playbackPosition.isActivated = false
|
requireBinding().playbackPosition.isActivated = false
|
||||||
playbackModel.setPosition(slider.value.toLong())
|
playbackModel.seekTo(slider.value.toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
||||||
|
@ -182,7 +182,7 @@ class PlaybackPanelFragment :
|
||||||
binding.playbackAlbum.textSafe = song.album.resolveName(context)
|
binding.playbackAlbum.textSafe = song.album.resolveName(context)
|
||||||
|
|
||||||
// Normally if a song had a duration
|
// Normally if a song had a duration
|
||||||
val seconds = song.seconds
|
val seconds = song.durationSecs
|
||||||
binding.playbackDuration.textSafe = seconds.formatDuration(false)
|
binding.playbackDuration.textSafe = seconds.formatDuration(false)
|
||||||
binding.playbackSeekBar.apply {
|
binding.playbackSeekBar.apply {
|
||||||
isEnabled = seconds > 0L
|
isEnabled = seconds > 0L
|
||||||
|
|
|
@ -99,7 +99,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
* Play a [song] with the [mode] specified. [mode] will default to the preferred song playback
|
* Play a [song] with the [mode] specified. [mode] will default to the preferred song playback
|
||||||
* mode of the user if not specified.
|
* 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)
|
playbackManager.play(song, mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,10 +107,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
* Play an [album].
|
* Play an [album].
|
||||||
* @param shuffled Whether to shuffle the new queue
|
* @param shuffled Whether to shuffle the new queue
|
||||||
*/
|
*/
|
||||||
fun playAlbum(album: Album, shuffled: Boolean) {
|
fun play(album: Album, shuffled: Boolean) {
|
||||||
if (album.songs.isEmpty()) {
|
if (album.songs.isEmpty()) {
|
||||||
logE("Album is empty, Not playing")
|
logE("Album is empty, Not playing")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,10 +120,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
* Play an [artist].
|
* Play an [artist].
|
||||||
* @param shuffled Whether to shuffle the new queue
|
* @param shuffled Whether to shuffle the new queue
|
||||||
*/
|
*/
|
||||||
fun playArtist(artist: Artist, shuffled: Boolean) {
|
fun play(artist: Artist, shuffled: Boolean) {
|
||||||
if (artist.songs.isEmpty()) {
|
if (artist.songs.isEmpty()) {
|
||||||
logE("Artist is empty, Not playing")
|
logE("Artist is empty, Not playing")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,10 +133,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
* Play a [genre].
|
* Play a [genre].
|
||||||
* @param shuffled Whether to shuffle the new queue
|
* @param shuffled Whether to shuffle the new queue
|
||||||
*/
|
*/
|
||||||
fun playGenre(genre: Genre, shuffled: Boolean) {
|
fun play(genre: Genre, shuffled: Boolean) {
|
||||||
if (genre.songs.isEmpty()) {
|
if (genre.songs.isEmpty()) {
|
||||||
logE("Genre is empty, Not playing")
|
logE("Genre is empty, Not playing")
|
||||||
|
|
||||||
return
|
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.
|
* 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
|
// Check if everything is already running to run the URI play
|
||||||
if (playbackManager.isInitialized && musicStore.library != null) {
|
if (playbackManager.isInitialized && musicStore.library != null) {
|
||||||
playWithUriInternal(uri, context)
|
playUriImpl(uri, context)
|
||||||
} else {
|
} else {
|
||||||
logD("Cant play this URI right now, waiting")
|
logD("Cant play this URI right now, waiting")
|
||||||
|
|
||||||
intentUri = uri
|
intentUri = uri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Play with a file URI. This is called after [playWithUri] once its deemed safe to do so. */
|
/** Play with a file URI. This is called after [play] once its deemed safe to do so. */
|
||||||
private fun playWithUriInternal(uri: Uri, context: Context) {
|
private fun playUriImpl(uri: Uri, context: Context) {
|
||||||
logD("Playing with uri $uri")
|
logD("Playing with uri $uri")
|
||||||
val library = musicStore.library ?: return
|
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 */
|
/** Shuffle all songs */
|
||||||
|
@ -174,19 +170,19 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
// --- POSITION FUNCTIONS ---
|
// --- POSITION FUNCTIONS ---
|
||||||
|
|
||||||
/** Update the position and push it to [PlaybackStateManager] */
|
/** Update the position and push it to [PlaybackStateManager] */
|
||||||
fun setPosition(progress: Long) {
|
fun seekTo(positionSecs: Long) {
|
||||||
playbackManager.seekTo(progress * 1000)
|
playbackManager.seekTo(positionSecs * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- QUEUE FUNCTIONS ---
|
// --- QUEUE FUNCTIONS ---
|
||||||
|
|
||||||
/** Skip to the next song. */
|
/** Skip to the next song. */
|
||||||
fun skipNext() {
|
fun next() {
|
||||||
playbackManager.next()
|
playbackManager.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Skip to the previous song. */
|
/** Skip to the previous song. */
|
||||||
fun skipPrev() {
|
fun prev() {
|
||||||
playbackManager.prev()
|
playbackManager.prev()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,14 +267,14 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore playback on startup. This can do one of two things:
|
* 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.
|
* - Restore the last playback state if there is no active file intent.
|
||||||
*/
|
*/
|
||||||
fun setupPlayback(context: Context) {
|
fun setupPlayback(context: Context) {
|
||||||
val intentUri = intentUri
|
val intentUri = intentUri
|
||||||
|
|
||||||
if (intentUri != null) {
|
if (intentUri != null) {
|
||||||
playWithUriInternal(intentUri, context)
|
playUriImpl(intentUri, context)
|
||||||
// Remove the uri after finishing the calls so that this does not fire again.
|
// Remove the uri after finishing the calls so that this does not fire again.
|
||||||
this.intentUri = null
|
this.intentUri = null
|
||||||
} else if (!playbackManager.isInitialized) {
|
} else if (!playbackManager.isInitialized) {
|
||||||
|
|
|
@ -277,7 +277,7 @@ class PlaybackStateManager private constructor() {
|
||||||
*/
|
*/
|
||||||
fun synchronizePosition(positionMs: Long) {
|
fun synchronizePosition(positionMs: Long) {
|
||||||
// Don't accept any bugged positions that are over the duration of the song.
|
// 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) {
|
if (positionMs <= maxDuration) {
|
||||||
this.positionMs = positionMs
|
this.positionMs = positionMs
|
||||||
notifyPositionChanged()
|
notifyPositionChanged()
|
||||||
|
|
|
@ -100,7 +100,7 @@ class MediaSessionComponent(private val context: Context, private val player: Pl
|
||||||
.putText(MediaMetadataCompat.METADATA_KEY_GENRE, song.genre.resolveName(context))
|
.putText(MediaMetadataCompat.METADATA_KEY_GENRE, song.genre.resolveName(context))
|
||||||
.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, song.track?.toLong() ?: 0L)
|
.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, song.track?.toLong() ?: 0L)
|
||||||
.putText(MediaMetadataCompat.METADATA_KEY_DATE, song.album.year?.toString())
|
.putText(MediaMetadataCompat.METADATA_KEY_DATE, song.album.year?.toString())
|
||||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration)
|
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs)
|
||||||
.putText(
|
.putText(
|
||||||
MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI,
|
MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI,
|
||||||
song.album.albumCoverUri.toString())
|
song.album.albumCoverUri.toString())
|
||||||
|
|
|
@ -134,7 +134,7 @@ class SearchFragment :
|
||||||
|
|
||||||
override fun onItemClick(item: Item) {
|
override fun onItemClick(item: Item) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> playbackModel.playSong(item)
|
is Song -> playbackModel.play(item)
|
||||||
is MusicParent -> navModel.exploreNavigateTo(item)
|
is MusicParent -> navModel.exploreNavigateTo(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,8 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
|
||||||
binding.aboutSongCount.textSafe = getString(R.string.fmt_songs_loaded, songs.size)
|
binding.aboutSongCount.textSafe = getString(R.string.fmt_songs_loaded, songs.size)
|
||||||
binding.aboutTotalDuration.textSafe =
|
binding.aboutTotalDuration.textSafe =
|
||||||
getString(
|
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 ->
|
homeModel.albums.observe(viewLifecycleOwner) { albums ->
|
||||||
|
|
|
@ -129,17 +129,17 @@ class ActionMenu(
|
||||||
when (id) {
|
when (id) {
|
||||||
R.id.action_play -> {
|
R.id.action_play -> {
|
||||||
when (data) {
|
when (data) {
|
||||||
is Album -> playbackModel.playAlbum(data, false)
|
is Album -> playbackModel.play(data, false)
|
||||||
is Artist -> playbackModel.playArtist(data, false)
|
is Artist -> playbackModel.play(data, false)
|
||||||
is Genre -> playbackModel.playGenre(data, false)
|
is Genre -> playbackModel.play(data, false)
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.action_shuffle -> {
|
R.id.action_shuffle -> {
|
||||||
when (data) {
|
when (data) {
|
||||||
is Album -> playbackModel.playAlbum(data, true)
|
is Album -> playbackModel.play(data, true)
|
||||||
is Artist -> playbackModel.playArtist(data, true)
|
is Artist -> playbackModel.play(data, true)
|
||||||
is Genre -> playbackModel.playGenre(data, true)
|
is Genre -> playbackModel.play(data, true)
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
* 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
|
* 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_artist -> ByArtist(isAscending)
|
||||||
R.id.option_sort_album -> ByAlbum(isAscending)
|
R.id.option_sort_album -> ByAlbum(isAscending)
|
||||||
R.id.option_sort_year -> ByYear(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_disc -> ByDisc(isAscending)
|
||||||
R.id.option_sort_track -> ByTrack(isAscending)
|
R.id.option_sort_track -> ByTrack(isAscending)
|
||||||
else -> null
|
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> {
|
class NameComparator<T : Music> : Comparator<T> {
|
||||||
override fun compare(a: T, b: T): Int {
|
override fun compare(a: T, b: T): Int {
|
||||||
val aSortName = a.sortName
|
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.
|
* @return A [Sort] instance, null if the data is malformed.
|
||||||
*/
|
*/
|
||||||
fun fromIntCode(value: Int): Sort? {
|
fun fromIntCode(value: Int): Sort? {
|
||||||
val ascending = (value and 1) == 1
|
val isAscending = (value and 1) == 1
|
||||||
|
|
||||||
return when (value.shr(1)) {
|
return when (value.shr(1)) {
|
||||||
IntegerTable.SORT_BY_NAME -> ByName(ascending)
|
IntegerTable.SORT_BY_NAME -> ByName(isAscending)
|
||||||
IntegerTable.SORT_BY_ARTIST -> ByArtist(ascending)
|
IntegerTable.SORT_BY_ARTIST -> ByArtist(isAscending)
|
||||||
IntegerTable.SORT_BY_ALBUM -> ByAlbum(ascending)
|
IntegerTable.SORT_BY_ALBUM -> ByAlbum(isAscending)
|
||||||
IntegerTable.SORT_BY_YEAR -> ByYear(ascending)
|
IntegerTable.SORT_BY_YEAR -> ByYear(isAscending)
|
||||||
IntegerTable.SORT_BY_DISC -> ByDisc(ascending)
|
IntegerTable.SORT_BY_DURATION -> ByDuration(isAscending)
|
||||||
IntegerTable.SORT_BY_TRACK -> ByTrack(ascending)
|
IntegerTable.SORT_BY_COUNT -> ByCount(isAscending)
|
||||||
|
IntegerTable.SORT_BY_DISC -> ByDisc(isAscending)
|
||||||
|
IntegerTable.SORT_BY_TRACK -> ByTrack(isAscending)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,12 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/option_sort_year"
|
android:id="@+id/option_sort_year"
|
||||||
android:title="@string/lbl_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>
|
||||||
<group android:checkableBehavior="all">
|
<group android:checkableBehavior="all">
|
||||||
<item
|
<item
|
||||||
|
|
|
@ -41,6 +41,8 @@
|
||||||
|
|
||||||
<dimen name="slider_thumb_radius">6dp</dimen>
|
<dimen name="slider_thumb_radius">6dp</dimen>
|
||||||
<dimen name="slider_halo_radius">16dp</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_normal">88dp</dimen>
|
||||||
<dimen name="recycler_fab_space_large">128dp</dimen>
|
<dimen name="recycler_fab_space_large">128dp</dimen>
|
||||||
|
|
|
@ -24,6 +24,8 @@
|
||||||
<string name="lbl_sort_artist">Artist</string>
|
<string name="lbl_sort_artist">Artist</string>
|
||||||
<string name="lbl_sort_album">Album</string>
|
<string name="lbl_sort_album">Album</string>
|
||||||
<string name="lbl_sort_year">Year</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_disc">Disc</string>
|
||||||
<string name="lbl_sort_track">Track</string>
|
<string name="lbl_sort_track">Track</string>
|
||||||
<string name="lbl_sort_asc">Ascending</string>
|
<string name="lbl_sort_asc">Ascending</string>
|
||||||
|
@ -114,7 +116,7 @@
|
||||||
<string name="err_load_failed">Music loading failed</string>
|
<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_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_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>
|
<string name="err_too_small">Auxio does not support this window size</string>
|
||||||
|
|
||||||
<!-- Hint Namespace | EditText Hints -->
|
<!-- Hint Namespace | EditText Hints -->
|
||||||
|
|
Loading…
Reference in a new issue