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