From 05a5ef5c3f8d931e9a471dc8505d90a6edbc03f1 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Sun, 27 Mar 2022 09:59:35 -0600 Subject: [PATCH] ui: migrate esoteric adapters to framework Migrate all esoteric adapters to the new RecyclerView framework. One of the shortcomings with the previous RecyclerView utilities was how more esoteric adapters with data that does not implement Item could not use the utlities. The new system, by comparison, is capable of taking any type of data, so we can no migrate all of the esoteric adapters to the new system. --- .../java/org/oxycblt/auxio/MainActivity.kt | 2 + .../org/oxycblt/auxio/accent/AccentAdapter.kt | 124 ++++++++++------- .../auxio/accent/AccentCustomizeDialog.kt | 35 +++-- .../auxio/coil/SquareFrameTransform.kt | 2 - .../auxio/detail/AlbumDetailFragment.kt | 3 +- .../auxio/detail/ArtistDetailFragment.kt | 4 +- .../auxio/detail/GenreDetailFragment.kt | 4 +- .../detail/recycler/AlbumDetailAdapter.kt | 20 ++- .../detail/recycler/ArtistDetailAdapter.kt | 14 +- .../auxio/detail/recycler/DetailAdapter.kt | 18 +-- .../detail/recycler/GenreDetailAdapter.kt | 14 +- .../java/org/oxycblt/auxio/home/tabs/Tab.kt | 23 ++-- .../org/oxycblt/auxio/home/tabs/TabAdapter.kt | 126 ++++++++++-------- .../auxio/home/tabs/TabCustomizeDialog.kt | 93 ++++++++----- .../auxio/home/tabs/TabDragCallback.kt | 25 +--- .../auxio/music/excluded/ExcludedDialog.kt | 22 ++- .../music/excluded/ExcludedEntryAdapter.kt | 67 +++++----- .../auxio/music/excluded/ExcludedViewModel.kt | 16 ++- .../auxio/playback/queue/QueueAdapter.kt | 68 +++++++++- .../org/oxycblt/auxio/ui/RecyclerFramework.kt | 83 ------------ .../oxycblt/auxio/widgets/WidgetProvider.kt | 2 +- info/ARCHITECTURE.md | 7 +- 22 files changed, 409 insertions(+), 363 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 95ab4f9d4..e71d0908f 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -43,6 +43,8 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat * TODO: Custom language support * * TODO: Rework menus [perhaps add multi-select] + * + * TODO: Rework navigation to be based on a viewmodel */ class MainActivity : AppCompatActivity() { private val playbackModel: PlaybackViewModel by viewModels() diff --git a/app/src/main/java/org/oxycblt/auxio/accent/AccentAdapter.kt b/app/src/main/java/org/oxycblt/auxio/accent/AccentAdapter.kt index 18a6fb486..4f471dfd6 100644 --- a/app/src/main/java/org/oxycblt/auxio/accent/AccentAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/accent/AccentAdapter.kt @@ -17,71 +17,93 @@ package org.oxycblt.auxio.accent -import android.view.ViewGroup +import android.content.Context import androidx.appcompat.widget.TooltipCompat import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemAccentBinding +import org.oxycblt.auxio.ui.BackingData +import org.oxycblt.auxio.ui.BindingViewHolder +import org.oxycblt.auxio.ui.MonoAdapter import org.oxycblt.auxio.util.getAttrColorSafe import org.oxycblt.auxio.util.getColorSafe +import org.oxycblt.auxio.util.getViewHolderAt import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.stateList -/** - * An adapter that displays the list of all possible accents, and highlights the current one. - * @author OxygenCobalt - * @param onSelect What to do when an accent is selected. - */ -class AccentAdapter(private var curAccent: Accent, private val onSelect: (accent: Accent) -> Unit) : - RecyclerView.Adapter() { - private var selectedViewHolder: ViewHolder? = null +/** An adapter that displays the accent palette. */ +class AccentAdapter(listener: Listener) : + MonoAdapter(listener) { + var selectedAccent: Accent? = null + private set + private var selectedViewHolder: NewAccentViewHolder? = null - override fun getItemCount(): Int = ACCENT_COUNT + override val data = AccentData() + override val creator = NewAccentViewHolder.CREATOR - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return ViewHolder(ItemAccentBinding.inflate(parent.context.inflater)) - } + override fun onBindViewHolder(viewHolder: NewAccentViewHolder, position: Int) { + super.onBindViewHolder(viewHolder, position) - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(Accent(position)) - } - - private fun setAccent(accent: Accent) { - curAccent = accent - onSelect(accent) - } - - inner class ViewHolder(private val binding: ItemAccentBinding) : - RecyclerView.ViewHolder(binding.root) { - - fun bind(accent: Accent) { - setSelected(accent == curAccent) - - binding.accent.apply { - backgroundTintList = context.getColorSafe(accent.primary).stateList - contentDescription = context.getString(accent.name) - TooltipCompat.setTooltipText(this, contentDescription) - } - - binding.accent.setOnClickListener { - setAccent(accent) - setSelected(true) - } + if (data.getItem(position) == selectedAccent) { + selectedViewHolder?.setSelected(false) + selectedViewHolder = viewHolder + viewHolder.setSelected(true) } + } - private fun setSelected(isSelected: Boolean) { - val context = binding.accent.context + fun setSelectedAccent(accent: Accent, recycler: RecyclerView) { + if (accent == selectedAccent) return + selectedAccent = accent + selectedViewHolder?.setSelected(false) + selectedViewHolder = recycler.getViewHolderAt(accent.index) as NewAccentViewHolder? + selectedViewHolder?.setSelected(true) + } - binding.accent.isEnabled = !isSelected - binding.accent.imageTintList = - if (isSelected) { - // Switch out the currently selected ViewHolder with this one. - selectedViewHolder?.setSelected(false) - selectedViewHolder = this - context.getAttrColorSafe(R.attr.colorSurface).stateList - } else { - context.getColorSafe(android.R.color.transparent).stateList - } - } + interface Listener { + fun onAccentSelected(accent: Accent) + } + + class AccentData : BackingData() { + override fun getItem(position: Int) = Accent(position) + override fun getItemCount() = ACCENT_COUNT + } +} + +class NewAccentViewHolder private constructor(private val binding: ItemAccentBinding) : + BindingViewHolder(binding.root) { + + override fun bind(item: Accent, listener: AccentAdapter.Listener) { + setSelected(false) + + binding.accent.apply { + backgroundTintList = context.getColorSafe(item.primary).stateList + contentDescription = context.getString(item.name) + TooltipCompat.setTooltipText(this, contentDescription) + } + + binding.accent.setOnClickListener { listener.onAccentSelected(item) } + } + + fun setSelected(isSelected: Boolean) { + val context = binding.accent.context + + binding.accent.isEnabled = !isSelected + binding.accent.imageTintList = + if (isSelected) { + context.getAttrColorSafe(R.attr.colorSurface).stateList + } else { + context.getColorSafe(android.R.color.transparent).stateList + } + } + + companion object { + val CREATOR = + object : Creator { + override val viewType: Int + get() = throw UnsupportedOperationException() + + override fun create(context: Context) = + NewAccentViewHolder(ItemAccentBinding.inflate(context.inflater)) + } } } diff --git a/app/src/main/java/org/oxycblt/auxio/accent/AccentCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/accent/AccentCustomizeDialog.kt index 2f8c95e16..55f961667 100644 --- a/app/src/main/java/org/oxycblt/auxio/accent/AccentCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/accent/AccentCustomizeDialog.kt @@ -31,9 +31,10 @@ import org.oxycblt.auxio.util.logD * Dialog responsible for showing the list of accents to select. * @author OxygenCobalt */ -class AccentCustomizeDialog : ViewBindingDialogFragment() { +class AccentCustomizeDialog : + ViewBindingDialogFragment(), AccentAdapter.Listener { private val settingsManager = SettingsManager.getInstance() - private var pendingAccent = settingsManager.accent + private var accentAdapter = AccentAdapter(this) override fun onCreateBinding(inflater: LayoutInflater) = DialogAccentBinding.inflate(inflater) @@ -41,9 +42,9 @@ class AccentCustomizeDialog : ViewBindingDialogFragment() { builder.setTitle(R.string.set_accent) builder.setPositiveButton(android.R.string.ok) { _, _ -> - if (pendingAccent != settingsManager.accent) { + if (accentAdapter.selectedAccent != settingsManager.accent) { logD("Applying new accent") - settingsManager.accent = pendingAccent + settingsManager.accent = requireNotNull(accentAdapter.selectedAccent) requireActivity().recreate() } @@ -55,22 +56,30 @@ class AccentCustomizeDialog : ViewBindingDialogFragment() { } override fun onBindingCreated(binding: DialogAccentBinding, savedInstanceState: Bundle?) { - savedInstanceState?.getInt(KEY_PENDING_ACCENT)?.let { index -> - pendingAccent = Accent(index) - } + accentAdapter.setSelectedAccent( + if (savedInstanceState != null) { + Accent(savedInstanceState.getInt(KEY_PENDING_ACCENT)) + } else { + settingsManager.accent + }, + binding.accentRecycler) // --- UI SETUP --- - binding.accentRecycler.adapter = - AccentAdapter(pendingAccent) { accent -> - logD("Switching selected accent to $accent") - pendingAccent = accent - } + binding.accentRecycler.adapter = accentAdapter } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putInt(KEY_PENDING_ACCENT, pendingAccent.index) + outState.putInt(KEY_PENDING_ACCENT, requireNotNull(accentAdapter.selectedAccent).index) + } + + override fun onDestroyBinding(binding: DialogAccentBinding) { + binding.accentRecycler.adapter = null + } + + override fun onAccentSelected(accent: Accent) { + accentAdapter.setSelectedAccent(accent, requireBinding().accentRecycler) } companion object { diff --git a/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt b/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt index d011ad073..8b347d8b9 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt @@ -22,7 +22,6 @@ import coil.size.Size import coil.size.pxOrElse import coil.transform.Transformation import kotlin.math.min -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE /** @@ -47,7 +46,6 @@ class SquareFrameTransform : Transformation { val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize) if (dstSize != desiredWidth || dstSize != desiredHeight) { - logD("RETARD YOU STUPID FUCKING IDIOT $desiredWidth $desiredHeight") try { // Desired size differs from the cropped size, resize the bitmap. return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index d258f06bd..d48d70837 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -27,7 +27,6 @@ import androidx.recyclerview.widget.LinearSmoothScroller import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter -import org.oxycblt.auxio.detail.recycler.AlbumDetailItemListener import org.oxycblt.auxio.detail.recycler.SortHeader import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist @@ -47,7 +46,7 @@ import org.oxycblt.auxio.util.showToast * The [DetailFragment] for an album. * @author OxygenCobalt */ -class AlbumDetailFragment : DetailFragment(), AlbumDetailItemListener { +class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener { private val args: AlbumDetailFragmentArgs by navArgs() private val detailAdapter = AlbumDetailAdapter(this) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 7c96ecbc3..b6ea6e0d5 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -24,7 +24,7 @@ import androidx.navigation.fragment.navArgs import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter -import org.oxycblt.auxio.detail.recycler.DetailItemListener +import org.oxycblt.auxio.detail.recycler.DetailAdapter import org.oxycblt.auxio.detail.recycler.SortHeader import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist @@ -43,7 +43,7 @@ import org.oxycblt.auxio.util.logW * The [DetailFragment] for an artist. * @author OxygenCobalt */ -class ArtistDetailFragment : DetailFragment(), DetailItemListener { +class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener { private val args: ArtistDetailFragmentArgs by navArgs() private val detailAdapter = ArtistDetailAdapter(this) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 958c6a7af..e744a02cd 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -22,7 +22,7 @@ import android.view.View import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import org.oxycblt.auxio.databinding.FragmentDetailBinding -import org.oxycblt.auxio.detail.recycler.DetailItemListener +import org.oxycblt.auxio.detail.recycler.DetailAdapter import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter import org.oxycblt.auxio.detail.recycler.SortHeader import org.oxycblt.auxio.music.Album @@ -42,7 +42,7 @@ import org.oxycblt.auxio.util.logW * The [DetailFragment] for a genre. * @author OxygenCobalt */ -class GenreDetailFragment : DetailFragment(), DetailItemListener { +class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener { private val args: GenreDetailFragmentArgs by navArgs() private val detailAdapter = GenreDetailAdapter(this) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt index ed5d81cf1..2fd4252b4 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt @@ -40,8 +40,8 @@ import org.oxycblt.auxio.util.textSafe * An adapter for displaying [Album] information and it's children. * @author OxygenCobalt */ -class AlbumDetailAdapter(listener: AlbumDetailItemListener) : - DetailAdapter(listener, DIFFER) { +class AlbumDetailAdapter(listener: Listener) : + DetailAdapter(listener, DIFFER) { private var highlightedSong: Song? = null private var highlightedViewHolder: Highlightable? = null @@ -61,11 +61,7 @@ class AlbumDetailAdapter(listener: AlbumDetailItemListener) : else -> null } - override fun onBind( - viewHolder: RecyclerView.ViewHolder, - item: Item, - listener: AlbumDetailItemListener - ) { + override fun onBind(viewHolder: RecyclerView.ViewHolder, item: Item, listener: Listener) { super.onBind(viewHolder, item, listener) when (item) { @@ -112,16 +108,16 @@ class AlbumDetailAdapter(listener: AlbumDetailItemListener) : } } } -} -interface AlbumDetailItemListener : DetailItemListener { - fun onNavigateToArtist() + interface Listener : DetailAdapter.Listener { + fun onNavigateToArtist() + } } private class AlbumDetailViewHolder private constructor(private val binding: ItemDetailBinding) : - BindingViewHolder(binding.root) { + BindingViewHolder(binding.root) { - override fun bind(item: Album, listener: AlbumDetailItemListener) { + override fun bind(item: Album, listener: AlbumDetailAdapter.Listener) { binding.detailCover.bindAlbumCover(item) binding.detailName.textSafe = item.resolvedName diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt index c4c21aa6c..47d35c9c4 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt @@ -44,8 +44,8 @@ import org.oxycblt.auxio.util.textSafe * one actually contains both album information and song information. * @author OxygenCobalt */ -class ArtistDetailAdapter(listener: DetailItemListener) : - DetailAdapter(listener, DIFFER) { +class ArtistDetailAdapter(listener: Listener) : + DetailAdapter(listener, DIFFER) { private var currentAlbum: Album? = null private var currentAlbumHolder: Highlightable? = null @@ -70,11 +70,7 @@ class ArtistDetailAdapter(listener: DetailItemListener) : else -> null } - override fun onBind( - viewHolder: RecyclerView.ViewHolder, - item: Item, - listener: DetailItemListener - ) { + override fun onBind(viewHolder: RecyclerView.ViewHolder, item: Item, listener: Listener) { super.onBind(viewHolder, item, listener) when (item) { is Artist -> (viewHolder as ArtistDetailViewHolder).bind(item, listener) @@ -140,9 +136,9 @@ class ArtistDetailAdapter(listener: DetailItemListener) : } private class ArtistDetailViewHolder private constructor(private val binding: ItemDetailBinding) : - BindingViewHolder(binding.root) { + BindingViewHolder(binding.root) { - override fun bind(item: Artist, listener: DetailItemListener) { + override fun bind(item: Artist, listener: DetailAdapter.Listener) { binding.detailCover.bindArtistImage(item) binding.detailName.textSafe = item.resolvedName diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt index 598238632..6e1518710 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt @@ -39,7 +39,7 @@ import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.textSafe -abstract class DetailAdapter( +abstract class DetailAdapter( listener: L, diffCallback: DiffUtil.ItemCallback ) : MultiAdapter(listener) { @@ -112,13 +112,19 @@ abstract class DetailAdapter( } } } + + interface Listener : MenuItemListener { + fun onPlayParent() + fun onShuffleParent() + fun onShowSortMenu(anchor: View) + } } data class SortHeader(override val id: Long, @StringRes val string: Int) : Item() class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) : - BindingViewHolder(binding.root) { - override fun bind(item: SortHeader, listener: DetailItemListener) { + BindingViewHolder(binding.root) { + override fun bind(item: SortHeader, listener: DetailAdapter.Listener) { binding.headerTitle.textSafe = binding.context.getString(item.string) binding.headerButton.apply { TooltipCompat.setTooltipText(this, contentDescription) @@ -148,9 +154,3 @@ class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) : interface Highlightable { fun setHighlighted(isHighlighted: Boolean) } - -interface DetailItemListener : MenuItemListener { - fun onPlayParent() - fun onShuffleParent() - fun onShowSortMenu(anchor: View) -} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt index 6ca49d509..0c56656a0 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt @@ -41,8 +41,8 @@ import org.oxycblt.auxio.util.textSafe * An adapter for displaying genre information and it's children. * @author OxygenCobalt */ -class GenreDetailAdapter(listener: DetailItemListener) : - DetailAdapter(listener, DIFFER) { +class GenreDetailAdapter(listener: Listener) : + DetailAdapter(listener, DIFFER) { private var currentSong: Song? = null private var currentHolder: Highlightable? = null @@ -62,11 +62,7 @@ class GenreDetailAdapter(listener: DetailItemListener) : else -> null } - override fun onBind( - viewHolder: RecyclerView.ViewHolder, - item: Item, - listener: DetailItemListener - ) { + override fun onBind(viewHolder: RecyclerView.ViewHolder, item: Item, listener: Listener) { super.onBind(viewHolder, item, listener) when (item) { is Genre -> (viewHolder as GenreDetailViewHolder).bind(item, listener) @@ -115,8 +111,8 @@ class GenreDetailAdapter(listener: DetailItemListener) : } private class GenreDetailViewHolder private constructor(private val binding: ItemDetailBinding) : - BindingViewHolder(binding.root) { - override fun bind(item: Genre, listener: DetailItemListener) { + BindingViewHolder(binding.root) { + override fun bind(item: Genre, listener: DetailAdapter.Listener) { binding.detailCover.bindGenreImage(item) binding.detailName.textSafe = item.resolvedName binding.detailSubhead.textSafe = diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt index 8c234f5a0..58a3415bf 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt @@ -53,6 +53,16 @@ sealed class Tab(open val mode: DisplayMode) { /** The default tab sequence, represented in integer form */ const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100 + /** + * Maps between the integer code in the tab sequence and the actual [DisplayMode] instance + */ + private val MODE_TABLE = + arrayOf( + DisplayMode.SHOW_SONGS, + DisplayMode.SHOW_ALBUMS, + DisplayMode.SHOW_ARTISTS, + DisplayMode.SHOW_GENRES) + /** Convert an array [tabs] into a sequence of tabs. */ fun toSequence(tabs: Array): Int { // Like when deserializing, make sure there are no duplicate tabs for whatever reason. @@ -64,8 +74,8 @@ sealed class Tab(open val mode: DisplayMode) { for (tab in distinct) { val bin = when (tab) { - is Visible -> 1.shl(3) or tab.mode.ordinal - is Invisible -> tab.mode.ordinal + is Visible -> 1.shl(3) or MODE_TABLE.indexOf(tab.mode) + is Invisible -> MODE_TABLE.indexOf(tab.mode) } sequence = sequence or bin.shl(shift) @@ -84,14 +94,7 @@ sealed class Tab(open val mode: DisplayMode) { for (shift in (0..4 * SEQUENCE_LEN).reversed() step 4) { val chunk = sequence.shr(shift) and 0b1111 - val mode = - when (chunk and 7) { - 0 -> DisplayMode.SHOW_SONGS - 1 -> DisplayMode.SHOW_ALBUMS - 2 -> DisplayMode.SHOW_ARTISTS - 3 -> DisplayMode.SHOW_GENRES - else -> continue - } + val mode = MODE_TABLE.getOrNull(chunk and 7) ?: continue // Figure out the visibility tabs += diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt index 1a6dfbe31..e07658d1f 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt @@ -18,70 +18,92 @@ package org.oxycblt.auxio.home.tabs import android.annotation.SuppressLint +import android.content.Context import android.view.MotionEvent -import android.view.ViewGroup -import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.databinding.ItemTabBinding +import org.oxycblt.auxio.ui.BackingData +import org.oxycblt.auxio.ui.BindingViewHolder +import org.oxycblt.auxio.ui.DisplayMode +import org.oxycblt.auxio.ui.MonoAdapter import org.oxycblt.auxio.util.inflater -class TabAdapter( - private val touchHelper: ItemTouchHelper, - private val getTabs: () -> Array, - private val onTabSwitch: (Tab) -> Unit, -) : RecyclerView.Adapter() { - private val tabs: Array - get() = getTabs() +class TabAdapter(listener: Listener) : + MonoAdapter(listener) { + override val data = TabData(this) + override val creator = TabViewHolder.CREATOR - override fun getItemCount(): Int = Tab.SEQUENCE_LEN - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabViewHolder { - return TabViewHolder(ItemTabBinding.inflate(parent.context.inflater)) + interface Listener { + fun onVisibilityToggled(displayMode: DisplayMode) + fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) } - override fun onBindViewHolder(holder: TabViewHolder, position: Int) { - holder.bind(tabs[position]) - } + class TabData(private val adapter: RecyclerView.Adapter<*>) : BackingData() { + var tabs = arrayOf() + private set - inner class TabViewHolder(private val binding: ItemTabBinding) : - RecyclerView.ViewHolder(binding.root) { - init { - binding.root.layoutParams = - RecyclerView.LayoutParams( - RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) + override fun getItem(position: Int) = tabs[position] + override fun getItemCount() = tabs.size + + @Suppress("NotifyDatasetChanged") + fun submitTabs(newTabs: Array) { + tabs = newTabs + adapter.notifyDataSetChanged() } - @SuppressLint("ClickableViewAccessibility") - fun bind(tab: Tab) { - binding.root.apply { - setOnClickListener { - // Don't do a typical notifyDataSetChanged call here, because - // A. We don't have a real ViewModel state since this is a dialog - // B. Doing so would cause a relayout and the ripple effect to disappear - // Instead, simply notify a tab change and let TabCustomizeDialog handle it. - binding.tabIcon.isChecked = !binding.tabIcon.isChecked - onTabSwitch(tab) - } - } + fun setTab(at: Int, tab: Tab) { + tabs[at] = tab + } - binding.tabIcon.apply { - setText(tab.mode.string) - isChecked = tab is Tab.Visible - } - - // Roll our own drag handlers as the default ones suck - binding.tabDragHandle.setOnTouchListener { _, motionEvent -> - binding.tabDragHandle.performClick() - if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) { - touchHelper.startDrag(this) - true - } else false - } - - binding.root.setOnLongClickListener { - touchHelper.startDrag(this) - true - } + fun moveItems(from: Int, to: Int) { + val t = tabs[to] + val f = tabs[from] + tabs[from] = t + tabs[to] = f + adapter.notifyItemMoved(from, to) } } } + +class TabViewHolder private constructor(private val binding: ItemTabBinding) : + BindingViewHolder(binding.root) { + @SuppressLint("ClickableViewAccessibility") + override fun bind(item: Tab, listener: TabAdapter.Listener) { + binding.root.apply { + setOnClickListener { + binding.tabIcon.isChecked = !binding.tabIcon.isChecked + listener.onVisibilityToggled(item.mode) + } + } + + binding.tabIcon.apply { + setText(item.mode.string) + isChecked = item is Tab.Visible + } + + // Roll our own drag handlers as the default ones suck + binding.tabDragHandle.setOnTouchListener { _, motionEvent -> + binding.tabDragHandle.performClick() + if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) { + listener.onPickUpTab(this) + true + } else false + } + + binding.root.setOnLongClickListener { + listener.onPickUpTab(this) + true + } + } + + companion object { + val CREATOR = + object : Creator { + override val viewType: Int + get() = throw UnsupportedOperationException() + + override fun create(context: Context) = + TabViewHolder(ItemTabBinding.inflate(context.inflater)) + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt index 2ba910544..6a1416b8d 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt @@ -21,21 +21,26 @@ import android.os.Bundle import android.view.LayoutInflater import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogTabsBinding import org.oxycblt.auxio.settings.SettingsManager +import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.requireAttached /** * The dialog for customizing library tabs. This dialog does not rely on any specific ViewModel and * serializes it's state instead of * @author OxygenCobalt */ -class TabCustomizeDialog : ViewBindingDialogFragment() { +class TabCustomizeDialog : ViewBindingDialogFragment(), TabAdapter.Listener { private val settingsManager = SettingsManager.getInstance() - private var pendingTabs = settingsManager.libTabs + private val tabAdapter = TabAdapter(this) + private var touchHelper: ItemTouchHelper? = null + private var callback: TabDragCallback? = null override fun onCreateBinding(inflater: LayoutInflater) = DialogTabsBinding.inflate(inflater) @@ -44,7 +49,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment() { builder.setPositiveButton(android.R.string.ok) { _, _ -> logD("Committing tab changes") - settingsManager.libTabs = pendingTabs + settingsManager.libTabs = tabAdapter.data.tabs } // Negative button just dismisses, no need for a listener. @@ -52,49 +57,73 @@ class TabCustomizeDialog : ViewBindingDialogFragment() { } override fun onBindingCreated(binding: DialogTabsBinding, savedInstanceState: Bundle?) { - if (savedInstanceState != null) { - // Restore any pending tab configurations - val tabs = Tab.fromSequence(savedInstanceState.getInt(KEY_TABS)) - if (tabs != null) { - pendingTabs = tabs - } + val savedTabs = findSavedTabState(savedInstanceState) + if (savedTabs != null) { + logD("Found saved tab state") + tabAdapter.data.submitTabs(savedTabs) + } else { + tabAdapter.data.submitTabs(settingsManager.libTabs) } - // Set up adapter & drag callback - val callback = TabDragCallback { pendingTabs } - val helper = ItemTouchHelper(callback) - val tabAdapter = TabAdapter(helper, getTabs = { pendingTabs }, onTabSwitch = ::moveTabs) - - callback.addTabAdapter(tabAdapter) - binding.tabRecycler.apply { adapter = tabAdapter - helper.attachToRecyclerView(this) + requireTouchHelper().attachToRecyclerView(this) } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putInt(KEY_TABS, Tab.toSequence(pendingTabs)) + outState.putInt(KEY_TABS, Tab.toSequence(tabAdapter.data.tabs)) } - private fun moveTabs(tab: Tab) { - // Don't find the specific tab [Which might be outdated due to the nature - // of how ViewHolders are bound], but instead simply look for the mode in - // the list of pending tabs and update that instead. - val index = pendingTabs.indexOfFirst { it.mode == tab.mode } - if (index != -1) { - val curTab = pendingTabs[index] - logD("Updating tab $curTab to $tab") - pendingTabs[index] = - when (curTab) { - is Tab.Visible -> Tab.Invisible(curTab.mode) - is Tab.Invisible -> Tab.Visible(curTab.mode) - } + override fun onDestroyBinding(binding: DialogTabsBinding) { + super.onDestroyBinding(binding) + binding.tabRecycler.adapter = null + } + + override fun onVisibilityToggled(displayMode: DisplayMode) { + // Tab viewholders bind with the initial tab state, which will drift from the actual + // state of the tabs over editing. So, this callback simply provides the displayMode + // for us to locate within the data and then update. + val index = tabAdapter.data.tabs.indexOfFirst { it.mode == displayMode } + if (index > -1) { + val tab = tabAdapter.data.tabs[index] + tabAdapter.data.setTab( + index, + when (tab) { + is Tab.Visible -> Tab.Invisible(tab.mode) + is Tab.Invisible -> Tab.Visible(tab.mode) + }) } (requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = - pendingTabs.filterIsInstance().isNotEmpty() + tabAdapter.data.tabs.filterIsInstance().isNotEmpty() + } + + override fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) { + requireTouchHelper().startDrag(viewHolder) + } + + private fun findSavedTabState(savedInstanceState: Bundle?): Array? { + if (savedInstanceState != null) { + // Restore any pending tab configurations + return Tab.fromSequence(savedInstanceState.getInt(KEY_TABS)) + } + + return null + } + + private fun requireTouchHelper(): ItemTouchHelper { + requireAttached() + val instance = touchHelper + if (instance != null) { + return instance + } + val newCallback = TabDragCallback(tabAdapter) + val newInstance = ItemTouchHelper(newCallback) + callback = newCallback + touchHelper = newInstance + return newInstance } companion object { diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt index 0b86b8f2a..9603fb428 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt @@ -28,11 +28,7 @@ import androidx.recyclerview.widget.RecyclerView * TODO: Consider unifying the shared behavior between this and QueueDragCallback into a single * class. */ -class TabDragCallback(private val getTabs: () -> Array) : ItemTouchHelper.Callback() { - private val tabs: Array - get() = getTabs() - private lateinit var tabAdapter: TabAdapter - +class TabDragCallback(private val adapter: TabAdapter) : ItemTouchHelper.Callback() { override fun getMovementFlags( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder @@ -63,8 +59,7 @@ class TabDragCallback(private val getTabs: () -> Array) : ItemTouchHelper.C viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder ): Boolean { - tabs.swap(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) - tabAdapter.notifyItemMoved(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) + adapter.data.moveItems(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) return true } @@ -72,20 +67,4 @@ class TabDragCallback(private val getTabs: () -> Array) : ItemTouchHelper.C // We use a custom drag handle, so disable the long press action. override fun isLongPressDragEnabled(): Boolean = false - - /** - * Add the tab adapter to this callback. Done because there's a circular dependency between the - * two objects - */ - fun addTabAdapter(adapter: TabAdapter) { - tabAdapter = adapter - } - - private fun Array.swap(from: Int, to: Int) { - val t = get(to) - val f = get(from) - - set(from, t) - set(to, f) - } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt index 2e4672deb..4c4076b06 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt @@ -40,12 +40,14 @@ import org.oxycblt.auxio.util.showToast * Dialog that manages the currently excluded directories. * @author OxygenCobalt */ -class ExcludedDialog : ViewBindingDialogFragment() { +class ExcludedDialog : + ViewBindingDialogFragment(), ExcludedAdapter.Listener { private val excludedModel: ExcludedViewModel by viewModels { ExcludedViewModel.Factory(requireContext()) } private val playbackModel: PlaybackViewModel by activityViewModels() + private val excludedAdapter = ExcludedAdapter(this) override fun onCreateBinding(inflater: LayoutInflater) = DialogExcludedBinding.inflate(inflater) @@ -59,11 +61,10 @@ class ExcludedDialog : ViewBindingDialogFragment() { } override fun onBindingCreated(binding: DialogExcludedBinding, savedInstanceState: Bundle?) { - val adapter = ExcludedEntryAdapter { path -> excludedModel.removePath(path) } val launcher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), ::addDocTreePath) - binding.excludedRecycler.adapter = adapter + binding.excludedRecycler.adapter = excludedAdapter // Now that the dialog exists, we get the view manually when the dialog is shown // and override its click listener so that the dialog does not auto-dismiss when we @@ -90,13 +91,22 @@ class ExcludedDialog : ViewBindingDialogFragment() { // --- VIEWMODEL SETUP --- - excludedModel.paths.observe(viewLifecycleOwner) { paths -> updatePaths(paths, adapter) } + excludedModel.paths.observe(viewLifecycleOwner, ::updatePaths) logD("Dialog created") } - private fun updatePaths(paths: MutableList, adapter: ExcludedEntryAdapter) { - adapter.submitList(paths) + override fun onDestroyBinding(binding: DialogExcludedBinding) { + super.onDestroyBinding(binding) + binding.excludedRecycler.adapter = null + } + + override fun onRemovePath(path: String) { + excludedModel.removePath(path) + } + + private fun updatePaths(paths: MutableList) { + excludedAdapter.data.submitList(paths) requireBinding().excludedEmpty.isVisible = paths.isEmpty() } diff --git a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedEntryAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedEntryAdapter.kt index 7da9ff4f8..75844d534 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedEntryAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedEntryAdapter.kt @@ -17,10 +17,12 @@ package org.oxycblt.auxio.music.excluded -import android.annotation.SuppressLint -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView +import android.content.Context +import androidx.recyclerview.widget.DiffUtil import org.oxycblt.auxio.databinding.ItemExcludedDirBinding +import org.oxycblt.auxio.ui.BindingViewHolder +import org.oxycblt.auxio.ui.MonoAdapter +import org.oxycblt.auxio.ui.PrimitiveBackingData import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.textSafe @@ -28,37 +30,34 @@ import org.oxycblt.auxio.util.textSafe * Adapter that shows the excluded directories and their "Clear" button. * @author OxygenCobalt */ -class ExcludedEntryAdapter(private val onClear: (String) -> Unit) : - RecyclerView.Adapter() { - private var paths = mutableListOf() +class ExcludedAdapter(listener: Listener) : + MonoAdapter(listener) { + override val data = PrimitiveBackingData(this) + override val creator = ExcludedViewHolder.CREATOR - override fun getItemCount() = paths.size - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return ViewHolder(ItemExcludedDirBinding.inflate(parent.context.inflater)) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(paths[position]) - } - - @SuppressLint("NotifyDataSetChanged") - fun submitList(newPaths: MutableList) { - paths = newPaths - notifyDataSetChanged() - } - - inner class ViewHolder(private val binding: ItemExcludedDirBinding) : - RecyclerView.ViewHolder(binding.root) { - init { - binding.root.layoutParams = - RecyclerView.LayoutParams( - RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) - } - - fun bind(path: String) { - binding.excludedPath.textSafe = path - binding.excludedClear.setOnClickListener { onClear(path) } - } + interface Listener { + fun onRemovePath(path: String) + } +} + +/** + * The viewholder for [ExcludedAdapter]. Not intended for use in other adapters. + */ +class ExcludedViewHolder private constructor(private val binding: ItemExcludedDirBinding) : + BindingViewHolder(binding.root) { + override fun bind(item: String, listener: ExcludedAdapter.Listener) { + binding.excludedPath.textSafe = item + binding.excludedClear.setOnClickListener { listener.onRemovePath(item) } + } + + companion object { + val CREATOR = + object : Creator { + override val viewType: Int + get() = throw UnsupportedOperationException() + + override fun create(context: Context) = + ExcludedViewHolder(ItemExcludedDirBinding.inflate(context.inflater)) + } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt index 610a614b8..6db5683ef 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt @@ -40,11 +40,8 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo val paths: LiveData> get() = mPaths - private var dbPaths = listOf() - - /** Check if changes have been made to the ViewModel's paths. */ - val isModified: Boolean - get() = dbPaths != paths.value + var isModified: Boolean = false + private set init { loadDatabasePaths() @@ -58,6 +55,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo if (!mPaths.value!!.contains(path)) { mPaths.value!!.add(path) mPaths.value = mPaths.value + isModified = true } } @@ -68,6 +66,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo fun removePath(path: String) { mPaths.value!!.remove(path) mPaths.value = mPaths.value + isModified = true } /** Save the pending paths to the database. [onDone] will be called on completion. */ @@ -75,7 +74,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo viewModelScope.launch(Dispatchers.IO) { val start = System.currentTimeMillis() excludedDatabase.writePaths(mPaths.value!!) - dbPaths = mPaths.value!! + isModified = false onDone() this@ExcludedViewModel.logD( "Path save completed successfully in ${System.currentTimeMillis() - start}ms") @@ -86,8 +85,11 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo private fun loadDatabasePaths() { viewModelScope.launch(Dispatchers.IO) { val start = System.currentTimeMillis() - dbPaths = excludedDatabase.readPaths() + isModified = false + + val dbPaths = excludedDatabase.readPaths() withContext(Dispatchers.Main) { mPaths.value = dbPaths.toMutableList() } + this@ExcludedViewModel.logD( "Path load completed successfully in ${System.currentTimeMillis() - start}ms") } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt index 335a22379..2ffaca04a 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt @@ -23,14 +23,16 @@ import android.graphics.drawable.ColorDrawable import android.view.MotionEvent import android.view.View import androidx.core.view.isInvisible +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.google.android.material.shape.MaterialShapeDrawable import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.coil.bindAlbumCover import org.oxycblt.auxio.databinding.ItemQueueSongBinding import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.ui.BackingData import org.oxycblt.auxio.ui.BindingViewHolder -import org.oxycblt.auxio.ui.HybridBackingData import org.oxycblt.auxio.ui.MonoAdapter import org.oxycblt.auxio.ui.SongViewHolder import org.oxycblt.auxio.util.disableDropShadowCompat @@ -105,3 +107,67 @@ private constructor( val DIFFER = SongViewHolder.DIFFER } } + +/** + * A list-backed [BackingData] that can be modified with both adapter primitives and + * [AsyncListDiffer]. This is incredibly dangerous and can probably crash the app if you look at it + * the wrong way, so please don't use it outside of the queue module. + */ +class HybridBackingData( + private val adapter: RecyclerView.Adapter<*>, + diffCallback: DiffUtil.ItemCallback +) : BackingData() { + private var mCurrentList = mutableListOf() + val currentList: List + get() = mCurrentList + + private val differ = AsyncListDiffer(adapter, diffCallback) + + override fun getItem(position: Int): T = mCurrentList[position] + override fun getItemCount(): Int = mCurrentList.size + + fun submitList(newData: List, onDone: () -> Unit = {}) { + if (newData != mCurrentList) { + mCurrentList = newData.toMutableList() + differ.submitList(newData, onDone) + } + } + + fun moveItems(from: Int, to: Int) { + mCurrentList.add(to, mCurrentList.removeAt(from)) + differ.rewriteListUnsafe(mCurrentList) + adapter.notifyItemMoved(from, to) + } + + fun removeItem(at: Int) { + mCurrentList.removeAt(at) + differ.rewriteListUnsafe(mCurrentList) + adapter.notifyItemRemoved(at) + } + + /** + * Rewrites the AsyncListDiffer's internal list, cancelling any diffs that are currently in + * progress. I cannot describe in words how dangerous this is, but it's also the only thing I + * can do to marry the adapter primitives with DiffUtil. + */ + private fun AsyncListDiffer.rewriteListUnsafe(newList: List) { + differMaxGenerationsField.set(this, (differMaxGenerationsField.get(this) as Int).inc()) + differListField.set(this, newList.toMutableList()) + differImmutableListField.set(this, newList) + } + + companion object { + private val differListField = + AsyncListDiffer::class.java.getDeclaredField("mList").apply { isAccessible = true } + + private val differImmutableListField = + AsyncListDiffer::class.java.getDeclaredField("mReadOnlyList").apply { + isAccessible = true + } + + private val differMaxGenerationsField = + AsyncListDiffer::class.java.getDeclaredField("mMaxScheduledGeneration").apply { + isAccessible = true + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt b/app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt index d1e06f067..69dd20b20 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt @@ -178,15 +178,6 @@ class PrimitiveBackingData(private val adapter: RecyclerView.Adapter<*>) : Ba mCurrentList = newList.toMutableList() adapter.notifyDataSetChanged() } - - /** - * Move an item from [from] to [to]. This calls [RecyclerView.Adapter.notifyItemMoved] - * internally. - */ - fun moveItems(from: Int, to: Int) { - mCurrentList.add(to, mCurrentList.removeAt(from)) - adapter.notifyItemMoved(from, to) - } } /** @@ -215,80 +206,6 @@ class AsyncBackingData( } } -/** - * A list-backed [BackingData] that can be modified with both adapter primitives and - * [AsyncListDiffer]. Never use this class unless absolutely necessary, such as when dealing with - * item dragging. This is mostly because the class is a terrible hacky mess that could easily crash - * the app if you are not careful with it. You have been warned. - */ -class HybridBackingData( - private val adapter: RecyclerView.Adapter<*>, - diffCallback: DiffUtil.ItemCallback -) : BackingData() { - private var mCurrentList = mutableListOf() - val currentList: List - get() = mCurrentList - - private val differ = AsyncListDiffer(adapter, diffCallback) - - override fun getItem(position: Int): T = mCurrentList[position] - override fun getItemCount(): Int = mCurrentList.size - - fun submitList(newData: List, onDone: () -> Unit = {}) { - if (newData != mCurrentList) { - mCurrentList = newData.toMutableList() - differ.submitList(newData, onDone) - } - } - - // @Suppress("NotifyDatasetChanged") - // fun submitListHard(newList: List) { - // if (newList != mCurrentList) { - // mCurrentList = newList.toMutableList() - // differ.rewriteListUnsafe(mCurrentList) - // adapter.notifyDataSetChanged() - // } - // } - - fun moveItems(from: Int, to: Int) { - mCurrentList.add(to, mCurrentList.removeAt(from)) - differ.rewriteListUnsafe(mCurrentList) - adapter.notifyItemMoved(from, to) - } - - fun removeItem(at: Int) { - mCurrentList.removeAt(at) - differ.rewriteListUnsafe(mCurrentList) - adapter.notifyItemRemoved(at) - } - - /** - * Rewrites the AsyncListDiffer's internal list, cancelling any diffs that are currently in - * progress. I cannot describe in words how dangerous this is, but it's also the only thing I - * can do to marry the adapter primitives with DiffUtil. - */ - private fun AsyncListDiffer.rewriteListUnsafe(newList: List) { - differMaxGenerationsField.set(this, (differMaxGenerationsField.get(this) as Int).inc()) - differListField.set(this, newList.toMutableList()) - differImmutableListField.set(this, newList) - } - - companion object { - private val differListField = - AsyncListDiffer::class.java.getDeclaredField("mList").apply { isAccessible = true } - - private val differImmutableListField = - AsyncListDiffer::class.java.getDeclaredField("mReadOnlyList").apply { - isAccessible = true - } - - private val differMaxGenerationsField = - AsyncListDiffer::class.java.getDeclaredField("mMaxScheduledGeneration").apply { - isAccessible = true - } - } -} - /** * A base [DiffUtil.ItemCallback] that automatically provides an implementation of * [areContentsTheSame] any object that is derived from [Item]. diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index 5d95d1569..069445e62 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -122,7 +122,7 @@ class WidgetProvider : AppWidgetProvider() { .size(min(metrics.widthPixels, metrics.heightPixels)) } else { // Note: Explicitly use the "original" size as without it the scaling logic - // in SquareFrameTransform breaks down and results in an error. + // in coil breaks down and results in an error. coverRequest.transformations(SquareFrameTransform()).size(Size.ORIGINAL) } diff --git a/info/ARCHITECTURE.md b/info/ARCHITECTURE.md index 4ad420628..64383fda7 100644 --- a/info/ARCHITECTURE.md +++ b/info/ARCHITECTURE.md @@ -58,9 +58,10 @@ the binding being obtained by calling `requireBinding`. At times it may be more appropriate to use a `View` instead of a full blown fragment. This is okay as long as view-binding is still used. -When creating a ViewHolder for a `RecyclerView`, one should use `BaseViewHolder` to standardize the binding process -and automate some code shared across all ViewHolders. The only exceptions to this case are for ViewHolders that -correspond to non-`BaseModel` data, in which a normal ViewHolder can be used instead. +Auxio uses `RecyclerView` for all list information. Due to the complexities of Auxio, the way one defines an +adapter differs quite heavily from the normal library. Generally, start with `MonoAdapter` for a list with one +type of data and `MultiAdapter` for lists with many types of data, then follow the documentation to see how +to fully implement the class. #### Object communication Auxio's codebase is mostly centered around 4 different types of code that communicates with each-other.