Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package com.lagradost.cloudstream3.ui.download

import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.format.Formatter.formatShortFileSize
import android.view.View
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.mvvm.observeNullable
import com.lagradost.cloudstream3.ui.BaseFragment
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
Expand Down Expand Up @@ -55,22 +55,6 @@ class DownloadChildFragment : BaseFragment<FragmentChildDownloadsBinding>(
}

override fun onBindingCreated(binding: FragmentChildDownloadsBinding) {
/**
* We never want to retain multi-delete state
* when navigating to downloads. Setting this state
* immediately can sometimes result in the observer
* not being notified in time to update the UI.
*
* By posting to the main looper, we ensure that this
* operation is executed after the view has been fully created
* and all initializations are completed, allowing the
* observer to properly receive and handle the state change.
*/
Handler(Looper.getMainLooper()).post {
downloadViewModel.setIsMultiDeleteState(false)
}


val folder = arguments?.getString("folder")
val name = arguments?.getString("name")
if (folder == null) {
Expand Down Expand Up @@ -101,30 +85,56 @@ class DownloadChildFragment : BaseFragment<FragmentChildDownloadsBinding>(
}
(binding.downloadChildList.adapter as? DownloadAdapter)?.submitList(cards.value)
}

else -> {
(binding.downloadChildList.adapter as? DownloadAdapter)?.submitList(null)
}
}
}
observe(downloadViewModel.isMultiDeleteState) { isMultiDeleteState ->

observe(downloadViewModel.selectedBytes) {
updateDeleteButton(downloadViewModel.selectedItemIds.value?.count() ?: 0, it)
}


binding.apply {
btnDelete.setOnClickListener { view ->
downloadViewModel.handleMultiDelete(view.context ?: return@setOnClickListener)
}

btnCancel.setOnClickListener {
downloadViewModel.cancelSelection()
}

btnToggleAll.setOnClickListener {
val allSelected = downloadViewModel.isAllChildrenSelected()
if (allSelected) {
downloadViewModel.clearSelectedItems()
} else {
downloadViewModel.selectAllChildren()
}
}
}

observeNullable(downloadViewModel.selectedItemIds) { selection ->
val isMultiDeleteState = selection != null
val adapter = binding.downloadChildList.adapter as? DownloadAdapter
adapter?.setIsMultiDeleteState(isMultiDeleteState)
binding.downloadDeleteAppbar.isVisible = isMultiDeleteState
if (!isMultiDeleteState) {
binding.downloadChildToolbar.isGone = isMultiDeleteState

if (selection == null) {
activity?.detachBackPressedCallback("Downloads")
downloadViewModel.clearSelectedItems()
binding.downloadChildToolbar.isVisible = true
return@observeNullable
}
}
observe(downloadViewModel.selectedBytes) {
updateDeleteButton(downloadViewModel.selectedItemIds.value?.count() ?: 0, it)
}
observe(downloadViewModel.selectedItemIds) {
handleSelectedChange(it)
updateDeleteButton(it.count(), downloadViewModel.selectedBytes.value ?: 0L)
activity?.attachBackPressedCallback("Downloads") {
downloadViewModel.cancelSelection()
}

updateDeleteButton(selection.count(), downloadViewModel.selectedBytes.value ?: 0L)

binding.btnDelete.isVisible = it.isNotEmpty()
binding.selectItemsText.isVisible = it.isEmpty()
binding.btnDelete.isVisible = selection.isNotEmpty()
binding.selectItemsText.isVisible = selection.isEmpty()

val allSelected = downloadViewModel.isAllChildrenSelected()
if (allSelected) {
Expand Down Expand Up @@ -160,37 +170,6 @@ class DownloadChildFragment : BaseFragment<FragmentChildDownloadsBinding>(
}
}

private fun handleSelectedChange(selected: Set<Int>) {
if (selected.isNotEmpty()) {
binding?.downloadDeleteAppbar?.isVisible = true
binding?.downloadChildToolbar?.isVisible = false
activity?.attachBackPressedCallback("Downloads") {
downloadViewModel.setIsMultiDeleteState(false)
}

binding?.btnDelete?.setOnClickListener {
context?.let { ctx ->
downloadViewModel.handleMultiDelete(ctx)
}
}

binding?.btnCancel?.setOnClickListener {
downloadViewModel.setIsMultiDeleteState(false)
}

binding?.btnToggleAll?.setOnClickListener {
val allSelected = downloadViewModel.isAllChildrenSelected()
if (allSelected) {
downloadViewModel.clearSelectedItems()
} else {
downloadViewModel.selectAllChildren()
}
}

downloadViewModel.setIsMultiDeleteState(true)
}
}

private fun updateDeleteButton(count: Int, selectedBytes: Long) {
val formattedSize = formatShortFileSize(context, selectedBytes)
binding?.btnDelete?.text =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.text.format.Formatter.formatShortFileSize
import android.view.View
import android.widget.LinearLayout
Expand All @@ -28,6 +26,7 @@ import com.lagradost.cloudstream3.isEpisodeBased
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.safe
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.mvvm.observeNullable
import com.lagradost.cloudstream3.ui.BaseFragment
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.player.BasicLink
Expand Down Expand Up @@ -87,21 +86,6 @@ class DownloadFragment : BaseFragment<FragmentDownloadsBinding>(
binding.downloadAppbar.setAppBarNoScrollFlagsOnTV()
binding.downloadDeleteAppbar.setAppBarNoScrollFlagsOnTV()

/**
* We never want to retain multi-delete state
* when navigating to downloads. Setting this state
* immediately can sometimes result in the observer
* not being notified in time to update the UI.
*
* By posting to the main looper, we ensure that this
* operation is executed after the view has been fully created
* and all initializations are completed, allowing the
* observer to properly receive and handle the state change.
*/
Handler(Looper.getMainLooper()).post {
downloadViewModel.setIsMultiDeleteState(false)
}

observe(downloadViewModel.headerCards) { cards ->
when (cards) {
is Resource.Success -> {
Expand Down Expand Up @@ -161,26 +145,44 @@ class DownloadFragment : BaseFragment<FragmentDownloadsBinding>(
observe(downloadViewModel.selectedBytes) {
updateDeleteButton(downloadViewModel.selectedItemIds.value?.count() ?: 0, it)
}
observe(downloadViewModel.isMultiDeleteState) { isMultiDeleteState ->

binding.apply {
btnDelete.setOnClickListener { view ->
downloadViewModel.handleMultiDelete(view.context ?: return@setOnClickListener)
}

btnCancel.setOnClickListener {
downloadViewModel.cancelSelection()
}

btnToggleAll.setOnClickListener {
val allSelected = downloadViewModel.isAllHeadersSelected()
if (allSelected) {
downloadViewModel.clearSelectedItems()
} else {
downloadViewModel.selectAllHeaders()
}
}
}

observeNullable(downloadViewModel.selectedItemIds) { selection ->
val isMultiDeleteState = selection != null
val adapter = binding.downloadList.adapter as? DownloadAdapter
adapter?.setIsMultiDeleteState(isMultiDeleteState)
binding.downloadDeleteAppbar.isVisible = isMultiDeleteState
if (!isMultiDeleteState) {
binding.downloadAppbar.isGone = isMultiDeleteState

if (selection == null) {
activity?.detachBackPressedCallback("Downloads")
downloadViewModel.clearSelectedItems()
// Prevent race condition and make sure
// we don't display it early
if (downloadViewModel.usedBytes.value?.let { it > 0 } == true) {
binding.downloadAppbar.isVisible = true
}
return@observeNullable
}
}
observe(downloadViewModel.selectedItemIds) {
handleSelectedChange(it)
updateDeleteButton(it.count(), downloadViewModel.selectedBytes.value ?: 0L)
activity?.attachBackPressedCallback("Downloads") {
downloadViewModel.cancelSelection()
}
updateDeleteButton(selection.count(), downloadViewModel.selectedBytes.value ?: 0L)

binding.btnDelete.isVisible = it.isNotEmpty()
binding.selectItemsText.isVisible = it.isEmpty()
binding.btnDelete.isVisible = selection.isNotEmpty()
binding.selectItemsText.isVisible = selection.isEmpty()

val allSelected = downloadViewModel.isAllHeadersSelected()
if (allSelected) {
Expand Down Expand Up @@ -260,37 +262,6 @@ class DownloadFragment : BaseFragment<FragmentDownloadsBinding>(
}
}

private fun handleSelectedChange(selected: Set<Int>) {
if (selected.isNotEmpty()) {
binding?.downloadDeleteAppbar?.isVisible = true
binding?.downloadAppbar?.isVisible = false
activity?.attachBackPressedCallback("Downloads") {
downloadViewModel.setIsMultiDeleteState(false)
}

binding?.btnDelete?.setOnClickListener {
context?.let { ctx ->
downloadViewModel.handleMultiDelete(ctx)
}
}

binding?.btnCancel?.setOnClickListener {
downloadViewModel.setIsMultiDeleteState(false)
}

binding?.btnToggleAll?.setOnClickListener {
val allSelected = downloadViewModel.isAllHeadersSelected()
if (allSelected) {
downloadViewModel.clearSelectedItems()
} else {
downloadViewModel.selectAllHeaders()
}
}

downloadViewModel.setIsMultiDeleteState(true)
}
}

private fun updateDeleteButton(count: Int, selectedBytes: Long) {
val formattedSize = formatShortFileSize(context, selectedBytes)
binding?.btnDelete?.text =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class DownloadViewModel : ViewModel() {
private val _headerCards = ResourceLiveData<List<VisualDownloadCached.Header>>(Resource.Loading())
private val _headerCards =
ResourceLiveData<List<VisualDownloadCached.Header>>(Resource.Loading())
val headerCards: LiveData<Resource<List<VisualDownloadCached.Header>>> = _headerCards

private val _childCards = ResourceLiveData<List<VisualDownloadCached.Child>>(Resource.Loading())
Expand All @@ -47,22 +48,20 @@ class DownloadViewModel : ViewModel() {
private val _selectedBytes = ConsistentLiveData<Long>(0)
val selectedBytes: LiveData<Long> = _selectedBytes

private val _isMultiDeleteState = ConsistentLiveData(false)
val isMultiDeleteState: LiveData<Boolean> = _isMultiDeleteState
private val _selectedItemIds = ConsistentLiveData<Set<Int>?>(null)
val selectedItemIds: LiveData<Set<Int>?> = _selectedItemIds

private val _selectedItemIds = ConsistentLiveData<Set<Int>>(mutableSetOf())
val selectedItemIds: LiveData<Set<Int>> = _selectedItemIds

fun setIsMultiDeleteState(value: Boolean) {
_isMultiDeleteState.postValue(value)
fun cancelSelection() {
updateSelectedItems { null }
}

fun addSelected(itemId: Int) {
updateSelectedItems { it + itemId }
updateSelectedItems { it?.plus(itemId) ?: setOf(itemId) }
}

fun removeSelected(itemId: Int) {
updateSelectedItems { it - itemId }
updateSelectedItems { it?.minus(itemId) ?: emptySet() }
}

fun selectAllHeaders() {
Expand Down Expand Up @@ -97,8 +96,8 @@ class DownloadViewModel : ViewModel() {
return currentSelected.size == headers.size && headers.all { it.data.id in currentSelected }
}

private fun updateSelectedItems(action: (Set<Int>) -> Set<Int>) {
val currentSelected = action(selectedItemIds.value ?: mutableSetOf())
private fun updateSelectedItems(action: (Set<Int>?) -> Set<Int>?) {
val currentSelected = action(selectedItemIds.value)
_selectedItemIds.postValue(currentSelected)
postHeaders()
postChildren()
Expand All @@ -115,7 +114,6 @@ class DownloadViewModel : ViewModel() {
fun updateHeaderList(context: Context) = viewModelScope.launchSafe {
// Do not push loading as it interrupts the UI
//_headerCards.postValue(Resource.Loading())
clearSelectedItems()

val visual = withContext(Dispatchers.IO) {
val children = context.getKeys(DOWNLOAD_EPISODE_CACHE)
Expand Down Expand Up @@ -232,7 +230,6 @@ class DownloadViewModel : ViewModel() {

fun updateChildList(context: Context, folder: String) = viewModelScope.launchSafe {
_childCards.postValue(Resource.Loading()) // always push loading
clearSelectedItems()

val visual = withContext(Dispatchers.IO) {
context.getKeys(folder).mapNotNull { key ->
Expand Down Expand Up @@ -260,6 +257,7 @@ class DownloadViewModel : ViewModel() {
}

private fun removeItems(idsToRemove: Set<Int>) = viewModelScope.launchSafe {
_selectedItemIds.postValue(null)
postHeaders(_headerCards.success?.filter { it.data.id !in idsToRemove })
postChildren(_childCards.success?.filter { it.data.id !in idsToRemove })
}
Expand Down Expand Up @@ -368,16 +366,16 @@ class DownloadViewModel : ViewModel() {
.joinToString(separator = "\n") { "• $it" }

return when {
data.seriesNames.isNotEmpty() && data.names.isEmpty() -> {
context.getString(R.string.delete_message_series_only).format(formattedSeriesNames)
}

data.ids.count() == 1 -> {
context.getString(R.string.delete_message).format(
data.names.firstOrNull()
)
}

data.seriesNames.isNotEmpty() && data.names.isEmpty() -> {
context.getString(R.string.delete_message_series_only).format(formattedSeriesNames)
}

data.parentName != null && data.names.isNotEmpty() -> {
context.getString(R.string.delete_message_series_episodes)
.format(data.parentName, formattedNames)
Expand Down Expand Up @@ -406,7 +404,6 @@ class DownloadViewModel : ViewModel() {
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
viewModelScope.launchSafe {
setIsMultiDeleteState(false)
deleteFilesAndUpdateSettings(context, ids, this) { successfulIds ->
// We always remove parent because if we are deleting from here
// and we have it as non-empty, it was triggered on
Expand Down
Loading