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: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<AccountController>();
return BlocProvider(
create: (_) => _Bloc(
container: KiwiContainer().resolve(),
account: context.read<AccountController>().account,
account: accountController.account,
prefController: context.read(),
collectionsController:
context.read<AccountController>().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<ExceptionEvent?>(
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<T> = BlocListenerT<_Bloc, _State, T>;
// typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
extension on BuildContext {

View file

@ -17,6 +17,8 @@ abstract class $_StateCopyWithWorker {
{Collection? collection,
String? coverUrl,
List<CollectionItem>? items,
List<CollectionItem>? rawItems,
Set<int>? 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<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,
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}";
}
}

View file

@ -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<void> _onLoad(_LoadItems ev, Emitter<_State> emit) async {
_log.info(ev);
return emit.forEach<CollectionItemStreamData>(
itemsController.stream,
onData: (data) => state.copyWith(
items: data.items,
isLoading: data.hasNext,
await Future.wait([
emit.forEach<CollectionItemStreamData>(
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<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) {
@ -306,23 +327,18 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
}
}
Future<void> _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<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() {
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

View file

@ -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<CollectionItem> items;
final List<CollectionItem> rawItems;
final Set<int>? itemsWhitelist;
final bool isLoading;
final List<_Item> transformedItems;

View file

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