music: add playlist addition
Implement playlist addition and it's UI flow.
This commit is contained in:
parent
4fe91c25e3
commit
7435165929
48 changed files with 669 additions and 86 deletions
|
@ -50,6 +50,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
* TODO: Unit testing
|
* TODO: Unit testing
|
||||||
* TODO: Fix UID naming
|
* TODO: Fix UID naming
|
||||||
* TODO: Leverage FlexibleListAdapter more in dialogs (Disable item anims)
|
* TODO: Leverage FlexibleListAdapter more in dialogs (Disable item anims)
|
||||||
|
* TODO: Add more logging
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
|
@ -135,6 +135,7 @@ class MainFragment :
|
||||||
collect(navModel.exploreNavigationItem.flow, ::handleExploreNavigation)
|
collect(navModel.exploreNavigationItem.flow, ::handleExploreNavigation)
|
||||||
collect(navModel.exploreArtistNavigationItem.flow, ::handleArtistNavigationPicker)
|
collect(navModel.exploreArtistNavigationItem.flow, ::handleArtistNavigationPicker)
|
||||||
collect(musicModel.newPlaylistSongs.flow, ::handleNewPlaylist)
|
collect(musicModel.newPlaylistSongs.flow, ::handleNewPlaylist)
|
||||||
|
collect(musicModel.songsToAdd.flow, ::handleAddToPlaylist)
|
||||||
collectImmediately(playbackModel.song, ::updateSong)
|
collectImmediately(playbackModel.song, ::updateSong)
|
||||||
collect(playbackModel.artistPickerSong.flow, ::handlePlaybackArtistPicker)
|
collect(playbackModel.artistPickerSong.flow, ::handlePlaybackArtistPicker)
|
||||||
collect(playbackModel.genrePickerSong.flow, ::handlePlaybackGenrePicker)
|
collect(playbackModel.genrePickerSong.flow, ::handlePlaybackGenrePicker)
|
||||||
|
@ -261,7 +262,7 @@ class MainFragment :
|
||||||
initialNavDestinationChange = true
|
initialNavDestinationChange = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selectionModel.consume()
|
selectionModel.drop()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleMainNavigation(action: MainNavigationAction?) {
|
private fun handleMainNavigation(action: MainNavigationAction?) {
|
||||||
|
@ -312,6 +313,15 @@ class MainFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleAddToPlaylist(songs: List<Song>?) {
|
||||||
|
if (songs != null) {
|
||||||
|
findNavController()
|
||||||
|
.navigateSafe(
|
||||||
|
MainFragmentDirections.actionAddToPlaylist(songs.map { it.uid }.toTypedArray()))
|
||||||
|
musicModel.songsToAdd.consume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handlePlaybackArtistPicker(song: Song?) {
|
private fun handlePlaybackArtistPicker(song: Song?) {
|
||||||
if (song != null) {
|
if (song != null) {
|
||||||
navModel.mainNavigateTo(
|
navModel.mainNavigateTo(
|
||||||
|
@ -430,7 +440,7 @@ class MainFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear out any prior selections.
|
// Clear out any prior selections.
|
||||||
if (selectionModel.consume().isNotEmpty()) {
|
if (selectionModel.drop()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.navigation.NavigationViewModel
|
import org.oxycblt.auxio.navigation.NavigationViewModel
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
|
@ -61,6 +62,7 @@ class AlbumDetailFragment :
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
override val navModel: NavigationViewModel by activityViewModels()
|
override val navModel: NavigationViewModel by activityViewModels()
|
||||||
override val playbackModel: PlaybackViewModel by activityViewModels()
|
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
override val musicModel: MusicViewModel by activityViewModels()
|
||||||
override val selectionModel: SelectionViewModel by activityViewModels()
|
override val selectionModel: SelectionViewModel by activityViewModels()
|
||||||
// Information about what album to display is initially within the navigation arguments
|
// Information about what album to display is initially within the navigation arguments
|
||||||
// as a UID, as that is the only safe way to parcel an album.
|
// as a UID, as that is the only safe way to parcel an album.
|
||||||
|
@ -136,6 +138,10 @@ class AlbumDetailFragment :
|
||||||
onNavigateToParentArtist()
|
onNavigateToParentArtist()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.action_playlist_add -> {
|
||||||
|
musicModel.addToPlaylist(currentAlbum)
|
||||||
|
true
|
||||||
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ import org.oxycblt.auxio.music.Album
|
||||||
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.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.navigation.NavigationViewModel
|
import org.oxycblt.auxio.navigation.NavigationViewModel
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
|
@ -60,6 +61,7 @@ class ArtistDetailFragment :
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
override val navModel: NavigationViewModel by activityViewModels()
|
override val navModel: NavigationViewModel by activityViewModels()
|
||||||
override val playbackModel: PlaybackViewModel by activityViewModels()
|
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
override val musicModel: MusicViewModel by activityViewModels()
|
||||||
override val selectionModel: SelectionViewModel by activityViewModels()
|
override val selectionModel: SelectionViewModel by activityViewModels()
|
||||||
// Information about what artist to display is initially within the navigation arguments
|
// Information about what artist to display is initially within the navigation arguments
|
||||||
// as a UID, as that is the only safe way to parcel an artist.
|
// as a UID, as that is the only safe way to parcel an artist.
|
||||||
|
@ -131,6 +133,10 @@ class ArtistDetailFragment :
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.action_playlist_add -> {
|
||||||
|
musicModel.addToPlaylist(currentArtist)
|
||||||
|
true
|
||||||
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ class GenreDetailFragment :
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
override val navModel: NavigationViewModel by activityViewModels()
|
override val navModel: NavigationViewModel by activityViewModels()
|
||||||
override val playbackModel: PlaybackViewModel by activityViewModels()
|
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
override val musicModel: MusicViewModel by activityViewModels()
|
||||||
override val selectionModel: SelectionViewModel by activityViewModels()
|
override val selectionModel: SelectionViewModel by activityViewModels()
|
||||||
// Information about what genre to display is initially within the navigation arguments
|
// Information about what genre to display is initially within the navigation arguments
|
||||||
// as a UID, as that is the only safe way to parcel an genre.
|
// as a UID, as that is the only safe way to parcel an genre.
|
||||||
|
@ -125,6 +126,10 @@ class GenreDetailFragment :
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.action_playlist_add -> {
|
||||||
|
musicModel.addToPlaylist(currentGenre)
|
||||||
|
true
|
||||||
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ class PlaylistDetailFragment :
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
override val navModel: NavigationViewModel by activityViewModels()
|
override val navModel: NavigationViewModel by activityViewModels()
|
||||||
override val playbackModel: PlaybackViewModel by activityViewModels()
|
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
override val musicModel: MusicViewModel by activityViewModels()
|
||||||
override val selectionModel: SelectionViewModel by activityViewModels()
|
override val selectionModel: SelectionViewModel by activityViewModels()
|
||||||
// Information about what playlist to display is initially within the navigation arguments
|
// 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.
|
// as a UID, as that is the only safe way to parcel an playlist.
|
||||||
|
@ -81,7 +82,7 @@ class PlaylistDetailFragment :
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
binding.detailToolbar.apply {
|
binding.detailToolbar.apply {
|
||||||
inflateMenu(R.menu.menu_parent_detail)
|
inflateMenu(R.menu.menu_playlist_detail)
|
||||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
setOnMenuItemClickListener(this@PlaylistDetailFragment)
|
setOnMenuItemClickListener(this@PlaylistDetailFragment)
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ abstract class DetailHeaderAdapter<T : MusicParent, VH : RecyclerView.ViewHolder
|
||||||
notifyItemChanged(0, PAYLOAD_UPDATE_HEADER)
|
notifyItemChanged(0, PAYLOAD_UPDATE_HEADER)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An extended listener for [DetailHeaderAdapter] implementations. */
|
/** A listener for [DetailHeaderAdapter] implementations. */
|
||||||
interface Listener {
|
interface Listener {
|
||||||
/**
|
/**
|
||||||
* Called when the play button in a detail header is pressed, requesting that the current
|
* Called when the play button in a detail header is pressed, requesting that the current
|
||||||
|
|
|
@ -68,8 +68,8 @@ class HomeFragment :
|
||||||
SelectionFragment<FragmentHomeBinding>(), AppBarLayout.OnOffsetChangedListener {
|
SelectionFragment<FragmentHomeBinding>(), AppBarLayout.OnOffsetChangedListener {
|
||||||
override val playbackModel: PlaybackViewModel by activityViewModels()
|
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
override val selectionModel: SelectionViewModel by activityViewModels()
|
override val selectionModel: SelectionViewModel by activityViewModels()
|
||||||
|
override val musicModel: MusicViewModel by activityViewModels()
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
private val musicModel: MusicViewModel by activityViewModels()
|
|
||||||
private val navModel: NavigationViewModel by activityViewModels()
|
private val navModel: NavigationViewModel by activityViewModels()
|
||||||
private var storagePermissionLauncher: ActivityResultLauncher<String>? = null
|
private var storagePermissionLauncher: ActivityResultLauncher<String>? = null
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,7 @@ class AlbumListFragment :
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
override val navModel: NavigationViewModel by activityViewModels()
|
override val navModel: NavigationViewModel by activityViewModels()
|
||||||
override val playbackModel: PlaybackViewModel by activityViewModels()
|
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
override val musicModel: MusicViewModel by activityViewModels()
|
||||||
override val selectionModel: SelectionViewModel by activityViewModels()
|
override val selectionModel: SelectionViewModel by activityViewModels()
|
||||||
private val albumAdapter = AlbumAdapter(this)
|
private val albumAdapter = AlbumAdapter(this)
|
||||||
// Save memory by re-using the same formatter and string builder when creating popup text
|
// Save memory by re-using the same formatter and string builder when creating popup text
|
||||||
|
|
|
@ -38,6 +38,7 @@ import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.navigation.NavigationViewModel
|
import org.oxycblt.auxio.navigation.NavigationViewModel
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
|
@ -58,6 +59,7 @@ class ArtistListFragment :
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
override val navModel: NavigationViewModel by activityViewModels()
|
override val navModel: NavigationViewModel by activityViewModels()
|
||||||
override val playbackModel: PlaybackViewModel by activityViewModels()
|
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
override val musicModel: MusicViewModel by activityViewModels()
|
||||||
override val selectionModel: SelectionViewModel by activityViewModels()
|
override val selectionModel: SelectionViewModel by activityViewModels()
|
||||||
private val artistAdapter = ArtistAdapter(this)
|
private val artistAdapter = ArtistAdapter(this)
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.navigation.NavigationViewModel
|
import org.oxycblt.auxio.navigation.NavigationViewModel
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
|
@ -57,6 +58,7 @@ class GenreListFragment :
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
override val navModel: NavigationViewModel by activityViewModels()
|
override val navModel: NavigationViewModel by activityViewModels()
|
||||||
override val playbackModel: PlaybackViewModel by activityViewModels()
|
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
override val musicModel: MusicViewModel by activityViewModels()
|
||||||
override val selectionModel: SelectionViewModel by activityViewModels()
|
override val selectionModel: SelectionViewModel by activityViewModels()
|
||||||
private val genreAdapter = GenreAdapter(this)
|
private val genreAdapter = GenreAdapter(this)
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.navigation.NavigationViewModel
|
import org.oxycblt.auxio.navigation.NavigationViewModel
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
|
@ -50,6 +51,7 @@ class PlaylistListFragment :
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
override val navModel: NavigationViewModel by activityViewModels()
|
override val navModel: NavigationViewModel by activityViewModels()
|
||||||
override val playbackModel: PlaybackViewModel by activityViewModels()
|
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
override val musicModel: MusicViewModel by activityViewModels()
|
||||||
override val selectionModel: SelectionViewModel by activityViewModels()
|
override val selectionModel: SelectionViewModel by activityViewModels()
|
||||||
private val playlistAdapter = PlaylistAdapter(this)
|
private val playlistAdapter = PlaylistAdapter(this)
|
||||||
|
|
||||||
|
@ -107,7 +109,7 @@ class PlaylistListFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Playlist, anchor: View) {
|
override fun onOpenMenu(item: Playlist, anchor: View) {
|
||||||
openMusicMenu(anchor, R.menu.menu_parent_actions, item)
|
openMusicMenu(anchor, R.menu.menu_playlist_actions, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePlaylists(playlists: List<Playlist>) {
|
private fun updatePlaylists(playlists: List<Playlist>) {
|
||||||
|
|
|
@ -39,6 +39,7 @@ import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.navigation.NavigationViewModel
|
import org.oxycblt.auxio.navigation.NavigationViewModel
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
|
@ -59,6 +60,7 @@ class SongListFragment :
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
override val navModel: NavigationViewModel by activityViewModels()
|
override val navModel: NavigationViewModel by activityViewModels()
|
||||||
override val playbackModel: PlaybackViewModel by activityViewModels()
|
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
override val musicModel: MusicViewModel by activityViewModels()
|
||||||
override val selectionModel: SelectionViewModel by activityViewModels()
|
override val selectionModel: SelectionViewModel by activityViewModels()
|
||||||
private val songAdapter = SongAdapter(this)
|
private val songAdapter = SongAdapter(this)
|
||||||
// Save memory by re-using the same formatter and string builder when creating popup text
|
// Save memory by re-using the same formatter and string builder when creating popup text
|
||||||
|
|
|
@ -41,6 +41,7 @@ import org.oxycblt.auxio.music.*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class MusicKeyer : Keyer<Music> {
|
class MusicKeyer : Keyer<Music> {
|
||||||
|
// TODO: Include hashcode of child songs for parents
|
||||||
override fun key(data: Music, options: Options) =
|
override fun key(data: Music, options: Options) =
|
||||||
if (data is Song) {
|
if (data is Song) {
|
||||||
// Group up song covers with album covers for better caching
|
// Group up song covers with album covers for better caching
|
||||||
|
|
|
@ -99,6 +99,9 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
|
||||||
R.id.action_go_album -> {
|
R.id.action_go_album -> {
|
||||||
navModel.exploreNavigateTo(song.album)
|
navModel.exploreNavigateTo(song.album)
|
||||||
}
|
}
|
||||||
|
R.id.action_playlist_add -> {
|
||||||
|
musicModel.addToPlaylist(song)
|
||||||
|
}
|
||||||
R.id.action_song_detail -> {
|
R.id.action_song_detail -> {
|
||||||
navModel.mainNavigateTo(
|
navModel.mainNavigateTo(
|
||||||
MainNavigationAction.Directions(
|
MainNavigationAction.Directions(
|
||||||
|
@ -141,6 +144,9 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
|
||||||
R.id.action_go_artist -> {
|
R.id.action_go_artist -> {
|
||||||
navModel.exploreNavigateToParentArtist(album)
|
navModel.exploreNavigateToParentArtist(album)
|
||||||
}
|
}
|
||||||
|
R.id.action_playlist_add -> {
|
||||||
|
musicModel.addToPlaylist(album)
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
error("Unexpected menu item selected")
|
error("Unexpected menu item selected")
|
||||||
}
|
}
|
||||||
|
@ -175,6 +181,9 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
|
||||||
playbackModel.addToQueue(artist)
|
playbackModel.addToQueue(artist)
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
}
|
}
|
||||||
|
R.id.action_playlist_add -> {
|
||||||
|
musicModel.addToPlaylist(artist)
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
error("Unexpected menu item selected")
|
error("Unexpected menu item selected")
|
||||||
}
|
}
|
||||||
|
@ -209,6 +218,9 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
|
||||||
playbackModel.addToQueue(genre)
|
playbackModel.addToQueue(genre)
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
}
|
}
|
||||||
|
R.id.action_playlist_add -> {
|
||||||
|
musicModel.addToPlaylist(genre)
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
error("Unexpected menu item selected")
|
error("Unexpected menu item selected")
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,8 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
* - Adapter-based [SpanSizeLookup] implementation
|
* - Adapter-based [SpanSizeLookup] implementation
|
||||||
* - Automatic [setHasFixedSize] setup
|
* - Automatic [setHasFixedSize] setup
|
||||||
*
|
*
|
||||||
|
* FIXME: Broken span configuration
|
||||||
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
open class AuxioRecyclerView
|
open class AuxioRecyclerView
|
||||||
|
|
|
@ -353,6 +353,10 @@ class BasicHeaderViewHolder private constructor(private val binding: ItemHeaderB
|
||||||
/**
|
/**
|
||||||
* A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical [T] item, for use
|
* A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical [T] item, for use
|
||||||
* in choice dialogs. Use [from] to create an instance.
|
* in choice dialogs. Use [from] to create an instance.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*
|
||||||
|
* TODO: Unwind this into specific impls
|
||||||
*/
|
*/
|
||||||
class ChoiceViewHolder<T : Music>
|
class ChoiceViewHolder<T : Music>
|
||||||
private constructor(private val binding: ItemPickerChoiceBinding) :
|
private constructor(private val binding: ItemPickerChoiceBinding) :
|
||||||
|
|
|
@ -23,6 +23,7 @@ import android.view.MenuItem
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
|
@ -35,6 +36,7 @@ import org.oxycblt.auxio.util.showToast
|
||||||
abstract class SelectionFragment<VB : ViewBinding> :
|
abstract class SelectionFragment<VB : ViewBinding> :
|
||||||
ViewBindingFragment<VB>(), Toolbar.OnMenuItemClickListener {
|
ViewBindingFragment<VB>(), Toolbar.OnMenuItemClickListener {
|
||||||
protected abstract val selectionModel: SelectionViewModel
|
protected abstract val selectionModel: SelectionViewModel
|
||||||
|
protected abstract val musicModel: MusicViewModel
|
||||||
protected abstract val playbackModel: PlaybackViewModel
|
protected abstract val playbackModel: PlaybackViewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,7 +52,7 @@ abstract class SelectionFragment<VB : ViewBinding> :
|
||||||
super.onBindingCreated(binding, savedInstanceState)
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
getSelectionToolbar(binding)?.apply {
|
getSelectionToolbar(binding)?.apply {
|
||||||
// Add cancel and menu item listeners to manage what occurs with the selection.
|
// Add cancel and menu item listeners to manage what occurs with the selection.
|
||||||
setOnSelectionCancelListener { selectionModel.consume() }
|
setOnSelectionCancelListener { selectionModel.drop() }
|
||||||
setOnMenuItemClickListener(this@SelectionFragment)
|
setOnMenuItemClickListener(this@SelectionFragment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,21 +65,25 @@ abstract class SelectionFragment<VB : ViewBinding> :
|
||||||
override fun onMenuItemClick(item: MenuItem) =
|
override fun onMenuItemClick(item: MenuItem) =
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_selection_play_next -> {
|
R.id.action_selection_play_next -> {
|
||||||
playbackModel.playNext(selectionModel.consume())
|
playbackModel.playNext(selectionModel.take())
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.action_selection_queue_add -> {
|
R.id.action_selection_queue_add -> {
|
||||||
playbackModel.addToQueue(selectionModel.consume())
|
playbackModel.addToQueue(selectionModel.take())
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.action_selection_playlist_add -> {
|
||||||
|
musicModel.addToPlaylist(selectionModel.take())
|
||||||
|
true
|
||||||
|
}
|
||||||
R.id.action_selection_play -> {
|
R.id.action_selection_play -> {
|
||||||
playbackModel.play(selectionModel.consume())
|
playbackModel.play(selectionModel.take())
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.action_selection_shuffle -> {
|
R.id.action_selection_shuffle -> {
|
||||||
playbackModel.shuffle(selectionModel.consume())
|
playbackModel.shuffle(selectionModel.take())
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> false
|
else -> false
|
||||||
|
|
|
@ -31,8 +31,12 @@ import org.oxycblt.auxio.music.*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SelectionViewModel @Inject constructor(private val musicRepository: MusicRepository) :
|
class SelectionViewModel
|
||||||
ViewModel(), MusicRepository.UpdateListener {
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val musicRepository: MusicRepository,
|
||||||
|
private val musicSettings: MusicSettings
|
||||||
|
) : ViewModel(), MusicRepository.UpdateListener {
|
||||||
private val _selected = MutableStateFlow(listOf<Music>())
|
private val _selected = MutableStateFlow(listOf<Music>())
|
||||||
/** the currently selected items. These are ordered in earliest selected and latest selected. */
|
/** the currently selected items. These are ordered in earliest selected and latest selected. */
|
||||||
val selected: StateFlow<List<Music>>
|
val selected: StateFlow<List<Music>>
|
||||||
|
@ -80,9 +84,27 @@ class SelectionViewModel @Inject constructor(private val musicRepository: MusicR
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consume the current selection. This will clear any items that were selected prior.
|
* Clear the current selection and return it.
|
||||||
*
|
*
|
||||||
* @return The list of selected items before it was cleared.
|
* @return A list of [Song]s collated from each item selected.
|
||||||
*/
|
*/
|
||||||
fun consume() = _selected.value.also { _selected.value = listOf() }
|
fun take() =
|
||||||
|
_selected.value
|
||||||
|
.flatMap {
|
||||||
|
when (it) {
|
||||||
|
is Song -> listOf(it)
|
||||||
|
is Album -> musicSettings.albumSongSort.songs(it.songs)
|
||||||
|
is Artist -> musicSettings.artistSongSort.songs(it.songs)
|
||||||
|
is Genre -> musicSettings.genreSongSort.songs(it.songs)
|
||||||
|
is Playlist -> musicSettings.playlistSongSort.songs(it.songs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.also { drop() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the current selection.
|
||||||
|
*
|
||||||
|
* @return true if the prior selection was non-empty, false otherwise.
|
||||||
|
*/
|
||||||
|
fun drop() = _selected.value.isNotEmpty().also { _selected.value = listOf() }
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,11 +114,19 @@ interface MusicRepository {
|
||||||
/**
|
/**
|
||||||
* Create a new [Playlist] of the given [Song]s.
|
* Create a new [Playlist] of the given [Song]s.
|
||||||
*
|
*
|
||||||
* @param name The name of the new [Playlist]
|
* @param name The name of the new [Playlist].
|
||||||
* @param songs The songs to populate the new [Playlist] with.
|
* @param songs The songs to populate the new [Playlist] with.
|
||||||
*/
|
*/
|
||||||
fun createPlaylist(name: String, songs: List<Song>)
|
fun createPlaylist(name: String, songs: List<Song>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the given [Song]s to a [Playlist].
|
||||||
|
*
|
||||||
|
* @param songs The [Song]s to add to the [Playlist].
|
||||||
|
* @param playlist The [Playlist] to add to.
|
||||||
|
*/
|
||||||
|
fun addToPlaylist(songs: List<Song>, playlist: Playlist)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request that a music loading operation is started by the current [IndexingWorker]. Does
|
* Request that a music loading operation is started by the current [IndexingWorker]. Does
|
||||||
* nothing if one is not available.
|
* nothing if one is not available.
|
||||||
|
@ -255,6 +263,15 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun addToPlaylist(songs: List<Song>, playlist: Playlist) {
|
||||||
|
val userLibrary = userLibrary ?: return
|
||||||
|
userLibrary.addToPlaylist(playlist, songs)
|
||||||
|
for (listener in updateListeners) {
|
||||||
|
listener.onMusicChanges(
|
||||||
|
MusicRepository.Changes(deviceLibrary = false, userLibrary = true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun requestIndex(withCache: Boolean) {
|
override fun requestIndex(withCache: Boolean) {
|
||||||
indexingWorker?.requestIndex(withCache)
|
indexingWorker?.requestIndex(withCache)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,8 +32,12 @@ import org.oxycblt.auxio.util.MutableEvent
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MusicViewModel @Inject constructor(private val musicRepository: MusicRepository) :
|
class MusicViewModel
|
||||||
ViewModel(), MusicRepository.UpdateListener, MusicRepository.IndexingListener {
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val musicRepository: MusicRepository,
|
||||||
|
private val musicSettings: MusicSettings
|
||||||
|
) : ViewModel(), MusicRepository.UpdateListener, MusicRepository.IndexingListener {
|
||||||
|
|
||||||
private val _indexingState = MutableStateFlow<IndexingState?>(null)
|
private val _indexingState = MutableStateFlow<IndexingState?>(null)
|
||||||
/** The current music loading state, or null if no loading is going on. */
|
/** The current music loading state, or null if no loading is going on. */
|
||||||
|
@ -48,6 +52,10 @@ class MusicViewModel @Inject constructor(private val musicRepository: MusicRepos
|
||||||
/** Flag for opening a dialog to create a playlist of the given [Song]s. */
|
/** Flag for opening a dialog to create a playlist of the given [Song]s. */
|
||||||
val newPlaylistSongs: Event<List<Song>?> = _newPlaylistSongs
|
val newPlaylistSongs: Event<List<Song>?> = _newPlaylistSongs
|
||||||
|
|
||||||
|
private val _songsToAdd = MutableEvent<List<Song>?>()
|
||||||
|
/** Flag for opening a dialog to add the given [Song]s to a playlist. */
|
||||||
|
val songsToAdd: Event<List<Song>?> = _songsToAdd
|
||||||
|
|
||||||
init {
|
init {
|
||||||
musicRepository.addUpdateListener(this)
|
musicRepository.addUpdateListener(this)
|
||||||
musicRepository.addIndexingListener(this)
|
musicRepository.addIndexingListener(this)
|
||||||
|
@ -85,23 +93,71 @@ class MusicViewModel @Inject constructor(private val musicRepository: MusicRepos
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new generic playlist. This will first open a dialog for the user to make a naming
|
* Create a new generic [Playlist].
|
||||||
* choice before committing the playlist to the database.
|
|
||||||
*
|
*
|
||||||
|
* @param name The name of the new [Playlist]. If null, the user will be prompted for one.
|
||||||
* @param songs The [Song]s to be contained in the new playlist.
|
* @param songs The [Song]s to be contained in the new playlist.
|
||||||
*/
|
*/
|
||||||
fun createPlaylist(songs: List<Song> = listOf()) {
|
fun createPlaylist(name: String? = null, songs: List<Song> = listOf()) {
|
||||||
_newPlaylistSongs.put(songs)
|
if (name != null) {
|
||||||
|
musicRepository.createPlaylist(name, songs)
|
||||||
|
} else {
|
||||||
|
_newPlaylistSongs.put(songs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new generic playlist. This will immediately commit the playlist to the database.
|
* Add a [Song] to a [Playlist].
|
||||||
*
|
*
|
||||||
* @param name The name of the new playlist.
|
* @param song The [Song] to add to the [Playlist].
|
||||||
* @param songs The [Song]s to be contained in the new playlist.
|
* @param playlist The [Playlist] to add to. If null, the user will be prompted for one.
|
||||||
*/
|
*/
|
||||||
fun createPlaylist(name: String, songs: List<Song> = listOf()) {
|
fun addToPlaylist(song: Song, playlist: Playlist? = null) {
|
||||||
musicRepository.createPlaylist(name, songs)
|
addToPlaylist(listOf(song), playlist)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an [Album] to a [Playlist].
|
||||||
|
*
|
||||||
|
* @param album The [Album] to add to the [Playlist].
|
||||||
|
* @param playlist The [Playlist] to add to. If null, the user will be prompted for one.
|
||||||
|
*/
|
||||||
|
fun addToPlaylist(album: Album, playlist: Playlist? = null) {
|
||||||
|
addToPlaylist(musicSettings.albumSongSort.songs(album.songs), playlist)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an [Artist] to a [Playlist].
|
||||||
|
*
|
||||||
|
* @param artist The [Artist] to add to the [Playlist].
|
||||||
|
* @param playlist The [Playlist] to add to. If null, the user will be prompted for one.
|
||||||
|
*/
|
||||||
|
fun addToPlaylist(artist: Artist, playlist: Playlist? = null) {
|
||||||
|
addToPlaylist(musicSettings.artistSongSort.songs(artist.songs), playlist)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a [Genre] to a [Playlist].
|
||||||
|
*
|
||||||
|
* @param genre The [Genre] to add to the [Playlist].
|
||||||
|
* @param playlist The [Playlist] to add to. If null, the user will be prompted for one.
|
||||||
|
*/
|
||||||
|
fun addToPlaylist(genre: Genre, playlist: Playlist? = null) {
|
||||||
|
addToPlaylist(musicSettings.genreSongSort.songs(genre.songs), playlist)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add [Song]s to a [Playlist].
|
||||||
|
*
|
||||||
|
* @param songs The [Song]s to add to the [Playlist].
|
||||||
|
* @param playlist The [Playlist] to add to. If null, the user will be prompted for one.
|
||||||
|
*/
|
||||||
|
fun addToPlaylist(songs: List<Song>, playlist: Playlist? = null) {
|
||||||
|
if (playlist != null) {
|
||||||
|
musicRepository.addToPlaylist(songs, playlist)
|
||||||
|
} else {
|
||||||
|
_songsToAdd.put(songs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -16,14 +16,13 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.config
|
package org.oxycblt.auxio.music.fs
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.databinding.ItemMusicDirBinding
|
import org.oxycblt.auxio.databinding.ItemMusicDirBinding
|
||||||
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
|
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
|
||||||
import org.oxycblt.auxio.music.fs.Directory
|
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.config
|
package org.oxycblt.auxio.music.fs
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
@ -35,8 +35,6 @@ import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
|
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
import org.oxycblt.auxio.music.fs.Directory
|
|
||||||
import org.oxycblt.auxio.music.fs.MusicDirectories
|
|
||||||
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.config
|
package org.oxycblt.auxio.music.metadata
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* AddToPlaylistDialog.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.music.picker
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
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 dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
|
||||||
|
import org.oxycblt.auxio.list.ClickableListListener
|
||||||
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
import org.oxycblt.auxio.util.showToast
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dialog that allows the user to pick a specific playlist to add song(s) to.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class AddToPlaylistDialog :
|
||||||
|
ViewBindingDialogFragment<DialogMusicPickerBinding>(),
|
||||||
|
ClickableListListener<PlaylistChoice>,
|
||||||
|
NewPlaylistFooterAdapter.Listener {
|
||||||
|
private val musicModel: MusicViewModel by activityViewModels()
|
||||||
|
private val pickerModel: PlaylistPickerViewModel by activityViewModels()
|
||||||
|
// Information about what playlist to name for is initially within the navigation arguments
|
||||||
|
// as UIDs, as that is the only safe way to parcel playlist information.
|
||||||
|
private val args: AddToPlaylistDialogArgs by navArgs()
|
||||||
|
private val choiceAdapter = PlaylistChoiceAdapter(this)
|
||||||
|
private val footerAdapter = NewPlaylistFooterAdapter(this)
|
||||||
|
|
||||||
|
override fun onConfigDialog(builder: AlertDialog.Builder) {
|
||||||
|
builder.setTitle(R.string.lbl_playlist_add).setNegativeButton(R.string.lbl_cancel, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||||
|
DialogMusicPickerBinding.inflate(inflater)
|
||||||
|
|
||||||
|
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
|
||||||
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
|
binding.pickerChoiceRecycler.apply {
|
||||||
|
itemAnimator = null
|
||||||
|
adapter = ConcatAdapter(choiceAdapter, footerAdapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- VIEWMODEL SETUP ---
|
||||||
|
pickerModel.setPendingSongs(args.songUids)
|
||||||
|
collectImmediately(pickerModel.currentPendingSongs, ::updatePendingSongs)
|
||||||
|
collectImmediately(pickerModel.playlistChoices, ::updatePlaylistChoices)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyBinding(binding: DialogMusicPickerBinding) {
|
||||||
|
super.onDestroyBinding(binding)
|
||||||
|
binding.pickerChoiceRecycler.adapter = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(item: PlaylistChoice, viewHolder: RecyclerView.ViewHolder) {
|
||||||
|
musicModel.addToPlaylist(pickerModel.currentPendingSongs.value ?: return, item.playlist)
|
||||||
|
pickerModel.confirmPlaylistAddition()
|
||||||
|
requireContext().showToast(R.string.lng_playlist_added)
|
||||||
|
findNavController().navigateUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewPlaylist() {
|
||||||
|
musicModel.createPlaylist(songs = pickerModel.currentPendingSongs.value ?: return)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePendingSongs(songs: List<Song>?) {
|
||||||
|
if (songs == null) {
|
||||||
|
// No songs to feasibly add to a playlist, leave.
|
||||||
|
findNavController().navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePlaylistChoices(choices: List<PlaylistChoice>) {
|
||||||
|
choiceAdapter.update(choices, null)
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,7 +23,6 @@ import android.view.LayoutInflater
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
@ -32,6 +31,7 @@ import org.oxycblt.auxio.databinding.DialogPlaylistNameBinding
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
import org.oxycblt.auxio.util.showToast
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,7 +42,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class NewPlaylistDialog : ViewBindingDialogFragment<DialogPlaylistNameBinding>() {
|
class NewPlaylistDialog : ViewBindingDialogFragment<DialogPlaylistNameBinding>() {
|
||||||
private val musicModel: MusicViewModel by activityViewModels()
|
private val musicModel: MusicViewModel by activityViewModels()
|
||||||
private val pickerModel: PlaylistPickerViewModel by viewModels()
|
private val pickerModel: PlaylistPickerViewModel by activityViewModels()
|
||||||
// Information about what playlist to name for is initially within the navigation arguments
|
// Information about what playlist to name for is initially within the navigation arguments
|
||||||
// as UIDs, as that is the only safe way to parcel playlist information.
|
// as UIDs, as that is the only safe way to parcel playlist information.
|
||||||
private val args: NewPlaylistDialogArgs by navArgs()
|
private val args: NewPlaylistDialogArgs by navArgs()
|
||||||
|
@ -58,7 +58,10 @@ class NewPlaylistDialog : ViewBindingDialogFragment<DialogPlaylistNameBinding>()
|
||||||
is ChosenName.Empty -> pendingPlaylist.preferredName
|
is ChosenName.Empty -> pendingPlaylist.preferredName
|
||||||
else -> throw IllegalStateException()
|
else -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
|
// TODO: Navigate to playlist if there are songs in it
|
||||||
musicModel.createPlaylist(name, pendingPlaylist.songs)
|
musicModel.createPlaylist(name, pendingPlaylist.songs)
|
||||||
|
pickerModel.confirmPlaylistCreation()
|
||||||
|
requireContext().showToast(R.string.lng_playlist_created)
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.lbl_cancel, null)
|
.setNegativeButton(R.string.lbl_cancel, null)
|
||||||
}
|
}
|
||||||
|
@ -69,11 +72,13 @@ class NewPlaylistDialog : ViewBindingDialogFragment<DialogPlaylistNameBinding>()
|
||||||
override fun onBindingCreated(binding: DialogPlaylistNameBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: DialogPlaylistNameBinding, savedInstanceState: Bundle?) {
|
||||||
super.onBindingCreated(binding, savedInstanceState)
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
|
// --- UI SETUP ---
|
||||||
binding.playlistName.addTextChangedListener { pickerModel.updateChosenName(it?.toString()) }
|
binding.playlistName.addTextChangedListener { pickerModel.updateChosenName(it?.toString()) }
|
||||||
|
|
||||||
|
// --- VIEWMODEL SETUP ---
|
||||||
pickerModel.setPendingPlaylist(requireContext(), args.songUids)
|
pickerModel.setPendingPlaylist(requireContext(), args.songUids)
|
||||||
collectImmediately(pickerModel.currentPendingPlaylist, ::updatePendingPlaylist)
|
collectImmediately(pickerModel.currentPendingPlaylist, ::updatePendingPlaylist)
|
||||||
collectImmediately(pickerModel.chosenName, ::handleChosenName)
|
collectImmediately(pickerModel.chosenName, ::updateChosenName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePendingPlaylist(pendingPlaylist: PendingPlaylist?) {
|
private fun updatePendingPlaylist(pendingPlaylist: PendingPlaylist?) {
|
||||||
|
@ -85,7 +90,7 @@ class NewPlaylistDialog : ViewBindingDialogFragment<DialogPlaylistNameBinding>()
|
||||||
requireBinding().playlistName.hint = pendingPlaylist.preferredName
|
requireBinding().playlistName.hint = pendingPlaylist.preferredName
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleChosenName(chosenName: ChosenName) {
|
private fun updateChosenName(chosenName: ChosenName) {
|
||||||
(dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled =
|
(dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled =
|
||||||
chosenName is ChosenName.Valid || chosenName is ChosenName.Empty
|
chosenName is ChosenName.Valid || chosenName is ChosenName.Empty
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* NewPlaylistFooterAdapter.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.music.picker
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.oxycblt.auxio.databinding.ItemNewPlaylistChoiceBinding
|
||||||
|
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
|
||||||
|
import org.oxycblt.auxio.util.inflater
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A purely-visual [RecyclerView.Adapter] that acts as a footer providing a "New Playlist" choice in
|
||||||
|
* [AddToPlaylistDialog].
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
class NewPlaylistFooterAdapter(private val listener: Listener) :
|
||||||
|
RecyclerView.Adapter<NewPlaylistFooterViewHolder>() {
|
||||||
|
override fun getItemCount() = 1
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
|
NewPlaylistFooterViewHolder.from(parent)
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: NewPlaylistFooterViewHolder, position: Int) {
|
||||||
|
holder.bind(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A listener for [NewPlaylistFooterAdapter] interactions. */
|
||||||
|
interface Listener {
|
||||||
|
/**
|
||||||
|
* Called when the footer has been pressed, requesting to create a new playlist to add to.
|
||||||
|
*/
|
||||||
|
fun onNewPlaylist()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [RecyclerView.ViewHolder] that displays a "New Playlist" choice in [NewPlaylistFooterAdapter].
|
||||||
|
* Use [from] to create an instance.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
class NewPlaylistFooterViewHolder
|
||||||
|
private constructor(private val binding: ItemNewPlaylistChoiceBinding) :
|
||||||
|
DialogRecyclerView.ViewHolder(binding.root) {
|
||||||
|
/**
|
||||||
|
* Bind new data to this instance.
|
||||||
|
*
|
||||||
|
* @param listener A [NewPlaylistFooterAdapter.Listener] to bind interactions to.
|
||||||
|
*/
|
||||||
|
fun bind(listener: NewPlaylistFooterAdapter.Listener) {
|
||||||
|
binding.root.setOnClickListener { listener.onNewPlaylist() }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Create a new instance.
|
||||||
|
*
|
||||||
|
* @param parent The parent to inflate this instance from.
|
||||||
|
* @return A new instance.
|
||||||
|
*/
|
||||||
|
fun from(parent: View) =
|
||||||
|
NewPlaylistFooterViewHolder(
|
||||||
|
ItemNewPlaylistChoiceBinding.inflate(parent.context.inflater))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* PlaylistChoiceAdapter.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.music.picker
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import org.oxycblt.auxio.databinding.ItemPickerChoiceBinding
|
||||||
|
import org.oxycblt.auxio.list.ClickableListListener
|
||||||
|
import org.oxycblt.auxio.list.adapter.FlexibleListAdapter
|
||||||
|
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
|
||||||
|
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
|
||||||
|
import org.oxycblt.auxio.util.context
|
||||||
|
import org.oxycblt.auxio.util.inflater
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [FlexibleListAdapter] that displays a list of [PlaylistChoice] options to select from in
|
||||||
|
* [AddToPlaylistDialog].
|
||||||
|
*
|
||||||
|
* @param listener [ClickableListListener] to bind interactions to.
|
||||||
|
*/
|
||||||
|
class PlaylistChoiceAdapter(val listener: ClickableListListener<PlaylistChoice>) :
|
||||||
|
FlexibleListAdapter<PlaylistChoice, PlaylistChoiceViewHolder>(
|
||||||
|
PlaylistChoiceViewHolder.DIFF_CALLBACK) {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
|
PlaylistChoiceViewHolder.from(parent)
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: PlaylistChoiceViewHolder, position: Int) {
|
||||||
|
holder.bind(getItem(position), listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [DialogRecyclerView.ViewHolder] that displays an individual playlist choice. Use [from] to
|
||||||
|
* create an instance.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
class PlaylistChoiceViewHolder private constructor(private val binding: ItemPickerChoiceBinding) :
|
||||||
|
DialogRecyclerView.ViewHolder(binding.root) {
|
||||||
|
fun bind(choice: PlaylistChoice, listener: ClickableListListener<PlaylistChoice>) {
|
||||||
|
listener.bind(choice, this)
|
||||||
|
binding.pickerImage.apply {
|
||||||
|
bind(choice.playlist)
|
||||||
|
isActivated = choice.alreadyAdded
|
||||||
|
}
|
||||||
|
binding.pickerName.text = choice.playlist.name.resolve(binding.context)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Create a new instance.
|
||||||
|
*
|
||||||
|
* @param parent The parent to inflate this instance from.
|
||||||
|
* @return A new instance.
|
||||||
|
*/
|
||||||
|
fun from(parent: View) =
|
||||||
|
PlaylistChoiceViewHolder(ItemPickerChoiceBinding.inflate(parent.context.inflater))
|
||||||
|
|
||||||
|
/** A comparator that can be used with DiffUtil. */
|
||||||
|
val DIFF_CALLBACK =
|
||||||
|
object : SimpleDiffCallback<PlaylistChoice>() {
|
||||||
|
override fun areContentsTheSame(oldItem: PlaylistChoice, newItem: PlaylistChoice) =
|
||||||
|
oldItem.playlist.name == newItem.playlist.name &&
|
||||||
|
oldItem.alreadyAdded == newItem.alreadyAdded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,8 +25,11 @@ import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.list.Item
|
||||||
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
|
import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,11 +48,20 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
|
||||||
val chosenName: StateFlow<ChosenName>
|
val chosenName: StateFlow<ChosenName>
|
||||||
get() = _chosenName
|
get() = _chosenName
|
||||||
|
|
||||||
|
private val _currentPendingSongs = MutableStateFlow<List<Song>?>(null)
|
||||||
|
val currentPendingSongs: StateFlow<List<Song>?>
|
||||||
|
get() = _currentPendingSongs
|
||||||
|
|
||||||
|
private val _playlistChoices = MutableStateFlow<List<PlaylistChoice>>(listOf())
|
||||||
|
val playlistChoices: StateFlow<List<PlaylistChoice>>
|
||||||
|
get() = _playlistChoices
|
||||||
|
|
||||||
init {
|
init {
|
||||||
musicRepository.addUpdateListener(this)
|
musicRepository.addUpdateListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
||||||
|
var refreshChoicesWith: List<Song>? = null
|
||||||
val deviceLibrary = musicRepository.deviceLibrary
|
val deviceLibrary = musicRepository.deviceLibrary
|
||||||
if (changes.deviceLibrary && deviceLibrary != null) {
|
if (changes.deviceLibrary && deviceLibrary != null) {
|
||||||
_currentPendingPlaylist.value =
|
_currentPendingPlaylist.value =
|
||||||
|
@ -58,6 +70,13 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
|
||||||
pendingPlaylist.preferredName,
|
pendingPlaylist.preferredName,
|
||||||
pendingPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.uid) })
|
pendingPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.uid) })
|
||||||
}
|
}
|
||||||
|
_currentPendingSongs.value =
|
||||||
|
_currentPendingSongs.value?.let { pendingSongs ->
|
||||||
|
pendingSongs
|
||||||
|
.mapNotNull { deviceLibrary.findSong(it.uid) }
|
||||||
|
.ifEmpty { null }
|
||||||
|
.also { refreshChoicesWith = it }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val chosenName = _chosenName.value
|
val chosenName = _chosenName.value
|
||||||
|
@ -69,7 +88,10 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
|
||||||
// Nothing to do.
|
// Nothing to do.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
refreshChoicesWith = refreshChoicesWith ?: _currentPendingSongs.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshChoicesWith?.let(::refreshPlaylistChoices)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
@ -80,7 +102,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
|
||||||
* Update the current [PendingPlaylist]. Will do nothing if already equal.
|
* Update the current [PendingPlaylist]. Will do nothing if already equal.
|
||||||
*
|
*
|
||||||
* @param context [Context] required to generate a playlist name.
|
* @param context [Context] required to generate a playlist name.
|
||||||
* @param songUids The list of [Music.UID] representing the songs to be present in the playlist.
|
* @param songUids The [Music.UID]s of songs to be present in the playlist.
|
||||||
*/
|
*/
|
||||||
fun setPendingPlaylist(context: Context, songUids: Array<Music.UID>) {
|
fun setPendingPlaylist(context: Context, songUids: Array<Music.UID>) {
|
||||||
if (currentPendingPlaylist.value?.songs?.map { it.uid } == songUids) {
|
if (currentPendingPlaylist.value?.songs?.map { it.uid } == songUids) {
|
||||||
|
@ -89,8 +111,8 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
|
||||||
}
|
}
|
||||||
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
||||||
val songs = songUids.mapNotNull(deviceLibrary::findSong)
|
val songs = songUids.mapNotNull(deviceLibrary::findSong)
|
||||||
val userLibrary = musicRepository.userLibrary ?: return
|
|
||||||
|
|
||||||
|
val userLibrary = musicRepository.userLibrary ?: return
|
||||||
var i = 1
|
var i = 1
|
||||||
while (true) {
|
while (true) {
|
||||||
val possibleName = context.getString(R.string.fmt_def_playlist, i)
|
val possibleName = context.getString(R.string.fmt_def_playlist, i)
|
||||||
|
@ -123,6 +145,43 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Confirm the playlist creation process as completed. */
|
||||||
|
fun confirmPlaylistCreation() {
|
||||||
|
// Confirm any playlist additions if needed, as the creation process may have been started
|
||||||
|
// by it and is still waiting on a result.
|
||||||
|
confirmPlaylistAddition()
|
||||||
|
_currentPendingPlaylist.value = null
|
||||||
|
_chosenName.value = ChosenName.Empty
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the current [Song]s that to show playlist add choices for. Will do nothing if already
|
||||||
|
* equal.
|
||||||
|
*
|
||||||
|
* @param songUids The [Music.UID]s of songs to add to a playlist.
|
||||||
|
*/
|
||||||
|
fun setPendingSongs(songUids: Array<Music.UID>) {
|
||||||
|
if (currentPendingSongs.value?.map { it.uid } == songUids) return
|
||||||
|
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
||||||
|
val songs = songUids.mapNotNull(deviceLibrary::findSong)
|
||||||
|
_currentPendingSongs.value = songs
|
||||||
|
refreshPlaylistChoices(songs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mark the addition process as complete. */
|
||||||
|
fun confirmPlaylistAddition() {
|
||||||
|
_currentPendingSongs.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshPlaylistChoices(songs: List<Song>) {
|
||||||
|
val userLibrary = musicRepository.userLibrary ?: return
|
||||||
|
_playlistChoices.value =
|
||||||
|
Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).playlists(userLibrary.playlists).map {
|
||||||
|
val songSet = it.songs.toSet()
|
||||||
|
PlaylistChoice(it, songs.all(songSet::contains))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -149,3 +208,13 @@ sealed interface ChosenName {
|
||||||
/** The current name only consists of whitespace. */
|
/** The current name only consists of whitespace. */
|
||||||
object Blank : ChosenName
|
object Blank : ChosenName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An individual [Playlist] choice to add [Song]s to.
|
||||||
|
*
|
||||||
|
* @param playlist The [Playlist] represented.
|
||||||
|
* @param alreadyAdded Whether the songs currently pending addition have already been added to the
|
||||||
|
* [Playlist].
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
data class PlaylistChoice(val playlist: Playlist, val alreadyAdded: Boolean) : Item
|
||||||
|
|
|
@ -107,7 +107,7 @@ private class UserLibraryImpl(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// TODO: Actually read playlists
|
// TODO: Actually read playlists
|
||||||
createPlaylist("Playlist 1", deviceLibrary.songs.slice(58..100))
|
createPlaylist("Playlist 1", deviceLibrary.songs.slice(58..200))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findPlaylist(uid: Music.UID) = playlistMap[uid]
|
override fun findPlaylist(uid: Music.UID) = playlistMap[uid]
|
||||||
|
|
|
@ -70,7 +70,7 @@ class NavigateToArtistDialog :
|
||||||
}
|
}
|
||||||
|
|
||||||
pickerModel.setArtistChoiceUid(args.itemUid)
|
pickerModel.setArtistChoiceUid(args.itemUid)
|
||||||
collectImmediately(pickerModel.currentArtistChoices) {
|
collectImmediately(pickerModel.artistChoices) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
choiceAdapter.update(it.choices, UpdateInstructions.Replace(0))
|
choiceAdapter.update(it.choices, UpdateInstructions.Replace(0))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -33,10 +33,10 @@ import org.oxycblt.auxio.music.*
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class NavigationPickerViewModel @Inject constructor(private val musicRepository: MusicRepository) :
|
class NavigationPickerViewModel @Inject constructor(private val musicRepository: MusicRepository) :
|
||||||
ViewModel(), MusicRepository.UpdateListener {
|
ViewModel(), MusicRepository.UpdateListener {
|
||||||
private val _currentArtistChoices = MutableStateFlow<ArtistNavigationChoices?>(null)
|
private val _artistChoices = MutableStateFlow<ArtistNavigationChoices?>(null)
|
||||||
/** The current set of [Artist] choices to show in the picker, or null if to show nothing. */
|
/** The current set of [Artist] choices to show in the picker, or null if to show nothing. */
|
||||||
val currentArtistChoices: StateFlow<ArtistNavigationChoices?>
|
val artistChoices: StateFlow<ArtistNavigationChoices?>
|
||||||
get() = _currentArtistChoices
|
get() = _artistChoices
|
||||||
|
|
||||||
init {
|
init {
|
||||||
musicRepository.addUpdateListener(this)
|
musicRepository.addUpdateListener(this)
|
||||||
|
@ -46,8 +46,8 @@ class NavigationPickerViewModel @Inject constructor(private val musicRepository:
|
||||||
if (!changes.deviceLibrary) return
|
if (!changes.deviceLibrary) return
|
||||||
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
||||||
// Need to sanitize different items depending on the current set of choices.
|
// Need to sanitize different items depending on the current set of choices.
|
||||||
_currentArtistChoices.value =
|
_artistChoices.value =
|
||||||
when (val choices = _currentArtistChoices.value) {
|
when (val choices = _artistChoices.value) {
|
||||||
is SongArtistNavigationChoices ->
|
is SongArtistNavigationChoices ->
|
||||||
deviceLibrary.findSong(choices.song.uid)?.let {
|
deviceLibrary.findSong(choices.song.uid)?.let {
|
||||||
SongArtistNavigationChoices(it)
|
SongArtistNavigationChoices(it)
|
||||||
|
@ -72,7 +72,7 @@ class NavigationPickerViewModel @Inject constructor(private val musicRepository:
|
||||||
*/
|
*/
|
||||||
fun setArtistChoiceUid(itemUid: Music.UID) {
|
fun setArtistChoiceUid(itemUid: Music.UID) {
|
||||||
// Support Songs and Albums, which have parent artists.
|
// Support Songs and Albums, which have parent artists.
|
||||||
_currentArtistChoices.value =
|
_artistChoices.value =
|
||||||
when (val music = musicRepository.find(itemUid)) {
|
when (val music = musicRepository.find(itemUid)) {
|
||||||
is Song -> SongArtistNavigationChoices(music)
|
is Song -> SongArtistNavigationChoices(music)
|
||||||
is Album -> AlbumArtistNavigationChoices(music)
|
is Album -> AlbumArtistNavigationChoices(music)
|
||||||
|
|
|
@ -256,12 +256,11 @@ constructor(
|
||||||
fun play(playlist: Playlist) = playImpl(null, playlist, false)
|
fun play(playlist: Playlist) = playImpl(null, playlist, false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play a [Music] selection.
|
* Play a list of [Song]s.
|
||||||
*
|
*
|
||||||
* @param selection The selection to play.
|
* @param songs The [Song]s to play.
|
||||||
*/
|
*/
|
||||||
fun play(selection: List<Music>) =
|
fun play(songs: List<Song>) = playbackManager.play(null, null, songs, false)
|
||||||
playbackManager.play(null, null, selectionToSongs(selection), false)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shuffle an [Album].
|
* Shuffle an [Album].
|
||||||
|
@ -292,12 +291,11 @@ constructor(
|
||||||
fun shuffle(playlist: Playlist) = playImpl(null, playlist, true)
|
fun shuffle(playlist: Playlist) = playImpl(null, playlist, true)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shuffle a [Music] selection.
|
* Shuffle a list of [Song]s.
|
||||||
*
|
*
|
||||||
* @param selection The selection to shuffle.
|
* @param songs The [Song]s to shuffle.
|
||||||
*/
|
*/
|
||||||
fun shuffle(selection: List<Music>) =
|
fun shuffle(songs: List<Song>) = playbackManager.play(null, null, songs, true)
|
||||||
playbackManager.play(null, null, selectionToSongs(selection), true)
|
|
||||||
|
|
||||||
private fun playImpl(
|
private fun playImpl(
|
||||||
song: Song?,
|
song: Song?,
|
||||||
|
@ -400,12 +398,12 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a selection to the top of the queue.
|
* Add [Song]s to the top of the queue.
|
||||||
*
|
*
|
||||||
* @param selection The [Music] selection to add.
|
* @param songs The [Song]s to add.
|
||||||
*/
|
*/
|
||||||
fun playNext(selection: List<Music>) {
|
fun playNext(songs: List<Song>) {
|
||||||
playbackManager.playNext(selectionToSongs(selection))
|
playbackManager.playNext(songs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -454,12 +452,12 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a selection to the end of the queue.
|
* Add [Song]s to the end of the queue.
|
||||||
*
|
*
|
||||||
* @param selection The [Music] selection to add.
|
* @param songs The [Song]s to add.
|
||||||
*/
|
*/
|
||||||
fun addToQueue(selection: List<Music>) {
|
fun addToQueue(songs: List<Song>) {
|
||||||
playbackManager.addToQueue(selectionToSongs(selection))
|
playbackManager.addToQueue(songs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- STATUS FUNCTIONS ---
|
// --- STATUS FUNCTIONS ---
|
||||||
|
@ -522,23 +520,4 @@ constructor(
|
||||||
onDone(false)
|
onDone(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert the given selection to a list of [Song]s.
|
|
||||||
*
|
|
||||||
* @param selection The selection of [Music] to convert.
|
|
||||||
* @return A [Song] list containing the child items of any [MusicParent] instances in the list
|
|
||||||
* alongside the unchanged [Song]s or the original selection.
|
|
||||||
*/
|
|
||||||
private fun selectionToSongs(selection: List<Music>): List<Song> {
|
|
||||||
return selection.flatMap {
|
|
||||||
when (it) {
|
|
||||||
is Song -> listOf(it)
|
|
||||||
is Album -> musicSettings.albumSongSort.songs(it.songs)
|
|
||||||
is Artist -> musicSettings.artistSongSort.songs(it.songs)
|
|
||||||
is Genre -> musicSettings.genreSongSort.songs(it.songs)
|
|
||||||
is Playlist -> musicSettings.playlistSongSort.songs(it.songs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ import org.oxycblt.auxio.util.*
|
||||||
class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
||||||
override val navModel: NavigationViewModel by activityViewModels()
|
override val navModel: NavigationViewModel by activityViewModels()
|
||||||
override val playbackModel: PlaybackViewModel by activityViewModels()
|
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
override val musicModel: MusicViewModel by activityViewModels()
|
||||||
override val selectionModel: SelectionViewModel by activityViewModels()
|
override val selectionModel: SelectionViewModel by activityViewModels()
|
||||||
private val searchModel: SearchViewModel by viewModels()
|
private val searchModel: SearchViewModel by viewModels()
|
||||||
private val searchAdapter = SearchAdapter(this)
|
private val searchAdapter = SearchAdapter(this)
|
||||||
|
@ -150,7 +151,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
||||||
is Album -> openMusicMenu(anchor, R.menu.menu_album_actions, item)
|
is Album -> openMusicMenu(anchor, R.menu.menu_album_actions, item)
|
||||||
is Artist -> openMusicMenu(anchor, R.menu.menu_parent_actions, item)
|
is Artist -> openMusicMenu(anchor, R.menu.menu_parent_actions, item)
|
||||||
is Genre -> openMusicMenu(anchor, R.menu.menu_parent_actions, item)
|
is Genre -> openMusicMenu(anchor, R.menu.menu_parent_actions, item)
|
||||||
is Playlist -> openMusicMenu(anchor, R.menu.menu_parent_actions, item)
|
is Playlist -> openMusicMenu(anchor, R.menu.menu_playlist_actions, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- TODO: Rename picker usages to choice usages now that the former is used more generally -->
|
||||||
<org.oxycblt.auxio.list.recycler.DialogRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
<org.oxycblt.auxio.list.recycler.DialogRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
|
35
app/src/main/res/layout/item_new_playlist_choice.xml
Normal file
35
app/src/main/res/layout/item_new_playlist_choice.xml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:paddingStart="@dimen/spacing_large"
|
||||||
|
android:paddingTop="@dimen/spacing_mid_medium"
|
||||||
|
android:paddingEnd="@dimen/spacing_large"
|
||||||
|
android:paddingBottom="@dimen/spacing_mid_medium">
|
||||||
|
|
||||||
|
<org.oxycblt.auxio.image.ImageGroup
|
||||||
|
android:id="@+id/picker_image"
|
||||||
|
style="@style/Widget.Auxio.Image.Small"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:staticIcon="@drawable/ic_add_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/picker_name"
|
||||||
|
style="@style/Widget.Auxio.TextView.Item.Primary"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/spacing_mid_medium"
|
||||||
|
android:textColor="@color/sel_selectable_text_primary"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/picker_image"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
android:text="@string/lbl_new_playlist" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -15,4 +15,7 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_go_artist"
|
android:id="@+id/action_go_artist"
|
||||||
android:title="@string/lbl_go_artist" />
|
android:title="@string/lbl_go_artist" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_playlist_add"
|
||||||
|
android:title="@string/lbl_playlist_add" />
|
||||||
</menu>
|
</menu>
|
|
@ -9,6 +9,9 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_go_artist"
|
android:id="@+id/action_go_artist"
|
||||||
android:title="@string/lbl_go_artist" />
|
android:title="@string/lbl_go_artist" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_playlist_add"
|
||||||
|
android:title="@string/lbl_playlist_add" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_song_detail"
|
android:id="@+id/action_song_detail"
|
||||||
android:title="@string/lbl_song_detail" />
|
android:title="@string/lbl_song_detail" />
|
||||||
|
|
|
@ -15,4 +15,7 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_queue_add"
|
android:id="@+id/action_queue_add"
|
||||||
android:title="@string/lbl_queue_add" />
|
android:title="@string/lbl_queue_add" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_playlist_add"
|
||||||
|
android:title="@string/lbl_playlist_add" />
|
||||||
</menu>
|
</menu>
|
|
@ -9,6 +9,9 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_go_album"
|
android:id="@+id/action_go_album"
|
||||||
android:title="@string/lbl_go_album" />
|
android:title="@string/lbl_go_album" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_playlist_add"
|
||||||
|
android:title="@string/lbl_playlist_add" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_song_detail"
|
android:id="@+id/action_song_detail"
|
||||||
android:title="@string/lbl_song_detail" />
|
android:title="@string/lbl_song_detail" />
|
||||||
|
|
|
@ -12,4 +12,7 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_queue_add"
|
android:id="@+id/action_queue_add"
|
||||||
android:title="@string/lbl_queue_add" />
|
android:title="@string/lbl_queue_add" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_playlist_add"
|
||||||
|
android:title="@string/lbl_playlist_add" />
|
||||||
</menu>
|
</menu>
|
|
@ -6,4 +6,7 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_queue_add"
|
android:id="@+id/action_queue_add"
|
||||||
android:title="@string/lbl_queue_add" />
|
android:title="@string/lbl_queue_add" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_playlist_add"
|
||||||
|
android:title="@string/lbl_playlist_add" />
|
||||||
</menu>
|
</menu>
|
15
app/src/main/res/menu/menu_playlist_actions.xml
Normal file
15
app/src/main/res/menu/menu_playlist_actions.xml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_play"
|
||||||
|
android:title="@string/lbl_play" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_shuffle"
|
||||||
|
android:title="@string/lbl_shuffle" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_play_next"
|
||||||
|
android:title="@string/lbl_play_next" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_queue_add"
|
||||||
|
android:title="@string/lbl_queue_add" />
|
||||||
|
</menu>
|
9
app/src/main/res/menu/menu_playlist_detail.xml
Normal file
9
app/src/main/res/menu/menu_playlist_detail.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_play_next"
|
||||||
|
android:title="@string/lbl_play_next" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_queue_add"
|
||||||
|
android:title="@string/lbl_queue_add" />
|
||||||
|
</menu>
|
|
@ -10,6 +10,9 @@
|
||||||
android:id="@+id/action_selection_queue_add"
|
android:id="@+id/action_selection_queue_add"
|
||||||
android:title="@string/lbl_queue_add"
|
android:title="@string/lbl_queue_add"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_selection_playlist_add"
|
||||||
|
android:title="@string/lbl_playlist_add" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_selection_play"
|
android:id="@+id/action_selection_play"
|
||||||
android:title="@string/lbl_play_selected"
|
android:title="@string/lbl_play_selected"
|
||||||
|
|
|
@ -12,6 +12,9 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_go_album"
|
android:id="@+id/action_go_album"
|
||||||
android:title="@string/lbl_go_album" />
|
android:title="@string/lbl_go_album" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_playlist_add"
|
||||||
|
android:title="@string/lbl_playlist_add" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_song_detail"
|
android:id="@+id/action_song_detail"
|
||||||
android:title="@string/lbl_song_detail" />
|
android:title="@string/lbl_song_detail" />
|
||||||
|
|
|
@ -20,6 +20,9 @@
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_new_playlist"
|
android:id="@+id/action_new_playlist"
|
||||||
app:destination="@id/new_playlist_dialog" />
|
app:destination="@id/new_playlist_dialog" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_add_to_playlist"
|
||||||
|
app:destination="@id/add_to_playlist_dialog" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_pick_navigation_artist"
|
android:id="@+id/action_pick_navigation_artist"
|
||||||
app:destination="@id/navigate_to_artist_dialog" />
|
app:destination="@id/navigate_to_artist_dialog" />
|
||||||
|
@ -51,6 +54,19 @@
|
||||||
app:argType="org.oxycblt.auxio.music.Music$UID[]" />
|
app:argType="org.oxycblt.auxio.music.Music$UID[]" />
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<dialog
|
||||||
|
android:id="@+id/add_to_playlist_dialog"
|
||||||
|
android:name="org.oxycblt.auxio.music.picker.AddToPlaylistDialog"
|
||||||
|
android:label="new_playlist_dialog"
|
||||||
|
tools:layout="@layout/dialog_playlist_name">
|
||||||
|
<argument
|
||||||
|
android:name="songUids"
|
||||||
|
app:argType="org.oxycblt.auxio.music.Music$UID[]" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_new_playlist"
|
||||||
|
app:destination="@id/new_playlist_dialog" />
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<dialog
|
<dialog
|
||||||
android:id="@+id/navigate_to_artist_dialog"
|
android:id="@+id/navigate_to_artist_dialog"
|
||||||
android:name="org.oxycblt.auxio.navigation.picker.NavigateToArtistDialog"
|
android:name="org.oxycblt.auxio.navigation.picker.NavigateToArtistDialog"
|
||||||
|
@ -155,12 +171,12 @@
|
||||||
tools:layout="@layout/dialog_pre_amp" />
|
tools:layout="@layout/dialog_pre_amp" />
|
||||||
<dialog
|
<dialog
|
||||||
android:id="@+id/music_dirs_dialog"
|
android:id="@+id/music_dirs_dialog"
|
||||||
android:name="org.oxycblt.auxio.music.config.MusicDirsDialog"
|
android:name="org.oxycblt.auxio.music.fs.MusicDirsDialog"
|
||||||
android:label="music_dirs_dialog"
|
android:label="music_dirs_dialog"
|
||||||
tools:layout="@layout/dialog_music_dirs" />
|
tools:layout="@layout/dialog_music_dirs" />
|
||||||
<dialog
|
<dialog
|
||||||
android:id="@+id/separators_dialog"
|
android:id="@+id/separators_dialog"
|
||||||
android:name="org.oxycblt.auxio.music.config.SeparatorsDialog"
|
android:name="org.oxycblt.auxio.music.metadata.SeparatorsDialog"
|
||||||
android:label="music_dirs_dialog"
|
android:label="music_dirs_dialog"
|
||||||
tools:layout="@layout/dialog_separators" />
|
tools:layout="@layout/dialog_separators" />
|
||||||
|
|
||||||
|
|
|
@ -111,6 +111,8 @@
|
||||||
<string name="lbl_play_next">Play next</string>
|
<string name="lbl_play_next">Play next</string>
|
||||||
<string name="lbl_queue_add">Add to queue</string>
|
<string name="lbl_queue_add">Add to queue</string>
|
||||||
|
|
||||||
|
<string name="lbl_playlist_add">Add to playlist</string>
|
||||||
|
|
||||||
<string name="lbl_go_artist">Go to artist</string>
|
<string name="lbl_go_artist">Go to artist</string>
|
||||||
<string name="lbl_go_album">Go to album</string>
|
<string name="lbl_go_album">Go to album</string>
|
||||||
<string name="lbl_song_detail">View properties</string>
|
<string name="lbl_song_detail">View properties</string>
|
||||||
|
@ -160,6 +162,8 @@
|
||||||
<string name="lng_indexing">Loading your music library…</string>
|
<string name="lng_indexing">Loading your music library…</string>
|
||||||
<string name="lng_observing">Monitoring your music library for changes…</string>
|
<string name="lng_observing">Monitoring your music library for changes…</string>
|
||||||
<string name="lng_queue_added">Added to queue</string>
|
<string name="lng_queue_added">Added to queue</string>
|
||||||
|
<string name="lng_playlist_created">Playlist created</string>
|
||||||
|
<string name="lng_playlist_added">Added to playlist</string>
|
||||||
<string name="lng_author">Developed by Alexander Capehart</string>
|
<string name="lng_author">Developed by Alexander Capehart</string>
|
||||||
<!-- As in music library -->
|
<!-- As in music library -->
|
||||||
<string name="lng_search_library">Search your library…</string>
|
<string name="lng_search_library">Search your library…</string>
|
||||||
|
|
Loading…
Reference in a new issue