home: add playlist add button

Refactor the home FAB to switch to playlist addition button
when at the playlist tab.
This commit is contained in:
Alexander Capehart 2023-03-25 14:37:55 -06:00
parent 9988a1b76b
commit 829e2a42c4
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
7 changed files with 131 additions and 11 deletions

View file

@ -0,0 +1,101 @@
/*
* Copyright (c) 2023 Auxio Project
* FlipFloatingActionButton.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.home
import android.content.Context
import android.util.AttributeSet
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.google.android.material.floatingactionbutton.FloatingActionButton
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.logD
/**
* An extension of [FloatingActionButton] that enables the ability to fade in and out between
* several states, as in the Material Design 3 specification.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class FlipFloatingActionButton
@JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.floatingActionButtonStyle
) : FloatingActionButton(context, attrs, defStyleAttr) {
private var pendingConfig: PendingConfig? = null
private var flipping = false
override fun show() {
// Will already show eventually, need to do nothing.
if (flipping) return
// Apply the new configuration possibly set in flipTo. This should occur even if
// a flip was canceled by a hide.
pendingConfig?.run {
setImageResource(iconRes)
contentDescription = context.getString(contentDescriptionRes)
setOnClickListener(clickListener)
}
pendingConfig = null
super.show()
}
override fun hide() {
// Not flipping anymore, disable the flag so that the FAB is not re-shown.
flipping = false
// Don't pass any kind of listener so that future flip operations will not be able
// to show the FAB again.
super.hide()
}
/**
* Flip to a new FAB state.
*
* @param iconRes The resource of the new FAB icon.
* @param contentDescriptionRes The resource of the new FAB content description.
*/
fun flipTo(
@DrawableRes iconRes: Int,
@StringRes contentDescriptionRes: Int,
clickListener: OnClickListener
) {
// Avoid doing a flip if the given config is already being applied.
if (tag == iconRes) return
tag = iconRes
flipping = true
pendingConfig = PendingConfig(iconRes, contentDescriptionRes, clickListener)
// We will re-show the FAB later, assuming that there was not a prior flip operation.
super.hide(FlipVisibilityListener())
}
private data class PendingConfig(
@DrawableRes val iconRes: Int,
@StringRes val contentDescriptionRes: Int,
val clickListener: OnClickListener
)
private inner class FlipVisibilityListener : OnVisibilityChangedListener() {
override fun onHidden(fab: FloatingActionButton) {
if (!flipping) return
logD("Showing for a flip operation")
flipping = false
show()
}
}
}

View file

