Migrate collections browser to use files controller

This commit is contained in:
Ming Ming 2024-01-13 12:32:16 +08:00
parent f1a340d550
commit db8f93b052
5 changed files with 137 additions and 55 deletions

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection';
import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:cached_network_image/cached_network_image.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/account_controller.dart';
import 'package:nc_photos/controller/collection_items_controller.dart'; import 'package:nc_photos/controller/collection_items_controller.dart';
import 'package:nc_photos/controller/collections_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/controller/pref_controller.dart';
import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/download_handler.dart'; import 'package:nc_photos/download_handler.dart';
import 'package:nc_photos/entity/collection.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/object_extension.dart';
import 'package:nc_photos/session_storage.dart'; import 'package:nc_photos/session_storage.dart';
import 'package:nc_photos/snack_bar_manager.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/album_share_outlier_browser.dart';
import 'package:nc_photos/widget/collection_picker.dart'; import 'package:nc_photos/widget/collection_picker.dart';
import 'package:nc_photos/widget/draggable_item_list.dart'; import 'package:nc_photos/widget/draggable_item_list.dart';
@ -103,13 +101,14 @@ class CollectionBrowser extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final accountController = context.read<AccountController>();
return BlocProvider( return BlocProvider(
create: (_) => _Bloc( create: (_) => _Bloc(
container: KiwiContainer().resolve(), container: KiwiContainer().resolve(),
account: context.read<AccountController>().account, account: accountController.account,
prefController: context.read(), prefController: context.read(),
collectionsController: collectionsController: accountController.collectionsController,
context.read<AccountController>().collectionsController, filesController: accountController.filesController,
collection: collection, collection: collection,
), ),
child: const _WrappedCollectionBrowser(), child: const _WrappedCollectionBrowser(),
@ -211,14 +210,22 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser>
} }
}, },
), ),
_BlocListener( _BlocListenerT<ExceptionEvent?>(
listenWhen: (previous, current) => selector: (state) => state.error,
previous.error != current.error, listener: (context, error) {
listener: (context, state) { if (error != null && isPageVisible()) {
if (state.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( SnackBarManager().showSnackBar(SnackBar(
content: content: Text(content),
Text(exception_util.toUserString(state.error!.error)),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
} }
@ -395,6 +402,7 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser>
typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
typedef _BlocListener = BlocListener<_Bloc, _State>; typedef _BlocListener = BlocListener<_Bloc, _State>;
typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
// typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>; // typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
extension on BuildContext { extension on BuildContext {

View file

@ -17,6 +17,8 @@ abstract class $_StateCopyWithWorker {
{Collection? collection, {Collection? collection,
String? coverUrl, String? coverUrl,
List<CollectionItem>? items, List<CollectionItem>? items,
List<CollectionItem>? rawItems,
Set<int>? itemsWhitelist,
bool? isLoading, bool? isLoading,
List<_Item>? transformedItems, List<_Item>? transformedItems,
Set<_Item>? selectedItems, Set<_Item>? selectedItems,
@ -45,6 +47,8 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
{dynamic collection, {dynamic collection,
dynamic coverUrl = copyWithNull, dynamic coverUrl = copyWithNull,
dynamic items, dynamic items,
dynamic rawItems,
dynamic itemsWhitelist = copyWithNull,
dynamic isLoading, dynamic isLoading,
dynamic transformedItems, dynamic transformedItems,
dynamic selectedItems, dynamic selectedItems,
@ -68,6 +72,10 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
coverUrl: coverUrl:
coverUrl == copyWithNull ? that.coverUrl : coverUrl as String?, coverUrl == copyWithNull ? that.coverUrl : coverUrl as String?,
items: items as List<CollectionItem>? ?? that.items, items: items as List<CollectionItem>? ?? that.items,
rawItems: rawItems as List<CollectionItem>? ?? that.rawItems,
itemsWhitelist: itemsWhitelist == copyWithNull
? that.itemsWhitelist
: itemsWhitelist as Set<int>?,
isLoading: isLoading as bool? ?? that.isLoading, isLoading: isLoading as bool? ?? that.isLoading,
transformedItems: transformedItems:
transformedItems as List<_Item>? ?? that.transformedItems, transformedItems as List<_Item>? ?? that.transformedItems,
@ -143,7 +151,7 @@ extension _$_BlocNpLog on _Bloc {
extension _$_StateToString on _State { extension _$_StateToString on _State {
String _$toString() { String _$toString() {
// ignore: unnecessary_string_interpolations // 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}"; 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}";
}
}

View file

@ -7,6 +7,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
required this.account, required this.account,
required this.prefController, required this.prefController,
required this.collectionsController, required this.collectionsController,
required this.filesController,
required Collection collection, required Collection collection,
}) : _c = container, }) : _c = container,
_isAdHocCollection = !collectionsController.stream.value.data _isAdHocCollection = !collectionsController.stream.value.data
@ -109,10 +110,12 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
Future<void> _onLoad(_LoadItems ev, Emitter<_State> emit) async { Future<void> _onLoad(_LoadItems ev, Emitter<_State> emit) async {
_log.info(ev); _log.info(ev);
return emit.forEach<CollectionItemStreamData>( await Future.wait([
emit.forEach<CollectionItemStreamData>(
itemsController.stream, itemsController.stream,
onData: (data) => state.copyWith( onData: (data) => state.copyWith(
items: data.items, items: _filterItems(data.items, state.itemsWhitelist),
rawItems: data.items,
isLoading: data.hasNext, isLoading: data.hasNext,
), ),
onError: (e, stackTrace) { onError: (e, stackTrace) {
@ -122,7 +125,25 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
error: ExceptionEvent(e, stackTrace), error: ExceptionEvent(e, stackTrace),
); );
}, },
),
emit.forEach<FilesStreamEvent>(
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) { void _onTransformItems(_TransformItems ev, Emitter<_State> emit) {
@ -306,23 +327,18 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
} }
} }
Future<void> _onArchiveSelectedItems( void _onArchiveSelectedItems(_ArchiveSelectedItems ev, Emitter<_State> emit) {
_ArchiveSelectedItems ev, Emitter<_State> emit) async {
_log.info(ev); _log.info(ev);
final selected = state.selectedItems; final selected = state.selectedItems;
_clearSelection(emit); _clearSelection(emit);
final selectedFds =
selected.whereType<_FileItem>().map((e) => e.file).toList();
if (selectedFds.isNotEmpty) {
final selectedFiles = final selectedFiles =
await InflateFileDescriptor(_c)(account, selectedFds); selected.whereType<_FileItem>().map((e) => e.file).toList();
final count = await ArchiveFile(_c)(account, selectedFiles); if (selectedFiles.isNotEmpty) {
if (count != selectedFiles.length) { filesController.updateProperty(
emit(state.copyWith( selectedFiles,
message: L10n.global() isArchived: const OrNull(true),
.archiveSelectedFailureNotification(selectedFiles.length - count), errorBuilder: (fileIds) => _ArchiveFailedError(fileIds.length),
)); );
}
} }
} }
@ -332,29 +348,25 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
final selected = state.selectedItems; final selected = state.selectedItems;
_clearSelection(emit); _clearSelection(emit);
final adapter = CollectionAdapter.of(_c, account, state.collection); final adapter = CollectionAdapter.of(_c, account, state.collection);
final selectedItems = selected
.whereType<_ActualItem>()
.map((e) => e.original)
.where(adapter.isItemRemovable)
.toList();
final selectedFiles = selected final selectedFiles = selected
.whereType<_FileItem>() .whereType<_FileItem>()
.where((e) => adapter.isItemDeletable(e.original)) .where((e) => adapter.isItemDeletable(e.original))
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
if (selectedFiles.isNotEmpty) { if (selectedFiles.isNotEmpty) {
final count = await Remove(_c)( await filesController.remove(
account,
selectedFiles, selectedFiles,
onError: (_, f, e, stackTrace) { errorBuilder: (fileIds) {
_log.severe( return _RemoveFailedError(fileIds.length);
"[_onDeleteSelectedItems] Failed while Remove: ${logFilename(f.strippedPath)}",
e,
stackTrace,
);
}, },
); );
if (count != selectedFiles.length) { // deleting files will also remove them from the collection
emit(state.copyWith( unawaited(itemsController.removeItems(selectedItems));
message: L10n.global()
.deleteSelectedFailureNotification(selectedFiles.length - count),
));
}
} }
} }
@ -488,6 +500,20 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
); );
} }
List<CollectionItem> _filterItems(
List<CollectionItem> rawItems, Set<int>? 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() { String? _getCoverUrlByItems() {
try { try {
final firstFile = final firstFile =
@ -524,6 +550,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
final Account account; final Account account;
final PrefController prefController; final PrefController prefController;
final CollectionsController collectionsController; final CollectionsController collectionsController;
final FilesController filesController;
late final CollectionItemsController itemsController; late final CollectionItemsController itemsController;
/// Specify if the supplied [collection] is an "inline" one, which means it's /// Specify if the supplied [collection] is an "inline" one, which means it's

View file

@ -7,6 +7,8 @@ class _State {
required this.collection, required this.collection,
this.coverUrl, this.coverUrl,
required this.items, required this.items,
required this.rawItems,
this.itemsWhitelist,
required this.isLoading, required this.isLoading,
required this.transformedItems, required this.transformedItems,
required this.selectedItems, required this.selectedItems,
@ -36,6 +38,7 @@ class _State {
collection: collection, collection: collection,
coverUrl: coverUrl, coverUrl: coverUrl,
items: const [], items: const [],
rawItems: const [],
isLoading: false, isLoading: false,
transformedItems: const [], transformedItems: const [],
selectedItems: const {}, selectedItems: const {},
@ -57,6 +60,8 @@ class _State {
final Collection collection; final Collection collection;
final String? coverUrl; final String? coverUrl;
final List<CollectionItem> items; final List<CollectionItem> items;
final List<CollectionItem> rawItems;
final Set<int>? itemsWhitelist;
final bool isLoading; final bool isLoading;
final List<_Item> transformedItems; final List<_Item> transformedItems;

View file

@ -174,3 +174,23 @@ class _DateItem extends _Item {
final DateTime date; 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;
}