#676 snackbar indicator for failure messages

This commit is contained in:
Thibault Deckers 2023-08-21 00:01:50 +02:00
parent 2ba96c78b2
commit 3f1a6452e5
13 changed files with 119 additions and 78 deletions

View file

@ -174,16 +174,16 @@ class _BugReportState extends State<BugReport> 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<void> _copySystemInfo() async {
await Clipboard.setData(ClipboardData(text: await _infoLoader));
showFeedback(context, context.l10n.genericSuccessFeedback);
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
}
Future<void> _goToGithub() => AvesApp.launchUrl(bugReportUrl);

View file

@ -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);
}
}
}

View file

@ -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<ValueNotifier<AppMode>?>()?.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();
}
},

View file

@ -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<T> extends State<ReportOverlay<T>> 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<DurationsData>().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<DurationsData>().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;

View file

@ -74,7 +74,7 @@ mixin VaultAwareMixin on FeedbackMixin {
Future<bool> 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;
}

View file

@ -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<AlbumFilter> with
}
},
);
showFeedback(context, l10n.genericSuccessFeedback, showAction);
showFeedback(context, FeedbackType.info, l10n.genericSuccessFeedback, showAction);
}
Future<void> _delete(BuildContext context, Set<AlbumFilter> filters) async {
@ -363,7 +364,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> 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<AlbumFilter> 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

View file

@ -119,9 +119,9 @@ class _SettingsMobilePageState extends State<SettingsMobilePage> 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<SettingsMobilePage> 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<SettingsMobilePage> with FeedbackMi
await Future.forEach<AppExportItem>(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);
}
}
}

View file

@ -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<CollectionSource>();
if (source.initState != SourceInitializationState.none) {

View file

@ -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);
}
}
}

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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);
}
}