@ -147,12 +147,11 @@ class HomeFragment :
// re-creating the ViewPager.
setupPager(binding)
binding.homeFab.setOnClickListener { playbackModel.shuffleAll() }
// --- VIEWMODEL SETUP ---
collect(homeModel.recreateTabs.flow, ::handleRecreate)
collectImmediately(homeModel.currentTabMode, ::updateCurrentTab)
collectImmediately(homeModel.songsList, homeModel.isFastScrolling, ::updateFab)
collectImmediately(
homeModel.songsList, homeModel.isFastScrolling, homeModel.currentTabMode, ::updateFab)
collectImmediately(musicModel.indexingState, ::updateIndexerState)
collect(navModel.exploreNavigationItem.flow, ::handleNavigation)
collectImmediately(selectionModel.selected, ::updateSelection)
@ -268,6 +267,7 @@ class HomeFragment :
}
private fun updateCurrentTab(tabMode: MusicMode) {
val binding = requireBinding()
// Update the sort options to align with those allowed by the tab
val isVisible: (Int) -> Boolean =
when (tabMode) {
@ -286,8 +286,7 @@ class HomeFragment :
}
val sortMenu =
unlikelyToBeNull(
requireBinding().homeToolbar.menu.findItem(R.id.submenu_sorting).subMenu)
unlikelyToBeNull(binding.homeToolbar.menu.findItem(R.id.submenu_sorting).subMenu)
val toHighlight = homeModel.getSortForTab(tabMode)
for (option in sortMenu) {
@ -308,7 +307,7 @@ class HomeFragment :
// Update the scrolling view in AppBarLayout to align with the current tab's
// scrolling state. This prevents the lift state from being confused as one
// goes between different tabs.
requireBinding().homeAppbar.liftOnScrollTargetViewId =
binding.homeAppbar.liftOnScrollTargetViewId =
when (tabMode) {
MusicMode.SONGS -> R.id.home_song_recycler
MusicMode.ALBUMS -> R.id.home_album_recycler
@ -316,6 +315,16 @@ class HomeFragment :
MusicMode.GENRES -> R.id.home_genre_recycler
MusicMode.PLAYLISTS -> R.id.home_playlist_recycler
}
if (tabMode != MusicMode.PLAYLISTS) {
binding.homeFab.flipTo(R.drawable.ic_shuffle_off_24, R.string.desc_shuffle_all) {
playbackModel.shuffleAll()
}
} else {
binding.homeFab.flipTo(R.drawable.ic_add_24, R.string.desc_new_playlist) {
musicModel.createPlaylist()
}
}
}
private fun handleRecreate(recreate: Unit?) {
@ -419,7 +428,7 @@ class HomeFragment :
}
}
private fun updateFab(songs: List<Song>, isFastScrolling: Boolean) {
private fun updateFab(songs: List<Song>, isFastScrolling: Boolean, currentTabMode: MusicMode) {
val binding = requireBinding()
// If there are no songs, it's likely that the library has not been loaded, so
// displaying the shuffle FAB makes no sense. We also don't want the fast scroll

View file

@ -137,6 +137,7 @@ constructor(
override fun onMusicChanges(changes: MusicRepository.Changes) {
val deviceLibrary = musicRepository.deviceLibrary
logD(changes.deviceLibrary)
if (changes.deviceLibrary && deviceLibrary != null) {
logD("Refreshing library")
// Get the each list of items in the library to use as our list data.

View file

@ -189,6 +189,7 @@ constructor(
@Synchronized
override fun addUpdateListener(listener: MusicRepository.UpdateListener) {
updateListeners.add(listener)
listener.onMusicChanges(MusicRepository.Changes(deviceLibrary = true, userLibrary = true))
}
@Synchronized
@ -199,6 +200,7 @@ constructor(
@Synchronized
override fun addIndexingListener(listener: MusicRepository.IndexingListener) {
indexingListeners.add(listener)
listener.onIndexingStateChanged()
}
@Synchronized

View file

@ -78,6 +78,14 @@ class MusicViewModel @Inject constructor(private val musicRepository: MusicRepos
musicRepository.requestIndex(false)
}
/**
* Create a new generic playlist.
* @param name The name of the new playlist. If null, the user will be prompted for a name.
*/
fun createPlaylist(name: String? = null) {
// TODO: Implement
}
/**
* Non-manipulated statistics bound the last successful music load.
*

View file

@ -123,14 +123,12 @@
app:layout_anchor="@id/home_content"
app:layout_anchorGravity="bottom|end">
<com.google.android.material.floatingactionbutton.FloatingActionButton
<org.oxycblt.auxio.home.FlipFloatingActionButton
android:id="@+id/home_fab"
style="@style/Widget.Auxio.FloatingActionButton.Adaptive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_medium"
android:contentDescription="@string/desc_shuffle_all"
android:src="@drawable/ic_shuffle_off_24" />
android:layout_margin="@dimen/spacing_medium" />
</org.oxycblt.auxio.home.EdgeFrameLayout>

View file

@ -297,6 +297,7 @@
<string name="desc_change_repeat">Change repeat mode</string>
<string name="desc_shuffle">Turn shuffle on or off</string>
<string name="desc_shuffle_all">Shuffle all songs</string>
<string name="desc_new_playlist">Create a new playlist</string>
<string name="desc_exit">Stop playback</string>
<string name="desc_clear_queue_item">Remove this queue song</string>