FINALLY make selected state fully managed by the view model

This commit is contained in:
Luna712 2024-07-16 17:24:17 -06:00 committed by GitHub
parent b601704764
commit e41ca6318a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 61 additions and 28 deletions

View file

@ -35,20 +35,23 @@ sealed class VisualDownloadCached {
abstract val currentBytes: Long abstract val currentBytes: Long
abstract val totalBytes: Long abstract val totalBytes: Long
abstract val data: VideoDownloadHelper.DownloadCached abstract val data: VideoDownloadHelper.DownloadCached
abstract var isSelected: Boolean
data class Child( data class Child(
override val currentBytes: Long, override val currentBytes: Long,
override val totalBytes: Long, override val totalBytes: Long,
override val data: VideoDownloadHelper.DownloadEpisodeCached, override val data: VideoDownloadHelper.DownloadEpisodeCached,
override var isSelected: Boolean,
) : VisualDownloadCached() ) : VisualDownloadCached()
data class Header( data class Header(
override val currentBytes: Long, override val currentBytes: Long,
override val totalBytes: Long, override val totalBytes: Long,
override val data: VideoDownloadHelper.DownloadHeaderCached, override val data: VideoDownloadHelper.DownloadHeaderCached,
override var isSelected: Boolean,
val child: VideoDownloadHelper.DownloadEpisodeCached?, val child: VideoDownloadHelper.DownloadEpisodeCached?,
val currentOngoingDownloads: Int, val currentOngoingDownloads: Int,
val totalDownloads: Int val totalDownloads: Int,
) : VisualDownloadCached() ) : VisualDownloadCached()
} }
@ -69,7 +72,6 @@ class DownloadAdapter(
) : ListAdapter<VisualDownloadCached, DownloadAdapter.DownloadViewHolder>(DiffCallback()) { ) : ListAdapter<VisualDownloadCached, DownloadAdapter.DownloadViewHolder>(DiffCallback()) {
private var isMultiDeleteState: Boolean = false private var isMultiDeleteState: Boolean = false
private val selectedIds: HashMap<Int, Boolean> = HashMap()
companion object { companion object {
private const val VIEW_TYPE_HEADER = 0 private const val VIEW_TYPE_HEADER = 0
@ -136,14 +138,13 @@ class DownloadAdapter(
if (isMultiDeleteState) { if (isMultiDeleteState) {
deleteCheckbox.setOnCheckedChangeListener { _, isChecked -> deleteCheckbox.setOnCheckedChangeListener { _, isChecked ->
selectedIds[data.id] = isChecked
onItemSelectionChanged.invoke(card, isChecked) onItemSelectionChanged.invoke(card, isChecked)
} }
} else deleteCheckbox.setOnCheckedChangeListener(null) } else deleteCheckbox.setOnCheckedChangeListener(null)
deleteCheckbox.apply { deleteCheckbox.apply {
isVisible = isMultiDeleteState isVisible = isMultiDeleteState
isChecked = selectedIds[data.id] == true isChecked = card.isSelected
} }
} }
} }
@ -310,14 +311,13 @@ class DownloadAdapter(
if (isMultiDeleteState) { if (isMultiDeleteState) {
deleteCheckbox.setOnCheckedChangeListener { _, isChecked -> deleteCheckbox.setOnCheckedChangeListener { _, isChecked ->
selectedIds[data.id] = isChecked
onItemSelectionChanged.invoke(card, isChecked) onItemSelectionChanged.invoke(card, isChecked)
} }
} else deleteCheckbox.setOnCheckedChangeListener(null) } else deleteCheckbox.setOnCheckedChangeListener(null)
deleteCheckbox.apply { deleteCheckbox.apply {
isVisible = isMultiDeleteState isVisible = isMultiDeleteState
isChecked = selectedIds[data.id] == true isChecked = card.isSelected
} }
} }
} }
@ -349,37 +349,30 @@ class DownloadAdapter(
if (isMultiDeleteState == value) return if (isMultiDeleteState == value) return
isMultiDeleteState = value isMultiDeleteState = value
if (!value) { if (!value) {
selectedIds.clear()
currentList.forEachIndexed { index, _ -> currentList.forEachIndexed { index, _ ->
notifyItemChanged(index) notifyItemChanged(index)
} }
} else notifyItemRangeChanged(0, itemCount) } else notifyItemRangeChanged(0, itemCount)
} }
fun selectAllItems() { fun notifyAllSelected() {
currentList.forEachIndexed { index, item -> currentList.forEachIndexed { index, item ->
val id = item.data.id if (item.isSelected) return@forEachIndexed
if (selectedIds[id] == true) return@forEachIndexed
selectedIds[id] = true
notifyItemChanged(index) notifyItemChanged(index)
} }
} }
fun clearSelectedItems() { fun notifySelectionStates() {
val selectedPositions = selectedIds.keys.mapNotNull { id -> currentList.indices.forEach { index ->
currentList.indexOfFirst { it.data.id == id }.takeIf { it != -1 } if (currentList[index].isSelected) {
} notifyItemChanged(index)
selectedIds.clear() }
selectedPositions.forEach {
notifyItemChanged(it)
} }
} }
private fun toggleIsChecked(checkbox: CheckBox, item: VisualDownloadCached) { private fun toggleIsChecked(checkbox: CheckBox, item: VisualDownloadCached) {
val isChecked = !checkbox.isChecked val isChecked = !checkbox.isChecked
checkbox.isChecked = isChecked checkbox.isChecked = isChecked
selectedIds[item.data.id] = isChecked
onItemSelectionChanged.invoke(item, isChecked) onItemSelectionChanged.invoke(item, isChecked)
} }

