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.
This commit is contained in:
parent
2406c371db
commit
05a5ef5c3f
22 changed files with 409 additions and 363 deletions
|
@ -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()
|
||||
|
|
|
@ -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<AccentAdapter.ViewHolder>() {
|
||||
private var selectedViewHolder: ViewHolder? = null
|
||||
/** An adapter that displays the accent palette. */
|
||||
class AccentAdapter(listener: Listener) :
|
||||
MonoAdapter<Accent, AccentAdapter.Listener, NewAccentViewHolder>(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<Accent>() {
|
||||
override fun getItem(position: Int) = Accent(position)
|
||||
override fun getItemCount() = ACCENT_COUNT
|
||||
}
|
||||
}
|
||||
|
||||
class NewAccentViewHolder private constructor(private val binding: ItemAccentBinding) :
|
||||
BindingViewHolder<Accent, AccentAdapter.Listener>(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<NewAccentViewHolder> {
|
||||
override val viewType: Int
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override fun create(context: Context) =
|
||||
NewAccentViewHolder(ItemAccentBinding.inflate(context.inflater))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<DialogAccentBinding>() {
|
||||
class AccentCustomizeDialog :
|
||||
ViewBindingDialogFragment<DialogAccentBinding>(), 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<DialogAccentBinding>() {
|
|||
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<DialogAccentBinding>() {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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<AlbumDetailItemListener>(listener, DIFFER) {
|
||||
class AlbumDetailAdapter(listener: Listener) :
|
||||
DetailAdapter<AlbumDetailAdapter.Listener>(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<Album, AlbumDetailItemListener>(binding.root) {
|
||||
BindingViewHolder<Album, AlbumDetailAdapter.Listener>(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
|
||||
|
||||
|
|
|
@ -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<DetailItemListener>(listener, DIFFER) {
|
||||
class ArtistDetailAdapter(listener: Listener) :
|
||||
DetailAdapter<DetailAdapter.Listener>(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<Artist, DetailItemListener>(binding.root) {
|
||||
BindingViewHolder<Artist, DetailAdapter.Listener>(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
|
||||
|
||||
|
|
|
@ -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<L : DetailItemListener>(
|
||||
abstract class DetailAdapter<L : DetailAdapter.Listener>(
|
||||
listener: L,
|
||||
diffCallback: DiffUtil.ItemCallback<Item>
|
||||
) : MultiAdapter<L>(listener) {
|
||||
|
@ -112,13 +112,19 @@ abstract class DetailAdapter<L : DetailItemListener>(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<SortHeader, DetailItemListener>(binding.root) {
|
||||
override fun bind(item: SortHeader, listener: DetailItemListener) {
|
||||
BindingViewHolder<SortHeader, DetailAdapter.Listener>(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)
|
||||
}
|
||||
|
|
|
@ -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<DetailItemListener>(listener, DIFFER) {
|
||||
class GenreDetailAdapter(listener: Listener) :
|
||||
DetailAdapter<DetailAdapter.Listener>(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<Genre, DetailItemListener>(binding.root) {
|
||||
override fun bind(item: Genre, listener: DetailItemListener) {
|
||||
BindingViewHolder<Genre, DetailAdapter.Listener>(binding.root) {
|
||||
override fun bind(item: Genre, listener: DetailAdapter.Listener) {
|
||||
binding.detailCover.bindGenreImage(item)
|
||||
binding.detailName.textSafe = item.resolvedName
|
||||
binding.detailSubhead.textSafe =
|
||||
|
|
|
@ -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<Tab>): 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 +=
|
||||
|
|
|
@ -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<Tab>,
|
||||
private val onTabSwitch: (Tab) -> Unit,
|
||||
) : RecyclerView.Adapter<TabAdapter.TabViewHolder>() {
|
||||
private val tabs: Array<Tab>
|
||||
get() = getTabs()
|
||||
class TabAdapter(listener: Listener) :
|
||||
MonoAdapter<Tab, TabAdapter.Listener, TabViewHolder>(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<Tab>() {
|
||||
var tabs = arrayOf<Tab>()
|
||||
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<Tab>) {
|
||||
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<Tab, TabAdapter.Listener>(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<TabViewHolder> {
|
||||
override val viewType: Int
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override fun create(context: Context) =
|
||||
TabViewHolder(ItemTabBinding.inflate(context.inflater))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<DialogTabsBinding>() {
|
||||
class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), 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<DialogTabsBinding>() {
|
|||
|
||||
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<DialogTabsBinding>() {
|
|||
}
|
||||
|
||||
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<Tab.Visible>().isNotEmpty()
|
||||
tabAdapter.data.tabs.filterIsInstance<Tab.Visible>().isNotEmpty()
|
||||
}
|
||||
|
||||
override fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) {
|
||||
requireTouchHelper().startDrag(viewHolder)
|
||||
}
|
||||
|
||||
private fun findSavedTabState(savedInstanceState: Bundle?): Array<Tab>? {
|
||||
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 {
|
||||
|
|
|
@ -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<Tab>) : ItemTouchHelper.Callback() {
|
||||
private val tabs: Array<Tab>
|
||||
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<Tab>) : 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<Tab>) : 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 <T : Any> Array<T>.swap(from: Int, to: Int) {
|
||||
val t = get(to)
|
||||
val f = get(from)
|
||||
|
||||
set(from, t)
|
||||
set(to, f)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,12 +40,14 @@ import org.oxycblt.auxio.util.showToast
|
|||
* Dialog that manages the currently excluded directories.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class ExcludedDialog : ViewBindingDialogFragment<DialogExcludedBinding>() {
|
||||
class ExcludedDialog :
|
||||
ViewBindingDialogFragment<DialogExcludedBinding>(), 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<DialogExcludedBinding>() {
|
|||
}
|
||||
|
||||
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<DialogExcludedBinding>() {
|
|||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
excludedModel.paths.observe(viewLifecycleOwner) { paths -> updatePaths(paths, adapter) }
|
||||
excludedModel.paths.observe(viewLifecycleOwner, ::updatePaths)
|
||||
|
||||
logD("Dialog created")
|
||||
}
|
||||
|
||||
private fun updatePaths(paths: MutableList<String>, 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<String>) {
|
||||
excludedAdapter.data.submitList(paths)
|
||||
requireBinding().excludedEmpty.isVisible = paths.isEmpty()
|
||||
}
|
||||
|
||||
|
|
|
@ -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<ExcludedEntryAdapter.ViewHolder>() {
|
||||
private var paths = mutableListOf<String>()
|
||||
class ExcludedAdapter(listener: Listener) :
|
||||
MonoAdapter<String, ExcludedAdapter.Listener, ExcludedViewHolder>(listener) {
|
||||
override val data = PrimitiveBackingData<String>(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<String>) {
|
||||
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<String, ExcludedAdapter.Listener>(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<ExcludedViewHolder> {
|
||||
override val viewType: Int
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override fun create(context: Context) =
|
||||
ExcludedViewHolder(ItemExcludedDirBinding.inflate(context.inflater))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,11 +40,8 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
|
|||
val paths: LiveData<MutableList<String>>
|
||||
get() = mPaths
|
||||
|
||||
private var dbPaths = listOf<String>()
|
||||
|
||||
/** 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")
|
||||
}
|
||||
|
|
|
@ -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<T>(
|
||||
private val adapter: RecyclerView.Adapter<*>,
|
||||
diffCallback: DiffUtil.ItemCallback<T>
|
||||
) : BackingData<T>() {
|
||||
private var mCurrentList = mutableListOf<T>()
|
||||
val currentList: List<T>
|
||||
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<T>, 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 <T> AsyncListDiffer<T>.rewriteListUnsafe(newList: List<T>) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -178,15 +178,6 @@ class PrimitiveBackingData<T>(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<T>(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<T>(
|
||||
private val adapter: RecyclerView.Adapter<*>,
|
||||
diffCallback: DiffUtil.ItemCallback<T>
|
||||
) : BackingData<T>() {
|
||||
private var mCurrentList = mutableListOf<T>()
|
||||
val currentList: List<T>
|
||||
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<T>, onDone: () -> Unit = {}) {
|
||||
if (newData != mCurrentList) {
|
||||
mCurrentList = newData.toMutableList()
|
||||
differ.submitList(newData, onDone)
|
||||
}
|
||||
}
|
||||
|
||||
// @Suppress("NotifyDatasetChanged")
|
||||
// fun submitListHard(newList: List<T>) {
|
||||
// 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 <T> AsyncListDiffer<T>.rewriteListUnsafe(newList: List<T>) {
|
||||
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].
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue