diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 7d750a43e..375472363 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -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?) { + 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 = diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 291607ab8..2dba17668 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -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(), + MenuFragment(), 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?) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 9ae99370c..b40b69ab1 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -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(), - Toolbar.OnMenuItemClickListener, - DetailAdapter.Listener { + MenuFragment(), 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}") } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 5eaadc61b..c3018be04 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -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(), - Toolbar.OnMenuItemClickListener, - DetailAdapter.Listener { + MenuFragment(), 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}") diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt index f952d72cc..c95ccfed2 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt @@ -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 : - MusicFragment(), + MenuFragment(), MenuItemListener, FastScrollRecyclerView.PopupProvider, FastScrollRecyclerView.OnFastScrollListener { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 0695f8448..629541d4d 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -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() { 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}") } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt index 908131d15..8aaf32fe1 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt @@ -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 -> diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt index 9b58f798e..1ea46176c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt @@ -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 { val split = mutableListOf() var currentString = "" diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistNavigationPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistNavigationPickerDialog.kt new file mode 100644 index 000000000..3a6e6ad35 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistNavigationPickerDialog.kt @@ -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 . + */ + +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) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt index 477a5ae6f..48b8da362 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt @@ -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(), 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) - } - } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPlaybackPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPlaybackPickerDialog.kt new file mode 100644 index 000000000..24796243d --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPlaybackPickerDialog.kt @@ -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 . + */ + +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) + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/MusicPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/MusicPickerViewModel.kt new file mode 100644 index 000000000..186dacb6e --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/MusicPickerViewModel.kt @@ -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(null) + val currentSong: StateFlow get() = _currentSong + + private val _currentArtists = MutableStateFlow?>(null) + val currentArtists: StateFlow?> 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) { + val library = unlikelyToBeNull(musicStore.library) + _currentArtists.value = uids.mapNotNull { library.find(it) }.ifEmpty { null } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/PickerMode.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/PickerMode.kt deleted file mode 100644 index b5207cd8c..000000000 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/PickerMode.kt +++ /dev/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 . - */ - -package org.oxycblt.auxio.music.picker - -/** Represents the actions available to the picker UI. */ -enum class PickerMode { - PLAY, - SHOW -} diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt deleted file mode 100644 index db51bd0a6..000000000 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt +++ /dev/null @@ -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 . - */ - -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(null) - val currentItem: StateFlow = _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}") - } - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 3cd15aa56..807b72a66 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -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(), + MenuFragment(), 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 diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 4e95744bf..e8b53a704 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -77,11 +77,18 @@ class PlaybackViewModel(application: Application) : val isShuffled: StateFlow 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(null) + + /** Flag for resolving an ambiguous artist choice when playing from a song's artists. */ + val artistPlaybackPickerSong: StateFlow + 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. */ diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index 002afa02c..1af044431 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index 23e3a8c52..718ede499 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -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(), MenuItemListener, Toolbar.OnMenuItemClickListener { + MenuFragment(), 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) diff --git a/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt b/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt index c9cc298f1..00ad683dc 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt @@ -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 get() = _exploreNavigationItem + private val _exploreNavigationArtists = MutableStateFlow?>(null) + + /** + * Flag for navigation within the explore fragments. In this case, it involves an ambiguous list + * of artist choices. + */ + val exploreNavigationArtists: StateFlow?> + 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) { + 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() } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/fastscroll/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/ui/fastscroll/FastScrollRecyclerView.kt index e543e3532..760928958 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/fastscroll/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/fastscroll/FastScrollRecyclerView.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/ui/fragment/MusicFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/fragment/MenuFragment.kt similarity index 85% rename from app/src/main/java/org/oxycblt/auxio/ui/fragment/MusicFragment.kt rename to app/src/main/java/org/oxycblt/auxio/ui/fragment/MenuFragment.kt index 5e524ba99..edb42989a 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/fragment/MusicFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/fragment/MenuFragment.kt @@ -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 : ViewBindingFragment() { +abstract class MenuFragment : ViewBindingFragment() { 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 : ViewBindingFragment() { 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 : ViewBindingFragment() { requireContext().showToast(R.string.lng_queue_added) } R.id.action_go_artist -> { - navigateToArtist(album) + navModel.exploreNavigateTo(album.artists) } else -> { error("Unexpected menu item selected") diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt index 9a03852e8..bb48cceec 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -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") diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml index 549dac63e..bfd0dff88 100644 --- a/app/src/main/res/navigation/nav_main.xml +++ b/app/src/main/res/navigation/nav_main.xml @@ -18,22 +18,31 @@ android:id="@+id/action_show_details" app:destination="@id/song_detail_dialog" /> + android:id="@+id/action_pick_playback_artist" + app:destination="@id/artist_playback_picker_dialog" /> + - + + + + android:name="artistUids" + app:argType="org.oxycblt.auxio.music.Music$UID[]" />