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:
parent
9988a1b76b
commit
829e2a42c4
7 changed files with 131 additions and 11 deletions
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -147,12 +147,11 @@ class HomeFragment :
|
||||||
// re-creating the ViewPager.
|
// re-creating the ViewPager.
|
||||||
setupPager(binding)
|
setupPager(binding)
|
||||||
|
|
||||||
binding.homeFab.setOnClickListener { playbackModel.shuffleAll() }
|
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
collect(homeModel.recreateTabs.flow, ::handleRecreate)
|
collect(homeModel.recreateTabs.flow, ::handleRecreate)
|
||||||
collectImmediately(homeModel.currentTabMode, ::updateCurrentTab)
|
collectImmediately(homeModel.currentTabMode, ::updateCurrentTab)
|
||||||
collectImmediately(homeModel.songsList, homeModel.isFastScrolling, ::updateFab)
|
collectImmediately(
|
||||||
|
homeModel.songsList, homeModel.isFastScrolling, homeModel.currentTabMode, ::updateFab)
|
||||||
collectImmediately(musicModel.indexingState, ::updateIndexerState)
|
collectImmediately(musicModel.indexingState, ::updateIndexerState)
|
||||||
collect(navModel.exploreNavigationItem.flow, ::handleNavigation)
|
collect(navModel.exploreNavigationItem.flow, ::handleNavigation)
|
||||||
collectImmediately(selectionModel.selected, ::updateSelection)
|
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||||
|
@ -268,6 +267,7 @@ class HomeFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateCurrentTab(tabMode: MusicMode) {
|
private fun updateCurrentTab(tabMode: MusicMode) {
|
||||||
|
val binding = requireBinding()
|
||||||
// Update the sort options to align with those allowed by the tab
|
// Update the sort options to align with those allowed by the tab
|
||||||
val isVisible: (Int) -> Boolean =
|
val isVisible: (Int) -> Boolean =
|
||||||
when (tabMode) {
|
when (tabMode) {
|
||||||
|
@ -286,8 +286,7 @@ class HomeFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
val sortMenu =
|
val sortMenu =
|
||||||
unlikelyToBeNull(
|
unlikelyToBeNull(binding.homeToolbar.menu.findItem(R.id.submenu_sorting).subMenu)
|
||||||
requireBinding().homeToolbar.menu.findItem(R.id.submenu_sorting).subMenu)
|
|
||||||
val toHighlight = homeModel.getSortForTab(tabMode)
|
val toHighlight = homeModel.getSortForTab(tabMode)
|
||||||
|
|
||||||
for (option in sortMenu) {
|
for (option in sortMenu) {
|
||||||
|
@ -308,7 +307,7 @@ class HomeFragment :
|
||||||
// Update the scrolling view in AppBarLayout to align with the current tab's
|
// 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
|
// scrolling state. This prevents the lift state from being confused as one
|
||||||
// goes between different tabs.
|
// goes between different tabs.
|
||||||
requireBinding().homeAppbar.liftOnScrollTargetViewId =
|
binding.homeAppbar.liftOnScrollTargetViewId =
|
||||||
when (tabMode) {
|
when (tabMode) {
|
||||||
MusicMode.SONGS -> R.id.home_song_recycler
|
MusicMode.SONGS -> R.id.home_song_recycler
|
||||||
MusicMode.ALBUMS -> R.id.home_album_recycler
|
MusicMode.ALBUMS -> R.id.home_album_recycler
|
||||||
|
@ -316,6 +315,16 @@ class HomeFragment :
|
||||||
MusicMode.GENRES -> R.id.home_genre_recycler
|
MusicMode.GENRES -> R.id.home_genre_recycler
|
||||||
MusicMode.PLAYLISTS -> R.id.home_playlist_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?) {
|
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()
|
val binding = requireBinding()
|
||||||
// If there are no songs, it's likely that the library has not been loaded, so
|
// 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
|
// displaying the shuffle FAB makes no sense. We also don't want the fast scroll
|
||||||
|
|
|
@ -137,6 +137,7 @@ constructor(
|
||||||
|
|
||||||
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
||||||
val deviceLibrary = musicRepository.deviceLibrary
|
val deviceLibrary = musicRepository.deviceLibrary
|
||||||
|
logD(changes.deviceLibrary)
|
||||||
if (changes.deviceLibrary && deviceLibrary != null) {
|
if (changes.deviceLibrary && deviceLibrary != null) {
|
||||||
logD("Refreshing library")
|
logD("Refreshing library")
|
||||||
// Get the each list of items in the library to use as our list data.
|
// Get the each list of items in the library to use as our list data.
|
||||||
|
|
|
@ -189,6 +189,7 @@ constructor(
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun addUpdateListener(listener: MusicRepository.UpdateListener) {
|
override fun addUpdateListener(listener: MusicRepository.UpdateListener) {
|
||||||
updateListeners.add(listener)
|
updateListeners.add(listener)
|
||||||
|
listener.onMusicChanges(MusicRepository.Changes(deviceLibrary = true, userLibrary = true))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
|
@ -199,6 +200,7 @@ constructor(
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun addIndexingListener(listener: MusicRepository.IndexingListener) {
|
override fun addIndexingListener(listener: MusicRepository.IndexingListener) {
|
||||||
indexingListeners.add(listener)
|
indexingListeners.add(listener)
|
||||||
|
listener.onIndexingStateChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
|
|
|
@ -78,6 +78,14 @@ class MusicViewModel @Inject constructor(private val musicRepository: MusicRepos
|
||||||
musicRepository.requestIndex(false)
|
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.
|
* Non-manipulated statistics bound the last successful music load.
|
||||||
*
|
*
|
||||||
|
|
|
@ -123,14 +123,12 @@
|
||||||
app:layout_anchor="@id/home_content"
|
app:layout_anchor="@id/home_content"
|
||||||
app:layout_anchorGravity="bottom|end">
|
app:layout_anchorGravity="bottom|end">
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<org.oxycblt.auxio.home.FlipFloatingActionButton
|
||||||
android:id="@+id/home_fab"
|
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"
|
||||||
android:layout_margin="@dimen/spacing_medium"
|
android:layout_margin="@dimen/spacing_medium" />
|
||||||
android:contentDescription="@string/desc_shuffle_all"
|
|
||||||
android:src="@drawable/ic_shuffle_off_24" />
|
|
||||||
|
|
||||||
</org.oxycblt.auxio.home.EdgeFrameLayout>
|
</org.oxycblt.auxio.home.EdgeFrameLayout>
|
||||||
|
|
||||||
|
|
|
@ -297,6 +297,7 @@
|
||||||
<string name="desc_change_repeat">Change repeat mode</string>
|
<string name="desc_change_repeat">Change repeat mode</string>
|
||||||
<string name="desc_shuffle">Turn shuffle on or off</string>
|
<string name="desc_shuffle">Turn shuffle on or off</string>
|
||||||
<string name="desc_shuffle_all">Shuffle all songs</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_exit">Stop playback</string>
|
||||||
|
|
||||||
<string name="desc_clear_queue_item">Remove this queue song</string>
|
<string name="desc_clear_queue_item">Remove this queue song</string>
|
||||||
|
|
Loading…
Reference in a new issue