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.