ui: create viewmodel for navigation
Create a ViewModel for the more complicated navigation pathways. Normally, navigation was fragmented along a complicated stretch of fragment hacks and DetailViewModel's navToItem attitbute, both of which were not really that ideal. Dumpster them for a single, unified viewmodel for the more complicated navigation situations. This removes much of the duplicate navigation logic and is likely much more maintainable for future situations.
This commit is contained in:
parent
74f5962844
commit
ab194c14c2
43 changed files with 283 additions and 247 deletions
|
@ -27,12 +27,16 @@ import androidx.activity.result.ActivityResultLauncher
|
|||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.logW
|
||||
|
||||
|
@ -45,13 +49,13 @@ import org.oxycblt.auxio.util.logW
|
|||
*/
|
||||
class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
private val navModel: NavigationViewModel by activityViewModels()
|
||||
private val musicModel: MusicViewModel by activityViewModels()
|
||||
private var callback: DynamicBackPressedCallback? = null
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentMainBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) {
|
||||
|
||||
// --- UI SETUP ---
|
||||
// Build the permission launcher here as you can only do it in onCreateView/onCreate
|
||||
val permLauncher =
|
||||
|
@ -89,6 +93,9 @@ class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
|
|||
handleLoaderResponse(response, permLauncher)
|
||||
}
|
||||
|
||||
navModel.mainNavigationAction.observe(viewLifecycleOwner, ::handleMainNavigation)
|
||||
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleExploreNavigation)
|
||||
|
||||
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
||||
}
|
||||
|
||||
|
@ -146,6 +153,30 @@ class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleMainNavigation(action: MainNavigationAction?) {
|
||||
if (action == null) return
|
||||
|
||||
val binding = requireBinding()
|
||||
when (action) {
|
||||
MainNavigationAction.EXPAND -> binding.bottomSheetLayout.expand()
|
||||
MainNavigationAction.COLLAPSE -> binding.bottomSheetLayout.collapse()
|
||||
MainNavigationAction.SETTINGS ->
|
||||
findNavController().navigate(MainFragmentDirections.actionShowSettings())
|
||||
MainNavigationAction.ABOUT ->
|
||||
findNavController().navigate(MainFragmentDirections.actionShowAbout())
|
||||
MainNavigationAction.QUEUE ->
|
||||
findNavController().navigate(MainFragmentDirections.actionShowQueue())
|
||||
}
|
||||
|
||||
navModel.finishMainNavigation()
|
||||
}
|
||||
|
||||
private fun handleExploreNavigation(item: Music?) {
|
||||
if (item != null) {
|
||||
requireBinding().bottomSheetLayout.collapse()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSong(song: Song?) {
|
||||
val binding = requireBinding()
|
||||
if (song != null) {
|
||||
|
|
|
@ -33,15 +33,15 @@ import org.oxycblt.auxio.util.stateList
|
|||
|
||||
/** An adapter that displays the accent palette. */
|
||||
class AccentAdapter(listener: Listener) :
|
||||
MonoAdapter<Accent, AccentAdapter.Listener, NewAccentViewHolder>(listener) {
|
||||
MonoAdapter<Accent, AccentAdapter.Listener, AccentViewHolder>(listener) {
|
||||
var selectedAccent: Accent? = null
|
||||
private set
|
||||
private var selectedViewHolder: NewAccentViewHolder? = null
|
||||
private var selectedViewHolder: AccentViewHolder? = null
|
||||
|
||||
override val data = AccentData()
|
||||
override val creator = NewAccentViewHolder.CREATOR
|
||||
override val creator = AccentViewHolder.CREATOR
|
||||
|
||||
override fun onBindViewHolder(viewHolder: NewAccentViewHolder, position: Int) {
|
||||
override fun onBindViewHolder(viewHolder: AccentViewHolder, position: Int) {
|
||||
super.onBindViewHolder(viewHolder, position)
|
||||
|
||||
if (data.getItem(position) == selectedAccent) {
|
||||
|
@ -55,7 +55,7 @@ class AccentAdapter(listener: Listener) :
|
|||
if (accent == selectedAccent) return
|
||||
selectedAccent = accent
|
||||
selectedViewHolder?.setSelected(false)
|
||||
selectedViewHolder = recycler.getViewHolderAt(accent.index) as NewAccentViewHolder?
|
||||
selectedViewHolder = recycler.getViewHolderAt(accent.index) as AccentViewHolder?
|
||||
selectedViewHolder?.setSelected(true)
|
||||
}
|
||||
|
||||
|
@ -69,7 +69,7 @@ class AccentAdapter(listener: Listener) :
|
|||
}
|
||||
}
|
||||
|
||||
class NewAccentViewHolder private constructor(private val binding: ItemAccentBinding) :
|
||||
class AccentViewHolder private constructor(private val binding: ItemAccentBinding) :
|
||||
BindingViewHolder<Accent, AccentAdapter.Listener>(binding.root) {
|
||||
|
||||
override fun bind(item: Accent, listener: AccentAdapter.Listener) {
|
||||
|
@ -79,9 +79,8 @@ class NewAccentViewHolder private constructor(private val binding: ItemAccentBin
|
|||
backgroundTintList = context.getColorSafe(item.primary).stateList
|
||||
contentDescription = context.getString(item.name)
|
||||
TooltipCompat.setTooltipText(this, contentDescription)
|
||||
setOnClickListener { listener.onAccentSelected(item) }
|
||||
}
|
||||
|
||||
binding.accent.setOnClickListener { listener.onAccentSelected(item) }
|
||||
}
|
||||
|
||||
fun setSelected(isSelected: Boolean) {
|
||||
|
@ -98,12 +97,12 @@ class NewAccentViewHolder private constructor(private val binding: ItemAccentBin
|
|||
|
||||
companion object {
|
||||
val CREATOR =
|
||||
object : Creator<NewAccentViewHolder> {
|
||||
object : Creator<AccentViewHolder> {
|
||||
override val viewType: Int
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override fun create(context: Context) =
|
||||
NewAccentViewHolder(ItemAccentBinding.inflate(context.inflater))
|
||||
AccentViewHolder(ItemAccentBinding.inflate(context.inflater))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import coil.decode.ImageSource
|
|||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.key.Keyer
|
||||
import coil.request.Options
|
||||
import coil.size.Size
|
||||
import kotlin.math.min
|
||||
|
@ -32,9 +33,22 @@ import okio.source
|
|||
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.Song
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
|
||||
/** A basic keyer for music data. */
|
||||
class MusicKeyer : Keyer<Music> {
|
||||
override fun key(data: Music, options: Options): String {
|
||||
return if (data is Song) {
|
||||
// Group up song covers with album covers for better caching
|
||||
key(data.album, options)
|
||||
} else {
|
||||
"${data::class.simpleName}: ${data.id}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetcher that returns the album art for a given [Album] or [Song], depending on the factory used.
|
||||
* @author OxygenCobalt
|
||||
|
@ -128,7 +142,7 @@ private inline fun <T : Any, R : Any> Collection<T>.mapAtMost(
|
|||
break
|
||||
}
|
||||
|
||||
transform(item)?.let { out.add(it) }
|
||||
transform(item)?.let(out::add)
|
||||
}
|
||||
|
||||
return out
|
|
@ -1,35 +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.coil
|
||||
|
||||
import coil.key.Keyer
|
||||
import coil.request.Options
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
||||
/** A basic keyer for music data. */
|
||||
class MusicKeyer : Keyer<Music> {
|
||||
override fun key(data: Music, options: Options): String {
|
||||
return if (data is Song) {
|
||||
// Group up song covers with album covers for better caching
|
||||
key(data.album, options)
|
||||
} else {
|
||||
"${data::class.simpleName}: ${data.id}"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -44,7 +44,10 @@ import org.oxycblt.auxio.music.Song
|
|||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.util.getColorStateListSafe
|
||||
|
||||
/** An [AppCompatImageView] that applies many of the stylistic choices thjat Auxio uses wi */
|
||||
/**
|
||||
* An [AppCompatImageView] that applies many of the stylistic choices that Auxio uses regarding
|
||||
* images.
|
||||
*/
|
||||
class StyledImageView
|
||||
@JvmOverloads
|
||||
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||
|
@ -85,6 +88,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
|
||||
// Scale the image down to half-size
|
||||
imageMatrix =
|
||||
centerMatrix.apply {
|
||||
reset()
|
||||
|
|
|
@ -81,16 +81,9 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
|||
|
||||
// -- VIEWMODEL SETUP ---
|
||||
|
||||
detailModel.albumData.observe(viewLifecycleOwner) { list ->
|
||||
detailAdapter.data.submitList(list)
|
||||
}
|
||||
|
||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
||||
handleNavigation(item, detailAdapter)
|
||||
}
|
||||
|
||||
updateSong(playbackModel.song.value, detailAdapter)
|
||||
playbackModel.song.observe(viewLifecycleOwner) { song -> updateSong(song, detailAdapter) }
|
||||
detailModel.albumData.observe(viewLifecycleOwner, detailAdapter.data::submitList)
|
||||
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
|
||||
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
|
@ -126,7 +119,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
|||
unlikelyToBeNull(detailModel.currentAlbum.value).artist.id))
|
||||
}
|
||||
|
||||
private fun handleNavigation(item: Music?, adapter: AlbumDetailAdapter) {
|
||||
private fun handleNavigation(item: Music?) {
|
||||
val binding = requireBinding()
|
||||
when (item) {
|
||||
// Songs should be scrolled to if the album matches, or a new detail
|
||||
|
@ -134,8 +127,8 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
|||
is Song -> {
|
||||
if (unlikelyToBeNull(detailModel.currentAlbum.value).id == item.album.id) {
|
||||
logD("Navigating to a song in this album")
|
||||
scrollToItem(item.id, adapter)
|
||||
detailModel.finishNavToItem()
|
||||
scrollToItem(item.id)
|
||||
navModel.finishExploreNavigation()
|
||||
} else {
|
||||
logD("Navigating to another album")
|
||||
findNavController()
|
||||
|
@ -149,7 +142,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
|||
if (unlikelyToBeNull(detailModel.currentAlbum.value).id == item.id) {
|
||||
logD("Navigating to the top of this album")
|
||||
binding.detailRecycler.scrollToPosition(0)
|
||||
detailModel.finishNavToItem()
|
||||
navModel.finishExploreNavigation()
|
||||
} else {
|
||||
logD("Navigating to another album")
|
||||
findNavController()
|
||||
|
@ -169,9 +162,9 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
|||
}
|
||||
|
||||
/** Scroll to an song using its [id]. */
|
||||
private fun scrollToItem(id: Long, adapter: AlbumDetailAdapter) {
|
||||
private fun scrollToItem(id: Long) {
|
||||
// Calculate where the item for the currently played song is
|
||||
val pos = adapter.data.currentList.indexOfFirst { it.id == id && it is Song }
|
||||
val pos = detailAdapter.data.currentList.indexOfFirst { it.id == id && it is Song }
|
||||
|
||||
if (pos != -1) {
|
||||
val binding = requireBinding()
|
||||
|
@ -189,7 +182,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
|||
}
|
||||
|
||||
/** Updates the queue actions when a song is present or not */
|
||||
private fun updateSong(song: Song?, adapter: AlbumDetailAdapter) {
|
||||
private fun updateSong(song: Song?) {
|
||||
val binding = requireBinding()
|
||||
|
||||
for (item in binding.detailToolbar.menu.children) {
|
||||
|
@ -200,10 +193,10 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
|||
|
||||
if (playbackModel.playbackMode.value == PlaybackMode.IN_ALBUM &&
|
||||
playbackModel.parent.value?.id == unlikelyToBeNull(detailModel.currentAlbum.value).id) {
|
||||
adapter.highlightSong(song, binding.detailRecycler)
|
||||
detailAdapter.highlightSong(song, binding.detailRecycler)
|
||||
} else {
|
||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||
adapter.highlightSong(null, binding.detailRecycler)
|
||||
detailAdapter.highlightSong(null, binding.detailRecycler)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -63,27 +63,16 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
|||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
detailModel.artistData.observe(viewLifecycleOwner) { list ->
|
||||
detailAdapter.data.submitList(list)
|
||||
}
|
||||
|
||||
detailModel.navToItem.observe(viewLifecycleOwner, ::handleNavigation)
|
||||
|
||||
// Highlight songs if they are being played
|
||||
playbackModel.song.observe(viewLifecycleOwner) { song -> updateSong(song, detailAdapter) }
|
||||
|
||||
// Highlight albums if they are being played
|
||||
playbackModel.parent.observe(viewLifecycleOwner) { parent ->
|
||||
updateParent(parent, detailAdapter)
|
||||
}
|
||||
detailModel.artistData.observe(viewLifecycleOwner, detailAdapter.data::submitList)
|
||||
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
|
||||
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
||||
playbackModel.parent.observe(viewLifecycleOwner, ::updateParent)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
when (item) {
|
||||
is Song -> playbackModel.playSong(item, PlaybackMode.IN_ARTIST)
|
||||
is Album ->
|
||||
findNavController()
|
||||
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.id))
|
||||
is Album -> navModel.exploreNavigateTo(item)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,49 +100,49 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
|||
val binding = requireBinding()
|
||||
|
||||
when (item) {
|
||||
is Artist -> {
|
||||
if (item.id == detailModel.currentArtist.value?.id) {
|
||||
logD("Navigating to the top of this artist")
|
||||
binding.detailRecycler.scrollToPosition(0)
|
||||
detailModel.finishNavToItem()
|
||||
} else {
|
||||
logD("Navigating to another artist")
|
||||
is Song -> {
|
||||
logD("Navigating to another album")
|
||||
findNavController()
|
||||
.navigate(ArtistDetailFragmentDirections.actionShowArtist(item.id))
|
||||
}
|
||||
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.album.id))
|
||||
}
|
||||
is Album -> {
|
||||
logD("Navigating to another album")
|
||||
findNavController()
|
||||
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.id))
|
||||
}
|
||||
is Song -> {
|
||||
logD("Navigating to another album")
|
||||
is Artist -> {
|
||||
if (item.id == detailModel.currentArtist.value?.id) {
|
||||
logD("Navigating to the top of this artist")
|
||||
binding.detailRecycler.scrollToPosition(0)
|
||||
navModel.finishExploreNavigation()
|
||||
} else {
|
||||
logD("Navigating to another artist")
|
||||
findNavController()
|
||||
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.album.id))
|
||||
.navigate(ArtistDetailFragmentDirections.actionShowArtist(item.id))
|
||||
}
|
||||
}
|
||||
null -> {}
|
||||
else -> logW("Unsupported navigation item ${item::class.java}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSong(song: Song?, adapter: ArtistDetailAdapter) {
|
||||
private fun updateSong(song: Song?) {
|
||||
val binding = requireBinding()
|
||||
if (playbackModel.playbackMode.value == PlaybackMode.IN_ARTIST &&
|
||||
playbackModel.parent.value?.id == detailModel.currentArtist.value?.id) {
|
||||
adapter.highlightSong(song, binding.detailRecycler)
|
||||
detailAdapter.highlightSong(song, binding.detailRecycler)
|
||||
} else {
|
||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||
adapter.highlightSong(null, binding.detailRecycler)
|
||||
detailAdapter.highlightSong(null, binding.detailRecycler)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateParent(parent: MusicParent?, adapter: ArtistDetailAdapter) {
|
||||
private fun updateParent(parent: MusicParent?) {
|
||||
val binding = requireBinding()
|
||||
if (parent is Album?) {
|
||||
adapter.highlightAlbum(parent, binding.detailRecycler)
|
||||
detailAdapter.highlightAlbum(parent, binding.detailRecycler)
|
||||
} else {
|
||||
adapter.highlightAlbum(null, binding.detailRecycler)
|
||||
detailAdapter.highlightAlbum(null, binding.detailRecycler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -40,16 +41,12 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
|||
*/
|
||||
abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
|
||||
protected val detailModel: DetailViewModel by activityViewModels()
|
||||
protected val navModel: NavigationViewModel by activityViewModels()
|
||||
protected val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater): FragmentDetailBinding =
|
||||
FragmentDetailBinding.inflate(inflater)
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
detailModel.setNavigating(false)
|
||||
}
|
||||
|
||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.detailRecycler.adapter = null
|
||||
|
|
|
@ -25,7 +25,6 @@ import org.oxycblt.auxio.detail.recycler.SortHeader
|
|||
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.MusicStore
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.ui.Header
|
||||
|
@ -38,7 +37,6 @@ import org.oxycblt.auxio.util.logD
|
|||
* - What item the fragment should be showing
|
||||
* - The RecyclerView data for each fragment
|
||||
* - Menu triggers for each fragment
|
||||
* - Navigation triggers for each fragment [e.g "Go to artist"]
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class DetailViewModel : ViewModel() {
|
||||
|
@ -87,15 +85,6 @@ class DetailViewModel : ViewModel() {
|
|||
currentGenre.value?.let(::refreshGenreData)
|
||||
}
|
||||
|
||||
private val mNavToItem = MutableLiveData<Music?>()
|
||||
|
||||
/** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */
|
||||
val navToItem: LiveData<Music?>
|
||||
get() = mNavToItem
|
||||
|
||||
var isNavigating = false
|
||||
private set
|
||||
|
||||
fun setAlbumId(id: Long) {
|
||||
if (mCurrentAlbum.value?.id == id) return
|
||||
val musicStore = MusicStore.requireInstance()
|
||||
|
@ -122,21 +111,6 @@ class DetailViewModel : ViewModel() {
|
|||
refreshGenreData(genre)
|
||||
}
|
||||
|
||||
/** Navigate to an item, whether a song/album/artist */
|
||||
fun navToItem(item: Music) {
|
||||
mNavToItem.value = item
|
||||
}
|
||||
|
||||
/** Mark that the navigation process is done. */
|
||||
fun finishNavToItem() {
|
||||
mNavToItem.value = null
|
||||
}
|
||||
|
||||
/** Update the current navigation status to [isNavigating] */
|
||||
fun setNavigating(navigating: Boolean) {
|
||||
isNavigating = navigating
|
||||
}
|
||||
|
||||
private fun refreshGenreData(genre: Genre) {
|
||||
logD("Refreshing genre data")
|
||||
val data = mutableListOf<Item>(genre)
|
||||
|
|
|
@ -36,7 +36,6 @@ import org.oxycblt.auxio.ui.Item
|
|||
import org.oxycblt.auxio.ui.newMenu
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
||||
/**
|
||||
|
@ -61,13 +60,9 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
|||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
detailModel.genreData.observe(viewLifecycleOwner) { list ->
|
||||
detailAdapter.data.submitList(list)
|
||||
}
|
||||
|
||||
detailModel.navToItem.observe(viewLifecycleOwner, ::handleNavigation)
|
||||
|
||||
playbackModel.song.observe(viewLifecycleOwner) { song -> updateSong(song, detailAdapter) }
|
||||
detailModel.genreData.observe(viewLifecycleOwner, detailAdapter.data::submitList)
|
||||
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
|
||||
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
|
@ -97,34 +92,36 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
|||
|
||||
private fun handleNavigation(item: Music?) {
|
||||
when (item) {
|
||||
is Song -> {
|
||||
logD("Navigating to another song")
|
||||
findNavController()
|
||||
.navigate(GenreDetailFragmentDirections.actionShowAlbum(item.album.id))
|
||||
}
|
||||
is Album -> {
|
||||
logD("Navigating to another album")
|
||||
findNavController().navigate(GenreDetailFragmentDirections.actionShowAlbum(item.id))
|
||||
}
|
||||
// All items will launch new detail fragments.
|
||||
is Artist -> {
|
||||
logD("Navigating to another artist")
|
||||
findNavController()
|
||||
.navigate(GenreDetailFragmentDirections.actionShowArtist(item.id))
|
||||
}
|
||||
is Album -> {
|
||||
logD("Navigating to another album")
|
||||
findNavController().navigate(GenreDetailFragmentDirections.actionShowAlbum(item.id))
|
||||
}
|
||||
is Song -> {
|
||||
logD("Navigating to another song")
|
||||
findNavController()
|
||||
.navigate(GenreDetailFragmentDirections.actionShowAlbum(item.album.id))
|
||||
is Genre -> {
|
||||
navModel.finishExploreNavigation()
|
||||
}
|
||||
null -> {}
|
||||
else -> logW("Unsupported navigation command ${item::class.java}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSong(song: Song?, adapter: GenreDetailAdapter) {
|
||||
private fun updateSong(song: Song?) {
|
||||
val binding = requireBinding()
|
||||
if (playbackModel.playbackMode.value == PlaybackMode.IN_GENRE &&
|
||||
playbackModel.parent.value?.id == unlikelyToBeNull(detailModel.currentGenre.value).id) {
|
||||
adapter.highlightSong(song, binding.detailRecycler)
|
||||
detailAdapter.highlightSong(song, binding.detailRecycler)
|
||||
} else {
|
||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||
adapter.highlightSong(null, binding.detailRecycler)
|
||||
detailAdapter.highlightSong(null, binding.detailRecycler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,10 +28,8 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
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.home.list.AlbumListFragment
|
||||
import org.oxycblt.auxio.home.list.ArtistListFragment
|
||||
import org.oxycblt.auxio.home.list.GenreListFragment
|
||||
|
@ -45,6 +43,8 @@ import org.oxycblt.auxio.music.MusicViewModel
|
|||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logE
|
||||
|
@ -62,7 +62,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
|||
*/
|
||||
class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
private val navModel: NavigationViewModel by activityViewModels()
|
||||
private val homeModel: HomeViewModel by activityViewModels()
|
||||
private val musicModel: MusicViewModel by activityViewModels()
|
||||
|
||||
|
@ -108,7 +108,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
|
|||
homeModel.recreateTabs.observe(viewLifecycleOwner, ::handleRecreateTabs)
|
||||
|
||||
musicModel.loaderResponse.observe(viewLifecycleOwner, ::handleLoaderResponse)
|
||||
detailModel.navToItem.observe(viewLifecycleOwner, ::handleNavigation)
|
||||
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
|
||||
}
|
||||
|
||||
private fun onMenuClick(item: MenuItem) {
|
||||
|
@ -119,19 +119,15 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
|
|||
}
|
||||
R.id.action_settings -> {
|
||||
logD("Navigating to settings")
|
||||
parentFragment
|
||||
?.parentFragment
|
||||
?.findNavController()
|
||||
?.navigate(MainFragmentDirections.actionShowSettings())
|
||||
navModel.mainNavigateTo(MainNavigationAction.SETTINGS)
|
||||
}
|
||||
R.id.action_about -> {
|
||||
logD("Navigating to about")
|
||||
parentFragment
|
||||
?.parentFragment
|
||||
?.findNavController()
|
||||
?.navigate(MainFragmentDirections.actionShowAbout())
|
||||
navModel.mainNavigateTo(MainNavigationAction.ABOUT)
|
||||
}
|
||||
R.id.submenu_sorting -> {
|
||||
// Junk click event when opening the menu
|
||||
}
|
||||
R.id.submenu_sorting -> {}
|
||||
R.id.option_sort_asc -> {
|
||||
item.isChecked = !item.isChecked
|
||||
homeModel.updateCurrentSort(
|
||||
|
|
|
@ -18,11 +18,10 @@
|
|||
package org.oxycblt.auxio.home.list
|
||||
|
||||
import android.view.View
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.home.HomeFragmentDirections
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.ui.AlbumViewHolder
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
|
@ -70,8 +69,8 @@ class AlbumListFragment : HomeListFragment<Album>() {
|
|||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
check(item is Album)
|
||||
findNavController().navigate(HomeFragmentDirections.actionShowAlbum(item.id))
|
||||
check(item is Music)
|
||||
navModel.exploreNavigateTo(item)
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
|
|
|
@ -18,11 +18,10 @@
|
|||
package org.oxycblt.auxio.home.list
|
||||
|
||||
import android.view.View
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.home.HomeFragmentDirections
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.ui.ArtistViewHolder
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
|
@ -56,8 +55,8 @@ class ArtistListFragment : HomeListFragment<Artist>() {
|
|||
.uppercase()
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
check(item is Artist)
|
||||
findNavController().navigate(HomeFragmentDirections.actionShowArtist(item.id))
|
||||
check(item is Music)
|
||||
navModel.exploreNavigateTo(item)
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
|
|
|
@ -18,11 +18,10 @@
|
|||
package org.oxycblt.auxio.home.list
|
||||
|
||||
import android.view.View
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.home.HomeFragmentDirections
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.ui.GenreViewHolder
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
|
@ -56,8 +55,8 @@ class GenreListFragment : HomeListFragment<Genre>() {
|
|||
.uppercase()
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
check(item is Genre)
|
||||
findNavController().navigate(HomeFragmentDirections.actionShowGenre(item.id))
|
||||
check(item is Music)
|
||||
navModel.exploreNavigateTo(item)
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
|||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
|
||||
/**
|
||||
|
@ -41,8 +42,9 @@ abstract class HomeListFragment<T : Item> :
|
|||
FastScrollRecyclerView.OnFastScrollListener {
|
||||
abstract fun setupRecycler(recycler: RecyclerView)
|
||||
|
||||
protected val homeModel: HomeViewModel by activityViewModels()
|
||||
protected val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
protected val navModel: NavigationViewModel by activityViewModels()
|
||||
protected val homeModel: HomeViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||
FragmentHomeListBinding.inflate(inflater)
|
||||
|
|
|
@ -54,7 +54,7 @@ sealed class Tab(open val mode: DisplayMode) {
|
|||
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100
|
||||
|
||||
/**
|
||||
* Maps between the integer code in the tab sequence and the actual [DisplayMode] instance
|
||||
* Maps between the integer code in the tab sequence and the actual [DisplayMode] instance.
|
||||
*/
|
||||
private val MODE_TABLE =
|
||||
arrayOf(
|
||||
|
|
|
@ -27,8 +27,8 @@ import com.google.android.material.color.MaterialColors
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.coil.bindAlbumCover
|
||||
import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding
|
||||
import org.oxycblt.auxio.detail.DetailViewModel
|
||||
import org.oxycblt.auxio.ui.BottomSheetLayout
|
||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.getAttrColorSafe
|
||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||
|
@ -36,7 +36,7 @@ import org.oxycblt.auxio.util.textSafe
|
|||
|
||||
class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
private val navModel: NavigationViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||
FragmentPlaybackBarBinding.inflate(inflater)
|
||||
|
@ -46,14 +46,10 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
|
|||
savedInstanceState: Bundle?
|
||||
) {
|
||||
binding.root.apply {
|
||||
setOnClickListener {
|
||||
// This is a dumb and fragile hack but this fragment isn't part of the navigation
|
||||
// stack so we can't really do much
|
||||
(requireView().parent.parent.parent as BottomSheetLayout).expand()
|
||||
}
|
||||
setOnClickListener { navModel.mainNavigateTo(MainNavigationAction.EXPAND) }
|
||||
|
||||
setOnLongClickListener {
|
||||
playbackModel.song.value?.let { song -> detailModel.navToItem(song) }
|
||||
playbackModel.song.value?.let(navModel::exploreNavigateTo)
|
||||
true
|
||||
}
|
||||
|
||||
|
|
|
@ -23,11 +23,9 @@ import android.view.MenuItem
|
|||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.slider.Slider
|
||||
import kotlin.math.max
|
||||
import org.oxycblt.auxio.MainFragmentDirections
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.coil.bindAlbumCover
|
||||
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
|
||||
|
@ -36,7 +34,8 @@ import org.oxycblt.auxio.music.MusicParent
|
|||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.toDuration
|
||||
import org.oxycblt.auxio.playback.state.LoopMode
|
||||
import org.oxycblt.auxio.ui.BottomSheetLayout
|
||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.getAttrColorSafe
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -56,6 +55,7 @@ class PlaybackPanelFragment :
|
|||
Slider.OnChangeListener,
|
||||
Slider.OnSliderTouchListener {
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
private val navModel: NavigationViewModel by activityViewModels()
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||
|
@ -75,11 +75,11 @@ class PlaybackPanelFragment :
|
|||
val queueItem: MenuItem
|
||||
|
||||
binding.playbackToolbar.apply {
|
||||
setNavigationOnClickListener { navigateUp() }
|
||||
setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.COLLAPSE) }
|
||||
|
||||
setOnMenuItemClickListener { item ->
|
||||
if (item.itemId == R.id.action_queue) {
|
||||
findNavController().navigate(MainFragmentDirections.actionShowQueue())
|
||||
navModel.mainNavigateTo(MainNavigationAction.QUEUE)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
@ -92,15 +92,15 @@ class PlaybackPanelFragment :
|
|||
binding.playbackSong.apply {
|
||||
// Make marquee of the song title work
|
||||
isSelected = true
|
||||
setOnClickListener { playbackModel.song.value?.let { detailModel.navToItem(it) } }
|
||||
setOnClickListener { playbackModel.song.value?.let(navModel::exploreNavigateTo) }
|
||||
}
|
||||
|
||||
binding.playbackArtist.setOnClickListener {
|
||||
playbackModel.song.value?.let { detailModel.navToItem(it.album.artist) }
|
||||
playbackModel.song.value?.let { navModel.exploreNavigateTo(it.album.artist) }
|
||||
}
|
||||
|
||||
binding.playbackAlbum.setOnClickListener {
|
||||
playbackModel.song.value?.let { detailModel.navToItem(it.album) }
|
||||
playbackModel.song.value?.let { navModel.exploreNavigateTo(it.album) }
|
||||
}
|
||||
|
||||
binding.playbackSeekBar.apply {
|
||||
|
@ -141,12 +141,6 @@ class PlaybackPanelFragment :
|
|||
queueItem.isEnabled = nextUp.isNotEmpty()
|
||||
}
|
||||
|
||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
||||
if (item != null) {
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
logD("Fragment Created")
|
||||
}
|
||||
|
||||
|
@ -218,10 +212,4 @@ class PlaybackPanelFragment :
|
|||
private fun updateShuffle(isShuffling: Boolean) {
|
||||
requireBinding().playbackShuffle.isActivated = isShuffling
|
||||
}
|
||||
|
||||
private fun navigateUp() {
|
||||
// This is a dumb and fragile hack but this fragment isn't part of the navigation stack
|
||||
// so we can't really do much
|
||||
(requireView().parent.parent.parent as BottomSheetLayout).collapse()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,6 @@ import androidx.fragment.app.viewModels
|
|||
import androidx.navigation.fragment.findNavController
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentSearchBinding
|
||||
import org.oxycblt.auxio.detail.DetailViewModel
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
|
@ -41,11 +40,11 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
|
|||
import org.oxycblt.auxio.ui.Header
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
import org.oxycblt.auxio.ui.newMenu
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.requireAttached
|
||||
|
||||
/**
|
||||
|
@ -56,7 +55,7 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
|
|||
// SearchViewModel is only scoped to this Fragment
|
||||
private val searchModel: SearchViewModel by viewModels()
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
private val navModel: NavigationViewModel by activityViewModels()
|
||||
|
||||
private val searchAdapter = SearchAdapter(this)
|
||||
private var imm: InputMethodManager? = null
|
||||
|
@ -109,11 +108,7 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
|
|||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
searchModel.searchResults.observe(viewLifecycleOwner, ::updateResults)
|
||||
|
||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
||||
handleNavigation(item)
|
||||
requireImm().hide()
|
||||
}
|
||||
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -128,25 +123,9 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
|
|||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
if (item is Song) {
|
||||
playbackModel.playSong(item)
|
||||
return
|
||||
}
|
||||
|
||||
if (item is MusicParent && !searchModel.isNavigating) {
|
||||
searchModel.setNavigating(true)
|
||||
|
||||
logD("Navigating to the detail fragment for ${item.rawName}")
|
||||
|
||||
findNavController()
|
||||
.navigate(
|
||||
when (item) {
|
||||
is Genre -> SearchFragmentDirections.actionShowGenre(item.id)
|
||||
is Artist -> SearchFragmentDirections.actionShowArtist(item.id)
|
||||
is Album -> SearchFragmentDirections.actionShowAlbum(item.id)
|
||||
})
|
||||
|
||||
requireImm().hide()
|
||||
is Song -> playbackModel.playSong(item)
|
||||
is MusicParent -> navModel.exploreNavigateTo(item)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -178,8 +157,11 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
|
|||
is Song -> SearchFragmentDirections.actionShowAlbum(item.album.id)
|
||||
is Album -> SearchFragmentDirections.actionShowAlbum(item.id)
|
||||
is Artist -> SearchFragmentDirections.actionShowArtist(item.id)
|
||||
is Genre -> SearchFragmentDirections.actionShowGenre(item.id)
|
||||
else -> return
|
||||
})
|
||||
|
||||
requireImm().hide()
|
||||
}
|
||||
|
||||
private fun requireImm(): InputMethodManager {
|
||||
|
|
|
@ -47,8 +47,6 @@ class SearchViewModel : ViewModel() {
|
|||
/** Current search results from the last [search] call. */
|
||||
val searchResults: LiveData<List<Item>>
|
||||
get() = mSearchResults
|
||||
val isNavigating: Boolean
|
||||
get() = mIsNavigating
|
||||
val filterMode: DisplayMode?
|
||||
get() = mFilterMode
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ import androidx.core.view.children
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.detail.DetailViewModel
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
|
@ -70,8 +69,8 @@ class ActionMenu(
|
|||
private val context = activity.applicationContext
|
||||
|
||||
// Get ViewModels using the activity as the store owner
|
||||
private val detailModel: DetailViewModel by lazy {
|
||||
ViewModelProvider(activity)[DetailViewModel::class.java]
|
||||
private val navModel: NavigationViewModel by lazy {
|
||||
ViewModelProvider(activity)[NavigationViewModel::class.java]
|
||||
}
|
||||
|
||||
private val playbackModel: PlaybackViewModel by lazy {
|
||||
|
@ -172,14 +171,14 @@ class ActionMenu(
|
|||
}
|
||||
R.id.action_go_album -> {
|
||||
if (data is Song) {
|
||||
detailModel.navToItem(data.album)
|
||||
navModel.exploreNavigateTo(data.album)
|
||||
}
|
||||
}
|
||||
R.id.action_go_artist -> {
|
||||
if (data is Song) {
|
||||
detailModel.navToItem(data.album.artist)
|
||||
navModel.exploreNavigateTo(data.album.artist)
|
||||
} else if (data is Album) {
|
||||
detailModel.navToItem(data.artist)
|
||||
navModel.exploreNavigateTo(data.artist)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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.ui
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.oxycblt.auxio.music.Music
|
||||
|
||||
/**
|
||||
* A ViewModel that handles complicated navigation situations.
|
||||
*/
|
||||
class NavigationViewModel : ViewModel() {
|
||||
private val mMainNavigationAction = MutableLiveData<MainNavigationAction?>()
|
||||
/** Flag for main fragment navigation. Intended for MainFragment use only. */
|
||||
val mainNavigationAction: LiveData<MainNavigationAction?>
|
||||
get() = mMainNavigationAction
|
||||
|
||||
private val mExploreNavigationItem = MutableLiveData<Music?>()
|
||||
/** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */
|
||||
val exploreNavigationItem: LiveData<Music?>
|
||||
get() = mExploreNavigationItem
|
||||
|
||||
/**
|
||||
* Notify MainFragment to navigate to the location outlined in [MainNavigationAction].
|
||||
*/
|
||||
fun mainNavigateTo(action: MainNavigationAction) {
|
||||
if (mMainNavigationAction.value != null) return
|
||||
mMainNavigationAction.value = action
|
||||
}
|
||||
|
||||
/** Mark that the main navigation process is done. */
|
||||
fun finishMainNavigation() {
|
||||
mMainNavigationAction.value = null
|
||||
}
|
||||
|
||||
/** Navigate to an item's detail menu, whether a song/album/artist */
|
||||
fun exploreNavigateTo(item: Music) {
|
||||
if (mExploreNavigationItem.value != null) return
|
||||
mExploreNavigationItem.value = item
|
||||
}
|
||||
|
||||
/** Mark that the item navigation process is done. */
|
||||
fun finishExploreNavigation() {
|
||||
mExploreNavigationItem.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the navigation options for the Main Fragment, which tends to be multiple layers above
|
||||
* normal fragments. This can be passed to [NavigationViewModel.mainNavigateTo] in order to
|
||||
* facilitate navigation without stupid fragment hacks.
|
||||
*/
|
||||
enum class MainNavigationAction {
|
||||
/** Expand the playback panel. */
|
||||
EXPAND,
|
||||
/** Collapse the playback panel. */
|
||||
COLLAPSE,
|
||||
/** Go to settings. */
|
||||
SETTINGS,
|
||||
/** Go to the about page. */
|
||||
ABOUT,
|
||||
/** Go to the queue. */
|
||||
QUEUE
|
||||
}
|
|
@ -94,6 +94,7 @@
|
|||
app:thumbRadius="@dimen/slider_thumb_radius" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playback_position"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_medium"
|
||||
|
@ -105,6 +106,7 @@
|
|||
tools:text="11:38" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playback_duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_small_inv"
|
||||
|
|
|
@ -92,6 +92,7 @@
|
|||
app:thumbRadius="@dimen/slider_thumb_radius" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playback_position"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_medium"
|
||||
|
@ -103,6 +104,7 @@
|
|||
tools:text="11:38" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playback_duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_small_inv"
|
||||
|
|
|
@ -81,6 +81,7 @@
|
|||
app:thumbRadius="@dimen/slider_thumb_radius" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playback_position"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_medium"
|
||||
|
@ -92,6 +93,7 @@
|
|||
tools:text="11:38" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playback_duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_small_inv"
|
||||
|
|
|
@ -94,6 +94,7 @@
|
|||
app:thumbRadius="@dimen/slider_thumb_radius" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playback_position"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_medium"
|
||||
|
@ -105,6 +106,7 @@
|
|||
tools:text="11:38" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playback_duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_small_inv"
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/accent_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:overScrollMode="never"
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
android:orientation="vertical"
|
||||
android:paddingTop="@dimen/spacing_small">
|
||||
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/excluded_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="center"
|
||||
|
@ -19,6 +19,7 @@
|
|||
tools:listitem="@layout/item_excluded_dir" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/excluded_empty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/spacing_medium"
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/tab_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:overScrollMode="never"
|
||||
|
|
|
@ -8,16 +8,19 @@
|
|||
tools:context=".settings.AboutFragment">
|
||||
|
||||
<org.oxycblt.auxio.ui.EdgeAppBarLayout
|
||||
android:id="@+id/about_appbar"
|
||||
style="@style/Widget.Auxio.AppBarLayout"
|
||||
app:liftOnScroll="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/about_toolbar"
|
||||
style="@style/Widget.Auxio.Toolbar.Icon"
|
||||
app:title="@string/lbl_about" />
|
||||
|
||||
</org.oxycblt.auxio.ui.EdgeAppBarLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/about_contents"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
android:layout_height="match_parent">
|
||||
|
||||
<org.oxycblt.auxio.detail.DetailAppBarLayout
|
||||
android:id="@+id/detail_appbar"
|
||||
style="@style/Widget.Auxio.AppBarLayout"
|
||||
app:liftOnScroll="true"
|
||||
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
||||
|
|
|
@ -6,15 +6,18 @@
|
|||
android:layout_height="match_parent">
|
||||
|
||||
<org.oxycblt.auxio.ui.EdgeAppBarLayout
|
||||
android:id="@+id/home_appbar"
|
||||
style="@style/Widget.Auxio.AppBarLayout"
|
||||
app:liftOnScroll="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/home_toolbar"
|
||||
style="@style/Widget.Auxio.Toolbar"
|
||||
app:menu="@menu/menu_home"
|
||||
app:title="@string/info_app_name" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/home_tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
|
@ -40,6 +43,7 @@
|
|||
app:layout_anchorGravity="bottom|end">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/home_fab"
|
||||
style="@style/Widget.Auxio.FloatingActionButton.Adaptive"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/Widget.Auxio.RecyclerView.WithAdaptiveFab"
|
||||
android:id="@+id/home_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
android:layout_height="match_parent">
|
||||
|
||||
<org.oxycblt.auxio.ui.BottomSheetLayout
|
||||
android:id="@+id/bottom_sheet_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
@ -32,6 +33,7 @@
|
|||
</org.oxycblt.auxio.ui.BottomSheetLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/layout_too_small"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface"
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
app:thumbRadius="@dimen/slider_thumb_radius" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playback_position"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_medium"
|
||||
|
@ -89,6 +90,7 @@
|
|||
tools:text="11:38" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playback_duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_small_inv"
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
app:liftOnScrollTargetViewId="@id/queue_recycler">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/queue_toolbar"
|
||||
style="@style/Widget.Auxio.Toolbar.Icon.Down"
|
||||
android:elevation="0dp"
|
||||
app:navigationIcon="@drawable/ic_down"
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
app:liftOnScrollTargetViewId="@id/search_recycler">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/search_toolbar"
|
||||
style="@style/Widget.Auxio.Toolbar.Icon"
|
||||
app:menu="@menu/menu_search">
|
||||
|
||||
|
@ -25,6 +26,7 @@
|
|||
app:hintEnabled="false">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/search_edit_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
|
|
|
@ -7,12 +7,14 @@
|
|||
android:orientation="vertical">
|
||||
|
||||
<org.oxycblt.auxio.ui.EdgeAppBarLayout
|
||||
android:id="@+id/settings_appbar"
|
||||
style="@style/Widget.Auxio.AppBarLayout"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:liftOnScroll="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/settings_toolbar"
|
||||
style="@style/Widget.Auxio.Toolbar.Icon"
|
||||
app:title="@string/set_title" />
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
android:theme="@style/ThemeOverlay.Accent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/accent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
view. The way we do this is odd, but it's designed this way-->
|
||||
|
||||
<org.oxycblt.auxio.coil.StyledImageView
|
||||
android:id="@+id/song_track_bg"
|
||||
style="@style/Widget.Auxio.Image.Small"
|
||||
android:src="@drawable/ic_song"
|
||||
android:scaleType="matrix"
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
android:padding="0dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/excluded_path"
|
||||
style="@style/Widget.Auxio.TextView.Item.Primary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
android:layout_height="wrap_content">
|
||||
|
||||
<View
|
||||
android:id="@+id/background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorPrimary"
|
||||
|
@ -21,6 +22,7 @@
|
|||
app:tint="?attr/colorSurface" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface">
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/header_title"
|
||||
style="@style/Widget.Auxio.TextView.Header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -16,6 +17,7 @@
|
|||
tools:text="Songs" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/header_button"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
Loading…
Reference in a new issue