detail: add playlist view
Add a detail view for playlists. This is most equivelent to the genre detail view right now, but will be differentiated eventually.
This commit is contained in:
parent
f3a2d94086
commit
4068c3e009
24 changed files with 628 additions and 56 deletions
|
@ -77,24 +77,26 @@ object IntegerTable {
|
|||
const val MUSIC_MODE_GENRES = 0xA108
|
||||
/** MusicMode.PLAYLISTS */
|
||||
const val MUSIC_MODE_PLAYLISTS = 0xA107
|
||||
/** Sort.ByName */
|
||||
/** Sort.Mode.ByName */
|
||||
const val SORT_BY_NAME = 0xA10C
|
||||
/** Sort.ByArtist */
|
||||
/** Sort.Mode.ByArtist */
|
||||
const val SORT_BY_ARTIST = 0xA10D
|
||||
/** Sort.ByAlbum */
|
||||
/** Sort.Mode.ByAlbum */
|
||||
const val SORT_BY_ALBUM = 0xA10E
|
||||
/** Sort.ByYear */
|
||||
/** Sort.Mode.ByYear */
|
||||
const val SORT_BY_YEAR = 0xA10F
|
||||
/** Sort.ByDuration */
|
||||
/** Sort.Mode.ByDuration */
|
||||
const val SORT_BY_DURATION = 0xA114
|
||||
/** Sort.ByCount */
|
||||
/** Sort.Mode.ByCount */
|
||||
const val SORT_BY_COUNT = 0xA115
|
||||
/** Sort.ByDisc */
|
||||
/** Sort.Mode.ByDisc */
|
||||
const val SORT_BY_DISC = 0xA116
|
||||
/** Sort.ByTrack */
|
||||
/** Sort.Mode.ByTrack */
|
||||
const val SORT_BY_TRACK = 0xA117
|
||||
/** Sort.ByDateAdded */
|
||||
/** Sort.Mode.ByDateAdded */
|
||||
const val SORT_BY_DATE_ADDED = 0xA118
|
||||
/** Sort.Mode.None */
|
||||
const val SORT_BY_NONE = 0xA11F
|
||||
/** ReplayGainMode.Off (No longer used but still reserved) */
|
||||
// const val REPLAY_GAIN_MODE_OFF = 0xA110
|
||||
/** ReplayGainMode.Track */
|
||||
|
|
|
@ -159,8 +159,10 @@ class AlbumDetailFragment :
|
|||
|
||||
override fun onOpenSortMenu(anchor: View) {
|
||||
openMenu(anchor, R.menu.menu_album_sort) {
|
||||
// Select the corresponding sort mode option
|
||||
val sort = detailModel.albumSongSort
|
||||
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
|
||||
// Select the corresponding sort direction option
|
||||
val directionItemId =
|
||||
when (sort.direction) {
|
||||
Sort.Direction.ASCENDING -> R.id.option_sort_asc
|
||||
|
@ -171,8 +173,10 @@ class AlbumDetailFragment :
|
|||
item.isChecked = !item.isChecked
|
||||
detailModel.albumSongSort =
|
||||
when (item.itemId) {
|
||||
// Sort direction options
|
||||
R.id.option_sort_asc -> sort.withDirection(Sort.Direction.ASCENDING)
|
||||
R.id.option_sort_dec -> sort.withDirection(Sort.Direction.DESCENDING)
|
||||
// Any other option is a sort mode
|
||||
else -> sort.withMode(unlikelyToBeNull(Sort.Mode.fromItemId(item.itemId)))
|
||||
}
|
||||
true
|
||||
|
|
|
@ -87,7 +87,7 @@ class ArtistDetailFragment :
|
|||
|
||||
// --- UI SETUP ---
|
||||
binding.detailToolbar.apply {
|
||||
inflateMenu(R.menu.menu_genre_artist_detail)
|
||||
inflateMenu(R.menu.menu_parent_detail)
|
||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
setOnMenuItemClickListener(this@ArtistDetailFragment)
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ class ArtistDetailFragment :
|
|||
// --- VIEWMODEL SETUP ---
|
||||
// DetailViewModel handles most initialization from the navigation argument.
|
||||
detailModel.setArtistUid(args.artistUid)
|
||||
collectImmediately(detailModel.currentArtist, ::updateItem)
|
||||
collectImmediately(detailModel.currentArtist, ::updateArtist)
|
||||
collectImmediately(detailModel.artistList, ::updateList)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
|
@ -171,8 +171,10 @@ class ArtistDetailFragment :
|
|||
|
||||
override fun onOpenSortMenu(anchor: View) {
|
||||
openMenu(anchor, R.menu.menu_artist_sort) {
|
||||
// Select the corresponding sort mode option
|
||||
val sort = detailModel.artistSongSort
|
||||
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
|
||||
// Select the corresponding sort direction option
|
||||
val directionItemId =
|
||||
when (sort.direction) {
|
||||
Sort.Direction.ASCENDING -> R.id.option_sort_asc
|
||||
|
@ -184,8 +186,10 @@ class ArtistDetailFragment :
|
|||
|
||||
detailModel.artistSongSort =
|
||||
when (item.itemId) {
|
||||
// Sort direction options
|
||||
R.id.option_sort_asc -> sort.withDirection(Sort.Direction.ASCENDING)
|
||||
R.id.option_sort_dec -> sort.withDirection(Sort.Direction.DESCENDING)
|
||||
// Any other option is a sort mode
|
||||
else -> sort.withMode(unlikelyToBeNull(Sort.Mode.fromItemId(item.itemId)))
|
||||
}
|
||||
|
||||
|
@ -194,7 +198,7 @@ class ArtistDetailFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateItem(artist: Artist?) {
|
||||
private fun updateArtist(artist: Artist?) {
|
||||
if (artist == null) {
|
||||
// Artist we were showing no longer exists.
|
||||
findNavController().navigateUp()
|
||||
|
|
|
@ -143,6 +143,31 @@ constructor(
|
|||
currentGenre.value?.let { refreshGenreList(it, true) }
|
||||
}
|
||||
|
||||
// --- PLAYLIST ---
|
||||
private val _currentPlaylist = MutableStateFlow<Playlist?>(null)
|
||||
/** The current [Playlist] to display. Null if there is nothing to do. */
|
||||
val currentPlaylist: StateFlow<Playlist?>
|
||||
get() = _currentPlaylist
|
||||
|
||||
private val _playlistList = MutableStateFlow(listOf<Item>())
|
||||
/** The current list data derived from [currentPlaylist] */
|
||||
val playlistList: StateFlow<List<Item>> = _playlistList
|
||||
private val _playlistInstructions = MutableEvent<UpdateInstructions>()
|
||||
/** Instructions for updating [playlistList] in the UI. */
|
||||
val playlistInstructions: Event<UpdateInstructions>
|
||||
get() = _playlistInstructions
|
||||
|
||||
/** The current [Sort] used for [Song]s in [playlistList]. */
|
||||
var playlistSongSort: Sort
|
||||
get() = musicSettings.playlistSongSort
|
||||
set(value) {
|
||||
logD(value)
|
||||
musicSettings.playlistSongSort = value
|
||||
logD(musicSettings.playlistSongSort)
|
||||
// Refresh the playlist list to reflect the new sort.
|
||||
currentPlaylist.value?.let { refreshPlaylistList(it, true) }
|
||||
}
|
||||
|
||||
/**
|
||||
* The [MusicMode] to use when playing a [Song] from the UI, or null to play from the currently
|
||||
* shown item.
|
||||
|
@ -161,6 +186,7 @@ constructor(
|
|||
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
||||
if (!changes.deviceLibrary) return
|
||||
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
||||
val userLibrary = musicRepository.userLibrary ?: return
|
||||
|
||||
// If we are showing any item right now, we will need to refresh it (and any information
|
||||
// related to it) with the new library in order to prevent stale items from showing up
|
||||
|
@ -175,13 +201,13 @@ constructor(
|
|||
val album = currentAlbum.value
|
||||
if (album != null) {
|
||||
_currentAlbum.value = deviceLibrary.findAlbum(album.uid)?.also(::refreshAlbumList)
|
||||
logD("Updated genre to ${currentAlbum.value}")
|
||||
logD("Updated album to ${currentAlbum.value}")
|
||||
}
|
||||
|
||||
val artist = currentArtist.value
|
||||
if (artist != null) {
|
||||
_currentArtist.value = deviceLibrary.findArtist(artist.uid)?.also(::refreshArtistList)
|
||||
logD("Updated genre to ${currentArtist.value}")
|
||||
logD("Updated artist to ${currentArtist.value}")
|
||||
}
|
||||
|
||||
val genre = currentGenre.value
|
||||
|
@ -189,6 +215,12 @@ constructor(
|
|||
_currentGenre.value = deviceLibrary.findGenre(genre.uid)?.also(::refreshGenreList)
|
||||
logD("Updated genre to ${currentGenre.value}")
|
||||
}
|
||||
|
||||
val playlist = currentPlaylist.value
|
||||
if (playlist != null) {
|
||||
_currentPlaylist.value =
|
||||
userLibrary.findPlaylist(playlist.uid)?.also(::refreshPlaylistList)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -254,6 +286,22 @@ constructor(
|
|||
musicRepository.deviceLibrary?.findGenre(uid)?.also(::refreshGenreList)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new [currentPlaylist] from it's [Music.UID]. If the [Music.UID] differs,
|
||||
* [currentPlaylist] and [currentPlaylist] will be updated to align with the new album.
|
||||
*
|
||||
* @param uid The [Music.UID] of the [Playlist] to update [currentPlaylist] to. Must be valid.
|
||||
*/
|
||||
fun setPlaylistUid(uid: Music.UID) {
|
||||
if (_currentPlaylist.value?.uid == uid) {
|
||||
// Nothing to do.
|
||||
return
|
||||
}
|
||||
logD("Opening Playlist [uid: $uid]")
|
||||
_currentPlaylist.value =
|
||||
musicRepository.userLibrary?.findPlaylist(uid)?.also(::refreshPlaylistList)
|
||||
}
|
||||
|
||||
private fun refreshAudioInfo(song: Song) {
|
||||
// Clear any previous job in order to avoid stale data from appearing in the UI.
|
||||
currentSongJob?.cancel()
|
||||
|
@ -267,7 +315,7 @@ constructor(
|
|||
}
|
||||
|
||||
private fun refreshAlbumList(album: Album, replace: Boolean = false) {
|
||||
logD("Refreshing album data")
|
||||
logD("Refreshing album list")
|
||||
val list = mutableListOf<Item>()
|
||||
list.add(SortHeader(R.string.lbl_songs))
|
||||
val instructions =
|
||||
|
@ -299,7 +347,7 @@ constructor(
|
|||
}
|
||||
|
||||
private fun refreshArtistList(artist: Artist, replace: Boolean = false) {
|
||||
logD("Refreshing artist data")
|
||||
logD("Refreshing artist list")
|
||||
val list = mutableListOf<Item>()
|
||||
val albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(artist.albums)
|
||||
|
||||
|
@ -348,7 +396,7 @@ constructor(
|
|||
}
|
||||
|
||||
private fun refreshGenreList(genre: Genre, replace: Boolean = false) {
|
||||
logD("Refreshing genre data")
|
||||
logD("Refreshing genre list")
|
||||
val list = mutableListOf<Item>()
|
||||
// Genre is guaranteed to always have artists and songs.
|
||||
list.add(BasicHeader(R.string.lbl_artists))
|
||||
|
@ -366,6 +414,21 @@ constructor(
|
|||
_genreList.value = list
|
||||
}
|
||||
|
||||
private fun refreshPlaylistList(playlist: Playlist, replace: Boolean = false) {
|
||||
logD("Refreshing playlist list")
|
||||
val list = mutableListOf<Item>()
|
||||
list.add(SortHeader(R.string.lbl_songs))
|
||||
val instructions =
|
||||
if (replace) {
|
||||
UpdateInstructions.Replace(list.size)
|
||||
} else {
|
||||
UpdateInstructions.Diff
|
||||
}
|
||||
list.addAll(playlistSongSort.songs(playlist.songs))
|
||||
_playlistInstructions.put(instructions)
|
||||
_playlistList.value = list
|
||||
}
|
||||
|
||||
/**
|
||||
* A simpler mapping of [ReleaseType] used for grouping and sorting songs.
|
||||
*
|
||||
|
|
|
@ -81,7 +81,7 @@ class GenreDetailFragment :
|
|||
|
||||
// --- UI SETUP ---
|
||||
binding.detailToolbar.apply {
|
||||
inflateMenu(R.menu.menu_genre_artist_detail)
|
||||
inflateMenu(R.menu.menu_parent_detail)
|
||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
setOnMenuItemClickListener(this@GenreDetailFragment)
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ class GenreDetailFragment :
|
|||
// --- VIEWMODEL SETUP ---
|
||||
// DetailViewModel handles most initialization from the navigation argument.
|
||||
detailModel.setGenreUid(args.genreUid)
|
||||
collectImmediately(detailModel.currentGenre, ::updateItem)
|
||||
collectImmediately(detailModel.currentGenre, ::updatePlaylist)
|
||||
collectImmediately(detailModel.genreList, ::updateList)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
|
@ -165,8 +165,10 @@ class GenreDetailFragment :
|
|||
|
||||
override fun onOpenSortMenu(anchor: View) {
|
||||
openMenu(anchor, R.menu.menu_genre_sort) {
|
||||
// Select the corresponding sort mode option
|
||||
val sort = detailModel.genreSongSort
|
||||
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
|
||||
// Select the corresponding sort direction option
|
||||
val directionItemId =
|
||||
when (sort.direction) {
|
||||
Sort.Direction.ASCENDING -> R.id.option_sort_asc
|
||||
|
@ -177,8 +179,10 @@ class GenreDetailFragment :
|
|||
item.isChecked = !item.isChecked
|
||||
detailModel.genreSongSort =
|
||||
when (item.itemId) {
|
||||
// Sort direction options
|
||||
R.id.option_sort_asc -> sort.withDirection(Sort.Direction.ASCENDING)
|
||||
R.id.option_sort_dec -> sort.withDirection(Sort.Direction.DESCENDING)
|
||||
// Any other option is a sort mode
|
||||
else -> sort.withMode(unlikelyToBeNull(Sort.Mode.fromItemId(item.itemId)))
|
||||
}
|
||||
true
|
||||
|
@ -186,7 +190,7 @@ class GenreDetailFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateItem(genre: Genre?) {
|
||||
private fun updatePlaylist(genre: Genre?) {
|
||||
if (genre == null) {
|
||||
// Genre we were showing no longer exists.
|
||||
findNavController().navigateUp()
|
||||
|
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* PlaylistDetailFragment.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.detail
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||
import org.oxycblt.auxio.detail.header.DetailHeaderAdapter
|
||||
import org.oxycblt.auxio.detail.header.PlaylistDetailHeaderAdapter
|
||||
import org.oxycblt.auxio.detail.list.DetailListAdapter
|
||||
import org.oxycblt.auxio.detail.list.PlaylistDetailListAdapter
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.ListFragment
|
||||
import org.oxycblt.auxio.list.Sort
|
||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.util.*
|
||||
|
||||
/**
|
||||
* A [ListFragment] that shows information for a particular [Playlist].
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class PlaylistDetailFragment :
|
||||
ListFragment<Song, FragmentDetailBinding>(),
|
||||
DetailHeaderAdapter.Listener,
|
||||
DetailListAdapter.Listener<Song> {
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
override val navModel: NavigationViewModel by activityViewModels()
|
||||
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
override val selectionModel: SelectionViewModel by activityViewModels()
|
||||
// Information about what playlist to display is initially within the navigation arguments
|
||||
// as a UID, as that is the only safe way to parcel an playlist.
|
||||
private val args: PlaylistDetailFragmentArgs by navArgs()
|
||||
private val playlistHeaderAdapter = PlaylistDetailHeaderAdapter(this)
|
||||
private val playlistListAdapter = PlaylistDetailListAdapter(this)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||
}
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
||||
|
||||
override fun getSelectionToolbar(binding: FragmentDetailBinding) =
|
||||
binding.detailSelectionToolbar
|
||||
|
||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||
super.onBindingCreated(binding, savedInstanceState)
|
||||
|
||||
// --- UI SETUP ---
|
||||
binding.detailToolbar.apply {
|
||||
inflateMenu(R.menu.menu_parent_detail)
|
||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
setOnMenuItemClickListener(this@PlaylistDetailFragment)
|
||||
}
|
||||
|
||||
binding.detailRecycler.adapter = ConcatAdapter(playlistHeaderAdapter, playlistListAdapter)
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
// DetailViewModel handles most initialization from the navigation argument.
|
||||
detailModel.setPlaylistUid(args.playlistUid)
|
||||
collectImmediately(detailModel.currentPlaylist, ::updatePlaylist)
|
||||
collectImmediately(detailModel.playlistList, ::updateList)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
collect(navModel.exploreNavigationItem.flow, ::handleNavigation)
|
||||
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||
}
|
||||
|
||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.detailToolbar.setOnMenuItemClickListener(null)
|
||||
binding.detailRecycler.adapter = null
|
||||
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
||||
// during list initialization and crash the app. Could happen if the user is fast enough.
|
||||
detailModel.playlistInstructions.consume()
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
if (super.onMenuItemClick(item)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO: Handle
|
||||
val currentPlaylist = unlikelyToBeNull(detailModel.currentPlaylist.value)
|
||||
return when (item.itemId) {
|
||||
R.id.action_play_next -> {
|
||||
// playbackModel.playNext(currentPlaylist)
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
true
|
||||
}
|
||||
R.id.action_queue_add -> {
|
||||
// playbackModel.addToQueue(currentPlaylist)
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(item: Song, viewHolder: RecyclerView.ViewHolder) {
|
||||
// TODO: Handle
|
||||
}
|
||||
|
||||
override fun onRealClick(item: Song) {
|
||||
// TODO: Handle
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Song, anchor: View) {
|
||||
openMusicMenu(anchor, R.menu.menu_song_actions, item)
|
||||
}
|
||||
|
||||
override fun onPlay() {
|
||||
// TODO: Handle
|
||||
// playbackModel.play(unlikelyToBeNull(detailModel.currentPlaylist.value))
|
||||
}
|
||||
|
||||
override fun onShuffle() {
|
||||
// playbackModel.shuffle(unlikelyToBeNull(detailModel.currentPlaylist.value))
|
||||
}
|
||||
|
||||
override fun onOpenSortMenu(anchor: View) {
|
||||
openMenu(anchor, R.menu.menu_playlist_sort) {
|
||||
// Select the corresponding sort mode option
|
||||
val sort = detailModel.playlistSongSort
|
||||
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
|
||||
// Select the corresponding sort direction option
|
||||
val directionItemId =
|
||||
when (sort.direction) {
|
||||
Sort.Direction.ASCENDING -> R.id.option_sort_asc
|
||||
Sort.Direction.DESCENDING -> R.id.option_sort_dec
|
||||
}
|
||||
unlikelyToBeNull(menu.findItem(directionItemId)).isChecked = true
|
||||
// If there is no sort specified, disable the ascending/descending options, as
|
||||
// they make no sense. We still do want to indicate the state however, in the case
|
||||
// that the user wants to switch to a sort mode where they do make sense.
|
||||
if (sort.mode is Sort.Mode.ByNone) {
|
||||
menu.findItem(R.id.option_sort_dec).isEnabled = false
|
||||
menu.findItem(R.id.option_sort_asc).isEnabled = false
|
||||
}
|
||||
|
||||
setOnMenuItemClickListener { item ->
|
||||
item.isChecked = !item.isChecked
|
||||
detailModel.playlistSongSort =
|
||||
when (item.itemId) {
|
||||
// Sort direction options
|
||||
R.id.option_sort_asc -> sort.withDirection(Sort.Direction.ASCENDING)
|
||||
R.id.option_sort_dec -> sort.withDirection(Sort.Direction.DESCENDING)
|
||||
// Any other option is a sort mode
|
||||
else -> sort.withMode(unlikelyToBeNull(Sort.Mode.fromItemId(item.itemId)))
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePlaylist(playlist: Playlist?) {
|
||||
if (playlist == null) {
|
||||
// Playlist we were showing no longer exists.
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
requireBinding().detailToolbar.title = playlist.resolveName(requireContext())
|
||||
playlistHeaderAdapter.setParent(playlist)
|
||||
}
|
||||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
// Prefer songs that might be playing from this playlist.
|
||||
if (parent is Playlist &&
|
||||
parent.uid == unlikelyToBeNull(detailModel.currentPlaylist.value).uid) {
|
||||
playlistListAdapter.setPlaying(song, isPlaying)
|
||||
} else {
|
||||
playlistListAdapter.setPlaying(null, isPlaying)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNavigation(item: Music?) {
|
||||
when (item) {
|
||||
is Song -> {
|
||||
logD("Navigating to another song")
|
||||
findNavController()
|
||||
.navigateSafe(PlaylistDetailFragmentDirections.actionShowAlbum(item.album.uid))
|
||||
}
|
||||
is Album -> {
|
||||
logD("Navigating to another album")
|
||||
findNavController()
|
||||
.navigateSafe(PlaylistDetailFragmentDirections.actionShowAlbum(item.uid))
|
||||
}
|
||||
is Artist -> {
|
||||
logD("Navigating to another artist")
|
||||
findNavController()
|
||||
.navigateSafe(PlaylistDetailFragmentDirections.actionShowArtist(item.uid))
|
||||
}
|
||||
is Playlist -> {
|
||||
navModel.exploreNavigationItem.consume()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateList(list: List<Item>) {
|
||||
playlistListAdapter.update(list, detailModel.playlistInstructions.consume())
|
||||
}
|
||||
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
playlistListAdapter.setSelected(selected.toSet())
|
||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||
}
|
||||
}
|
|
@ -33,6 +33,7 @@ import org.oxycblt.auxio.util.inflater
|
|||
/**
|
||||
* A [DetailHeaderAdapter] that shows [Album] information.
|
||||
*
|
||||
* @param listener [DetailHeaderAdapter.Listener] to bind interactions to.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class AlbumDetailHeaderAdapter(private val listener: Listener) :
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.oxycblt.auxio.util.inflater
|
|||
/**
|
||||
* A [DetailHeaderAdapter] that shows [Artist] information.
|
||||
*
|
||||
* @param listener [DetailHeaderAdapter.Listener] to bind interactions to.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class ArtistDetailHeaderAdapter(private val listener: Listener) :
|
||||
|
|
|
@ -24,7 +24,6 @@ import androidx.core.view.isVisible
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.ItemDetailHeaderBinding
|
||||
import org.oxycblt.auxio.detail.list.DetailListAdapter
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getPlural
|
||||
|
@ -33,6 +32,7 @@ import org.oxycblt.auxio.util.inflater
|
|||
/**
|
||||
* A [DetailHeaderAdapter] that shows [Genre] information.
|
||||
*
|
||||
* @param listener [DetailHeaderAdapter.Listener] to bind interactions to.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class GenreDetailHeaderAdapter(private val listener: Listener) :
|
||||
|
@ -57,7 +57,7 @@ private constructor(private val binding: ItemDetailHeaderBinding) :
|
|||
* Bind new data to this instance.
|
||||
*
|
||||
* @param genre The new [Genre] to bind.
|
||||
* @param listener A [DetailListAdapter.Listener] to bind interactions to.
|
||||
* @param listener A [DetailHeaderAdapter.Listener] to bind interactions to.
|
||||
*/
|
||||
fun bind(genre: Genre, listener: DetailHeaderAdapter.Listener) {
|
||||
binding.detailCover.bind(genre)
|
||||
|
@ -65,7 +65,7 @@ private constructor(private val binding: ItemDetailHeaderBinding) :
|
|||
binding.detailName.text = genre.resolveName(binding.context)
|
||||
// Nothing about a genre is applicable to the sub-head text.
|
||||
binding.detailSubhead.isVisible = false
|
||||
// The song count of the genre maps to the info text.
|
||||
// The song and artist count of the genre maps to the info text.
|
||||
binding.detailInfo.text =
|
||||
binding.context.getString(
|
||||
R.string.fmt_two,
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* PlaylistDetailHeaderAdapter.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.detail.header
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.ItemDetailHeaderBinding
|
||||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getPlural
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
/**
|
||||
* A [DetailHeaderAdapter] that shows [Playlist] information.
|
||||
*
|
||||
* @param listener [DetailHeaderAdapter.Listener] to bind interactions to.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class PlaylistDetailHeaderAdapter(private val listener: Listener) :
|
||||
DetailHeaderAdapter<Playlist, PlaylistDetailHeaderViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
PlaylistDetailHeaderViewHolder.from(parent)
|
||||
|
||||
override fun onBindHeader(holder: PlaylistDetailHeaderViewHolder, parent: Playlist) =
|
||||
holder.bind(parent, listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* A [RecyclerView.ViewHolder] that displays the [Playlist] header in the detail view. Use [from] to
|
||||
* create an instance.
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class PlaylistDetailHeaderViewHolder
|
||||
private constructor(private val binding: ItemDetailHeaderBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
/**
|
||||
* Bind new data to this instance.
|
||||
*
|
||||
* @param playlist The new [Playlist] to bind.
|
||||
* @param listener A [DetailHeaderAdapter.Listener] to bind interactions to.
|
||||
*/
|
||||
fun bind(playlist: Playlist, listener: DetailHeaderAdapter.Listener) {
|
||||
binding.detailCover.bind(playlist)
|
||||
binding.detailType.text = binding.context.getString(R.string.lbl_playlist)
|
||||
binding.detailName.text = playlist.resolveName(binding.context)
|
||||
// Nothing about a playlist is applicable to the sub-head text.
|
||||
binding.detailSubhead.isVisible = false
|
||||
// The song count of the playlist maps to the info text.
|
||||
binding.detailInfo.text =
|
||||
binding.context.getPlural(R.plurals.fmt_song_count, playlist.songs.size)
|
||||
binding.detailPlayButton.setOnClickListener { listener.onPlay() }
|
||||
binding.detailShuffleButton.setOnClickListener { listener.onShuffle() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create a new instance.
|
||||
*
|
||||
* @param parent The parent to inflate this instance from.
|
||||
* @return A new instance.
|
||||
*/
|
||||
fun from(parent: View) =
|
||||
PlaylistDetailHeaderViewHolder(ItemDetailHeaderBinding.inflate(parent.context.inflater))
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ import org.oxycblt.auxio.music.Music
|
|||
import org.oxycblt.auxio.music.Song
|
||||
|
||||
/**
|
||||
* An [DetailListAdapter] implementing the header and sub-items for the [Genre] detail view.
|
||||
* A [DetailListAdapter] implementing the header and sub-items for the [Genre] detail view.
|
||||
*
|
||||
* @param listener A [DetailListAdapter.Listener] to bind interactions to.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* PlaylistDetailListAdapter.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.detail.list
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
|
||||
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
||||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
||||
/**
|
||||
* A [DetailListAdapter] implementing the header and sub-items for the [Playlist] detail view.
|
||||
*
|
||||
* @param listener A [DetailListAdapter.Listener] to bind interactions to.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class PlaylistDetailListAdapter(private val listener: Listener<Song>) :
|
||||
DetailListAdapter(listener, DIFF_CALLBACK) {
|
||||
override fun getItemViewType(position: Int) =
|
||||
when (getItem(position)) {
|
||||
// Support generic song items.
|
||||
is Song -> SongViewHolder.VIEW_TYPE
|
||||
else -> super.getItemViewType(position)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
if (viewType == SongViewHolder.VIEW_TYPE) {
|
||||
SongViewHolder.from(parent)
|
||||
} else {
|
||||
super.onCreateViewHolder(parent, viewType)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
super.onBindViewHolder(holder, position)
|
||||
val item = getItem(position)
|
||||
if (item is Song) {
|
||||
(holder as SongViewHolder).bind(item, listener)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isItemFullWidth(position: Int): Boolean {
|
||||
if (super.isItemFullWidth(position)) {
|
||||
return true
|
||||
}
|
||||
// Playlist headers should be full-width in all configurations
|
||||
return getItem(position) is Playlist
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DIFF_CALLBACK =
|
||||
object : SimpleDiffCallback<Item>() {
|
||||
override fun areContentsTheSame(oldItem: Item, newItem: Item) =
|
||||
when {
|
||||
oldItem is Song && newItem is Song ->
|
||||
SongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
else -> DetailListAdapter.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -438,7 +438,8 @@ class HomeFragment :
|
|||
is Album -> HomeFragmentDirections.actionShowAlbum(item.uid)
|
||||
is Artist -> HomeFragmentDirections.actionShowArtist(item.uid)
|
||||
is Genre -> HomeFragmentDirections.actionShowGenre(item.uid)
|
||||
else -> return
|
||||
is Playlist -> HomeFragmentDirections.actionShowPlaylist(item.uid)
|
||||
null -> return
|
||||
}
|
||||
|
||||
setupAxisTransitions(MaterialSharedAxis.X)
|
||||
|
|
|
@ -22,7 +22,6 @@ import android.view.MenuItem
|
|||
import android.view.View
|
||||
import androidx.annotation.MenuRes
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.internal.view.SupportMenu
|
||||
import androidx.core.view.MenuCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
@ -281,7 +280,6 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
|
|||
currentMenu =
|
||||
PopupMenu(requireContext(), anchor).apply {
|
||||
inflate(menuRes)
|
||||
logD(menu is SupportMenu)
|
||||
MenuCompat.setGroupDividerEnabled(menu, true)
|
||||
block()
|
||||
setOnDismissListener { currentMenu = null }
|
||||
|
|
|
@ -114,23 +114,28 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
|||
}
|
||||
|
||||
private fun songsInPlace(songs: MutableList<out Song>) {
|
||||
songs.sortWith(mode.getSongComparator(direction))
|
||||
val comparator = mode.getSongComparator(direction) ?: return
|
||||
songs.sortWith(comparator)
|
||||
}
|
||||
|
||||
private fun albumsInPlace(albums: MutableList<out Album>) {
|
||||
albums.sortWith(mode.getAlbumComparator(direction))
|
||||
val comparator = mode.getAlbumComparator(direction) ?: return
|
||||
albums.sortWith(comparator)
|
||||
}
|
||||
|
||||
private fun artistsInPlace(artists: MutableList<out Artist>) {
|
||||
artists.sortWith(mode.getArtistComparator(direction))
|
||||
val comparator = mode.getArtistComparator(direction) ?: return
|
||||
artists.sortWith(comparator)
|
||||
}
|
||||
|
||||
private fun genresInPlace(genres: MutableList<out Genre>) {
|
||||
genres.sortWith(mode.getGenreComparator(direction))
|
||||
val comparator = mode.getGenreComparator(direction) ?: return
|
||||
genres.sortWith(comparator)
|
||||
}
|
||||
|
||||
private fun playlistsInPlace(playlists: MutableList<out Playlist>) {
|
||||
playlists.sortWith(mode.getPlaylistComparator(direction))
|
||||
val comparator = mode.getPlaylistComparator(direction) ?: return
|
||||
playlists.sortWith(comparator)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -160,50 +165,57 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
|||
* Get a [Comparator] that sorts [Song]s according to this [Mode].
|
||||
*
|
||||
* @param direction The direction to sort in.
|
||||
* @return A [Comparator] that can be used to sort a [Song] list according to this [Mode].
|
||||
* @return A [Comparator] that can be used to sort a [Song] list according to this [Mode],
|
||||
* or null to not sort at all.
|
||||
*/
|
||||
open fun getSongComparator(direction: Direction): Comparator<Song> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
open fun getSongComparator(direction: Direction): Comparator<Song>? = null
|
||||
|
||||
/**
|
||||
* Get a [Comparator] that sorts [Album]s according to this [Mode].
|
||||
*
|
||||
* @param direction The direction to sort in.
|
||||
* @return A [Comparator] that can be used to sort a [Album] list according to this [Mode].
|
||||
* @return A [Comparator] that can be used to sort a [Album] list according to this [Mode],
|
||||
* or null to not sort at all.
|
||||
*/
|
||||
open fun getAlbumComparator(direction: Direction): Comparator<Album> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
open fun getAlbumComparator(direction: Direction): Comparator<Album>? = null
|
||||
/**
|
||||
* Return a [Comparator] that sorts [Artist]s according to this [Mode].
|
||||
*
|
||||
* @param direction The direction to sort in.
|
||||
* @return A [Comparator] that can be used to sort a [Artist] list according to this [Mode].
|
||||
* or null to not sort at all.
|
||||
*/
|
||||
open fun getArtistComparator(direction: Direction): Comparator<Artist> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
open fun getArtistComparator(direction: Direction): Comparator<Artist>? = null
|
||||
|
||||
/**
|
||||
* Return a [Comparator] that sorts [Genre]s according to this [Mode].
|
||||
*
|
||||
* @param direction The direction to sort in.
|
||||
* @return A [Comparator] that can be used to sort a [Genre] list according to this [Mode].
|
||||
* or null to not sort at all.
|
||||
*/
|
||||
open fun getGenreComparator(direction: Direction): Comparator<Genre> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
open fun getGenreComparator(direction: Direction): Comparator<Genre>? = null
|
||||
|
||||
/**
|
||||
* Return a [Comparator] that sorts [Playlist]s according to this [Mode].
|
||||
*
|
||||
* @param direction The direction to sort in.
|
||||
* @return A [Comparator] that can be used to sort a [Genre] list according to this [Mode].
|
||||
* or null to not sort at all.
|
||||
*/
|
||||
open fun getPlaylistComparator(direction: Direction): Comparator<Playlist> {
|
||||
throw UnsupportedOperationException()
|
||||
open fun getPlaylistComparator(direction: Direction): Comparator<Playlist>? = null
|
||||
|
||||
/**
|
||||
* Sort by the item's natural order.
|
||||
*
|
||||
* @see Music.sortName
|
||||
*/
|
||||
object ByNone : Mode() {
|
||||
override val intCode: Int
|
||||
get() = IntegerTable.SORT_BY_NONE
|
||||
|
||||
override val itemId: Int
|
||||
get() = R.id.option_sort_none
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -614,6 +626,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
|||
*/
|
||||
fun fromIntCode(intCode: Int) =
|
||||
when (intCode) {
|
||||
ByNone.intCode -> ByNone
|
||||
ByName.intCode -> ByName
|
||||
ByArtist.intCode -> ByArtist
|
||||
ByAlbum.intCode -> ByAlbum
|
||||
|
@ -635,6 +648,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
|||
*/
|
||||
fun fromItemId(@IdRes itemId: Int) =
|
||||
when (itemId) {
|
||||
ByNone.itemId -> ByNone
|
||||
ByName.itemId -> ByName
|
||||
ByAlbum.itemId -> ByAlbum
|
||||
ByArtist.itemId -> ByArtist
|
||||
|
|
|
@ -46,6 +46,7 @@ interface MusicSettings : Settings<MusicSettings.Listener> {
|
|||
var multiValueSeparators: String
|
||||
/** Whether to trim english articles with song sort names. */
|
||||
val automaticSortNames: Boolean
|
||||
// TODO: Move sort settings to list module
|
||||
/** The [Sort] mode used in [Song] lists. */
|
||||
var songSort: Sort
|
||||
/** The [Sort] mode used in [Album] lists. */
|
||||
|
@ -62,8 +63,8 @@ interface MusicSettings : Settings<MusicSettings.Listener> {
|
|||
var artistSongSort: Sort
|
||||
/** The [Sort] mode used in a [Genre]'s [Song] list. */
|
||||
var genreSongSort: Sort
|
||||
/** The [Sort] mode used in a [Playlist]'s [Song] list, or null if sorting by original ordering. */
|
||||
var playlistSongSort: Sort?
|
||||
/** The [Sort] mode used in a [Playlist]'s [Song] list. */
|
||||
var playlistSongSort: Sort
|
||||
|
||||
interface Listener {
|
||||
/** Called when a setting controlling how music is loaded has changed. */
|
||||
|
@ -224,12 +225,15 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context
|
|||
}
|
||||
}
|
||||
|
||||
override var playlistSongSort: Sort?
|
||||
get() = Sort.fromIntCode(sharedPreferences.getInt(
|
||||
override var playlistSongSort: Sort
|
||||
get() =
|
||||
Sort.fromIntCode(
|
||||
sharedPreferences.getInt(
|
||||
getString(R.string.set_key_playlist_songs_sort), Int.MIN_VALUE))
|
||||
?: Sort(Sort.Mode.ByNone, Sort.Direction.ASCENDING)
|
||||
set(value) {
|
||||
sharedPreferences.edit {
|
||||
putInt(getString(R.string.lbl_playlist), value?.intCode ?: -1)
|
||||
putInt(getString(R.string.set_key_playlist_songs_sort), value.intCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ package org.oxycblt.auxio.music.user
|
|||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.music.MusicSettings
|
||||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.music.device.DeviceLibrary
|
||||
|
@ -75,5 +76,17 @@ private class UserLibraryImpl(
|
|||
override val playlists: List<Playlist>
|
||||
get() = playlistMap.values.toList()
|
||||
|
||||
init {
|
||||
val uid = Music.UID.auxio(MusicMode.PLAYLISTS) { update("Playlist 1".toByteArray()) }
|
||||
playlistMap[uid] =
|
||||
PlaylistImpl(
|
||||
RawPlaylist(
|
||||
PlaylistInfo(uid, "Playlist 1"),
|
||||
deviceLibrary.songs.slice(10..30).map { PlaylistSong(it.uid) }),
|
||||
deviceLibrary,
|
||||
musicSettings,
|
||||
)
|
||||
}
|
||||
|
||||
override fun findPlaylist(uid: Music.UID) = playlistMap[uid]
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ package org.oxycblt.auxio.playback
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.room.util.query
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
|
@ -178,7 +178,8 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
|||
is Album -> SearchFragmentDirections.actionShowAlbum(item.uid)
|
||||
is Artist -> SearchFragmentDirections.actionShowArtist(item.uid)
|
||||
is Genre -> SearchFragmentDirections.actionShowGenre(item.uid)
|
||||
else -> return
|
||||
is Playlist -> SearchFragmentDirections.actionShowPlaylist(item.uid)
|
||||
null -> return
|
||||
}
|
||||
// Keyboard is no longer needed.
|
||||
hideKeyboard()
|
||||
|
|
33
app/src/main/res/menu/menu_playlist_sort.xml
Normal file
33
app/src/main/res/menu/menu_playlist_sort.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<group android:checkableBehavior="single"
|
||||
android:id="@+id/sort_modes">
|
||||
<item
|
||||
android:id="@+id/option_sort_none"
|
||||
android:title="@string/lbl_none" />
|
||||
<item
|
||||
android:id="@+id/option_sort_name"
|
||||
android:title="@string/lbl_name" />
|
||||
<item
|
||||
android:id="@+id/option_sort_artist"
|
||||
android:title="@string/lbl_artist" />
|
||||
<item
|
||||
android:id="@+id/option_sort_album"
|
||||
android:title="@string/lbl_album" />
|
||||
<item
|
||||
android:id="@+id/option_sort_year"
|
||||
android:title="@string/lbl_date" />
|
||||
<item
|
||||
android:id="@+id/option_sort_duration"
|
||||
android:title="@string/lbl_duration" />
|
||||
</group>
|
||||
<group android:checkableBehavior="single"
|
||||
android:id="@+id/sort_direction">
|
||||
<item
|
||||
android:id="@+id/option_sort_asc"
|
||||
android:title="@string/lbl_sort_asc" />
|
||||
<item
|
||||
android:id="@+id/option_sort_dec"
|
||||
android:title="@string/lbl_sort_dec" />
|
||||
</group>
|
||||
</menu>
|
|
@ -49,11 +49,29 @@
|
|||
android:id="@+id/action_show_album"
|
||||
app:destination="@id/album_detail_fragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/playlist_detail_fragment"
|
||||
android:name="org.oxycblt.auxio.detail.PlaylistDetailFragment"
|
||||
android:label="PlaylistDetailFragment"
|
||||
tools:layout="@layout/fragment_detail">
|
||||
<argument
|
||||
android:name="playlistUid"
|
||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||
<action
|
||||
android:id="@+id/action_show_artist"
|
||||
app:destination="@id/artist_detail_fragment" />
|
||||
<action
|
||||
android:id="@+id/action_show_album"
|
||||
app:destination="@id/album_detail_fragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/search_fragment"
|
||||
android:name="org.oxycblt.auxio.search.SearchFragment"
|
||||
android:label="SearchFragment"
|
||||
tools:layout="@layout/fragment_search">
|
||||
<action
|
||||
android:id="@+id/action_show_playlist"
|
||||
app:destination="@id/playlist_detail_fragment" />
|
||||
<action
|
||||
android:id="@+id/action_show_genre"
|
||||
app:destination="@id/genre_detail_fragment" />
|
||||
|
@ -72,6 +90,9 @@
|
|||
<action
|
||||
android:id="@+id/action_show_search"
|
||||
app:destination="@id/search_fragment" />
|
||||
<action
|
||||
android:id="@+id/action_show_playlist"
|
||||
app:destination="@id/playlist_detail_fragment" />
|
||||
<action
|
||||
android:id="@+id/action_show_genre"
|
||||
app:destination="@id/genre_detail_fragment" />
|
||||
|
|
|
@ -86,6 +86,7 @@
|
|||
<!-- As in to not filter -->
|
||||
<string name="lbl_filter_all">All</string>
|
||||
|
||||
<string name="lbl_none">None</string>
|
||||
<string name="lbl_name">Name</string>
|
||||
<string name="lbl_date">Date</string>
|
||||
<string name="lbl_duration">Duration</string>
|
||||
|
|
|
@ -60,4 +60,7 @@ open class FakeMusicSettings : MusicSettings {
|
|||
override var genreSongSort: Sort
|
||||
get() = throw NotImplementedError()
|
||||
set(_) = throw NotImplementedError()
|
||||
override var playlistSongSort: Sort?
|
||||
get() = throw NotImplementedError()
|
||||
set(_) = throw NotImplementedError()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue