search: add selection
Add selection to the search view.
This commit is contained in:
parent
3d03194878
commit
8aeb6d092e
6 changed files with 149 additions and 100 deletions
|
@ -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<FragmentHomeBinding>(), Toolbar.OnMenuItemClickListener {
|
||||
class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), 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<FragmentHomeBinding>(), 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<FragmentHomeBinding>(), 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<FragmentHomeBinding>(), 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<FragmentHomeBinding>(), 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<FragmentHomeBinding>(), 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<FragmentHomeBinding>(), 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"
|
||||
|
|
|
@ -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<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
|
||||
SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
|
||||
private val differ = AsyncListDiffer(this, DIFFER)
|
||||
|
||||
override fun getItemCount() = differ.currentList.size
|
||||
|
|
|
@ -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<FragmentSearchBinding>(), MenuItemListener, Toolbar.OnMenuItemClickListener {
|
||||
MenuFragment<FragmentSearchBinding>(), 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<Music>) {
|
||||
searchAdapter.updateSelection(selected)
|
||||
if (requireBinding().searchToolbarOverlay.updateSelectionAmount(selected.size) && selected.isNotEmpty()) {
|
||||
imm.hide()
|
||||
}
|
||||
}
|
||||
|
||||
private fun InputMethodManager.hide() {
|
||||
hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,37 +12,44 @@
|
|||
app:liftOnScroll="true"
|
||||
app:liftOnScrollTargetViewId="@id/search_recycler">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/search_toolbar"
|
||||
<org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay
|
||||
android:id="@+id/search_toolbar_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:menu="@menu/menu_search"
|
||||
app:navigationIcon="@drawable/ic_back_24">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/search_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
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">
|
||||
app:menu="@menu/menu_search"
|
||||
app:navigationIcon="@drawable/ic_back_24">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/search_edit_text"
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:hint="@string/lng_search_library"
|
||||
android:imeOptions="actionSearch|flagNoExtractUi"
|
||||
android:inputType="textFilter"
|
||||
android:paddingStart="@dimen/spacing_tiny"
|
||||
android:paddingEnd="0dp" />
|
||||
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">
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/search_edit_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:hint="@string/lng_search_library"
|
||||
android:imeOptions="actionSearch|flagNoExtractUi"
|
||||
android:inputType="textFilter"
|
||||
android:paddingStart="@dimen/spacing_tiny"
|
||||
android:paddingEnd="0dp" />
|
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
</org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay>
|
||||
|
||||
</org.oxycblt.auxio.ui.AuxioAppBarLayout>
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_play_next_selection"
|
||||
android:id="@+id/action_play_next"
|
||||
android:title="@string/lbl_play_next"
|
||||
android:icon="@drawable/ic_play_next"
|
||||
app:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/action_queue_add_selection"
|
||||
android:id="@+id/action_queue_add"
|
||||
android:icon="@drawable/ic_play_next"
|
||||
android:title="@string/lbl_queue_add"
|
||||
app:showAsAction="never" />
|
||||
|
|
Loading…
Reference in a new issue