From f7a37922ea12bdaa5dbc55027398b2896bda3268 Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Fri, 24 Nov 2023 03:01:58 +0200 Subject: [PATCH] feat: ability to cancel downloads for groups --- lib/youtube/pages/yt_downloads_page.dart | 120 ++++++++++++++++-- .../widgets/yt_download_task_item_card.dart | 83 +++++++----- 2 files changed, 159 insertions(+), 44 deletions(-) diff --git a/lib/youtube/pages/yt_downloads_page.dart b/lib/youtube/pages/yt_downloads_page.dart index cf86a8e9..a3fcce55 100644 --- a/lib/youtube/pages/yt_downloads_page.dart +++ b/lib/youtube/pages/yt_downloads_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_scrollbar_modified/flutter_scrollbar_modified.dart'; import 'package:get/get.dart'; import 'package:namida/controller/current_color.dart'; +import 'package:namida/controller/navigator_controller.dart'; import 'package:namida/core/dimensions.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; @@ -62,7 +63,8 @@ class YTDownloadsPage extends StatelessWidget { final smallList = YoutubeController.inst.youtubeDownloadTasksMap[key]?.values.toList(); smallList?.reverseLoop((v, index) { final match = YoutubeController.inst.downloadedFilesMap[key]?[v.filename] == null; - if (match) itemsList.add((key, v)); + final isDownloadingOrFetching = (YoutubeController.inst.isDownloading[v.id]?[v.filename] ?? false) || (YoutubeController.inst.isFetchingData[v.id]?[v.filename] ?? false); + if (match || isDownloadingOrFetching) itemsList.add((key, v)); }); }); } else { @@ -70,14 +72,70 @@ class YTDownloadsPage extends StatelessWidget { final smallList = YoutubeController.inst.youtubeDownloadTasksMap[key]?.values.toList(); smallList?.reverseLoop((v, index) { final match = YoutubeController.inst.downloadedFilesMap[key]?[v.filename] != null; - if (match) itemsList.add((key, v)); + final isDownloadingOrFetching = (YoutubeController.inst.isDownloading[v.id]?[v.filename] ?? false) || (YoutubeController.inst.isFetchingData[v.id]?[v.filename] ?? false); + if (match && !isDownloadingOrFetching) itemsList.add((key, v)); }); }); } } + Future _confirmCancelDialog({ + required BuildContext context, + String operationTitle = '', + String confirmMessage = '', + String groupTitle = '', + required int itemsLength, + }) async { + bool confirmed = false; + + final groupTitleText = groupTitle == '' ? lang.DEFAULT : groupTitle; + await NamidaNavigator.inst.navigateDialog( + dialog: CustomBlurryDialog( + title: lang.WARNING, + normalTitleStyle: true, + isWarning: true, + actions: [ + const CancelButton(), + const SizedBox(width: 4.0), + NamidaButton( + text: (confirmMessage != '' ? confirmMessage : lang.CONFIRM).toUpperCase(), + onPressed: () { + confirmed = true; + NamidaNavigator.inst.closeDialog(); + }, + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12.0), + RichText( + text: TextSpan( + children: [ + TextSpan(text: "$operationTitle: ", style: context.textTheme.displayLarge), + TextSpan( + text: '$groupTitleText ($itemsLength)', + style: context.textTheme.displayMedium, + ), + TextSpan(text: " ?", style: context.textTheme.displayLarge), + ], + ), + ), + const SizedBox(height: 12.0), + ], + ), + ), + ), + ); + return confirmed; + } + @override Widget build(BuildContext context) { + _updateTempList(_isOnGoingSelected.value); // refresh for when coming back to page. + return BackgroundWrapper( child: Column( children: [ @@ -136,7 +194,7 @@ class YTDownloadsPage extends StatelessWidget { iconSize: 24.0, onPressed: () { YoutubeController.inst.youtubeDownloadTasksTempList.loop((e, index) { - YoutubeController.inst.resumeDownloadTasks(groupName: e.$1); + YoutubeController.inst.resumeDownloadTasks(groupName: e.$1, itemsConfig: [e.$2]); }); }, ), @@ -146,13 +204,32 @@ class YTDownloadsPage extends StatelessWidget { onPressed: () { YoutubeController.inst.youtubeDownloadTasksTempList.loop((e, index) { YoutubeController.inst.pauseDownloadTask( - itemsConfig: [], + itemsConfig: [e.$2], groupName: e.$1, - allInGroupName: true, ); }); }, ), + NamidaIconButton( + icon: Broken.close_circle, + iconSize: 24.0, + onPressed: () async { + final confirmed = await _confirmCancelDialog( + context: context, + operationTitle: lang.CANCEL, + groupTitle: lang.ONGOING, + itemsLength: YoutubeController.inst.youtubeDownloadTasksTempList.length, + ); + if (confirmed) { + YoutubeController.inst.youtubeDownloadTasksTempList.loop((e, index) { + YoutubeController.inst.cancelDownloadTask( + itemsConfig: [e.$2], + groupName: e.$1, + ); + }); + } + }, + ), ], const SizedBox(width: 12.0), ], @@ -174,7 +251,7 @@ class YTDownloadsPage extends StatelessWidget { final list = YoutubeController.inst.youtubeDownloadTasksMap[groupName]?.values.toList() ?? []; return NamidaExpansionTile( initiallyExpanded: true, - titleText: groupName, + titleText: groupName == '' ? lang.DEFAULT : groupName, trailing: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -198,6 +275,26 @@ class YTDownloadsPage extends StatelessWidget { }, icon: const Icon(Broken.pause, size: 18.0), ), + IconButton.filledTonal( + padding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + onPressed: () async { + final confirmed = await _confirmCancelDialog( + context: context, + operationTitle: lang.CANCEL, + groupTitle: groupName, + itemsLength: list.length, + ); + if (confirmed) { + YoutubeController.inst.cancelDownloadTask( + itemsConfig: [], + groupName: groupName, + allInGroupName: true, + ); + } + }, + icon: const Icon(Broken.close_circle, size: 18.0), + ), const SizedBox(width: 4.0), const Icon( Broken.arrow_down_2, @@ -215,13 +312,10 @@ class YTDownloadsPage extends StatelessWidget { style: context.textTheme.displayLarge, ), ), - children: list - .asMap() - .keys - .map( - (key) => YTDownloadTaskItemCard(videos: list, index: key, groupName: groupName), - ) - .toList(), + children: List.generate( + list.length, + (index) => YTDownloadTaskItemCard(videos: list, index: index, groupName: groupName), + ), ); }, ) diff --git a/lib/youtube/widgets/yt_download_task_item_card.dart b/lib/youtube/widgets/yt_download_task_item_card.dart index 9d81df4f..711d640d 100644 --- a/lib/youtube/widgets/yt_download_task_item_card.dart +++ b/lib/youtube/widgets/yt_download_task_item_card.dart @@ -38,6 +38,7 @@ class YTDownloadTaskItemCard extends StatelessWidget { required this.index, required this.groupName, }); + Widget _getChip({ required BuildContext context, required IconData icon, @@ -45,6 +46,7 @@ class YTDownloadTaskItemCard extends StatelessWidget { String betweenBrackets = '', bool displayTitle = false, void Function()? onTap, + Widget? Function(double size)? iconWidget, }) { final textWidget = RichText( text: TextSpan( @@ -58,6 +60,7 @@ class YTDownloadTaskItemCard extends StatelessWidget { ], ), ); + final iconSize = displayTitle ? 16.0 : 17.0; return NamidaInkWell( borderRadius: 6.0, padding: const EdgeInsets.only(left: 6.0, right: 6.0, top: 6.0, bottom: 6.0), @@ -67,7 +70,7 @@ class YTDownloadTaskItemCard extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, size: displayTitle ? 16.0 : 17.0), + iconWidget?.call(iconSize) ?? Icon(icon, size: iconSize), if (displayTitle) ...[ const SizedBox(width: 4.0), textWidget, @@ -78,28 +81,30 @@ class YTDownloadTaskItemCard extends StatelessWidget { ); } - void _onPauseDownloadTap(List itemsConfig, bool isDownloading, BuildContext context) { - isDownloading - ? YoutubeController.inst.pauseDownloadTask( - itemsConfig: itemsConfig, - groupName: groupName, - ) - : YoutubeController.inst.downloadYoutubeVideos( - useCachedVersionsIfAvailable: true, - autoExtractTitleAndArtist: settings.ytAutoExtractVideoTagsFromInfo.value, - keepCachedVersionsIfDownloaded: settings.downloadFilesKeepCachedVersions.value, - downloadFilesWriteUploadDate: settings.downloadFilesWriteUploadDate.value, - itemsConfig: itemsConfig, - groupName: groupName, - onFileDownloaded: (downloadedFile) async { - if (downloadedFile != null) { - build(context); - } - }, - onOldFileDeleted: (deletedFile) async { - build(context); - }, - ); + void _onPauseDownloadTap(List itemsConfig) { + YoutubeController.inst.pauseDownloadTask( + itemsConfig: itemsConfig, + groupName: groupName, + ); + } + + void _onResumeDownloadTap(List itemsConfig, BuildContext context) { + YoutubeController.inst.downloadYoutubeVideos( + useCachedVersionsIfAvailable: true, + autoExtractTitleAndArtist: settings.ytAutoExtractVideoTagsFromInfo.value, + keepCachedVersionsIfDownloaded: settings.downloadFilesKeepCachedVersions.value, + downloadFilesWriteUploadDate: settings.downloadFilesWriteUploadDate.value, + itemsConfig: itemsConfig, + groupName: groupName, + onFileDownloaded: (downloadedFile) async { + if (downloadedFile != null) { + build(context); + } + }, + onOldFileDeleted: (deletedFile) async { + build(context); + }, + ); } void _onCancelDeleteDownloadTap(List itemsConfig) { @@ -305,7 +310,7 @@ class YTDownloadTaskItemCard extends StatelessWidget { const SizedBox(width: 6.0), Expanded( child: Text( - texts.where((element) => element != '').join(' • '), + texts.joinText(), style: context.textTheme.displaySmall?.copyWith(fontSize: 12.0.multipliedFontScale), ), ), @@ -420,6 +425,12 @@ class YTDownloadTaskItemCard extends StatelessWidget { videoYTID: null, ); + final itemIcon = item.videoStream != null + ? Broken.video + : item.audioStream != null + ? Broken.musicnote + : null; + return NamidaPopupWrapper( openOnTap: false, openOnLongPress: true, @@ -561,12 +572,15 @@ class YTDownloadTaskItemCard extends StatelessWidget { Obx( () { final isDownloading = YoutubeController.inst.isDownloading[item.id]?[item.filename] ?? false; - return isDownloading + final isFetching = YoutubeController.inst.isFetchingData[item.id]?[item.filename] ?? false; + final willBeDownloaded = YoutubeController.inst.youtubeDownloadTasksInQueueMap[groupName]?[item.filename] == true; + final canPause = isDownloading || isFetching; + return canPause || willBeDownloaded ? _getChip( context: context, title: lang.PAUSE, icon: Broken.pause, - onTap: () => _onPauseDownloadTap([item], isDownloading, context), + onTap: () => _onPauseDownloadTap([item]), ) : fileExists ? _getChip( @@ -579,13 +593,13 @@ class YTDownloadTaskItemCard extends StatelessWidget { operationTitle: lang.RESTART, ); // ignore: use_build_context_synchronously - if (confirmed) _onPauseDownloadTap([item], isDownloading, context); + if (confirmed) _onResumeDownloadTap([item], context); }) : _getChip( context: context, title: lang.RESUME, icon: Broken.play, - onTap: () => _onPauseDownloadTap([item], isDownloading, context), + onTap: () => _onResumeDownloadTap([item], context), ); }, ), @@ -631,12 +645,19 @@ class YTDownloadTaskItemCard extends StatelessWidget { const Spacer(), Text( [ - if (item.videoStream?.resolution != null) item.videoStream?.resolution ?? '', - downloadedFile.fileSizeFormatted() ?? '', - ].join(' • '), + item.videoStream?.resolution, + downloadedFile.fileSizeFormatted(), + ].joinText(), style: context.textTheme.displaySmall?.copyWith(fontSize: 11.0.multipliedFontScale), ), const SizedBox(width: 4.0), + if (itemIcon != null) + Icon( + itemIcon, + size: 16.0, + color: context.defaultIconColor(), + ), + const SizedBox(width: 4.0), fileExists ? Icon( Broken.tick_circle,