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.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) {

View file

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

View file

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

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.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()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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" />
<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"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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