From 734924da7b650821a80c72e04d3b07cc3ef70cc6 Mon Sep 17 00:00:00 2001
From: junkfood <69683722+JunkFood02@users.noreply.github.com>
Date: Sun, 11 Aug 2024 20:26:15 +0800
Subject: [PATCH] feat(download): re-run info fetching when hit error
---
.../main/java/com/junkfood/seal/Downloader.kt | 11 +-
.../junkfood/seal/QuickDownloadActivity.kt | 2 +-
.../seal/ui/page/download/DownloadPage.kt | 2 +-
.../ui/page/downloadv2/DownloadDialogV2.kt | 200 ++++++++++++++----
.../downloadv2/DownloadDialogViewModel.kt | 22 +-
app/src/main/res/values/strings.xml | 2 +-
6 files changed, 174 insertions(+), 65 deletions(-)
diff --git a/app/src/main/java/com/junkfood/seal/Downloader.kt b/app/src/main/java/com/junkfood/seal/Downloader.kt
index 1a0fab59f6..e876c0632a 100644
--- a/app/src/main/java/com/junkfood/seal/Downloader.kt
+++ b/app/src/main/java/com/junkfood/seal/Downloader.kt
@@ -26,8 +26,6 @@ import com.junkfood.seal.util.VideoClip
import com.junkfood.seal.util.VideoInfo
import com.junkfood.seal.util.toHttpsUrl
import com.yausername.youtubedl_android.YoutubeDL
-import java.util.concurrent.CancellationException
-import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@@ -36,6 +34,8 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import java.util.concurrent.CancellationException
+import kotlin.math.roundToInt
/** Singleton Downloader for state holder & perform downloads, used by `Activity` & `Service` */
object Downloader {
@@ -437,10 +437,6 @@ object Downloader {
Log.d(TAG, "downloadVideo: id=${videoInfo.id} " + videoInfo.title)
Log.d(TAG, "notificationId: $notificationId")
- // TextUtil.makeToastSuspend(
- // context.getString(R.string.download_start_msg).format(videoInfo.title)
- // )
-
NotificationUtil.notifyProgress(notificationId = notificationId, title = videoInfo.title)
return DownloadUtil.downloadVideo(
videoInfo = videoInfo,
@@ -454,7 +450,8 @@ object Downloader {
notificationId = notificationId,
progress = progress.toInt(),
text = line,
- title = videoInfo.title)
+ title = videoInfo.title,
+ taskId = taskId)
}
.onFailure {
manageDownloadError(
diff --git a/app/src/main/java/com/junkfood/seal/QuickDownloadActivity.kt b/app/src/main/java/com/junkfood/seal/QuickDownloadActivity.kt
index f838a57a94..6d65eded1e 100644
--- a/app/src/main/java/com/junkfood/seal/QuickDownloadActivity.kt
+++ b/app/src/main/java/com/junkfood/seal/QuickDownloadActivity.kt
@@ -111,7 +111,7 @@ class QuickDownloadActivity : ComponentActivity() {
config = Config(),
preferences = preferences,
onPreferencesUpdate = { preferences = it },
- onActionPosted = {
+ onActionPost = {
viewModel.postAction(it)
if (it !is Action.FetchFormats && it !is Action.FetchPlaylist) {
finish()
diff --git a/app/src/main/java/com/junkfood/seal/ui/page/download/DownloadPage.kt b/app/src/main/java/com/junkfood/seal/ui/page/download/DownloadPage.kt
index 8ec150402f..22fac353d4 100644
--- a/app/src/main/java/com/junkfood/seal/ui/page/download/DownloadPage.kt
+++ b/app/src/main/java/com/junkfood/seal/ui/page/download/DownloadPage.kt
@@ -294,7 +294,7 @@ fun DownloadPage(
config = Config(),
preferences = preferences,
onPreferencesUpdate = { preferences = it },
- onActionPosted = { dialogViewModel.postAction(it) },
+ onActionPost = { dialogViewModel.postAction(it) },
)
}
when (selectionState) {
diff --git a/app/src/main/java/com/junkfood/seal/ui/page/downloadv2/DownloadDialogV2.kt b/app/src/main/java/com/junkfood/seal/ui/page/downloadv2/DownloadDialogV2.kt
index d1de2a7634..50abdf6344 100644
--- a/app/src/main/java/com/junkfood/seal/ui/page/downloadv2/DownloadDialogV2.kt
+++ b/app/src/main/java/com/junkfood/seal/ui/page/downloadv2/DownloadDialogV2.kt
@@ -33,6 +33,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.icons.Icons
@@ -42,6 +43,7 @@ import androidx.compose.material.icons.filled.SettingsSuggest
import androidx.compose.material.icons.filled.VideoFile
import androidx.compose.material.icons.outlined.Cancel
import androidx.compose.material.icons.outlined.DoneAll
+import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material.icons.outlined.ExpandMore
import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.outlined.MoreVert
@@ -51,6 +53,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -72,15 +75,21 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.junkfood.seal.App
import com.junkfood.seal.R
+import com.junkfood.seal.ui.common.HapticFeedback.longPressHapticFeedback
import com.junkfood.seal.ui.common.motion.materialSharedAxisX
import com.junkfood.seal.ui.component.DrawerSheetSubtitle
import com.junkfood.seal.ui.component.OutlinedButtonWithIcon
@@ -122,6 +131,7 @@ import com.junkfood.seal.util.PreferenceUtil.updateBoolean
import com.junkfood.seal.util.PreferenceUtil.updateInt
import com.junkfood.seal.util.SUBTITLE
import com.junkfood.seal.util.THUMBNAIL
+import com.junkfood.seal.util.ToastUtil
import com.junkfood.seal.util.USE_CUSTOM_AUDIO_PRESET
import com.junkfood.seal.util.VIDEO_FORMAT
import com.junkfood.seal.util.VIDEO_QUALITY
@@ -220,7 +230,7 @@ fun ConfigureDialog(
preferences: DownloadUtil.DownloadPreferences,
onPreferencesUpdate: (DownloadUtil.DownloadPreferences) -> Unit,
state: DownloadDialogViewModel.SheetState = Configure,
- onActionPosted: (Action) -> Unit = {}
+ onActionPost: (Action) -> Unit = {}
) {
var showVideoPresetDialog by remember { mutableStateOf(false) }
@@ -276,56 +286,152 @@ fun ConfigureDialog(
SealModalBottomSheet(
sheetState = sheetState,
contentPadding = PaddingValues(),
- onDismissRequest = { onActionPosted(Action.HideSheet) }) {
- AnimatedContent(
- targetState = state,
- label = "",
- transitionSpec = {
- materialSharedAxisX(initialOffsetX = { it / 4 }, targetOffsetX = { -it / 4 })
- }) { state ->
- when (state) {
- Configure -> {
- ConfigurePageImpl(
- url = url,
- config = config,
- preferences = preferences,
- onPresetEdit = { type ->
- when (type) {
- Audio -> showAudioPresetDialog = true
-
- Video -> showVideoPresetDialog = true
-
- else -> {}
- }
- },
- onConfigSave = { Config.updatePreferences(it) },
- settingChips = {
- AdditionalSettings(
- modifier = Modifier.padding(horizontal = 16.dp),
- isQuickDownload = false,
- preference = preferences,
- selectedType = Audio,
- onPreferenceUpdate = {
- onPreferencesUpdate(
- DownloadUtil.DownloadPreferences
- .createFromPreferences())
- })
- },
- onActionPost = { onActionPosted(it) })
- }
+ onDismissRequest = { onActionPost(Action.HideSheet) },
+ ) {
+ ConfigureDialogContent(
+ modifier = modifier,
+ state = state,
+ url = url,
+ config = config,
+ preferences = preferences,
+ onPreferencesUpdate = onPreferencesUpdate,
+ onPresetEdit = { type ->
+ when (type) {
+ Audio -> showAudioPresetDialog = true
- is Error -> {
- Text(state.throwable.stackTrace.contentToString())
- }
+ Video -> showVideoPresetDialog = true
- else -> {
- Column(modifier = Modifier.fillMaxWidth().padding(vertical = 120.dp)) {
- CircularProgressIndicator(
- modifier = Modifier.align(Alignment.CenterHorizontally))
- }
- }
+ else -> {}
+ }
+ },
+ onActionPost = onActionPost)
+ }
+}
+
+@Composable
+private fun ErrorPage(modifier: Modifier = Modifier, state: Error, onActionPost: (Action) -> Unit) {
+ val view = LocalView.current
+ val clipboardManager = LocalClipboardManager.current
+ val url =
+ state.action.run {
+ when (this) {
+ is Action.FetchFormats -> url
+ is Action.FetchPlaylist -> url
+ else -> {
+ throw IllegalArgumentException()
+ }
+ }
+ }
+ Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
+ Icon(
+ imageVector = Icons.Outlined.ErrorOutline,
+ contentDescription = null,
+ modifier = Modifier.size(40.dp))
+ Text(
+ text = stringResource(R.string.fetch_info_error_msg),
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.padding(top = 12.dp))
+ Text(
+ text = state.throwable.message.toString(),
+ style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier =
+ Modifier.padding(vertical = 16.dp, horizontal = 20.dp)
+ .fillMaxWidth()
+ .verticalScroll(rememberScrollState()),
+ maxLines = 20,
+ overflow = TextOverflow.Clip)
+
+ Row {
+ Button(
+ onClick = {
+ view.longPressHapticFeedback()
+ clipboardManager.setText(
+ AnnotatedString(
+ App.getVersionReport() + "\nURL: ${url}\n${state.throwable.message}"))
+ ToastUtil.makeToast(R.string.error_copied)
+ }) {
+ Text(stringResource(R.string.copy_error_report))
+ }
+ Spacer(Modifier.width(8.dp))
+ FilledTonalButton(onClick = { onActionPost(state.action) }) { Text("Retry") }
+ }
+ }
+}
+
+@Composable
+private fun ConfigureDialogContent(
+ modifier: Modifier = Modifier,
+ url: String,
+ state: DownloadDialogViewModel.SheetState,
+ config: Config,
+ preferences: DownloadUtil.DownloadPreferences,
+ onPreferencesUpdate: (DownloadUtil.DownloadPreferences) -> Unit,
+ onPresetEdit: (DownloadType?) -> Unit,
+ onActionPost: (Action) -> Unit,
+) {
+ AnimatedContent(
+ modifier = modifier,
+ targetState = state,
+ label = "",
+ transitionSpec = {
+ materialSharedAxisX(initialOffsetX = { it / 4 }, targetOffsetX = { -it / 4 })
+ }) { state ->
+ when (state) {
+ Configure -> {
+ ConfigurePageImpl(
+ url = url,
+ config = config,
+ preferences = preferences,
+ onPresetEdit = onPresetEdit,
+ onConfigSave = { Config.updatePreferences(it) },
+ settingChips = {
+ AdditionalSettings(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ isQuickDownload = false,
+ preference = preferences,
+ selectedType = Audio,
+ onPreferenceUpdate = {
+ onPreferencesUpdate(
+ DownloadUtil.DownloadPreferences.createFromPreferences())
+ })
+ },
+ onActionPost = { onActionPost(it) })
+ }
+
+ is Error -> {
+ ErrorPage(state = state, onActionPost = onActionPost)
+ }
+
+ else -> {
+ Column(modifier = Modifier.fillMaxWidth().padding(vertical = 120.dp)) {
+ CircularProgressIndicator(
+ modifier = Modifier.align(Alignment.CenterHorizontally))
}
}
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Composable
+private fun ErrorPreview() {
+ SealModalBottomSheet(
+ onDismissRequest = {},
+ sheetState =
+ SheetState(
+ skipPartiallyExpanded = true,
+ initialValue = SheetValue.Expanded,
+ density = LocalDensity.current)) {
+ ErrorPage(
+ state =
+ Error(
+ action =
+ Action.FetchFormats(
+ url = "", audioOnly = true, preferences = PreferencesMock),
+ throwable = Exception("Not good")),
+ onActionPost = {})
}
}
diff --git a/app/src/main/java/com/junkfood/seal/ui/page/downloadv2/DownloadDialogViewModel.kt b/app/src/main/java/com/junkfood/seal/ui/page/downloadv2/DownloadDialogViewModel.kt
index 1af538f7fe..8287efeb51 100644
--- a/app/src/main/java/com/junkfood/seal/ui/page/downloadv2/DownloadDialogViewModel.kt
+++ b/app/src/main/java/com/junkfood/seal/ui/page/downloadv2/DownloadDialogViewModel.kt
@@ -35,7 +35,7 @@ class DownloadDialogViewModel : ViewModel() {
data class Loading(val taskKey: String, val job: Job) : SheetState
- data class Error(val throwable: Throwable) : SheetState
+ data class Error(val action: Action, val throwable: Throwable) : SheetState
}
sealed interface SheetValue {
@@ -96,8 +96,8 @@ class DownloadDialogViewModel : ViewModel() {
fun postAction(action: Action) {
with(action) {
when (this) {
- is Action.FetchFormats -> fetchFormat(url, preferences)
- is Action.FetchPlaylist -> fetchPlaylist(url)
+ is Action.FetchFormats -> fetchFormat(this)
+ is Action.FetchPlaylist -> fetchPlaylist(this)
is Action.DownloadWithPreset -> downloadWithPreset(url, preferences)
is Action.RunCommand -> runCommand(url, template)
Action.HideSheet -> hideDialog()
@@ -109,7 +109,8 @@ class DownloadDialogViewModel : ViewModel() {
}
}
- private fun fetchPlaylist(url: String) {
+ private fun fetchPlaylist(action: Action.FetchPlaylist) {
+ val (url) = action
// TODO: handle downloader state
Downloader.clearErrorState()
@@ -133,17 +134,22 @@ class DownloadDialogViewModel : ViewModel() {
}
}
}
- .onFailure { th -> mSheetStateFlow.update { SheetState.Error(th) } }
+ .onFailure { th ->
+ mSheetStateFlow.update { SheetState.Error(action = action, throwable = th) }
+ }
}
mSheetStateFlow.update { SheetState.Loading(taskKey = "FetchPlaylist_$url", job = job) }
}
- private fun fetchFormat(url: String, preferences: DownloadUtil.DownloadPreferences) {
+ private fun fetchFormat(action: Action.FetchFormats) {
+ val (url, audioOnly, preferences) = action
val job =
viewModelScope.launch(Dispatchers.IO) {
DownloadUtil.fetchVideoInfoFromUrl(
- url = url, preferences = preferences, taskKey = "FetchFormat_$url")
+ url = url,
+ preferences = preferences.copy(extractAudio = audioOnly),
+ taskKey = "FetchFormat_$url")
.onSuccess { info ->
withContext(Dispatchers.Main) {
mSelectionStateFlow.update {
@@ -154,7 +160,7 @@ class DownloadDialogViewModel : ViewModel() {
}
.onFailure { th ->
withContext(Dispatchers.Main) {
- mSheetStateFlow.update { SheetState.Error(th) }
+ mSheetStateFlow.update { SheetState.Error(action, throwable = th) }
}
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4cc14e0e13..4000810a90 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -200,7 +200,7 @@
Restart
Error
Copy link
- Error report
+ Copy report
Video resolution
Video file size
Export to clipboard