music: rework picker system
Rework the music picker system to be a reactive, viewmodel-based system instead of a janky UI system. This should make it much easier to maintain and extend in the future.
This commit is contained in:
parent
086f7836bd
commit
c13a57f694
24 changed files with 264 additions and 211 deletions
|
@ -34,6 +34,7 @@ import com.google.android.material.transition.MaterialFadeThrough
|
|||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackSheetBehavior
|
||||
|
@ -124,7 +125,9 @@ class MainFragment :
|
|||
|
||||
collect(navModel.mainNavigationAction, ::handleMainNavigation)
|
||||
collect(navModel.exploreNavigationItem, ::handleExploreNavigation)
|
||||
collect(navModel.exploreNavigationArtists, ::handleExplorePicker)
|
||||
collectImmediately(playbackModel.song, ::updateSong)
|
||||
collect(playbackModel.artistPlaybackPickerSong, ::handlePlaybackPicker)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -213,14 +216,6 @@ class MainFragment :
|
|||
return true
|
||||
}
|
||||
|
||||
private fun updateSong(song: Song?) {
|
||||
if (song != null) {
|
||||
tryUnhideAll()
|
||||
} else {
|
||||
tryHideAll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMainNavigation(action: MainNavigationAction?) {
|
||||
if (action == null) return
|
||||
|
||||
|
@ -239,6 +234,32 @@ class MainFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleExplorePicker(items: List<Artist>?) {
|
||||
if (items != null) {
|
||||
navModel.mainNavigateTo(MainNavigationAction.Directions(
|
||||
MainFragmentDirections.actionPickNavigationArtist(items.map { it.uid }.toTypedArray())
|
||||
))
|
||||
navModel.finishExploreNavigation()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSong(song: Song?) {
|
||||
if (song != null) {
|
||||
tryUnhideAll()
|
||||
} else {
|
||||
tryHideAll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePlaybackPicker(song: Song?) {
|
||||
if (song != null) {
|
||||
navModel.mainNavigateTo(MainNavigationAction.Directions(
|
||||
MainFragmentDirections.actionPickPlaybackArtist(song.uid)
|
||||
))
|
||||
playbackModel.finishPlaybackArtistPicker()
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryExpandAll() {
|
||||
val binding = requireBinding()
|
||||
val playbackSheetBehavior =
|
||||
|
|
|
@ -39,9 +39,8 @@ import org.oxycblt.auxio.music.MusicMode
|
|||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.music.picker.PickerMode
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.fragment.MusicFragment
|
||||
import org.oxycblt.auxio.ui.fragment.MenuFragment
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.util.canScroll
|
||||
import org.oxycblt.auxio.util.collect
|
||||
|
@ -56,7 +55,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
class AlbumDetailFragment :
|
||||
MusicFragment<FragmentDetailBinding>(),
|
||||
MenuFragment<FragmentDetailBinding>(),
|
||||
Toolbar.OnMenuItemClickListener,
|
||||
AlbumDetailAdapter.Listener {
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
|
@ -127,7 +126,7 @@ class AlbumDetailFragment :
|
|||
null,
|
||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
|
||||
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
||||
else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}")
|
||||
}
|
||||
}
|
||||
|
@ -164,7 +163,7 @@ class AlbumDetailFragment :
|
|||
}
|
||||
|
||||
override fun onNavigateToArtist() {
|
||||
navigateToArtist(unlikelyToBeNull(detailModel.currentAlbum.value))
|
||||
navModel.exploreNavigateTo(unlikelyToBeNull(detailModel.currentAlbum.value).artists)
|
||||
}
|
||||
|
||||
private fun handleItemChange(album: Album?) {
|
||||
|
|
|
@ -37,9 +37,8 @@ import org.oxycblt.auxio.music.MusicMode
|
|||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.music.picker.PickerMode
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.fragment.MusicFragment
|
||||
import org.oxycblt.auxio.ui.fragment.MenuFragment
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.util.collect
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
|
@ -53,9 +52,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
class ArtistDetailFragment :
|
||||
MusicFragment<FragmentDetailBinding>(),
|
||||
Toolbar.OnMenuItemClickListener,
|
||||
DetailAdapter.Listener {
|
||||
MenuFragment<FragmentDetailBinding>(), Toolbar.OnMenuItemClickListener, DetailAdapter.Listener {
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
|
||||
private val args: ArtistDetailFragmentArgs by navArgs()
|
||||
|
@ -123,7 +120,7 @@ class ArtistDetailFragment :
|
|||
item, unlikelyToBeNull(detailModel.currentArtist.value))
|
||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
|
||||
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
||||
else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,9 +38,8 @@ import org.oxycblt.auxio.music.MusicMode
|
|||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.music.picker.PickerMode
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.fragment.MusicFragment
|
||||
import org.oxycblt.auxio.ui.fragment.MenuFragment
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.util.collect
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
|
@ -54,9 +53,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
class GenreDetailFragment :
|
||||
MusicFragment<FragmentDetailBinding>(),
|
||||
Toolbar.OnMenuItemClickListener,
|
||||
DetailAdapter.Listener {
|
||||
MenuFragment<FragmentDetailBinding>(), Toolbar.OnMenuItemClickListener, DetailAdapter.Listener {
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
|
||||
private val args: GenreDetailFragmentArgs by navArgs()
|
||||
|
@ -125,7 +122,7 @@ class GenreDetailFragment :
|
|||
item, unlikelyToBeNull(detailModel.currentGenre.value))
|
||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
|
||||
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
||||
else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}")
|
||||
}
|
||||
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
||||
|
|
|
@ -23,7 +23,7 @@ import androidx.fragment.app.Fragment
|
|||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.home.HomeViewModel
|
||||
import org.oxycblt.auxio.ui.fastscroll.FastScrollRecyclerView
|
||||
import org.oxycblt.auxio.ui.fragment.MusicFragment
|
||||
import org.oxycblt.auxio.ui.fragment.MenuFragment
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||
|
@ -33,7 +33,7 @@ import org.oxycblt.auxio.util.androidActivityViewModels
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
abstract class HomeListFragment<T : Item> :
|
||||
MusicFragment<FragmentHomeListBinding>(),
|
||||
MenuFragment<FragmentHomeListBinding>(),
|
||||
MenuItemListener,
|
||||
FastScrollRecyclerView.PopupProvider,
|
||||
FastScrollRecyclerView.OnFastScrollListener {
|
||||
|
|
|
@ -28,7 +28,6 @@ import org.oxycblt.auxio.music.MusicMode
|
|||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.music.picker.PickerMode
|
||||
import org.oxycblt.auxio.playback.formatDurationMs
|
||||
import org.oxycblt.auxio.playback.secsToMs
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
|
@ -110,7 +109,7 @@ class SongListFragment : HomeListFragment<Song>() {
|
|||
when (settings.libPlaybackMode) {
|
||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
|
||||
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
||||
else -> error("Unexpected playback mode: ${settings.libPlaybackMode}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -204,8 +204,8 @@ abstract class MediaStoreExtractor(
|
|||
|
||||
// Since we can't obtain the genre tag from a song query, we must construct
|
||||
// our own equivalent from genre database queries. Theoretically, this isn't
|
||||
// needed since MetadataLayer will fill this in for us, but there are some
|
||||
// obscure formats where genre support is only really covered by this.
|
||||
// needed since MetadataLayer will fill this in for us, but I'd imagine there
|
||||
// are some obscure formats where genre support is only really covered by this.
|
||||
context.contentResolverSafe.useQuery(
|
||||
MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
|
||||
arrayOf(MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME)) { genreCursor ->
|
||||
|
|
|
@ -51,7 +51,7 @@ fun String.parseYear() = toIntOrNull()?.toDate()
|
|||
/** Parse an ISO-8601 time-stamp from this field into a [Date]. */
|
||||
fun String.parseTimestamp() = Date.from(this)
|
||||
|
||||
/** Parse a string by [selector], also handling string escaping. */
|
||||
/** Split a string by [selector], also handling escaping. */
|
||||
inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList<String> {
|
||||
val split = mutableListOf<String>()
|
||||
var currentString = ""
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Auxio Project
|
||||
*
|
||||
* 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 androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
|
||||
/**
|
||||
* The [ArtistPickerDialog] for ambiguous artist navigation operations.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class ArtistNavigationPickerDialog : ArtistPickerDialog() {
|
||||
private val navModel: NavigationViewModel by activityViewModels()
|
||||
private val args: ArtistNavigationPickerDialogArgs by navArgs()
|
||||
|
||||
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
|
||||
pickerModel.setArtistUids(args.artistUids)
|
||||
super.onBindingCreated(binding, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
super.onItemClick(item)
|
||||
check(item is Music) { "Unexpected datatype: ${item::class.simpleName}" }
|
||||
navModel.exploreNavigateTo(item)
|
||||
}
|
||||
}
|
|
@ -20,38 +20,19 @@ 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.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.ItemClickListener
|
||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
|
||||
/**
|
||||
* A dialog that shows several artist options if the result of an artist-reliant operation is
|
||||
* ambiguous.
|
||||
* @author OxygenCobalt
|
||||
*
|
||||
* TODO: Clean up the picker flow to reduce the amount of duplication I had to do.
|
||||
*/
|
||||
class ArtistPickerDialog :
|
||||
abstract class ArtistPickerDialog :
|
||||
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ItemClickListener {
|
||||
private val pickerModel: PickerViewModel by viewModels()
|
||||
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
||||
private val navModel: NavigationViewModel by activityViewModels()
|
||||
|
||||
private val args: ArtistPickerDialogArgs by navArgs()
|
||||
private val adapter = ArtistChoiceAdapter(this)
|
||||
protected val pickerModel: MusicPickerViewModel by viewModels()
|
||||
private val artistAdapter = ArtistChoiceAdapter(this)
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||
DialogMusicPickerBinding.inflate(inflater)
|
||||
|
@ -61,16 +42,12 @@ class ArtistPickerDialog :
|
|||
}
|
||||
|
||||
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
|
||||
pickerModel.setSongUid(args.uid)
|
||||
|
||||
binding.pickerRecycler.adapter = adapter
|
||||
|
||||
collectImmediately(pickerModel.currentItem) { item ->
|
||||
when (item) {
|
||||
is Song -> adapter.submitList(item.artists)
|
||||
is Album -> adapter.submitList(item.artists)
|
||||
null -> findNavController().navigateUp()
|
||||
else -> error("Invalid datatype: ${item::class.java}")
|
||||
binding.pickerRecycler.adapter = artistAdapter
|
||||
collectImmediately(pickerModel.currentArtists) { artists ->
|
||||
if (artists != null) {
|
||||
artistAdapter.submitList(artists)
|
||||
} else {
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,15 +57,6 @@ class ArtistPickerDialog :
|
|||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
check(item is Artist) { "Unexpected datatype: ${item::class.simpleName}" }
|
||||
findNavController().navigateUp()
|
||||
when (args.pickerMode) {
|
||||
PickerMode.SHOW -> navModel.exploreNavigateTo(item)
|
||||
PickerMode.PLAY -> {
|
||||
val currentItem = pickerModel.currentItem.value
|
||||
check(currentItem is Song) { "PickerMode.PLAY is only allowed with Songs" }
|
||||
playbackModel.playFromArtist(currentItem, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Auxio Project
|
||||
*
|
||||
* 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 androidx.navigation.fragment.navArgs
|
||||
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
||||
/**
|
||||
* The [ArtistPickerDialog] for ambiguous artist playback operations.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class ArtistPlaybackPickerDialog : ArtistPickerDialog() {
|
||||
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
||||
private val args: ArtistPlaybackPickerDialogArgs by navArgs()
|
||||
|
||||
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
|
||||
pickerModel.setSongUid(args.songUid)
|
||||
super.onBindingCreated(binding, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
super.onItemClick(item)
|
||||
check(item is Artist) { "Unexpected datatype: ${item::class.simpleName}" }
|
||||
pickerModel.currentSong.value?.let { song ->
|
||||
playbackModel.playFromArtist(song, item)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package org.oxycblt.auxio.music.picker
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
||||
class MusicPickerViewModel : ViewModel() {
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
|
||||
private val _currentSong = MutableStateFlow<Song?>(null)
|
||||
val currentSong: StateFlow<Song?> get() = _currentSong
|
||||
|
||||
private val _currentArtists = MutableStateFlow<List<Artist>?>(null)
|
||||
val currentArtists: StateFlow<List<Artist>?> get() = _currentArtists
|
||||
|
||||
fun setSongUid(uid: Music.UID) {
|
||||
val library = unlikelyToBeNull(musicStore.library)
|
||||
_currentSong.value = library.find(uid)
|
||||
_currentArtists.value = _currentSong.value?.artists
|
||||
}
|
||||
|
||||
fun setArtistUids(uids: Array<Music.UID>) {
|
||||
val library = unlikelyToBeNull(musicStore.library)
|
||||
_currentArtists.value = uids.mapNotNull { library.find<Artist>(it) }.ifEmpty { null }
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Auxio Project
|
||||
*
|
||||
* 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
|
||||
|
||||
/** Represents the actions available to the picker UI. */
|
||||
enum class PickerMode {
|
||||
PLAY,
|
||||
SHOW
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Auxio Project
|
||||
*
|
||||
* 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 androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
||||
/**
|
||||
* A small ViewModel holding and updating the current song being shown in the picker UI.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class PickerViewModel : ViewModel(), MusicStore.Callback {
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
|
||||
private var _currentItem = MutableStateFlow<Music?>(null)
|
||||
val currentItem: StateFlow<Music?> = _currentItem
|
||||
|
||||
fun setSongUid(uid: Music.UID) {
|
||||
if (_currentItem.value?.uid == uid) return
|
||||
val library = unlikelyToBeNull(musicStore.library)
|
||||
val item = requireNotNull(library.find(uid)) { "Invalid song id provided" }
|
||||
_currentItem.value = item
|
||||
}
|
||||
|
||||
override fun onLibraryChanged(library: MusicStore.Library?) {
|
||||
if (library != null) {
|
||||
when (val item = currentItem.value) {
|
||||
is Song -> {
|
||||
_currentItem.value = library.sanitize(item)
|
||||
}
|
||||
is Album -> {
|
||||
_currentItem.value = library.sanitize(item)
|
||||
}
|
||||
null -> {}
|
||||
else -> error("Invalid datatype: ${item::class.java}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,11 +32,10 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.picker.PickerMode
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.playback.ui.StyledSeekBar
|
||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||
import org.oxycblt.auxio.ui.fragment.MusicFragment
|
||||
import org.oxycblt.auxio.ui.fragment.MenuFragment
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.showToast
|
||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||
|
@ -49,7 +48,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
|||
* TODO: Make seek thumb grow when selected
|
||||
*/
|
||||
class PlaybackPanelFragment :
|
||||
MusicFragment<FragmentPlaybackPanelBinding>(),
|
||||
MenuFragment<FragmentPlaybackPanelBinding>(),
|
||||
StyledSeekBar.Callback,
|
||||
Toolbar.OnMenuItemClickListener {
|
||||
// AudioEffect expects you to use startActivityForResult with the panel intent. Use
|
||||
|
@ -207,7 +206,7 @@ class PlaybackPanelFragment :
|
|||
|
||||
private fun showCurrentArtist() {
|
||||
val song = playbackModel.song.value ?: return
|
||||
doArtistDependentAction(song, PickerMode.SHOW)
|
||||
navModel.exploreNavigateTo(song.artists)
|
||||
}
|
||||
private fun showCurrentAlbum() {
|
||||
val song = playbackModel.song.value ?: return
|
||||
|
|
|
@ -77,11 +77,18 @@ class PlaybackViewModel(application: Application) :
|
|||
val isShuffled: StateFlow<Boolean>
|
||||
get() = _isShuffled
|
||||
|
||||
/** The current ID of the app's audio session. */
|
||||
val currentAudioSessionId: Int?
|
||||
get() = playbackManager.currentAudioSessionId
|
||||
|
||||
private var lastPositionJob: Job? = null
|
||||
|
||||
private val _artistPlaybackPickerSong = MutableStateFlow<Song?>(null)
|
||||
|
||||
/** Flag for resolving an ambiguous artist choice when playing from a song's artists. */
|
||||
val artistPlaybackPickerSong: StateFlow<Song?>
|
||||
get() = _artistPlaybackPickerSong
|
||||
|
||||
init {
|
||||
playbackManager.addCallback(this)
|
||||
}
|
||||
|
@ -104,9 +111,22 @@ class PlaybackViewModel(application: Application) :
|
|||
}
|
||||
|
||||
/** Play a song from it's artist. */
|
||||
fun playFromArtist(song: Song, artist: Artist) {
|
||||
check(artist.songs.contains(song)) { "Invalid input: Artist is not linked to song" }
|
||||
fun playFromArtist(song: Song, artist: Artist? = null) {
|
||||
if (artist != null) {
|
||||
check(artist in song.artists) { "Artist not in song artists" }
|
||||
playbackManager.play(song, artist, settings)
|
||||
} else {
|
||||
if (song.artists.size == 1) {
|
||||
playbackManager.play(song, song.artists[0], settings)
|
||||
} else {
|
||||
_artistPlaybackPickerSong.value = song
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Complete the picker opening process when playing from an artist. */
|
||||
fun finishPlaybackArtistPicker() {
|
||||
_artistPlaybackPickerSong.value = null
|
||||
}
|
||||
|
||||
/** Play a song from the specific genre that contains the song. */
|
||||
|
|
|
@ -71,6 +71,8 @@ import org.oxycblt.auxio.widgets.WidgetProvider
|
|||
* not the source of truth for the state, but rather the means to control system-side playback. Both
|
||||
* of those tasks are what [PlaybackStateManager] is for.
|
||||
*
|
||||
* TODO: Refactor lifecycle to run completely headless (i.e no activity needed)
|
||||
*
|
||||
* TODO: Android Auto
|
||||
*
|
||||
* @author OxygenCobalt
|
||||
|
|
|
@ -38,9 +38,8 @@ import org.oxycblt.auxio.music.Music
|
|||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.picker.PickerMode
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.fragment.MusicFragment
|
||||
import org.oxycblt.auxio.ui.fragment.MenuFragment
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.util.androidViewModels
|
||||
|
@ -55,7 +54,7 @@ import org.oxycblt.auxio.util.logW
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
class SearchFragment :
|
||||
MusicFragment<FragmentSearchBinding>(), MenuItemListener, Toolbar.OnMenuItemClickListener {
|
||||
MenuFragment<FragmentSearchBinding>(), MenuItemListener, Toolbar.OnMenuItemClickListener {
|
||||
|
||||
// SearchViewModel is only scoped to this Fragment
|
||||
private val searchModel: SearchViewModel by androidViewModels()
|
||||
|
@ -149,7 +148,7 @@ class SearchFragment :
|
|||
when (settings.libPlaybackMode) {
|
||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
|
||||
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
||||
else -> error("Unexpected playback mode: ${settings.libPlaybackMode}")
|
||||
}
|
||||
is MusicParent -> navModel.exploreNavigateTo(item)
|
||||
|
|
|
@ -21,6 +21,7 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.navigation.NavDirections
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
|
@ -44,6 +45,15 @@ class NavigationViewModel : ViewModel() {
|
|||
val exploreNavigationItem: StateFlow<Music?>
|
||||
get() = _exploreNavigationItem
|
||||
|
||||
private val _exploreNavigationArtists = MutableStateFlow<List<Artist>?>(null)
|
||||
|
||||
/**
|
||||
* Flag for navigation within the explore fragments. In this case, it involves an ambiguous list
|
||||
* of artist choices.
|
||||
*/
|
||||
val exploreNavigationArtists: StateFlow<List<Artist>?>
|
||||
get() = _exploreNavigationArtists
|
||||
|
||||
/** Notify MainFragment to navigate to the location outlined in [MainNavigationAction]. */
|
||||
fun mainNavigateTo(action: MainNavigationAction) {
|
||||
if (_mainNavigationAction.value != null) {
|
||||
|
@ -72,10 +82,26 @@ class NavigationViewModel : ViewModel() {
|
|||
_exploreNavigationItem.value = item
|
||||
}
|
||||
|
||||
/** Navigate to one item out of a list of items. */
|
||||
fun exploreNavigateTo(items: List<Artist>) {
|
||||
if (_exploreNavigationArtists.value != null) {
|
||||
logD("Already navigating, not doing explore action")
|
||||
return
|
||||
}
|
||||
|
||||
if (items.size == 1) {
|
||||
exploreNavigateTo(items[0])
|
||||
} else {
|
||||
logD("Navigating to a choice of ${items.map { it.rawName }}")
|
||||
_exploreNavigationArtists.value = items
|
||||
}
|
||||
}
|
||||
|
||||
/** Mark that the item navigation process is done. */
|
||||
fun finishExploreNavigation() {
|
||||
logD("Finishing explore navigation process")
|
||||
_exploreNavigationItem.value = null
|
||||
_exploreNavigationArtists.value = null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,5 +117,6 @@ sealed class MainNavigationAction {
|
|||
/** Collapse the playback panel. */
|
||||
object Collapse : MainNavigationAction()
|
||||
|
||||
/** Provide raw navigation directions. */
|
||||
data class Directions(val directions: NavDirections) : MainNavigationAction()
|
||||
}
|
||||
|
|
|
@ -67,6 +67,8 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
|||
*
|
||||
* TODO: Add vibration when popup changes
|
||||
*
|
||||
* TODO: Improve this for variably sized items
|
||||
*
|
||||
* @author Hai Zhang, OxygenCobalt
|
||||
*/
|
||||
class FastScrollRecyclerView
|
||||
|
|
|
@ -29,7 +29,6 @@ import org.oxycblt.auxio.music.Album
|
|||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.picker.PickerMode
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
|
@ -42,40 +41,12 @@ import org.oxycblt.auxio.util.showToast
|
|||
* preventing UI issues and memory leaks.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
abstract class MusicFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
||||
abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
||||
private var currentMenu: PopupMenu? = null
|
||||
|
||||
protected val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
||||
protected val navModel: NavigationViewModel by activityViewModels()
|
||||
|
||||
/**
|
||||
* Run the UI flow to perform a specific [PickerMode] action with a particular artist from
|
||||
* [song].
|
||||
*/
|
||||
fun doArtistDependentAction(song: Song, mode: PickerMode) {
|
||||
if (song.artists.size == 1) {
|
||||
when (mode) {
|
||||
PickerMode.PLAY -> playbackModel.playFromArtist(song, song.artists[0])
|
||||
PickerMode.SHOW -> navModel.exploreNavigateTo(song.artists[0])
|
||||
}
|
||||
} else {
|
||||
navModel.mainNavigateTo(
|
||||
MainNavigationAction.Directions(
|
||||
MainFragmentDirections.actionPickArtist(song.uid, mode)))
|
||||
}
|
||||
}
|
||||
|
||||
/** Run the UI flow to navigate to a particular artist from [album]. */
|
||||
fun navigateToArtist(album: Album) {
|
||||
if (album.artists.size == 1) {
|
||||
navModel.exploreNavigateTo(album.artists[0])
|
||||
} else {
|
||||
navModel.mainNavigateTo(
|
||||
MainNavigationAction.Directions(
|
||||
MainFragmentDirections.actionPickArtist(album.uid, PickerMode.SHOW)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the given menu in context of [song]. Assumes that the menu is only composed of common
|
||||
* [Song] options.
|
||||
|
@ -94,7 +65,7 @@ abstract class MusicFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
|||
requireContext().showToast(R.string.lng_queue_added)
|
||||
}
|
||||
R.id.action_go_artist -> {
|
||||
doArtistDependentAction(song, PickerMode.SHOW)
|
||||
navModel.exploreNavigateTo(song.artists)
|
||||
}
|
||||
R.id.action_go_album -> {
|
||||
navModel.exploreNavigateTo(song.album)
|
||||
|
@ -137,7 +108,7 @@ abstract class MusicFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
|||
requireContext().showToast(R.string.lng_queue_added)
|
||||
}
|
||||
R.id.action_go_artist -> {
|
||||
navigateToArtist(album)
|
||||
navModel.exploreNavigateTo(album.artists)
|
||||
}
|
||||
else -> {
|
||||
error("Unexpected menu item selected")
|
|
@ -65,8 +65,6 @@ class WidgetComponent(private val context: Context) :
|
|||
// 1. We can't use the typical primitives like ViewModels
|
||||
// 2. The component range is far smaller, so we have to do some odd hacks to get
|
||||
// the same UX.
|
||||
// 3. RemoteView memory is limited, so we want to batch updates as much as physically
|
||||
// possible.
|
||||
val song = playbackManager.song
|
||||
if (song == null) {
|
||||
logD("No song, resetting widget")
|
||||
|
|
|
@ -18,22 +18,31 @@
|
|||
android:id="@+id/action_show_details"
|
||||
app:destination="@id/song_detail_dialog" />
|
||||
<action
|
||||
android:id="@+id/action_pick_artist"
|
||||
app:destination="@id/artist_picker_dialog" />
|
||||
android:id="@+id/action_pick_playback_artist"
|
||||
app:destination="@id/artist_playback_picker_dialog" />
|
||||
<action
|
||||
android:id="@+id/action_pick_navigation_artist"
|
||||
app:destination="@id/artist_navigation_picker_dialog" />
|
||||
</fragment>
|
||||
|
||||
|
||||
<dialog
|
||||
android:id="@+id/artist_picker_dialog"
|
||||
android:name="org.oxycblt.auxio.music.picker.ArtistPickerDialog"
|
||||
android:label="artist_picker_dialog"
|
||||
android:id="@+id/artist_playback_picker_dialog"
|
||||
android:name="org.oxycblt.auxio.music.picker.ArtistPlaybackPickerDialog"
|
||||
android:label="artist_playback_picker_dialog"
|
||||
tools:layout="@layout/dialog_music_picker">
|
||||
<argument
|
||||
android:name="uid"
|
||||
android:name="songUid"
|
||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/artist_navigation_picker_dialog"
|
||||
android:name="org.oxycblt.auxio.music.picker.ArtistNavigationPickerDialog"
|
||||
android:label="artist_navigation_picker_dialog"
|
||||
tools:layout="@layout/dialog_music_picker">
|
||||
<argument
|
||||
android:name="pickerMode"
|
||||
app:argType="org.oxycblt.auxio.music.picker.PickerMode" />
|
||||
android:name="artistUids"
|
||||
app:argType="org.oxycblt.auxio.music.Music$UID[]" />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
|
|
|
@ -29,6 +29,7 @@ OK="\033[1;92m"
|
|||
NC="\033[0m"
|
||||
|
||||
# We do some shell scripting later on, so we can't support windows.
|
||||
# TODO: Support windows
|
||||
system = platform.system()
|
||||
if system not in ["Linux", "Darwin"]:
|
||||
print("fatal: unsupported platform " + system)
|
||||
|
|
Loading…
Reference in a new issue