Improve how snack bars are queued

This commit is contained in:
Ming Ming 2022-07-25 13:04:22 +08:00
parent 9c8afda6a7
commit 8eccce506c
6 changed files with 165 additions and 70 deletions

View file

@ -157,13 +157,13 @@ class _DownloadHandlerWeb extends _DownloadHandlerBase {
String? parentDir,
}) async {
_log.info("[downloadFiles] Downloading ${files.length} file");
var controller = SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().downloadProcessingNotification),
duration: k.snackBarDurationShort,
));
controller?.closed.whenComplete(() {
controller = null;
});
SnackBarManager().showSnackBar(
SnackBar(
content: Text(L10n.global().downloadProcessingNotification),
duration: k.snackBarDurationShort,
),
canBeReplaced: true,
);
int successCount = 0;
for (final f in files) {
try {
@ -175,7 +175,6 @@ class _DownloadHandlerWeb extends _DownloadHandlerBase {
++successCount;
} on PermissionException catch (_) {
_log.warning("[downloadFiles] Permission not granted");
controller?.close();
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().errorNoStoragePermission),
duration: k.snackBarDurationNormal,
@ -186,7 +185,6 @@ class _DownloadHandlerWeb extends _DownloadHandlerBase {
break;
} catch (e, stackTrace) {
_log.shout("[downloadFiles] Failed while DownloadFile", e, stackTrace);
controller?.close();
SnackBarManager().showSnackBar(SnackBar(
content: Text("${L10n.global().downloadFailureNotification}: "
"${exception_util.toUserString(e)}"),
@ -195,7 +193,6 @@ class _DownloadHandlerWeb extends _DownloadHandlerBase {
}
}
if (successCount > 0) {
controller?.close();
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().downloadSuccessNotification),
duration: k.snackBarDurationShort,

View file

@ -14,25 +14,22 @@ class NotifiedAction {
});
Future<void> call() async {
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? controller;
if (processingText != null) {
controller = SnackBarManager().showSnackBar(SnackBar(
content: Text(processingText!),
duration: k.snackBarDurationShort,
));
SnackBarManager().showSnackBar(
SnackBar(
content: Text(processingText!),
duration: k.snackBarDurationShort,
),
canBeReplaced: true,
);
}
controller?.closed.whenComplete(() {
controller = null;
});
try {
await action();
controller?.close();
SnackBarManager().showSnackBar(SnackBar(
content: Text(successText),
duration: k.snackBarDurationNormal,
));
} catch (e) {
controller?.close();
SnackBarManager().showSnackBar(SnackBar(
content: Text(
(failureText?.isNotEmpty == true ? "$failureText: " : "") +
@ -67,16 +64,15 @@ class NotifiedListAction<T> {
/// Perform the action and return the success count
Future<int> call() async {
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? controller;
if (processingText != null) {
controller = SnackBarManager().showSnackBar(SnackBar(
content: Text(processingText!),
duration: k.snackBarDurationShort,
));
SnackBarManager().showSnackBar(
SnackBar(
content: Text(processingText!),
duration: k.snackBarDurationShort,
),
canBeReplaced: true,
);
}
controller?.closed.whenComplete(() {
controller = null;
});
final failedItems = <T>[];
for (final item in list) {
try {
@ -87,7 +83,6 @@ class NotifiedListAction<T> {
}
}
if (failedItems.isEmpty) {
controller?.close();
SnackBarManager().showSnackBar(SnackBar(
content: Text(successText),
duration: k.snackBarDurationNormal,
@ -95,7 +90,6 @@ class NotifiedListAction<T> {
} else {
final failureText = getFailureText?.call(failedItems);
if (failureText?.isNotEmpty == true) {
controller?.close();
SnackBarManager().showSnackBar(SnackBar(
content: Text(failureText!),
duration: k.snackBarDurationNormal,

View file

@ -166,21 +166,19 @@ class ShareHandler {
return;
}
_log.info("[_shareAsLink] Share as folder: ${result.albumName}");
ScaffoldFeatureController? controller =
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().createShareProgressText),
duration: k.snackBarDurationShort,
));
controller?.closed.whenComplete(() {
controller = null;
});
SnackBarManager().showSnackBar(
SnackBar(
content: Text(L10n.global().createShareProgressText),
duration: k.snackBarDurationShort,
),
canBeReplaced: true,
);
clearSelection?.call();
isSelectionCleared = true;
final c = KiwiContainer().resolve<DiContainer>();
final path = await _createDir(c.fileRepo, account, result.albumName);
await _copyFilesToDir(c.fileRepo, account, files, path);
controller?.close();
return _shareFileAsLink(
account,
File(

View file

@ -1,3 +1,5 @@
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
@ -9,7 +11,8 @@ import 'package:logging/logging.dart';
class SnackBarManager {
factory SnackBarManager() => _inst;
SnackBarManager._();
@visibleForTesting
SnackBarManager.scoped();
void registerHandler(SnackBarHandler handler) {
_handlers.add(handler);
@ -19,42 +22,69 @@ class SnackBarManager {
_handlers.remove(handler);
}
/// Show a snack bar if possible
/// Queue a snack bar to be shown ASAP
///
/// If the snack bar can't be shown at this time, return null.
/// If the snack bar can't be shown, return null.
///
/// If [canBeReplaced] is true, this snackbar will be dismissed by the next
/// snack bar
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? showSnackBar(
void showSnackBar(
SnackBar snackBar, {
bool canBeReplaced = false,
}) {
if (_canPrevBeReplaced) {
_prevController?.close();
_add(_Item(snackBar, canBeReplaced));
_ensureRunning();
}
void _ensureRunning() {
if (!_isRunning) {
_isRunning = true;
_next();
}
_canPrevBeReplaced = canBeReplaced;
}
void _add(_Item item) {
_queue.add(item);
if (_currentItem?.canBeReplaced == true) {
_currentItem!.controller?.close();
}
}
Future<void> _next() async {
if (_queue.isEmpty) {
_isRunning = false;
return;
}
final item = _queue.removeFirst();
if (item.canBeReplaced && _queue.isNotEmpty) {
_log.info("[_next] Skip replaceable snack bar");
return _next();
}
// show item
for (final h in _handlers.reversed) {
final result = h.showSnackBar(snackBar);
if (result != null) {
_prevController = result;
result.closed.whenComplete(() {
if (identical(_prevController, result)) {
_prevController = null;
}
});
return result;
final controller = h.showSnackBar(item.snackBar);
if (controller != null) {
item.controller = controller;
_currentItem = item;
try {
final reason = await controller.closed;
_log.fine("[_next] Snack bar closed: ${reason.name}");
} finally {
_currentItem = null;
}
return _next();
}
}
_log.warning("[showSnackBar] No handler available");
_prevController = null;
return null;
_log.warning("[_next] No handler available");
return _next();
}
final _handlers = <SnackBarHandler>[];
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? _prevController;
var _canPrevBeReplaced = false;
final Queue<_Item> _queue = DoubleLinkedQueue();
var _isRunning = false;
_Item? _currentItem;
static final _inst = SnackBarManager._();
static final _inst = SnackBarManager.scoped();
static final _log = Logger("snack_bar_manager.SnackBarManager");
}
@ -63,3 +93,11 @@ abstract class SnackBarHandler {
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? showSnackBar(
SnackBar snackBar);
}
class _Item {
_Item(this.snackBar, this.canBeReplaced);
final SnackBar snackBar;
final bool canBeReplaced;
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? controller;
}

View file

@ -157,17 +157,16 @@ class _TrashbinViewerState extends State<TrashbinViewer> {
void _onRestorePressed() async {
final file = widget.streamFiles[_viewerController.currentPage];
_log.info("[_onRestorePressed] Restoring file: ${file.path}");
var controller = SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().restoreProcessingNotification),
duration: k.snackBarDurationShort,
));
controller?.closed.whenComplete(() {
controller = null;
});
SnackBarManager().showSnackBar(
SnackBar(
content: Text(L10n.global().restoreProcessingNotification),
duration: k.snackBarDurationShort,
),
canBeReplaced: true,
);
try {
await RestoreTrashbin(KiwiContainer().resolve<DiContainer>())(
widget.account, file);
controller?.close();
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().restoreSuccessNotification),
duration: k.snackBarDurationNormal,
@ -178,7 +177,6 @@ class _TrashbinViewerState extends State<TrashbinViewer> {
} catch (e, stacktrace) {
_log.shout("Failed while restore trashbin: ${logFilename(file.path)}", e,
stacktrace);
controller?.close();
SnackBarManager().showSnackBar(SnackBar(
content: Text("${L10n.global().restoreFailureNotification}: "
"${exception_util.toUserString(e)}"),

View file

@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:nc_photos/snack_bar_manager.dart';
void main() {
group("SnackBarManager", () {
group("showSnackBar", () {
testWidgets("canBeReplaced = true", (tester) async {
final manager = SnackBarManager.scoped();
await tester.pumpWidget(_TestWidget(manager));
manager.showSnackBar(
const SnackBar(
content: Text("test1"),
duration: Duration(seconds: 10),
),
canBeReplaced: true,
);
await tester.pumpAndSettle();
expect(find.text("test1"), findsOneWidget);
manager.showSnackBar(const SnackBar(
content: Text("test2"),
duration: Duration(seconds: 1),
));
await tester.pumpAndSettle();
expect(find.text("test1"), findsNothing);
expect(find.text("test2"), findsOneWidget);
});
});
});
}
class _TestWidget extends StatefulWidget {
const _TestWidget(this.manager, {Key? key}) : super(key: key);
@override
createState() => _TestWidgetState();
final SnackBarManager manager;
}
class _TestWidgetState extends State<_TestWidget> implements SnackBarHandler {
@override
initState() {
super.initState();
widget.manager.registerHandler(this);
}
@override
build(BuildContext context) {
return MaterialApp(
scaffoldMessengerKey: _scaffoldMessengerKey,
home: Scaffold(
body: Container(),
),
);
}
@override
dispose() {
super.dispose();
widget.manager.unregisterHandler(this);
}
@override
showSnackBar(SnackBar snackBar) =>
_scaffoldMessengerKey.currentState?.showSnackBar(snackBar);
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
}