diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index ecc8f21f5..00c88bd53 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -31,7 +31,6 @@ import androidx.fragment.app.activityViewModels import androidx.navigation.NavController import androidx.navigation.NavDestination import androidx.navigation.findNavController -import androidx.navigation.fragment.findNavController import com.google.android.material.R as MR import com.google.android.material.bottomsheet.BackportBottomSheetBehavior import com.google.android.material.shape.MaterialShapeDrawable @@ -43,23 +42,18 @@ import org.oxycblt.auxio.databinding.FragmentMainBinding import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.list.selection.SelectionViewModel import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.MusicViewModel -import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.navigation.MainNavigationAction -import org.oxycblt.auxio.navigation.NavigationViewModel +import org.oxycblt.auxio.playback.Panel import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior import org.oxycblt.auxio.ui.ViewBindingFragment -import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.coordinatorLayoutBehavior import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.unlikelyToBeNull @@ -76,8 +70,6 @@ class MainFragment : ViewBindingFragment(), ViewTreeObserver.OnPreDrawListener, NavController.OnDestinationChangedListener { - private val navModel: NavigationViewModel by activityViewModels() - private val musicModel: MusicViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels() private val selectionModel: SelectionViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels() @@ -148,11 +140,7 @@ class MainFragment : // In portrait mode, set up click listeners on the stacked sheets. logD("Configuring stacked bottom sheets") unlikelyToBeNull(binding.queueHandleWrapper).setOnClickListener { - if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED && - queueSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_COLLAPSED) { - // Playback sheet is expanded and queue sheet is collapsed, we can expand it. - queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_EXPANDED - } + playbackModel.openQueue() } } else { // Dual-pane mode, manually style the static queue sheet. @@ -175,16 +163,8 @@ class MainFragment : // --- VIEWMODEL SETUP --- collectImmediately(detailModel.editedPlaylist, detailBackCallback::invalidateEnabled) collectImmediately(selectionModel.selected, selectionBackCallback::invalidateEnabled) - collect(musicModel.newPlaylistSongs.flow, ::handleNewPlaylist) - collect(musicModel.playlistToRename.flow, ::handleRenamePlaylist) - collect(musicModel.playlistToDelete.flow, ::handleDeletePlaylist) - collect(musicModel.songsToAdd.flow, ::handleAddToPlaylist) - collect(navModel.mainNavigationAction.flow, ::handleMainNavigation) - collect(navModel.exploreNavigationItem.flow, ::handleExploreNavigation) - collect(navModel.exploreArtistNavigationItem.flow, ::handleArtistNavigationPicker) collectImmediately(playbackModel.song, ::updateSong) - collect(playbackModel.artistPickerSong.flow, ::handlePlaybackArtistPicker) - collect(playbackModel.genrePickerSong.flow, ::handlePlaybackGenrePicker) + collectImmediately(playbackModel.openPanel.flow, ::handlePanel) } override fun onStart() { @@ -322,33 +302,6 @@ class MainFragment : selectionModel.drop() } - private fun handleMainNavigation(action: MainNavigationAction?) { - if (action != null) { - when (action) { - is MainNavigationAction.OpenPlaybackPanel -> tryOpenPlaybackPanel() - is MainNavigationAction.ClosePlaybackPanel -> tryClosePlaybackPanel() - is MainNavigationAction.Directions -> - findNavController().navigateSafe(action.directions) - } - navModel.mainNavigationAction.consume() - } - } - - private fun handleExploreNavigation(item: Music?) { - if (item != null) { - tryClosePlaybackPanel() - } - } - - private fun handleArtistNavigationPicker(item: Music?) { - if (item != null) { - navModel.mainNavigateTo( - MainNavigationAction.Directions( - MainFragmentDirections.actionPickNavigationArtist(item.uid))) - navModel.exploreArtistNavigationItem.consume() - } - } - private fun updateSong(song: Song?) { if (song != null) { tryShowSheets() @@ -357,56 +310,15 @@ class MainFragment : } } - private fun handleNewPlaylist(songs: List?) { - if (songs != null) { - findNavController() - .navigateSafe( - MainFragmentDirections.actionNewPlaylist(songs.map { it.uid }.toTypedArray())) - musicModel.newPlaylistSongs.consume() - } - } - - private fun handleRenamePlaylist(playlist: Playlist?) { - if (playlist != null) { - findNavController() - .navigateSafe(MainFragmentDirections.actionRenamePlaylist(playlist.uid)) - musicModel.playlistToRename.consume() - } - } - - private fun handleDeletePlaylist(playlist: Playlist?) { - if (playlist != null) { - findNavController() - .navigateSafe(MainFragmentDirections.actionDeletePlaylist(playlist.uid)) - musicModel.playlistToDelete.consume() - } - } - - private fun handleAddToPlaylist(songs: List?) { - if (songs != null) { - findNavController() - .navigateSafe( - MainFragmentDirections.actionAddToPlaylist(songs.map { it.uid }.toTypedArray())) - musicModel.songsToAdd.consume() - } - } - - private fun handlePlaybackArtistPicker(song: Song?) { - if (song != null) { - navModel.mainNavigateTo( - MainNavigationAction.Directions( - MainFragmentDirections.actionPickPlaybackArtist(song.uid))) - playbackModel.artistPickerSong.consume() - } - } - - private fun handlePlaybackGenrePicker(song: Song?) { - if (song != null) { - navModel.mainNavigateTo( - MainNavigationAction.Directions( - MainFragmentDirections.actionPickPlaybackGenre(song.uid))) - playbackModel.genrePickerSong.consume() + private fun handlePanel(panel: Panel?) { + if (panel == null) return + logD("Trying to update panel to $panel") + when (panel) { + is Panel.Main -> tryClosePlaybackPanel() + is Panel.Playback -> tryOpenPlaybackPanel() + is Panel.Queue -> tryOpenQueuePanel() } + playbackModel.openPanel.consume() } private fun tryOpenPlaybackPanel() { @@ -446,6 +358,19 @@ class MainFragment : } } + private fun tryOpenQueuePanel() { + val binding = requireBinding() + val playbackSheetBehavior = + binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior + val queueSheetBehavior = + (binding.queueSheet.coordinatorLayoutBehavior ?: return) as QueueBottomSheetBehavior + if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED && + queueSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_COLLAPSED) { + // Playback sheet is expanded and queue sheet is collapsed, we can expand it. + queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_EXPANDED + } + } + private fun tryShowSheets() { 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 d32f5254b..8844de56e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -42,14 +42,12 @@ import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.selection.SelectionViewModel import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.info.Disc -import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.util.canScroll import org.oxycblt.auxio.util.collect @@ -72,11 +70,10 @@ class AlbumDetailFragment : ListFragment(), AlbumDetailHeaderAdapter.Listener, DetailListAdapter.Listener { - private val detailModel: DetailViewModel by activityViewModels() - override val navModel: NavigationViewModel by activityViewModels() - override val playbackModel: PlaybackViewModel by activityViewModels() - override val musicModel: MusicViewModel by activityViewModels() + override val detailModel: DetailViewModel by activityViewModels() override val selectionModel: SelectionViewModel by activityViewModels() + override val musicModel: MusicViewModel by activityViewModels() + override val playbackModel: PlaybackViewModel by activityViewModels() // Information about what album to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an album. private val args: AlbumDetailFragmentArgs by navArgs() @@ -125,10 +122,12 @@ class AlbumDetailFragment : detailModel.setAlbum(args.albumUid) collectImmediately(detailModel.currentAlbum, ::updateAlbum) collectImmediately(detailModel.albumList, ::updateList) + collect(detailModel.toShow.flow, ::handleShow) + collectImmediately(selectionModel.selected, ::updateSelection) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) - collect(navModel.exploreNavigationItem.flow, ::handleNavigation) - collectImmediately(selectionModel.selected, ::updateSelection) + collect(playbackModel.artistPickerSong.flow, ::handlePlayFromArtist) + collect(playbackModel.genrePickerSong.flow, ::handlePlayFromGenre) } override fun onDestroyBinding(binding: FragmentDetailBinding) { @@ -221,7 +220,7 @@ class AlbumDetailFragment : } override fun onNavigateToParentArtist() { - navModel.exploreNavigateToParentArtist(unlikelyToBeNull(detailModel.currentAlbum.value)) + detailModel.showAlbum(unlikelyToBeNull(detailModel.currentAlbum.value)) } private fun updateAlbum(album: Album?) { @@ -234,53 +233,88 @@ class AlbumDetailFragment : albumHeaderAdapter.setParent(album) } - private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { - albumListAdapter.setPlaying( - song.takeIf { parent == detailModel.currentAlbum.value }, isPlaying) + private fun updateList(list: List) { + albumListAdapter.update(list, detailModel.albumInstructions.consume()) } - private fun handleNavigation(item: Music?) { + private fun handleShow(show: Show?) { val binding = requireBinding() - when (item) { + when (show) { + is Show.SongDetails -> { + logD("Navigating to ${show.song}") + findNavController() + .navigateSafe(AlbumDetailFragmentDirections.showSong(show.song.uid)) + } + // Songs should be scrolled to if the album matches, or a new detail // fragment should be launched otherwise. - is Song -> { - if (unlikelyToBeNull(detailModel.currentAlbum.value) == item.album) { - logD("Navigating to a song in this album") - scrollToAlbumSong(item) - navModel.exploreNavigationItem.consume() + is Show.SongAlbumDetails -> { + if (unlikelyToBeNull(detailModel.currentAlbum.value) == show.song.album) { + logD("Navigating to a ${show.song} in this album") + scrollToAlbumSong(show.song) + detailModel.toShow.consume() } else { - logD("Navigating to another album") + logD("Navigating to the album of ${show.song}") findNavController() - .navigateSafe(AlbumDetailFragmentDirections.actionShowAlbum(item.album.uid)) + .navigateSafe(AlbumDetailFragmentDirections.showAlbum(show.song.album.uid)) } } // If the album matches, no need to do anything. Otherwise launch a new // detail fragment. - is Album -> { - if (unlikelyToBeNull(detailModel.currentAlbum.value) == item) { + is Show.AlbumDetails -> { + if (unlikelyToBeNull(detailModel.currentAlbum.value) == show.album) { logD("Navigating to the top of this album") binding.detailRecycler.scrollToPosition(0) - navModel.exploreNavigationItem.consume() + detailModel.toShow.consume() } else { - logD("Navigating to another album") + logD("Navigating to ${show.album}") findNavController() - .navigateSafe(AlbumDetailFragmentDirections.actionShowAlbum(item.uid)) + .navigateSafe(AlbumDetailFragmentDirections.showAlbum(show.album.uid)) } } // Always launch a new ArtistDetailFragment. - is Artist -> { - logD("Navigating to another artist") + is Show.ArtistDetails -> { + logD("Navigating to ${show.artist}") findNavController() - .navigateSafe(AlbumDetailFragmentDirections.actionShowArtist(item.uid)) + .navigateSafe(AlbumDetailFragmentDirections.showArtist(show.artist.uid)) + } + is Show.SongArtistDetails -> { + logD("Navigating to artist choices for ${show.song}") + findNavController() + .navigateSafe(AlbumDetailFragmentDirections.showArtist(show.song.uid)) + } + is Show.AlbumArtistDetails -> { + logD("Navigating to artist choices for ${show.album}") + findNavController() + .navigateSafe(AlbumDetailFragmentDirections.showArtist(show.album.uid)) + } + is Show.GenreDetails, + is Show.PlaylistDetails -> { + error("Unexpected show command $show") } null -> {} - else -> error("Unexpected datatype: ${item::class.java}") } } + private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { + albumListAdapter.setPlaying( + song.takeIf { parent == detailModel.currentAlbum.value }, isPlaying) + } + + private fun handlePlayFromArtist(song: Song?) { + if (song == null) return + logD("Launching play from artist dialog for $song") + findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromArtist(song.uid)) + } + + private fun handlePlayFromGenre(song: Song?) { + if (song == null) return + logD("Launching play from genre dialog for $song") + findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromGenre(song.uid)) + } + private fun scrollToAlbumSong(song: Song) { // Calculate where the item for the currently played song is val pos = detailModel.albumList.value.indexOf(song) @@ -319,10 +353,6 @@ class AlbumDetailFragment : } } - private fun updateList(list: List) { - albumListAdapter.update(list, detailModel.albumInstructions.consume()) - } - private fun updateSelection(selected: List) { albumListAdapter.setSelected(selected.toSet()) 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 601c2ed50..b55bc9be1 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -47,7 +47,6 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately @@ -69,11 +68,10 @@ class ArtistDetailFragment : ListFragment(), DetailHeaderAdapter.Listener, DetailListAdapter.Listener { - private val detailModel: DetailViewModel by activityViewModels() - override val navModel: NavigationViewModel by activityViewModels() - override val playbackModel: PlaybackViewModel by activityViewModels() - override val musicModel: MusicViewModel by activityViewModels() + override val detailModel: DetailViewModel by activityViewModels() override val selectionModel: SelectionViewModel by activityViewModels() + override val musicModel: MusicViewModel by activityViewModels() + override val playbackModel: PlaybackViewModel by activityViewModels() // Information about what artist to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an artist. private val args: ArtistDetailFragmentArgs by navArgs() @@ -125,9 +123,11 @@ class ArtistDetailFragment : detailModel.setArtist(args.artistUid) collectImmediately(detailModel.currentArtist, ::updateArtist) collectImmediately(detailModel.artistList, ::updateList) + collect(detailModel.toShow.flow, ::handleShow) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) - collect(navModel.exploreNavigationItem.flow, ::handleNavigation) + collect(playbackModel.artistPickerSong.flow, ::handlePlayFromArtist) + collect(playbackModel.genrePickerSong.flow, ::handlePlayFromGenre) collectImmediately(selectionModel.selected, ::updateSelection) } @@ -174,7 +174,7 @@ class ArtistDetailFragment : override fun onRealClick(item: Music) { when (item) { - is Album -> navModel.exploreNavigateTo(item) + is Album -> detailModel.showAlbum(item) is Song -> { val playbackMode = detailModel.playbackMode if (playbackMode != null) { @@ -257,6 +257,57 @@ class ArtistDetailFragment : artistHeaderAdapter.setParent(artist) } + private fun updateList(list: List) { + artistListAdapter.update(list, detailModel.artistInstructions.consume()) + } + + private fun handleShow(show: Show?) { + val binding = requireBinding() + when (show) { + is Show.SongDetails -> { + logD("Navigating to ${show.song}") + findNavController() + .navigateSafe(ArtistDetailFragmentDirections.showSong(show.song.uid)) + } + + // Songs should be shown in their album, not in their artist. + is Show.SongAlbumDetails -> { + logD("Navigating to the album of ${show.song}") + findNavController() + .navigateSafe(ArtistDetailFragmentDirections.showAlbum(show.song.album.uid)) + } + + // Launch a new detail view for an album, even if it is part of + // this artist. + is Show.AlbumDetails -> { + logD("Navigating to ${show.album}") + findNavController() + .navigateSafe(ArtistDetailFragmentDirections.showAlbum(show.album.uid)) + } + + // If the artist that should be navigated to is this artist, then + // scroll back to the top. Otherwise launch a new detail view. + is Show.ArtistDetails -> { + if (show.artist == detailModel.currentArtist.value) { + logD("Navigating to the top of this artist") + binding.detailRecycler.scrollToPosition(0) + detailModel.toShow.consume() + } else { + logD("Navigating to ${show.artist}") + findNavController() + .navigateSafe(ArtistDetailFragmentDirections.showArtist(show.artist.uid)) + } + } + is Show.SongArtistDetails, + is Show.AlbumArtistDetails, + is Show.GenreDetails, + is Show.PlaylistDetails -> { + error("Unexpected show command $show") + } + null -> {} + } + } + private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { val currentArtist = unlikelyToBeNull(detailModel.currentArtist.value) val playingItem = @@ -272,43 +323,16 @@ class ArtistDetailFragment : artistListAdapter.setPlaying(playingItem, isPlaying) } - private fun handleNavigation(item: Music?) { - val binding = requireBinding() - - when (item) { - // Songs should be shown in their album, not in their artist. - is Song -> { - logD("Navigating to another album") - findNavController() - .navigateSafe(ArtistDetailFragmentDirections.actionShowAlbum(item.album.uid)) - } - // Launch a new detail view for an album, even if it is part of - // this artist. - is Album -> { - logD("Navigating to another album") - findNavController() - .navigateSafe(ArtistDetailFragmentDirections.actionShowAlbum(item.uid)) - } - // If the artist that should be navigated to is this artist, then - // scroll back to the top. Otherwise launch a new detail view. - is Artist -> { - if (item.uid == detailModel.currentArtist.value?.uid) { - logD("Navigating to the top of this artist") - binding.detailRecycler.scrollToPosition(0) - navModel.exploreNavigationItem.consume() - } else { - logD("Navigating to another artist") - findNavController() - .navigateSafe(ArtistDetailFragmentDirections.actionShowArtist(item.uid)) - } - } - null -> {} - else -> error("Unexpected datatype: ${item::class.java}") - } + private fun handlePlayFromArtist(song: Song?) { + if (song == null) return + logD("Launching play from artist dialog for $song") + findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromArtist(song.uid)) } - private fun updateList(list: List) { - artistListAdapter.update(list, detailModel.artistInstructions.consume()) + private fun handlePlayFromGenre(song: Song?) { + if (song == null) return + logD("Launching play from genre dialog for $song") + findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromGenre(song.uid)) } private fun updateSelection(selected: List) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index c63c76151..de285ffa9 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -70,6 +70,10 @@ constructor( private val musicSettings: MusicSettings, private val playbackSettings: PlaybackSettings ) : ViewModel(), MusicRepository.UpdateListener { + private val _toShow = MutableEvent() + val toShow: Event + get() = _toShow + // --- SONG --- private var currentSongJob: Job? = null @@ -237,6 +241,43 @@ constructor( } } + fun showSong(song: Song) = showImpl(Show.SongDetails(song)) + + fun showAlbum(song: Song) = showImpl(Show.SongAlbumDetails(song)) + + fun showAlbum(album: Album) = showImpl(Show.AlbumDetails(album)) + + fun showArtist(song: Song) = + showImpl( + if (song.artists.size > 1) { + Show.SongArtistDetails(song) + } else { + Show.ArtistDetails(song.artists.first()) + }) + + fun showArtist(album: Album) = + showImpl( + if (album.artists.size > 1) { + Show.AlbumArtistDetails(album) + } else { + Show.ArtistDetails(album.artists.first()) + }) + + fun showArtist(artist: Artist) = showImpl(Show.ArtistDetails(artist)) + + fun showGenre(genre: Genre) = showImpl(Show.GenreDetails(genre)) + + fun showPlaylist(playlist: Playlist) = showImpl(Show.PlaylistDetails(playlist)) + + private fun showImpl(show: Show) { + val existing = toShow.flow.value + if (existing != null) { + logD("Already have pending show command $existing, ignoring $show") + return + } + _toShow.put(show) + } + /** * Set a new [currentSong] from it's [Music.UID]. [currentSong] and [songAudioProperties] will * be updated to align with the new [Song]. @@ -582,3 +623,14 @@ constructor( val GENRE_ARTIST_SORT = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING) } } + +sealed interface Show { + data class SongDetails(val song: Song) : Show + data class AlbumDetails(val album: Album) : Show + data class SongAlbumDetails(val song: Song) : Show + data class ArtistDetails(val artist: Artist) : Show + data class SongArtistDetails(val song: Song) : Show + data class AlbumArtistDetails(val album: Album) : Show + data class GenreDetails(val genre: Genre) : Show + data class PlaylistDetails(val playlist: Playlist) : Show +} 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 3968c1379..5934d4a11 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -41,14 +41,12 @@ import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.selection.SelectionViewModel -import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately @@ -70,11 +68,10 @@ class GenreDetailFragment : ListFragment(), DetailHeaderAdapter.Listener, DetailListAdapter.Listener { - private val detailModel: DetailViewModel by activityViewModels() - override val navModel: NavigationViewModel by activityViewModels() - override val playbackModel: PlaybackViewModel by activityViewModels() - override val musicModel: MusicViewModel by activityViewModels() + override val detailModel: DetailViewModel by activityViewModels() override val selectionModel: SelectionViewModel by activityViewModels() + override val musicModel: MusicViewModel by activityViewModels() + override val playbackModel: PlaybackViewModel by activityViewModels() // Information about what genre to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an genre. private val args: GenreDetailFragmentArgs by navArgs() @@ -124,9 +121,11 @@ class GenreDetailFragment : detailModel.setGenre(args.genreUid) collectImmediately(detailModel.currentGenre, ::updatePlaylist) collectImmediately(detailModel.genreList, ::updateList) + collect(detailModel.toShow.flow, ::handleShow) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) - collect(navModel.exploreNavigationItem.flow, ::handleNavigation) + collect(playbackModel.artistPickerSong.flow, ::handlePlayFromArtist) + collect(playbackModel.genrePickerSong.flow, ::handlePlayFromGenre) collectImmediately(selectionModel.selected, ::updateSelection) } @@ -173,7 +172,7 @@ class GenreDetailFragment : override fun onRealClick(item: Music) { when (item) { - is Artist -> navModel.exploreNavigateTo(item) + is Artist -> detailModel.showArtist(item) is Song -> { val playbackMode = detailModel.playbackMode if (playbackMode != null) { @@ -242,6 +241,60 @@ class GenreDetailFragment : genreHeaderAdapter.setParent(genre) } + private fun updateList(list: List) { + genreListAdapter.update(list, detailModel.genreInstructions.consume()) + } + + private fun handleShow(show: Show?) { + when (show) { + is Show.SongDetails -> { + logD("Navigating to ${show.song}") + findNavController() + .navigateSafe(GenreDetailFragmentDirections.showSong(show.song.uid)) + } + + // Songs should be scrolled to if the album matches, or a new detail + // fragment should be launched otherwise. + is Show.SongAlbumDetails -> { + logD("Navigating to the album of ${show.song}") + findNavController() + .navigateSafe(GenreDetailFragmentDirections.showAlbum(show.song.album.uid)) + } + + // If the album matches, no need to do anything. Otherwise launch a new + // detail fragment. + is Show.AlbumDetails -> { + logD("Navigating to ${show.album}") + findNavController() + .navigateSafe(GenreDetailFragmentDirections.showAlbum(show.album.uid)) + } + + // Always launch a new ArtistDetailFragment. + is Show.ArtistDetails -> { + logD("Navigating to ${show.artist}") + findNavController() + .navigateSafe(GenreDetailFragmentDirections.showArtist(show.artist.uid)) + } + is Show.SongArtistDetails -> { + logD("Navigating to artist choices for ${show.song}") + findNavController() + .navigateSafe(GenreDetailFragmentDirections.showArtist(show.song.uid)) + } + is Show.AlbumArtistDetails -> { + logD("Navigating to artist choices for ${show.album}") + findNavController() + .navigateSafe(GenreDetailFragmentDirections.showArtist(show.album.uid)) + } + is Show.GenreDetails -> { + logD("Navigated to this genre") + } + is Show.PlaylistDetails -> { + error("Unexpected show command $show") + } + null -> {} + } + } + private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { val currentGenre = unlikelyToBeNull(detailModel.currentGenre.value) val playingItem = @@ -257,32 +310,16 @@ class GenreDetailFragment : genreListAdapter.setPlaying(playingItem, isPlaying) } - private fun handleNavigation(item: Music?) { - when (item) { - is Song -> { - logD("Navigating to another song") - findNavController() - .navigateSafe(GenreDetailFragmentDirections.actionShowAlbum(item.album.uid)) - } - is Album -> { - logD("Navigating to another album") - findNavController() - .navigateSafe(GenreDetailFragmentDirections.actionShowAlbum(item.uid)) - } - is Artist -> { - logD("Navigating to another artist") - findNavController() - .navigateSafe(GenreDetailFragmentDirections.actionShowArtist(item.uid)) - } - is Genre -> { - navModel.exploreNavigationItem.consume() - } - else -> {} - } + private fun handlePlayFromArtist(song: Song?) { + if (song == null) return + logD("Launching play from artist dialog for $song") + findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromArtist(song.uid)) } - private fun updateList(list: List) { - genreListAdapter.update(list, detailModel.genreInstructions.consume()) + private fun handlePlayFromGenre(song: Song?) { + if (song == null) return + logD("Launching play from genre dialog for $song") + findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromGenre(song.uid)) } private fun updateSelection(selected: List) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt index 7cdf9443c..e2ae60c93 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt @@ -44,14 +44,11 @@ import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.selection.SelectionViewModel -import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately @@ -74,11 +71,10 @@ class PlaylistDetailFragment : DetailHeaderAdapter.Listener, PlaylistDetailListAdapter.Listener, NavController.OnDestinationChangedListener { - private val detailModel: DetailViewModel by activityViewModels() - override val navModel: NavigationViewModel by activityViewModels() - override val playbackModel: PlaybackViewModel by activityViewModels() - override val musicModel: MusicViewModel by activityViewModels() + override val detailModel: DetailViewModel by activityViewModels() override val selectionModel: SelectionViewModel by activityViewModels() + override val musicModel: MusicViewModel by activityViewModels() + override val playbackModel: PlaybackViewModel by activityViewModels() // Information about what playlist to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an playlist. private val args: PlaylistDetailFragmentArgs by navArgs() @@ -139,10 +135,12 @@ class PlaylistDetailFragment : detailModel.setPlaylist(args.playlistUid) collectImmediately(detailModel.currentPlaylist, ::updatePlaylist) collectImmediately(detailModel.playlistList, ::updateList) - collectImmediately(detailModel.editedPlaylist, ::updateEditedPlaylist) + collectImmediately(detailModel.editedPlaylist, ::updateEditedList) + collect(detailModel.toShow.flow, ::handleShow) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) - collect(navModel.exploreNavigationItem.flow, ::handleNavigation) + collect(playbackModel.artistPickerSong.flow, ::handlePlayFromArtist) + collect(playbackModel.genrePickerSong.flow, ::handlePlayFromGenre) collectImmediately(selectionModel.selected, ::updateSelection) } @@ -275,41 +273,11 @@ class PlaylistDetailFragment : playlistHeaderAdapter.setParent(playlist) } - private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { - // Prefer songs that are playing from this playlist. - playlistListAdapter.setPlaying( - song.takeIf { parent == detailModel.currentPlaylist.value }, isPlaying) - } - - private fun handleNavigation(item: Music?) { - when (item) { - is Song -> { - logD("Navigating to another song") - findNavController() - .navigateSafe(PlaylistDetailFragmentDirections.actionShowAlbum(item.album.uid)) - } - is Album -> { - logD("Navigating to another album") - findNavController() - .navigateSafe(PlaylistDetailFragmentDirections.actionShowAlbum(item.uid)) - } - is Artist -> { - logD("Navigating to another artist") - findNavController() - .navigateSafe(PlaylistDetailFragmentDirections.actionShowArtist(item.uid)) - } - is Playlist -> { - navModel.exploreNavigationItem.consume() - } - else -> {} - } - } - private fun updateList(list: List) { playlistListAdapter.update(list, detailModel.playlistInstructions.consume()) } - private fun updateEditedPlaylist(editedPlaylist: List?) { + private fun updateEditedList(editedPlaylist: List?) { playlistListAdapter.setEditing(editedPlaylist != null) playlistHeaderAdapter.setEditedPlaylist(editedPlaylist) selectionModel.drop() @@ -324,6 +292,74 @@ class PlaylistDetailFragment : updateMultiToolbar() } + private fun handleShow(show: Show?) { + when (show) { + is Show.SongDetails -> { + logD("Navigating to ${show.song}") + findNavController() + .navigateSafe(PlaylistDetailFragmentDirections.showSong(show.song.uid)) + } + + // Songs should be scrolled to if the album matches, or a new detail + // fragment should be launched otherwise. + is Show.SongAlbumDetails -> { + logD("Navigating to the album of ${show.song}") + findNavController() + .navigateSafe(PlaylistDetailFragmentDirections.showAlbum(show.song.album.uid)) + } + + // If the album matches, no need to do anything. Otherwise launch a new + // detail fragment. + is Show.AlbumDetails -> { + logD("Navigating to ${show.album}") + findNavController() + .navigateSafe(PlaylistDetailFragmentDirections.showAlbum(show.album.uid)) + } + + // Always launch a new ArtistDetailFragment. + is Show.ArtistDetails -> { + logD("Navigating to ${show.artist}") + findNavController() + .navigateSafe(PlaylistDetailFragmentDirections.showArtist(show.artist.uid)) + } + is Show.SongArtistDetails -> { + logD("Navigating to artist choices for ${show.song}") + findNavController() + .navigateSafe(PlaylistDetailFragmentDirections.showArtist(show.song.uid)) + } + is Show.AlbumArtistDetails -> { + logD("Navigating to artist choices for ${show.album}") + findNavController() + .navigateSafe(PlaylistDetailFragmentDirections.showArtist(show.album.uid)) + } + is Show.PlaylistDetails -> { + logD("Navigated to this playlist") + } + is Show.GenreDetails -> { + error("Unexpected show command $show") + } + null -> {} + } + } + + private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { + // Prefer songs that are playing from this playlist. + playlistListAdapter.setPlaying( + song.takeIf { parent == detailModel.currentPlaylist.value }, isPlaying) + } + + private fun handlePlayFromArtist(song: Song?) { + if (song == null) return + logD("Launching play from artist dialog for $song") + findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromArtist(song.uid)) + } + + private fun handlePlayFromGenre(song: Song?) { + if (song == null) return + logD("Launching play from genre dialog for $song") + findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromGenre(song.uid)) + } + private fun updateSelection(selected: List) { playlistListAdapter.setSelected(selected.toSet()) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt index ca38c061e..c46d23ea7 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -70,6 +70,7 @@ class SongDetailDialog : ViewBindingDialogFragment() { // DetailViewModel handles most initialization from the navigation argument. detailModel.setSong(args.songUid) collectImmediately(detailModel.currentSong, detailModel.songAudioProperties, ::updateSong) + collectImmediately(detailModel.toShow.flow, ::handleShow) } private fun updateSong(song: Song?, info: AudioProperties?) { @@ -125,6 +126,15 @@ class SongDetailDialog : ViewBindingDialogFragment() { } } + private fun handleShow(show: Show?) { + if (show == null) return + if (show is Show.SongDetails) { + logD("Navigated to this song") + } else { + error("Unexpected show command $show") + } + } + private fun T.zipName(context: Context): String { val name = name return if (name is Name.Known && name.sort != null) { diff --git a/app/src/main/java/org/oxycblt/auxio/navigation/picker/ArtistNavigationChoiceAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/picker/ArtistShowChoice.kt similarity index 91% rename from app/src/main/java/org/oxycblt/auxio/navigation/picker/ArtistNavigationChoiceAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/detail/picker/ArtistShowChoice.kt index a397ec23b..f28b9d765 100644 --- a/app/src/main/java/org/oxycblt/auxio/navigation/picker/ArtistNavigationChoiceAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/picker/ArtistShowChoice.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2023 Auxio Project - * ArtistNavigationChoiceAdapter.kt is part of Auxio. + * ArtistShowChoice.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.navigation.picker +package org.oxycblt.auxio.detail.picker import android.view.View import android.view.ViewGroup @@ -31,11 +31,11 @@ import org.oxycblt.auxio.util.inflater /** * A [FlexibleListAdapter] that displays a list of [Artist] navigation choices, for use with - * [NavigateToArtistDialog]. + * [ShowArtistDialog]. * * @param listener A [ClickableListListener] to bind interactions to. */ -class ArtistNavigationChoiceAdapter(private val listener: ClickableListListener) : +class ArtistShowChoice(private val listener: ClickableListListener) : FlexibleListAdapter( ArtistNavigationChoiceViewHolder.DIFF_CALLBACK) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = @@ -48,7 +48,7 @@ class ArtistNavigationChoiceAdapter(private val listener: ClickableListListener< /** * A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical [Artist] item, for - * use [ArtistNavigationChoiceAdapter]. Use [from] to create an instance. + * use [ArtistShowChoice]. Use [from] to create an instance. * * @author Alexander Capehart (OxygenCobalt) */ diff --git a/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigationPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/picker/NavigationPickerViewModel.kt similarity index 68% rename from app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigationPickerViewModel.kt rename to app/src/main/java/org/oxycblt/auxio/detail/picker/NavigationPickerViewModel.kt index f02621d5b..a510b6e4a 100644 --- a/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigationPickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/picker/NavigationPickerViewModel.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.navigation.picker +package org.oxycblt.auxio.detail.picker import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel @@ -28,7 +28,9 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.logW /** * A [ViewModel] that stores the current information required for navigation picker dialogs @@ -38,9 +40,9 @@ import org.oxycblt.auxio.util.logD @HiltViewModel class NavigationPickerViewModel @Inject constructor(private val musicRepository: MusicRepository) : ViewModel(), MusicRepository.UpdateListener { - private val _artistChoices = MutableStateFlow(null) + private val _artistChoices = MutableStateFlow(null) /** The current set of [Artist] choices to show in the picker, or null if to show nothing. */ - val artistChoices: StateFlow + val artistChoices: StateFlow get() = _artistChoices init { @@ -51,18 +53,7 @@ class NavigationPickerViewModel @Inject constructor(private val musicRepository: if (!changes.deviceLibrary) return val deviceLibrary = musicRepository.deviceLibrary ?: return // Need to sanitize different items depending on the current set of choices. - _artistChoices.value = - when (val choices = _artistChoices.value) { - is SongArtistNavigationChoices -> - deviceLibrary.findSong(choices.song.uid)?.let { - SongArtistNavigationChoices(it) - } - is AlbumArtistNavigationChoices -> - deviceLibrary.findAlbum(choices.album.uid)?.let { - AlbumArtistNavigationChoices(it) - } - else -> null - } + _artistChoices.value = _artistChoices.value?.sanitize(deviceLibrary) logD("Updated artist choices: ${_artistChoices.value}") } @@ -83,14 +74,14 @@ class NavigationPickerViewModel @Inject constructor(private val musicRepository: when (val music = musicRepository.find(itemUid)) { is Song -> { logD("Creating navigation choices for song") - SongArtistNavigationChoices(music) + ArtistShowChoices.FromSong(music) } is Album -> { logD("Creating navigation choices for album") - AlbumArtistNavigationChoices(music) + ArtistShowChoices.FromAlbum(music) } else -> { - logD("Given song/album UID was invalid") + logW("Given song/album UID was invalid") null } } @@ -102,20 +93,29 @@ class NavigationPickerViewModel @Inject constructor(private val musicRepository: * * @author Alexander Capehart (OxygenCobalt) */ -sealed interface ArtistNavigationChoices { +sealed interface ArtistShowChoices { + /** The UID of the item. */ + val uid: Music.UID /** The current [Artist] choices. */ val choices: List -} + /** Sanitize this instance with a [DeviceLibrary]. */ + fun sanitize(newLibrary: DeviceLibrary): ArtistShowChoices? -/** Backing implementation of [ArtistNavigationChoices] that is based on a [Song]. */ -private data class SongArtistNavigationChoices(val song: Song) : ArtistNavigationChoices { - override val choices = song.artists -} + /** Backing implementation of [ArtistShowChoices] that is based on a [Song]. */ + class FromSong(val song: Song) : ArtistShowChoices { + override val uid = song.uid + override val choices = song.artists + override fun sanitize(newLibrary: DeviceLibrary) = + newLibrary.findSong(uid)?.let { FromSong(it) } + } -/** - * Backing implementation of [ArtistNavigationChoices] that is based on an - * [AlbumArtistNavigationChoices]. - */ -private data class AlbumArtistNavigationChoices(val album: Album) : ArtistNavigationChoices { - override val choices = album.artists + /** + * Backing implementation of [ArtistShowChoices] that is based on an [AlbumArtistShowChoices]. + */ + data class FromAlbum(val album: Album) : ArtistShowChoices { + override val uid = album.uid + override val choices = album.artists + override fun sanitize(newLibrary: DeviceLibrary) = + newLibrary.findAlbum(uid)?.let { FromAlbum(it) } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigateToArtistDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/picker/ShowArtistDialog.kt similarity index 85% rename from app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigateToArtistDialog.kt rename to app/src/main/java/org/oxycblt/auxio/detail/picker/ShowArtistDialog.kt index ade74f930..a98dfb162 100644 --- a/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigateToArtistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/picker/ShowArtistDialog.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2022 Auxio Project - * NavigateToArtistDialog.kt is part of Auxio. + * ShowArtistDialog.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.navigation.picker +package org.oxycblt.auxio.detail.picker import android.os.Bundle import android.view.LayoutInflater @@ -29,27 +29,27 @@ import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogMusicChoicesBinding +import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.collectImmediately /** - * A picker [ViewBindingDialogFragment] intended for when [Artist] navigation is ambiguous. + * A picker [ViewBindingDialogFragment] intended for when the [Artist] to show is ambiguous. * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint -class NavigateToArtistDialog : +class ShowArtistDialog : ViewBindingDialogFragment(), ClickableListListener { - private val navigationModel: NavigationViewModel by activityViewModels() + private val detailModel: DetailViewModel by activityViewModels() private val pickerModel: NavigationPickerViewModel by viewModels() // Information about what artists to show choices for is initially within the navigation // arguments as UIDs, as that is the only safe way to parcel an artist. - private val args: NavigateToArtistDialogArgs by navArgs() - private val choiceAdapter = ArtistNavigationChoiceAdapter(this) + private val args: ShowArtistDialogArgs by navArgs() + private val choiceAdapter = ArtistShowChoice(this) override fun onConfigDialog(builder: AlertDialog.Builder) { builder.setTitle(R.string.lbl_artists).setNegativeButton(R.string.lbl_cancel, null) @@ -83,7 +83,7 @@ class NavigateToArtistDialog : override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) { // User made a choice, navigate to the artist. - navigationModel.exploreNavigateTo(item) + detailModel.showArtist(item) findNavController().navigateUp() } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index f39a54e1f..0d2e40ad3 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -43,9 +43,10 @@ import dagger.hilt.android.AndroidEntryPoint import java.lang.reflect.Field import kotlin.math.abs import org.oxycblt.auxio.BuildConfig -import org.oxycblt.auxio.MainFragmentDirections import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeBinding +import org.oxycblt.auxio.detail.DetailViewModel +import org.oxycblt.auxio.detail.Show import org.oxycblt.auxio.home.list.AlbumListFragment import org.oxycblt.auxio.home.list.ArtistListFragment import org.oxycblt.auxio.home.list.GenreListFragment @@ -56,9 +57,6 @@ import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.selection.SelectionFragment import org.oxycblt.auxio.list.selection.SelectionViewModel -import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.IndexingProgress import org.oxycblt.auxio.music.IndexingState import org.oxycblt.auxio.music.Music @@ -67,10 +65,7 @@ import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.NoAudioPermissionException import org.oxycblt.auxio.music.NoMusicException import org.oxycblt.auxio.music.PERMISSION_READ_AUDIO -import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.navigation.MainNavigationAction -import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately @@ -94,7 +89,7 @@ class HomeFragment : override val selectionModel: SelectionViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels() private val homeModel: HomeViewModel by activityViewModels() - private val navModel: NavigationViewModel by activityViewModels() + private val detailModel: DetailViewModel by activityViewModels() private var storagePermissionLauncher: ActivityResultLauncher? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -177,7 +172,7 @@ class HomeFragment : collectImmediately(homeModel.currentTabMode, ::updateCurrentTab) collectImmediately(homeModel.songsList, homeModel.isFastScrolling, ::updateFab) collectImmediately(musicModel.indexingState, ::updateIndexerState) - collect(navModel.exploreNavigationItem.flow, ::handleNavigation) + collect(detailModel.toShow.flow, ::handleShow) collectImmediately(selectionModel.selected, ::updateSelection) } @@ -218,19 +213,17 @@ class HomeFragment : R.id.action_search -> { logD("Navigating to search") setupAxisTransitions(MaterialSharedAxis.Z) - findNavController().navigateSafe(HomeFragmentDirections.actionShowSearch()) + findNavController().navigateSafe(HomeFragmentDirections.search()) true } R.id.action_settings -> { - logD("Navigating to settings") - navModel.mainNavigateTo( - MainNavigationAction.Directions(MainFragmentDirections.actionShowSettings())) + logD("Navigating to preferences") + findNavController().navigateSafe(HomeFragmentDirections.preferences()) true } R.id.action_about -> { logD("Navigating to about") - navModel.mainNavigateTo( - MainNavigationAction.Directions(MainFragmentDirections.actionShowAbout())) + findNavController().navigateSafe(HomeFragmentDirections.about()) true } @@ -500,19 +493,55 @@ class HomeFragment : } } - private fun handleNavigation(item: Music?) { - val action = - when (item) { - is Song -> HomeFragmentDirections.actionShowAlbum(item.album.uid) - is Album -> HomeFragmentDirections.actionShowAlbum(item.uid) - is Artist -> HomeFragmentDirections.actionShowArtist(item.uid) - is Genre -> HomeFragmentDirections.actionShowGenre(item.uid) - is Playlist -> HomeFragmentDirections.actionShowPlaylist(item.uid) - null -> return + private fun handleShow(show: Show?) { + when (show) { + is Show.SongDetails -> { + logD("Navigating to ${show.song}") + findNavController().navigateSafe(HomeFragmentDirections.showSong(show.song.uid)) } - setupAxisTransitions(MaterialSharedAxis.X) - findNavController().navigateSafe(action) + // Songs should be scrolled to if the album matches, or a new detail + // fragment should be launched otherwise. + is Show.SongAlbumDetails -> { + logD("Navigating to the album of ${show.song}") + setupAxisTransitions(MaterialSharedAxis.X) + findNavController() + .navigateSafe(HomeFragmentDirections.showAlbum(show.song.album.uid)) + } + + // If the album matches, no need to do anything. Otherwise launch a new + // detail fragment. + is Show.AlbumDetails -> { + logD("Navigating to ${show.album}") + setupAxisTransitions(MaterialSharedAxis.X) + findNavController().navigateSafe(HomeFragmentDirections.showAlbum(show.album.uid)) + } + + // Always launch a new ArtistDetailFragment. + is Show.ArtistDetails -> { + logD("Navigating to ${show.artist}") + setupAxisTransitions(MaterialSharedAxis.X) + findNavController().navigateSafe(HomeFragmentDirections.showArtist(show.artist.uid)) + } + is Show.SongArtistDetails -> { + logD("Navigating to artist choices for ${show.song}") + findNavController().navigateSafe(HomeFragmentDirections.showArtist(show.song.uid)) + } + is Show.AlbumArtistDetails -> { + logD("Navigating to artist choices for ${show.album}") + findNavController().navigateSafe(HomeFragmentDirections.showArtist(show.album.uid)) + } + is Show.GenreDetails -> { + logD("Navigating to ${show.genre}") + findNavController().navigateSafe(HomeFragmentDirections.showGenre(show.genre.uid)) + } + is Show.PlaylistDetails -> { + logD("Navigating to ${show.playlist}") + findNavController() + .navigateSafe(HomeFragmentDirections.showGenre(show.playlist.uid)) + } + null -> {} + } } private fun updateSelection(selected: List) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index 3495bc85a..b4aac9121 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -28,6 +28,7 @@ import dagger.hilt.android.AndroidEntryPoint import java.util.Formatter import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding +import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.home.HomeViewModel import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.list.ListFragment @@ -42,7 +43,6 @@ import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.secsToMs @@ -59,7 +59,7 @@ class AlbumListFragment : FastScrollRecyclerView.Listener, FastScrollRecyclerView.PopupProvider { private val homeModel: HomeViewModel by activityViewModels() - override val navModel: NavigationViewModel by activityViewModels() + override val detailModel: DetailViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels() override val selectionModel: SelectionViewModel by activityViewModels() @@ -138,7 +138,7 @@ class AlbumListFragment : } override fun onRealClick(item: Album) { - navModel.exploreNavigateTo(item) + detailModel.showAlbum(item) } override fun onOpenMenu(item: Album, anchor: View) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt index e270fa7d2..b66c6e965 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt @@ -26,6 +26,7 @@ import androidx.fragment.app.activityViewModels import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding +import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.home.HomeViewModel import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.list.ListFragment @@ -40,7 +41,6 @@ import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.util.collectImmediately @@ -57,7 +57,7 @@ class ArtistListFragment : FastScrollRecyclerView.PopupProvider, FastScrollRecyclerView.Listener { private val homeModel: HomeViewModel by activityViewModels() - override val navModel: NavigationViewModel by activityViewModels() + override val detailModel: DetailViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels() override val selectionModel: SelectionViewModel by activityViewModels() @@ -114,7 +114,7 @@ class ArtistListFragment : } override fun onRealClick(item: Artist) { - navModel.exploreNavigateTo(item) + detailModel.showArtist(item) } override fun onOpenMenu(item: Artist, anchor: View) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt index ee9544d55..d751e3699 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt @@ -26,6 +26,7 @@ import androidx.fragment.app.activityViewModels import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding +import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.home.HomeViewModel import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.list.ListFragment @@ -40,7 +41,6 @@ import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.util.collectImmediately @@ -56,7 +56,7 @@ class GenreListFragment : FastScrollRecyclerView.PopupProvider, FastScrollRecyclerView.Listener { private val homeModel: HomeViewModel by activityViewModels() - override val navModel: NavigationViewModel by activityViewModels() + override val detailModel: DetailViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels() override val selectionModel: SelectionViewModel by activityViewModels() @@ -113,7 +113,7 @@ class GenreListFragment : } override fun onRealClick(item: Genre) { - navModel.exploreNavigateTo(item) + detailModel.showGenre(item) } override fun onOpenMenu(item: Genre, anchor: View) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt index 61fa54b7c..405dbe312 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt @@ -25,6 +25,7 @@ import android.view.ViewGroup import androidx.fragment.app.activityViewModels import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding +import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.home.HomeViewModel import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.list.ListFragment @@ -39,7 +40,6 @@ import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.util.collectImmediately @@ -54,7 +54,7 @@ class PlaylistListFragment : FastScrollRecyclerView.PopupProvider, FastScrollRecyclerView.Listener { private val homeModel: HomeViewModel by activityViewModels() - override val navModel: NavigationViewModel by activityViewModels() + override val detailModel: DetailViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels() override val selectionModel: SelectionViewModel by activityViewModels() @@ -111,7 +111,7 @@ class PlaylistListFragment : } override fun onRealClick(item: Playlist) { - navModel.exploreNavigateTo(item) + detailModel.showPlaylist(item) } override fun onOpenMenu(item: Playlist, anchor: View) { 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 62643f4cf..d827adbf7 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,6 +28,7 @@ import dagger.hilt.android.AndroidEntryPoint import java.util.Formatter import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding +import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.home.HomeViewModel import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.list.ListFragment @@ -41,7 +42,6 @@ import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.secsToMs @@ -58,7 +58,7 @@ class SongListFragment : FastScrollRecyclerView.PopupProvider, FastScrollRecyclerView.Listener { private val homeModel: HomeViewModel by activityViewModels() - override val navModel: NavigationViewModel by activityViewModels() + override val detailModel: DetailViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels() override val selectionModel: SelectionViewModel by activityViewModels() diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt index bca4fb774..7931f63e6 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt @@ -24,8 +24,8 @@ import androidx.appcompat.widget.PopupMenu import androidx.core.view.MenuCompat import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding -import org.oxycblt.auxio.MainFragmentDirections import org.oxycblt.auxio.R +import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.list.selection.SelectionFragment import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist @@ -33,8 +33,6 @@ import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.navigation.MainNavigationAction -import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.share @@ -47,7 +45,7 @@ import org.oxycblt.auxio.util.showToast */ abstract class ListFragment : SelectionFragment(), SelectableListListener { - protected abstract val navModel: NavigationViewModel + protected abstract val detailModel: DetailViewModel private var currentMenu: PopupMenu? = null override fun onDestroyBinding(binding: VB) { @@ -103,11 +101,11 @@ abstract class ListFragment : true } R.id.action_go_artist -> { - navModel.exploreNavigateToParentArtist(song) + detailModel.showArtist(song) true } R.id.action_go_album -> { - navModel.exploreNavigateTo(song.album) + detailModel.showAlbum(song.album) true } R.id.action_share -> { @@ -119,9 +117,7 @@ abstract class ListFragment : true } R.id.action_song_detail -> { - navModel.mainNavigateTo( - MainNavigationAction.Directions( - MainFragmentDirections.actionShowDetails(song.uid))) + detailModel.showSong(song) true } else -> { @@ -166,7 +162,7 @@ abstract class ListFragment : true } R.id.action_go_artist -> { - navModel.exploreNavigateToParentArtist(album) + detailModel.showArtist(album) true } R.id.action_playlist_add -> { diff --git a/app/src/main/java/org/oxycblt/auxio/navigation/MainNavigationAction.kt b/app/src/main/java/org/oxycblt/auxio/navigation/MainNavigationAction.kt deleted file mode 100644 index 59c778320..000000000 --- a/app/src/main/java/org/oxycblt/auxio/navigation/MainNavigationAction.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * MainNavigationAction.kt is part of Auxio. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.navigation - -import androidx.navigation.NavDirections - -/** - * Represents the possible actions within the main navigation graph. This can be used with - * [NavigationViewModel] to initiate navigation in the main navigation graph from anywhere in the - * app, including outside the main navigation graph. - * - * @author Alexander Capehart (OxygenCobalt) - */ -sealed interface MainNavigationAction { - /** Expand the playback panel. */ - object OpenPlaybackPanel : MainNavigationAction - - /** Collapse the playback bottom sheet. */ - object ClosePlaybackPanel : MainNavigationAction - - /** - * Navigate to the given [NavDirections]. - * - * @param directions The [NavDirections] to navigate to. Assumed to be part of the main - * navigation graph. - */ - data class Directions(val directions: NavDirections) : MainNavigationAction -} diff --git a/app/src/main/java/org/oxycblt/auxio/navigation/NavigationViewModel.kt b/app/src/main/java/org/oxycblt/auxio/navigation/NavigationViewModel.kt deleted file mode 100644 index 5271e49c7..000000000 --- a/app/src/main/java/org/oxycblt/auxio/navigation/NavigationViewModel.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (c) 2022 Auxio Project - * NavigationViewModel.kt is part of Auxio. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.navigation - -import androidx.lifecycle.ViewModel -import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.util.Event -import org.oxycblt.auxio.util.MutableEvent -import org.oxycblt.auxio.util.logD - -/** - * A [ViewModel] that handles complicated navigation functionality. - * - * @author Alexander Capehart (OxygenCobalt) - * - * TODO: Unwind this into ViewModel-specific actions, and then reference those. - */ -class NavigationViewModel : ViewModel() { - private val _mainNavigationAction = MutableEvent() - /** - * Flag for navigation within the main navigation graph. Only intended for use by MainFragment. - */ - val mainNavigationAction: Event - get() = _mainNavigationAction - - private val _exploreNavigationItem = MutableEvent() - /** - * Flag for navigation within the explore navigation graph. Observe this to coordinate - * navigation to a specific [Music] item. - */ - val exploreNavigationItem: Event - get() = _exploreNavigationItem - - private val _exploreArtistNavigationItem = MutableEvent() - /** - * Variation of [exploreNavigationItem] for situations where the choice of parent [Artist] to - * navigate to is ambiguous. Only intended for use by MainFragment, as the resolved choice will - * eventually be assigned to [exploreNavigationItem]. - */ - val exploreArtistNavigationItem: Event - get() = _exploreArtistNavigationItem - - /** - * Navigate to something in the main navigation graph. This can be used by UIs in the explore - * navigation graph to trigger navigation in the higher-level main navigation graph. Will do - * nothing if already navigating. - * - * @param action The [MainNavigationAction] to perform. - */ - fun mainNavigateTo(action: MainNavigationAction) { - if (_mainNavigationAction.flow.value != null) { - logD("Already navigating, not doing main action") - return - } - logD("Navigating with action $action") - _mainNavigationAction.put(action) - } - - /** - * Navigate to a given [Music] item. Will do nothing if already navigating. - * - * @param music The [Music] to navigate to. - */ - fun exploreNavigateTo(music: Music) { - if (_exploreNavigationItem.flow.value != null) { - logD("Already navigating, not doing explore action") - return - } - logD("Navigating to ${music.name}") - _exploreNavigationItem.put(music) - } - - /** - * Navigate to one of the parent [Artist]'s of the given [Song]. - * - * @param song The [Song] to navigate with. If there are multiple parent [Artist]s, a picker - * dialog will be shown. - */ - fun exploreNavigateToParentArtist(song: Song) { - logD("Navigating to parent artist of $song") - exploreNavigateToParentArtistImpl(song, song.artists) - } - - /** - * Navigate to one of the parent [Artist]'s of the given [Album]. - * - * @param album The [Album] to navigate with. If there are multiple parent [Artist]s, a picker - * dialog will be shown. - */ - fun exploreNavigateToParentArtist(album: Album) { - logD("Navigating to parent artist of $album") - exploreNavigateToParentArtistImpl(album, album.artists) - } - - private fun exploreNavigateToParentArtistImpl(item: Music, artists: List) { - if (_exploreArtistNavigationItem.flow.value != null) { - logD("Already navigating, not doing explore action") - return - } - - if (artists.size == 1) { - exploreNavigateTo(artists[0]) - } else { - logD("Navigating to a choice of ${artists.map { it.name }}") - _exploreArtistNavigationItem.put(item) - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt index 05f438c38..036f07c99 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -25,10 +25,9 @@ import com.google.android.material.R as MR import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding +import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.resolveNames -import org.oxycblt.auxio.navigation.MainNavigationAction -import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.collectImmediately @@ -44,7 +43,7 @@ import org.oxycblt.auxio.util.logD @AndroidEntryPoint class PlaybackBarFragment : ViewBindingFragment() { private val playbackModel: PlaybackViewModel by activityViewModels() - private val navModel: NavigationViewModel by activityViewModels() + private val detailModel: DetailViewModel by activityViewModels() override fun onCreateBinding(inflater: LayoutInflater) = FragmentPlaybackBarBinding.inflate(inflater) @@ -58,9 +57,9 @@ class PlaybackBarFragment : ViewBindingFragment() { // --- UI SETUP --- binding.root.apply { - setOnClickListener { navModel.mainNavigateTo(MainNavigationAction.OpenPlaybackPanel) } + setOnClickListener { playbackModel.openPlayback() } setOnLongClickListener { - playbackModel.song.value?.let(navModel::exploreNavigateTo) + playbackModel.song.value?.let(detailModel::showAlbum) true } } 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 6bcc4114e..0f1c2b57b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -30,15 +30,13 @@ import androidx.appcompat.widget.Toolbar import androidx.core.view.updatePadding import androidx.fragment.app.activityViewModels import dagger.hilt.android.AndroidEntryPoint -import org.oxycblt.auxio.MainFragmentDirections import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding +import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.resolveNames -import org.oxycblt.auxio.navigation.MainNavigationAction -import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.ui.StyledSeekBar import org.oxycblt.auxio.ui.ViewBindingFragment @@ -63,7 +61,7 @@ class PlaybackPanelFragment : StyledSeekBar.Listener { private val playbackModel: PlaybackViewModel by activityViewModels() private val musicModel: MusicViewModel by activityViewModels() - private val navModel: NavigationViewModel by activityViewModels() + private val detailModel: DetailViewModel by activityViewModels() private var equalizerLauncher: ActivityResultLauncher? = null override fun onCreateBinding(inflater: LayoutInflater) = @@ -90,9 +88,7 @@ class PlaybackPanelFragment : } binding.playbackToolbar.apply { - setNavigationOnClickListener { - navModel.mainNavigateTo(MainNavigationAction.ClosePlaybackPanel) - } + setNavigationOnClickListener { playbackModel.openMain() } setOnMenuItemClickListener(this@PlaybackPanelFragment) } @@ -100,7 +96,7 @@ class PlaybackPanelFragment : // respective item. binding.playbackSong.apply { isSelected = true - setOnClickListener { playbackModel.song.value?.let(navModel::exploreNavigateTo) } + setOnClickListener { playbackModel.song.value?.let(detailModel::showAlbum) } } binding.playbackArtist.apply { isSelected = true @@ -176,11 +172,7 @@ class PlaybackPanelFragment : true } R.id.action_song_detail -> { - playbackModel.song.value?.let { song -> - navModel.mainNavigateTo( - MainNavigationAction.Directions( - MainFragmentDirections.actionShowDetails(song.uid))) - } + playbackModel.song.value?.let(detailModel::showSong) true } R.id.action_share -> { @@ -237,12 +229,10 @@ class PlaybackPanelFragment : } private fun navigateToCurrentArtist() { - val song = playbackModel.song.value ?: return - navModel.exploreNavigateToParentArtist(song) + playbackModel.song.value?.let(detailModel::showArtist) } private fun navigateToCurrentAlbum() { - val song = playbackModel.song.value ?: return - navModel.exploreNavigateTo(song.album) + playbackModel.song.value?.let(detailModel::showAlbum) } } 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 115d00344..ea6b7b53a 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -89,6 +89,10 @@ constructor( val isShuffled: StateFlow get() = _isShuffled + private val _openPanel = MutableEvent() + val openPanel: Event + get() = _openPanel + private val _artistPlaybackPickerSong = MutableEvent() /** * Flag signaling to open a picker dialog in order to resolve an ambiguous choice when playing a @@ -555,6 +559,20 @@ constructor( playbackManager.repeatMode = playbackManager.repeatMode.increment() } + // --- UI CONTROL --- + fun openMain() = openImpl(Panel.Main) + fun openPlayback() = openImpl(Panel.Playback) + fun openQueue() = openImpl(Panel.Queue) + + private fun openImpl(panel: Panel) { + val existing = openPanel.flow.value + if (existing != null) { + logD("Already opening $existing, ignoring opening $panel") + return + } + _openPanel.put(panel) + } + // --- SAVE/RESTORE FUNCTIONS --- /** @@ -598,3 +616,9 @@ constructor( } } } + +sealed interface Panel { + object Main : Panel + object Playback : Panel + object Queue : Panel +} 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 0839d12fb..90184814b 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -34,6 +34,8 @@ import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentSearchBinding +import org.oxycblt.auxio.detail.DetailViewModel +import org.oxycblt.auxio.detail.Show import org.oxycblt.auxio.list.Divider import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item @@ -47,7 +49,6 @@ import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately @@ -67,7 +68,7 @@ import org.oxycblt.auxio.util.setFullWidthLookup */ @AndroidEntryPoint class SearchFragment : ListFragment() { - override val navModel: NavigationViewModel by activityViewModels() + override val detailModel: DetailViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels() override val selectionModel: SelectionViewModel by activityViewModels() @@ -137,7 +138,7 @@ class SearchFragment : ListFragment() { collectImmediately(searchModel.searchResults, ::updateSearchResults) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) - collect(navModel.exploreNavigationItem.flow, ::handleNavigation) + collect(detailModel.toShow.flow, ::handleShow) collectImmediately(selectionModel.selected, ::updateSelection) } @@ -167,8 +168,11 @@ class SearchFragment : ListFragment() { override fun onRealClick(item: Music) { when (item) { - is MusicParent -> navModel.exploreNavigateTo(item) is Song -> playbackModel.playFrom(item, searchModel.playbackMode) + is Album -> detailModel.showAlbum(item) + is Artist -> detailModel.showArtist(item) + is Genre -> detailModel.showGenre(item) + is Playlist -> detailModel.showPlaylist(item) } } @@ -200,19 +204,57 @@ class SearchFragment : ListFragment() { searchAdapter.setPlaying(parent ?: song, isPlaying) } - private fun handleNavigation(item: Music?) { - val action = - when (item) { - is Song -> SearchFragmentDirections.actionShowAlbum(item.album.uid) - is Album -> SearchFragmentDirections.actionShowAlbum(item.uid) - is Artist -> SearchFragmentDirections.actionShowArtist(item.uid) - is Genre -> SearchFragmentDirections.actionShowGenre(item.uid) - is Playlist -> SearchFragmentDirections.actionShowPlaylist(item.uid) - null -> return + private fun handleShow(show: Show?) { + when (show) { + is Show.SongDetails -> { + logD("Navigating to ${show.song}") + findNavController().navigateSafe(SearchFragmentDirections.showSong(show.song.uid)) } + + // Songs should be scrolled to if the album matches, or a new detail + // fragment should be launched otherwise. + is Show.SongAlbumDetails -> { + logD("Navigating to the album of ${show.song}") + findNavController() + .navigateSafe(SearchFragmentDirections.showAlbum(show.song.album.uid)) + } + + // If the album matches, no need to do anything. Otherwise launch a new + // detail fragment. + is Show.AlbumDetails -> { + logD("Navigating to ${show.album}") + findNavController().navigateSafe(SearchFragmentDirections.showAlbum(show.album.uid)) + } + + // Always launch a new ArtistDetailFragment. + is Show.ArtistDetails -> { + logD("Navigating to ${show.artist}") + findNavController() + .navigateSafe(SearchFragmentDirections.showArtist(show.artist.uid)) + } + is Show.SongArtistDetails -> { + logD("Navigating to artist choices for ${show.song}") + findNavController().navigateSafe(SearchFragmentDirections.showArtist(show.song.uid)) + } + is Show.AlbumArtistDetails -> { + logD("Navigating to artist choices for ${show.album}") + findNavController() + .navigateSafe(SearchFragmentDirections.showArtist(show.album.uid)) + } + is Show.GenreDetails -> { + logD("Navigating to ${show.genre}") + findNavController().navigateSafe(SearchFragmentDirections.showGenre(show.genre.uid)) + } + is Show.PlaylistDetails -> { + logD("Navigating to ${show.playlist}") + findNavController() + .navigateSafe(SearchFragmentDirections.showGenre(show.playlist.uid)) + } + null -> {} + } + // Keyboard is no longer needed. hideKeyboard() - findNavController().navigateSafe(action) } private fun updateSelection(selected: List) { diff --git a/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt index fe2bf0066..8e8f3d589 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt @@ -55,7 +55,7 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) { override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_music_dirs)) { - findNavController().navigate(RootPreferenceFragmentDirections.goToMusicDirsDialog()) + findNavController().navigate(RootPreferenceFragmentDirections.musicDirsSettings()) } } @@ -66,23 +66,21 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) { when (preference.key) { getString(R.string.set_key_ui) -> { logD("Navigating to UI preferences") - findNavController() - .navigateSafe(RootPreferenceFragmentDirections.goToUiPreferences()) + findNavController().navigateSafe(RootPreferenceFragmentDirections.uiPreferences()) } getString(R.string.set_key_personalize) -> { logD("Navigating to personalization preferences") findNavController() - .navigateSafe(RootPreferenceFragmentDirections.goToPersonalizePreferences()) + .navigateSafe(RootPreferenceFragmentDirections.personalizePreferences()) } getString(R.string.set_key_music) -> { logD("Navigating to music preferences") findNavController() - .navigateSafe(RootPreferenceFragmentDirections.goToMusicPreferences()) + .navigateSafe(RootPreferenceFragmentDirections.musicPreferences()) } getString(R.string.set_key_audio) -> { logD("Navigating to audio preferences") - findNavController() - .navigateSafe(RootPreferenceFragmentDirections.goToAudioPreferences()) + findNavController().navigateSafe(RootPreferenceFragmentDirections.audioPeferences()) } getString(R.string.set_key_reindex) -> musicModel.refresh() getString(R.string.set_key_rescan) -> musicModel.rescan() diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt index 5bcee531f..765c86987 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt @@ -35,7 +35,7 @@ class AudioPreferenceFragment : BasePreferenceFragment(R.xml.preferences_audio) override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_pre_amp)) { logD("Navigating to pre-amp dialog") - findNavController().navigateSafe(AudioPreferenceFragmentDirections.goToPreAmpDialog()) + findNavController().navigateSafe(AudioPreferenceFragmentDirections.preAmpSettings()) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt index 9e1e83af5..19b507f1d 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt @@ -41,8 +41,7 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music) override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_separators)) { logD("Navigating to separator dialog") - findNavController() - .navigateSafe(MusicPreferenceFragmentDirections.goToSeparatorsDialog()) + findNavController().navigateSafe(MusicPreferenceFragmentDirections.separatorsSettings()) } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt index f284e1d69..361ec1162 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt @@ -34,8 +34,7 @@ class PersonalizePreferenceFragment : BasePreferenceFragment(R.xml.preferences_p override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_home_tabs)) { logD("Navigating to home tab dialog") - findNavController() - .navigateSafe(PersonalizePreferenceFragmentDirections.goToTabDialog()) + findNavController().navigateSafe(PersonalizePreferenceFragmentDirections.tabSettings()) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt index 8d7ba5114..b756559dc 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt @@ -43,7 +43,7 @@ class UIPreferenceFragment : BasePreferenceFragment(R.xml.preferences_ui) { override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_accent)) { logD("Navigating to accent dialog") - findNavController().navigateSafe(UIPreferenceFragmentDirections.goToAccentDialog()) + findNavController().navigateSafe(UIPreferenceFragmentDirections.accentSettings()) } } diff --git a/app/src/main/res/layout-w600dp-land/fragment_main.xml b/app/src/main/res/layout-w600dp-land/fragment_main.xml index a8a98f12d..e7213924f 100644 --- a/app/src/main/res/layout-w600dp-land/fragment_main.xml +++ b/app/src/main/res/layout-w600dp-land/fragment_main.xml @@ -13,7 +13,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior" - app:navGraph="@navigation/nav_explore" + app:navGraph="@navigation/main" + app:defaultNavHost="true" tools:layout="@layout/fragment_home" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index 5b2e5c96b..81141391f 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -14,7 +14,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior" - app:navGraph="@navigation/nav_explore" + app:navGraph="@navigation/main" + app:defaultNavHost="true" tools:layout="@layout/fragment_home" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_explore.xml b/app/src/main/res/navigation/nav_explore.xml deleted file mode 100644 index dfce49cb9..000000000 --- a/app/src/main/res/navigation/nav_explore.xml +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml deleted file mode 100644 index 54a1a4f37..000000000 --- a/app/src/main/res/navigation/nav_main.xml +++ /dev/null @@ -1,214 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle index efe210373..5c4648215 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.8.22' - navigation_version = "2.5.0" + navigation_version = "2.6.0" hilt_version = '2.46.1' }