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:
OxygenCobalt 2022-04-02 20:21:06 -06:00
parent 74f5962844
commit ab194c14c2
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
43 changed files with 283 additions and 247 deletions

View file

@ -27,12 +27,16 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.oxycblt.auxio.databinding.FragmentMainBinding import org.oxycblt.auxio.databinding.FragmentMainBinding
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel 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.ui.ViewBindingFragment
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
@ -45,13 +49,13 @@ import org.oxycblt.auxio.util.logW
*/ */
class MainFragment : ViewBindingFragment<FragmentMainBinding>() { class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
private val navModel: NavigationViewModel by activityViewModels()
private val musicModel: MusicViewModel by activityViewModels() private val musicModel: MusicViewModel by activityViewModels()
private var callback: DynamicBackPressedCallback? = null private var callback: DynamicBackPressedCallback? = null
override fun onCreateBinding(inflater: LayoutInflater) = FragmentMainBinding.inflate(inflater) override fun onCreateBinding(inflater: LayoutInflater) = FragmentMainBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) {
// --- UI SETUP --- // --- UI SETUP ---
// Build the permission launcher here as you can only do it in onCreateView/onCreate // Build the permission launcher here as you can only do it in onCreateView/onCreate
val permLauncher = val permLauncher =
@ -89,6 +93,9 @@ class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
handleLoaderResponse(response, permLauncher) handleLoaderResponse(response, permLauncher)
} }
navModel.mainNavigationAction.observe(viewLifecycleOwner, ::handleMainNavigation)
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleExploreNavigation)
playbackModel.song.observe(viewLifecycleOwner, ::updateSong) 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?) { private fun updateSong(song: Song?) {
val binding = requireBinding() val binding = requireBinding()
if (song != null) { if (song != null) {

View file

@ -33,15 +33,15 @@ import org.oxycblt.auxio.util.stateList
/** An adapter that displays the accent palette. */ /** An adapter that displays the accent palette. */
class AccentAdapter(listener: Listener) : class AccentAdapter(listener: Listener) :
MonoAdapter<Accent, AccentAdapter.Listener, NewAccentViewHolder>(listener) { MonoAdapter<Accent, AccentAdapter.Listener, AccentViewHolder>(listener) {
var selectedAccent: Accent? = null var selectedAccent: Accent? = null
private set private set
private var selectedViewHolder: NewAccentViewHolder? = null private var selectedViewHolder: AccentViewHolder? = null
override val data = AccentData() 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) super.onBindViewHolder(viewHolder, position)
if (data.getItem(position) == selectedAccent) { if (data.getItem(position) == selectedAccent) {
@ -55,7 +55,7 @@ class AccentAdapter(listener: Listener) :
if (accent == selectedAccent) return if (accent == selectedAccent) return
selectedAccent = accent selectedAccent = accent
selectedViewHolder?.setSelected(false) selectedViewHolder?.setSelected(false)
selectedViewHolder = recycler.getViewHolderAt(accent.index) as NewAccentViewHolder? selectedViewHolder = recycler.getViewHolderAt(accent.index) as AccentViewHolder?
selectedViewHolder?.setSelected(true) 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) { BindingViewHolder<Accent, AccentAdapter.Listener>(binding.root) {
override fun bind(item: Accent, listener: AccentAdapter.Listener) { 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 backgroundTintList = context.getColorSafe(item.primary).stateList
contentDescription = context.getString(item.name) contentDescription = context.getString(item.name)
TooltipCompat.setTooltipText(this, contentDescription) TooltipCompat.setTooltipText(this, contentDescription)
setOnClickListener { listener.onAccentSelected(item) }
} }
binding.accent.setOnClickListener { listener.onAccentSelected(item) }
} }
fun setSelected(isSelected: Boolean) { fun setSelected(isSelected: Boolean) {
@ -98,12 +97,12 @@ class NewAccentViewHolder private constructor(private val binding: ItemAccentBin
companion object { companion object {
val CREATOR = val CREATOR =
object : Creator<NewAccentViewHolder> { object : Creator<AccentViewHolder> {
override val viewType: Int override val viewType: Int
get() = throw UnsupportedOperationException() get() = throw UnsupportedOperationException()
override fun create(context: Context) = override fun create(context: Context) =
NewAccentViewHolder(ItemAccentBinding.inflate(context.inflater)) AccentViewHolder(ItemAccentBinding.inflate(context.inflater))
} }
} }
} }

View file

@ -24,6 +24,7 @@ import coil.decode.ImageSource
import coil.fetch.FetchResult import coil.fetch.FetchResult
import coil.fetch.Fetcher import coil.fetch.Fetcher
import coil.fetch.SourceResult import coil.fetch.SourceResult
import coil.key.Keyer
import coil.request.Options import coil.request.Options
import coil.size.Size import coil.size.Size
import kotlin.math.min import kotlin.math.min
@ -32,9 +33,22 @@ import okio.source
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.Sort 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. * Fetcher that returns the album art for a given [Album] or [Song], depending on the factory used.
* @author OxygenCobalt * @author OxygenCobalt
@ -128,7 +142,7 @@ private inline fun <T : Any, R : Any> Collection<T>.mapAtMost(
break break
} }
transform(item)?.let { out.add(it) } transform(item)?.let(out::add)
} }
return out return out

View file

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

View file

@ -44,7 +44,10 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.getColorStateListSafe 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 class StyledImageView
@JvmOverloads @JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : 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) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec) super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// Scale the image down to half-size
imageMatrix = imageMatrix =
centerMatrix.apply { centerMatrix.apply {
reset() reset()

View file

@ -81,16 +81,9 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
// -- VIEWMODEL SETUP --- // -- VIEWMODEL SETUP ---
detailModel.albumData.observe(viewLifecycleOwner) { list -> detailModel.albumData.observe(viewLifecycleOwner, detailAdapter.data::submitList)
detailAdapter.data.submitList(list) navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
} playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
handleNavigation(item, detailAdapter)
}
updateSong(playbackModel.song.value, detailAdapter)
playbackModel.song.observe(viewLifecycleOwner) { song -> updateSong(song, detailAdapter) }
} }
override fun onItemClick(item: Item) { override fun onItemClick(item: Item) {
@ -126,7 +119,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
unlikelyToBeNull(detailModel.currentAlbum.value).artist.id)) unlikelyToBeNull(detailModel.currentAlbum.value).artist.id))
} }
private fun handleNavigation(item: Music?, adapter: AlbumDetailAdapter) { private fun handleNavigation(item: Music?) {
val binding = requireBinding() val binding = requireBinding()
when (item) { when (item) {
// Songs should be scrolled to if the album matches, or a new detail // Songs should be scrolled to if the album matches, or a new detail
@ -134,8 +127,8 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
is Song -> { is Song -> {
if (unlikelyToBeNull(detailModel.currentAlbum.value).id == item.album.id) { if (unlikelyToBeNull(detailModel.currentAlbum.value).id == item.album.id) {
logD("Navigating to a song in this album") logD("Navigating to a song in this album")
scrollToItem(item.id, adapter) scrollToItem(item.id)
detailModel.finishNavToItem() navModel.finishExploreNavigation()
} else { } else {
logD("Navigating to another album") logD("Navigating to another album")
findNavController() findNavController()
@ -149,7 +142,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
if (unlikelyToBeNull(detailModel.currentAlbum.value).id == item.id) { if (unlikelyToBeNull(detailModel.currentAlbum.value).id == item.id) {
logD("Navigating to the top of this album") logD("Navigating to the top of this album")
binding.detailRecycler.scrollToPosition(0) binding.detailRecycler.scrollToPosition(0)
detailModel.finishNavToItem() navModel.finishExploreNavigation()
} else { } else {
logD("Navigating to another album") logD("Navigating to another album")
findNavController() findNavController()
@ -169,9 +162,9 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
} }
/** Scroll to an song using its [id]. */ /** 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 // 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) { if (pos != -1) {
val binding = requireBinding() val binding = requireBinding()
@ -189,7 +182,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
} }
/** Updates the queue actions when a song is present or not */ /** 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() val binding = requireBinding()
for (item in binding.detailToolbar.menu.children) { for (item in binding.detailToolbar.menu.children) {
@ -200,10 +193,10 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
if (playbackModel.playbackMode.value == PlaybackMode.IN_ALBUM && if (playbackModel.playbackMode.value == PlaybackMode.IN_ALBUM &&
playbackModel.parent.value?.id == unlikelyToBeNull(detailModel.currentAlbum.value).id) { playbackModel.parent.value?.id == unlikelyToBeNull(detailModel.currentAlbum.value).id) {
adapter.highlightSong(song, binding.detailRecycler) detailAdapter.highlightSong(song, binding.detailRecycler)
} else { } else {
// Clear the ViewHolders if the mode isn't ALL_SONGS // Clear the ViewHolders if the mode isn't ALL_SONGS
adapter.highlightSong(null, binding.detailRecycler) detailAdapter.highlightSong(null, binding.detailRecycler)
} }
} }

View file

@ -63,27 +63,16 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
detailModel.artistData.observe(viewLifecycleOwner) { list -> detailModel.artistData.observe(viewLifecycleOwner, detailAdapter.data::submitList)
detailAdapter.data.submitList(list) navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
} playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
playbackModel.parent.observe(viewLifecycleOwner, ::updateParent)
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)
}
} }
override fun onItemClick(item: Item) { override fun onItemClick(item: Item) {
when (item) { when (item) {
is Song -> playbackModel.playSong(item, PlaybackMode.IN_ARTIST) is Song -> playbackModel.playSong(item, PlaybackMode.IN_ARTIST)
is Album -> is Album -> navModel.exploreNavigateTo(item)
findNavController()
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.id))
} }
} }
@ -111,49 +100,49 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
val binding = requireBinding() val binding = requireBinding()
when (item) { when (item) {
is Artist -> { is Song -> {
if (item.id == detailModel.currentArtist.value?.id) { logD("Navigating to another album")
logD("Navigating to the top of this artist")
binding.detailRecycler.scrollToPosition(0)
detailModel.finishNavToItem()
} else {
logD("Navigating to another artist")
findNavController() findNavController()
.navigate(ArtistDetailFragmentDirections.actionShowArtist(item.id)) .navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.album.id))
}
} }
is Album -> { is Album -> {
logD("Navigating to another album") logD("Navigating to another album")
findNavController() findNavController()
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.id)) .navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.id))
} }
is Song -> { is Artist -> {
logD("Navigating to another album") 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() findNavController()
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.album.id)) .navigate(ArtistDetailFragmentDirections.actionShowArtist(item.id))
}
} }
null -> {} null -> {}
else -> logW("Unsupported navigation item ${item::class.java}") else -> logW("Unsupported navigation item ${item::class.java}")
} }
} }
private fun updateSong(song: Song?, adapter: ArtistDetailAdapter) { private fun updateSong(song: Song?) {
val binding = requireBinding() val binding = requireBinding()
if (playbackModel.playbackMode.value == PlaybackMode.IN_ARTIST && if (playbackModel.playbackMode.value == PlaybackMode.IN_ARTIST &&
playbackModel.parent.value?.id == detailModel.currentArtist.value?.id) { playbackModel.parent.value?.id == detailModel.currentArtist.value?.id) {
adapter.highlightSong(song, binding.detailRecycler) detailAdapter.highlightSong(song, binding.detailRecycler)
} else { } else {
// Clear the ViewHolders if the mode isn't ALL_SONGS // 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() val binding = requireBinding()
if (parent is Album?) { if (parent is Album?) {
adapter.highlightAlbum(parent, binding.detailRecycler) detailAdapter.highlightAlbum(parent, binding.detailRecycler)
} else { } else {
adapter.highlightAlbum(null, binding.detailRecycler) detailAdapter.highlightAlbum(null, binding.detailRecycler)
} }
} }
} }

View file

@ -29,6 +29,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -40,16 +41,12 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
*/ */
abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() { abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
protected val detailModel: DetailViewModel by activityViewModels() protected val detailModel: DetailViewModel by activityViewModels()
protected val navModel: NavigationViewModel by activityViewModels()
protected val playbackModel: PlaybackViewModel by activityViewModels() protected val playbackModel: PlaybackViewModel by activityViewModels()
override fun onCreateBinding(inflater: LayoutInflater): FragmentDetailBinding = override fun onCreateBinding(inflater: LayoutInflater): FragmentDetailBinding =
FragmentDetailBinding.inflate(inflater) FragmentDetailBinding.inflate(inflater)
override fun onResume() {
super.onResume()
detailModel.setNavigating(false)
}
override fun onDestroyBinding(binding: FragmentDetailBinding) { override fun onDestroyBinding(binding: FragmentDetailBinding) {
super.onDestroyBinding(binding) super.onDestroyBinding(binding)
binding.detailRecycler.adapter = null binding.detailRecycler.adapter = null

View file

@ -25,7 +25,6 @@ import org.oxycblt.auxio.detail.recycler.SortHeader
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.ui.Header import org.oxycblt.auxio.ui.Header
@ -38,7 +37,6 @@ import org.oxycblt.auxio.util.logD
* - What item the fragment should be showing * - What item the fragment should be showing
* - The RecyclerView data for each fragment * - The RecyclerView data for each fragment
* - Menu triggers for each fragment * - Menu triggers for each fragment
* - Navigation triggers for each fragment [e.g "Go to artist"]
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class DetailViewModel : ViewModel() { class DetailViewModel : ViewModel() {
@ -87,15 +85,6 @@ class DetailViewModel : ViewModel() {
currentGenre.value?.let(::refreshGenreData) 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) { fun setAlbumId(id: Long) {
if (mCurrentAlbum.value?.id == id) return if (mCurrentAlbum.value?.id == id) return
val musicStore = MusicStore.requireInstance() val musicStore = MusicStore.requireInstance()
@ -122,21 +111,6 @@ class DetailViewModel : ViewModel() {
refreshGenreData(genre) 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) { private fun refreshGenreData(genre: Genre) {
logD("Refreshing genre data") logD("Refreshing genre data")
val data = mutableListOf<Item>(genre) val data = mutableListOf<Item>(genre)

View file

@ -36,7 +36,6 @@ import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
@ -61,13 +60,9 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
detailModel.genreData.observe(viewLifecycleOwner) { list -> detailModel.genreData.observe(viewLifecycleOwner, detailAdapter.data::submitList)
detailAdapter.data.submitList(list) navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
} playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
detailModel.navToItem.observe(viewLifecycleOwner, ::handleNavigation)
playbackModel.song.observe(viewLifecycleOwner) { song -> updateSong(song, detailAdapter) }
} }
override fun onItemClick(item: Item) { override fun onItemClick(item: Item) {
@ -97,34 +92,36 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
private fun handleNavigation(item: Music?) { private fun handleNavigation(item: Music?) {
when (item) { 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. // All items will launch new detail fragments.
is Artist -> { is Artist -> {
logD("Navigating to another artist") logD("Navigating to another artist")
findNavController() findNavController()
.navigate(GenreDetailFragmentDirections.actionShowArtist(item.id)) .navigate(GenreDetailFragmentDirections.actionShowArtist(item.id))
} }
is Album -> { is Genre -> {
logD("Navigating to another album") navModel.finishExploreNavigation()
findNavController().navigate(GenreDetailFragmentDirections.actionShowAlbum(item.id))
}
is Song -> {
logD("Navigating to another song")
findNavController()
.navigate(GenreDetailFragmentDirections.actionShowAlbum(item.album.id))
} }
null -> {} null -> {}
else -> logW("Unsupported navigation command ${item::class.java}")
} }
} }
private fun updateSong(song: Song?, adapter: GenreDetailAdapter) { private fun updateSong(song: Song?) {
val binding = requireBinding() val binding = requireBinding()
if (playbackModel.playbackMode.value == PlaybackMode.IN_GENRE && if (playbackModel.playbackMode.value == PlaybackMode.IN_GENRE &&
playbackModel.parent.value?.id == unlikelyToBeNull(detailModel.currentGenre.value).id) { playbackModel.parent.value?.id == unlikelyToBeNull(detailModel.currentGenre.value).id) {
adapter.highlightSong(song, binding.detailRecycler) detailAdapter.highlightSong(song, binding.detailRecycler)
} else { } else {
// Clear the ViewHolders if the mode isn't ALL_SONGS // Clear the ViewHolders if the mode isn't ALL_SONGS
adapter.highlightSong(null, binding.detailRecycler) detailAdapter.highlightSong(null, binding.detailRecycler)
} }
} }
} }

View file

@ -28,10 +28,8 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeBinding 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.AlbumListFragment
import org.oxycblt.auxio.home.list.ArtistListFragment import org.oxycblt.auxio.home.list.ArtistListFragment
import org.oxycblt.auxio.home.list.GenreListFragment 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.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.DisplayMode 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.ui.ViewBindingFragment
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
@ -62,7 +62,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
*/ */
class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() { class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
private val playbackModel: PlaybackViewModel by activityViewModels() 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 homeModel: HomeViewModel by activityViewModels()
private val musicModel: MusicViewModel by activityViewModels() private val musicModel: MusicViewModel by activityViewModels()
@ -108,7 +108,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
homeModel.recreateTabs.observe(viewLifecycleOwner, ::handleRecreateTabs) homeModel.recreateTabs.observe(viewLifecycleOwner, ::handleRecreateTabs)
musicModel.loaderResponse.observe(viewLifecycleOwner, ::handleLoaderResponse) musicModel.loaderResponse.observe(viewLifecycleOwner, ::handleLoaderResponse)
detailModel.navToItem.observe(viewLifecycleOwner, ::handleNavigation) navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
} }
private fun onMenuClick(item: MenuItem) { private fun onMenuClick(item: MenuItem) {
@ -119,19 +119,15 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
} }
R.id.action_settings -> { R.id.action_settings -> {
logD("Navigating to settings") logD("Navigating to settings")
parentFragment navModel.mainNavigateTo(MainNavigationAction.SETTINGS)
?.parentFragment
?.findNavController()
?.navigate(MainFragmentDirections.actionShowSettings())
} }
R.id.action_about -> { R.id.action_about -> {
logD("Navigating to about") logD("Navigating to about")
parentFragment navModel.mainNavigateTo(MainNavigationAction.ABOUT)
?.parentFragment }
?.findNavController() R.id.submenu_sorting -> {
?.navigate(MainFragmentDirections.actionShowAbout()) // Junk click event when opening the menu
} }
R.id.submenu_sorting -> {}
R.id.option_sort_asc -> { R.id.option_sort_asc -> {
item.isChecked = !item.isChecked item.isChecked = !item.isChecked
homeModel.updateCurrentSort( homeModel.updateCurrentSort(

View file

@ -18,11 +18,10 @@
package org.oxycblt.auxio.home.list package org.oxycblt.auxio.home.list
import android.view.View import android.view.View
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.HomeFragmentDirections
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.ui.AlbumViewHolder import org.oxycblt.auxio.ui.AlbumViewHolder
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.Item
@ -70,8 +69,8 @@ class AlbumListFragment : HomeListFragment<Album>() {
} }
override fun onItemClick(item: Item) { override fun onItemClick(item: Item) {
check(item is Album) check(item is Music)
findNavController().navigate(HomeFragmentDirections.actionShowAlbum(item.id)) navModel.exploreNavigateTo(item)
} }
override fun onOpenMenu(item: Item, anchor: View) { override fun onOpenMenu(item: Item, anchor: View) {

View file

@ -18,11 +18,10 @@
package org.oxycblt.auxio.home.list package org.oxycblt.auxio.home.list
import android.view.View import android.view.View
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.HomeFragmentDirections
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.ui.ArtistViewHolder import org.oxycblt.auxio.ui.ArtistViewHolder
import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuItemListener import org.oxycblt.auxio.ui.MenuItemListener
@ -56,8 +55,8 @@ class ArtistListFragment : HomeListFragment<Artist>() {
.uppercase() .uppercase()
override fun onItemClick(item: Item) { override fun onItemClick(item: Item) {
check(item is Artist) check(item is Music)
findNavController().navigate(HomeFragmentDirections.actionShowArtist(item.id)) navModel.exploreNavigateTo(item)
} }
override fun onOpenMenu(item: Item, anchor: View) { override fun onOpenMenu(item: Item, anchor: View) {

View file

@ -18,11 +18,10 @@
package org.oxycblt.auxio.home.list package org.oxycblt.auxio.home.list
import android.view.View import android.view.View
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.HomeFragmentDirections
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.ui.GenreViewHolder import org.oxycblt.auxio.ui.GenreViewHolder
import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuItemListener import org.oxycblt.auxio.ui.MenuItemListener
@ -56,8 +55,8 @@ class GenreListFragment : HomeListFragment<Genre>() {
.uppercase() .uppercase()
override fun onItemClick(item: Item) { override fun onItemClick(item: Item) {
check(item is Genre) check(item is Music)
findNavController().navigate(HomeFragmentDirections.actionShowGenre(item.id)) navModel.exploreNavigateTo(item)
} }
override fun onOpenMenu(item: Item, anchor: View) { override fun onOpenMenu(item: Item, anchor: View) {

View file

@ -28,6 +28,7 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuItemListener import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
/** /**
@ -41,8 +42,9 @@ abstract class HomeListFragment<T : Item> :
FastScrollRecyclerView.OnFastScrollListener { FastScrollRecyclerView.OnFastScrollListener {
abstract fun setupRecycler(recycler: RecyclerView) abstract fun setupRecycler(recycler: RecyclerView)
protected val homeModel: HomeViewModel by activityViewModels()
protected val playbackModel: PlaybackViewModel 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) = override fun onCreateBinding(inflater: LayoutInflater) =
FragmentHomeListBinding.inflate(inflater) FragmentHomeListBinding.inflate(inflater)

View file

@ -54,7 +54,7 @@ sealed class Tab(open val mode: DisplayMode) {
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100 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 = private val MODE_TABLE =
arrayOf( arrayOf(

View file

@ -27,8 +27,8 @@ import com.google.android.material.color.MaterialColors
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.coil.bindAlbumCover import org.oxycblt.auxio.coil.bindAlbumCover
import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding
import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.BottomSheetLayout import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.getAttrColorSafe import org.oxycblt.auxio.util.getAttrColorSafe
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
@ -36,7 +36,7 @@ import org.oxycblt.auxio.util.textSafe
class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() { class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
private val detailModel: DetailViewModel by activityViewModels() private val navModel: NavigationViewModel by activityViewModels()
override fun onCreateBinding(inflater: LayoutInflater) = override fun onCreateBinding(inflater: LayoutInflater) =
FragmentPlaybackBarBinding.inflate(inflater) FragmentPlaybackBarBinding.inflate(inflater)
@ -46,14 +46,10 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
savedInstanceState: Bundle? savedInstanceState: Bundle?
) { ) {
binding.root.apply { binding.root.apply {
setOnClickListener { setOnClickListener { navModel.mainNavigateTo(MainNavigationAction.EXPAND) }
// 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()
}
setOnLongClickListener { setOnLongClickListener {
playbackModel.song.value?.let { song -> detailModel.navToItem(song) } playbackModel.song.value?.let(navModel::exploreNavigateTo)
true true
} }

View file

@ -23,11 +23,9 @@ import android.view.MenuItem
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import kotlin.math.max import kotlin.math.max
import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.coil.bindAlbumCover import org.oxycblt.auxio.coil.bindAlbumCover
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding 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.Song
import org.oxycblt.auxio.music.toDuration import org.oxycblt.auxio.music.toDuration
import org.oxycblt.auxio.playback.state.LoopMode 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.ui.ViewBindingFragment
import org.oxycblt.auxio.util.getAttrColorSafe import org.oxycblt.auxio.util.getAttrColorSafe
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -56,6 +55,7 @@ class PlaybackPanelFragment :
Slider.OnChangeListener, Slider.OnChangeListener,
Slider.OnSliderTouchListener { Slider.OnSliderTouchListener {
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
private val navModel: NavigationViewModel by activityViewModels()
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
override fun onCreateBinding(inflater: LayoutInflater) = override fun onCreateBinding(inflater: LayoutInflater) =
@ -75,11 +75,11 @@ class PlaybackPanelFragment :
val queueItem: MenuItem val queueItem: MenuItem
binding.playbackToolbar.apply { binding.playbackToolbar.apply {
setNavigationOnClickListener { navigateUp() } setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.COLLAPSE) }
setOnMenuItemClickListener { item -> setOnMenuItemClickListener { item ->
if (item.itemId == R.id.action_queue) { if (item.itemId == R.id.action_queue) {
findNavController().navigate(MainFragmentDirections.actionShowQueue()) navModel.mainNavigateTo(MainNavigationAction.QUEUE)
true true
} else { } else {
false false
@ -92,15 +92,15 @@ class PlaybackPanelFragment :
binding.playbackSong.apply { binding.playbackSong.apply {
// Make marquee of the song title work // Make marquee of the song title work
isSelected = true isSelected = true
setOnClickListener { playbackModel.song.value?.let { detailModel.navToItem(it) } } setOnClickListener { playbackModel.song.value?.let(navModel::exploreNavigateTo) }
} }
binding.playbackArtist.setOnClickListener { binding.playbackArtist.setOnClickListener {
playbackModel.song.value?.let { detailModel.navToItem(it.album.artist) } playbackModel.song.value?.let { navModel.exploreNavigateTo(it.album.artist) }
} }
binding.playbackAlbum.setOnClickListener { binding.playbackAlbum.setOnClickListener {
playbackModel.song.value?.let { detailModel.navToItem(it.album) } playbackModel.song.value?.let { navModel.exploreNavigateTo(it.album) }
} }
binding.playbackSeekBar.apply { binding.playbackSeekBar.apply {
@ -141,12 +141,6 @@ class PlaybackPanelFragment :
queueItem.isEnabled = nextUp.isNotEmpty() queueItem.isEnabled = nextUp.isNotEmpty()
} }
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
if (item != null) {
navigateUp()
}
}
logD("Fragment Created") logD("Fragment Created")
} }
@ -218,10 +212,4 @@ class PlaybackPanelFragment :
private fun updateShuffle(isShuffling: Boolean) { private fun updateShuffle(isShuffling: Boolean) {
requireBinding().playbackShuffle.isActivated = isShuffling 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()
}
} }

View file

@ -30,7 +30,6 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentSearchBinding import org.oxycblt.auxio.databinding.FragmentSearchBinding
import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
@ -41,11 +40,11 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.Header import org.oxycblt.auxio.ui.Header
import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuItemListener import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.getSystemServiceSafe
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.requireAttached import org.oxycblt.auxio.util.requireAttached
/** /**
@ -56,7 +55,7 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
// SearchViewModel is only scoped to this Fragment // SearchViewModel is only scoped to this Fragment
private val searchModel: SearchViewModel by viewModels() private val searchModel: SearchViewModel by viewModels()
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
private val detailModel: DetailViewModel by activityViewModels() private val navModel: NavigationViewModel by activityViewModels()
private val searchAdapter = SearchAdapter(this) private val searchAdapter = SearchAdapter(this)
private var imm: InputMethodManager? = null private var imm: InputMethodManager? = null
@ -109,11 +108,7 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
searchModel.searchResults.observe(viewLifecycleOwner, ::updateResults) searchModel.searchResults.observe(viewLifecycleOwner, ::updateResults)
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
handleNavigation(item)
requireImm().hide()
}
} }
override fun onResume() { override fun onResume() {
@ -128,25 +123,9 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
} }
override fun onItemClick(item: Item) { 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) { when (item) {
is Genre -> SearchFragmentDirections.actionShowGenre(item.id) is Song -> playbackModel.playSong(item)
is Artist -> SearchFragmentDirections.actionShowArtist(item.id) is MusicParent -> navModel.exploreNavigateTo(item)
is Album -> SearchFragmentDirections.actionShowAlbum(item.id)
})
requireImm().hide()
} }
} }
@ -178,8 +157,11 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
is Song -> SearchFragmentDirections.actionShowAlbum(item.album.id) is Song -> SearchFragmentDirections.actionShowAlbum(item.album.id)
is Album -> SearchFragmentDirections.actionShowAlbum(item.id) is Album -> SearchFragmentDirections.actionShowAlbum(item.id)
is Artist -> SearchFragmentDirections.actionShowArtist(item.id) is Artist -> SearchFragmentDirections.actionShowArtist(item.id)
is Genre -> SearchFragmentDirections.actionShowGenre(item.id)
else -> return else -> return
}) })
requireImm().hide()
} }
private fun requireImm(): InputMethodManager { private fun requireImm(): InputMethodManager {

View file

@ -47,8 +47,6 @@ class SearchViewModel : ViewModel() {
/** Current search results from the last [search] call. */ /** Current search results from the last [search] call. */
val searchResults: LiveData<List<Item>> val searchResults: LiveData<List<Item>>
get() = mSearchResults get() = mSearchResults
val isNavigating: Boolean
get() = mIsNavigating
val filterMode: DisplayMode? val filterMode: DisplayMode?
get() = mFilterMode get() = mFilterMode

View file

@ -26,7 +26,6 @@ import androidx.core.view.children
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
@ -70,8 +69,8 @@ class ActionMenu(
private val context = activity.applicationContext private val context = activity.applicationContext
// Get ViewModels using the activity as the store owner // Get ViewModels using the activity as the store owner
private val detailModel: DetailViewModel by lazy { private val navModel: NavigationViewModel by lazy {
ViewModelProvider(activity)[DetailViewModel::class.java] ViewModelProvider(activity)[NavigationViewModel::class.java]
} }
private val playbackModel: PlaybackViewModel by lazy { private val playbackModel: PlaybackViewModel by lazy {
@ -172,14 +171,14 @@ class ActionMenu(
} }
R.id.action_go_album -> { R.id.action_go_album -> {
if (data is Song) { if (data is Song) {
detailModel.navToItem(data.album) navModel.exploreNavigateTo(data.album)
} }
} }
R.id.action_go_artist -> { R.id.action_go_artist -> {
if (data is Song) { if (data is Song) {
detailModel.navToItem(data.album.artist) navModel.exploreNavigateTo(data.album.artist)
} else if (data is Album) { } else if (data is Album) {
detailModel.navToItem(data.artist) navModel.exploreNavigateTo(data.artist)
} }
} }
} }

View file

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

View file

@ -94,6 +94,7 @@
app:thumbRadius="@dimen/slider_thumb_radius" /> app:thumbRadius="@dimen/slider_thumb_radius" />
<TextView <TextView
android:id="@+id/playback_position"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium" android:layout_marginStart="@dimen/spacing_medium"
@ -105,6 +106,7 @@
tools:text="11:38" /> tools:text="11:38" />
<TextView <TextView
android:id="@+id/playback_duration"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_small_inv" android:layout_marginTop="@dimen/spacing_small_inv"

View file

@ -92,6 +92,7 @@
app:thumbRadius="@dimen/slider_thumb_radius" /> app:thumbRadius="@dimen/slider_thumb_radius" />
<TextView <TextView
android:id="@+id/playback_position"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium" android:layout_marginStart="@dimen/spacing_medium"
@ -103,6 +104,7 @@
tools:text="11:38" /> tools:text="11:38" />
<TextView <TextView
android:id="@+id/playback_duration"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_small_inv" android:layout_marginTop="@dimen/spacing_small_inv"

View file

@ -81,6 +81,7 @@
app:thumbRadius="@dimen/slider_thumb_radius" /> app:thumbRadius="@dimen/slider_thumb_radius" />
<TextView <TextView
android:id="@+id/playback_position"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium" android:layout_marginStart="@dimen/spacing_medium"
@ -92,6 +93,7 @@
tools:text="11:38" /> tools:text="11:38" />
<TextView <TextView
android:id="@+id/playback_duration"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_small_inv" android:layout_marginTop="@dimen/spacing_small_inv"

View file

@ -94,6 +94,7 @@
app:thumbRadius="@dimen/slider_thumb_radius" /> app:thumbRadius="@dimen/slider_thumb_radius" />
<TextView <TextView
android:id="@+id/playback_position"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium" android:layout_marginStart="@dimen/spacing_medium"
@ -105,6 +106,7 @@
tools:text="11:38" /> tools:text="11:38" />
<TextView <TextView
android:id="@+id/playback_duration"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_small_inv" android:layout_marginTop="@dimen/spacing_small_inv"

View file

@ -2,6 +2,7 @@
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" <androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/accent_recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:overScrollMode="never" android:overScrollMode="never"

View file

@ -7,8 +7,8 @@
android:orientation="vertical" android:orientation="vertical"
android:paddingTop="@dimen/spacing_small"> android:paddingTop="@dimen/spacing_small">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/excluded_recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_gravity="center" android:layout_gravity="center"
@ -19,6 +19,7 @@
tools:listitem="@layout/item_excluded_dir" /> tools:listitem="@layout/item_excluded_dir" />
<TextView <TextView
android:id="@+id/excluded_empty"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="@dimen/spacing_medium" android:padding="@dimen/spacing_medium"

View file

@ -2,6 +2,7 @@
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" <androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tab_recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:overScrollMode="never" android:overScrollMode="never"

View file

@ -8,16 +8,19 @@
tools:context=".settings.AboutFragment"> tools:context=".settings.AboutFragment">
<org.oxycblt.auxio.ui.EdgeAppBarLayout <org.oxycblt.auxio.ui.EdgeAppBarLayout
android:id="@+id/about_appbar"
style="@style/Widget.Auxio.AppBarLayout" style="@style/Widget.Auxio.AppBarLayout"
app:liftOnScroll="true"> app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/about_toolbar"
style="@style/Widget.Auxio.Toolbar.Icon" style="@style/Widget.Auxio.Toolbar.Icon"
app:title="@string/lbl_about" /> app:title="@string/lbl_about" />
</org.oxycblt.auxio.ui.EdgeAppBarLayout> </org.oxycblt.auxio.ui.EdgeAppBarLayout>
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:id="@+id/about_contents"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"

View file

@ -10,6 +10,7 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
<org.oxycblt.auxio.detail.DetailAppBarLayout <org.oxycblt.auxio.detail.DetailAppBarLayout
android:id="@+id/detail_appbar"
style="@style/Widget.Auxio.AppBarLayout" style="@style/Widget.Auxio.AppBarLayout"
app:liftOnScroll="true" app:liftOnScroll="true"
app:liftOnScrollTargetViewId="@id/detail_recycler"> app:liftOnScrollTargetViewId="@id/detail_recycler">

View file

@ -6,15 +6,18 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
<org.oxycblt.auxio.ui.EdgeAppBarLayout <org.oxycblt.auxio.ui.EdgeAppBarLayout
android:id="@+id/home_appbar"
style="@style/Widget.Auxio.AppBarLayout" style="@style/Widget.Auxio.AppBarLayout"
app:liftOnScroll="true"> app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/home_toolbar"
style="@style/Widget.Auxio.Toolbar" style="@style/Widget.Auxio.Toolbar"
app:menu="@menu/menu_home" app:menu="@menu/menu_home"
app:title="@string/info_app_name" /> app:title="@string/info_app_name" />
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/home_tabs"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@android:color/transparent" android:background="@android:color/transparent"
@ -40,6 +43,7 @@
app:layout_anchorGravity="bottom|end"> app:layout_anchorGravity="bottom|end">
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/home_fab"
style="@style/Widget.Auxio.FloatingActionButton.Adaptive" style="@style/Widget.Auxio.FloatingActionButton.Adaptive"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView xmlns:android="http://schemas.android.com/apk/res/android" <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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
style="@style/Widget.Auxio.RecyclerView.WithAdaptiveFab" style="@style/Widget.Auxio.RecyclerView.WithAdaptiveFab"
android:id="@+id/home_recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"

View file

@ -6,6 +6,7 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
<org.oxycblt.auxio.ui.BottomSheetLayout <org.oxycblt.auxio.ui.BottomSheetLayout
android:id="@+id/bottom_sheet_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -32,6 +33,7 @@
</org.oxycblt.auxio.ui.BottomSheetLayout> </org.oxycblt.auxio.ui.BottomSheetLayout>
<FrameLayout <FrameLayout
android:id="@+id/layout_too_small"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurface" android:background="?attr/colorSurface"

View file

@ -78,6 +78,7 @@
app:thumbRadius="@dimen/slider_thumb_radius" /> app:thumbRadius="@dimen/slider_thumb_radius" />
<TextView <TextView
android:id="@+id/playback_position"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium" android:layout_marginStart="@dimen/spacing_medium"
@ -89,6 +90,7 @@
tools:text="11:38" /> tools:text="11:38" />
<TextView <TextView
android:id="@+id/playback_duration"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_small_inv" android:layout_marginTop="@dimen/spacing_small_inv"

View file

@ -12,6 +12,7 @@
app:liftOnScrollTargetViewId="@id/queue_recycler"> app:liftOnScrollTargetViewId="@id/queue_recycler">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/queue_toolbar"
style="@style/Widget.Auxio.Toolbar.Icon.Down" style="@style/Widget.Auxio.Toolbar.Icon.Down"
android:elevation="0dp" android:elevation="0dp"
app:navigationIcon="@drawable/ic_down" app:navigationIcon="@drawable/ic_down"

View file

@ -11,6 +11,7 @@
app:liftOnScrollTargetViewId="@id/search_recycler"> app:liftOnScrollTargetViewId="@id/search_recycler">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/search_toolbar"
style="@style/Widget.Auxio.Toolbar.Icon" style="@style/Widget.Auxio.Toolbar.Icon"
app:menu="@menu/menu_search"> app:menu="@menu/menu_search">
@ -25,6 +26,7 @@
app:hintEnabled="false"> app:hintEnabled="false">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/search_edit_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@android:color/transparent" android:background="@android:color/transparent"

View file

@ -7,12 +7,14 @@
android:orientation="vertical"> android:orientation="vertical">
<org.oxycblt.auxio.ui.EdgeAppBarLayout <org.oxycblt.auxio.ui.EdgeAppBarLayout
android:id="@+id/settings_appbar"
style="@style/Widget.Auxio.AppBarLayout" style="@style/Widget.Auxio.AppBarLayout"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
app:liftOnScroll="true"> app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/settings_toolbar"
style="@style/Widget.Auxio.Toolbar.Icon" style="@style/Widget.Auxio.Toolbar.Icon"
app:title="@string/set_title" /> app:title="@string/set_title" />

View file

@ -8,6 +8,7 @@
android:theme="@style/ThemeOverlay.Accent"> android:theme="@style/ThemeOverlay.Accent">
<ImageButton <ImageButton
android:id="@+id/accent"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"

View file

@ -11,6 +11,7 @@
view. The way we do this is odd, but it's designed this way--> view. The way we do this is odd, but it's designed this way-->
<org.oxycblt.auxio.coil.StyledImageView <org.oxycblt.auxio.coil.StyledImageView
android:id="@+id/song_track_bg"
style="@style/Widget.Auxio.Image.Small" style="@style/Widget.Auxio.Image.Small"
android:src="@drawable/ic_song" android:src="@drawable/ic_song"
android:scaleType="matrix" android:scaleType="matrix"

View file

@ -11,6 +11,7 @@
android:padding="0dp"> android:padding="0dp">
<TextView <TextView
android:id="@+id/excluded_path"
style="@style/Widget.Auxio.TextView.Item.Primary" style="@style/Widget.Auxio.TextView.Item.Primary"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

@ -6,6 +6,7 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<View <View
android:id="@+id/background"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorPrimary" android:background="?attr/colorPrimary"
@ -21,6 +22,7 @@
app:tint="?attr/colorSurface" /> app:tint="?attr/colorSurface" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/body"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/colorSurface"> android:background="?attr/colorSurface">

View file

@ -7,6 +7,7 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<TextView <TextView
android:id="@+id/header_title"
style="@style/Widget.Auxio.TextView.Header" style="@style/Widget.Auxio.TextView.Header"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -16,6 +17,7 @@
tools:text="Songs" /> tools:text="Songs" />
<ImageButton <ImageButton
android:id="@+id/header_button"
style="@style/Widget.AppCompat.Button.Borderless" style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"