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:
Alexander Capehart 2022-11-19 16:12:04 -07:00
parent 086f7836bd
commit c13a57f694
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
24 changed files with 264 additions and 211 deletions

View file

@ -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 =

View file

@ -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?) {

View file

@ -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}")
}
}

View file

@ -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}")

View file

@ -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 {

View file

@ -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}")
}
}

View file

@ -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 ->

View file

@ -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 = ""

View file

@ -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)
}
}

View file

@ -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)
}
}
}
}

View file

@ -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)
}
}
}

View file

@ -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 }
}
}

View file

@ -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
}

View file

@ -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}")
}
}
}
}

View file

@ -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

View file

@ -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" }
playbackManager.play(song, artist, settings)
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. */

View file

@ -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

View file

@ -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)

View file

@ -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()
}

View file

@ -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

View file

@ -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")

View file

@ -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")

View file

@ -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

View file

@ -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)