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 9d5a9e972..0dcc1c96a 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -59,6 +59,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.fragment.ViewBindingFragment +import org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay import org.oxycblt.auxio.ui.selection.SelectionViewModel import org.oxycblt.auxio.util.* @@ -67,7 +68,7 @@ import org.oxycblt.auxio.util.* * respective item. * @author OxygenCobalt */ -class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuItemClickListener { +class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuItemClickListener, SelectionToolbarOverlay.Callback { private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val homeModel: HomeViewModel by androidActivityViewModels() private val musicModel: MusicViewModel by activityViewModels() @@ -114,9 +115,7 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI } } - binding.homeToolbarOverlay.registerListeners( - onExit = { selectionModel.consume() }, onMenuItemClick = this) - + binding.homeToolbarOverlay.callback = this binding.homeToolbar.setOnMenuItemClickListener(this@HomeFragment) updateTabConfiguration() @@ -174,14 +173,12 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI override fun onDestroyBinding(binding: FragmentHomeBinding) { super.onDestroyBinding(binding) - binding.homeToolbarOverlay.unregisterListeners() + binding.homeToolbarOverlay.callback = null binding.homeToolbar.setOnMenuItemClickListener(null) } override fun onMenuItemClick(item: MenuItem): Boolean { when (item.itemId) { - // HOME - R.id.action_search -> { logD("Navigating to search") // Reset selection (navigating to another selectable screen) @@ -210,16 +207,6 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI .withAscending(item.isChecked)) } - // SELECTION - - R.id.action_play_next_selection -> { - playbackModel.playNext(selectionModel.consume()) - requireContext().showToast(R.string.lng_queue_added) - } - R.id.action_queue_add_selection -> { - playbackModel.addToQueue(selectionModel.consume()) - requireContext().showToast(R.string.lng_queue_added) - } else -> { // Sorting option was selected, mark it as selected and update the mode item.isChecked = true @@ -230,9 +217,24 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI } } + // Always handling an item return true } + override fun onClearSelection() { + selectionModel.consume() + } + + override fun onPlaySelectionNext() { + playbackModel.playNext(selectionModel.consume()) + requireContext().showToast(R.string.lng_queue_added) + } + + override fun onAddSelectionToQueue() { + playbackModel.addToQueue(selectionModel.consume()) + requireContext().showToast(R.string.lng_queue_added) + } + private fun updateCurrentTab(tab: MusicMode) { // Make sure that we update the scrolling view and allowed menu items whenever // the tab changes. @@ -453,9 +455,9 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI * https://al-e-shevelev.medium.com/how-to-reduce-scroll-sensitivity-of-viewpager2-widget-87797ad02414 */ private fun ViewPager2.reduceSensitivity(by: Int) { - val recycler = VIEW_PAGER_RECYCLER_FIELD.get(this@reduceSensitivity) - val slop = VIEW_PAGER_TOUCH_SLOP_FIELD.get(recycler) as Int - VIEW_PAGER_TOUCH_SLOP_FIELD.set(recycler, slop * by) + val recycler = VP_RECYCLER_FIELD.get(this@reduceSensitivity) + val slop = RV_TOUCH_SLOP_FIELD.get(recycler) as Int + RV_TOUCH_SLOP_FIELD.set(recycler, slop * by) } /** Forces the view to recreate all fragments contained within it. */ @@ -480,9 +482,9 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI } companion object { - private val VIEW_PAGER_RECYCLER_FIELD: Field by + private val VP_RECYCLER_FIELD: Field by lazyReflectedField(ViewPager2::class, "mRecyclerView") - private val VIEW_PAGER_TOUCH_SLOP_FIELD: Field by + private val RV_TOUCH_SLOP_FIELD: Field by lazyReflectedField(RecyclerView::class, "mTouchSlop") private const val KEY_LAST_TRANSITION_AXIS = BuildConfig.APPLICATION_ID + ".key.LAST_TRANSITION_AXIS" diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt index ce626d726..b4b6d693d 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt @@ -24,20 +24,10 @@ import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.ui.recycler.AlbumViewHolder -import org.oxycblt.auxio.ui.recycler.ArtistViewHolder -import org.oxycblt.auxio.ui.recycler.AuxioRecyclerView -import org.oxycblt.auxio.ui.recycler.GenreViewHolder -import org.oxycblt.auxio.ui.recycler.Header -import org.oxycblt.auxio.ui.recycler.HeaderViewHolder -import org.oxycblt.auxio.ui.recycler.Item -import org.oxycblt.auxio.ui.recycler.MenuItemListener -import org.oxycblt.auxio.ui.recycler.PlayingIndicatorAdapter -import org.oxycblt.auxio.ui.recycler.SimpleItemCallback -import org.oxycblt.auxio.ui.recycler.SongViewHolder +import org.oxycblt.auxio.ui.recycler.* class SearchAdapter(private val listener: MenuItemListener) : - PlayingIndicatorAdapter(), AuxioRecyclerView.SpanSizeLookup { + SelectionIndicatorAdapter(), AuxioRecyclerView.SpanSizeLookup { private val differ = AsyncListDiffer(this, DIFFER) override fun getItemCount() = differ.currentList.size diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index 718ede499..17387bbed 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -27,6 +27,7 @@ import androidx.core.view.isInvisible import androidx.core.view.postDelayed import androidx.core.widget.addTextChangedListener import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.google.android.material.transition.MaterialSharedAxis import org.oxycblt.auxio.R @@ -42,22 +43,21 @@ import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.ui.fragment.MenuFragment import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.MenuItemListener -import org.oxycblt.auxio.util.androidViewModels -import org.oxycblt.auxio.util.collect -import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.context -import org.oxycblt.auxio.util.getSystemServiceCompat -import org.oxycblt.auxio.util.logW +import org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay +import org.oxycblt.auxio.ui.selection.SelectionViewModel +import org.oxycblt.auxio.util.* /** * A [Fragment] that allows for the searching of the entire music library. + * FIXME: Keyboard logic is really wonky * @author OxygenCobalt */ class SearchFragment : - MenuFragment(), MenuItemListener, Toolbar.OnMenuItemClickListener { + MenuFragment(), MenuItemListener, Toolbar.OnMenuItemClickListener, SelectionToolbarOverlay.Callback { // SearchViewModel is only scoped to this Fragment private val searchModel: SearchViewModel by androidViewModels() + private val selectionModel: SelectionViewModel by activityViewModels() private val searchAdapter = SearchAdapter(this) private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } @@ -78,6 +78,8 @@ class SearchFragment : override fun onCreateBinding(inflater: LayoutInflater) = FragmentSearchBinding.inflate(inflater) override fun onBindingCreated(binding: FragmentSearchBinding, savedInstanceState: Bundle?) { + binding.searchToolbarOverlay.callback = this + binding.searchToolbar.apply { val itemIdToSelect = when (searchModel.filterMode) { @@ -91,7 +93,12 @@ class SearchFragment : menu.findItem(itemIdToSelect).isChecked = true setNavigationOnClickListener { + // Reset selection (navigating to another selectable screen) + selectionModel.consume() + + // Drop keyboard as it's no longer needed imm.hide() + findNavController().navigateUp() } @@ -121,40 +128,61 @@ class SearchFragment : collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback) collect(navModel.exploreNavigationItem, ::handleNavigation) + collectImmediately(selectionModel.selected, ::handleSelection) } override fun onDestroyBinding(binding: FragmentSearchBinding) { + binding.searchToolbarOverlay.callback = null binding.searchToolbar.setOnMenuItemClickListener(null) binding.searchRecycler.adapter = null } override fun onMenuItemClick(item: MenuItem): Boolean { - when (item.itemId) { - R.id.submenu_filtering -> {} - else -> { - if (item.itemId != R.id.submenu_filtering) { - searchModel.updateFilterModeWithId(item.itemId) - item.isChecked = true - } - } + if (item.itemId != R.id.submenu_filtering) { + searchModel.updateFilterModeWithId(item.itemId) + item.isChecked = true } return true } + override fun onClearSelection() { + selectionModel.consume() + } + + override fun onPlaySelectionNext() { + playbackModel.playNext(selectionModel.consume()) + requireContext().showToast(R.string.lng_queue_added) + } + + override fun onAddSelectionToQueue() { + playbackModel.addToQueue(selectionModel.consume()) + requireContext().showToast(R.string.lng_queue_added) + } + override fun onItemClick(item: Item) { - when (item) { - is Song -> - when (settings.libPlaybackMode) { - MusicMode.SONGS -> playbackModel.playFromAll(item) - MusicMode.ALBUMS -> playbackModel.playFromAlbum(item) - MusicMode.ARTISTS -> playbackModel.playFromArtist(item) - else -> error("Unexpected playback mode: ${settings.libPlaybackMode}") - } - is MusicParent -> navModel.exploreNavigateTo(item) + check(item is Music) { "Unexpected datatype ${item::class.simpleName}"} + if (selectionModel.selected.value.isEmpty()) { + when (item) { + is Song -> + when (settings.libPlaybackMode) { + MusicMode.SONGS -> playbackModel.playFromAll(item) + MusicMode.ALBUMS -> playbackModel.playFromAlbum(item) + MusicMode.ARTISTS -> playbackModel.playFromArtist(item) + else -> error("Unexpected playback mode: ${settings.libPlaybackMode}") + } + is MusicParent -> navModel.exploreNavigateTo(item) + } + } else { + selectionModel.select(item) } } + override fun onSelect(item: Item) { + check(item is Music) { "Unexpected datatype ${item::class.simpleName}"} + selectionModel.select(item) + } + override fun onOpenMenu(item: Item, anchor: View) { when (item) { is Song -> musicMenu(anchor, R.menu.menu_song_actions, item) @@ -193,9 +221,20 @@ class SearchFragment : else -> return }) + // Reset selection (navigating to another selectable screen) + selectionModel.consume() + + // Drop keyboard as it's no longer needed imm.hide() } + private fun handleSelection(selected: List) { + searchAdapter.updateSelection(selected) + if (requireBinding().searchToolbarOverlay.updateSelectionAmount(selected.size) && selected.isNotEmpty()) { + imm.hide() + } + } + private fun InputMethodManager.hide() { hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS) } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/selection/SelectionToolbarOverlay.kt b/app/src/main/java/org/oxycblt/auxio/ui/selection/SelectionToolbarOverlay.kt index 905892eb9..eaa7c2232 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/selection/SelectionToolbarOverlay.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/selection/SelectionToolbarOverlay.kt @@ -20,6 +20,7 @@ package org.oxycblt.auxio.ui.selection import android.animation.ValueAnimator import android.content.Context import android.util.AttributeSet +import android.view.MenuItem import android.widget.FrameLayout import androidx.annotation.AttrRes import androidx.appcompat.widget.Toolbar @@ -37,11 +38,29 @@ class SelectionToolbarOverlay @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) { + var callback: Callback? = null + private lateinit var innerToolbar: MaterialToolbar private val selectionToolbar = MaterialToolbar(context).apply { - inflateMenu(R.menu.menu_selection_actions) setNavigationIcon(R.drawable.ic_close_24) + setNavigationOnClickListener { + callback?.onClearSelection() + } + + inflateMenu(R.menu.menu_selection_actions) + setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_play_next -> { + callback?.onPlaySelectionNext() + } + R.id.action_queue_add -> { + callback?.onAddSelectionToQueue() + } + } + + true + } } private var fadeThroughAnimator: ValueAnimator? = null @@ -56,23 +75,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr addView(selectionToolbar) } - /** Add listeners for the selection toolbar. */ - fun registerListeners( - onExit: OnClickListener, - onMenuItemClick: Toolbar.OnMenuItemClickListener - ) { - selectionToolbar.apply { - setNavigationOnClickListener(onExit) - setOnMenuItemClickListener(onMenuItemClick) - } - } - - /** Unregister listeners for this instance. */ - fun unregisterListeners() { - selectionToolbar.apply { - setNavigationOnClickListener(null) - setOnMenuItemClickListener(null) - } + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + callback = null } /** @@ -146,4 +151,10 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr isInvisible = innerAlpha == 1f } } + + interface Callback { + fun onClearSelection() + fun onPlaySelectionNext() + fun onAddSelectionToQueue() + } } diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index acb71a15d..b3be6d198 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -12,37 +12,44 @@ app:liftOnScroll="true" app:liftOnScrollTargetViewId="@id/search_recycler"> - + android:layout_height="wrap_content"> - + app:menu="@menu/menu_search" + app:navigationIcon="@drawable/ic_back_24"> - + app:endIconContentDescription="@string/desc_clear_search" + app:endIconDrawable="@drawable/ic_close_24" + app:endIconMode="clear_text" + app:endIconTint="?attr/colorControlNormal" + app:errorEnabled="false" + app:hintEnabled="false"> - + - + + + + + diff --git a/app/src/main/res/menu/menu_selection_actions.xml b/app/src/main/res/menu/menu_selection_actions.xml index 7e20e20eb..3d553a4b4 100644 --- a/app/src/main/res/menu/menu_selection_actions.xml +++ b/app/src/main/res/menu/menu_selection_actions.xml @@ -2,12 +2,12 @@