View file

@ -185,12 +185,12 @@ class DownloadChildFragment : Fragment() {
binding?.btnToggleAll?.setOnClickListener { binding?.btnToggleAll?.setOnClickListener {
val allSelected = downloadsViewModel.isAllSelected() val allSelected = downloadsViewModel.isAllSelected()
val binding = binding?.downloadChildList?.adapter as? DownloadAdapter val adapter = binding?.downloadChildList?.adapter as? DownloadAdapter
if (allSelected) { if (allSelected) {
binding?.clearSelectedItems() adapter?.notifySelectionStates()
downloadsViewModel.clearSelectedItems() downloadsViewModel.clearSelectedItems()
} else { } else {
binding?.selectAllItems() adapter?.notifyAllSelected()
downloadsViewModel.selectAllItems() downloadsViewModel.selectAllItems()
} }
} }

View file

@ -133,7 +133,13 @@ class DownloadFragment : Fragment() {
binding?.downloadUsedTxt, binding?.downloadUsedTxt,
binding?.downloadUsed binding?.downloadUsed
) )
binding?.downloadStorageAppbar?.isVisible = it > 0
// Prevent race condition and make sure
// we don't display it early
if (
downloadsViewModel.isMultiDeleteState.value == null ||
downloadsViewModel.isMultiDeleteState.value == false
) binding?.downloadStorageAppbar?.isVisible = it > 0
} }
observe(downloadsViewModel.downloadBytes) { observe(downloadsViewModel.downloadBytes) {
updateStorageInfo( updateStorageInfo(
@ -154,7 +160,8 @@ class DownloadFragment : Fragment() {
if (!isMultiDeleteState) { if (!isMultiDeleteState) {
detachBackPressedCallback() detachBackPressedCallback()
downloadsViewModel.clearSelectedItems() downloadsViewModel.clearSelectedItems()
// Make sure we don't display it early // Prevent race condition and make sure
// we don't display it early
if (downloadsViewModel.usedBytes.value?.let { it > 0 } == true) { if (downloadsViewModel.usedBytes.value?.let { it > 0 } == true) {
binding?.downloadStorageAppbar?.isVisible = true binding?.downloadStorageAppbar?.isVisible = true
} }
@ -260,12 +267,12 @@ class DownloadFragment : Fragment() {
binding?.btnToggleAll?.setOnClickListener { binding?.btnToggleAll?.setOnClickListener {
val allSelected = downloadsViewModel.isAllSelected() val allSelected = downloadsViewModel.isAllSelected()
val binding = binding?.downloadList?.adapter as? DownloadAdapter val adapter = binding?.downloadList?.adapter as? DownloadAdapter
if (allSelected) { if (allSelected) {
binding?.clearSelectedItems() adapter?.notifySelectionStates()
downloadsViewModel.clearSelectedItems() downloadsViewModel.clearSelectedItems()
} else { } else {
binding?.selectAllItems() adapter?.notifyAllSelected()
downloadsViewModel.selectAllItems() downloadsViewModel.selectAllItems()
} }
} }

View file

@ -67,6 +67,7 @@ class DownloadViewModel : ViewModel() {
currentSelected.add(item) currentSelected.add(item)
_selectedItems.postValue(currentSelected) _selectedItems.postValue(currentSelected)
updateSelectedBytes() updateSelectedBytes()
updateSelectedCards()
} }
} }
@ -75,6 +76,7 @@ class DownloadViewModel : ViewModel() {
selected.remove(item) selected.remove(item)
_selectedItems.postValue(selected) _selectedItems.postValue(selected)
updateSelectedBytes() updateSelectedBytes()
updateSelectedCards()
} }
} }
@ -89,12 +91,14 @@ class DownloadViewModel : ViewModel() {
} }
_selectedItems.postValue(currentSelected) _selectedItems.postValue(currentSelected)
updateSelectedBytes() updateSelectedBytes()
updateSelectedCards()
} }
fun clearSelectedItems() { fun clearSelectedItems() {
// We need this to be done immediately // We need this to be done immediately
// so we can't use postValue // so we can't use postValue
_selectedItems.value = mutableListOf() _selectedItems.value = mutableListOf()
updateSelectedCards()
} }
fun isAllSelected(): Boolean { fun isAllSelected(): Boolean {
@ -127,6 +131,27 @@ class DownloadViewModel : ViewModel() {
_selectedBytes.postValue(totalSelectedBytes) _selectedBytes.postValue(totalSelectedBytes)
} }
private fun updateSelectedCards() = viewModelScope.launchSafe {
val currentSelected = selectedItems.value ?: return@launchSafe
val updatedHeaderCards = headerCards.value?.toMutableList()
val updatedChildCards = childCards.value?.toMutableList()
updatedHeaderCards?.forEach { header ->
header.isSelected = currentSelected.any {
it.data.id == header.data.id
}
}
updatedChildCards?.forEach { child ->
child.isSelected = currentSelected.any {
it.data.id == child.data.id
}
}
_headerCards.postValue(updatedHeaderCards)
_childCards.postValue(updatedChildCards)
}
fun updateList(context: Context) = viewModelScope.launchSafe { fun updateList(context: Context) = viewModelScope.launchSafe {
val children = withContext(Dispatchers.IO) { val children = withContext(Dispatchers.IO) {
context.getKeys(DOWNLOAD_EPISODE_CACHE) context.getKeys(DOWNLOAD_EPISODE_CACHE)
@ -174,6 +199,9 @@ class DownloadViewModel : ViewModel() {
val bytes = totalBytesUsedByChild[it.id] ?: 0 val bytes = totalBytesUsedByChild[it.id] ?: 0
val currentBytes = currentBytesUsedByChild[it.id] ?: 0 val currentBytes = currentBytesUsedByChild[it.id] ?: 0
if (bytes <= 0 || downloads <= 0) return@mapNotNull null if (bytes <= 0 || downloads <= 0) return@mapNotNull null
val isSelected = selectedItems.value?.any { header ->
it.id == header.data.id
} ?: false
val movieEpisode = val movieEpisode =
if (!it.type.isMovieType()) null if (!it.type.isMovieType()) null
else context.getKey<VideoDownloadHelper.DownloadEpisodeCached>( else context.getKey<VideoDownloadHelper.DownloadEpisodeCached>(
@ -187,6 +215,7 @@ class DownloadViewModel : ViewModel() {
child = movieEpisode, child = movieEpisode,
currentOngoingDownloads = 0, currentOngoingDownloads = 0,
totalDownloads = downloads, totalDownloads = downloads,
isSelected = isSelected,
) )
}.sortedBy { }.sortedBy {
(it.child?.episode ?: 0) + (it.child?.season?.times(10000) ?: 0) (it.child?.episode ?: 0) + (it.child?.season?.times(10000) ?: 0)
@ -212,9 +241,13 @@ class DownloadViewModel : ViewModel() {
}.mapNotNull { }.mapNotNull {
val info = getDownloadFileInfoAndUpdateSettings(context, it.id) val info = getDownloadFileInfoAndUpdateSettings(context, it.id)
?: return@mapNotNull null ?: return@mapNotNull null
val isSelected = selectedItems.value?.any { child ->
it.id == child.data.id
} ?: false
VisualDownloadCached.Child( VisualDownloadCached.Child(
currentBytes = info.fileLength, currentBytes = info.fileLength,
totalBytes = info.totalBytes, totalBytes = info.totalBytes,
isSelected = isSelected,
data = it, data = it,
) )
} }