diff --git a/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt b/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt
new file mode 100644
index 000000000..c2b2842a3
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt
@@ -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 .
+ */
+
+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()
+ }
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
index 62563f159..0827a00b9 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
@@ -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, isFastScrolling: Boolean) {
+ private fun updateFab(songs: List, 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
diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt
index bc2baefe9..8b4e6d581 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt
@@ -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.
diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
index bcd001aa7..ac0bf498b 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
@@ -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
diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt
index 40746dd9c..c613fc8ea 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt
@@ -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.
*
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index 5e8da8d81..8fb877122 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -123,14 +123,12 @@
app:layout_anchor="@id/home_content"
app:layout_anchorGravity="bottom|end">
-
+ android:layout_margin="@dimen/spacing_medium" />
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d37d2ab0a..c0de9a5c8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -297,6 +297,7 @@
Change repeat mode
Turn shuffle on or off
Shuffle all songs
+ Create a new playlist
Stop playback
Remove this queue song