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