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.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
||||||
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackSheetBehavior
|
import org.oxycblt.auxio.playback.PlaybackSheetBehavior
|
||||||
|
@ -124,7 +125,9 @@ class MainFragment :
|
||||||
|
|
||||||
collect(navModel.mainNavigationAction, ::handleMainNavigation)
|
collect(navModel.mainNavigationAction, ::handleMainNavigation)
|
||||||
collect(navModel.exploreNavigationItem, ::handleExploreNavigation)
|
collect(navModel.exploreNavigationItem, ::handleExploreNavigation)
|
||||||
|
collect(navModel.exploreNavigationArtists, ::handleExplorePicker)
|
||||||
collectImmediately(playbackModel.song, ::updateSong)
|
collectImmediately(playbackModel.song, ::updateSong)
|
||||||
|
collect(playbackModel.artistPlaybackPickerSong, ::handlePlaybackPicker)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
@ -213,14 +216,6 @@ class MainFragment :
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSong(song: Song?) {
|
|
||||||
if (song != null) {
|
|
||||||
tryUnhideAll()
|
|
||||||
} else {
|
|
||||||
tryHideAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleMainNavigation(action: MainNavigationAction?) {
|
private fun handleMainNavigation(action: MainNavigationAction?) {
|
||||||
if (action == null) return
|
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() {
|
private fun tryExpandAll() {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
val playbackSheetBehavior =
|
val playbackSheetBehavior =
|
||||||
|
|
|
@ -39,9 +39,8 @@ import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.Sort
|
import org.oxycblt.auxio.music.Sort
|
||||||
import org.oxycblt.auxio.music.picker.PickerMode
|
|
||||||
import org.oxycblt.auxio.settings.Settings
|
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.Item
|
||||||
import org.oxycblt.auxio.util.canScroll
|
import org.oxycblt.auxio.util.canScroll
|
||||||
import org.oxycblt.auxio.util.collect
|
import org.oxycblt.auxio.util.collect
|
||||||
|
@ -56,7 +55,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class AlbumDetailFragment :
|
class AlbumDetailFragment :
|
||||||
MusicFragment<FragmentDetailBinding>(),
|
MenuFragment<FragmentDetailBinding>(),
|
||||||
Toolbar.OnMenuItemClickListener,
|
Toolbar.OnMenuItemClickListener,
|
||||||
AlbumDetailAdapter.Listener {
|
AlbumDetailAdapter.Listener {
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
@ -127,7 +126,7 @@ class AlbumDetailFragment :
|
||||||
null,
|
null,
|
||||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||||
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
|
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
||||||
else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}")
|
else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,7 +163,7 @@ class AlbumDetailFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigateToArtist() {
|
override fun onNavigateToArtist() {
|
||||||
navigateToArtist(unlikelyToBeNull(detailModel.currentAlbum.value))
|
navModel.exploreNavigateTo(unlikelyToBeNull(detailModel.currentAlbum.value).artists)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleItemChange(album: Album?) {
|
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.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.Sort
|
import org.oxycblt.auxio.music.Sort
|
||||||
import org.oxycblt.auxio.music.picker.PickerMode
|
|
||||||
import org.oxycblt.auxio.settings.Settings
|
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.Item
|
||||||
import org.oxycblt.auxio.util.collect
|
import org.oxycblt.auxio.util.collect
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
@ -53,9 +52,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class ArtistDetailFragment :
|
class ArtistDetailFragment :
|
||||||
MusicFragment<FragmentDetailBinding>(),
|
MenuFragment<FragmentDetailBinding>(), Toolbar.OnMenuItemClickListener, DetailAdapter.Listener {
|
||||||
Toolbar.OnMenuItemClickListener,
|
|
||||||
DetailAdapter.Listener {
|
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
||||||
private val args: ArtistDetailFragmentArgs by navArgs()
|
private val args: ArtistDetailFragmentArgs by navArgs()
|
||||||
|
@ -123,7 +120,7 @@ class ArtistDetailFragment :
|
||||||
item, unlikelyToBeNull(detailModel.currentArtist.value))
|
item, unlikelyToBeNull(detailModel.currentArtist.value))
|
||||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(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 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.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.Sort
|
import org.oxycblt.auxio.music.Sort
|
||||||
import org.oxycblt.auxio.music.picker.PickerMode
|
|
||||||
import org.oxycblt.auxio.settings.Settings
|
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.Item
|
||||||
import org.oxycblt.auxio.util.collect
|
import org.oxycblt.auxio.util.collect
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
@ -54,9 +53,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class GenreDetailFragment :
|
class GenreDetailFragment :
|
||||||
MusicFragment<FragmentDetailBinding>(),
|
MenuFragment<FragmentDetailBinding>(), Toolbar.OnMenuItemClickListener, DetailAdapter.Listener {
|
||||||
Toolbar.OnMenuItemClickListener,
|
|
||||||
DetailAdapter.Listener {
|
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
||||||
private val args: GenreDetailFragmentArgs by navArgs()
|
private val args: GenreDetailFragmentArgs by navArgs()
|
||||||
|
@ -125,7 +122,7 @@ class GenreDetailFragment :
|
||||||
item, unlikelyToBeNull(detailModel.currentGenre.value))
|
item, unlikelyToBeNull(detailModel.currentGenre.value))
|
||||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(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 playback mode: ${settings.detailPlaybackMode}")
|
||||||
}
|
}
|
||||||
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
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.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.home.HomeViewModel
|
import org.oxycblt.auxio.home.HomeViewModel
|
||||||
import org.oxycblt.auxio.ui.fastscroll.FastScrollRecyclerView
|
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.Item
|
||||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||||
|
@ -33,7 +33,7 @@ import org.oxycblt.auxio.util.androidActivityViewModels
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
abstract class HomeListFragment<T : Item> :
|
abstract class HomeListFragment<T : Item> :
|
||||||
MusicFragment<FragmentHomeListBinding>(),
|
MenuFragment<FragmentHomeListBinding>(),
|
||||||
MenuItemListener,
|
MenuItemListener,
|
||||||
FastScrollRecyclerView.PopupProvider,
|
FastScrollRecyclerView.PopupProvider,
|
||||||
FastScrollRecyclerView.OnFastScrollListener {
|
FastScrollRecyclerView.OnFastScrollListener {
|
||||||
|
|
|
@ -28,7 +28,6 @@ import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.Sort
|
import org.oxycblt.auxio.music.Sort
|
||||||
import org.oxycblt.auxio.music.picker.PickerMode
|
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.playback.secsToMs
|
import org.oxycblt.auxio.playback.secsToMs
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
|
@ -110,7 +109,7 @@ class SongListFragment : HomeListFragment<Song>() {
|
||||||
when (settings.libPlaybackMode) {
|
when (settings.libPlaybackMode) {
|
||||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||||
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
|
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
||||||
else -> error("Unexpected playback mode: ${settings.libPlaybackMode}")
|
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
|
// 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
|
// our own equivalent from genre database queries. Theoretically, this isn't
|
||||||
// needed since MetadataLayer will fill this in for us, but there are some
|
// needed since MetadataLayer will fill this in for us, but I'd imagine there
|
||||||
// obscure formats where genre support is only really covered by this.
|
// are some obscure formats where genre support is only really covered by this.
|
||||||
context.contentResolverSafe.useQuery(
|
context.contentResolverSafe.useQuery(
|
||||||
MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
|
MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
|
||||||
arrayOf(MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME)) { genreCursor ->
|
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]. */
|
/** Parse an ISO-8601 time-stamp from this field into a [Date]. */
|
||||||
fun String.parseTimestamp() = Date.from(this)
|
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> {
|
inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList<String> {
|
||||||
val split = mutableListOf<String>()
|
val split = mutableListOf<String>()
|
||||||
var currentString = ""
|
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.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
|
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.fragment.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.ui.recycler.ItemClickListener
|
import org.oxycblt.auxio.ui.recycler.ItemClickListener
|
||||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
|
||||||
/**
|
abstract class ArtistPickerDialog :
|
||||||
* 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 :
|
|
||||||
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ItemClickListener {
|
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ItemClickListener {
|
||||||
private val pickerModel: PickerViewModel by viewModels()
|
protected val pickerModel: MusicPickerViewModel by viewModels()
|
||||||
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
private val artistAdapter = ArtistChoiceAdapter(this)
|
||||||
private val navModel: NavigationViewModel by activityViewModels()
|
|
||||||
|
|
||||||
private val args: ArtistPickerDialogArgs by navArgs()
|
|
||||||
private val adapter = ArtistChoiceAdapter(this)
|
|
||||||
|
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||||
DialogMusicPickerBinding.inflate(inflater)
|
DialogMusicPickerBinding.inflate(inflater)
|
||||||
|
@ -61,16 +42,12 @@ class ArtistPickerDialog :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
|
||||||
pickerModel.setSongUid(args.uid)
|
binding.pickerRecycler.adapter = artistAdapter
|
||||||
|
collectImmediately(pickerModel.currentArtists) { artists ->
|
||||||
binding.pickerRecycler.adapter = adapter
|
if (artists != null) {
|
||||||
|
artistAdapter.submitList(artists)
|
||||||
collectImmediately(pickerModel.currentItem) { item ->
|
} else {
|
||||||
when (item) {
|
findNavController().navigateUp()
|
||||||
is Song -> adapter.submitList(item.artists)
|
|
||||||
is Album -> adapter.submitList(item.artists)
|
|
||||||
null -> findNavController().navigateUp()
|
|
||||||
else -> error("Invalid datatype: ${item::class.java}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,15 +57,6 @@ class ArtistPickerDialog :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: Item) {
|
override fun onItemClick(item: Item) {
|
||||||
check(item is Artist) { "Unexpected datatype: ${item::class.simpleName}" }
|
|
||||||
findNavController().navigateUp()
|
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.databinding.FragmentPlaybackPanelBinding
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
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.state.RepeatMode
|
||||||
import org.oxycblt.auxio.playback.ui.StyledSeekBar
|
import org.oxycblt.auxio.playback.ui.StyledSeekBar
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
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.collectImmediately
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
@ -49,7 +48,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
* TODO: Make seek thumb grow when selected
|
* TODO: Make seek thumb grow when selected
|
||||||
*/
|
*/
|
||||||
class PlaybackPanelFragment :
|
class PlaybackPanelFragment :
|
||||||
MusicFragment<FragmentPlaybackPanelBinding>(),
|
MenuFragment<FragmentPlaybackPanelBinding>(),
|
||||||
StyledSeekBar.Callback,
|
StyledSeekBar.Callback,
|
||||||
Toolbar.OnMenuItemClickListener {
|
Toolbar.OnMenuItemClickListener {
|
||||||
// AudioEffect expects you to use startActivityForResult with the panel intent. Use
|
// AudioEffect expects you to use startActivityForResult with the panel intent. Use
|
||||||
|
@ -207,7 +206,7 @@ class PlaybackPanelFragment :
|
||||||
|
|
||||||
private fun showCurrentArtist() {
|
private fun showCurrentArtist() {
|
||||||
val song = playbackModel.song.value ?: return
|
val song = playbackModel.song.value ?: return
|
||||||
doArtistDependentAction(song, PickerMode.SHOW)
|
navModel.exploreNavigateTo(song.artists)
|
||||||
}
|
}
|
||||||
private fun showCurrentAlbum() {
|
private fun showCurrentAlbum() {
|
||||||
val song = playbackModel.song.value ?: return
|
val song = playbackModel.song.value ?: return
|
||||||
|
|
|
@ -77,11 +77,18 @@ class PlaybackViewModel(application: Application) :
|
||||||
val isShuffled: StateFlow<Boolean>
|
val isShuffled: StateFlow<Boolean>
|
||||||
get() = _isShuffled
|
get() = _isShuffled
|
||||||
|
|
||||||
|
/** The current ID of the app's audio session. */
|
||||||
val currentAudioSessionId: Int?
|
val currentAudioSessionId: Int?
|
||||||
get() = playbackManager.currentAudioSessionId
|
get() = playbackManager.currentAudioSessionId
|
||||||
|
|
||||||
private var lastPositionJob: Job? = null
|
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 {
|
init {
|
||||||
playbackManager.addCallback(this)
|
playbackManager.addCallback(this)
|
||||||
}
|
}
|
||||||
|
@ -104,9 +111,22 @@ class PlaybackViewModel(application: Application) :
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Play a song from it's artist. */
|
/** Play a song from it's artist. */
|
||||||
fun playFromArtist(song: Song, artist: Artist) {
|
fun playFromArtist(song: Song, artist: Artist? = null) {
|
||||||
check(artist.songs.contains(song)) { "Invalid input: Artist is not linked to song" }
|
if (artist != null) {
|
||||||
|
check(artist in song.artists) { "Artist not in song artists" }
|
||||||
playbackManager.play(song, artist, settings)
|
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. */
|
/** 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
|
* 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.
|
* of those tasks are what [PlaybackStateManager] is for.
|
||||||
*
|
*
|
||||||
|
* TODO: Refactor lifecycle to run completely headless (i.e no activity needed)
|
||||||
|
*
|
||||||
* TODO: Android Auto
|
* TODO: Android Auto
|
||||||
*
|
*
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
|
|
|
@ -38,9 +38,8 @@ import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.picker.PickerMode
|
|
||||||
import org.oxycblt.auxio.settings.Settings
|
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.Item
|
||||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||||
import org.oxycblt.auxio.util.androidViewModels
|
import org.oxycblt.auxio.util.androidViewModels
|
||||||
|
@ -55,7 +54,7 @@ import org.oxycblt.auxio.util.logW
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class SearchFragment :
|
class SearchFragment :
|
||||||
MusicFragment<FragmentSearchBinding>(), MenuItemListener, Toolbar.OnMenuItemClickListener {
|
MenuFragment<FragmentSearchBinding>(), MenuItemListener, Toolbar.OnMenuItemClickListener {
|
||||||
|
|
||||||
// SearchViewModel is only scoped to this Fragment
|
// SearchViewModel is only scoped to this Fragment
|
||||||
private val searchModel: SearchViewModel by androidViewModels()
|
private val searchModel: SearchViewModel by androidViewModels()
|
||||||
|
@ -149,7 +148,7 @@ class SearchFragment :
|
||||||
when (settings.libPlaybackMode) {
|
when (settings.libPlaybackMode) {
|
||||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||||
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
|
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
||||||
else -> error("Unexpected playback mode: ${settings.libPlaybackMode}")
|
else -> error("Unexpected playback mode: ${settings.libPlaybackMode}")
|
||||||
}
|
}
|
||||||
is MusicParent -> navModel.exploreNavigateTo(item)
|
is MusicParent -> navModel.exploreNavigateTo(item)
|
||||||
|
|
|
@ -21,6 +21,7 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
@ -44,6 +45,15 @@ class NavigationViewModel : ViewModel() {
|
||||||
val exploreNavigationItem: StateFlow<Music?>
|
val exploreNavigationItem: StateFlow<Music?>
|
||||||
get() = _exploreNavigationItem
|
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]. */
|
/** Notify MainFragment to navigate to the location outlined in [MainNavigationAction]. */
|
||||||
fun mainNavigateTo(action: MainNavigationAction) {
|
fun mainNavigateTo(action: MainNavigationAction) {
|
||||||
if (_mainNavigationAction.value != null) {
|
if (_mainNavigationAction.value != null) {
|
||||||
|
@ -72,10 +82,26 @@ class NavigationViewModel : ViewModel() {
|
||||||
_exploreNavigationItem.value = item
|
_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. */
|
/** Mark that the item navigation process is done. */
|
||||||
fun finishExploreNavigation() {
|
fun finishExploreNavigation() {
|
||||||
logD("Finishing explore navigation process")
|
logD("Finishing explore navigation process")
|
||||||
_exploreNavigationItem.value = null
|
_exploreNavigationItem.value = null
|
||||||
|
_exploreNavigationArtists.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,5 +117,6 @@ sealed class MainNavigationAction {
|
||||||
/** Collapse the playback panel. */
|
/** Collapse the playback panel. */
|
||||||
object Collapse : MainNavigationAction()
|
object Collapse : MainNavigationAction()
|
||||||
|
|
||||||
|
/** Provide raw navigation directions. */
|
||||||
data class Directions(val directions: NavDirections) : MainNavigationAction()
|
data class Directions(val directions: NavDirections) : MainNavigationAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,8 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
*
|
*
|
||||||
* TODO: Add vibration when popup changes
|
* TODO: Add vibration when popup changes
|
||||||
*
|
*
|
||||||
|
* TODO: Improve this for variably sized items
|
||||||
|
*
|
||||||
* @author Hai Zhang, OxygenCobalt
|
* @author Hai Zhang, OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class FastScrollRecyclerView
|
class FastScrollRecyclerView
|
||||||
|
|
|
@ -29,7 +29,6 @@ import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.picker.PickerMode
|
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
|
@ -42,40 +41,12 @@ import org.oxycblt.auxio.util.showToast
|
||||||
* preventing UI issues and memory leaks.
|
* preventing UI issues and memory leaks.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
abstract class MusicFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
||||||
private var currentMenu: PopupMenu? = null
|
private var currentMenu: PopupMenu? = null
|
||||||
|
|
||||||
protected val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
protected val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
||||||
protected val navModel: NavigationViewModel by activityViewModels()
|
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
|
* Opens the given menu in context of [song]. Assumes that the menu is only composed of common
|
||||||
* [Song] options.
|
* [Song] options.
|
||||||
|
@ -94,7 +65,7 @@ abstract class MusicFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
}
|
}
|
||||||
R.id.action_go_artist -> {
|
R.id.action_go_artist -> {
|
||||||
doArtistDependentAction(song, PickerMode.SHOW)
|
navModel.exploreNavigateTo(song.artists)
|
||||||
}
|
}
|
||||||
R.id.action_go_album -> {
|
R.id.action_go_album -> {
|
||||||
navModel.exploreNavigateTo(song.album)
|
navModel.exploreNavigateTo(song.album)
|
||||||
|
@ -137,7 +108,7 @@ abstract class MusicFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
}
|
}
|
||||||
R.id.action_go_artist -> {
|
R.id.action_go_artist -> {
|
||||||
navigateToArtist(album)
|
navModel.exploreNavigateTo(album.artists)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
error("Unexpected menu item selected")
|
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
|
// 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
|
// 2. The component range is far smaller, so we have to do some odd hacks to get
|
||||||
// the same UX.
|
// the same UX.
|
||||||
// 3. RemoteView memory is limited, so we want to batch updates as much as physically
|
|
||||||
// possible.
|
|
||||||
val song = playbackManager.song
|
val song = playbackManager.song
|
||||||
if (song == null) {
|
if (song == null) {
|
||||||
logD("No song, resetting widget")
|
logD("No song, resetting widget")
|
||||||
|
|
|
@ -18,22 +18,31 @@
|
||||||
android:id="@+id/action_show_details"
|
android:id="@+id/action_show_details"
|
||||||
app:destination="@id/song_detail_dialog" />
|
app:destination="@id/song_detail_dialog" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_pick_artist"
|
android:id="@+id/action_pick_playback_artist"
|
||||||
app:destination="@id/artist_picker_dialog" />
|
app:destination="@id/artist_playback_picker_dialog" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_pick_navigation_artist"
|
||||||
|
app:destination="@id/artist_navigation_picker_dialog" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
|
|
||||||
<dialog
|
<dialog
|
||||||
android:id="@+id/artist_picker_dialog"
|
android:id="@+id/artist_playback_picker_dialog"
|
||||||
android:name="org.oxycblt.auxio.music.picker.ArtistPickerDialog"
|
android:name="org.oxycblt.auxio.music.picker.ArtistPlaybackPickerDialog"
|
||||||
android:label="artist_picker_dialog"
|
android:label="artist_playback_picker_dialog"
|
||||||
tools:layout="@layout/dialog_music_picker">
|
tools:layout="@layout/dialog_music_picker">
|
||||||
<argument
|
<argument
|
||||||
android:name="uid"
|
android:name="songUid"
|
||||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
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
|
<argument
|
||||||
android:name="pickerMode"
|
android:name="artistUids"
|
||||||
app:argType="org.oxycblt.auxio.music.picker.PickerMode" />
|
app:argType="org.oxycblt.auxio.music.Music$UID[]" />
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<dialog
|
<dialog
|
||||||
|
|
|
@ -29,6 +29,7 @@ OK="\033[1;92m"
|
||||||
NC="\033[0m"
|
NC="\033[0m"
|
||||||
|
|
||||||
# We do some shell scripting later on, so we can't support windows.
|
# We do some shell scripting later on, so we can't support windows.
|
||||||
|
# TODO: Support windows
|
||||||
system = platform.system()
|
system = platform.system()
|
||||||
if system not in ["Linux", "Darwin"]:
|
if system not in ["Linux", "Darwin"]:
|
||||||
print("fatal: unsupported platform " + system)
|
print("fatal: unsupported platform " + system)
|
||||||
|
|
Loading…
Reference in a new issue