From 3f1a6452e5fe98e12aeb338aacfc5f52ac419ea7 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 21 Aug 2023 00:01:50 +0200 Subject: [PATCH] #676 snackbar indicator for failure messages --- lib/widgets/about/bug_report.dart | 6 +- .../collection/entry_set_action_delegate.dart | 8 +- .../common/action_mixins/entry_storage.dart | 13 +- .../common/action_mixins/feedback.dart | 123 +++++++++++------- .../common/action_mixins/vault_aware.dart | 2 +- .../common/action_delegates/album_set.dart | 9 +- .../settings/settings_mobile_page.dart | 10 +- .../viewer/action/entry_action_delegate.dart | 10 +- .../action/entry_info_action_delegate.dart | 4 +- .../viewer/action/single_entry_editor.dart | 4 +- .../viewer/action/video_action_delegate.dart | 4 +- .../info/embedded/embedded_data_opener.dart | 2 +- .../viewer/overlay/wallpaper_buttons.dart | 2 +- 13 files changed, 119 insertions(+), 78 deletions(-) diff --git a/lib/widgets/about/bug_report.dart b/lib/widgets/about/bug_report.dart index 9a4efcc76..6c5ad4a43 100644 --- a/lib/widgets/about/bug_report.dart +++ b/lib/widgets/about/bug_report.dart @@ -174,16 +174,16 @@ class _BugReportState extends State with FeedbackMixin { ); if (success != null) { if (success) { - showFeedback(context, context.l10n.genericSuccessFeedback); + showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback); } else { - showFeedback(context, context.l10n.genericFailureFeedback); + showFeedback(context, FeedbackType.warn, context.l10n.genericFailureFeedback); } } } Future _copySystemInfo() async { await Clipboard.setData(ClipboardData(text: await _infoLoader)); - showFeedback(context, context.l10n.genericSuccessFeedback); + showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback); } Future _goToGithub() => AvesApp.launchUrl(bugReportUrl); diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 09b0bca10..564724969 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -321,7 +321,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware final successCount = successOps.length; if (successCount < todoCount) { final count = todoCount - successCount; - showFeedback(context, context.l10n.collectionDeleteFailureFeedback(count)); + showFeedback(context, FeedbackType.warn, context.l10n.collectionDeleteFailureFeedback(count)); } // cleanup @@ -438,10 +438,10 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware final successCount = successOps.length; if (successCount < todoCount) { final count = todoCount - successCount; - showFeedback(context, l10n.collectionEditFailureFeedback(count)); + showFeedback(context, FeedbackType.warn, l10n.collectionEditFailureFeedback(count)); } else { final count = editedOps.length; - showFeedback(context, l10n.collectionEditSuccessFeedback(count)); + showFeedback(context, FeedbackType.info, l10n.collectionEditSuccessFeedback(count)); } } }, @@ -723,7 +723,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware await appService.pinToHomeScreen(name, coverEntry, filters: filters); if (!device.showPinShortcutFeedback) { - showFeedback(context, context.l10n.genericSuccessFeedback); + showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback); } } } diff --git a/lib/widgets/common/action_mixins/entry_storage.dart b/lib/widgets/common/action_mixins/entry_storage.dart index 4129459fd..171d3341f 100644 --- a/lib/widgets/common/action_mixins/entry_storage.dart +++ b/lib/widgets/common/action_mixins/entry_storage.dart @@ -116,12 +116,14 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { final count = selectionCount - successCount; showFeedback( context, + FeedbackType.warn, l10n.collectionExportFailureFeedback(count), showAction, ); } else { showFeedback( context, + FeedbackType.info, l10n.genericSuccessFeedback, showAction, ); @@ -226,7 +228,11 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { final successCount = successOps.length; if (successCount < todoCount) { final count = todoCount - successCount; - showFeedback(context, copy ? l10n.collectionCopyFailureFeedback(count) : l10n.collectionMoveFailureFeedback(count)); + showFeedback( + context, + FeedbackType.warn, + copy ? l10n.collectionCopyFailureFeedback(count) : l10n.collectionMoveFailureFeedback(count), + ); } else { final count = movedOps.length; final appMode = context.read?>()?.value; @@ -268,6 +274,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { if (!toBin || (toBin && settings.confirmAfterMoveToBin)) { showFeedback( context, + FeedbackType.info, copy ? l10n.collectionCopySuccessFeedback(count) : l10n.collectionMoveSuccessFeedback(count), action, ); @@ -366,10 +373,10 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { final successCount = successOps.length; if (successCount < todoCount) { final count = todoCount - successCount; - showFeedback(context, l10n.collectionRenameFailureFeedback(count)); + showFeedback(context, FeedbackType.warn, l10n.collectionRenameFailureFeedback(count)); } else { final count = movedOps.length; - showFeedback(context, l10n.collectionRenameSuccessFeedback(count)); + showFeedback(context, FeedbackType.info, l10n.collectionRenameSuccessFeedback(count)); onSuccess?.call(); } }, diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index e56d555a0..30fd47900 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -18,10 +18,12 @@ import 'package:overlay_support/overlay_support.dart'; import 'package:percent_indicator/circular_percent_indicator.dart'; import 'package:provider/provider.dart'; +enum FeedbackType { info, warn } + mixin FeedbackMixin { void dismissFeedback(BuildContext context) => ScaffoldMessenger.of(context).hideCurrentSnackBar(); - void showFeedback(BuildContext context, String message, [SnackBarAction? action]) { + void showFeedback(BuildContext context, FeedbackType type, String message, [SnackBarAction? action]) { ScaffoldMessengerState? scaffoldMessenger; try { scaffoldMessenger = ScaffoldMessenger.of(context); @@ -31,18 +33,19 @@ mixin FeedbackMixin { debugPrint('failed to find ScaffoldMessenger in context'); } if (scaffoldMessenger != null) { - showFeedbackWithMessenger(context, scaffoldMessenger, message, action); + showFeedbackWithMessenger(context, scaffoldMessenger, type, message, action); } } // provide the messenger if feedback happens as the widget is disposed - void showFeedbackWithMessenger(BuildContext context, ScaffoldMessengerState messenger, String message, [SnackBarAction? action]) { + void showFeedbackWithMessenger(BuildContext context, ScaffoldMessengerState messenger, FeedbackType type, String message, [SnackBarAction? action]) { settings.timeToTakeAction.getSnackBarDuration(action != null).then((duration) { final start = DateTime.now(); final theme = Theme.of(context); final snackBarTheme = theme.snackBarTheme; final snackBarContent = _FeedbackMessage( + type: type, message: message, progressColor: theme.colorScheme.secondary, start: start, @@ -274,11 +277,13 @@ class _ReportOverlayState extends State> with SingleTickerPr } class _FeedbackMessage extends StatefulWidget { + final FeedbackType type; final String message; final DateTime? start, stop; final Color progressColor; const _FeedbackMessage({ + required this.type, required this.message, required this.progressColor, this.start, @@ -326,56 +331,80 @@ class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerPro @override Widget build(BuildContext context) { - final text = Text(widget.message); + final textScaleFactor = MediaQuery.textScaleFactorOf(context); final theme = Theme.of(context); final contentTextStyle = theme.snackBarTheme.contentTextStyle ?? ThemeData(brightness: theme.brightness).textTheme.titleMedium!; + final fontSize = theme.snackBarTheme.contentTextStyle?.fontSize ?? theme.textTheme.bodyMedium!.fontSize!; final timerChangeShadowColor = theme.colorScheme.primary; - return _remainingDurationMillis == null - ? text - : Row( - children: [ - Expanded(child: text), - const SizedBox(width: 16), - AnimatedBuilder( - animation: _remainingDurationMillis!, - builder: (context, child) { - final remainingDurationMillis = _remainingDurationMillis!.value; - return CircularIndicator( - radius: 16, - lineWidth: 2, - percent: remainingDurationMillis / _totalDurationMillis!, - background: Colors.grey, - // progress color is provided by the caller, - // because we cannot use the app context theme here - foreground: widget.progressColor, - center: ChangeHighlightText( - '${(remainingDurationMillis / 1000).ceil()}', - style: contentTextStyle.copyWith( - shadows: [ - Shadow( - color: timerChangeShadowColor.withOpacity(0), - blurRadius: 0, - ) - ], - ), - changedStyle: contentTextStyle.copyWith( - shadows: [ - Shadow( - color: timerChangeShadowColor, - blurRadius: 5, - ) - ], - ), - duration: context.read().formTextStyleTransition, - ), - ); - }, - ), - ], - ); + + return Row( + children: [ + if (widget.type == FeedbackType.warn) ...[ + CustomPaint( + painter: _WarnIndicator(), + size: Size(4, fontSize * textScaleFactor), + ), + const SizedBox(width: 8), + ], + Expanded(child: Text(widget.message)), + if (_remainingDurationMillis != null) ...[ + const SizedBox(width: 16), + AnimatedBuilder( + animation: _remainingDurationMillis!, + builder: (context, child) { + final remainingDurationMillis = _remainingDurationMillis!.value; + return CircularIndicator( + radius: 16, + lineWidth: 2, + percent: remainingDurationMillis / _totalDurationMillis!, + background: Colors.grey, + // progress color is provided by the caller, + // because we cannot use the app context theme here + foreground: widget.progressColor, + center: ChangeHighlightText( + '${(remainingDurationMillis / 1000).ceil()}', + style: contentTextStyle.copyWith( + shadows: [ + Shadow( + color: timerChangeShadowColor.withOpacity(0), + blurRadius: 0, + ) + ], + ), + changedStyle: contentTextStyle.copyWith( + shadows: [ + Shadow( + color: timerChangeShadowColor, + blurRadius: 5, + ) + ], + ), + duration: context.read().formTextStyleTransition, + ), + ); + }, + ), + ] + ], + ); } } +class _WarnIndicator extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + canvas.drawRRect( + RRect.fromLTRBR(0, 0, size.width, size.height, Radius.circular(size.shortestSide / 2)), + Paint() + ..style = PaintingStyle.fill + ..color = Colors.amber, + ); + } + + @override + bool shouldRepaint(_WarnIndicator oldDelegate) => false; +} + class ActionFeedback extends StatefulWidget { final Widget? child; diff --git a/lib/widgets/common/action_mixins/vault_aware.dart b/lib/widgets/common/action_mixins/vault_aware.dart index d900a3382..2da5a7662 100644 --- a/lib/widgets/common/action_mixins/vault_aware.dart +++ b/lib/widgets/common/action_mixins/vault_aware.dart @@ -74,7 +74,7 @@ mixin VaultAwareMixin on FeedbackMixin { Future unlockAlbum(BuildContext context, String dirPath) async { final success = await _tryUnlock(dirPath, context); if (!success) { - showFeedback(context, context.l10n.genericFailureFeedback); + showFeedback(context, FeedbackType.warn, context.l10n.genericFailureFeedback); } return success; } diff --git a/lib/widgets/filter_grids/common/action_delegates/album_set.dart b/lib/widgets/filter_grids/common/action_delegates/album_set.dart index d0bcbaf80..8f9be79c8 100644 --- a/lib/widgets/filter_grids/common/action_delegates/album_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/album_set.dart @@ -17,6 +17,7 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/action_mixins/entry_storage.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart'; import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; @@ -255,7 +256,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with } }, ); - showFeedback(context, l10n.genericSuccessFeedback, showAction); + showFeedback(context, FeedbackType.info, l10n.genericSuccessFeedback, showAction); } Future _delete(BuildContext context, Set filters) async { @@ -363,7 +364,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with final successCount = successOps.length; if (successCount < todoCount) { final count = todoCount - successCount; - showFeedbackWithMessenger(context, messenger, l10n.collectionDeleteFailureFeedback(count)); + showFeedbackWithMessenger(context, messenger, FeedbackType.warn, l10n.collectionDeleteFailureFeedback(count)); } // cleanup @@ -442,9 +443,9 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with final successCount = successOps.length; if (successCount < todoCount) { final count = todoCount - successCount; - showFeedbackWithMessenger(context, messenger, l10n.collectionMoveFailureFeedback(count)); + showFeedbackWithMessenger(context, messenger, FeedbackType.warn, l10n.collectionMoveFailureFeedback(count)); } else { - showFeedbackWithMessenger(context, messenger, l10n.genericSuccessFeedback); + showFeedbackWithMessenger(context, messenger, FeedbackType.info, l10n.genericSuccessFeedback); } // cleanup diff --git a/lib/widgets/settings/settings_mobile_page.dart b/lib/widgets/settings/settings_mobile_page.dart index 567ec4da4..6dc4cbd2d 100644 --- a/lib/widgets/settings/settings_mobile_page.dart +++ b/lib/widgets/settings/settings_mobile_page.dart @@ -119,9 +119,9 @@ class _SettingsMobilePageState extends State with FeedbackMi ); if (success != null) { if (success) { - showFeedback(context, context.l10n.genericSuccessFeedback); + showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback); } else { - showFeedback(context, context.l10n.genericFailureFeedback); + showFeedback(context, FeedbackType.warn, context.l10n.genericFailureFeedback); } } case SettingsAction.import: @@ -141,7 +141,7 @@ class _SettingsMobilePageState extends State with FeedbackMi } else { if (allJsonMap is! Map) { debugPrint('failed to import app json=$allJsonMap'); - showFeedback(context, context.l10n.genericFailureFeedback); + showFeedback(context, FeedbackType.warn, context.l10n.genericFailureFeedback); return; } allJsonMap.keys.where((v) => v != exportVersionKey).forEach((k) { @@ -165,10 +165,10 @@ class _SettingsMobilePageState extends State with FeedbackMi await Future.forEach(toImport, (item) async { return item.import(importable[item], source); }); - showFeedback(context, context.l10n.genericSuccessFeedback); + showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback); } catch (error) { debugPrint('failed to import app json, error=$error'); - showFeedback(context, context.l10n.genericFailureFeedback); + showFeedback(context, FeedbackType.warn, context.l10n.genericFailureFeedback); } } } diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index 7da87dd81..56685c672 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -184,7 +184,11 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix _addShortcut(context, targetEntry); case EntryAction.copyToClipboard: appService.copyToClipboard(targetEntry.uri, targetEntry.bestTitle).then((success) { - showFeedback(context, success ? context.l10n.genericSuccessFeedback : context.l10n.genericFailureFeedback); + if (success) { + showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback); + } else { + showFeedback(context, FeedbackType.warn, context.l10n.genericFailureFeedback); + } }); case EntryAction.delete: _delete(context, targetEntry); @@ -338,7 +342,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix await appService.pinToHomeScreen(name, targetEntry, uri: targetEntry.uri); if (!device.showPinShortcutFeedback) { - showFeedback(context, context.l10n.genericSuccessFeedback); + showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback); } } @@ -375,7 +379,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix if (!await checkStoragePermission(context, {targetEntry})) return; if (!await targetEntry.delete()) { - showFeedback(context, l10n.genericFailureFeedback); + showFeedback(context, FeedbackType.warn, l10n.genericFailureFeedback); } else { final source = context.read(); if (source.initState != SourceInitializationState.none) { diff --git a/lib/widgets/viewer/action/entry_info_action_delegate.dart b/lib/widgets/viewer/action/entry_info_action_delegate.dart index 0487d219e..bce630316 100644 --- a/lib/widgets/viewer/action/entry_info_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_info_action_delegate.dart @@ -217,9 +217,9 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi ); if (success != null) { if (success) { - showFeedback(context, context.l10n.genericSuccessFeedback); + showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback); } else { - showFeedback(context, context.l10n.genericFailureFeedback); + showFeedback(context, FeedbackType.warn, context.l10n.genericFailureFeedback); } } } diff --git a/lib/widgets/viewer/action/single_entry_editor.dart b/lib/widgets/viewer/action/single_entry_editor.dart index dd8d3e538..85acb841f 100644 --- a/lib/widgets/viewer/action/single_entry_editor.dart +++ b/lib/widgets/viewer/action/single_entry_editor.dart @@ -57,9 +57,9 @@ mixin SingleEntryEditorMixin on FeedbackMixin, PermissionAwareMixin { await targetEntry.catalog(background: background, force: dataTypes.contains(EntryDataType.catalog), persist: persist); await targetEntry.locate(background: background, force: dataTypes.contains(EntryDataType.address), geocoderLocale: settings.appliedLocale); } - showFeedback(context, l10n.genericSuccessFeedback); + showFeedback(context, FeedbackType.info, l10n.genericSuccessFeedback); } else { - showFeedback(context, l10n.genericFailureFeedback); + showFeedback(context, FeedbackType.warn, l10n.genericFailureFeedback); } } catch (error, stack) { await reportService.recordError(error, stack); diff --git a/lib/widgets/viewer/action/video_action_delegate.dart b/lib/widgets/viewer/action/video_action_delegate.dart index 710c71416..739e79bb1 100644 --- a/lib/widgets/viewer/action/video_action_delegate.dart +++ b/lib/widgets/viewer/action/video_action_delegate.dart @@ -131,9 +131,9 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix }, ) : null; - showFeedback(context, l10n.genericSuccessFeedback, showAction); + showFeedback(context, FeedbackType.info, l10n.genericSuccessFeedback, showAction); } else { - showFeedback(context, l10n.genericFailureFeedback); + showFeedback(context, FeedbackType.warn, l10n.genericFailureFeedback); } } diff --git a/lib/widgets/viewer/info/embedded/embedded_data_opener.dart b/lib/widgets/viewer/info/embedded/embedded_data_opener.dart index 54faa81f6..c7dc91b81 100644 --- a/lib/widgets/viewer/info/embedded/embedded_data_opener.dart +++ b/lib/widgets/viewer/info/embedded/embedded_data_opener.dart @@ -50,7 +50,7 @@ class EmbeddedDataOpener extends StatelessWidget with FeedbackMixin { fields = await embeddedDataService.extractXmpDataProp(entry, notification.props, notification.mimeType); } if (!fields.containsKey('mimeType') || !fields.containsKey('uri')) { - showFeedback(context, context.l10n.viewerInfoOpenEmbeddedFailureFeedback); + showFeedback(context, FeedbackType.warn, context.l10n.viewerInfoOpenEmbeddedFailureFeedback); return; } diff --git a/lib/widgets/viewer/overlay/wallpaper_buttons.dart b/lib/widgets/viewer/overlay/wallpaper_buttons.dart index 2772c6056..1d4b2d519 100644 --- a/lib/widgets/viewer/overlay/wallpaper_buttons.dart +++ b/lib/widgets/viewer/overlay/wallpaper_buttons.dart @@ -86,7 +86,7 @@ class WallpaperButtons extends StatelessWidget with FeedbackMixin { if (success) { await SystemNavigator.pop(); } else { - showFeedback(context, l10n.genericFailureFeedback); + showFeedback(context, FeedbackType.warn, l10n.genericFailureFeedback); } }