diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index c9da385f63b..efea59624df 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -6,6 +6,7 @@ import android.content.Intent import android.content.res.ColorStateList import android.content.res.Configuration import android.graphics.Rect +import android.net.Uri import android.os.Build import android.os.Bundle import android.text.Editable @@ -25,6 +26,7 @@ import androidx.core.view.isVisible import androidx.core.widget.NestedScrollView import androidx.core.widget.doOnTextChanged import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope import com.discord.panels.OverlappingPanelsLayout import com.discord.panels.PanelState import com.discord.panels.PanelsChildGestureRegionObserver @@ -46,6 +48,7 @@ import com.lagradost.cloudstream3.databinding.FragmentResultSwipeBinding import com.lagradost.cloudstream3.databinding.ResultRecommendationsBinding import com.lagradost.cloudstream3.databinding.ResultSyncBinding import com.lagradost.cloudstream3.mvvm.Resource +import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable @@ -66,6 +69,8 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.setRecycledViewPool +import com.lagradost.cloudstream3.ui.settings.SettingsGeneral.Companion.pickDownloadPath +import com.lagradost.cloudstream3.ui.settings.utils.getChooseFolderLauncher import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull import com.lagradost.cloudstream3.utils.AppContextUtils.isCastApiAvailable import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache @@ -90,6 +95,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.populateChips import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.UIHelper.setListViewHeightBasedOnItems import com.lagradost.cloudstream3.utils.UIHelper.setNavigationBarColorCompat +import com.lagradost.cloudstream3.utils.downloader.DownloadFileManagement.getBasePath import com.lagradost.cloudstream3.utils.downloader.DownloadObjects import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager import com.lagradost.cloudstream3.utils.getImageFromDrawable @@ -97,6 +103,7 @@ import com.lagradost.cloudstream3.utils.setText import com.lagradost.cloudstream3.utils.setTextHtml import com.lagradost.cloudstream3.utils.txt import java.net.URLEncoder +import java.util.concurrent.ConcurrentLinkedDeque import kotlin.math.roundToInt open class ResultFragmentPhone : FullScreenPlayer() { @@ -107,6 +114,72 @@ open class ResultFragmentPhone : FullScreenPlayer() { } } + /** Queue of pending actions that is deferred to after a custom path is set */ + private val pendingPathActions = ConcurrentLinkedDeque>() + + /** + * Appends all actions to a queue, and asks for a user to enter the download folder if not already set up. + * + * Then processes the queue in the given order, only after the user has selected a folder. + * This is to defer the download to after a file path is set, due to perms. + * */ + private fun requirePathForActions(list: Collection>) { + pendingPathActions.addAll(list) + val (_, path) = context?.getBasePath() ?: return + if (path == null) { + /** If we have not set any download path, then ask the user for it before we download it */ + try { + /** Give the user some info of what we are doing and why, even if it may be missed */ + showToast(R.string.download_path_pref) + pathPicker.launch(Uri.EMPTY) + } catch (t: Throwable) { + logError(t) + /** Something went wrong, TV Device? + * Use the fallback behavior of just downloading it even if no path is selected, + * and hope it works */ + processPendingActions() + } + } else { + /** + * Otherwise dispatch everything, as we already have a valid download path + * Even if this is "wrong", we do not care as the user has entered something + * */ + processPendingActions() + } + } + + /** Clear all the items in the queue and dispatch them to the viewmodel in order */ + private fun processPendingActions() = viewModel.viewModelScope.launchSafe { + while (!pendingPathActions.isEmpty()) { + try { + val (action, data) = pendingPathActions.pop() + viewModel.handleAction( + EpisodeClickEvent( + action, + data + ) + ) + } catch (_: NoSuchElementException) { + /** In case of a race */ + } + } + } + + private val pathPicker = getChooseFolderLauncher { uri, path -> + if (uri == null) { + /** No path selected, clear the list without acting on it, canceling */ + if (!pendingPathActions.isEmpty()) { + /** Only show on non-empty, just in case */ + showToast(R.string.download_canceled) + pendingPathActions.clear() + } + } else { + /** Select the folder, and dispatch everything */ + pickDownloadPath(uri, path) + processPendingActions() + } + } + protected lateinit var viewModel: ResultViewModel2 protected lateinit var syncModel: SyncViewModel @@ -433,7 +506,13 @@ open class ResultFragmentPhone : FullScreenPlayer() { EpisodeAdapter( api?.hasDownloadSupport == true, { episodeClick -> - viewModel.handleAction(episodeClick) + when (episodeClick.action) { + ACTION_DOWNLOAD_EPISODE, ACTION_DOWNLOAD_MIRROR -> { + requirePathForActions(listOf(episodeClick.action to episodeClick.data)) + } + + else -> viewModel.handleAction(episodeClick) + } }, { downloadClickEvent -> DownloadButtonSetup.handleDownloadClick(downloadClickEvent) @@ -753,30 +832,12 @@ open class ResultFragmentPhone : FullScreenPlayer() { .setTitle(R.string.download_all) .setMessage(rangeMessage) .setPositiveButton(R.string.yes) { _, _ -> - ioSafe { - episodes.value.forEach { episode -> - viewModel.handleAction( - EpisodeClickEvent( - ACTION_DOWNLOAD_EPISODE, - episode - ) - ) - // Join to make the episodes ordered - .join() - } - } + requirePathForActions(episodes.value.map { ACTION_DOWNLOAD_EPISODE to it }) } - .setNegativeButton(R.string.cancel) { _, _ -> - - }.show() - + .setNegativeButton(R.string.cancel) { _, _ -> }.show() } - } - - } - } observeNullable(viewModel.movie) { data -> @@ -825,18 +886,11 @@ open class ResultFragmentPhone : FullScreenPlayer() { when (click.action) { DOWNLOAD_ACTION_DOWNLOAD -> { - viewModel.handleAction( - EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, ep) - ) + requirePathForActions(listOf(ACTION_DOWNLOAD_EPISODE to ep)) } DOWNLOAD_ACTION_LONG_CLICK -> { - viewModel.handleAction( - EpisodeClickEvent( - ACTION_DOWNLOAD_MIRROR, - ep - ) - ) + requirePathForActions(listOf(ACTION_DOWNLOAD_MIRROR to ep)) } else -> DownloadButtonSetup.handleDownloadClick(click) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index b13987f2827..dbf2ff1dc53 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -8,6 +8,7 @@ import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.content.edit import androidx.core.os.ConfigurationCompat +import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.APIHolder.allProviders @@ -155,16 +156,23 @@ class SettingsGeneral : BasePreferenceFragmentCompat() { val lang: String, ) - private val pathPicker = getChooseFolderLauncher { uri, path -> - val context = context ?: CloudStreamApp.context ?: return@getChooseFolderLauncher - (path ?: uri.toString()).let { + companion object { + fun Fragment.pickDownloadPath(uri: Uri?, path: String?) { + if (uri == null) return + + val context = context ?: CloudStreamApp.context ?: return + val visual = path ?: uri.toString() PreferenceManager.getDefaultSharedPreferences(context).edit { putString(getString(R.string.download_path_key), uri.toString()) - putString(getString(R.string.download_path_key_visual), it) + putString(context.getString(R.string.download_path_key_visual), visual) } } } + private val pathPicker = getChooseFolderLauncher { uri, path -> + pickDownloadPath(uri, path) + } + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { hideKeyboard() setPreferencesFromResource(R.xml.settings_general, rootKey) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt index 9250f6f6f2b..c04215594e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt @@ -58,6 +58,8 @@ class SettingsUpdates : BasePreferenceFragmentCompat() { } private val pathPicker = getChooseFolderLauncher { uri, path -> + if(uri == null) return@getChooseFolderLauncher + val context = context ?: CloudStreamApp.context ?: return@getChooseFolderLauncher (path ?: uri.toString()).let { PreferenceManager.getDefaultSharedPreferences(context).edit { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/utils/DirectoryPicker.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/utils/DirectoryPicker.kt index 08a79b4b497..dfc93117481 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/utils/DirectoryPicker.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/utils/DirectoryPicker.kt @@ -10,7 +10,10 @@ import com.lagradost.safefile.SafeFile fun Fragment.getChooseFolderLauncher(dirSelected: (uri: Uri?, path: String?) -> Unit) = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> // It lies, it can be null if file manager quits. - if (uri == null) return@registerForActivityResult + if(uri == null) { + dirSelected(null, null) + return@registerForActivityResult + } val context = context ?: CloudStreamApp.context ?: return@registerForActivityResult // RW perms for the path val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or