mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-24 02:18:50 +01:00
Improve how snack bars are queued
This commit is contained in:
parent
9c8afda6a7
commit
8eccce506c
6 changed files with 165 additions and 70 deletions
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)}"),
|
||||
|
|
70
app/test/snack_bar_manager_test.dart
Normal file
70
app/test/snack_bar_manager_test.dart
Normal 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>();
|
||||
}
|
Loading…
Reference in a new issue