mirror of
https://github.com/recloudstream/cloudstream.git
synced 2026-06-18 19:56:50 +00:00
Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eef078d07c |
5 changed files with 105 additions and 155 deletions
|
|
@ -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
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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 ->
|
||||
val adapter = binding.downloadChildList.adapter as? DownloadAdapter
|
||||
adapter?.setIsMultiDeleteState(isMultiDeleteState)
|
||||
binding.downloadDeleteAppbar.isVisible = isMultiDeleteState
|
||||
if (!isMultiDeleteState) {
|
||||
activity?.detachBackPressedCallback("Downloads")
|
||||
downloadViewModel.clearSelectedItems()
|
||||
binding.downloadChildToolbar.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
observe(downloadViewModel.selectedBytes) {
|
||||
updateDeleteButton(downloadViewModel.selectedItemIds.value?.count() ?: 0, it)
|
||||
}
|
||||
observe(downloadViewModel.selectedItemIds) {
|
||||
handleSelectedChange(it)
|
||||
updateDeleteButton(it.count(), downloadViewModel.selectedBytes.value ?: 0L)
|
||||
|
||||
binding.btnDelete.isVisible = it.isNotEmpty()
|
||||
binding.selectItemsText.isVisible = it.isEmpty()
|
||||
|
||||
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
|
||||
binding.downloadChildToolbar.isGone = isMultiDeleteState
|
||||
|
||||
if (selection == null) {
|
||||
activity?.detachBackPressedCallback("Downloads")
|
||||
return@observeNullable
|
||||
}
|
||||
activity?.attachBackPressedCallback("Downloads") {
|
||||
downloadViewModel.cancelSelection()
|
||||
}
|
||||
|
||||
updateDeleteButton(selection.count(), downloadViewModel.selectedBytes.value ?: 0L)
|
||||
|
||||
binding.btnDelete.isVisible = selection.isNotEmpty()
|
||||
binding.selectItemsText.isVisible = selection.isEmpty()
|
||||
|
||||
val allSelected = downloadViewModel.isAllChildrenSelected()
|
||||
if (allSelected) {
|
||||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 -> {
|
||||
|
|
@ -161,26 +145,44 @@ class DownloadFragment : BaseFragment<FragmentDownloadsBinding>(
|
|||
observe(downloadViewModel.selectedBytes) {
|
||||
updateDeleteButton(downloadViewModel.selectedItemIds.value?.count() ?: 0, it)
|
||||
}
|
||||
observe(downloadViewModel.isMultiDeleteState) { isMultiDeleteState ->
|
||||
val adapter = binding.downloadList.adapter as? DownloadAdapter
|
||||
adapter?.setIsMultiDeleteState(isMultiDeleteState)
|
||||
binding.downloadDeleteAppbar.isVisible = isMultiDeleteState
|
||||
if (!isMultiDeleteState) {
|
||||
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
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
observe(downloadViewModel.selectedItemIds) {
|
||||
handleSelectedChange(it)
|
||||
updateDeleteButton(it.count(), downloadViewModel.selectedBytes.value ?: 0L)
|
||||
|
||||
binding.btnDelete.isVisible = it.isNotEmpty()
|
||||
binding.selectItemsText.isVisible = it.isEmpty()
|
||||
observeNullable(downloadViewModel.selectedItemIds) { selection ->
|
||||
val isMultiDeleteState = selection != null
|
||||
val adapter = binding.downloadList.adapter as? DownloadAdapter
|
||||
adapter?.setIsMultiDeleteState(isMultiDeleteState)
|
||||
binding.downloadDeleteAppbar.isVisible = isMultiDeleteState
|
||||
binding.downloadAppbar.isGone = isMultiDeleteState
|
||||
|
||||
if (selection == null) {
|
||||
activity?.detachBackPressedCallback("Downloads")
|
||||
return@observeNullable
|
||||
}
|
||||
activity?.attachBackPressedCallback("Downloads") {
|
||||
downloadViewModel.cancelSelection()
|
||||
}
|
||||
updateDeleteButton(selection.count(), downloadViewModel.selectedBytes.value ?: 0L)
|
||||
|
||||
binding.btnDelete.isVisible = selection.isNotEmpty()
|
||||
binding.selectItemsText.isVisible = selection.isEmpty()
|
||||
|
||||
val allSelected = downloadViewModel.isAllHeadersSelected()
|
||||
if (allSelected) {
|
||||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
@ -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() {
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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 ->
|
||||
|
|
@ -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 })
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ package com.lagradost.cloudstream3.utils
|
|||
|
||||
import android.content.Context
|
||||
import com.lagradost.api.Log
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFolder
|
||||
import com.lagradost.safefile.SafeFile
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.basePathToFile
|
||||
|
||||
object SubtitleUtils {
|
||||
|
||||
|
|
@ -14,16 +13,20 @@ object SubtitleUtils {
|
|||
)
|
||||
|
||||
fun deleteMatchingSubtitles(context: Context, info: VideoDownloadManager.DownloadedFileInfo) {
|
||||
val relative = info.relativePath
|
||||
val display = info.displayName
|
||||
val cleanDisplay = cleanDisplayName(display)
|
||||
val cleanDisplay = cleanDisplayName(info.displayName)
|
||||
|
||||
getFolder(context, relative, info.basePath)?.forEach { (name, uri) ->
|
||||
if (isMatchingSubtitle(name, display, cleanDisplay)) {
|
||||
val subtitleFile = SafeFile.fromUri(context, uri)
|
||||
if (subtitleFile == null || subtitleFile.delete() != true) {
|
||||
Log.e("SubtitleDeletion", "Failed to delete subtitle file: ${subtitleFile?.name()}")
|
||||
}
|
||||
val base = basePathToFile(context, info.basePath)
|
||||
val folder =
|
||||
base?.gotoDirectory(info.relativePath, createMissingDirectories = false) ?: return
|
||||
val folderFiles = folder.listFiles() ?: return
|
||||
|
||||
for (file in folderFiles) {
|
||||
val name = file.name() ?: continue
|
||||
if (!isMatchingSubtitle(name, info.displayName, cleanDisplay)) {
|
||||
continue
|
||||
}
|
||||
if (file.delete() != true) {
|
||||
Log.e("SubtitleDeletion", "Failed to delete subtitle file: $name")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1628,7 +1628,7 @@ object VideoDownloadManager {
|
|||
* Turns a string to an UniFile. Used for stored string paths such as settings.
|
||||
* Should only be used to get a download path.
|
||||
* */
|
||||
private fun basePathToFile(context: Context, path: String?): SafeFile? {
|
||||
fun basePathToFile(context: Context, path: String?): SafeFile? {
|
||||
return when {
|
||||
path.isNullOrBlank() -> getDefaultDir(context)
|
||||
path.startsWith("content://") -> SafeFile.fromUri(context, path.toUri())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue