diff --git a/app/lib/widget/collection_browser.dart b/app/lib/widget/collection_browser.dart index 5fa52621..2636dcdd 100644 --- a/app/lib/widget/collection_browser.dart +++ b/app/lib/widget/collection_browser.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:collection'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:cached_network_image/cached_network_image.dart'; @@ -19,8 +20,8 @@ import 'package:nc_photos/cache_manager_util.dart'; import 'package:nc_photos/controller/account_controller.dart'; import 'package:nc_photos/controller/collection_items_controller.dart'; import 'package:nc_photos/controller/collections_controller.dart'; +import 'package:nc_photos/controller/files_controller.dart'; import 'package:nc_photos/controller/pref_controller.dart'; -import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/download_handler.dart'; import 'package:nc_photos/entity/collection.dart'; @@ -41,9 +42,6 @@ import 'package:nc_photos/np_api_util.dart'; import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/session_storage.dart'; import 'package:nc_photos/snack_bar_manager.dart'; -import 'package:nc_photos/use_case/archive_file.dart'; -import 'package:nc_photos/use_case/inflate_file_descriptor.dart'; -import 'package:nc_photos/use_case/remove.dart'; import 'package:nc_photos/widget/album_share_outlier_browser.dart'; import 'package:nc_photos/widget/collection_picker.dart'; import 'package:nc_photos/widget/draggable_item_list.dart'; @@ -103,13 +101,14 @@ class CollectionBrowser extends StatelessWidget { @override Widget build(BuildContext context) { + final accountController = context.read(); return BlocProvider( create: (_) => _Bloc( container: KiwiContainer().resolve(), - account: context.read().account, + account: accountController.account, prefController: context.read(), - collectionsController: - context.read().collectionsController, + collectionsController: accountController.collectionsController, + filesController: accountController.filesController, collection: collection, ), child: const _WrappedCollectionBrowser(), @@ -211,14 +210,22 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser> } }, ), - _BlocListener( - listenWhen: (previous, current) => - previous.error != current.error, - listener: (context, state) { - if (state.error != null && isPageVisible()) { + _BlocListenerT( + selector: (state) => state.error, + listener: (context, error) { + if (error != null && isPageVisible()) { + final String content; + if (error.error is _ArchiveFailedError) { + content = L10n.global().archiveSelectedFailureNotification( + (error.error as _ArchiveFailedError).count); + } else if (error.error is _RemoveFailedError) { + content = L10n.global().deleteSelectedFailureNotification( + (error.error as _RemoveFailedError).count); + } else { + content = exception_util.toUserString(error.error); + } SnackBarManager().showSnackBar(SnackBar( - content: - Text(exception_util.toUserString(state.error!.error)), + content: Text(content), duration: k.snackBarDurationNormal, )); } @@ -395,6 +402,7 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser> typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; typedef _BlocListener = BlocListener<_Bloc, _State>; +typedef _BlocListenerT = BlocListenerT<_Bloc, _State, T>; // typedef _BlocSelector = BlocSelector<_Bloc, _State, T>; extension on BuildContext { diff --git a/app/lib/widget/collection_browser.g.dart b/app/lib/widget/collection_browser.g.dart index dc4ea01a..eeacd3b0 100644 --- a/app/lib/widget/collection_browser.g.dart +++ b/app/lib/widget/collection_browser.g.dart @@ -17,6 +17,8 @@ abstract class $_StateCopyWithWorker { {Collection? collection, String? coverUrl, List? items, + List? rawItems, + Set? itemsWhitelist, bool? isLoading, List<_Item>? transformedItems, Set<_Item>? selectedItems, @@ -45,6 +47,8 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { {dynamic collection, dynamic coverUrl = copyWithNull, dynamic items, + dynamic rawItems, + dynamic itemsWhitelist = copyWithNull, dynamic isLoading, dynamic transformedItems, dynamic selectedItems, @@ -68,6 +72,10 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { coverUrl: coverUrl == copyWithNull ? that.coverUrl : coverUrl as String?, items: items as List? ?? that.items, + rawItems: rawItems as List? ?? that.rawItems, + itemsWhitelist: itemsWhitelist == copyWithNull + ? that.itemsWhitelist + : itemsWhitelist as Set?, isLoading: isLoading as bool? ?? that.isLoading, transformedItems: transformedItems as List<_Item>? ?? that.transformedItems, @@ -143,7 +151,7 @@ extension _$_BlocNpLog on _Bloc { extension _$_StateToString on _State { String _$toString() { // ignore: unnecessary_string_interpolations - return "_State {collection: $collection, coverUrl: $coverUrl, items: [length: ${items.length}], isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, isSelectionRemovable: $isSelectionRemovable, isSelectionManageableFile: $isSelectionManageableFile, isSelectionDeletable: $isSelectionDeletable, isEditMode: $isEditMode, isEditBusy: $isEditBusy, editName: $editName, editItems: ${editItems == null ? null : "[length: ${editItems!.length}]"}, editTransformedItems: ${editTransformedItems == null ? null : "[length: ${editTransformedItems!.length}]"}, editSort: ${editSort == null ? null : "${editSort!.name}"}, isDragging: $isDragging, zoom: $zoom, scale: ${scale == null ? null : "${scale!.toStringAsFixed(3)}"}, importResult: $importResult, error: $error, message: $message}"; + return "_State {collection: $collection, coverUrl: $coverUrl, items: [length: ${items.length}], rawItems: [length: ${rawItems.length}], itemsWhitelist: ${itemsWhitelist == null ? null : "{length: ${itemsWhitelist!.length}}"}, isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, isSelectionRemovable: $isSelectionRemovable, isSelectionManageableFile: $isSelectionManageableFile, isSelectionDeletable: $isSelectionDeletable, isEditMode: $isEditMode, isEditBusy: $isEditBusy, editName: $editName, editItems: ${editItems == null ? null : "[length: ${editItems!.length}]"}, editTransformedItems: ${editTransformedItems == null ? null : "[length: ${editTransformedItems!.length}]"}, editSort: ${editSort == null ? null : "${editSort!.name}"}, isDragging: $isDragging, zoom: $zoom, scale: ${scale == null ? null : "${scale!.toStringAsFixed(3)}"}, importResult: $importResult, error: $error, message: $message}"; } } @@ -338,3 +346,17 @@ extension _$_SetMessageToString on _SetMessage { return "_SetMessage {message: $message}"; } } + +extension _$_ArchiveFailedErrorToString on _ArchiveFailedError { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_ArchiveFailedError {count: $count}"; + } +} + +extension _$_RemoveFailedErrorToString on _RemoveFailedError { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_RemoveFailedError {count: $count}"; + } +} diff --git a/app/lib/widget/collection_browser/bloc.dart b/app/lib/widget/collection_browser/bloc.dart index 4bdae8c0..64445168 100644 --- a/app/lib/widget/collection_browser/bloc.dart +++ b/app/lib/widget/collection_browser/bloc.dart @@ -7,6 +7,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { required this.account, required this.prefController, required this.collectionsController, + required this.filesController, required Collection collection, }) : _c = container, _isAdHocCollection = !collectionsController.stream.value.data @@ -109,20 +110,40 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { Future _onLoad(_LoadItems ev, Emitter<_State> emit) async { _log.info(ev); - return emit.forEach( - itemsController.stream, - onData: (data) => state.copyWith( - items: data.items, - isLoading: data.hasNext, + await Future.wait([ + emit.forEach( + itemsController.stream, + onData: (data) => state.copyWith( + items: _filterItems(data.items, state.itemsWhitelist), + rawItems: data.items, + isLoading: data.hasNext, + ), + onError: (e, stackTrace) { + _log.severe("[_onLoad] Uncaught exception", e, stackTrace); + return state.copyWith( + isLoading: false, + error: ExceptionEvent(e, stackTrace), + ); + }, ), - onError: (e, stackTrace) { - _log.severe("[_onLoad] Uncaught exception", e, stackTrace); - return state.copyWith( - isLoading: false, - error: ExceptionEvent(e, stackTrace), - ); - }, - ); + emit.forEach( + filesController.stream, + onData: (data) { + final whitelist = HashSet.of(data.dataMap.keys); + return state.copyWith( + items: _filterItems(state.rawItems, whitelist), + itemsWhitelist: whitelist, + ); + }, + onError: (e, stackTrace) { + _log.severe("[_onLoad] Uncaught exception", e, stackTrace); + return state.copyWith( + isLoading: false, + error: ExceptionEvent(e, stackTrace), + ); + }, + ), + ]); } void _onTransformItems(_TransformItems ev, Emitter<_State> emit) { @@ -306,23 +327,18 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { } } - Future _onArchiveSelectedItems( - _ArchiveSelectedItems ev, Emitter<_State> emit) async { + void _onArchiveSelectedItems(_ArchiveSelectedItems ev, Emitter<_State> emit) { _log.info(ev); final selected = state.selectedItems; _clearSelection(emit); - final selectedFds = + final selectedFiles = selected.whereType<_FileItem>().map((e) => e.file).toList(); - if (selectedFds.isNotEmpty) { - final selectedFiles = - await InflateFileDescriptor(_c)(account, selectedFds); - final count = await ArchiveFile(_c)(account, selectedFiles); - if (count != selectedFiles.length) { - emit(state.copyWith( - message: L10n.global() - .archiveSelectedFailureNotification(selectedFiles.length - count), - )); - } + if (selectedFiles.isNotEmpty) { + filesController.updateProperty( + selectedFiles, + isArchived: const OrNull(true), + errorBuilder: (fileIds) => _ArchiveFailedError(fileIds.length), + ); } } @@ -332,29 +348,25 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { final selected = state.selectedItems; _clearSelection(emit); final adapter = CollectionAdapter.of(_c, account, state.collection); + final selectedItems = selected + .whereType<_ActualItem>() + .map((e) => e.original) + .where(adapter.isItemRemovable) + .toList(); final selectedFiles = selected .whereType<_FileItem>() .where((e) => adapter.isItemDeletable(e.original)) .map((e) => e.file) .toList(); if (selectedFiles.isNotEmpty) { - final count = await Remove(_c)( - account, + await filesController.remove( selectedFiles, - onError: (_, f, e, stackTrace) { - _log.severe( - "[_onDeleteSelectedItems] Failed while Remove: ${logFilename(f.strippedPath)}", - e, - stackTrace, - ); + errorBuilder: (fileIds) { + return _RemoveFailedError(fileIds.length); }, ); - if (count != selectedFiles.length) { - emit(state.copyWith( - message: L10n.global() - .deleteSelectedFailureNotification(selectedFiles.length - count), - )); - } + // deleting files will also remove them from the collection + unawaited(itemsController.removeItems(selectedItems)); } } @@ -488,6 +500,20 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { ); } + List _filterItems( + List rawItems, Set? whitelist) { + if (whitelist == null) { + return rawItems; + } + return rawItems.where((e) { + if (e is CollectionFileItem) { + return whitelist.contains(e.file.fdId); + } else { + return true; + } + }).toList(); + } + String? _getCoverUrlByItems() { try { final firstFile = @@ -524,6 +550,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { final Account account; final PrefController prefController; final CollectionsController collectionsController; + final FilesController filesController; late final CollectionItemsController itemsController; /// Specify if the supplied [collection] is an "inline" one, which means it's diff --git a/app/lib/widget/collection_browser/state_event.dart b/app/lib/widget/collection_browser/state_event.dart index 06e82569..32e97dd1 100644 --- a/app/lib/widget/collection_browser/state_event.dart +++ b/app/lib/widget/collection_browser/state_event.dart @@ -7,6 +7,8 @@ class _State { required this.collection, this.coverUrl, required this.items, + required this.rawItems, + this.itemsWhitelist, required this.isLoading, required this.transformedItems, required this.selectedItems, @@ -36,6 +38,7 @@ class _State { collection: collection, coverUrl: coverUrl, items: const [], + rawItems: const [], isLoading: false, transformedItems: const [], selectedItems: const {}, @@ -57,6 +60,8 @@ class _State { final Collection collection; final String? coverUrl; final List items; + final List rawItems; + final Set? itemsWhitelist; final bool isLoading; final List<_Item> transformedItems; diff --git a/app/lib/widget/collection_browser/type.dart b/app/lib/widget/collection_browser/type.dart index 0706dd2d..0a29452a 100644 --- a/app/lib/widget/collection_browser/type.dart +++ b/app/lib/widget/collection_browser/type.dart @@ -174,3 +174,23 @@ class _DateItem extends _Item { final DateTime date; } + +@toString +class _ArchiveFailedError implements Exception { + const _ArchiveFailedError(this.count); + + @override + String toString() => _$toString(); + + final int count; +} + +@toString +class _RemoveFailedError implements Exception { + const _RemoveFailedError(this.count); + + @override + String toString() => _$toString(); + + final int count; +}