From 5b20ec4fff8e21684058ea5124b3d81b8e1967a1 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sat, 13 Jan 2024 02:12:32 +0800 Subject: [PATCH 01/42] Add new FilesController to improve file handling --- app/lib/app_init.dart | 6 + app/lib/controller/account_controller.dart | 11 + app/lib/controller/files_controller.dart | 309 +++++++++++++++++++++ app/lib/controller/files_controller.g.dart | 32 +++ app/lib/di_container.dart | 39 +++ app/lib/entity/file/data_source.dart | 2 +- app/lib/entity/file/data_source2.dart | 225 +++++++++++++++ app/lib/entity/file/data_source2.g.dart | 21 ++ app/lib/entity/file/repo.dart | 144 ++++++++++ app/lib/entity/file/repo.g.dart | 21 ++ app/lib/entity/file_util.dart | 6 + app/lib/use_case/file/list_file.dart | 12 + np_async/lib/src/future_util.dart | 4 + 13 files changed, 831 insertions(+), 1 deletion(-) create mode 100644 app/lib/controller/files_controller.dart create mode 100644 app/lib/controller/files_controller.g.dart create mode 100644 app/lib/entity/file/data_source2.dart create mode 100644 app/lib/entity/file/data_source2.g.dart create mode 100644 app/lib/entity/file/repo.dart create mode 100644 app/lib/entity/file/repo.g.dart create mode 100644 app/lib/use_case/file/list_file.dart diff --git a/app/lib/app_init.dart b/app/lib/app_init.dart index 7a2c13f3..04ec066c 100644 --- a/app/lib/app_init.dart +++ b/app/lib/app_init.dart @@ -14,6 +14,8 @@ import 'package:nc_photos/entity/favorite.dart'; import 'package:nc_photos/entity/favorite/data_source.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file/data_source.dart'; +import 'package:nc_photos/entity/file/data_source2.dart'; +import 'package:nc_photos/entity/file/repo.dart'; import 'package:nc_photos/entity/local_file.dart'; import 'package:nc_photos/entity/local_file/data_source.dart'; import 'package:nc_photos/entity/nc_album/data_source.dart'; @@ -147,6 +149,10 @@ Future _initDiContainer(InitIsolateType isolateType) async { c.fileRepo = FileRepo(FileCachedDataSource(c)); c.fileRepoRemote = const FileRepo(FileWebdavDataSource()); c.fileRepoLocal = FileRepo(FileSqliteDbDataSource(c)); + c.fileRepo2 = + CachedFileRepo(const FileRemoteDataSource(), FileNpDbDataSource(c.npDb)); + c.fileRepo2Remote = const BasicFileRepo(FileRemoteDataSource()); + c.fileRepo2Local = BasicFileRepo(FileNpDbDataSource(c.npDb)); c.shareRepo = ShareRepo(ShareRemoteDataSource()); c.shareeRepo = ShareeRepo(ShareeRemoteDataSource()); c.favoriteRepo = const FavoriteRepo(FavoriteRemoteDataSource()); diff --git a/app/lib/controller/account_controller.dart b/app/lib/controller/account_controller.dart index 314e2898..ecdcb1c2 100644 --- a/app/lib/controller/account_controller.dart +++ b/app/lib/controller/account_controller.dart @@ -2,6 +2,7 @@ import 'package:kiwi/kiwi.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/controller/account_pref_controller.dart'; import 'package:nc_photos/controller/collections_controller.dart'; +import 'package:nc_photos/controller/files_controller.dart'; import 'package:nc_photos/controller/persons_controller.dart'; import 'package:nc_photos/controller/places_controller.dart'; import 'package:nc_photos/controller/server_controller.dart'; @@ -29,6 +30,8 @@ class AccountController { _sharingsController = null; _placesController?.dispose(); _placesController = null; + _filesController?.dispose(); + _filesController = null; } Account get account => _account!; @@ -76,6 +79,13 @@ class AccountController { account: _account!, ); + FilesController get filesController => + _filesController ??= FilesController( + KiwiContainer().resolve(), + account: _account!, + accountPrefController: accountPrefController, + ); + Account? _account; CollectionsController? _collectionsController; ServerController? _serverController; @@ -85,4 +95,5 @@ class AccountController { SessionController? _sessionController; SharingsController? _sharingsController; PlacesController? _placesController; + FilesController? _filesController; } diff --git a/app/lib/controller/files_controller.dart b/app/lib/controller/files_controller.dart new file mode 100644 index 00000000..9ef308ea --- /dev/null +++ b/app/lib/controller/files_controller.dart @@ -0,0 +1,309 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:logging/logging.dart'; +import 'package:mutex/mutex.dart'; +import 'package:nc_photos/account.dart'; +import 'package:nc_photos/controller/account_pref_controller.dart'; +import 'package:nc_photos/debug_util.dart'; +import 'package:nc_photos/di_container.dart'; +import 'package:nc_photos/entity/file.dart'; +import 'package:nc_photos/entity/file_descriptor.dart'; +import 'package:nc_photos/entity/file_util.dart' as file_util; +import 'package:nc_photos/rx_extension.dart'; +import 'package:nc_photos/use_case/file/list_file.dart'; +import 'package:nc_photos/use_case/remove.dart'; +import 'package:nc_photos/use_case/update_property.dart'; +import 'package:np_codegen/np_codegen.dart'; +import 'package:np_common/lazy.dart'; +import 'package:np_common/object_util.dart'; +import 'package:np_common/or_null.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:to_string/to_string.dart'; + +part 'files_controller.g.dart'; + +abstract class FilesStreamEvent { + /// All files as a ordered list + List get data; + + /// All files as a map with the fileId as key + Map get dataMap; + bool get hasNext; +} + +@npLog +class FilesController { + FilesController( + this._c, { + required this.account, + required this.accountPrefController, + }); + + void dispose() { + _dataStreamController.close(); + } + + /// Return a stream of files associated with [account] + /// + /// The returned stream will emit new list of files whenever there are + /// changes to the files (e.g., new file, removed file, etc) + /// + /// There's no guarantee that the returned list is always sorted in some ways, + /// callers must sort it by themselves if the ordering is important + ValueStream get stream { + if (!_isDataStreamInited) { + _isDataStreamInited = true; + unawaited(_load()); + } + return _dataStreamController.stream; + } + + Future reload() async { + var results = []; + final completer = Completer(); + ListFile(_c)( + account, + file_util.unstripPath(account, accountPrefController.shareFolder.value), + ).listen( + (ev) { + results = ev; + }, + onError: _dataStreamController.addError, + onDone: () => completer.complete(), + ); + await completer.future; + _dataStreamController + .add(_convertListResultsToEvent(results, hasNext: false)); + } + + /// Update files property and return number of files updated + Future updateProperty( + List files, { + OrNull? metadata, + OrNull? isArchived, + OrNull? overrideDateTime, + bool? isFavorite, + OrNull? location, + Exception? Function(List fileIds) errorBuilder = + UpdatePropertyFailureError.new, + }) async { + final backups = {}; + // file ids that need to be queried again to get the correct + // FileDescriptor.fdDateTime + final outdated = []; + await _mutex.protect(() async { + final next = Map.of(_dataStreamController.value.files); + for (final f in files) { + final original = next[f.fdId]; + if (original == null) { + _log.warning("[updateProperty] File not found: $f"); + continue; + } + backups[f.fdId] = original; + if (original is File) { + next[f.fdId] = original.copyWith( + metadata: metadata, + isArchived: isArchived, + overrideDateTime: overrideDateTime, + isFavorite: isFavorite, + location: location, + ); + } else { + next[f.fdId] = original.copyWith( + fdIsArchived: isArchived == null ? null : (isArchived.obj ?? false), + // in case of unsetting, we can't work out the new value here + fdDateTime: overrideDateTime?.obj, + fdIsFavorite: isFavorite, + ); + if (OrNull.isSetNull(overrideDateTime)) { + outdated.add(f.fdId); + } + } + } + _dataStreamController + .addWithValue((value) => value.copyWith(files: next)); + }); + final failures = []; + for (final f in files) { + try { + await UpdateProperty(_c)( + account, + f, + metadata: metadata, + isArchived: isArchived, + overrideDateTime: overrideDateTime, + favorite: isFavorite, + location: location, + ); + } catch (e, stackTrace) { + _log.severe("Failed while UpdateProperty: ${logFilename(f.fdPath)}", e, + stackTrace); + failures.add(f.fdId); + outdated.remove(f.fdId); + } + } + if (failures.isNotEmpty) { + // restore + final next = Map.of(_dataStreamController.value.files); + for (final f in failures) { + if (backups.containsKey(f)) { + next[f] = backups[f]!; + } + } + _dataStreamController + .addWithValue((value) => value.copyWith(files: next)); + errorBuilder(failures)?.let(_dataStreamController.addError); + } + // TODO query outdated + } + + Future remove( + List files, { + Exception? Function(List fileIds) errorBuilder = + RemoveFailureError.new, + }) async { + final backups = {}; + await _mutex.protect(() async { + final next = Map.of(_dataStreamController.value.files); + for (final f in files) { + final original = next.remove(f.fdId); + if (original == null) { + _log.warning("[updateProperty] File not found: $f"); + continue; + } + backups[f.fdId] = original; + } + _dataStreamController + .addWithValue((value) => value.copyWith(files: next)); + }); + final failures = []; + try { + await Remove(_c)( + account, + files, + onError: (index, value, error, stackTrace) { + _log.severe("Failed while Remove: ${logFilename(value.fdPath)}", + error, stackTrace); + failures.add(value.fdId); + }, + ); + } catch (e, stackTrace) { + _log.severe("Failed while Remove", e, stackTrace); + failures.addAll(files.map((e) => e.fdId)); + } + if (failures.isNotEmpty) { + // restore + final next = LinkedHashMap.of(_dataStreamController.value.files); + for (final f in failures) { + if (backups.containsKey(f)) { + next[f] = backups[f]!; + } + } + _dataStreamController + .addWithValue((value) => value.copyWith(files: next)); + errorBuilder(failures)?.let(_dataStreamController.addError); + } + } + + Future _load() async { + var lastData = _FilesStreamEvent( + files: const {}, + hasNext: false, + ); + final completer = Completer(); + ListFile(_c)( + account, + file_util.unstripPath(account, accountPrefController.shareFolder.value), + ).listen( + (ev) { + lastData = _convertListResultsToEvent(ev, hasNext: true); + _dataStreamController.add(lastData); + }, + onError: _dataStreamController.addError, + onDone: () => completer.complete(), + ); + await completer.future; + _dataStreamController.add(lastData.copyWith(hasNext: false)); + } + + _FilesStreamEvent _convertListResultsToEvent( + List results, { + required bool hasNext, + }) { + return _FilesStreamEvent( + files: { + for (final f in results) f.fdId: f, + }, + hasNext: hasNext, + ); + } + + final DiContainer _c; + final Account account; + final AccountPrefController accountPrefController; + + var _isDataStreamInited = false; + final _dataStreamController = BehaviorSubject.seeded( + _FilesStreamEvent( + files: const {}, + hasNext: true, + ), + ); + + final _mutex = Mutex(); +} + +@toString +class UpdatePropertyFailureError implements Exception { + const UpdatePropertyFailureError(this.fileIds); + + @override + String toString() => _$toString(); + + final List fileIds; +} + +@toString +class RemoveFailureError implements Exception { + const RemoveFailureError(this.fileIds); + + @override + String toString() => _$toString(); + + final List fileIds; +} + +class _FilesStreamEvent implements FilesStreamEvent { + _FilesStreamEvent({ + required this.files, + Lazy>? dataLazy, + required this.hasNext, + }) { + this.dataLazy = dataLazy ?? (Lazy(() => files.values.toList())); + } + + _FilesStreamEvent copyWith({ + Map? files, + bool? hasNext, + }) { + return _FilesStreamEvent( + files: files ?? this.files, + dataLazy: (files == null) ? dataLazy : null, + hasNext: hasNext ?? this.hasNext, + ); + } + + @override + List get data => dataLazy(); + @override + Map get dataMap => files; + + final Map files; + late final Lazy> dataLazy; + + /// If true, the results are intermediate values and may not represent the + /// latest state + @override + final bool hasNext; +} diff --git a/app/lib/controller/files_controller.g.dart b/app/lib/controller/files_controller.g.dart new file mode 100644 index 00000000..05f790f6 --- /dev/null +++ b/app/lib/controller/files_controller.g.dart @@ -0,0 +1,32 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'files_controller.dart'; + +// ************************************************************************** +// NpLogGenerator +// ************************************************************************** + +extension _$FilesControllerNpLog on FilesController { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("controller.files_controller.FilesController"); +} + +// ************************************************************************** +// ToStringGenerator +// ************************************************************************** + +extension _$UpdatePropertyFailureErrorToString on UpdatePropertyFailureError { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "UpdatePropertyFailureError {fileIds: [length: ${fileIds.length}]}"; + } +} + +extension _$RemoveFailureErrorToString on RemoveFailureError { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "RemoveFailureError {fileIds: [length: ${fileIds.length}]}"; + } +} diff --git a/app/lib/di_container.dart b/app/lib/di_container.dart index dc4248fa..8e517167 100644 --- a/app/lib/di_container.dart +++ b/app/lib/di_container.dart @@ -3,6 +3,7 @@ import 'package:nc_photos/entity/album/repo2.dart'; import 'package:nc_photos/entity/face_recognition_person/repo.dart'; import 'package:nc_photos/entity/favorite.dart'; import 'package:nc_photos/entity/file.dart'; +import 'package:nc_photos/entity/file/repo.dart'; import 'package:nc_photos/entity/local_file.dart'; import 'package:nc_photos/entity/nc_album/repo.dart'; import 'package:nc_photos/entity/pref.dart'; @@ -26,6 +27,9 @@ enum DiType { fileRepo, fileRepoRemote, fileRepoLocal, + fileRepo2, + fileRepo2Remote, + fileRepo2Local, shareRepo, shareeRepo, favoriteRepo, @@ -60,6 +64,9 @@ class DiContainer { FileRepo? fileRepo, FileRepo? fileRepoRemote, FileRepo? fileRepoLocal, + FileRepo2? fileRepo2, + FileRepo2? fileRepo2Remote, + FileRepo2? fileRepo2Local, ShareRepo? shareRepo, ShareeRepo? shareeRepo, FavoriteRepo? favoriteRepo, @@ -90,6 +97,9 @@ class DiContainer { _fileRepo = fileRepo, _fileRepoRemote = fileRepoRemote, _fileRepoLocal = fileRepoLocal, + _fileRepo2 = fileRepo2, + _fileRepo2Remote = fileRepo2Remote, + _fileRepo2Local = fileRepo2Local, _shareRepo = shareRepo, _shareeRepo = shareeRepo, _favoriteRepo = favoriteRepo, @@ -134,6 +144,12 @@ class DiContainer { return contianer._fileRepoRemote != null; case DiType.fileRepoLocal: return contianer._fileRepoLocal != null; + case DiType.fileRepo2: + return contianer._fileRepo2 != null; + case DiType.fileRepo2Remote: + return contianer._fileRepo2Remote != null; + case DiType.fileRepo2Local: + return contianer._fileRepo2Local != null; case DiType.shareRepo: return contianer._shareRepo != null; case DiType.shareeRepo: @@ -183,6 +199,7 @@ class DiContainer { OrNull? albumRepo, OrNull? albumRepo2, OrNull? fileRepo, + OrNull? fileRepo2, OrNull? shareRepo, OrNull? shareeRepo, OrNull? favoriteRepo, @@ -201,6 +218,7 @@ class DiContainer { albumRepo: albumRepo == null ? _albumRepo : albumRepo.obj, albumRepo2: albumRepo2 == null ? _albumRepo2 : albumRepo2.obj, fileRepo: fileRepo == null ? _fileRepo : fileRepo.obj, + fileRepo2: fileRepo2 == null ? _fileRepo2 : fileRepo2.obj, shareRepo: shareRepo == null ? _shareRepo : shareRepo.obj, shareeRepo: shareeRepo == null ? _shareeRepo : shareeRepo.obj, favoriteRepo: favoriteRepo == null ? _favoriteRepo : favoriteRepo.obj, @@ -231,6 +249,9 @@ class DiContainer { FileRepo get fileRepo => _fileRepo!; FileRepo get fileRepoRemote => _fileRepoRemote!; FileRepo get fileRepoLocal => _fileRepoLocal!; + FileRepo2 get fileRepo2 => _fileRepo2!; + FileRepo2 get fileRepo2Remote => _fileRepo2Remote!; + FileRepo2 get fileRepo2Local => _fileRepo2Local!; ShareRepo get shareRepo => _shareRepo!; ShareeRepo get shareeRepo => _shareeRepo!; FavoriteRepo get favoriteRepo => _favoriteRepo!; @@ -302,6 +323,21 @@ class DiContainer { _fileRepoLocal = v; } + set fileRepo2(FileRepo2 v) { + assert(_fileRepo2 == null); + _fileRepo2 = v; + } + + set fileRepo2Remote(FileRepo2 v) { + assert(_fileRepo2Remote == null); + _fileRepo2Remote = v; + } + + set fileRepo2Local(FileRepo2 v) { + assert(_fileRepo2Local == null); + _fileRepo2Local = v; + } + set shareRepo(ShareRepo v) { assert(_shareRepo == null); _shareRepo = v; @@ -419,6 +455,9 @@ class DiContainer { FileRepo? _fileRepoRemote; // Explicitly request a FileRepo backed by local source FileRepo? _fileRepoLocal; + FileRepo2? _fileRepo2; + FileRepo2? _fileRepo2Remote; + FileRepo2? _fileRepo2Local; ShareRepo? _shareRepo; ShareeRepo? _shareeRepo; FavoriteRepo? _favoriteRepo; diff --git a/app/lib/entity/file/data_source.dart b/app/lib/entity/file/data_source.dart index f9c7f00d..ecec9b97 100644 --- a/app/lib/entity/file/data_source.dart +++ b/app/lib/entity/file/data_source.dart @@ -420,7 +420,7 @@ class FileSqliteDbDataSource implements FileDataSource { @override remove(Account account, FileDescriptor f) { _log.info("[remove] ${f.fdPath}"); - return FileSqliteCacheRemover(_c)(account, f); + return _c.fileRepo2.remove(account, f); } @override diff --git a/app/lib/entity/file/data_source2.dart b/app/lib/entity/file/data_source2.dart new file mode 100644 index 00000000..c1ee2b9e --- /dev/null +++ b/app/lib/entity/file/data_source2.dart @@ -0,0 +1,225 @@ +import 'dart:convert'; + +import 'package:logging/logging.dart'; +import 'package:nc_photos/account.dart'; +import 'package:nc_photos/db/entity_converter.dart'; +import 'package:nc_photos/debug_util.dart'; +import 'package:nc_photos/entity/file.dart'; +import 'package:nc_photos/entity/file/repo.dart'; +import 'package:nc_photos/entity/file_descriptor.dart'; +import 'package:nc_photos/entity/file_util.dart' as file_util; +import 'package:nc_photos/exception.dart'; +import 'package:nc_photos/np_api_util.dart'; +import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util; +import 'package:np_async/np_async.dart'; +import 'package:np_codegen/np_codegen.dart'; +import 'package:np_common/object_util.dart'; +import 'package:np_common/or_null.dart'; +import 'package:np_db/np_db.dart'; + +part 'data_source2.g.dart'; + +@npLog +class FileRemoteDataSource implements FileDataSource2 { + const FileRemoteDataSource(); + + @override + Stream> getFileDescriptors( + Account account, String shareDirPath) { + throw UnsupportedError("getFileDescriptors not supported"); + } + + @override + Future updateProperty( + Account account, + FileDescriptor f, { + OrNull? metadata, + OrNull? isArchived, + OrNull? overrideDateTime, + bool? favorite, + OrNull? location, + }) async { + _log.info("[updateProperty] ${f.fdPath}"); + if (f is File && + metadata?.obj != null && + metadata!.obj!.fileEtag != f.etag) { + _log.warning( + "[updateProperty] Metadata etag mismatch (metadata: ${metadata.obj!.fileEtag}, file: ${f.etag})"); + } + final setProps = { + if (metadata?.obj != null) + "app:metadata": jsonEncode(metadata!.obj!.toJson()), + if (isArchived?.obj != null) "app:is-archived": isArchived!.obj, + if (overrideDateTime?.obj != null) + "app:override-date-time": + overrideDateTime!.obj!.toUtc().toIso8601String(), + if (favorite != null) "oc:favorite": favorite ? 1 : 0, + if (location?.obj != null) + "app:location": jsonEncode(location!.obj!.toJson()), + }; + final removeProps = [ + if (OrNull.isSetNull(metadata)) "app:metadata", + if (OrNull.isSetNull(isArchived)) "app:is-archived", + if (OrNull.isSetNull(overrideDateTime)) "app:override-date-time", + if (OrNull.isSetNull(location)) "app:location", + ]; + final response = await ApiUtil.fromAccount(account).files().proppatch( + path: f.fdPath, + namespaces: { + "com.nkming.nc_photos": "app", + "http://owncloud.org/ns": "oc", + }, + set: setProps.isNotEmpty ? setProps : null, + remove: removeProps.isNotEmpty ? removeProps : null, + ); + if (!response.isGood) { + _log.severe("[updateProperty] Failed requesting server: $response"); + throw ApiException( + response: response, + message: "Server responed with an error: HTTP ${response.statusCode}", + ); + } + } + + @override + Future remove(Account account, FileDescriptor f) async { + _log.info("[remove] ${f.fdPath}"); + final response = + await ApiUtil.fromAccount(account).files().delete(path: f.fdPath); + if (!response.isGood) { + _log.severe("[remove] Failed requesting server: $response"); + throw ApiException( + response: response, + message: "Server responed with an error: HTTP ${response.statusCode}", + ); + } + } +} + +@npLog +class FileNpDbDataSource implements FileDataSource2 { + const FileNpDbDataSource(this.db); + + @override + Stream> getFileDescriptors( + Account account, String shareDirPath) async* { + _log.info("[getFileDescriptors] $account"); + yield await _getPartialFileDescriptors(account); + yield await _getCompleteFileDescriptors(account, shareDirPath); + } + + @override + Future updateProperty( + Account account, + FileDescriptor f, { + OrNull? metadata, + OrNull? isArchived, + OrNull? overrideDateTime, + bool? favorite, + OrNull? location, + }) async { + _log.info("[updateProperty] ${f.fdPath}"); + if (overrideDateTime != null || metadata != null) { + f = DbFileConverter.fromDb( + account.userId.toCaseInsensitiveString(), + await db.getFilesByFileIds( + account: account.toDb(), + fileIds: [f.fdId], + ).first, + ); + } + await db.updateFileByFileId( + account: account.toDb(), + fileId: f.fdId, + isFavorite: favorite?.let(OrNull.new), + isArchived: isArchived, + overrideDateTime: overrideDateTime, + bestDateTime: overrideDateTime == null && metadata == null + ? null + : file_util.getBestDateTime( + overrideDateTime: overrideDateTime == null + ? (f as File).overrideDateTime + : overrideDateTime.obj, + dateTimeOriginal: metadata == null + ? (f as File).metadata?.exif?.dateTimeOriginal + : metadata.obj?.exif?.dateTimeOriginal, + lastModified: (f as File).lastModified, + ), + imageData: metadata?.let((e) => OrNull(e.obj?.toDb())), + location: location?.let((e) => OrNull(e.obj?.toDb())), + ); + } + + @override + Future remove(Account account, FileDescriptor f) async { + _log.info("[remove] ${f.fdPath}"); + await db.deleteFile( + account: account.toDb(), + file: f.toDbKey(), + ); + } + + Future> _getPartialFileDescriptors( + Account account) async { + _log.info("[_getPartialFileDescriptors] $account"); + final results = await db.getFileDescriptors( + account: account.toDb(), + // need this because this arg expect empty string for root instead of "." + includeRelativeRoots: account.roots + .map((e) => File(path: file_util.unstripPath(account, e)) + .strippedPathWithEmpty) + .toList(), + excludeRelativeRoots: [remote_storage_util.remoteStorageDirRelativePath], + mimes: file_util.supportedFormatMimes, + limit: _partialCount, + ); + return results + .map((e) => + DbFileDescriptorConverter.fromDb(account.userId.toString(), e)) + .toList(); + } + + Future> _getCompleteFileDescriptors( + Account account, String shareDirPath) async { + _log.info("[_getCompleteFileDescriptors] $account"); + final dbResults = await db.getFileDescriptors( + account: account.toDb(), + includeRelativeRoots: account.roots + .map((e) => File(path: file_util.unstripPath(account, e)) + .strippedPathWithEmpty) + .toList(), + excludeRelativeRoots: [remote_storage_util.remoteStorageDirRelativePath], + mimes: file_util.supportedFormatMimes, + ); + final results = dbResults + .map((e) => DbFileDescriptorConverter.fromDb( + account.userId.toCaseInsensitiveString(), e)) + .toList(); + final isShareDirIncluded = account.roots.any((e) => file_util + .isOrUnderDirPath(shareDirPath, file_util.unstripPath(account, e))); + if (!isShareDirIncluded) { + _log.info( + "[_getCompleteFileDescriptors] Explicitly getting share folder"); + try { + final shareDirResults = await db.getFilesByDirKey( + account: account.toDb(), + dir: File(path: shareDirPath).toDbKey(), + ); + results.addAll(shareDirResults.map((e) => DbFileConverter.fromDb( + account.userId.toCaseInsensitiveString(), e))); + } on DbNotFoundException catch (_) { + // normal when there's no cache + } catch (e, stackTrace) { + _log.shout( + "[_getCompleteFileDescriptors] Failed while getFilesByDirKey: ${logFilename(shareDirPath)}", + e, + stackTrace); + } + } + return results; + } + + final NpDb db; + + static const _partialCount = 100; +} diff --git a/app/lib/entity/file/data_source2.g.dart b/app/lib/entity/file/data_source2.g.dart new file mode 100644 index 00000000..c21a5ca5 --- /dev/null +++ b/app/lib/entity/file/data_source2.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'data_source2.dart'; + +// ************************************************************************** +// NpLogGenerator +// ************************************************************************** + +extension _$FileRemoteDataSourceNpLog on FileRemoteDataSource { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("entity.file.data_source2.FileRemoteDataSource"); +} + +extension _$FileNpDbDataSourceNpLog on FileNpDbDataSource { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("entity.file.data_source2.FileNpDbDataSource"); +} diff --git a/app/lib/entity/file/repo.dart b/app/lib/entity/file/repo.dart new file mode 100644 index 00000000..6ccdc9c1 --- /dev/null +++ b/app/lib/entity/file/repo.dart @@ -0,0 +1,144 @@ +import 'package:logging/logging.dart'; +import 'package:nc_photos/account.dart'; +import 'package:nc_photos/entity/file.dart'; +import 'package:nc_photos/entity/file_descriptor.dart'; +import 'package:np_codegen/np_codegen.dart'; +import 'package:np_common/or_null.dart'; + +part 'repo.g.dart'; + +abstract class FileRepo2 { + /// Query all files belonging to [account] + /// + /// Normally the stream should complete with only a single event, but some + /// implementation might want to return multiple set of values, say one set of + /// cached value and later another set of updated value from a remote source. + /// In any case, each event is guaranteed to be one complete set of data + Stream> getFileDescriptors( + Account account, String shareDirPath); + + Future updateProperty( + Account account, + FileDescriptor f, { + OrNull? metadata, + OrNull? isArchived, + OrNull? overrideDateTime, + bool? favorite, + OrNull? location, + }); + + Future remove(Account account, FileDescriptor f); +} + +/// A repo that simply relay the call to the backed [FileDataSource] +@npLog +class BasicFileRepo implements FileRepo2 { + const BasicFileRepo(this.dataSrc); + + @override + Stream> getFileDescriptors( + Account account, String shareDirPath) => + dataSrc.getFileDescriptors(account, shareDirPath); + + @override + Future updateProperty( + Account account, + FileDescriptor f, { + OrNull? metadata, + OrNull? isArchived, + OrNull? overrideDateTime, + bool? favorite, + OrNull? location, + }) => + dataSrc.updateProperty( + account, + f, + metadata: metadata, + isArchived: isArchived, + overrideDateTime: overrideDateTime, + favorite: favorite, + location: location, + ); + + @override + Future remove(Account account, FileDescriptor f) => + dataSrc.remove(account, f); + + final FileDataSource2 dataSrc; +} + +/// A repo that manage a remote data source and a cache data source +@npLog +class CachedFileRepo implements FileRepo2 { + const CachedFileRepo(this.remoteDataSrc, this.cacheDataSrc); + + @override + Stream> getFileDescriptors( + Account account, String shareDirPath) => + cacheDataSrc.getFileDescriptors(account, shareDirPath); + + @override + Future updateProperty( + Account account, + FileDescriptor f, { + OrNull? metadata, + OrNull? isArchived, + OrNull? overrideDateTime, + bool? favorite, + OrNull? location, + }) async { + await remoteDataSrc.updateProperty( + account, + f, + metadata: metadata, + isArchived: isArchived, + overrideDateTime: overrideDateTime, + favorite: favorite, + location: location, + ); + try { + await cacheDataSrc.updateProperty( + account, + f, + metadata: metadata, + isArchived: isArchived, + overrideDateTime: overrideDateTime, + favorite: favorite, + location: location, + ); + } catch (e, stackTrace) { + _log.warning("[updateProperty] Failed to update cache", e, stackTrace); + } + } + + @override + Future remove(Account account, FileDescriptor f) async { + await remoteDataSrc.remove(account, f); + try { + await cacheDataSrc.remove(account, f); + } catch (e, stackTrace) { + _log.warning("[remove] Failed to update cache", e, stackTrace); + } + } + + final FileDataSource2 remoteDataSrc; + final FileDataSource2 cacheDataSrc; +} + +abstract class FileDataSource2 { + /// Query all files belonging to [account] + Stream> getFileDescriptors( + Account account, String shareDirPath); + + Future updateProperty( + Account account, + FileDescriptor f, { + OrNull? metadata, + OrNull? isArchived, + OrNull? overrideDateTime, + bool? favorite, + OrNull? location, + }); + + Future remove(Account account, FileDescriptor f); +} diff --git a/app/lib/entity/file/repo.g.dart b/app/lib/entity/file/repo.g.dart new file mode 100644 index 00000000..3e237627 --- /dev/null +++ b/app/lib/entity/file/repo.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'repo.dart'; + +// ************************************************************************** +// NpLogGenerator +// ************************************************************************** + +extension _$BasicFileRepoNpLog on BasicFileRepo { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("entity.file.repo.BasicFileRepo"); +} + +extension _$CachedFileRepoNpLog on CachedFileRepo { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("entity.file.repo.CachedFileRepo"); +} diff --git a/app/lib/entity/file_util.dart b/app/lib/entity/file_util.dart index 04f088b8..9f36e167 100644 --- a/app/lib/entity/file_util.dart +++ b/app/lib/entity/file_util.dart @@ -49,12 +49,18 @@ bool isNcAlbumFile(Account account, FileDescriptor file) => bool isUnderDir(FileDescriptor file, FileDescriptor dir) => file.fdPath.startsWith("${dir.fdPath}/"); +bool isUnderDirPath(String filePath, String dirPath) => + filePath.startsWith("$dirPath/"); + /// Return if [file] is [dir] or located under [dir] /// /// See [isUnderDir] bool isOrUnderDir(FileDescriptor file, FileDescriptor dir) => file.fdPath == dir.fdPath || isUnderDir(file, dir); +bool isOrUnderDirPath(String filePath, String dirPath) => + filePath == dirPath || isUnderDirPath(filePath, dirPath); + /// Convert a stripped path to a full path /// /// See [File.strippedPath] diff --git a/app/lib/use_case/file/list_file.dart b/app/lib/use_case/file/list_file.dart new file mode 100644 index 00000000..72753222 --- /dev/null +++ b/app/lib/use_case/file/list_file.dart @@ -0,0 +1,12 @@ +import 'package:nc_photos/account.dart'; +import 'package:nc_photos/di_container.dart'; +import 'package:nc_photos/entity/file_descriptor.dart'; + +class ListFile { + const ListFile(this._c); + + Stream> call(Account account, String shareDirPath) => + _c.fileRepo2.getFileDescriptors(account, shareDirPath); + + final DiContainer _c; +} diff --git a/np_async/lib/src/future_util.dart b/np_async/lib/src/future_util.dart index 7cd6d1c2..f08ec474 100644 --- a/np_async/lib/src/future_util.dart +++ b/np_async/lib/src/future_util.dart @@ -6,6 +6,10 @@ extension FutureNotNullExtension on Future { Future notNull() async => (await this)!; } +extension FutureCollectionExtension on Future> { + Future get first async => (await this).first; +} + Future> waitOr( Iterable> futures, T Function(Object error, StackTrace? stackTrace) onError, From 76292518865a81dc6d8f6e8408e19d85cb6bd9a0 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sat, 13 Jan 2024 02:18:23 +0800 Subject: [PATCH 02/42] Migrate UpdateProperty to controller --- app/lib/event/event.dart | 2 +- app/lib/metadata_task_manager.dart | 8 ++--- app/lib/service.dart | 4 +-- app/lib/use_case/archive_file.dart | 2 +- app/lib/use_case/update_missing_metadata.dart | 11 +++--- app/lib/use_case/update_property.dart | 35 ++++++++++++++----- app/lib/widget/archive_browser.dart | 2 +- .../handler/archive_selection_handler.dart | 2 +- .../handler/unarchive_selection_handler.dart | 3 +- app/lib/widget/viewer.dart | 4 +-- app/lib/widget/viewer_detail_pane.dart | 2 +- 11 files changed, 47 insertions(+), 28 deletions(-) diff --git a/app/lib/event/event.dart b/app/lib/event/event.dart index bbcd6f93..5fa710c8 100644 --- a/app/lib/event/event.dart +++ b/app/lib/event/event.dart @@ -49,7 +49,7 @@ class FilePropertyUpdatedEvent { FilePropertyUpdatedEvent(this.account, this.file, this.properties); final Account account; - final File file; + final FileDescriptor file; final int properties; // Bit masks for properties field diff --git a/app/lib/metadata_task_manager.dart b/app/lib/metadata_task_manager.dart index 108b091b..ed87305f 100644 --- a/app/lib/metadata_task_manager.dart +++ b/app/lib/metadata_task_manager.dart @@ -37,8 +37,8 @@ class MetadataTask { for (final r in account.roots) { final dir = File(path: file_util.unstripPath(account, r)); hasScanShareFolder |= file_util.isOrUnderDir(shareFolder, dir); - final op = UpdateMissingMetadata(_c.fileRepo, - const _UpdateMissingMetadataConfigProvider(), geocoder); + final op = UpdateMissingMetadata( + _c, const _UpdateMissingMetadataConfigProvider(), geocoder); await for (final _ in op(account, dir)) { if (!Pref().isEnableExifOr()) { _log.info("[call] EXIF disabled, task ending immaturely"); @@ -48,8 +48,8 @@ class MetadataTask { } } if (!hasScanShareFolder) { - final op = UpdateMissingMetadata(_c.fileRepo, - const _UpdateMissingMetadataConfigProvider(), geocoder); + final op = UpdateMissingMetadata( + _c, const _UpdateMissingMetadataConfigProvider(), geocoder); await for (final _ in op( account, shareFolder, diff --git a/app/lib/service.dart b/app/lib/service.dart index 24509cbf..5f7e6ee4 100644 --- a/app/lib/service.dart +++ b/app/lib/service.dart @@ -260,7 +260,7 @@ class _MetadataTask { final dir = File(path: file_util.unstripPath(account, r)); hasScanShareFolder |= file_util.isOrUnderDir(shareFolder, dir); final updater = UpdateMissingMetadata( - c.fileRepo, const _UpdateMissingMetadataConfigProvider(), geocoder); + c, const _UpdateMissingMetadataConfigProvider(), geocoder); void onServiceStop() { _log.info("[_updateMetadata] Stopping task: user canceled"); updater.stop(); @@ -283,7 +283,7 @@ class _MetadataTask { } if (!hasScanShareFolder) { final shareUpdater = UpdateMissingMetadata( - c.fileRepo, const _UpdateMissingMetadataConfigProvider(), geocoder); + c, const _UpdateMissingMetadataConfigProvider(), geocoder); void onServiceStop() { _log.info("[_updateMetadata] Stopping task: user canceled"); shareUpdater.stop(); diff --git a/app/lib/use_case/archive_file.dart b/app/lib/use_case/archive_file.dart index 8498341f..f8acbe95 100644 --- a/app/lib/use_case/archive_file.dart +++ b/app/lib/use_case/archive_file.dart @@ -54,7 +54,7 @@ class _SetArchiveFile { var count = 0; for (final f in files) { try { - await UpdateProperty(_c.fileRepo).updateIsArchived(account, f, flag); + await UpdateProperty(_c).updateIsArchived(account, f, flag); ++count; } catch (e, stackTrace) { _log.severe( diff --git a/app/lib/use_case/update_missing_metadata.dart b/app/lib/use_case/update_missing_metadata.dart index a1d1206c..8f494ff3 100644 --- a/app/lib/use_case/update_missing_metadata.dart +++ b/app/lib/use_case/update_missing_metadata.dart @@ -4,6 +4,7 @@ import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/connectivity_util.dart' as connectivity_util; +import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/exif_extension.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/event/event.dart'; @@ -26,7 +27,7 @@ abstract class UpdateMissingMetadataConfigProvider { @npLog class UpdateMissingMetadata { - UpdateMissingMetadata(this.fileRepo, this.configProvider, this.geocoder); + UpdateMissingMetadata(this._c, this.configProvider, this.geocoder); /// Update metadata for all files that support one under a dir /// @@ -44,7 +45,7 @@ class UpdateMissingMetadata { bool isRecursive = true, bool Function(File file)? filter, }) async* { - final dataStream = ScanMissingMetadata(fileRepo)( + final dataStream = ScanMissingMetadata(_c.fileRepo)( account, root, isRecursive: isRecursive, @@ -78,7 +79,7 @@ class UpdateMissingMetadata { return; } _log.fine("[call] Updating metadata for ${file.path}"); - final binary = await GetFileBinary(fileRepo)(account, file); + final binary = await GetFileBinary(_c.fileRepo)(account, file); final metadata = (await LoadMetadata().loadRemote(account, file, binary)).copyWith( fileEtag: file.etag, @@ -111,7 +112,7 @@ class UpdateMissingMetadata { } if (metadataUpdate != null || locationUpdate != null) { - await UpdateProperty(fileRepo)( + await UpdateProperty(_c)( account, file, metadata: metadataUpdate, @@ -164,7 +165,7 @@ class UpdateMissingMetadata { } } - final FileRepo fileRepo; + final DiContainer _c; final UpdateMissingMetadataConfigProvider configProvider; final ReverseGeocoder geocoder; diff --git a/app/lib/use_case/update_property.dart b/app/lib/use_case/update_property.dart index 3b6a1666..65f5e970 100644 --- a/app/lib/use_case/update_property.dart +++ b/app/lib/use_case/update_property.dart @@ -2,7 +2,9 @@ import 'package:event_bus/event_bus.dart'; import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; +import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/file.dart'; +import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/event/event.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:np_common/or_null.dart'; @@ -11,11 +13,11 @@ part 'update_property.g.dart'; @npLog class UpdateProperty { - UpdateProperty(this.fileRepo); + const UpdateProperty(this._c); Future call( Account account, - File file, { + FileDescriptor file, { OrNull? metadata, OrNull? isArchived, OrNull? overrideDateTime, @@ -32,11 +34,7 @@ class UpdateProperty { return; } - if (metadata?.obj != null && metadata!.obj!.fileEtag != file.etag) { - _log.warning( - "[call] Metadata fileEtag mismatch with actual file's (metadata: ${metadata.obj!.fileEtag}, file: ${file.etag})"); - } - await fileRepo.updateProperty( + await _c.fileRepo2.updateProperty( account, file, metadata: metadata, @@ -46,6 +44,27 @@ class UpdateProperty { location: location, ); + _notify( + account, + file, + metadata: metadata, + isArchived: isArchived, + overrideDateTime: overrideDateTime, + favorite: favorite, + location: location, + ); + } + + @Deprecated("legacy") + void _notify( + Account account, + FileDescriptor file, { + OrNull? metadata, + OrNull? isArchived, + OrNull? overrideDateTime, + bool? favorite, + OrNull? location, + }) { int properties = 0; if (metadata != null) { properties |= FilePropertyUpdatedEvent.propMetadata; @@ -68,7 +87,7 @@ class UpdateProperty { .fire(FilePropertyUpdatedEvent(account, file, properties)); } - final FileRepo fileRepo; + final DiContainer _c; } extension UpdatePropertyExtension on UpdateProperty { diff --git a/app/lib/widget/archive_browser.dart b/app/lib/widget/archive_browser.dart index d68a1d2e..9e9f8ba7 100644 --- a/app/lib/widget/archive_browser.dart +++ b/app/lib/widget/archive_browser.dart @@ -242,7 +242,7 @@ class _ArchiveBrowserState extends State final failures = []; for (final f in selectedFiles) { try { - await UpdateProperty(c.fileRepo) + await UpdateProperty(c) .updateIsArchived(widget.account, f, false); } catch (e, stacktrace) { _log.shout( diff --git a/app/lib/widget/handler/archive_selection_handler.dart b/app/lib/widget/handler/archive_selection_handler.dart index 69714600..43115700 100644 --- a/app/lib/widget/handler/archive_selection_handler.dart +++ b/app/lib/widget/handler/archive_selection_handler.dart @@ -30,7 +30,7 @@ class ArchiveSelectionHandler { return NotifiedListAction( list: selectedFiles, action: (file) async { - await UpdateProperty(_c.fileRepo).updateIsArchived(account, file, true); + await UpdateProperty(_c).updateIsArchived(account, file, true); }, processingText: shouldShowProcessingText ? L10n.global() diff --git a/app/lib/widget/handler/unarchive_selection_handler.dart b/app/lib/widget/handler/unarchive_selection_handler.dart index f6dd0dc2..02bb2b73 100644 --- a/app/lib/widget/handler/unarchive_selection_handler.dart +++ b/app/lib/widget/handler/unarchive_selection_handler.dart @@ -30,8 +30,7 @@ class UnarchiveSelectionHandler { return await NotifiedListAction( list: selectedFiles, action: (file) async { - await UpdateProperty(_c.fileRepo) - .updateIsArchived(account, file, false); + await UpdateProperty(_c).updateIsArchived(account, file, false); }, processingText: shouldShowProcessingText ? L10n.global() diff --git a/app/lib/widget/viewer.dart b/app/lib/widget/viewer.dart index 180d9a67..f0cd54b1 100644 --- a/app/lib/widget/viewer.dart +++ b/app/lib/widget/viewer.dart @@ -546,7 +546,7 @@ class _ViewerState extends State _pageStates[index]!.isProcessingFavorite = true; try { await NotifiedAction( - () => UpdateProperty(c.fileRepo)( + () => UpdateProperty(c)( widget.account, file, favorite: true, @@ -580,7 +580,7 @@ class _ViewerState extends State _pageStates[index]!.isProcessingFavorite = true; try { await NotifiedAction( - () => UpdateProperty(c.fileRepo)( + () => UpdateProperty(c)( widget.account, file, favorite: false, diff --git a/app/lib/widget/viewer_detail_pane.dart b/app/lib/widget/viewer_detail_pane.dart index 431ef1d2..ec5a566a 100644 --- a/app/lib/widget/viewer_detail_pane.dart +++ b/app/lib/widget/viewer_detail_pane.dart @@ -480,7 +480,7 @@ class _ViewerDetailPaneState extends State { return; } try { - await UpdateProperty(_c.fileRepo) + await UpdateProperty(_c) .updateOverrideDateTime(widget.account, _file!, value); if (mounted) { setState(() { From 378da233609ba74176b55074201ddf9c048b5ff9 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sat, 13 Jan 2024 02:37:13 +0800 Subject: [PATCH 03/42] Migrate Remove to use controller --- app/lib/entity/file/file_cache_manager.dart | 14 -------------- app/lib/use_case/album/remove_album.dart | 1 - app/lib/use_case/remove.dart | 12 ++---------- 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/app/lib/entity/file/file_cache_manager.dart b/app/lib/entity/file/file_cache_manager.dart index 853fdeb7..60475043 100644 --- a/app/lib/entity/file/file_cache_manager.dart +++ b/app/lib/entity/file/file_cache_manager.dart @@ -110,20 +110,6 @@ class FileSqliteCacheUpdater { final DiContainer _c; } -class FileSqliteCacheRemover { - const FileSqliteCacheRemover(this._c); - - /// Remove a file/dir from cache - Future call(Account account, FileDescriptor f) async { - await _c.npDb.deleteFile( - account: account.toDb(), - file: f.toDbKey(), - ); - } - - final DiContainer _c; -} - class FileSqliteCacheEmptier { const FileSqliteCacheEmptier(this._c); diff --git a/app/lib/use_case/album/remove_album.dart b/app/lib/use_case/album/remove_album.dart index f5773dfb..1fdbd2f6 100644 --- a/app/lib/use_case/album/remove_album.dart +++ b/app/lib/use_case/album/remove_album.dart @@ -20,7 +20,6 @@ class RemoveAlbum { RemoveAlbum(this._c) : assert(require(_c)), assert(ListShare.require(_c)), - assert(Remove.require(_c)), assert(UnshareFileFromAlbum.require(_c)); static bool require(DiContainer c) => diff --git a/app/lib/use_case/remove.dart b/app/lib/use_case/remove.dart index 0b0ab87b..df8cde7d 100644 --- a/app/lib/use_case/remove.dart +++ b/app/lib/use_case/remove.dart @@ -24,15 +24,7 @@ part 'remove.g.dart'; @npLog class Remove { - Remove(this._c) - : assert(require(_c)), - assert(ListAlbum.require(_c)), - assert(ListShare.require(_c)), - assert(RemoveFromAlbum.require(_c)); - - static bool require(DiContainer c) => - DiContainer.has(c, DiType.fileRepo) && - DiContainer.has(c, DiType.shareRepo); + const Remove(this._c); /// Remove list of [files] and return the removed count Future call( @@ -52,7 +44,7 @@ class Remove { final i = pair.item1; final f = pair.item2; try { - await _c.fileRepo.remove(account, f); + await _c.fileRepo2.remove(account, f); ++count; KiwiContainer().resolve().fire(FileRemovedEvent(account, f)); } catch (e, stackTrace) { From 4950bcef8f7743ed8b3d2b9caa7f35d0234269d3 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sat, 13 Jan 2024 02:53:14 +0800 Subject: [PATCH 04/42] Rewrite HomePhotos --- app/lib/bloc_util.dart | 32 ++ app/lib/controller/pref_controller.dart | 11 + app/lib/widget/home.dart | 5 +- app/lib/widget/home_photos/app_bar.dart | 124 ++++++++ app/lib/widget/home_photos/bloc.dart | 334 ++++++++++++++++++++ app/lib/widget/home_photos/state_event.dart | 218 +++++++++++++ app/lib/widget/home_photos/type.dart | 167 ++++++++++ app/lib/widget/home_photos/view.dart | 260 +++++++++++++++ app/lib/widget/home_photos2.dart | 325 +++++++++++++++++++ app/lib/widget/home_photos2.g.dart | 262 +++++++++++++++ app/lib/widget/selectable_item_list.dart | 77 +++-- app/pubspec.lock | 2 +- app/pubspec.yaml | 1 + np_collection/lib/np_collection.dart | 1 + np_collection/lib/src/set_util.dart | 5 + 15 files changed, 1794 insertions(+), 30 deletions(-) create mode 100644 app/lib/widget/home_photos/app_bar.dart create mode 100644 app/lib/widget/home_photos/bloc.dart create mode 100644 app/lib/widget/home_photos/state_event.dart create mode 100644 app/lib/widget/home_photos/type.dart create mode 100644 app/lib/widget/home_photos/view.dart create mode 100644 app/lib/widget/home_photos2.dart create mode 100644 app/lib/widget/home_photos2.g.dart create mode 100644 np_collection/lib/src/set_util.dart diff --git a/app/lib/bloc_util.dart b/app/lib/bloc_util.dart index 4ba5142e..0522cc5a 100644 --- a/app/lib/bloc_util.dart +++ b/app/lib/bloc_util.dart @@ -1,4 +1,6 @@ +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/single_child_widget.dart'; mixin BlocLogger { String? get tag => null; @@ -6,6 +8,28 @@ mixin BlocLogger { bool Function(dynamic currentState, dynamic nextState)? get shouldLog => null; } +class BlocListenerT, S, T> + extends SingleChildStatelessWidget { + const BlocListenerT({ + super.key, + required this.selector, + required this.listener, + }); + + @override + Widget buildWithChild(BuildContext context, Widget? child) { + return BlocListener( + listenWhen: (previous, current) => + selector(previous) != selector(current), + listener: (context, state) => listener(context, selector(state)), + child: child, + ); + } + + final BlocWidgetSelector selector; + final void Function(BuildContext context, T state) listener; +} + /// Wrap around a string such that two strings with the same value will fail /// the identical check class StateMessage { @@ -25,3 +49,11 @@ extension EmitterExtension on Emitter { onError: (_, __) {}, ); } + +extension BlocExtension on Bloc { + void safeAdd(E event) { + if (!isClosed) { + add(event); + } + } +} diff --git a/app/lib/controller/pref_controller.dart b/app/lib/controller/pref_controller.dart index ac64abca..86bb00d3 100644 --- a/app/lib/controller/pref_controller.dart +++ b/app/lib/controller/pref_controller.dart @@ -27,6 +27,15 @@ class PrefController { value: value, ); + ValueStream get homePhotosZoomLevel => + _homePhotosZoomLevelController.stream; + + Future setHomePhotosZoomLevel(int value) => _set( + controller: _homePhotosZoomLevelController, + setter: (pref, value) => pref.setHomePhotosZoomLevel(value), + value: value, + ); + ValueStream get albumBrowserZoomLevel => _albumBrowserZoomLevelController.stream; @@ -237,6 +246,8 @@ class PrefController { final DiContainer _c; late final _languageController = BehaviorSubject.seeded(_langIdToAppLanguage(_c.pref.getLanguageOr(0))); + late final _homePhotosZoomLevelController = + BehaviorSubject.seeded(_c.pref.getHomePhotosZoomLevelOr(0)); late final _albumBrowserZoomLevelController = BehaviorSubject.seeded(_c.pref.getAlbumBrowserZoomLevelOr(0)); late final _homeAlbumsSortController = diff --git a/app/lib/widget/home.dart b/app/lib/widget/home.dart index c8180d98..6c545aff 100644 --- a/app/lib/widget/home.dart +++ b/app/lib/widget/home.dart @@ -20,6 +20,7 @@ import 'package:nc_photos/theme/dimension.dart'; import 'package:nc_photos/use_case/import_potential_shared_album.dart'; import 'package:nc_photos/widget/home_collections.dart'; import 'package:nc_photos/widget/home_photos.dart'; +import 'package:nc_photos/widget/home_photos2.dart'; import 'package:nc_photos/widget/home_search.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:np_common/or_null.dart'; @@ -141,9 +142,7 @@ class _HomeState extends State with TickerProviderStateMixin { Widget _buildPage(BuildContext context, int index) { switch (index) { case 0: - return HomePhotos( - account: widget.account, - ); + return const HomePhotos2(); case 1: return HomeSearch( diff --git a/app/lib/widget/home_photos/app_bar.dart b/app/lib/widget/home_photos/app_bar.dart new file mode 100644 index 00000000..d5a6c57d --- /dev/null +++ b/app/lib/widget/home_photos/app_bar.dart @@ -0,0 +1,124 @@ +part of '../home_photos2.dart'; + +class _AppBar extends StatelessWidget { + const _AppBar(); + + @override + Widget build(BuildContext context) { + return _BlocBuilder( + buildWhen: (previous, current) => previous.isLoading != current.isLoading, + builder: (context, state) => HomeSliverAppBar( + account: context.bloc.account, + isShowProgressIcon: state.isLoading, + ), + ); + } +} + +@npLog +class _SelectionAppBar extends StatelessWidget { + const _SelectionAppBar(); + + @override + Widget build(BuildContext context) { + return _BlocBuilder( + buildWhen: (previous, current) => + previous.selectedItems != current.selectedItems, + builder: (context, state) => SelectionAppBar( + count: state.selectedItems.length, + onClosePressed: () { + context.addEvent(const _SetSelectedItems(items: {})); + }, + actions: [ + IconButton( + icon: const Icon(Icons.share_outlined), + tooltip: L10n.global().shareTooltip, + onPressed: () => _onSharePressed(context), + ), + IconButton( + icon: const Icon(Icons.add), + tooltip: L10n.global().addItemToCollectionTooltip, + onPressed: () => _onAddPressed(context), + ), + const _SelectionAppBarMenu(), + ], + ), + ); + } + + Future _onAddPressed(BuildContext context) async { + final collection = await Navigator.of(context) + .pushNamed(CollectionPicker.routeName); + if (collection == null) { + return; + } + context.bloc.add(_AddSelectedItemsToCollection(collection)); + } + + Future _onSharePressed(BuildContext context) async { + final bloc = context.read<_Bloc>(); + final selected = bloc.state.selectedItems + .whereType<_FileItem>() + .map((e) => e.file) + .toList(); + if (selected.isEmpty) { + SnackBarManager().showSnackBar(SnackBar( + content: Text(L10n.global().shareSelectedEmptyNotification), + duration: k.snackBarDurationNormal, + )); + return; + } + final result = await showDialog( + context: context, + builder: (context) => FileSharerDialog( + account: bloc.account, + files: selected, + ), + ); + if (result ?? false) { + bloc.add(const _SetSelectedItems(items: {})); + } + } +} + +@npLog +class _SelectionAppBarMenu extends StatelessWidget { + const _SelectionAppBarMenu(); + + @override + Widget build(BuildContext context) { + return PopupMenuButton<_SelectionMenuOption>( + tooltip: MaterialLocalizations.of(context).moreButtonTooltip, + itemBuilder: (context) => [ + PopupMenuItem( + value: _SelectionMenuOption.download, + child: Text(L10n.global().downloadTooltip), + ), + PopupMenuItem( + value: _SelectionMenuOption.archive, + child: Text(L10n.global().archiveTooltip), + ), + PopupMenuItem( + value: _SelectionMenuOption.delete, + child: Text(L10n.global().deleteTooltip), + ), + ], + onSelected: (option) { + switch (option) { + case _SelectionMenuOption.archive: + context.addEvent(const _ArchiveSelectedItems()); + break; + case _SelectionMenuOption.delete: + context.addEvent(const _DeleteSelectedItems()); + break; + case _SelectionMenuOption.download: + context.addEvent(const _DownloadSelectedItems()); + break; + default: + _log.shout("[build] Unknown option: $option"); + break; + } + }, + ); + } +} diff --git a/app/lib/widget/home_photos/bloc.dart b/app/lib/widget/home_photos/bloc.dart new file mode 100644 index 00000000..956cf645 --- /dev/null +++ b/app/lib/widget/home_photos/bloc.dart @@ -0,0 +1,334 @@ +part of '../home_photos2.dart'; + +@npLog +class _Bloc extends Bloc<_Event, _State> with BlocLogger { + _Bloc( + this._c, { + required this.account, + required this.controller, + required this.prefController, + required this.accountPrefController, + required this.collectionsController, + }) : super(_State.init( + zoom: prefController.homePhotosZoomLevel.value, + isEnableMemoryCollection: + accountPrefController.isEnableMemoryAlbum.value, + )) { + on<_LoadItems>(_onLoad); + on<_Reload>(_onReload); + on<_TransformItems>(_onTransformItems); + on<_OnItemTransformed>(_onOnItemTransformed); + + on<_SetSelectedItems>(_onSetSelectedItems); + on<_AddSelectedItemsToCollection>(_onAddSelectedItemsToCollection); + on<_ArchiveSelectedItems>(_onArchiveSelectedItems); + on<_DeleteSelectedItems>(_onDeleteSelectedItems); + on<_DownloadSelectedItems>(_onDownloadSelectedItems); + + on<_AddVisibleItem>(_onAddVisibleItem); + on<_RemoveVisibleItem>(_onRemoveVisibleItem); + + on<_SetContentListMaxExtent>(_onSetContentListMaxExtent); + + on<_StartScaling>(_onStartScaling); + on<_EndScaling>(_onEndScaling); + on<_SetScale>(_onSetScale); + + on<_SetEnableMemoryCollection>(_onSetEnableMemoryCollection); + + on<_SetError>(_onSetError); + + _subscriptions + .add(accountPrefController.isEnableMemoryAlbum.listen((event) { + add(_SetEnableMemoryCollection(event)); + })); + } + + @override + Future close() { + for (final s in _subscriptions) { + s.cancel(); + } + return super.close(); + } + + @override + String get tag => _log.fullName; + + @override + bool Function(dynamic, dynamic)? get shouldLog => (currentState, nextState) { + currentState = currentState as _State; + nextState = nextState as _State; + return currentState.scale == nextState.scale && + currentState.visibleItems == nextState.visibleItems; + }; + + @override + void onError(Object error, StackTrace stackTrace) { + // we need this to prevent onError being triggered recursively + if (!isClosed && !_isHandlingError) { + _isHandlingError = true; + try { + add(_SetError(error, stackTrace)); + } catch (_) {} + _isHandlingError = false; + } + super.onError(error, stackTrace); + } + + Future _onLoad(_LoadItems ev, Emitter<_State> emit) { + _log.info(ev); + return emit.forEach( + controller.stream, + onData: (data) => state.copyWith( + files: data.data, + isLoading: data.hasNext || _itemTransformerQueue.isProcessing, + ), + onError: (e, stackTrace) { + _log.severe("[_onLoad] Uncaught exception", e, stackTrace); + return state.copyWith( + isLoading: _itemTransformerQueue.isProcessing, + error: ExceptionEvent(e, stackTrace), + ); + }, + ); + } + + void _onReload(_Reload ev, Emitter<_State> emit) { + _log.info(ev); + unawaited(controller.reload()); + } + + void _onTransformItems(_TransformItems ev, Emitter<_State> emit) { + _log.info(ev); + _transformItems(ev.items); + emit(state.copyWith(isLoading: true)); + } + + void _onOnItemTransformed(_OnItemTransformed ev, Emitter<_State> emit) { + _log.info(ev); + emit(state.copyWith( + transformedItems: ev.items, + memoryCollections: ev.memoryCollections, + isLoading: _itemTransformerQueue.isProcessing, + )); + } + + void _onSetSelectedItems(_SetSelectedItems ev, Emitter<_State> emit) { + _log.info(ev); + emit(state.copyWith(selectedItems: ev.items)); + } + + void _onAddSelectedItemsToCollection( + _AddSelectedItemsToCollection ev, Emitter<_State> emit) { + _log.info(ev); + final selected = state.selectedItems; + _clearSelection(emit); + final selectedFiles = + selected.whereType<_FileItem>().map((e) => e.file).toList(); + if (selectedFiles.isNotEmpty) { + final targetController = collectionsController.stream.value + .itemsControllerByCollection(ev.collection); + targetController.addFiles(selectedFiles).onError((e, stackTrace) { + if (e != null) { + add(_SetError(e, stackTrace)); + } + }); + } + } + + void _onArchiveSelectedItems(_ArchiveSelectedItems ev, Emitter<_State> emit) { + _log.info(ev); + final selected = state.selectedItems; + _clearSelection(emit); + final selectedFiles = + selected.whereType<_FileItem>().map((e) => e.file).toList(); + if (selectedFiles.isNotEmpty) { + controller.updateProperty( + selectedFiles, + isArchived: const OrNull(true), + errorBuilder: (fileIds) => _ArchiveFailedError(fileIds.length), + ); + } + } + + void _onDeleteSelectedItems(_DeleteSelectedItems ev, Emitter<_State> emit) { + _log.info(ev); + final selected = state.selectedItems; + _clearSelection(emit); + final selectedFiles = + selected.whereType<_FileItem>().map((e) => e.file).toList(); + if (selectedFiles.isNotEmpty) { + controller.remove( + selectedFiles, + errorBuilder: (fileIds) => _RemoveFailedError(fileIds.length), + ); + } + } + + void _onDownloadSelectedItems( + _DownloadSelectedItems ev, Emitter<_State> emit) { + _log.info(ev); + final selected = state.selectedItems; + _clearSelection(emit); + final selectedFiles = + selected.whereType<_FileItem>().map((e) => e.file).toList(); + if (selectedFiles.isNotEmpty) { + unawaited(DownloadHandler(_c).downloadFiles(account, selectedFiles)); + } + } + + void _onAddVisibleItem(_AddVisibleItem ev, Emitter<_State> emit) { + // _log.info(ev); + if (state.visibleItems.contains(ev.item)) { + return; + } + emit(state.copyWith( + visibleItems: state.visibleItems.added(ev.item), + )); + } + + void _onRemoveVisibleItem(_RemoveVisibleItem ev, Emitter<_State> emit) { + // _log.info(ev); + if (!state.visibleItems.contains(ev.item)) { + return; + } + emit(state.copyWith( + visibleItems: state.visibleItems.removed(ev.item), + )); + } + + void _onSetContentListMaxExtent( + _SetContentListMaxExtent ev, Emitter<_State> emit) { + _log.info(ev); + emit(state.copyWith(contentListMaxExtent: ev.value)); + } + + void _onStartScaling(_StartScaling ev, Emitter<_State> emit) { + _log.info(ev); + } + + void _onEndScaling(_EndScaling ev, Emitter<_State> emit) { + _log.info(ev); + if (state.scale == null) { + return; + } + final int newZoom; + if (state.scale! >= 1.25) { + // scale up + newZoom = (state.zoom + 1).clamp(-1, 2); + } else if (state.scale! <= 0.75) { + newZoom = (state.zoom - 1).clamp(-1, 2); + } else { + newZoom = state.zoom; + } + emit(state.copyWith( + zoom: newZoom, + scale: null, + )); + unawaited(prefController.setHomePhotosZoomLevel(newZoom)); + } + + void _onSetScale(_SetScale ev, Emitter<_State> emit) { + // _log.info(ev); + emit(state.copyWith(scale: ev.scale)); + } + + void _onSetEnableMemoryCollection( + _SetEnableMemoryCollection ev, Emitter<_State> emit) { + _log.info(ev); + emit(state.copyWith(isEnableMemoryCollection: ev.value)); + } + + void _onSetError(_SetError ev, Emitter<_State> emit) { + _log.info(ev); + emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); + } + + Future _transformItems(List files) async { + _log.info("[_transformItems] Queue ${files.length} items"); + _itemTransformerQueue.addJob( + _ItemTransformerArgument( + account: account, + files: files, + memoriesDayRange: prefController.memoriesRange.value, + locale: language_util.getSelectedLocale() ?? + PlatformDispatcher.instance.locale, + ), + _buildItem, + (result) { + if (!isClosed) { + add(_OnItemTransformed(result.items, result.memoryCollections)); + } + }, + ); + } + + void _clearSelection(Emitter<_State> emit) { + emit(state.copyWith(selectedItems: const {})); + } + + final DiContainer _c; + final Account account; + final FilesController controller; + final PrefController prefController; + final AccountPrefController accountPrefController; + final CollectionsController collectionsController; + + final _itemTransformerQueue = + ComputeQueue<_ItemTransformerArgument, _ItemTransformerResult>(); + final _subscriptions = []; + var _isHandlingError = false; +} + +_ItemTransformerResult _buildItem(_ItemTransformerArgument arg) { + final sortedFiles = arg.files + .where((f) => f.fdIsArchived != true) + .sorted(compareFileDescriptorDateTimeDescending); + final dateHelper = photo_list_util.DateGroupHelper(isMonthOnly: false); + + final today = clock.now(); + final memoryCollectionHelper = photo_list_util.MemoryCollectionHelper( + arg.account, + today: today, + dayRange: arg.memoriesDayRange, + ); + final transformed = <_Item>[]; + for (int i = 0; i < sortedFiles.length; ++i) { + final file = sortedFiles[i]; + final item = _buildSingleItem(arg.account, file); + if (item == null) { + continue; + } + final date = dateHelper.onFile(file); + if (date != null) { + transformed.add(_DateItem(date: date)); + } + transformed.add(item); + memoryCollectionHelper.addFile(file); + } + final memoryCollections = memoryCollectionHelper + .build((year) => L10n.of(arg.locale).memoryAlbumName(today.year - year)); + return _ItemTransformerResult( + items: transformed, + memoryCollections: memoryCollections, + ); +} + +_Item? _buildSingleItem(Account account, FileDescriptor file) { + if (file_util.isSupportedImageFormat(file)) { + return _PhotoItem( + file: file, + account: account, + ); + } else if (file_util.isSupportedVideoFormat(file)) { + return _VideoItem( + file: file, + account: account, + ); + } else { + _$__NpLog.log + .shout("[_buildSingleItem] Unsupported file format: ${file.fdMime}"); + return null; + } +} diff --git a/app/lib/widget/home_photos/state_event.dart b/app/lib/widget/home_photos/state_event.dart new file mode 100644 index 00000000..77a36b01 --- /dev/null +++ b/app/lib/widget/home_photos/state_event.dart @@ -0,0 +1,218 @@ +part of '../home_photos2.dart'; + +@genCopyWith +@toString +class _State { + const _State({ + required this.files, + required this.isLoading, + required this.transformedItems, + required this.selectedItems, + required this.visibleItems, + required this.isEnableMemoryCollection, + required this.memoryCollections, + this.contentListMaxExtent, + required this.zoom, + this.scale, + this.error, + }); + + factory _State.init({ + required bool isEnableMemoryCollection, + required int zoom, + }) => + _State( + files: const [], + isLoading: false, + transformedItems: const [], + selectedItems: const {}, + visibleItems: const {}, + isEnableMemoryCollection: isEnableMemoryCollection, + memoryCollections: const [], + zoom: zoom, + ); + + @override + String toString() => _$toString(); + + final List files; + final bool isLoading; + final List<_Item> transformedItems; + final Set<_Item> selectedItems; + final Set<_VisibleItem> visibleItems; + + final bool isEnableMemoryCollection; + final List memoryCollections; + + final double? contentListMaxExtent; + + final int zoom; + final double? scale; + + final ExceptionEvent? error; +} + +abstract class _Event {} + +/// Load the files +@toString +class _LoadItems implements _Event { + const _LoadItems(); + + @override + String toString() => _$toString(); +} + +@toString +class _Reload implements _Event { + const _Reload(); + + @override + String toString() => _$toString(); +} + +/// Transform the file list (e.g., filtering, sorting, etc) +@toString +class _TransformItems implements _Event { + const _TransformItems(this.items); + + @override + String toString() => _$toString(); + + final List items; +} + +@toString +class _OnItemTransformed implements _Event { + const _OnItemTransformed(this.items, this.memoryCollections); + + @override + String toString() => _$toString(); + + final List<_Item> items; + final List memoryCollections; +} + +/// Set the currently selected items +@toString +class _SetSelectedItems implements _Event { + const _SetSelectedItems({ + required this.items, + }); + + @override + String toString() => _$toString(); + + final Set<_Item> items; +} + +@toString +class _AddSelectedItemsToCollection implements _Event { + const _AddSelectedItemsToCollection(this.collection); + + @override + String toString() => _$toString(); + + final Collection collection; +} + +@toString +class _ArchiveSelectedItems implements _Event { + const _ArchiveSelectedItems(); + + @override + String toString() => _$toString(); +} + +@toString +class _DeleteSelectedItems implements _Event { + const _DeleteSelectedItems(); + + @override + String toString() => _$toString(); +} + +@toString +class _DownloadSelectedItems implements _Event { + const _DownloadSelectedItems(); + + @override + String toString() => _$toString(); +} + +@toString +class _AddVisibleItem implements _Event { + const _AddVisibleItem(this.item); + + @override + String toString() => _$toString(); + + final _VisibleItem item; +} + +@toString +class _RemoveVisibleItem implements _Event { + const _RemoveVisibleItem(this.item); + + @override + String toString() => _$toString(); + + final _VisibleItem item; +} + +@toString +class _SetContentListMaxExtent implements _Event { + const _SetContentListMaxExtent(this.value); + + @override + String toString() => _$toString(); + + final double? value; +} + +@toString +class _StartScaling implements _Event { + const _StartScaling(); + + @override + String toString() => _$toString(); +} + +@toString +class _EndScaling implements _Event { + const _EndScaling(); + + @override + String toString() => _$toString(); +} + +@toString +class _SetScale implements _Event { + const _SetScale(this.scale); + + @override + String toString() => _$toString(); + + final double scale; +} + +@toString +class _SetEnableMemoryCollection implements _Event { + const _SetEnableMemoryCollection(this.value); + + @override + String toString() => _$toString(); + + final bool value; +} + +@toString +class _SetError implements _Event { + const _SetError(this.error, [this.stackTrace]); + + @override + String toString() => _$toString(); + + final Object error; + final StackTrace? stackTrace; +} diff --git a/app/lib/widget/home_photos/type.dart b/app/lib/widget/home_photos/type.dart new file mode 100644 index 00000000..9e9131a7 --- /dev/null +++ b/app/lib/widget/home_photos/type.dart @@ -0,0 +1,167 @@ +part of '../home_photos2.dart'; + +abstract class _Item implements SelectableItemMetadata { + const _Item(); + + StaggeredTile get staggeredTile; + + Widget buildWidget(BuildContext context); +} + +abstract class _FileItem extends _Item { + const _FileItem({ + required this.file, + }); + + @override + bool get isSelectable => true; + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is _FileItem && file.compareServerIdentity(other.file)); + + @override + int get hashCode => file.identityHashCode; + + final FileDescriptor file; +} + +class _PhotoItem extends _FileItem { + _PhotoItem({ + required super.file, + required this.account, + }) : _previewUrl = NetworkRectThumbnail.imageUrlForFile(account, file); + + @override + StaggeredTile get staggeredTile => const StaggeredTile.count(1, 1); + + @override + Widget buildWidget(BuildContext context) { + return PhotoListImage( + account: account, + previewUrl: _previewUrl, + isGif: file.fdMime == "image/gif", + isFavorite: file.fdIsFavorite, + heroKey: flutter_util.getImageHeroTag(file), + ); + } + + final Account account; + final String _previewUrl; +} + +class _VideoItem extends _FileItem { + _VideoItem({ + required super.file, + required this.account, + }) : _previewUrl = NetworkRectThumbnail.imageUrlForFile(account, file); + + @override + StaggeredTile get staggeredTile => const StaggeredTile.count(1, 1); + + @override + Widget buildWidget(BuildContext context) { + return PhotoListVideo( + account: account, + previewUrl: _previewUrl, + isFavorite: file.fdIsFavorite, + ); + } + + final Account account; + final String _previewUrl; +} + +class _DateItem extends _Item { + const _DateItem({ + required this.date, + }); + + @override + bool get isSelectable => false; + + @override + StaggeredTile get staggeredTile => const StaggeredTile.extent(99, 32); + + @override + Widget buildWidget(BuildContext context) { + return PhotoListDate( + date: date, + ); + } + + final DateTime date; +} + +class _ItemTransformerArgument { + const _ItemTransformerArgument({ + required this.account, + required this.files, + required this.memoriesDayRange, + required this.locale, + }); + + final Account account; + final List files; + final int memoriesDayRange; + final Locale locale; +} + +class _ItemTransformerResult { + const _ItemTransformerResult({ + required this.items, + required this.memoryCollections, + }); + + final List<_Item> items; + final List memoryCollections; +} + +class _MemoryCollectionItem { + static const width = 96.0; + static const height = width * 1.15; +} + +class _VisibleItem implements Comparable<_VisibleItem> { + const _VisibleItem(this.index, this.item); + + @override + bool operator ==(Object? other) => + other is _VisibleItem && index == other.index; + + @override + int compareTo(_VisibleItem other) => index.compareTo(other.index); + + @override + int get hashCode => index.hashCode; + + final int index; + final _Item item; +} + +enum _SelectionMenuOption { + archive, + delete, + download, +} + +@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; +} diff --git a/app/lib/widget/home_photos/view.dart b/app/lib/widget/home_photos/view.dart new file mode 100644 index 00000000..3112b852 --- /dev/null +++ b/app/lib/widget/home_photos/view.dart @@ -0,0 +1,260 @@ +part of '../home_photos2.dart'; + +class _ContentList extends StatelessWidget { + const _ContentList(); + + @override + Widget build(BuildContext context) { + return _BlocSelector( + selector: (state) => state.zoom, + builder: (context, zoom) => _ContentListBody( + maxCrossAxisExtent: photo_list_util.getThumbSize(zoom).toDouble(), + isNeedVisibilityInfo: true, + ), + ); + } +} + +class _ScalingList extends StatelessWidget { + const _ScalingList(); + + @override + Widget build(BuildContext context) { + return _BlocBuilder( + buildWhen: (previous, current) => previous.scale != current.scale, + builder: (context, state) { + if (state.scale == null) { + return const SizedBox.shrink(); + } + int nextZoom; + if (state.scale! > 1) { + nextZoom = state.zoom + 1; + } else { + nextZoom = state.zoom - 1; + } + nextZoom = nextZoom.clamp(-1, 2); + return _ContentListBody( + maxCrossAxisExtent: photo_list_util.getThumbSize(nextZoom).toDouble(), + isNeedVisibilityInfo: false, + ); + }, + ); + } +} + +@npLog +class _ContentListBody extends StatelessWidget { + const _ContentListBody({ + required this.maxCrossAxisExtent, + required this.isNeedVisibilityInfo, + }); + + @override + Widget build(BuildContext context) { + return _BlocBuilder( + buildWhen: (previous, current) => + previous.transformedItems != current.transformedItems || + previous.selectedItems != current.selectedItems, + builder: (context, state) => SelectableItemList<_Item>( + maxCrossAxisExtent: maxCrossAxisExtent, + items: state.transformedItems, + itemBuilder: (context, index, item) { + final w = item.buildWidget(context); + if (isNeedVisibilityInfo) { + return VisibilityDetector( + key: Key("${_log.fullName}.$index"), + onVisibilityChanged: (info) { + if (context.mounted) { + if (info.visibleFraction >= 0.2) { + context + .addEvent(_AddVisibleItem(_VisibleItem(index, item))); + } else { + context.addEvent( + _RemoveVisibleItem(_VisibleItem(index, item))); + } + } + }, + child: w, + ); + } else { + return w; + } + }, + staggeredTileBuilder: (_, item) => item.staggeredTile, + selectedItems: state.selectedItems, + onSelectionChange: (_, selected) { + context.addEvent(_SetSelectedItems(items: selected.cast())); + }, + onItemTap: (context, index, _) { + if (state.transformedItems[index] is! _FileItem) { + return; + } + final actualIndex = index - + state.transformedItems + .sublist(0, index) + .where((e) => e is! _FileItem) + .length; + Navigator.of(context).pushNamed( + Viewer.routeName, + arguments: ViewerArguments( + context.bloc.account, + state.transformedItems + .whereType<_FileItem>() + .map((e) => e.file) + .toList(), + actualIndex, + ), + ); + }, + onMaxExtentChange: (value) { + context.addEvent(_SetContentListMaxExtent(value)); + }, + ), + ); + } + + final double maxCrossAxisExtent; + final bool isNeedVisibilityInfo; +} + +class _MemoryCollectionList extends StatelessWidget { + const _MemoryCollectionList(); + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: SizedBox( + height: _MemoryCollectionItem.height, + child: _BlocSelector>( + selector: (state) => state.memoryCollections, + builder: (context, memoryCollections) => ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 8), + itemCount: memoryCollections.length, + itemBuilder: (context, index) { + final c = memoryCollections[index]; + return _MemoryCollectionItemView( + coverUrl: c.getCoverUrl( + k.photoThumbSize, + k.photoThumbSize, + isKeepAspectRatio: true, + ), + label: c.name, + onTap: () { + Navigator.of(context).pushNamed( + CollectionBrowser.routeName, + arguments: CollectionBrowserArguments(c), + ); + }, + ); + }, + separatorBuilder: (context, index) => const SizedBox(width: 8), + ), + ), + ), + ); + } +} + +class _MemoryCollectionItemView extends StatelessWidget { + static const width = 96.0; + static const height = width * 1.15; + + const _MemoryCollectionItemView({ + required this.coverUrl, + required this.label, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Align( + alignment: AlignmentDirectional.topStart, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: SizedBox( + width: width, + height: height, + child: Stack( + fit: StackFit.expand, + children: [ + PhotoListImage( + account: context.bloc.account, + previewUrl: coverUrl, + padding: const EdgeInsets.all(0), + ), + Positioned.fill( + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.center, + end: Alignment.bottomCenter, + colors: [Colors.transparent, Colors.black87], + ), + ), + ), + ), + Positioned.fill( + child: Align( + alignment: AlignmentDirectional.bottomStart, + child: Padding( + padding: const EdgeInsets.all(4), + child: Text( + label, + style: Theme.of(context).textTheme.labelLarge!.copyWith( + color: Theme.of(context).onDarkSurface, + ), + ), + ), + ), + ), + if (onTap != null) + Positioned.fill( + child: Material( + type: MaterialType.transparency, + child: InkWell( + onTap: onTap, + ), + ), + ), + ], + ), + ), + ), + ); + } + + final String? coverUrl; + final String label; + final VoidCallback? onTap; +} + +class _ScrollLabel extends StatelessWidget { + const _ScrollLabel(); + + @override + Widget build(BuildContext context) { + return _BlocSelector>( + selector: (state) => state.visibleItems, + builder: (context, visibleItems) { + final firstVisibleItem = + visibleItems.sorted().firstWhereOrNull((e) => e.item is _FileItem); + final date = firstVisibleItem?.item.as<_FileItem>()?.file.fdDateTime; + if (date == null) { + return const SizedBox.shrink(); + } + final text = DateFormat(DateFormat.YEAR_ABBR_MONTH, + Localizations.localeOf(context).languageCode) + .format(date.toLocal()); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: DefaultTextStyle( + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: Theme.of(context).colorScheme.onInverseSurface), + child: Text(text), + ), + ); + }, + ); + } +} diff --git a/app/lib/widget/home_photos2.dart b/app/lib/widget/home_photos2.dart new file mode 100644 index 00000000..638e605d --- /dev/null +++ b/app/lib/widget/home_photos2.dart @@ -0,0 +1,325 @@ +import 'dart:async'; + +import 'package:clock/clock.dart'; +import 'package:collection/collection.dart'; +import 'package:copy_with/copy_with.dart'; +import 'package:draggable_scrollbar/draggable_scrollbar.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:intl/intl.dart'; +import 'package:kiwi/kiwi.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/account.dart'; +import 'package:nc_photos/app_localizations.dart'; +import 'package:nc_photos/bloc_util.dart'; +import 'package:nc_photos/controller/account_controller.dart'; +import 'package:nc_photos/controller/account_pref_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/di_container.dart'; +import 'package:nc_photos/download_handler.dart'; +import 'package:nc_photos/entity/collection.dart'; +import 'package:nc_photos/entity/file_descriptor.dart'; +import 'package:nc_photos/entity/file_util.dart' as file_util; +import 'package:nc_photos/exception_event.dart'; +import 'package:nc_photos/exception_util.dart' as exception_util; +import 'package:nc_photos/flutter_util.dart' as flutter_util; +import 'package:nc_photos/k.dart' as k; +import 'package:nc_photos/language_util.dart' as language_util; +import 'package:nc_photos/snack_bar_manager.dart'; +import 'package:nc_photos/theme.dart'; +import 'package:nc_photos/theme/dimension.dart'; +import 'package:nc_photos/widget/collection_browser.dart'; +import 'package:nc_photos/widget/collection_picker.dart'; +import 'package:nc_photos/widget/file_sharer_dialog.dart'; +import 'package:nc_photos/widget/finger_listener.dart'; +import 'package:nc_photos/widget/home_app_bar.dart'; +import 'package:nc_photos/widget/navigation_bar_blur_filter.dart'; +import 'package:nc_photos/widget/network_thumbnail.dart'; +import 'package:nc_photos/widget/photo_list_item.dart'; +import 'package:nc_photos/widget/photo_list_util.dart' as photo_list_util; +import 'package:nc_photos/widget/selectable_item_list.dart'; +import 'package:nc_photos/widget/selection_app_bar.dart'; +import 'package:nc_photos/widget/sliver_visualized_scale.dart'; +import 'package:nc_photos/widget/viewer.dart'; +import 'package:np_async/np_async.dart'; +import 'package:np_codegen/np_codegen.dart'; +import 'package:np_collection/np_collection.dart'; +import 'package:np_common/object_util.dart'; +import 'package:np_common/or_null.dart'; +import 'package:to_string/to_string.dart'; +import 'package:visibility_detector/visibility_detector.dart'; + +part 'home_photos/app_bar.dart'; +part 'home_photos/bloc.dart'; +part 'home_photos/state_event.dart'; +part 'home_photos/type.dart'; +part 'home_photos/view.dart'; +part 'home_photos2.g.dart'; + +class HomePhotos2 extends StatelessWidget { + const HomePhotos2({super.key}); + + @override + Widget build(BuildContext context) { + final accountController = context.read(); + return BlocProvider( + create: (_) => _Bloc( + KiwiContainer().resolve(), + account: accountController.account, + controller: accountController.filesController, + prefController: context.read(), + accountPrefController: accountController.accountPrefController, + collectionsController: accountController.collectionsController, + ), + child: const _WrappedHomePhotos(), + ); + } +} + +class _WrappedHomePhotos extends StatefulWidget { + const _WrappedHomePhotos(); + + @override + State createState() => _WrappedHomePhotosState(); +} + +@npLog +class _WrappedHomePhotosState extends State<_WrappedHomePhotos> { + @override + void initState() { + super.initState(); + _bloc.add(const _LoadItems()); + } + + @override + Widget build(BuildContext context) { + return VisibilityDetector( + key: _key, + onVisibilityChanged: (info) { + final isVisible = info.visibleFraction >= 0.2; + if (isVisible != _isVisible) { + if (mounted) { + setState(() { + _isVisible = isVisible; + }); + } + } + }, + child: MultiBlocListener( + listeners: [ + _BlocListenerT>( + selector: (state) => state.files, + listener: (context, files) { + _bloc.add(_TransformItems(files)); + }, + ), + _BlocListenerT( + selector: (state) => state.error, + listener: (context, error) { + if (error != null && _isVisible == true) { + 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(content), + duration: k.snackBarDurationNormal, + )); + } + }, + ), + ], + child: FingerListener( + onFingerChanged: (finger) { + setState(() { + _finger = finger; + }); + }, + child: GestureDetector( + onScaleStart: (_) { + _bloc.add(const _StartScaling()); + }, + onScaleUpdate: (details) { + _bloc.add(_SetScale(details.scale)); + }, + onScaleEnd: (_) { + _bloc.add(const _EndScaling()); + }, + child: LayoutBuilder( + builder: (context, constraints) => _BlocBuilder( + buildWhen: (previous, current) => + previous.contentListMaxExtent != + current.contentListMaxExtent || + (previous.isEnableMemoryCollection && + previous.memoryCollections.isNotEmpty) != + (current.isEnableMemoryCollection && + current.memoryCollections.isNotEmpty), + builder: (context, state) { + final scrollExtent = _getScrollViewExtent( + context: context, + constraints: constraints, + hasMemoryCollection: state.isEnableMemoryCollection && + state.memoryCollections.isNotEmpty, + contentListMaxExtent: state.contentListMaxExtent, + ); + return Stack( + children: [ + DraggableScrollbar.semicircle( + controller: _scrollController, + overrideMaxScrollExtent: scrollExtent, + // status bar + app bar + topOffset: _getAppBarExtent(context), + bottomOffset: + AppDimension.of(context).homeBottomAppBarHeight, + labelTextBuilder: (_) => const _ScrollLabel(), + labelPadding: + const EdgeInsets.symmetric(horizontal: 40), + backgroundColor: Theme.of(context).elevate( + Theme.of(context).colorScheme.inverseSurface, 3), + heightScrollThumb: 60, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context) + .copyWith(scrollbars: false), + child: RefreshIndicator( + onRefresh: () async { + _bloc.add(const _Reload()); + await _bloc.stream.first; + }, + child: CustomScrollView( + controller: _scrollController, + physics: _finger >= 2 + ? const NeverScrollableScrollPhysics() + : null, + slivers: [ + _BlocSelector( + selector: (state) => + state.selectedItems.isEmpty, + builder: (context, isEmpty) => isEmpty + ? const _AppBar() + : const _SelectionAppBar(), + ), + _BlocBuilder( + buildWhen: (previous, current) => + (previous.isEnableMemoryCollection && + previous + .memoryCollections.isNotEmpty) != + (current.isEnableMemoryCollection && + current.memoryCollections.isNotEmpty), + builder: (context, state) { + if (state.isEnableMemoryCollection && + state.memoryCollections.isNotEmpty) { + return const _MemoryCollectionList(); + } else { + return const SliverToBoxAdapter(); + } + }, + ), + _BlocSelector( + selector: (state) => state.scale, + builder: (context, scale) => + SliverTransitionedScale( + scale: scale, + baseSliver: const _ContentList(), + overlaySliver: const _ScalingList(), + ), + ), + SliverToBoxAdapter( + child: SizedBox( + height: AppDimension.of(context) + .homeBottomAppBarHeight, + ), + ), + ], + ), + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: NavigationBarBlurFilter( + height: + AppDimension.of(context).homeBottomAppBarHeight, + ), + ), + ], + ); + }, + ), + ), + ), + ), + ), + ); + } + + /// Return the estimated scroll extent of the custom scroll view, or null + double? _getScrollViewExtent({ + required BuildContext context, + required BoxConstraints constraints, + required bool hasMemoryCollection, + required double? contentListMaxExtent, + }) { + if (contentListMaxExtent != null && constraints.hasBoundedHeight) { + final appBarExtent = _getAppBarExtent(context); + final bottomAppBarExtent = + AppDimension.of(context).homeBottomAppBarHeight; + // final metadataTaskHeaderExtent = _web?.getHeaderHeight() ?? 0; + final smartAlbumListHeight = + hasMemoryCollection ? _MemoryCollectionItem.height : 0; + // scroll extent = list height - widget viewport height + // + sliver app bar height + bottom app bar height + // + metadata task header height + smart album list height + final scrollExtent = contentListMaxExtent - + constraints.maxHeight + + appBarExtent + + bottomAppBarExtent + + // metadataTaskHeaderExtent + + smartAlbumListHeight; + _log.info("[_getScrollViewExtent] $contentListMaxExtent " + "- ${constraints.maxHeight} " + "+ $appBarExtent " + "+ $bottomAppBarExtent " + // "+ $metadataTaskHeaderExtent " + "+ $smartAlbumListHeight " + "= $scrollExtent"); + return scrollExtent; + } else { + return null; + } + } + + double _getAppBarExtent(BuildContext context) => + MediaQuery.of(context).padding.top + kToolbarHeight; + + late final _bloc = context.bloc; + + final _key = GlobalKey(); + final _scrollController = ScrollController(); + bool? _isVisible; + var _finger = 0; +} + +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 { + _Bloc get bloc => read<_Bloc>(); + // _State get state => bloc.state; + void addEvent(_Event event) => bloc.add(event); +} + +@npLog +// ignore: camel_case_types +class __ {} diff --git a/app/lib/widget/home_photos2.g.dart b/app/lib/widget/home_photos2.g.dart new file mode 100644 index 00000000..e562b685 --- /dev/null +++ b/app/lib/widget/home_photos2.g.dart @@ -0,0 +1,262 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'home_photos2.dart'; + +// ************************************************************************** +// CopyWithLintRuleGenerator +// ************************************************************************** + +// ignore_for_file: library_private_types_in_public_api, duplicate_ignore + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class $_StateCopyWithWorker { + _State call( + {List? files, + bool? isLoading, + List<_Item>? transformedItems, + Set<_Item>? selectedItems, + Set<_VisibleItem>? visibleItems, + bool? isEnableMemoryCollection, + List? memoryCollections, + double? contentListMaxExtent, + int? zoom, + double? scale, + ExceptionEvent? error}); +} + +class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { + _$_StateCopyWithWorkerImpl(this.that); + + @override + _State call( + {dynamic files, + dynamic isLoading, + dynamic transformedItems, + dynamic selectedItems, + dynamic visibleItems, + dynamic isEnableMemoryCollection, + dynamic memoryCollections, + dynamic contentListMaxExtent = copyWithNull, + dynamic zoom, + dynamic scale = copyWithNull, + dynamic error = copyWithNull}) { + return _State( + files: files as List? ?? that.files, + isLoading: isLoading as bool? ?? that.isLoading, + transformedItems: + transformedItems as List<_Item>? ?? that.transformedItems, + selectedItems: selectedItems as Set<_Item>? ?? that.selectedItems, + visibleItems: visibleItems as Set<_VisibleItem>? ?? that.visibleItems, + isEnableMemoryCollection: + isEnableMemoryCollection as bool? ?? that.isEnableMemoryCollection, + memoryCollections: + memoryCollections as List? ?? that.memoryCollections, + contentListMaxExtent: contentListMaxExtent == copyWithNull + ? that.contentListMaxExtent + : contentListMaxExtent as double?, + zoom: zoom as int? ?? that.zoom, + scale: scale == copyWithNull ? that.scale : scale as double?, + error: error == copyWithNull ? that.error : error as ExceptionEvent?); + } + + final _State that; +} + +extension $_StateCopyWith on _State { + $_StateCopyWithWorker get copyWith => _$copyWith; + $_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this); +} + +// ************************************************************************** +// NpLogGenerator +// ************************************************************************** + +extension _$_WrappedHomePhotosStateNpLog on _WrappedHomePhotosState { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.home_photos2._WrappedHomePhotosState"); +} + +extension _$__NpLog on __ { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.home_photos2.__"); +} + +extension _$_SelectionAppBarNpLog on _SelectionAppBar { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.home_photos2._SelectionAppBar"); +} + +extension _$_SelectionAppBarMenuNpLog on _SelectionAppBarMenu { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.home_photos2._SelectionAppBarMenu"); +} + +extension _$_BlocNpLog on _Bloc { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.home_photos2._Bloc"); +} + +extension _$_ContentListBodyNpLog on _ContentListBody { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.home_photos2._ContentListBody"); +} + +// ************************************************************************** +// ToStringGenerator +// ************************************************************************** + +extension _$_StateToString on _State { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_State {files: [length: ${files.length}], isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, visibleItems: {length: ${visibleItems.length}}, isEnableMemoryCollection: $isEnableMemoryCollection, memoryCollections: [length: ${memoryCollections.length}], contentListMaxExtent: ${contentListMaxExtent == null ? null : "${contentListMaxExtent!.toStringAsFixed(3)}"}, zoom: $zoom, scale: ${scale == null ? null : "${scale!.toStringAsFixed(3)}"}, error: $error}"; + } +} + +extension _$_LoadItemsToString on _LoadItems { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_LoadItems {}"; + } +} + +extension _$_ReloadToString on _Reload { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_Reload {}"; + } +} + +extension _$_TransformItemsToString on _TransformItems { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_TransformItems {items: [length: ${items.length}]}"; + } +} + +extension _$_OnItemTransformedToString on _OnItemTransformed { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_OnItemTransformed {items: [length: ${items.length}], memoryCollections: [length: ${memoryCollections.length}]}"; + } +} + +extension _$_SetSelectedItemsToString on _SetSelectedItems { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetSelectedItems {items: {length: ${items.length}}}"; + } +} + +extension _$_AddSelectedItemsToCollectionToString + on _AddSelectedItemsToCollection { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_AddSelectedItemsToCollection {collection: $collection}"; + } +} + +extension _$_ArchiveSelectedItemsToString on _ArchiveSelectedItems { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_ArchiveSelectedItems {}"; + } +} + +extension _$_DeleteSelectedItemsToString on _DeleteSelectedItems { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_DeleteSelectedItems {}"; + } +} + +extension _$_DownloadSelectedItemsToString on _DownloadSelectedItems { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_DownloadSelectedItems {}"; + } +} + +extension _$_AddVisibleItemToString on _AddVisibleItem { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_AddVisibleItem {item: $item}"; + } +} + +extension _$_RemoveVisibleItemToString on _RemoveVisibleItem { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_RemoveVisibleItem {item: $item}"; + } +} + +extension _$_SetContentListMaxExtentToString on _SetContentListMaxExtent { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetContentListMaxExtent {value: ${value == null ? null : "${value!.toStringAsFixed(3)}"}}"; + } +} + +extension _$_StartScalingToString on _StartScaling { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_StartScaling {}"; + } +} + +extension _$_EndScalingToString on _EndScaling { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_EndScaling {}"; + } +} + +extension _$_SetScaleToString on _SetScale { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetScale {scale: ${scale.toStringAsFixed(3)}}"; + } +} + +extension _$_SetEnableMemoryCollectionToString on _SetEnableMemoryCollection { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetEnableMemoryCollection {value: $value}"; + } +} + +extension _$_SetErrorToString on _SetError { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetError {error: $error, stackTrace: $stackTrace}"; + } +} + +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/selectable_item_list.dart b/app/lib/widget/selectable_item_list.dart index 60b25b35..cd93a0f1 100644 --- a/app/lib/widget/selectable_item_list.dart +++ b/app/lib/widget/selectable_item_list.dart @@ -8,6 +8,7 @@ import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/session_storage.dart'; import 'package:nc_photos/snack_bar_manager.dart'; +import 'package:nc_photos/widget/measurable_item_list.dart'; import 'package:nc_photos/widget/selectable.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:np_collection/np_collection.dart'; @@ -36,6 +37,7 @@ class SelectableItemList this.indicatorAlignment = Alignment.topLeft, this.onItemTap, this.onSelectionChange, + this.onMaxExtentChange, }); @override @@ -53,6 +55,7 @@ class SelectableItemList final void Function(BuildContext context, int index, T metadata)? onItemTap; final void Function(BuildContext context, Set selected)? onSelectionChange; + final ValueChanged? onMaxExtentChange; } @npLog @@ -90,32 +93,46 @@ class _SelectableItemListState } Widget _buildBody(BuildContext context) { - return SliverStaggeredGrid.extentBuilder( - key: ObjectKey(widget.maxCrossAxisExtent), - maxCrossAxisExtent: widget.maxCrossAxisExtent, - itemCount: widget.items.length, - itemBuilder: (context, i) { - final meta = widget.items[i]; - if (meta.isSelectable) { - return Selectable( - isSelected: widget.selectedItems.contains(meta), - iconSize: 32, - childBorderRadius: - widget.childBorderRadius ?? BorderRadius.circular(24), - indicatorAlignment: widget.indicatorAlignment, - onTap: _isSelecting - ? () => _onItemSelect(context, i, meta) - : () => _onItemTap(context, i, meta), - onLongPress: () => _onItemLongPress(i, meta), - child: widget.itemBuilder(context, i, meta), - ); - } else { - return widget.itemBuilder(context, i, meta); - } - }, - staggeredTileBuilder: (i) => - widget.staggeredTileBuilder(i, widget.items[i]), - ); + if (widget.onMaxExtentChange != null) { + return MeasurableItemList( + key: _listKey, + maxCrossAxisExtent: widget.maxCrossAxisExtent, + itemCount: widget.items.length, + itemBuilder: _buildItem, + staggeredTileBuilder: (i) => + widget.staggeredTileBuilder(i, widget.items[i]), + onMaxExtentChanged: widget.onMaxExtentChange, + ); + } else { + return SliverStaggeredGrid.extentBuilder( + key: ObjectKey(widget.maxCrossAxisExtent), + maxCrossAxisExtent: widget.maxCrossAxisExtent, + itemCount: widget.items.length, + itemBuilder: _buildItem, + staggeredTileBuilder: (i) => + widget.staggeredTileBuilder(i, widget.items[i]), + ); + } + } + + Widget _buildItem(BuildContext context, int index) { + final meta = widget.items[index]; + if (meta.isSelectable) { + return Selectable( + isSelected: widget.selectedItems.contains(meta), + iconSize: 32, + childBorderRadius: + widget.childBorderRadius ?? BorderRadius.circular(24), + indicatorAlignment: widget.indicatorAlignment, + onTap: _isSelecting + ? () => _onItemSelect(context, index, meta) + : () => _onItemTap(context, index, meta), + onLongPress: () => _onItemLongPress(index, meta), + child: widget.itemBuilder(context, index, meta), + ); + } else { + return widget.itemBuilder(context, index, meta); + } } void _onItemTap(BuildContext context, int index, T metadata) { @@ -214,6 +231,12 @@ class _SelectableItemListState "[_remapSelected] ${widget.selectedItems.length - newSelected.length} items not found in the new list"); } widget.onSelectionChange?.call(context, newSelected); + // TODO remap lastSelectPosition + + _log.info("[_remapSelected] updateListHeight: list item changed"); + WidgetsBinding.instance.addPostFrameCallback((_) => + (_listKey.currentState as MeasurableItemListState?) + ?.updateListHeight()); } bool get _isSelecting => widget.selectedItems.isNotEmpty; @@ -222,4 +245,6 @@ class _SelectableItemListState final _keyboardFocus = FocusNode(); int? _lastSelectPosition; bool _isKeyboardRangeSelecting = false; + + final _listKey = GlobalKey(); } diff --git a/app/pubspec.lock b/app/pubspec.lock index 72dc4073..6701d09f 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -1254,7 +1254,7 @@ packages: source: hosted version: "2.1.0" provider: - dependency: transitive + dependency: "direct main" description: name: provider sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f diff --git a/app/pubspec.yaml b/app/pubspec.yaml index f781896f..26d44623 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -136,6 +136,7 @@ dependencies: page_view_indicators: ^2.0.0 path: ^1.8.0 path_provider: ^2.0.15 + provider: any rxdart: ^0.27.7 screen_brightness: ^0.2.2 shared_preferences: ^2.0.8 diff --git a/np_collection/lib/np_collection.dart b/np_collection/lib/np_collection.dart index d3fff8b0..5373cb4e 100644 --- a/np_collection/lib/np_collection.dart +++ b/np_collection/lib/np_collection.dart @@ -5,3 +5,4 @@ export 'src/iterator_extension.dart'; export 'src/list_extension.dart'; export 'src/list_util.dart'; export 'src/map_extension.dart'; +export 'src/set_util.dart'; diff --git a/np_collection/lib/src/set_util.dart b/np_collection/lib/src/set_util.dart new file mode 100644 index 00000000..9c9819d0 --- /dev/null +++ b/np_collection/lib/src/set_util.dart @@ -0,0 +1,5 @@ +extension SetExtension on Set { + Set added(T element) => toSet()..add(element); + + Set removed(T element) => toSet()..remove(element); +} From f1a340d5507d802ff967277197acb28992cc0a76 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sat, 13 Jan 2024 03:29:47 +0800 Subject: [PATCH 05/42] Rewrite ArchiveBrowser --- app/lib/widget/archive_browser.dart | 397 +++++++----------- app/lib/widget/archive_browser.g.dart | 176 +++++++- app/lib/widget/archive_browser/app_bar.dart | 41 ++ app/lib/widget/archive_browser/bloc.dart | 217 ++++++++++ .../widget/archive_browser/state_event.dart | 140 ++++++ app/lib/widget/archive_browser/type.dart | 109 +++++ app/lib/widget/archive_browser/view.dart | 90 ++++ app/lib/widget/home_collections.dart | 5 +- app/lib/widget/my_app.dart | 16 +- 9 files changed, 919 insertions(+), 272 deletions(-) create mode 100644 app/lib/widget/archive_browser/app_bar.dart create mode 100644 app/lib/widget/archive_browser/bloc.dart create mode 100644 app/lib/widget/archive_browser/state_event.dart create mode 100644 app/lib/widget/archive_browser/type.dart create mode 100644 app/lib/widget/archive_browser/view.dart diff --git a/app/lib/widget/archive_browser.dart b/app/lib/widget/archive_browser.dart index 9e9f8ba7..4e36b10c 100644 --- a/app/lib/widget/archive_browser.dart +++ b/app/lib/widget/archive_browser.dart @@ -1,304 +1,197 @@ -import 'dart:ui'; +import 'dart:async'; +import 'package:collection/collection.dart'; +import 'package:copy_with/copy_with.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:kiwi/kiwi.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_localizations.dart'; -import 'package:nc_photos/bloc/scan_account_dir.dart'; -import 'package:nc_photos/debug_util.dart'; -import 'package:nc_photos/di_container.dart'; -import 'package:nc_photos/entity/file.dart'; +import 'package:nc_photos/bloc_util.dart'; +import 'package:nc_photos/controller/account_controller.dart'; +import 'package:nc_photos/controller/files_controller.dart'; +import 'package:nc_photos/controller/pref_controller.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; -import 'package:nc_photos/entity/pref.dart'; +import 'package:nc_photos/entity/file_util.dart' as file_util; +import 'package:nc_photos/exception_event.dart'; import 'package:nc_photos/exception_util.dart' as exception_util; +import 'package:nc_photos/flutter_util.dart' as flutter_util; import 'package:nc_photos/k.dart' as k; -import 'package:nc_photos/language_util.dart' as language_util; -import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/snack_bar_manager.dart'; -import 'package:nc_photos/use_case/inflate_file_descriptor.dart'; -import 'package:nc_photos/use_case/update_property.dart'; -import 'package:nc_photos/widget/builder/photo_list_item_builder.dart'; import 'package:nc_photos/widget/empty_list_indicator.dart'; +import 'package:nc_photos/widget/finger_listener.dart'; +import 'package:nc_photos/widget/network_thumbnail.dart'; +import 'package:nc_photos/widget/page_visibility_mixin.dart'; import 'package:nc_photos/widget/photo_list_item.dart'; import 'package:nc_photos/widget/photo_list_util.dart' as photo_list_util; -import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart'; +import 'package:nc_photos/widget/selectable_item_list.dart'; import 'package:nc_photos/widget/selection_app_bar.dart'; +import 'package:nc_photos/widget/sliver_visualized_scale.dart'; import 'package:nc_photos/widget/viewer.dart'; -import 'package:nc_photos/widget/zoom_menu_button.dart'; import 'package:np_async/np_async.dart'; import 'package:np_codegen/np_codegen.dart'; -import 'package:np_common/object_util.dart'; +import 'package:np_common/or_null.dart'; +import 'package:to_string/to_string.dart'; part 'archive_browser.g.dart'; +part 'archive_browser/app_bar.dart'; +part 'archive_browser/bloc.dart'; +part 'archive_browser/state_event.dart'; +part 'archive_browser/type.dart'; +part 'archive_browser/view.dart'; -class ArchiveBrowserArguments { - ArchiveBrowserArguments(this.account); - - final Account account; -} - -class ArchiveBrowser extends StatefulWidget { +class ArchiveBrowser extends StatelessWidget { static const routeName = "/archive-browser"; - static Route buildRoute(ArchiveBrowserArguments args) => MaterialPageRoute( - builder: (context) => ArchiveBrowser.fromArgs(args), + static Route buildRoute() => MaterialPageRoute( + builder: (_) => const ArchiveBrowser(), ); - const ArchiveBrowser({ - Key? key, - required this.account, - }) : super(key: key); - - ArchiveBrowser.fromArgs(ArchiveBrowserArguments args, {Key? key}) - : this( - key: key, - account: args.account, - ); + const ArchiveBrowser({super.key}); @override - createState() => _ArchiveBrowserState(); + Widget build(BuildContext context) { + final accountController = context.read(); + return BlocProvider( + create: (_) => _Bloc( + account: accountController.account, + controller: accountController.filesController, + prefController: context.read(), + ), + child: const _WrappedArchiveBrowser(), + ); + } +} - final Account account; +class _WrappedArchiveBrowser extends StatefulWidget { + const _WrappedArchiveBrowser(); + + @override + State createState() => _WrappedArchiveBrowserState(); } @npLog -class _ArchiveBrowserState extends State - with SelectableItemStreamListMixin { +class _WrappedArchiveBrowserState extends State<_WrappedArchiveBrowser> + with RouteAware, PageVisibilityMixin { @override - initState() { + void initState() { super.initState(); - _initBloc(); - _thumbZoomLevel = Pref().getAlbumBrowserZoomLevelOr(0); + _bloc.add(const _LoadItems()); } @override - build(BuildContext context) { + Widget build(BuildContext context) { return Scaffold( - body: BlocListener( - bloc: _bloc, - listener: (context, state) => _onStateChange(context, state), - child: BlocBuilder( - bloc: _bloc, - builder: (context, state) => _buildContent(context, state), - ), - ), - ); - } - - @override - onItemTap(SelectableItem item, int index) { - item.as()?.run((fileItem) { - Navigator.pushNamed( - context, - Viewer.routeName, - arguments: - ViewerArguments(widget.account, _backingFiles, fileItem.fileIndex), - ); - }); - } - - void _initBloc() { - if (_bloc.state is ScanAccountDirBlocInit) { - _log.info("[_initBloc] Initialize bloc"); - _reqQuery(); - } else { - // process the current state - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _onStateChange(context, _bloc.state); - }); - } - }); - } - } - - Widget _buildContent(BuildContext context, ScanAccountDirBlocState state) { - if (state is ScanAccountDirBlocSuccess && - !_buildItemQueue.isProcessing && - itemStreamListItems.isEmpty) { - return Column( - children: [ - AppBar( - title: Text(L10n.global().albumArchiveLabel), - elevation: 0, + body: MultiBlocListener( + listeners: [ + _BlocListenerT>( + selector: (state) => state.files, + listener: (context, files) { + _bloc.add(_TransformItems(files)); + }, ), - Expanded( - child: EmptyListIndicator( - icon: Icons.archive_outlined, - text: L10n.global().listEmptyText, - ), + _BlocListenerT( + selector: (state) => state.error, + listener: (context, error) { + if (error != null && isPageVisible()) { + if (error.error is _UnarchiveFailedError) { + SnackBarManager().showSnackBar(SnackBar( + content: Text(L10n.global() + .unarchiveSelectedFailureNotification( + (error.error as _UnarchiveFailedError).count)), + duration: k.snackBarDurationNormal, + )); + } else { + SnackBarManager().showSnackBar(SnackBar( + content: Text(exception_util.toUserString(error.error)), + duration: k.snackBarDurationNormal, + )); + } + } + }, ), ], - ); - } else { - return Stack( - children: [ - buildItemStreamListOuter( - context, + child: FingerListener( + onFingerChanged: (finger) { + setState(() { + _finger = finger; + }); + }, + child: GestureDetector( + onScaleStart: (_) { + _bloc.add(const _StartScaling()); + }, + onScaleUpdate: (details) { + _bloc.add(_SetScale(details.scale)); + }, + onScaleEnd: (_) { + _bloc.add(const _EndScaling()); + }, child: CustomScrollView( + physics: + _finger >= 2 ? const NeverScrollableScrollPhysics() : null, slivers: [ - _buildAppBar(context), - buildItemStreamList( - maxCrossAxisExtent: _thumbSize.toDouble(), + _BlocSelector( + selector: (state) => state.selectedItems.isEmpty, + builder: (context, isEmpty) => + isEmpty ? const _AppBar() : const _SelectionAppBar(), + ), + SliverToBoxAdapter( + child: _BlocSelector( + selector: (state) => state.isLoading, + builder: (context, isLoading) => isLoading + ? const LinearProgressIndicator() + : const SizedBox(height: 4), + ), + ), + _BlocBuilder( + buildWhen: (previous, current) => + previous.transformedItems.isEmpty != + current.transformedItems.isEmpty || + previous.isLoading != current.isLoading, + builder: (context, state) => state.transformedItems.isEmpty && + !state.isLoading + ? SliverFillRemaining( + hasScrollBody: false, + child: EmptyListIndicator( + icon: Icons.archive_outlined, + text: L10n.global().listEmptyText, + ), + ) + : _BlocSelector( + selector: (state) => state.scale, + builder: (context, scale) => SliverTransitionedScale( + scale: scale, + baseSliver: const _ContentList(), + overlaySliver: const _ScalingList(), + ), + ), ), ], ), ), - if (state is ScanAccountDirBlocLoading || - _buildItemQueue.isProcessing) - const Align( - alignment: Alignment.bottomCenter, - child: LinearProgressIndicator(), - ), - ], - ); - } - } - - Widget _buildAppBar(BuildContext context) { - if (isSelectionMode) { - return _buildSelectionAppBar(context); - } else { - return _buildNormalAppBar(context); - } - } - - Widget _buildSelectionAppBar(BuildContext context) { - return SelectionAppBar( - count: selectedListItems.length, - onClosePressed: () { - setState(() { - clearSelectedItems(); - }); - }, - actions: [ - IconButton( - icon: const Icon(Icons.unarchive), - tooltip: L10n.global().unarchiveTooltip, - onPressed: () { - _onSelectionAppBarUnarchivePressed(); - }, ), - ], - ); - } - - Widget _buildNormalAppBar(BuildContext context) { - return SliverAppBar( - title: Text(L10n.global().albumArchiveLabel), - floating: true, - actions: [ - ZoomMenuButton( - initialZoom: _thumbZoomLevel, - minZoom: 0, - maxZoom: 2, - onZoomChanged: (value) { - setState(() { - _thumbZoomLevel = value.round(); - }); - Pref().setAlbumBrowserZoomLevel(_thumbZoomLevel); - }, - ), - ], - ); - } - - void _onStateChange(BuildContext context, ScanAccountDirBlocState state) { - if (state is ScanAccountDirBlocInit) { - itemStreamListItems = []; - } else if (state is ScanAccountDirBlocSuccess || - state is ScanAccountDirBlocLoading) { - _transformItems(state.files); - } else if (state is ScanAccountDirBlocFailure) { - _transformItems(state.files); - SnackBarManager().showSnackBar(SnackBar( - content: Text(exception_util.toUserString(state.exception)), - duration: k.snackBarDurationNormal, - )); - } else if (state is ScanAccountDirBlocInconsistent) { - _reqQuery(); - } - } - - Future _onSelectionAppBarUnarchivePressed() async { - SnackBarManager().showSnackBar(SnackBar( - content: Text(L10n.global() - .unarchiveSelectedProcessingNotification(selectedListItems.length)), - duration: k.snackBarDurationShort, - )); - final selection = selectedListItems - .whereType() - .map((e) => e.file) - .toList(); - setState(() { - clearSelectedItems(); - }); - final c = KiwiContainer().resolve(); - final selectedFiles = - await InflateFileDescriptor(c)(widget.account, selection); - final failures = []; - for (final f in selectedFiles) { - try { - await UpdateProperty(c) - .updateIsArchived(widget.account, f, false); - } catch (e, stacktrace) { - _log.shout( - "[_onSelectionAppBarUnarchivePressed] Failed while unarchiving file: ${logFilename(f.path)}", - e, - stacktrace); - failures.add(f); - } - } - if (failures.isEmpty) { - SnackBarManager().showSnackBar(SnackBar( - content: Text(L10n.global().unarchiveSelectedSuccessNotification), - duration: k.snackBarDurationNormal, - )); - } else { - SnackBarManager().showSnackBar(SnackBar( - content: Text(L10n.global() - .unarchiveSelectedFailureNotification(failures.length)), - duration: k.snackBarDurationNormal, - )); - } - } - - void _transformItems(List files) { - _buildItemQueue.addJob( - PhotoListItemBuilderArguments( - widget.account, - files, - isArchived: true, - sorter: photoListFileDateTimeSorter, - locale: language_util.getSelectedLocale() ?? - PlatformDispatcher.instance.locale, ), - buildPhotoListItem, - (result) { - if (mounted) { - setState(() { - _backingFiles = result.backingFiles; - itemStreamListItems = result.listItems; - }); - } - }, ); } - void _reqQuery() { - _bloc.add(const ScanAccountDirBlocQuery()); - } + late final _bloc = context.bloc; - late final _bloc = ScanAccountDirBloc.of(widget.account); - - var _backingFiles = []; - - final _buildItemQueue = - ComputeQueue(); - - var _thumbZoomLevel = 0; - int get _thumbSize => photo_list_util.getThumbSize(_thumbZoomLevel); + var _finger = 0; } + +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 { + _Bloc get bloc => read<_Bloc>(); + // _State get state => bloc.state; + void addEvent(_Event event) => bloc.add(event); +} + +@npLog +// ignore: camel_case_types +class __ {} diff --git a/app/lib/widget/archive_browser.g.dart b/app/lib/widget/archive_browser.g.dart index b58eeda6..1520b792 100644 --- a/app/lib/widget/archive_browser.g.dart +++ b/app/lib/widget/archive_browser.g.dart @@ -2,13 +2,185 @@ part of 'archive_browser.dart'; +// ************************************************************************** +// CopyWithLintRuleGenerator +// ************************************************************************** + +// ignore_for_file: library_private_types_in_public_api, duplicate_ignore + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class $_StateCopyWithWorker { + _State call( + {List? files, + bool? isLoading, + List<_Item>? transformedItems, + Set<_Item>? selectedItems, + Set<_VisibleItem>? visibleItems, + int? zoom, + double? scale, + ExceptionEvent? error}); +} + +class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { + _$_StateCopyWithWorkerImpl(this.that); + + @override + _State call( + {dynamic files, + dynamic isLoading, + dynamic transformedItems, + dynamic selectedItems, + dynamic visibleItems, + dynamic zoom, + dynamic scale = copyWithNull, + dynamic error = copyWithNull}) { + return _State( + files: files as List? ?? that.files, + isLoading: isLoading as bool? ?? that.isLoading, + transformedItems: + transformedItems as List<_Item>? ?? that.transformedItems, + selectedItems: selectedItems as Set<_Item>? ?? that.selectedItems, + visibleItems: visibleItems as Set<_VisibleItem>? ?? that.visibleItems, + zoom: zoom as int? ?? that.zoom, + scale: scale == copyWithNull ? that.scale : scale as double?, + error: error == copyWithNull ? that.error : error as ExceptionEvent?); + } + + final _State that; +} + +extension $_StateCopyWith on _State { + $_StateCopyWithWorker get copyWith => _$copyWith; + $_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this); +} + // ************************************************************************** // NpLogGenerator // ************************************************************************** -extension _$_ArchiveBrowserStateNpLog on _ArchiveBrowserState { +extension _$_WrappedArchiveBrowserStateNpLog on _WrappedArchiveBrowserState { // ignore: unused_element Logger get _log => log; - static final log = Logger("widget.archive_browser._ArchiveBrowserState"); + static final log = + Logger("widget.archive_browser._WrappedArchiveBrowserState"); +} + +extension _$__NpLog on __ { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.archive_browser.__"); +} + +extension _$_SelectionAppBarNpLog on _SelectionAppBar { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.archive_browser._SelectionAppBar"); +} + +extension _$_BlocNpLog on _Bloc { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.archive_browser._Bloc"); +} + +extension _$_ContentListBodyNpLog on _ContentListBody { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.archive_browser._ContentListBody"); +} + +// ************************************************************************** +// ToStringGenerator +// ************************************************************************** + +extension _$_UnarchiveFailedErrorToString on _UnarchiveFailedError { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_UnarchiveFailedError {count: $count}"; + } +} + +extension _$_StateToString on _State { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_State {files: [length: ${files.length}], isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, visibleItems: {length: ${visibleItems.length}}, zoom: $zoom, scale: ${scale == null ? null : "${scale!.toStringAsFixed(3)}"}, error: $error}"; + } +} + +extension _$_LoadItemsToString on _LoadItems { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_LoadItems {}"; + } +} + +extension _$_ReloadToString on _Reload { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_Reload {}"; + } +} + +extension _$_TransformItemsToString on _TransformItems { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_TransformItems {items: [length: ${items.length}]}"; + } +} + +extension _$_OnItemTransformedToString on _OnItemTransformed { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_OnItemTransformed {items: [length: ${items.length}]}"; + } +} + +extension _$_SetSelectedItemsToString on _SetSelectedItems { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetSelectedItems {items: {length: ${items.length}}}"; + } +} + +extension _$_UnarchiveSelectedItemsToString on _UnarchiveSelectedItems { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_UnarchiveSelectedItems {}"; + } +} + +extension _$_StartScalingToString on _StartScaling { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_StartScaling {}"; + } +} + +extension _$_EndScalingToString on _EndScaling { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_EndScaling {}"; + } +} + +extension _$_SetScaleToString on _SetScale { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetScale {scale: ${scale.toStringAsFixed(3)}}"; + } +} + +extension _$_SetErrorToString on _SetError { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetError {error: $error, stackTrace: $stackTrace}"; + } } diff --git a/app/lib/widget/archive_browser/app_bar.dart b/app/lib/widget/archive_browser/app_bar.dart new file mode 100644 index 00000000..e1a32403 --- /dev/null +++ b/app/lib/widget/archive_browser/app_bar.dart @@ -0,0 +1,41 @@ +part of '../archive_browser.dart'; + +class _AppBar extends StatelessWidget { + const _AppBar(); + + @override + Widget build(BuildContext context) { + return SliverAppBar( + title: Text(L10n.global().albumArchiveLabel), + floating: true, + ); + } +} + +@npLog +class _SelectionAppBar extends StatelessWidget { + const _SelectionAppBar(); + + @override + Widget build(BuildContext context) { + return _BlocBuilder( + buildWhen: (previous, current) => + previous.selectedItems != current.selectedItems, + builder: (context, state) => SelectionAppBar( + count: state.selectedItems.length, + onClosePressed: () { + context.addEvent(const _SetSelectedItems(items: {})); + }, + actions: [ + IconButton( + icon: const Icon(Icons.unarchive_outlined), + tooltip: L10n.global().unarchiveTooltip, + onPressed: () { + context.addEvent(const _UnarchiveSelectedItems()); + }, + ), + ], + ), + ); + } +} diff --git a/app/lib/widget/archive_browser/bloc.dart b/app/lib/widget/archive_browser/bloc.dart new file mode 100644 index 00000000..1c7e1b1f --- /dev/null +++ b/app/lib/widget/archive_browser/bloc.dart @@ -0,0 +1,217 @@ +part of '../archive_browser.dart'; + +@npLog +class _Bloc extends Bloc<_Event, _State> with BlocLogger { + _Bloc({ + required this.account, + required this.controller, + required this.prefController, + }) : super(_State.init( + zoom: prefController.albumBrowserZoomLevel.value, + )) { + on<_LoadItems>(_onLoad); + on<_TransformItems>(_onTransformItems); + on<_OnItemTransformed>(_onOnItemTransformed); + + on<_SetSelectedItems>(_onSetSelectedItems); + on<_UnarchiveSelectedItems>(_onUnarchiveSelectedItems); + + on<_StartScaling>(_onStartScaling); + on<_EndScaling>(_onEndScaling); + on<_SetScale>(_onSetScale); + + on<_SetError>(_onSetError); + } + + @override + Future close() { + for (final s in _subscriptions) { + s.cancel(); + } + return super.close(); + } + + @override + String get tag => _log.fullName; + + @override + bool Function(dynamic, dynamic)? get shouldLog => (currentState, nextState) { + currentState = currentState as _State; + nextState = nextState as _State; + return currentState.scale == nextState.scale && + currentState.visibleItems == nextState.visibleItems; + }; + + @override + void onError(Object error, StackTrace stackTrace) { + // we need this to prevent onError being triggered recursively + if (!isClosed && !_isHandlingError) { + _isHandlingError = true; + try { + add(_SetError(error, stackTrace)); + } catch (_) {} + _isHandlingError = false; + } + super.onError(error, stackTrace); + } + + Future _onLoad(_LoadItems ev, Emitter<_State> emit) { + _log.info(ev); + return emit.forEach( + controller.stream, + onData: (data) => state.copyWith( + files: data.data, + isLoading: data.hasNext || _itemTransformerQueue.isProcessing, + ), + onError: (e, stackTrace) { + _log.severe("[_onLoad] Uncaught exception", e, stackTrace); + return state.copyWith( + isLoading: _itemTransformerQueue.isProcessing, + error: ExceptionEvent(e, stackTrace), + ); + }, + ); + } + + void _onTransformItems(_TransformItems ev, Emitter<_State> emit) { + _log.info(ev); + _transformItems(ev.items); + emit(state.copyWith(isLoading: true)); + } + + void _onOnItemTransformed(_OnItemTransformed ev, Emitter<_State> emit) { + _log.info(ev); + emit(state.copyWith( + transformedItems: ev.items, + isLoading: _itemTransformerQueue.isProcessing, + )); + } + + void _onSetSelectedItems(_SetSelectedItems ev, Emitter<_State> emit) { + _log.info(ev); + emit(state.copyWith(selectedItems: ev.items)); + } + + void _onUnarchiveSelectedItems( + _UnarchiveSelectedItems ev, Emitter<_State> emit) { + _log.info(ev); + final selected = state.selectedItems; + _clearSelection(emit); + final selectedFiles = + selected.whereType<_FileItem>().map((e) => e.file).toList(); + if (selectedFiles.isNotEmpty) { + controller.updateProperty( + selectedFiles, + isArchived: const OrNull(false), + errorBuilder: (fileIds) => _UnarchiveFailedError(fileIds.length), + ); + } + } + + void _onStartScaling(_StartScaling ev, Emitter<_State> emit) { + _log.info(ev); + } + + void _onEndScaling(_EndScaling ev, Emitter<_State> emit) { + _log.info(ev); + if (state.scale == null) { + return; + } + final int newZoom; + if (state.scale! >= 1.25) { + // scale up + newZoom = (state.zoom + 1).clamp(-1, 2); + } else if (state.scale! <= 0.75) { + newZoom = (state.zoom - 1).clamp(-1, 2); + } else { + newZoom = state.zoom; + } + emit(state.copyWith( + zoom: newZoom, + scale: null, + )); + unawaited(prefController.setAlbumBrowserZoomLevel(newZoom)); + } + + void _onSetScale(_SetScale ev, Emitter<_State> emit) { + // _log.info(ev); + emit(state.copyWith(scale: ev.scale)); + } + + void _onSetError(_SetError ev, Emitter<_State> emit) { + _log.info(ev); + emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); + } + + Future _transformItems(List files) async { + _log.info("[_transformItems] Queue ${files.length} items"); + _itemTransformerQueue.addJob( + _ItemTransformerArgument( + account: account, + files: files, + ), + _buildItem, + (result) { + safeAdd(_OnItemTransformed(result.items)); + }, + ); + } + + void _clearSelection(Emitter<_State> emit) { + emit(state.copyWith(selectedItems: const {})); + } + + final Account account; + final FilesController controller; + final PrefController prefController; + + final _itemTransformerQueue = + ComputeQueue<_ItemTransformerArgument, _ItemTransformerResult>(); + final _subscriptions = []; + var _isHandlingError = false; +} + +@toString +class _UnarchiveFailedError implements Exception { + const _UnarchiveFailedError(this.count); + + @override + String toString() => _$toString(); + + final int count; +} + +_ItemTransformerResult _buildItem(_ItemTransformerArgument arg) { + final sortedFiles = arg.files + .where((f) => f.fdIsArchived == true) + .sorted(compareFileDescriptorDateTimeDescending); + + final transformed = <_Item>[]; + for (int i = 0; i < sortedFiles.length; ++i) { + final file = sortedFiles[i]; + final item = _buildSingleItem(arg.account, file); + if (item == null) { + continue; + } + transformed.add(item); + } + return _ItemTransformerResult(items: transformed); +} + +_Item? _buildSingleItem(Account account, FileDescriptor file) { + if (file_util.isSupportedImageFormat(file)) { + return _PhotoItem( + file: file, + account: account, + ); + } else if (file_util.isSupportedVideoFormat(file)) { + return _VideoItem( + file: file, + account: account, + ); + } else { + _$__NpLog.log + .shout("[_buildSingleItem] Unsupported file format: ${file.fdMime}"); + return null; + } +} diff --git a/app/lib/widget/archive_browser/state_event.dart b/app/lib/widget/archive_browser/state_event.dart new file mode 100644 index 00000000..d7ca478b --- /dev/null +++ b/app/lib/widget/archive_browser/state_event.dart @@ -0,0 +1,140 @@ +part of '../archive_browser.dart'; + +@genCopyWith +@toString +class _State { + const _State({ + required this.files, + required this.isLoading, + required this.transformedItems, + required this.selectedItems, + required this.visibleItems, + required this.zoom, + this.scale, + this.error, + }); + + factory _State.init({ + required int zoom, + }) => + _State( + files: const [], + isLoading: false, + transformedItems: const [], + selectedItems: const {}, + visibleItems: const {}, + zoom: zoom, + ); + + @override + String toString() => _$toString(); + + final List files; + final bool isLoading; + final List<_Item> transformedItems; + final Set<_Item> selectedItems; + final Set<_VisibleItem> visibleItems; + + final int zoom; + final double? scale; + + final ExceptionEvent? error; +} + +abstract class _Event {} + +/// Load the files +@toString +class _LoadItems implements _Event { + const _LoadItems(); + + @override + String toString() => _$toString(); +} + +@toString +class _Reload implements _Event { + const _Reload(); + + @override + String toString() => _$toString(); +} + +/// Transform the file list (e.g., filtering, sorting, etc) +@toString +class _TransformItems implements _Event { + const _TransformItems(this.items); + + @override + String toString() => _$toString(); + + final List items; +} + +@toString +class _OnItemTransformed implements _Event { + const _OnItemTransformed(this.items); + + @override + String toString() => _$toString(); + + final List<_Item> items; +} + +/// Set the currently selected items +@toString +class _SetSelectedItems implements _Event { + const _SetSelectedItems({ + required this.items, + }); + + @override + String toString() => _$toString(); + + final Set<_Item> items; +} + +@toString +class _UnarchiveSelectedItems implements _Event { + const _UnarchiveSelectedItems(); + + @override + String toString() => _$toString(); +} + +@toString +class _StartScaling implements _Event { + const _StartScaling(); + + @override + String toString() => _$toString(); +} + +@toString +class _EndScaling implements _Event { + const _EndScaling(); + + @override + String toString() => _$toString(); +} + +@toString +class _SetScale implements _Event { + const _SetScale(this.scale); + + @override + String toString() => _$toString(); + + final double scale; +} + +@toString +class _SetError implements _Event { + const _SetError(this.error, [this.stackTrace]); + + @override + String toString() => _$toString(); + + final Object error; + final StackTrace? stackTrace; +} diff --git a/app/lib/widget/archive_browser/type.dart b/app/lib/widget/archive_browser/type.dart new file mode 100644 index 00000000..b2e97d85 --- /dev/null +++ b/app/lib/widget/archive_browser/type.dart @@ -0,0 +1,109 @@ +part of '../archive_browser.dart'; + +abstract class _Item implements SelectableItemMetadata { + const _Item(); + + StaggeredTile get staggeredTile; + + Widget buildWidget(BuildContext context); +} + +abstract class _FileItem extends _Item { + const _FileItem({ + required this.file, + }); + + @override + bool get isSelectable => true; + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is _FileItem && file.compareServerIdentity(other.file)); + + @override + int get hashCode => file.identityHashCode; + + final FileDescriptor file; +} + +class _PhotoItem extends _FileItem { + _PhotoItem({ + required super.file, + required this.account, + }) : _previewUrl = NetworkRectThumbnail.imageUrlForFile(account, file); + + @override + StaggeredTile get staggeredTile => const StaggeredTile.count(1, 1); + + @override + Widget buildWidget(BuildContext context) { + return PhotoListImage( + account: account, + previewUrl: _previewUrl, + isGif: file.fdMime == "image/gif", + isFavorite: file.fdIsFavorite, + heroKey: flutter_util.getImageHeroTag(file), + ); + } + + final Account account; + final String _previewUrl; +} + +class _VideoItem extends _FileItem { + _VideoItem({ + required super.file, + required this.account, + }) : _previewUrl = NetworkRectThumbnail.imageUrlForFile(account, file); + + @override + StaggeredTile get staggeredTile => const StaggeredTile.count(1, 1); + + @override + Widget buildWidget(BuildContext context) { + return PhotoListVideo( + account: account, + previewUrl: _previewUrl, + isFavorite: file.fdIsFavorite, + ); + } + + final Account account; + final String _previewUrl; +} + +class _VisibleItem implements Comparable<_VisibleItem> { + const _VisibleItem(this.index, this.item); + + @override + bool operator ==(Object? other) => + other is _VisibleItem && index == other.index; + + @override + int compareTo(_VisibleItem other) => index.compareTo(other.index); + + @override + int get hashCode => index.hashCode; + + final int index; + final _Item item; +} + +class _ItemTransformerArgument { + const _ItemTransformerArgument({ + required this.account, + required this.files, + }); + + final Account account; + final List files; +} + +class _ItemTransformerResult { + const _ItemTransformerResult({ + required this.items, + }); + + final List<_Item> items; +} diff --git a/app/lib/widget/archive_browser/view.dart b/app/lib/widget/archive_browser/view.dart new file mode 100644 index 00000000..a80a2ec0 --- /dev/null +++ b/app/lib/widget/archive_browser/view.dart @@ -0,0 +1,90 @@ +part of '../archive_browser.dart'; + +class _ContentList extends StatelessWidget { + const _ContentList(); + + @override + Widget build(BuildContext context) { + return _BlocSelector( + selector: (state) => state.zoom, + builder: (context, zoom) => _ContentListBody( + maxCrossAxisExtent: photo_list_util.getThumbSize(zoom).toDouble(), + ), + ); + } +} + +class _ScalingList extends StatelessWidget { + const _ScalingList(); + + @override + Widget build(BuildContext context) { + return _BlocBuilder( + buildWhen: (previous, current) => previous.scale != current.scale, + builder: (context, state) { + if (state.scale == null) { + return const SizedBox.shrink(); + } + int nextZoom; + if (state.scale! > 1) { + nextZoom = state.zoom + 1; + } else { + nextZoom = state.zoom - 1; + } + nextZoom = nextZoom.clamp(-1, 2); + return _ContentListBody( + maxCrossAxisExtent: photo_list_util.getThumbSize(nextZoom).toDouble(), + ); + }, + ); + } +} + +@npLog +class _ContentListBody extends StatelessWidget { + const _ContentListBody({ + required this.maxCrossAxisExtent, + }); + + @override + Widget build(BuildContext context) { + return _BlocBuilder( + buildWhen: (previous, current) => + previous.transformedItems != current.transformedItems || + previous.selectedItems != current.selectedItems, + builder: (context, state) => SelectableItemList<_Item>( + maxCrossAxisExtent: maxCrossAxisExtent, + items: state.transformedItems, + itemBuilder: (context, _, item) => item.buildWidget(context), + staggeredTileBuilder: (_, item) => item.staggeredTile, + selectedItems: state.selectedItems, + onSelectionChange: (_, selected) { + context.addEvent(_SetSelectedItems(items: selected.cast())); + }, + onItemTap: (context, index, _) { + if (state.transformedItems[index] is! _FileItem) { + return; + } + final actualIndex = index - + state.transformedItems + .sublist(0, index) + .where((e) => e is! _FileItem) + .length; + Navigator.of(context).pushNamed( + Viewer.routeName, + arguments: ViewerArguments( + context.bloc.account, + state.transformedItems + .whereType<_FileItem>() + .map((e) => e.file) + .toList(), + actualIndex, + ), + ); + }, + ), + ); + } + + final double maxCrossAxisExtent; +} diff --git a/app/lib/widget/home_collections.dart b/app/lib/widget/home_collections.dart index 4575122a..8d65efb5 100644 --- a/app/lib/widget/home_collections.dart +++ b/app/lib/widget/home_collections.dart @@ -161,9 +161,8 @@ class _WrappedHomeCollectionsState extends State<_WrappedHomeCollections> const EnhancedPhotoBrowserArguments(null)); }, onArchivePressed: () { - Navigator.of(context).pushNamed( - ArchiveBrowser.routeName, - arguments: ArchiveBrowserArguments(_bloc.account)); + Navigator.of(context) + .pushNamed(ArchiveBrowser.routeName); }, onTrashbinPressed: () { Navigator.of(context).pushNamed( diff --git a/app/lib/widget/my_app.dart b/app/lib/widget/my_app.dart index 83393c34..747ddb40 100644 --- a/app/lib/widget/my_app.dart +++ b/app/lib/widget/my_app.dart @@ -197,6 +197,7 @@ class _WrappedAppState extends State<_WrappedApp> Settings.routeName: Settings.buildRoute, SharingBrowser.routeName: SharingBrowser.buildRoute, PlacesBrowser.routeName: PlacesBrowser.buildRoute, + ArchiveBrowser.routeName: ArchiveBrowser.buildRoute, }; Route? _onGenerateRoute(RouteSettings settings) { @@ -208,7 +209,6 @@ class _WrappedAppState extends State<_WrappedApp> route ??= _handleConnectLegacyRoute(settings); route ??= _handleHomeRoute(settings); route ??= _handleRootPickerRoute(settings); - route ??= _handleArchiveBrowserRoute(settings); route ??= _handleAlbumDirPickerRoute(settings); route ??= _handleAlbumImporterRoute(settings); route ??= _handleTrashbinBrowserRoute(settings); @@ -300,20 +300,6 @@ class _WrappedAppState extends State<_WrappedApp> return null; } - Route? _handleArchiveBrowserRoute(RouteSettings settings) { - try { - if (settings.name == ArchiveBrowser.routeName && - settings.arguments != null) { - final args = settings.arguments as ArchiveBrowserArguments; - return ArchiveBrowser.buildRoute(args); - } - } catch (e) { - _log.severe( - "[_handleArchiveBrowserRoute] Failed while handling route", e); - } - return null; - } - Route? _handleAlbumDirPickerRoute(RouteSettings settings) { try { if (settings.name == AlbumDirPicker.routeName && From db8f93b0529a04f2bc9de8c9a9a98624695d16fc Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sat, 13 Jan 2024 12:32:16 +0800 Subject: [PATCH 06/42] Migrate collections browser to use files controller --- app/lib/widget/collection_browser.dart | 36 +++--- app/lib/widget/collection_browser.g.dart | 24 +++- app/lib/widget/collection_browser/bloc.dart | 107 +++++++++++------- .../collection_browser/state_event.dart | 5 + app/lib/widget/collection_browser/type.dart | 20 ++++ 5 files changed, 137 insertions(+), 55 deletions(-) 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; +} From 1a09dc37a09519f7b76c95690fab5a6d6fa7a3e5 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sat, 13 Jan 2024 18:57:44 +0800 Subject: [PATCH 07/42] Add remote sync to the new HomePhotos --- app/lib/controller/files_controller.dart | 80 +++-- app/lib/widget/home_photos/app_bar.dart | 8 +- app/lib/widget/home_photos/bloc.dart | 42 ++- app/lib/widget/home_photos/state_event.dart | 12 + app/lib/widget/home_photos2.dart | 306 ++++++++++++-------- app/lib/widget/home_photos2.g.dart | 21 +- 6 files changed, 326 insertions(+), 143 deletions(-) diff --git a/app/lib/controller/files_controller.dart b/app/lib/controller/files_controller.dart index 9ef308ea..c858dca2 100644 --- a/app/lib/controller/files_controller.dart +++ b/app/lib/controller/files_controller.dart @@ -10,9 +10,11 @@ import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; +import 'package:nc_photos/progress_util.dart'; import 'package:nc_photos/rx_extension.dart'; import 'package:nc_photos/use_case/file/list_file.dart'; import 'package:nc_photos/use_case/remove.dart'; +import 'package:nc_photos/use_case/sync_dir.dart'; import 'package:nc_photos/use_case/update_property.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:np_common/lazy.dart'; @@ -54,27 +56,52 @@ class FilesController { ValueStream get stream { if (!_isDataStreamInited) { _isDataStreamInited = true; - unawaited(_load()); + _load(); } return _dataStreamController.stream; } - Future reload() async { - var results = []; - final completer = Completer(); - ListFile(_c)( - account, - file_util.unstripPath(account, accountPrefController.shareFolder.value), - ).listen( - (ev) { - results = ev; - }, - onError: _dataStreamController.addError, - onDone: () => completer.complete(), - ); - await completer.future; - _dataStreamController - .add(_convertListResultsToEvent(results, hasNext: false)); + Future syncRemote({ + void Function(Progress progress)? onProgressUpdate, + }) async { + if (_isSyncing) { + _log.fine("[syncRemote] Skipped as another sync running"); + return; + } + _isSyncing = true; + try { + final shareDir = File( + path: file_util.unstripPath( + account, accountPrefController.shareFolder.value), + ); + var isShareDirIncluded = false; + + _c.touchManager.clearTouchCache(); + final progress = IntProgress(account.roots.length); + for (final r in account.roots) { + final dirPath = file_util.unstripPath(account, r); + await SyncDir(_c)( + account, + dirPath, + onProgressUpdate: (value) { + final merged = progress.progress + progress.step * value.progress; + onProgressUpdate?.call(Progress(merged, value.text)); + }, + ); + isShareDirIncluded |= + file_util.isOrUnderDirPath(shareDir.path, dirPath); + progress.next(); + } + + if (!isShareDirIncluded) { + _log.info("[syncRemote] Explicitly scanning share folder"); + await SyncDir(_c)(account, shareDir.path, isRecursive: false); + } + // load the synced content to stream + unawaited(_reload()); + } finally { + _isSyncing = false; + } } /// Update files property and return number of files updated @@ -227,6 +254,24 @@ class FilesController { _dataStreamController.add(lastData.copyWith(hasNext: false)); } + Future _reload() async { + var results = []; + final completer = Completer(); + ListFile(_c)( + account, + file_util.unstripPath(account, accountPrefController.shareFolder.value), + ).listen( + (ev) { + results = ev; + }, + onError: _dataStreamController.addError, + onDone: () => completer.complete(), + ); + await completer.future; + _dataStreamController + .add(_convertListResultsToEvent(results, hasNext: false)); + } + _FilesStreamEvent _convertListResultsToEvent( List results, { required bool hasNext, @@ -252,6 +297,7 @@ class FilesController { ); final _mutex = Mutex(); + var _isSyncing = false; } @toString diff --git a/app/lib/widget/home_photos/app_bar.dart b/app/lib/widget/home_photos/app_bar.dart index d5a6c57d..3c3ac7e0 100644 --- a/app/lib/widget/home_photos/app_bar.dart +++ b/app/lib/widget/home_photos/app_bar.dart @@ -5,11 +5,11 @@ class _AppBar extends StatelessWidget { @override Widget build(BuildContext context) { - return _BlocBuilder( - buildWhen: (previous, current) => previous.isLoading != current.isLoading, - builder: (context, state) => HomeSliverAppBar( + return _BlocSelector( + selector: (state) => state.isLoading || state.syncProgress != null, + builder: (context, isProcessing) => HomeSliverAppBar( account: context.bloc.account, - isShowProgressIcon: state.isLoading, + isShowProgressIcon: isProcessing, ), ); } diff --git a/app/lib/widget/home_photos/bloc.dart b/app/lib/widget/home_photos/bloc.dart index 956cf645..bc55ea59 100644 --- a/app/lib/widget/home_photos/bloc.dart +++ b/app/lib/widget/home_photos/bloc.dart @@ -29,6 +29,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { on<_RemoveVisibleItem>(_onRemoveVisibleItem); on<_SetContentListMaxExtent>(_onSetContentListMaxExtent); + on<_SetSyncProgress>(_onSetSyncProgress); on<_StartScaling>(_onStartScaling); on<_EndScaling>(_onEndScaling); @@ -60,7 +61,8 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { currentState = currentState as _State; nextState = nextState as _State; return currentState.scale == nextState.scale && - currentState.visibleItems == nextState.visibleItems; + currentState.visibleItems == nextState.visibleItems && + currentState.syncProgress == nextState.syncProgress; }; @override @@ -80,10 +82,16 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _log.info(ev); return emit.forEach( controller.stream, - onData: (data) => state.copyWith( - files: data.data, - isLoading: data.hasNext || _itemTransformerQueue.isProcessing, - ), + onData: (data) { + if (_isInitialLoad && !data.hasNext) { + _isInitialLoad = false; + _syncRemote(); + } + return state.copyWith( + files: data.data, + isLoading: data.hasNext || _itemTransformerQueue.isProcessing, + ); + }, onError: (e, stackTrace) { _log.severe("[_onLoad] Uncaught exception", e, stackTrace); return state.copyWith( @@ -96,7 +104,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { void _onReload(_Reload ev, Emitter<_State> emit) { _log.info(ev); - unawaited(controller.reload()); + _syncRemote(); } void _onTransformItems(_TransformItems ev, Emitter<_State> emit) { @@ -204,6 +212,11 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { emit(state.copyWith(contentListMaxExtent: ev.value)); } + void _onSetSyncProgress(_SetSyncProgress ev, Emitter<_State> emit) { + _log.info(ev); + emit(state.copyWith(syncProgress: ev.progress)); + } + void _onStartScaling(_StartScaling ev, Emitter<_State> emit) { _log.info(ev); } @@ -264,6 +277,22 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { ); } + void _syncRemote() { + final stopwatch = Stopwatch()..start(); + controller.syncRemote( + onProgressUpdate: (progress) { + if (!isClosed) { + add(_SetSyncProgress(progress)); + } + }, + ).whenComplete(() { + if (!isClosed) { + add(const _SetSyncProgress(null)); + } + _log.info("[_syncRemote] Elapsed time: ${stopwatch.elapsedMilliseconds}ms"); + }); + } + void _clearSelection(Emitter<_State> emit) { emit(state.copyWith(selectedItems: const {})); } @@ -279,6 +308,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { ComputeQueue<_ItemTransformerArgument, _ItemTransformerResult>(); final _subscriptions = []; var _isHandlingError = false; + var _isInitialLoad = true; } _ItemTransformerResult _buildItem(_ItemTransformerArgument arg) { diff --git a/app/lib/widget/home_photos/state_event.dart b/app/lib/widget/home_photos/state_event.dart index 77a36b01..df020711 100644 --- a/app/lib/widget/home_photos/state_event.dart +++ b/app/lib/widget/home_photos/state_event.dart @@ -12,6 +12,7 @@ class _State { required this.isEnableMemoryCollection, required this.memoryCollections, this.contentListMaxExtent, + this.syncProgress, required this.zoom, this.scale, this.error, @@ -45,6 +46,7 @@ class _State { final List memoryCollections; final double? contentListMaxExtent; + final Progress? syncProgress; final int zoom; final double? scale; @@ -170,6 +172,16 @@ class _SetContentListMaxExtent implements _Event { final double? value; } +@toString +class _SetSyncProgress implements _Event { + const _SetSyncProgress(this.progress); + + @override + String toString() => _$toString(); + + final Progress? progress; +} + @toString class _StartScaling implements _Event { const _StartScaling(); diff --git a/app/lib/widget/home_photos2.dart b/app/lib/widget/home_photos2.dart index 638e605d..d5a59c6d 100644 --- a/app/lib/widget/home_photos2.dart +++ b/app/lib/widget/home_photos2.dart @@ -29,6 +29,7 @@ import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/flutter_util.dart' as flutter_util; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/language_util.dart' as language_util; +import 'package:nc_photos/progress_util.dart'; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme/dimension.dart'; @@ -139,123 +140,200 @@ class _WrappedHomePhotosState extends State<_WrappedHomePhotos> { }, ), ], - child: FingerListener( - onFingerChanged: (finger) { - setState(() { - _finger = finger; - }); + child: _BlocSelector( + selector: (state) => + state.files.isEmpty && + state.syncProgress != null, + builder: (context, isInitialSyncing) { + if (isInitialSyncing) { + return const _InitialSyncBody(); + } else { + return const _Body(); + } }, - child: GestureDetector( - onScaleStart: (_) { - _bloc.add(const _StartScaling()); - }, - onScaleUpdate: (details) { - _bloc.add(_SetScale(details.scale)); - }, - onScaleEnd: (_) { - _bloc.add(const _EndScaling()); - }, - child: LayoutBuilder( - builder: (context, constraints) => _BlocBuilder( - buildWhen: (previous, current) => - previous.contentListMaxExtent != - current.contentListMaxExtent || - (previous.isEnableMemoryCollection && - previous.memoryCollections.isNotEmpty) != - (current.isEnableMemoryCollection && - current.memoryCollections.isNotEmpty), - builder: (context, state) { - final scrollExtent = _getScrollViewExtent( - context: context, - constraints: constraints, - hasMemoryCollection: state.isEnableMemoryCollection && - state.memoryCollections.isNotEmpty, - contentListMaxExtent: state.contentListMaxExtent, - ); - return Stack( - children: [ - DraggableScrollbar.semicircle( - controller: _scrollController, - overrideMaxScrollExtent: scrollExtent, - // status bar + app bar - topOffset: _getAppBarExtent(context), - bottomOffset: - AppDimension.of(context).homeBottomAppBarHeight, - labelTextBuilder: (_) => const _ScrollLabel(), - labelPadding: - const EdgeInsets.symmetric(horizontal: 40), - backgroundColor: Theme.of(context).elevate( - Theme.of(context).colorScheme.inverseSurface, 3), - heightScrollThumb: 60, - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context) - .copyWith(scrollbars: false), - child: RefreshIndicator( - onRefresh: () async { - _bloc.add(const _Reload()); - await _bloc.stream.first; - }, - child: CustomScrollView( - controller: _scrollController, - physics: _finger >= 2 - ? const NeverScrollableScrollPhysics() - : null, - slivers: [ - _BlocSelector( - selector: (state) => - state.selectedItems.isEmpty, - builder: (context, isEmpty) => isEmpty - ? const _AppBar() - : const _SelectionAppBar(), - ), - _BlocBuilder( - buildWhen: (previous, current) => - (previous.isEnableMemoryCollection && - previous - .memoryCollections.isNotEmpty) != - (current.isEnableMemoryCollection && - current.memoryCollections.isNotEmpty), - builder: (context, state) { - if (state.isEnableMemoryCollection && - state.memoryCollections.isNotEmpty) { - return const _MemoryCollectionList(); - } else { - return const SliverToBoxAdapter(); - } - }, - ), - _BlocSelector( - selector: (state) => state.scale, - builder: (context, scale) => - SliverTransitionedScale( - scale: scale, - baseSliver: const _ContentList(), - overlaySliver: const _ScalingList(), - ), - ), - SliverToBoxAdapter( - child: SizedBox( - height: AppDimension.of(context) - .homeBottomAppBarHeight, - ), - ), - ], - ), - ), + ), + ), + ); + } + + late final _bloc = context.bloc; + + final _key = GlobalKey(); + bool? _isVisible; +} + +class _InitialSyncBody extends StatelessWidget { + const _InitialSyncBody(); + + @override + Widget build(BuildContext context) { + return CustomScrollView( + slivers: [ + const _AppBar(), + _BlocSelector( + selector: (state) => state.syncProgress, + builder: (context, syncProgress) { + return SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 56, 16, 0), + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: Theme.of(context).widthLimitedContentMaxWidth, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + L10n.global().initialSyncMessage, + style: Theme.of(context).textTheme.bodyLarge, ), - ), - Align( - alignment: Alignment.bottomCenter, - child: NavigationBarBlurFilter( - height: - AppDimension.of(context).homeBottomAppBarHeight, + const SizedBox(height: 8), + LinearProgressIndicator( + value: (syncProgress?.progress ?? 0) == 0 + ? null + : syncProgress!.progress, ), - ), - ], - ); - }, + const SizedBox(height: 8), + Text( + syncProgress?.text ?? "", + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + ), ), - ), + ); + }, + ), + ], + ); + } +} + +class _Body extends StatefulWidget { + const _Body(); + + @override + State createState() => _BodyState(); +} + +@npLog +class _BodyState extends State<_Body> { + @override + Widget build(BuildContext context) { + return FingerListener( + onFingerChanged: (finger) { + setState(() { + _finger = finger; + }); + }, + child: GestureDetector( + onScaleStart: (_) { + _bloc.add(const _StartScaling()); + }, + onScaleUpdate: (details) { + _bloc.add(_SetScale(details.scale)); + }, + onScaleEnd: (_) { + _bloc.add(const _EndScaling()); + }, + child: LayoutBuilder( + builder: (context, constraints) => _BlocBuilder( + buildWhen: (previous, current) => + previous.contentListMaxExtent != current.contentListMaxExtent || + (previous.isEnableMemoryCollection && + previous.memoryCollections.isNotEmpty) != + (current.isEnableMemoryCollection && + current.memoryCollections.isNotEmpty), + builder: (context, state) { + final scrollExtent = _getScrollViewExtent( + context: context, + constraints: constraints, + hasMemoryCollection: state.isEnableMemoryCollection && + state.memoryCollections.isNotEmpty, + contentListMaxExtent: state.contentListMaxExtent, + ); + return Stack( + children: [ + DraggableScrollbar.semicircle( + controller: _scrollController, + overrideMaxScrollExtent: scrollExtent, + // status bar + app bar + topOffset: _getAppBarExtent(context), + bottomOffset: + AppDimension.of(context).homeBottomAppBarHeight, + labelTextBuilder: (_) => const _ScrollLabel(), + labelPadding: const EdgeInsets.symmetric(horizontal: 40), + backgroundColor: Theme.of(context).elevate( + Theme.of(context).colorScheme.inverseSurface, 3), + heightScrollThumb: 60, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context) + .copyWith(scrollbars: false), + child: RefreshIndicator( + onRefresh: () async { + _bloc.add(const _Reload()); + await _bloc.stream.first; + }, + child: CustomScrollView( + controller: _scrollController, + physics: _finger >= 2 + ? const NeverScrollableScrollPhysics() + : null, + slivers: [ + _BlocSelector( + selector: (state) => state.selectedItems.isEmpty, + builder: (context, isEmpty) => isEmpty + ? const _AppBar() + : const _SelectionAppBar(), + ), + _BlocBuilder( + buildWhen: (previous, current) => + (previous.isEnableMemoryCollection && + previous.memoryCollections.isNotEmpty) != + (current.isEnableMemoryCollection && + current.memoryCollections.isNotEmpty), + builder: (context, state) { + if (state.isEnableMemoryCollection && + state.memoryCollections.isNotEmpty) { + return const _MemoryCollectionList(); + } else { + return const SliverToBoxAdapter(); + } + }, + ), + _BlocSelector( + selector: (state) => state.scale, + builder: (context, scale) => + SliverTransitionedScale( + scale: scale, + baseSliver: const _ContentList(), + overlaySliver: const _ScalingList(), + ), + ), + SliverToBoxAdapter( + child: SizedBox( + height: AppDimension.of(context) + .homeBottomAppBarHeight, + ), + ), + ], + ), + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: NavigationBarBlurFilter( + height: AppDimension.of(context).homeBottomAppBarHeight, + ), + ), + ], + ); + }, ), ), ), @@ -303,9 +381,7 @@ class _WrappedHomePhotosState extends State<_WrappedHomePhotos> { late final _bloc = context.bloc; - final _key = GlobalKey(); final _scrollController = ScrollController(); - bool? _isVisible; var _finger = 0; } diff --git a/app/lib/widget/home_photos2.g.dart b/app/lib/widget/home_photos2.g.dart index e562b685..f66dc80f 100644 --- a/app/lib/widget/home_photos2.g.dart +++ b/app/lib/widget/home_photos2.g.dart @@ -22,6 +22,7 @@ abstract class $_StateCopyWithWorker { bool? isEnableMemoryCollection, List? memoryCollections, double? contentListMaxExtent, + Progress? syncProgress, int? zoom, double? scale, ExceptionEvent? error}); @@ -40,6 +41,7 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { dynamic isEnableMemoryCollection, dynamic memoryCollections, dynamic contentListMaxExtent = copyWithNull, + dynamic syncProgress = copyWithNull, dynamic zoom, dynamic scale = copyWithNull, dynamic error = copyWithNull}) { @@ -57,6 +59,9 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { contentListMaxExtent: contentListMaxExtent == copyWithNull ? that.contentListMaxExtent : contentListMaxExtent as double?, + syncProgress: syncProgress == copyWithNull + ? that.syncProgress + : syncProgress as Progress?, zoom: zoom as int? ?? that.zoom, scale: scale == copyWithNull ? that.scale : scale as double?, error: error == copyWithNull ? that.error : error as ExceptionEvent?); @@ -81,6 +86,13 @@ extension _$_WrappedHomePhotosStateNpLog on _WrappedHomePhotosState { static final log = Logger("widget.home_photos2._WrappedHomePhotosState"); } +extension _$_BodyStateNpLog on _BodyState { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.home_photos2._BodyState"); +} + extension _$__NpLog on __ { // ignore: unused_element Logger get _log => log; @@ -123,7 +135,7 @@ extension _$_ContentListBodyNpLog on _ContentListBody { extension _$_StateToString on _State { String _$toString() { // ignore: unnecessary_string_interpolations - return "_State {files: [length: ${files.length}], isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, visibleItems: {length: ${visibleItems.length}}, isEnableMemoryCollection: $isEnableMemoryCollection, memoryCollections: [length: ${memoryCollections.length}], contentListMaxExtent: ${contentListMaxExtent == null ? null : "${contentListMaxExtent!.toStringAsFixed(3)}"}, zoom: $zoom, scale: ${scale == null ? null : "${scale!.toStringAsFixed(3)}"}, error: $error}"; + return "_State {files: [length: ${files.length}], isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, visibleItems: {length: ${visibleItems.length}}, isEnableMemoryCollection: $isEnableMemoryCollection, memoryCollections: [length: ${memoryCollections.length}], contentListMaxExtent: ${contentListMaxExtent == null ? null : "${contentListMaxExtent!.toStringAsFixed(3)}"}, syncProgress: $syncProgress, zoom: $zoom, scale: ${scale == null ? null : "${scale!.toStringAsFixed(3)}"}, error: $error}"; } } @@ -212,6 +224,13 @@ extension _$_SetContentListMaxExtentToString on _SetContentListMaxExtent { } } +extension _$_SetSyncProgressToString on _SetSyncProgress { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetSyncProgress {progress: $progress}"; + } +} + extension _$_StartScalingToString on _StartScaling { String _$toString() { // ignore: unnecessary_string_interpolations From 0b3aa720159df2e241eaea0c23dad7b1135e9982 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sat, 13 Jan 2024 18:57:53 +0800 Subject: [PATCH 08/42] Add log --- app/lib/entity/file/data_source2.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/lib/entity/file/data_source2.dart b/app/lib/entity/file/data_source2.dart index c1ee2b9e..ec6b0df6 100644 --- a/app/lib/entity/file/data_source2.dart +++ b/app/lib/entity/file/data_source2.dart @@ -104,8 +104,11 @@ class FileNpDbDataSource implements FileDataSource2 { Stream> getFileDescriptors( Account account, String shareDirPath) async* { _log.info("[getFileDescriptors] $account"); + final stopwatch = Stopwatch()..start(); yield await _getPartialFileDescriptors(account); yield await _getCompleteFileDescriptors(account, shareDirPath); + _log.info( + "[getFileDescriptors] Elapsed time: ${stopwatch.elapsedMilliseconds}ms"); } @override From 5f7d898078616445e6a4b8b23cdae81e85f2de88 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sat, 13 Jan 2024 21:59:25 +0800 Subject: [PATCH 09/42] Fix refresh indicator in the new home photos --- app/lib/widget/home_photos/bloc.dart | 4 +++- app/lib/widget/home_photos2.dart | 11 ++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/lib/widget/home_photos/bloc.dart b/app/lib/widget/home_photos/bloc.dart index bc55ea59..a7116ac3 100644 --- a/app/lib/widget/home_photos/bloc.dart +++ b/app/lib/widget/home_photos/bloc.dart @@ -104,6 +104,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { void _onReload(_Reload ev, Emitter<_State> emit) { _log.info(ev); + emit(state.copyWith(syncProgress: const Progress(0))); _syncRemote(); } @@ -289,7 +290,8 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { if (!isClosed) { add(const _SetSyncProgress(null)); } - _log.info("[_syncRemote] Elapsed time: ${stopwatch.elapsedMilliseconds}ms"); + _log.info( + "[_syncRemote] Elapsed time: ${stopwatch.elapsedMilliseconds}ms"); }); } diff --git a/app/lib/widget/home_photos2.dart b/app/lib/widget/home_photos2.dart index d5a59c6d..8fcd5d02 100644 --- a/app/lib/widget/home_photos2.dart +++ b/app/lib/widget/home_photos2.dart @@ -142,8 +142,7 @@ class _WrappedHomePhotosState extends State<_WrappedHomePhotos> { ], child: _BlocSelector( selector: (state) => - state.files.isEmpty && - state.syncProgress != null, + state.files.isEmpty && state.syncProgress != null, builder: (context, isInitialSyncing) { if (isInitialSyncing) { return const _InitialSyncBody(); @@ -276,7 +275,13 @@ class _BodyState extends State<_Body> { child: RefreshIndicator( onRefresh: () async { _bloc.add(const _Reload()); - await _bloc.stream.first; + var hasNotNull = false; + await _bloc.stream.firstWhere((s) { + if (s.syncProgress != null) { + hasNotNull = true; + } + return hasNotNull && s.syncProgress == null; + }); }, child: CustomScrollView( controller: _scrollController, From 4bbfc805ccb4adec3795a98f2e5e795e8e00b946 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sun, 14 Jan 2024 00:33:12 +0800 Subject: [PATCH 10/42] Tap photos again to jump to the top in new photos --- app/lib/widget/home.dart | 2 +- app/lib/widget/home_photos2.dart | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/app/lib/widget/home.dart b/app/lib/widget/home.dart index 6c545aff..bce80336 100644 --- a/app/lib/widget/home.dart +++ b/app/lib/widget/home.dart @@ -162,7 +162,7 @@ class _HomeState extends State with TickerProviderStateMixin { if (index == 0) { KiwiContainer() .resolve() - .fire(const HomePhotosBackToTopEvent()); + .fire(const HomePhotos2BackToTopEvent()); } return; } diff --git a/app/lib/widget/home_photos2.dart b/app/lib/widget/home_photos2.dart index 8fcd5d02..5de8a7c4 100644 --- a/app/lib/widget/home_photos2.dart +++ b/app/lib/widget/home_photos2.dart @@ -24,6 +24,7 @@ import 'package:nc_photos/download_handler.dart'; import 'package:nc_photos/entity/collection.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; +import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/exception_event.dart'; import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/flutter_util.dart' as flutter_util; @@ -61,6 +62,10 @@ part 'home_photos/type.dart'; part 'home_photos/view.dart'; part 'home_photos2.g.dart'; +class HomePhotos2BackToTopEvent { + const HomePhotos2BackToTopEvent(); +} + class HomePhotos2 extends StatelessWidget { const HomePhotos2({super.key}); @@ -221,6 +226,18 @@ class _Body extends StatefulWidget { @npLog class _BodyState extends State<_Body> { + @override + void initState() { + super.initState(); + _onBackToTopListener.begin(); + } + + @override + void dispose() { + _onBackToTopListener.end(); + super.dispose(); + } + @override Widget build(BuildContext context) { return FingerListener( @@ -345,6 +362,10 @@ class _BodyState extends State<_Body> { ); } + void _onBackToTop(HomePhotos2BackToTopEvent ev) { + _scrollController.jumpTo(0); + } + /// Return the estimated scroll extent of the custom scroll view, or null double? _getScrollViewExtent({ required BuildContext context, @@ -388,6 +409,9 @@ class _BodyState extends State<_Body> { final _scrollController = ScrollController(); var _finger = 0; + + late final _onBackToTopListener = + AppEventListener(_onBackToTop); } typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; From ff401f399a0f7293d403b3749dd2a34b70bd9668 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sun, 14 Jan 2024 02:09:10 +0800 Subject: [PATCH 11/42] Port sorting files by name --- app/lib/widget/home_photos/bloc.dart | 52 +++++++++++++++------ app/lib/widget/home_photos/state_event.dart | 10 ++++ app/lib/widget/home_photos/type.dart | 4 ++ app/lib/widget/home_photos2.g.dart | 7 +++ 4 files changed, 60 insertions(+), 13 deletions(-) diff --git a/app/lib/widget/home_photos/bloc.dart b/app/lib/widget/home_photos/bloc.dart index a7116ac3..84133af2 100644 --- a/app/lib/widget/home_photos/bloc.dart +++ b/app/lib/widget/home_photos/bloc.dart @@ -36,6 +36,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { on<_SetScale>(_onSetScale); on<_SetEnableMemoryCollection>(_onSetEnableMemoryCollection); + on<_SetSortByName>(_onSetSortByName); on<_SetError>(_onSetError); @@ -43,6 +44,9 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { .add(accountPrefController.isEnableMemoryAlbum.listen((event) { add(_SetEnableMemoryCollection(event)); })); + _subscriptions.add(prefController.isPhotosTabSortByName.listen((event) { + add(_SetSortByName(event)); + })); } @override @@ -254,6 +258,11 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { emit(state.copyWith(isEnableMemoryCollection: ev.value)); } + void _onSetSortByName(_SetSortByName ev, Emitter<_State> emit) { + _log.info(ev); + _transformItems(state.files); + } + void _onSetError(_SetError ev, Emitter<_State> emit) { _log.info(ev); emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); @@ -265,6 +274,9 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _ItemTransformerArgument( account: account, files: files, + sort: prefController.isPhotosTabSortByName.value + ? _ItemSort.filename + : _ItemSort.dateTime, memoriesDayRange: prefController.memoriesRange.value, locale: language_util.getSelectedLocale() ?? PlatformDispatcher.instance.locale, @@ -314,17 +326,31 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { } _ItemTransformerResult _buildItem(_ItemTransformerArgument arg) { - final sortedFiles = arg.files - .where((f) => f.fdIsArchived != true) - .sorted(compareFileDescriptorDateTimeDescending); - final dateHelper = photo_list_util.DateGroupHelper(isMonthOnly: false); + final int Function(FileDescriptor, FileDescriptor) sorter; + switch (arg.sort) { + case _ItemSort.filename: + sorter = (a, b) => a.fdPath.compareTo(b.fdPath); + break; + case _ItemSort.dateTime: + default: + sorter = compareFileDescriptorDateTimeDescending; + break; + } + final sortedFiles = + arg.files.where((f) => f.fdIsArchived != true).sorted(sorter); + final dateHelper = arg.sort == _ItemSort.dateTime + ? photo_list_util.DateGroupHelper(isMonthOnly: false) + : null; final today = clock.now(); - final memoryCollectionHelper = photo_list_util.MemoryCollectionHelper( - arg.account, - today: today, - dayRange: arg.memoriesDayRange, - ); + final memoryCollectionHelper = arg.sort == _ItemSort.dateTime + ? photo_list_util.MemoryCollectionHelper( + arg.account, + today: today, + dayRange: arg.memoriesDayRange, + ) + : null; + final transformed = <_Item>[]; for (int i = 0; i < sortedFiles.length; ++i) { final file = sortedFiles[i]; @@ -332,18 +358,18 @@ _ItemTransformerResult _buildItem(_ItemTransformerArgument arg) { if (item == null) { continue; } - final date = dateHelper.onFile(file); + final date = dateHelper?.onFile(file); if (date != null) { transformed.add(_DateItem(date: date)); } transformed.add(item); - memoryCollectionHelper.addFile(file); + memoryCollectionHelper?.addFile(file); } final memoryCollections = memoryCollectionHelper - .build((year) => L10n.of(arg.locale).memoryAlbumName(today.year - year)); + ?.build((year) => L10n.of(arg.locale).memoryAlbumName(today.year - year)); return _ItemTransformerResult( items: transformed, - memoryCollections: memoryCollections, + memoryCollections: memoryCollections ?? [], ); } diff --git a/app/lib/widget/home_photos/state_event.dart b/app/lib/widget/home_photos/state_event.dart index df020711..1f6d3402 100644 --- a/app/lib/widget/home_photos/state_event.dart +++ b/app/lib/widget/home_photos/state_event.dart @@ -218,6 +218,16 @@ class _SetEnableMemoryCollection implements _Event { final bool value; } +@toString +class _SetSortByName implements _Event { + const _SetSortByName(this.value); + + @override + String toString() => _$toString(); + + final bool value; +} + @toString class _SetError implements _Event { const _SetError(this.error, [this.stackTrace]); diff --git a/app/lib/widget/home_photos/type.dart b/app/lib/widget/home_photos/type.dart index 9e9131a7..4068a0cb 100644 --- a/app/lib/widget/home_photos/type.dart +++ b/app/lib/widget/home_photos/type.dart @@ -94,16 +94,20 @@ class _DateItem extends _Item { final DateTime date; } +enum _ItemSort { dateTime, filename } + class _ItemTransformerArgument { const _ItemTransformerArgument({ required this.account, required this.files, + required this.sort, required this.memoriesDayRange, required this.locale, }); final Account account; final List files; + final _ItemSort sort; final int memoriesDayRange; final Locale locale; } diff --git a/app/lib/widget/home_photos2.g.dart b/app/lib/widget/home_photos2.g.dart index f66dc80f..f4a22877 100644 --- a/app/lib/widget/home_photos2.g.dart +++ b/app/lib/widget/home_photos2.g.dart @@ -259,6 +259,13 @@ extension _$_SetEnableMemoryCollectionToString on _SetEnableMemoryCollection { } } +extension _$_SetSortByNameToString on _SetSortByName { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetSortByName {value: $value}"; + } +} + extension _$_SetErrorToString on _SetError { String _$toString() { // ignore: unnecessary_string_interpolations From feea195e60095a21369538b7e9bee12a201ae28f Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sun, 14 Jan 2024 02:23:30 +0800 Subject: [PATCH 12/42] Port memories range handling --- app/lib/widget/home_photos/bloc.dart | 9 +++++++++ app/lib/widget/home_photos/state_event.dart | 10 ++++++++++ app/lib/widget/home_photos2.g.dart | 7 +++++++ 3 files changed, 26 insertions(+) diff --git a/app/lib/widget/home_photos/bloc.dart b/app/lib/widget/home_photos/bloc.dart index 84133af2..df10b1ed 100644 --- a/app/lib/widget/home_photos/bloc.dart +++ b/app/lib/widget/home_photos/bloc.dart @@ -37,6 +37,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { on<_SetEnableMemoryCollection>(_onSetEnableMemoryCollection); on<_SetSortByName>(_onSetSortByName); + on<_SetMemoriesRange>(_onSetMemoriesRange); on<_SetError>(_onSetError); @@ -47,6 +48,9 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _subscriptions.add(prefController.isPhotosTabSortByName.listen((event) { add(_SetSortByName(event)); })); + _subscriptions.add(prefController.memoriesRange.listen((event) { + add(_SetMemoriesRange(event)); + })); } @override @@ -263,6 +267,11 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _transformItems(state.files); } + void _onSetMemoriesRange(_SetMemoriesRange ev, Emitter<_State> emit) { + _log.info(ev); + _transformItems(state.files); + } + void _onSetError(_SetError ev, Emitter<_State> emit) { _log.info(ev); emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); diff --git a/app/lib/widget/home_photos/state_event.dart b/app/lib/widget/home_photos/state_event.dart index 1f6d3402..18a944f9 100644 --- a/app/lib/widget/home_photos/state_event.dart +++ b/app/lib/widget/home_photos/state_event.dart @@ -228,6 +228,16 @@ class _SetSortByName implements _Event { final bool value; } +@toString +class _SetMemoriesRange implements _Event { + const _SetMemoriesRange(this.value); + + @override + String toString() => _$toString(); + + final int value; +} + @toString class _SetError implements _Event { const _SetError(this.error, [this.stackTrace]); diff --git a/app/lib/widget/home_photos2.g.dart b/app/lib/widget/home_photos2.g.dart index f4a22877..8ffeb8c1 100644 --- a/app/lib/widget/home_photos2.g.dart +++ b/app/lib/widget/home_photos2.g.dart @@ -266,6 +266,13 @@ extension _$_SetSortByNameToString on _SetSortByName { } } +extension _$_SetMemoriesRangeToString on _SetMemoriesRange { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetMemoriesRange {value: $value}"; + } +} + extension _$_SetErrorToString on _SetError { String _$toString() { // ignore: unnecessary_string_interpolations From 47348cf10acc465d921c5157cab6da7278bcf9c0 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sun, 14 Jan 2024 02:48:49 +0800 Subject: [PATCH 13/42] Fix not grouping by month when zoomed out to the lowest level --- app/lib/widget/home_photos/bloc.dart | 21 ++++++++++++++++----- app/lib/widget/home_photos/state_event.dart | 8 ++++++++ app/lib/widget/home_photos/type.dart | 5 +++++ app/lib/widget/home_photos2.g.dart | 7 +++++++ 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/app/lib/widget/home_photos/bloc.dart b/app/lib/widget/home_photos/bloc.dart index df10b1ed..eafc61a3 100644 --- a/app/lib/widget/home_photos/bloc.dart +++ b/app/lib/widget/home_photos/bloc.dart @@ -38,6 +38,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { on<_SetEnableMemoryCollection>(_onSetEnableMemoryCollection); on<_SetSortByName>(_onSetSortByName); on<_SetMemoriesRange>(_onSetMemoriesRange); + on<_UpdateDateTimeGroup>(_onUpdateDateTimeGroup); on<_SetError>(_onSetError); @@ -236,18 +237,22 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { return; } final int newZoom; + final currZoom = state.zoom; if (state.scale! >= 1.25) { // scale up - newZoom = (state.zoom + 1).clamp(-1, 2); + newZoom = (currZoom + 1).clamp(-1, 2); } else if (state.scale! <= 0.75) { - newZoom = (state.zoom - 1).clamp(-1, 2); + newZoom = (currZoom - 1).clamp(-1, 2); } else { - newZoom = state.zoom; + newZoom = currZoom; } emit(state.copyWith( zoom: newZoom, scale: null, )); + if ((currZoom >= 0) != (newZoom >= 0)) { + add(const _UpdateDateTimeGroup()); + } unawaited(prefController.setHomePhotosZoomLevel(newZoom)); } @@ -272,6 +277,11 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _transformItems(state.files); } + void _onUpdateDateTimeGroup(_UpdateDateTimeGroup ev, Emitter<_State> emit) { + _log.info(ev); + _transformItems(state.files); + } + void _onSetError(_SetError ev, Emitter<_State> emit) { _log.info(ev); emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); @@ -286,6 +296,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { sort: prefController.isPhotosTabSortByName.value ? _ItemSort.filename : _ItemSort.dateTime, + isGroupByDay: prefController.homePhotosZoomLevel.value >= 0, memoriesDayRange: prefController.memoriesRange.value, locale: language_util.getSelectedLocale() ?? PlatformDispatcher.instance.locale, @@ -349,7 +360,7 @@ _ItemTransformerResult _buildItem(_ItemTransformerArgument arg) { final sortedFiles = arg.files.where((f) => f.fdIsArchived != true).sorted(sorter); final dateHelper = arg.sort == _ItemSort.dateTime - ? photo_list_util.DateGroupHelper(isMonthOnly: false) + ? photo_list_util.DateGroupHelper(isMonthOnly: !arg.isGroupByDay) : null; final today = clock.now(); final memoryCollectionHelper = arg.sort == _ItemSort.dateTime @@ -369,7 +380,7 @@ _ItemTransformerResult _buildItem(_ItemTransformerArgument arg) { } final date = dateHelper?.onFile(file); if (date != null) { - transformed.add(_DateItem(date: date)); + transformed.add(_DateItem(date: date, isMonthOnly: !arg.isGroupByDay)); } transformed.add(item); memoryCollectionHelper?.addFile(file); diff --git a/app/lib/widget/home_photos/state_event.dart b/app/lib/widget/home_photos/state_event.dart index 18a944f9..9af8fa5b 100644 --- a/app/lib/widget/home_photos/state_event.dart +++ b/app/lib/widget/home_photos/state_event.dart @@ -238,6 +238,14 @@ class _SetMemoriesRange implements _Event { final int value; } +@toString +class _UpdateDateTimeGroup implements _Event { + const _UpdateDateTimeGroup(); + + @override + String toString() => _$toString(); +} + @toString class _SetError implements _Event { const _SetError(this.error, [this.stackTrace]); diff --git a/app/lib/widget/home_photos/type.dart b/app/lib/widget/home_photos/type.dart index 4068a0cb..b7ea8521 100644 --- a/app/lib/widget/home_photos/type.dart +++ b/app/lib/widget/home_photos/type.dart @@ -76,6 +76,7 @@ class _VideoItem extends _FileItem { class _DateItem extends _Item { const _DateItem({ required this.date, + required this.isMonthOnly, }); @override @@ -88,10 +89,12 @@ class _DateItem extends _Item { Widget buildWidget(BuildContext context) { return PhotoListDate( date: date, + isMonthOnly: isMonthOnly, ); } final DateTime date; + final bool isMonthOnly; } enum _ItemSort { dateTime, filename } @@ -101,6 +104,7 @@ class _ItemTransformerArgument { required this.account, required this.files, required this.sort, + required this.isGroupByDay, required this.memoriesDayRange, required this.locale, }); @@ -108,6 +112,7 @@ class _ItemTransformerArgument { final Account account; final List files; final _ItemSort sort; + final bool isGroupByDay; final int memoriesDayRange; final Locale locale; } diff --git a/app/lib/widget/home_photos2.g.dart b/app/lib/widget/home_photos2.g.dart index 8ffeb8c1..a826c290 100644 --- a/app/lib/widget/home_photos2.g.dart +++ b/app/lib/widget/home_photos2.g.dart @@ -273,6 +273,13 @@ extension _$_SetMemoriesRangeToString on _SetMemoriesRange { } } +extension _$_UpdateDateTimeGroupToString on _UpdateDateTimeGroup { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_UpdateDateTimeGroup {}"; + } +} + extension _$_SetErrorToString on _SetError { String _$toString() { // ignore: unnecessary_string_interpolations From 5c1ab48aaeaf4cd1ab4a7d0fe74ab271c8fea122 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sun, 14 Jan 2024 19:50:41 +0800 Subject: [PATCH 14/42] Port starting exif service --- app/lib/widget/home_photos/bloc.dart | 46 +++++++++++++++++++++ app/lib/widget/home_photos/state_event.dart | 10 +++++ app/lib/widget/home_photos2.dart | 4 ++ app/lib/widget/home_photos2.g.dart | 7 ++++ 4 files changed, 67 insertions(+) diff --git a/app/lib/widget/home_photos/bloc.dart b/app/lib/widget/home_photos/bloc.dart index eafc61a3..8495105f 100644 --- a/app/lib/widget/home_photos/bloc.dart +++ b/app/lib/widget/home_photos/bloc.dart @@ -9,6 +9,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { required this.prefController, required this.accountPrefController, required this.collectionsController, + required this.sessionController, }) : super(_State.init( zoom: prefController.homePhotosZoomLevel.value, isEnableMemoryCollection: @@ -38,6 +39,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { on<_SetEnableMemoryCollection>(_onSetEnableMemoryCollection); on<_SetSortByName>(_onSetSortByName); on<_SetMemoriesRange>(_onSetMemoriesRange); + on<_SetEnableExif>(_onSetEnableExif); on<_UpdateDateTimeGroup>(_onUpdateDateTimeGroup); on<_SetError>(_onSetError); @@ -52,6 +54,9 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _subscriptions.add(prefController.memoriesRange.listen((event) { add(_SetMemoriesRange(event)); })); + _subscriptions.add(prefController.isEnableExif.listen((event) { + add(_SetEnableExif(event)); + })); } @override @@ -130,6 +135,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { memoryCollections: ev.memoryCollections, isLoading: _itemTransformerQueue.isProcessing, )); + _tryStartMetadataTask(); } void _onSetSelectedItems(_SetSelectedItems ev, Emitter<_State> emit) { @@ -277,6 +283,15 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _transformItems(state.files); } + void _onSetEnableExif(_SetEnableExif ev, Emitter<_State> emit) { + _log.info(ev); + if (ev.value) { + _tryStartMetadataTask(ignoreFired: true); + } else { + _stopMetadataTask(); + } + } + void _onUpdateDateTimeGroup(_UpdateDateTimeGroup ev, Emitter<_State> emit) { _log.info(ev); _transformItems(state.files); @@ -331,12 +346,43 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { emit(state.copyWith(selectedItems: const {})); } + Future _tryStartMetadataTask({ + bool ignoreFired = false, + }) async { + if (state.files.isNotEmpty && + prefController.isEnableExif.value && + (ignoreFired || !sessionController.hasFiredMetadataTask.value)) { + sessionController.setFiredMetadataTask(true); + try { + final missingMetadataCount = + await _c.npDb.countFilesByFileIdsMissingMetadata( + account: account.toDb(), + fileIds: state.files.map((e) => e.fdId).toList(), + mimes: file_util.supportedImageFormatMimes, + ); + _log.info( + "[_tryStartMetadataTask] Missing count: $missingMetadataCount"); + if (missingMetadataCount > 0) { + unawaited(service.startService()); + } + } catch (e, stackTrace) { + _log.shout("[_tryStartMetadataTask] Failed starting metadata task", e, + stackTrace); + } + } + } + + void _stopMetadataTask() { + service.stopService(); + } + final DiContainer _c; final Account account; final FilesController controller; final PrefController prefController; final AccountPrefController accountPrefController; final CollectionsController collectionsController; + final SessionController sessionController; final _itemTransformerQueue = ComputeQueue<_ItemTransformerArgument, _ItemTransformerResult>(); diff --git a/app/lib/widget/home_photos/state_event.dart b/app/lib/widget/home_photos/state_event.dart index 9af8fa5b..29e9ccb0 100644 --- a/app/lib/widget/home_photos/state_event.dart +++ b/app/lib/widget/home_photos/state_event.dart @@ -238,6 +238,16 @@ class _SetMemoriesRange implements _Event { final int value; } +@toString +class _SetEnableExif implements _Event { + const _SetEnableExif(this.value); + + @override + String toString() => _$toString(); + + final bool value; +} + @toString class _UpdateDateTimeGroup implements _Event { const _UpdateDateTimeGroup(); diff --git a/app/lib/widget/home_photos2.dart b/app/lib/widget/home_photos2.dart index 5de8a7c4..54e17d10 100644 --- a/app/lib/widget/home_photos2.dart +++ b/app/lib/widget/home_photos2.dart @@ -19,6 +19,8 @@ import 'package:nc_photos/controller/account_pref_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/session_controller.dart'; +import 'package:nc_photos/db/entity_converter.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/download_handler.dart'; import 'package:nc_photos/entity/collection.dart'; @@ -31,6 +33,7 @@ import 'package:nc_photos/flutter_util.dart' as flutter_util; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/language_util.dart' as language_util; import 'package:nc_photos/progress_util.dart'; +import 'package:nc_photos/service.dart' as service; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme/dimension.dart'; @@ -80,6 +83,7 @@ class HomePhotos2 extends StatelessWidget { prefController: context.read(), accountPrefController: accountController.accountPrefController, collectionsController: accountController.collectionsController, + sessionController: accountController.sessionController, ), child: const _WrappedHomePhotos(), ); diff --git a/app/lib/widget/home_photos2.g.dart b/app/lib/widget/home_photos2.g.dart index a826c290..e5ea0a66 100644 --- a/app/lib/widget/home_photos2.g.dart +++ b/app/lib/widget/home_photos2.g.dart @@ -273,6 +273,13 @@ extension _$_SetMemoriesRangeToString on _SetMemoriesRange { } } +extension _$_SetEnableExifToString on _SetEnableExif { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetEnableExif {value: $value}"; + } +} + extension _$_UpdateDateTimeGroupToString on _UpdateDateTimeGroup { String _$toString() { // ignore: unnecessary_string_interpolations From 37a30e927c853f3a6498802418723731b2269625 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sun, 14 Jan 2024 21:05:53 +0800 Subject: [PATCH 15/42] Refresh after receiving native exif event --- app/lib/event/native_event.dart | 4 ++++ app/lib/event/native_event.g.dart | 14 ++++++++++++++ app/lib/widget/home_photos/bloc.dart | 23 +++++++++++++++++++++++ app/lib/widget/home_photos2.dart | 3 +++ 4 files changed, 44 insertions(+) create mode 100644 app/lib/event/native_event.g.dart diff --git a/app/lib/event/native_event.dart b/app/lib/event/native_event.dart index 282a7c0f..1406207e 100644 --- a/app/lib/event/native_event.dart +++ b/app/lib/event/native_event.dart @@ -3,8 +3,11 @@ import 'dart:convert'; import 'package:logging/logging.dart'; import 'package:nc_photos/stream_extension.dart'; +import 'package:np_codegen/np_codegen.dart'; import 'package:np_platform_message_relay/np_platform_message_relay.dart'; +part 'native_event.g.dart'; + class NativeEventListener { NativeEventListener(this.listener); @@ -43,6 +46,7 @@ class NativeEventListener { Logger("event.native_event.NativeEventListener<${T.runtimeType}>"); } +@npLog class FileExifUpdatedEvent { const FileExifUpdatedEvent(this.fileIds); diff --git a/app/lib/event/native_event.g.dart b/app/lib/event/native_event.g.dart new file mode 100644 index 00000000..0fcc5da5 --- /dev/null +++ b/app/lib/event/native_event.g.dart @@ -0,0 +1,14 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'native_event.dart'; + +// ************************************************************************** +// NpLogGenerator +// ************************************************************************** + +extension _$FileExifUpdatedEventNpLog on FileExifUpdatedEvent { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("event.native_event.FileExifUpdatedEvent"); +} diff --git a/app/lib/widget/home_photos/bloc.dart b/app/lib/widget/home_photos/bloc.dart index 8495105f..b355eb2e 100644 --- a/app/lib/widget/home_photos/bloc.dart +++ b/app/lib/widget/home_photos/bloc.dart @@ -57,6 +57,8 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _subscriptions.add(prefController.isEnableExif.listen((event) { add(_SetEnableExif(event)); })); + + _nativeFileExifUpdatedListener?.begin(); } @override @@ -64,6 +66,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { for (final s in _subscriptions) { s.cancel(); } + _nativeFileExifUpdatedListener?.end(); return super.close(); } @@ -302,6 +305,14 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); } + void _onNativeFileExifUpdated(FileExifUpdatedEvent ev) { + _log.info(ev); + _refreshThrottler.trigger( + maxResponceTime: const Duration(seconds: 3), + maxPendingCount: 10, + ); + } + Future _transformItems(List files) async { _log.info("[_transformItems] Queue ${files.length} items"); _itemTransformerQueue.addJob( @@ -389,6 +400,18 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { final _subscriptions = []; var _isHandlingError = false; var _isInitialLoad = true; + + // Listen to updates from background isolates as the memories are not shared + late final _nativeFileExifUpdatedListener = + getRawPlatform() == NpPlatform.android + ? NativeEventListener(_onNativeFileExifUpdated) + : null; + late final _refreshThrottler = Throttler( + onTriggered: (_) { + add(const _Reload()); + }, + logTag: _log.name, + ); } _ItemTransformerResult _buildItem(_ItemTransformerArgument arg) { diff --git a/app/lib/widget/home_photos2.dart b/app/lib/widget/home_photos2.dart index 54e17d10..db204002 100644 --- a/app/lib/widget/home_photos2.dart +++ b/app/lib/widget/home_photos2.dart @@ -27,6 +27,7 @@ import 'package:nc_photos/entity/collection.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/event/event.dart'; +import 'package:nc_photos/event/native_event.dart'; import 'package:nc_photos/exception_event.dart'; import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/flutter_util.dart' as flutter_util; @@ -37,6 +38,7 @@ import 'package:nc_photos/service.dart' as service; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme/dimension.dart'; +import 'package:nc_photos/throttler.dart'; import 'package:nc_photos/widget/collection_browser.dart'; import 'package:nc_photos/widget/collection_picker.dart'; import 'package:nc_photos/widget/file_sharer_dialog.dart'; @@ -55,6 +57,7 @@ import 'package:np_codegen/np_codegen.dart'; import 'package:np_collection/np_collection.dart'; import 'package:np_common/object_util.dart'; import 'package:np_common/or_null.dart'; +import 'package:np_platform_util/np_platform_util.dart'; import 'package:to_string/to_string.dart'; import 'package:visibility_detector/visibility_detector.dart'; From a04ecefed9ea49e94ee0ea6a67fe07046db7ce63 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sun, 14 Jan 2024 21:39:20 +0800 Subject: [PATCH 16/42] Respond to share folder changes --- app/lib/widget/home_photos/bloc.dart | 9 +++++++++ app/lib/widget/home_photos/state_event.dart | 10 ++++++++++ app/lib/widget/home_photos2.g.dart | 7 +++++++ 3 files changed, 26 insertions(+) diff --git a/app/lib/widget/home_photos/bloc.dart b/app/lib/widget/home_photos/bloc.dart index b355eb2e..4ed8ad3e 100644 --- a/app/lib/widget/home_photos/bloc.dart +++ b/app/lib/widget/home_photos/bloc.dart @@ -41,6 +41,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { on<_SetMemoriesRange>(_onSetMemoriesRange); on<_SetEnableExif>(_onSetEnableExif); on<_UpdateDateTimeGroup>(_onUpdateDateTimeGroup); + on<_SetShareFolder>(_onSetShareFolder); on<_SetError>(_onSetError); @@ -57,6 +58,9 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _subscriptions.add(prefController.isEnableExif.listen((event) { add(_SetEnableExif(event)); })); + _subscriptions.add(accountPrefController.shareFolder.listen((event) { + add(_SetShareFolder(event)); + })); _nativeFileExifUpdatedListener?.begin(); } @@ -300,6 +304,11 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _transformItems(state.files); } + void _onSetShareFolder(_SetShareFolder ev, Emitter<_State> emit) { + _log.info(ev); + add(const _Reload()); + } + void _onSetError(_SetError ev, Emitter<_State> emit) { _log.info(ev); emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); diff --git a/app/lib/widget/home_photos/state_event.dart b/app/lib/widget/home_photos/state_event.dart index 29e9ccb0..ded809db 100644 --- a/app/lib/widget/home_photos/state_event.dart +++ b/app/lib/widget/home_photos/state_event.dart @@ -248,6 +248,16 @@ class _SetEnableExif implements _Event { final bool value; } +@toString +class _SetShareFolder implements _Event { + const _SetShareFolder(this.value); + + @override + String toString() => _$toString(); + + final String value; +} + @toString class _UpdateDateTimeGroup implements _Event { const _UpdateDateTimeGroup(); diff --git a/app/lib/widget/home_photos2.g.dart b/app/lib/widget/home_photos2.g.dart index e5ea0a66..bf18d3d9 100644 --- a/app/lib/widget/home_photos2.g.dart +++ b/app/lib/widget/home_photos2.g.dart @@ -280,6 +280,13 @@ extension _$_SetEnableExifToString on _SetEnableExif { } } +extension _$_SetShareFolderToString on _SetShareFolder { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetShareFolder {value: $value}"; + } +} + extension _$_UpdateDateTimeGroupToString on _UpdateDateTimeGroup { String _$toString() { // ignore: unnecessary_string_interpolations From f97476e4f0b19e7551dcf3ac7a83727dc473b631 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Thu, 18 Jan 2024 01:17:53 +0800 Subject: [PATCH 17/42] Fix startup sync results not propagated to main isolate --- app/lib/controller/files_controller.dart | 65 +++++++++++++++++++ app/lib/controller/sync_controller.dart | 29 +++++++-- app/lib/use_case/cache_favorite.dart | 11 ++-- app/lib/use_case/startup_sync.dart | 63 +++++++++++++----- app/lib/use_case/startup_sync.g.dart | 11 ++++ app/lib/use_case/sync_favorite.dart | 3 +- app/lib/use_case/sync_tag.dart | 8 ++- app/lib/widget/home_photos.dart | 6 +- app/lib/widget/home_photos/bloc.dart | 10 +++ app/lib/widget/home_photos2.dart | 4 ++ app/lib/widget/settings/account_settings.dart | 8 ++- np_db/lib/src/api.dart | 36 +++++++++- np_db_sqlite/lib/src/sqlite_api.dart | 20 +++--- 13 files changed, 229 insertions(+), 45 deletions(-) diff --git a/app/lib/controller/files_controller.dart b/app/lib/controller/files_controller.dart index c858dca2..e437498b 100644 --- a/app/lib/controller/files_controller.dart +++ b/app/lib/controller/files_controller.dart @@ -13,6 +13,7 @@ import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/progress_util.dart'; import 'package:nc_photos/rx_extension.dart'; import 'package:nc_photos/use_case/file/list_file.dart'; +import 'package:nc_photos/use_case/find_file_descriptor.dart'; import 'package:nc_photos/use_case/remove.dart'; import 'package:nc_photos/use_case/sync_dir.dart'; import 'package:nc_photos/use_case/update_property.dart'; @@ -20,6 +21,7 @@ import 'package:np_codegen/np_codegen.dart'; import 'package:np_common/lazy.dart'; import 'package:np_common/object_util.dart'; import 'package:np_common/or_null.dart'; +import 'package:np_db/np_db.dart'; import 'package:rxdart/rxdart.dart'; import 'package:to_string/to_string.dart'; @@ -233,6 +235,69 @@ class FilesController { } } + Future applySyncResult({ + DbSyncIdResult? favorites, + List? fileExifs, + }) async { + if (favorites?.isNotEmpty != true && fileExifs?.isNotEmpty != true) { + return; + } + + // do async ops first + final fileExifFiles = + await fileExifs?.letFuture((e) async => await FindFileDescriptor(_c)( + account, + e, + onFileNotFound: (id) { + _log.warning("[applySyncResult] File id not found: $id"); + }, + )); + + final next = LinkedHashMap.of(_dataStreamController.value.files); + if (favorites != null && favorites.isNotEmpty) { + _applySyncFavoriteResult(next, favorites); + } + if (fileExifFiles != null && fileExifFiles.isNotEmpty) { + _applySyncFileExifResult(next, fileExifFiles); + } + _dataStreamController.addWithValue((value) => value.copyWith(files: next)); + } + + void _applySyncFavoriteResult( + Map next, DbSyncIdResult result) { + for (final id in result.insert) { + final f = next[id]; + if (f == null) { + _log.warning("[_applySyncFavoriteResult] File id not found: $id"); + continue; + } + if (f is File) { + next[id] = f.copyWith(isFavorite: true); + } else { + next[id] = f.copyWith(fdIsFavorite: true); + } + } + for (final id in result.delete) { + final f = next[id]; + if (f == null) { + _log.warning("[_applySyncFavoriteResult] File id not found: $id"); + continue; + } + if (f is File) { + next[id] = f.copyWith(isFavorite: false); + } else { + next[id] = f.copyWith(fdIsFavorite: false); + } + } + } + + void _applySyncFileExifResult( + Map next, List results) { + for (final f in results) { + next[f.fdId] = f; + } + } + Future _load() async { var lastData = _FilesStreamEvent( files: const {}, diff --git a/app/lib/controller/sync_controller.dart b/app/lib/controller/sync_controller.dart index a394ac7d..2552cd97 100644 --- a/app/lib/controller/sync_controller.dart +++ b/app/lib/controller/sync_controller.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:nc_photos/account.dart'; +import 'package:nc_photos/controller/files_controller.dart'; +import 'package:nc_photos/controller/persons_controller.dart'; import 'package:nc_photos/entity/person.dart'; import 'package:nc_photos/use_case/startup_sync.dart'; @@ -15,14 +17,19 @@ class SyncController { _isDisposed = true; } - Future requestSync( - Account account, PersonProvider personProvider) async { + Future requestSync({ + required Account account, + required FilesController filesController, + required PersonsController personsController, + required PersonProvider personProvider, + }) async { if (_isDisposed) { return; } if (_syncCompleter == null) { _syncCompleter = Completer(); - final result = await StartupSync.runInIsolate(account, personProvider); + final result = await StartupSync.runInIsolate( + account, filesController, personsController, personProvider); if (!_isDisposed && result.isSyncPersonUpdated) { onPeopleUpdated?.call(); } @@ -32,15 +39,23 @@ class SyncController { } } - Future requestResync( - Account account, PersonProvider personProvider) async { + Future requestResync({ + required Account account, + required FilesController filesController, + required PersonsController personsController, + required PersonProvider personProvider, + }) async { if (_syncCompleter?.isCompleted == true) { _syncCompleter = null; - return requestSync(account, personProvider); } else { // already syncing - return requestSync(account, personProvider); } + return requestSync( + account: account, + filesController: filesController, + personsController: personsController, + personProvider: personProvider, + ); } final Account account; diff --git a/app/lib/use_case/cache_favorite.dart b/app/lib/use_case/cache_favorite.dart index bc6756f0..ab70e247 100644 --- a/app/lib/use_case/cache_favorite.dart +++ b/app/lib/use_case/cache_favorite.dart @@ -6,6 +6,7 @@ import 'package:nc_photos/db/entity_converter.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/event/event.dart'; import 'package:np_codegen/np_codegen.dart'; +import 'package:np_db/np_db.dart'; part 'cache_favorite.g.dart'; @@ -15,18 +16,18 @@ class CacheFavorite { /// Cache favorites using results from remote /// - /// Return number of files updated - Future call(Account account, Iterable remoteFileIds) async { + /// Return the fileIds of the affected files + Future call( + Account account, Iterable remoteFileIds) async { _log.info("[call] Cache favorites"); final result = await _c.npDb.syncFavoriteFiles( account: account.toDb(), favoriteFileIds: remoteFileIds.toList(), ); - final count = result.insert + result.delete + result.update; - if (count > 0) { + if (result.isNotEmpty) { KiwiContainer().resolve().fire(FavoriteResyncedEvent(account)); } - return count; + return result; } final DiContainer _c; diff --git a/app/lib/use_case/startup_sync.dart b/app/lib/use_case/startup_sync.dart index cafabc00..6d8546f5 100644 --- a/app/lib/use_case/startup_sync.dart +++ b/app/lib/use_case/startup_sync.dart @@ -7,6 +7,8 @@ import 'package:logging/logging.dart'; import 'package:mutex/mutex.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_init.dart' as app_init; +import 'package:nc_photos/controller/files_controller.dart'; +import 'package:nc_photos/controller/persons_controller.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/person.dart'; import 'package:nc_photos/event/event.dart'; @@ -14,8 +16,11 @@ import 'package:nc_photos/use_case/person/sync_person.dart'; import 'package:nc_photos/use_case/sync_favorite.dart'; import 'package:nc_photos/use_case/sync_tag.dart'; import 'package:np_codegen/np_codegen.dart'; +import 'package:np_common/object_util.dart'; import 'package:np_common/type.dart'; +import 'package:np_db/np_db.dart'; import 'package:np_platform_util/np_platform_util.dart'; +import 'package:to_string/to_string.dart'; part 'startup_sync.g.dart'; @@ -28,7 +33,11 @@ class StartupSync { /// Sync in a background isolate static Future runInIsolate( - Account account, PersonProvider personProvider) async { + Account account, + FilesController filesController, + PersonsController personsController, + PersonProvider personProvider, + ) async { return _mutex.protect(() async { if (getRawPlatform() == NpPlatform.web) { // not supported on web @@ -42,7 +51,7 @@ class StartupSync { final result = SyncResult.fromJson(resultJson); // events fired in background isolate won't be noticed by the main isolate, // so we fire them again here - _broadcastResult(account, result); + _broadcastResult(account, filesController, personsController, result); return result; } }); @@ -52,16 +61,16 @@ class StartupSync { Account account, PersonProvider personProvider) async { _log.info("[_run] Begin sync"); final stopwatch = Stopwatch()..start(); - late final int syncFavoriteCount; - late final bool isSyncPersonUpdated; + DbSyncIdResult? syncFavoriteResult; + DbSyncIdResult? syncTagResult; + var isSyncPersonUpdated = false; try { - syncFavoriteCount = await SyncFavorite(_c)(account); + syncFavoriteResult = await SyncFavorite(_c)(account); } catch (e, stackTrace) { _log.shout("[_run] Failed while SyncFavorite", e, stackTrace); - syncFavoriteCount = -1; } try { - await SyncTag(_c)(account); + syncTagResult = await SyncTag(_c)(account); } catch (e, stackTrace) { _log.shout("[_run] Failed while SyncTag", e, stackTrace); } @@ -72,16 +81,28 @@ class StartupSync { } _log.info("[_run] Elapsed time: ${stopwatch.elapsedMilliseconds}ms"); return SyncResult( - syncFavoriteCount: syncFavoriteCount, + syncFavoriteResult: syncFavoriteResult, + syncTagResult: syncTagResult, isSyncPersonUpdated: isSyncPersonUpdated, ); } - static void _broadcastResult(Account account, SyncResult result) { - final eventBus = KiwiContainer().resolve(); - if (result.syncFavoriteCount > 0) { + static void _broadcastResult( + Account account, + FilesController filesController, + PersonsController personsController, + SyncResult result, + ) { + _$StartupSyncNpLog.log.info('[_broadcastResult] $result'); + if (result.syncFavoriteResult != null) { + filesController.applySyncResult(favorites: result.syncFavoriteResult!); + // legacy + final eventBus = KiwiContainer().resolve(); eventBus.fire(FavoriteResyncedEvent(account)); } + if (result.isSyncPersonUpdated) { + personsController.reload(); + } } final DiContainer _c; @@ -89,23 +110,35 @@ class StartupSync { static final _mutex = Mutex(); } +@toString class SyncResult { const SyncResult({ - required this.syncFavoriteCount, + required this.syncFavoriteResult, + required this.syncTagResult, required this.isSyncPersonUpdated, }); factory SyncResult.fromJson(JsonObj json) => SyncResult( - syncFavoriteCount: json["syncFavoriteCount"], + syncFavoriteResult: (json["syncFavoriteResult"] as Map?) + ?.cast() + .let(DbSyncIdResult.fromJson), + syncTagResult: (json["syncTagResult"] as Map?) + ?.cast() + .let(DbSyncIdResult.fromJson), isSyncPersonUpdated: json["isSyncPersonUpdated"], ); JsonObj toJson() => { - "syncFavoriteCount": syncFavoriteCount, + "syncFavoriteResult": syncFavoriteResult?.toJson(), + "syncTagResult": syncTagResult?.toJson(), "isSyncPersonUpdated": isSyncPersonUpdated, }; - final int syncFavoriteCount; + @override + String toString() => _$toString(); + + final DbSyncIdResult? syncFavoriteResult; + final DbSyncIdResult? syncTagResult; final bool isSyncPersonUpdated; } diff --git a/app/lib/use_case/startup_sync.g.dart b/app/lib/use_case/startup_sync.g.dart index 7e45e33d..ca0cf08e 100644 --- a/app/lib/use_case/startup_sync.g.dart +++ b/app/lib/use_case/startup_sync.g.dart @@ -12,3 +12,14 @@ extension _$StartupSyncNpLog on StartupSync { static final log = Logger("use_case.startup_sync.StartupSync"); } + +// ************************************************************************** +// ToStringGenerator +// ************************************************************************** + +extension _$SyncResultToString on SyncResult { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "SyncResult {syncFavoriteResult: $syncFavoriteResult, syncTagResult: $syncTagResult, isSyncPersonUpdated: $isSyncPersonUpdated}"; + } +} diff --git a/app/lib/use_case/sync_favorite.dart b/app/lib/use_case/sync_favorite.dart index 02175043..d8a52b3d 100644 --- a/app/lib/use_case/sync_favorite.dart +++ b/app/lib/use_case/sync_favorite.dart @@ -6,6 +6,7 @@ import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/use_case/cache_favorite.dart'; import 'package:np_codegen/np_codegen.dart'; +import 'package:np_db/np_db.dart'; part 'sync_favorite.g.dart'; @@ -18,7 +19,7 @@ class SyncFavorite { /// Sync favorites in cache db with remote server /// /// Return number of files updated - Future call(Account account) async { + Future call(Account account) async { _log.info("[call] Sync favorites with remote"); final remote = await _getRemoteFavoriteFileIds(account); return await CacheFavorite(_c)(account, remote); diff --git a/app/lib/use_case/sync_tag.dart b/app/lib/use_case/sync_tag.dart index 7234c030..415933a6 100644 --- a/app/lib/use_case/sync_tag.dart +++ b/app/lib/use_case/sync_tag.dart @@ -3,6 +3,7 @@ import 'package:nc_photos/account.dart'; import 'package:nc_photos/db/entity_converter.dart'; import 'package:nc_photos/di_container.dart'; import 'package:np_codegen/np_codegen.dart'; +import 'package:np_db/np_db.dart'; part 'sync_tag.g.dart'; @@ -11,13 +12,16 @@ class SyncTag { const SyncTag(this._c); /// Sync tags in cache db with remote server - Future call(Account account) async { + /// + /// Return tagIds of the affected tags + Future call(Account account) async { _log.info("[call] Sync tags with remote"); final remote = await _c.tagRepoRemote.list(account); - await _c.npDb.syncTags( + final result = await _c.npDb.syncTags( account: account.toDb(), tags: remote.map(DbTagConverter.toDb).toList(), ); + return result; } final DiContainer _c; diff --git a/app/lib/widget/home_photos.dart b/app/lib/widget/home_photos.dart index e83036ed..40bb1ebc 100644 --- a/app/lib/widget/home_photos.dart +++ b/app/lib/widget/home_photos.dart @@ -609,7 +609,11 @@ class _HomePhotosState extends State if (isPostSuccess) { _isScrollbarVisible = true; context.read().syncController.requestSync( - widget.account, _accountPrefController.personProvider.value); + account: widget.account, + filesController: context.read(), + personsController: context.read(), + personProvider: _accountPrefController.personProvider.value, + ); _tryStartMetadataTask(context); } }); diff --git a/app/lib/widget/home_photos/bloc.dart b/app/lib/widget/home_photos/bloc.dart index 4ed8ad3e..c7715cb7 100644 --- a/app/lib/widget/home_photos/bloc.dart +++ b/app/lib/widget/home_photos/bloc.dart @@ -10,6 +10,8 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { required this.accountPrefController, required this.collectionsController, required this.sessionController, + required this.syncController, + required this.personsController, }) : super(_State.init( zoom: prefController.homePhotosZoomLevel.value, isEnableMemoryCollection: @@ -142,6 +144,12 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { memoryCollections: ev.memoryCollections, isLoading: _itemTransformerQueue.isProcessing, )); + syncController.requestSync( + account: account, + filesController: controller, + personsController: personsController, + personProvider: accountPrefController.personProvider.value, + ); _tryStartMetadataTask(); } @@ -403,6 +411,8 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { final AccountPrefController accountPrefController; final CollectionsController collectionsController; final SessionController sessionController; + final SyncController syncController; + final PersonsController personsController; final _itemTransformerQueue = ComputeQueue<_ItemTransformerArgument, _ItemTransformerResult>(); diff --git a/app/lib/widget/home_photos2.dart b/app/lib/widget/home_photos2.dart index db204002..22579630 100644 --- a/app/lib/widget/home_photos2.dart +++ b/app/lib/widget/home_photos2.dart @@ -18,8 +18,10 @@ import 'package:nc_photos/controller/account_controller.dart'; import 'package:nc_photos/controller/account_pref_controller.dart'; import 'package:nc_photos/controller/collections_controller.dart'; import 'package:nc_photos/controller/files_controller.dart'; +import 'package:nc_photos/controller/persons_controller.dart'; import 'package:nc_photos/controller/pref_controller.dart'; import 'package:nc_photos/controller/session_controller.dart'; +import 'package:nc_photos/controller/sync_controller.dart'; import 'package:nc_photos/db/entity_converter.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/download_handler.dart'; @@ -87,6 +89,8 @@ class HomePhotos2 extends StatelessWidget { accountPrefController: accountController.accountPrefController, collectionsController: accountController.collectionsController, sessionController: accountController.sessionController, + syncController: accountController.syncController, + personsController: accountController.personsController, ), child: const _WrappedHomePhotos(), ); diff --git a/app/lib/widget/settings/account_settings.dart b/app/lib/widget/settings/account_settings.dart index 64d704a7..7f9a7150 100644 --- a/app/lib/widget/settings/account_settings.dart +++ b/app/lib/widget/settings/account_settings.dart @@ -109,8 +109,12 @@ class _WrappedAccountSettingsState extends State<_WrappedAccountSettings> if (_bloc.state.shouldResync && _bloc.state.personProvider != PersonProvider.none) { _log.fine("[dispose] Requesting to resync account"); - _accountController.syncController - .requestResync(_bloc.state.account, _bloc.state.personProvider); + _accountController.syncController.requestResync( + account: _bloc.state.account, + filesController: context.read(), + personsController: context.read(), + personProvider: _bloc.state.personProvider, + ); } _animationController.dispose(); super.dispose(); diff --git a/np_db/lib/src/api.dart b/np_db/lib/src/api.dart index f99ef2ae..42b8fc39 100644 --- a/np_db/lib/src/api.dart +++ b/np_db/lib/src/api.dart @@ -49,6 +49,36 @@ class DbSyncResult { final int update; } +/// Sync results with ids +/// +/// The meaning of the ids returned depends on the specific call +class DbSyncIdResult { + const DbSyncIdResult({ + required this.insert, + required this.delete, + required this.update, + }); + + factory DbSyncIdResult.fromJson(JsonObj json) => DbSyncIdResult( + insert: (json["insert"] as List).cast(), + delete: (json["delete"] as List).cast(), + update: (json["update"] as List).cast(), + ); + + JsonObj toJson() => { + "insert": insert, + "delete": delete, + "update": update, + }; + + bool get isEmpty => insert.isEmpty && delete.isEmpty && update.isEmpty; + bool get isNotEmpty => !isEmpty; + + final List insert; + final List delete; + final List update; +} + @toString class DbLocationGroup with EquatableMixin { const DbLocationGroup({ @@ -235,7 +265,9 @@ abstract class NpDb { }); /// Add or replace nc albums in db - Future syncFavoriteFiles({ + /// + /// Return fileIds affected by this call + Future syncFavoriteFiles({ required DbAccount account, required List favoriteFileIds, }); @@ -374,7 +406,7 @@ abstract class NpDb { }); /// Replace all tags for [account] - Future syncTags({ + Future syncTags({ required DbAccount account, required List tags, }); diff --git a/np_db_sqlite/lib/src/sqlite_api.dart b/np_db_sqlite/lib/src/sqlite_api.dart index 67cabdeb..25c5d97e 100644 --- a/np_db_sqlite/lib/src/sqlite_api.dart +++ b/np_db_sqlite/lib/src/sqlite_api.dart @@ -337,7 +337,7 @@ class NpDbSqlite implements NpDb { } @override - Future syncFavoriteFiles({ + Future syncFavoriteFiles({ required DbAccount account, required List favoriteFileIds, }) async { @@ -370,10 +370,10 @@ class NpDbSqlite implements NpDb { isFavorite: const OrNull(false), ); } - return DbSyncResult( - insert: inserts.length, - delete: deletes.length, - update: 0, + return DbSyncIdResult( + insert: inserts, + delete: deletes, + update: const [], ); }); } @@ -834,7 +834,7 @@ class NpDbSqlite implements NpDb { } @override - Future syncTags({ + Future syncTags({ required DbAccount account, required List tags, }) async { @@ -863,10 +863,10 @@ class NpDbSqlite implements NpDb { updates: updates, ); } - return DbSyncResult( - insert: inserts.length, - delete: deletes.length, - update: updates.length, + return DbSyncIdResult( + insert: inserts.map((e) => e.id).toList(), + delete: deletes.map((e) => e.id).toList(), + update: updates.map((e) => e.id).toList(), ); }); } From 7bb2f2452a64384e2bd5e034126863efb2c4e843 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Thu, 18 Jan 2024 01:19:48 +0800 Subject: [PATCH 18/42] Fix native events not propagated to controllers --- app/lib/controller/account_controller.dart | 8 +++++ app/lib/event/native_event.dart | 9 ++--- app/lib/event/native_event_relay.dart | 40 ++++++++++++++++++++++ app/lib/event/native_event_relay.g.dart | 14 ++++++++ 4 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 app/lib/event/native_event_relay.dart create mode 100644 app/lib/event/native_event_relay.g.dart diff --git a/app/lib/controller/account_controller.dart b/app/lib/controller/account_controller.dart index ecdcb1c2..9d761182 100644 --- a/app/lib/controller/account_controller.dart +++ b/app/lib/controller/account_controller.dart @@ -10,10 +10,12 @@ import 'package:nc_photos/controller/session_controller.dart'; import 'package:nc_photos/controller/sharings_controller.dart'; import 'package:nc_photos/controller/sync_controller.dart'; import 'package:nc_photos/di_container.dart'; +import 'package:nc_photos/event/native_event_relay.dart'; class AccountController { void setCurrentAccount(Account account) { _account = account; + _collectionsController?.dispose(); _collectionsController = null; _serverController?.dispose(); @@ -32,6 +34,9 @@ class AccountController { _placesController = null; _filesController?.dispose(); _filesController = null; + + _nativeEventRelay?.dispose(); + _nativeEventRelay = NativeEventRelay(filesController: filesController); } Account get account => _account!; @@ -87,6 +92,7 @@ class AccountController { ); Account? _account; + CollectionsController? _collectionsController; ServerController? _serverController; AccountPrefController? _accountPrefController; @@ -96,4 +102,6 @@ class AccountController { SharingsController? _sharingsController; PlacesController? _placesController; FilesController? _filesController; + + NativeEventRelay? _nativeEventRelay; } diff --git a/app/lib/event/native_event.dart b/app/lib/event/native_event.dart index 1406207e..9d3f2067 100644 --- a/app/lib/event/native_event.dart +++ b/app/lib/event/native_event.dart @@ -8,6 +8,7 @@ import 'package:np_platform_message_relay/np_platform_message_relay.dart'; part 'native_event.g.dart'; +@Deprecated("See AccountController.NativeEventRelay") class NativeEventListener { NativeEventListener(this.listener); @@ -31,7 +32,7 @@ class NativeEventListener { static final _mappedStream = MessageRelay.stream.whereType().map((ev) { switch (ev.event) { - case FileExifUpdatedEvent._id: + case FileExifUpdatedEvent.id: return FileExifUpdatedEvent.fromEvent(ev); default: @@ -51,20 +52,20 @@ class FileExifUpdatedEvent { const FileExifUpdatedEvent(this.fileIds); factory FileExifUpdatedEvent.fromEvent(Message ev) { - assert(ev.event == _id); + assert(ev.event == id); assert(ev.data != null); final dataJson = jsonDecode(ev.data!) as Map; return FileExifUpdatedEvent((dataJson["fileIds"] as List).cast()); } Message toEvent() => Message( - _id, + id, jsonEncode({ "fileIds": fileIds, }), ); - static const _id = "FileExifUpdatedEvent"; + static const id = "FileExifUpdatedEvent"; final List fileIds; } diff --git a/app/lib/event/native_event_relay.dart b/app/lib/event/native_event_relay.dart new file mode 100644 index 00000000..fe4997e4 --- /dev/null +++ b/app/lib/event/native_event_relay.dart @@ -0,0 +1,40 @@ +import 'dart:async'; + +import 'package:logging/logging.dart'; +import 'package:nc_photos/controller/files_controller.dart'; +import 'package:nc_photos/event/native_event.dart'; +import 'package:nc_photos/stream_extension.dart'; +import 'package:np_codegen/np_codegen.dart'; +import 'package:np_platform_message_relay/np_platform_message_relay.dart'; + +part 'native_event_relay.g.dart'; + +@npLog +class NativeEventRelay { + NativeEventRelay({ + required this.filesController, + }) { + _subscription = MessageRelay.stream.whereType().listen((event) { + switch (event.event) { + case FileExifUpdatedEvent.id: + _onFileExifUpdatedEvent(FileExifUpdatedEvent.fromEvent(event)); + break; + + default: + _log.severe('Unknown event: ${event.event}'); + break; + } + }); + } + + void dispose() { + _subscription?.cancel(); + } + + void _onFileExifUpdatedEvent(FileExifUpdatedEvent ev) { + filesController.applySyncResult(fileExifs: ev.fileIds); + } + + final FilesController filesController; + StreamSubscription? _subscription; +} diff --git a/app/lib/event/native_event_relay.g.dart b/app/lib/event/native_event_relay.g.dart new file mode 100644 index 00000000..d3c5aa4d --- /dev/null +++ b/app/lib/event/native_event_relay.g.dart @@ -0,0 +1,14 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'native_event_relay.dart'; + +// ************************************************************************** +// NpLogGenerator +// ************************************************************************** + +extension _$NativeEventRelayNpLog on NativeEventRelay { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("event.native_event_relay.NativeEventRelay"); +} From b2c1c39abbc70bd980add6e86baa67d8bc438864 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sun, 28 Jan 2024 22:44:21 +0800 Subject: [PATCH 19/42] Remove obsolete native event handler --- app/lib/widget/home_photos/bloc.dart | 23 ----------------------- app/lib/widget/home_photos2.dart | 3 --- 2 files changed, 26 deletions(-) diff --git a/app/lib/widget/home_photos/bloc.dart b/app/lib/widget/home_photos/bloc.dart index c7715cb7..79037908 100644 --- a/app/lib/widget/home_photos/bloc.dart +++ b/app/lib/widget/home_photos/bloc.dart @@ -63,8 +63,6 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _subscriptions.add(accountPrefController.shareFolder.listen((event) { add(_SetShareFolder(event)); })); - - _nativeFileExifUpdatedListener?.begin(); } @override @@ -72,7 +70,6 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { for (final s in _subscriptions) { s.cancel(); } - _nativeFileExifUpdatedListener?.end(); return super.close(); } @@ -322,14 +319,6 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); } - void _onNativeFileExifUpdated(FileExifUpdatedEvent ev) { - _log.info(ev); - _refreshThrottler.trigger( - maxResponceTime: const Duration(seconds: 3), - maxPendingCount: 10, - ); - } - Future _transformItems(List files) async { _log.info("[_transformItems] Queue ${files.length} items"); _itemTransformerQueue.addJob( @@ -419,18 +408,6 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { final _subscriptions = []; var _isHandlingError = false; var _isInitialLoad = true; - - // Listen to updates from background isolates as the memories are not shared - late final _nativeFileExifUpdatedListener = - getRawPlatform() == NpPlatform.android - ? NativeEventListener(_onNativeFileExifUpdated) - : null; - late final _refreshThrottler = Throttler( - onTriggered: (_) { - add(const _Reload()); - }, - logTag: _log.name, - ); } _ItemTransformerResult _buildItem(_ItemTransformerArgument arg) { diff --git a/app/lib/widget/home_photos2.dart b/app/lib/widget/home_photos2.dart index 22579630..b04e6a73 100644 --- a/app/lib/widget/home_photos2.dart +++ b/app/lib/widget/home_photos2.dart @@ -29,7 +29,6 @@ import 'package:nc_photos/entity/collection.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/event/event.dart'; -import 'package:nc_photos/event/native_event.dart'; import 'package:nc_photos/exception_event.dart'; import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/flutter_util.dart' as flutter_util; @@ -40,7 +39,6 @@ import 'package:nc_photos/service.dart' as service; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme/dimension.dart'; -import 'package:nc_photos/throttler.dart'; import 'package:nc_photos/widget/collection_browser.dart'; import 'package:nc_photos/widget/collection_picker.dart'; import 'package:nc_photos/widget/file_sharer_dialog.dart'; @@ -59,7 +57,6 @@ import 'package:np_codegen/np_codegen.dart'; import 'package:np_collection/np_collection.dart'; import 'package:np_common/object_util.dart'; import 'package:np_common/or_null.dart'; -import 'package:np_platform_util/np_platform_util.dart'; import 'package:to_string/to_string.dart'; import 'package:visibility_detector/visibility_detector.dart'; From 962829568fa210b0f596c680c5a7cd359042a24f Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sun, 28 Jan 2024 23:31:56 +0800 Subject: [PATCH 20/42] Fix log --- app/lib/controller/files_controller.dart | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/lib/controller/files_controller.dart b/app/lib/controller/files_controller.dart index e437498b..10ddc9b4 100644 --- a/app/lib/controller/files_controller.dart +++ b/app/lib/controller/files_controller.dart @@ -166,7 +166,9 @@ class FilesController { location: location, ); } catch (e, stackTrace) { - _log.severe("Failed while UpdateProperty: ${logFilename(f.fdPath)}", e, + _log.severe( + "[updateProperty] Failed while UpdateProperty: ${logFilename(f.fdPath)}", + e, stackTrace); failures.add(f.fdId); outdated.remove(f.fdId); @@ -198,7 +200,7 @@ class FilesController { for (final f in files) { final original = next.remove(f.fdId); if (original == null) { - _log.warning("[updateProperty] File not found: $f"); + _log.warning("[remove] File not found: $f"); continue; } backups[f.fdId] = original; @@ -212,13 +214,15 @@ class FilesController { account, files, onError: (index, value, error, stackTrace) { - _log.severe("Failed while Remove: ${logFilename(value.fdPath)}", - error, stackTrace); + _log.severe( + "[remove] Failed while Remove: ${logFilename(value.fdPath)}", + error, + stackTrace); failures.add(value.fdId); }, ); } catch (e, stackTrace) { - _log.severe("Failed while Remove", e, stackTrace); + _log.severe("[remove] Failed while Remove", e, stackTrace); failures.addAll(files.map((e) => e.fdId)); } if (failures.isNotEmpty) { From b7c0b187e2cb2b42190216ad6149d89b8c99887d Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Mon, 29 Jan 2024 00:25:19 +0800 Subject: [PATCH 21/42] Fix set/unsetting favorite in viewer does not reflect in browser --- app/lib/widget/viewer.dart | 81 ++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/app/lib/widget/viewer.dart b/app/lib/widget/viewer.dart index f0cd54b1..4ed2bce3 100644 --- a/app/lib/widget/viewer.dart +++ b/app/lib/widget/viewer.dart @@ -22,14 +22,11 @@ import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/flutter_util.dart'; import 'package:nc_photos/k.dart' as k; -import 'package:nc_photos/notified_action.dart'; import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/platform/features.dart' as features; import 'package:nc_photos/share_handler.dart'; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/theme.dart'; -import 'package:nc_photos/use_case/inflate_file_descriptor.dart'; -import 'package:nc_photos/use_case/update_property.dart'; import 'package:nc_photos/widget/disposable.dart'; import 'package:nc_photos/widget/handler/archive_selection_handler.dart'; import 'package:nc_photos/widget/handler/remove_selection_handler.dart'; @@ -537,32 +534,31 @@ class _ViewerState extends State return; } - final fd = _streamFilesView[_viewerController.currentPage]; - final c = KiwiContainer().resolve(); - final file = (await InflateFileDescriptor(c)(widget.account, [fd])).first; setState(() { _pageStates[index]!.favoriteOverride = true; }); _pageStates[index]!.isProcessingFavorite = true; + final fd = _streamFilesView[_viewerController.currentPage]; try { - await NotifiedAction( - () => UpdateProperty(c)( - widget.account, - file, - favorite: true, - ), - null, - L10n.global().favoriteSuccessNotification, - failureText: L10n.global().favoriteFailureNotification, - )(); - } catch (e, stackTrace) { - _log.shout( - "[_onFavoritePressed] Failed while UpdateProperty", e, stackTrace); - setState(() { - _pageStates[index]!.favoriteOverride = false; - }); + await context.read().filesController.updateProperty( + [fd], + isFavorite: true, + errorBuilder: (fileIds) { + if (mounted) { + SnackBarManager().showSnackBar(SnackBar( + content: Text(L10n.global().unfavoriteFailureNotification), + duration: k.snackBarDurationNormal, + )); + setState(() { + _pageStates[index]!.favoriteOverride = false; + }); + } + return null; + }, + ); + } finally { + _pageStates[index]!.isProcessingFavorite = false; } - _pageStates[index]!.isProcessingFavorite = false; } Future _onUnfavoritePressed(int index) async { @@ -571,32 +567,31 @@ class _ViewerState extends State return; } - final fd = _streamFilesView[_viewerController.currentPage]; - final c = KiwiContainer().resolve(); - final file = (await InflateFileDescriptor(c)(widget.account, [fd])).first; setState(() { _pageStates[index]!.favoriteOverride = false; }); _pageStates[index]!.isProcessingFavorite = true; + final fd = _streamFilesView[_viewerController.currentPage]; try { - await NotifiedAction( - () => UpdateProperty(c)( - widget.account, - file, - favorite: false, - ), - null, - L10n.global().unfavoriteSuccessNotification, - failureText: L10n.global().unfavoriteFailureNotification, - )(); - } catch (e, stackTrace) { - _log.shout( - "[_onUnfavoritePressed] Failed while UpdateProperty", e, stackTrace); - setState(() { - _pageStates[index]!.favoriteOverride = true; - }); + await context.read().filesController.updateProperty( + [fd], + isFavorite: false, + errorBuilder: (fileIds) { + if (mounted) { + SnackBarManager().showSnackBar(SnackBar( + content: Text(L10n.global().unfavoriteFailureNotification), + duration: k.snackBarDurationNormal, + )); + setState(() { + _pageStates[index]!.favoriteOverride = true; + }); + } + return null; + }, + ); + } finally { + _pageStates[index]!.isProcessingFavorite = false; } - _pageStates[index]!.isProcessingFavorite = false; } void _onDetailsPressed() { From 50d38d374e7af00b359a9d3b317d1242ead5f0b6 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Wed, 31 Jan 2024 01:11:21 +0800 Subject: [PATCH 22/42] Deprecate FavoriteResyncedEvent --- app/lib/event/event.dart | 1 + app/lib/use_case/cache_favorite.dart | 9 +-------- app/lib/use_case/startup_sync.dart | 5 ----- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/app/lib/event/event.dart b/app/lib/event/event.dart index 5fa710c8..9c84221f 100644 --- a/app/lib/event/event.dart +++ b/app/lib/event/event.dart @@ -89,6 +89,7 @@ class ShareRemovedEvent { final Share share; } +@Deprecated("not fired anymore, to be removed") class FavoriteResyncedEvent { const FavoriteResyncedEvent(this.account); diff --git a/app/lib/use_case/cache_favorite.dart b/app/lib/use_case/cache_favorite.dart index ab70e247..8f70ed3f 100644 --- a/app/lib/use_case/cache_favorite.dart +++ b/app/lib/use_case/cache_favorite.dart @@ -1,10 +1,7 @@ -import 'package:event_bus/event_bus.dart'; -import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/db/entity_converter.dart'; import 'package:nc_photos/di_container.dart'; -import 'package:nc_photos/event/event.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:np_db/np_db.dart'; @@ -20,14 +17,10 @@ class CacheFavorite { Future call( Account account, Iterable remoteFileIds) async { _log.info("[call] Cache favorites"); - final result = await _c.npDb.syncFavoriteFiles( + return _c.npDb.syncFavoriteFiles( account: account.toDb(), favoriteFileIds: remoteFileIds.toList(), ); - if (result.isNotEmpty) { - KiwiContainer().resolve().fire(FavoriteResyncedEvent(account)); - } - return result; } final DiContainer _c; diff --git a/app/lib/use_case/startup_sync.dart b/app/lib/use_case/startup_sync.dart index 6d8546f5..129685cc 100644 --- a/app/lib/use_case/startup_sync.dart +++ b/app/lib/use_case/startup_sync.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:event_bus/event_bus.dart'; import 'package:flutter_isolate/flutter_isolate.dart'; import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; @@ -11,7 +10,6 @@ import 'package:nc_photos/controller/files_controller.dart'; import 'package:nc_photos/controller/persons_controller.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/person.dart'; -import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/use_case/person/sync_person.dart'; import 'package:nc_photos/use_case/sync_favorite.dart'; import 'package:nc_photos/use_case/sync_tag.dart'; @@ -96,9 +94,6 @@ class StartupSync { _$StartupSyncNpLog.log.info('[_broadcastResult] $result'); if (result.syncFavoriteResult != null) { filesController.applySyncResult(favorites: result.syncFavoriteResult!); - // legacy - final eventBus = KiwiContainer().resolve(); - eventBus.fire(FavoriteResyncedEvent(account)); } if (result.isSyncPersonUpdated) { personsController.reload(); From fcdb3a62752d9d06e473e2dca67b35621346f015 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Wed, 31 Jan 2024 01:16:44 +0800 Subject: [PATCH 23/42] Add log --- app/lib/event/native_event.dart | 7 +++++-- app/lib/event/native_event.g.dart | 12 ++++++------ app/lib/event/native_event_relay.dart | 1 + 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/lib/event/native_event.dart b/app/lib/event/native_event.dart index 9d3f2067..7ace815a 100644 --- a/app/lib/event/native_event.dart +++ b/app/lib/event/native_event.dart @@ -3,8 +3,8 @@ import 'dart:convert'; import 'package:logging/logging.dart'; import 'package:nc_photos/stream_extension.dart'; -import 'package:np_codegen/np_codegen.dart'; import 'package:np_platform_message_relay/np_platform_message_relay.dart'; +import 'package:to_string/to_string.dart'; part 'native_event.g.dart'; @@ -47,7 +47,7 @@ class NativeEventListener { Logger("event.native_event.NativeEventListener<${T.runtimeType}>"); } -@npLog +@toString class FileExifUpdatedEvent { const FileExifUpdatedEvent(this.fileIds); @@ -65,6 +65,9 @@ class FileExifUpdatedEvent { }), ); + @override + String toString() => _$toString(); + static const id = "FileExifUpdatedEvent"; final List fileIds; diff --git a/app/lib/event/native_event.g.dart b/app/lib/event/native_event.g.dart index 0fcc5da5..99e4cadd 100644 --- a/app/lib/event/native_event.g.dart +++ b/app/lib/event/native_event.g.dart @@ -3,12 +3,12 @@ part of 'native_event.dart'; // ************************************************************************** -// NpLogGenerator +// ToStringGenerator // ************************************************************************** -extension _$FileExifUpdatedEventNpLog on FileExifUpdatedEvent { - // ignore: unused_element - Logger get _log => log; - - static final log = Logger("event.native_event.FileExifUpdatedEvent"); +extension _$FileExifUpdatedEventToString on FileExifUpdatedEvent { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "FileExifUpdatedEvent {fileIds: [length: ${fileIds.length}]}"; + } } diff --git a/app/lib/event/native_event_relay.dart b/app/lib/event/native_event_relay.dart index fe4997e4..f05bcd46 100644 --- a/app/lib/event/native_event_relay.dart +++ b/app/lib/event/native_event_relay.dart @@ -32,6 +32,7 @@ class NativeEventRelay { } void _onFileExifUpdatedEvent(FileExifUpdatedEvent ev) { + _log.info(ev); filesController.applySyncResult(fileExifs: ev.fileIds); } From 5dca7db9920eef936c9716c59783b88101a9f226 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Wed, 31 Jan 2024 01:23:58 +0800 Subject: [PATCH 24/42] Move file refresh logic from photos bloc to controller --- app/lib/controller/files_controller.dart | 13 ++++++++++++- app/lib/widget/home_photos/bloc.dart | 9 --------- app/lib/widget/home_photos/state_event.dart | 10 ---------- app/lib/widget/home_photos2.g.dart | 7 ------- 4 files changed, 12 insertions(+), 27 deletions(-) diff --git a/app/lib/controller/files_controller.dart b/app/lib/controller/files_controller.dart index 10ddc9b4..1befcbd9 100644 --- a/app/lib/controller/files_controller.dart +++ b/app/lib/controller/files_controller.dart @@ -42,9 +42,19 @@ class FilesController { this._c, { required this.account, required this.accountPrefController, - }); + }) { + _subscriptions.add(accountPrefController.shareFolder.listen((event) { + // sync remote if share folder is modified + if (_isDataStreamInited) { + syncRemote(); + } + })); + } void dispose() { + for (final s in _subscriptions) { + s.cancel(); + } _dataStreamController.close(); } @@ -367,6 +377,7 @@ class FilesController { final _mutex = Mutex(); var _isSyncing = false; + final _subscriptions = []; } @toString diff --git a/app/lib/widget/home_photos/bloc.dart b/app/lib/widget/home_photos/bloc.dart index 79037908..b1ac1be1 100644 --- a/app/lib/widget/home_photos/bloc.dart +++ b/app/lib/widget/home_photos/bloc.dart @@ -43,7 +43,6 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { on<_SetMemoriesRange>(_onSetMemoriesRange); on<_SetEnableExif>(_onSetEnableExif); on<_UpdateDateTimeGroup>(_onUpdateDateTimeGroup); - on<_SetShareFolder>(_onSetShareFolder); on<_SetError>(_onSetError); @@ -60,9 +59,6 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _subscriptions.add(prefController.isEnableExif.listen((event) { add(_SetEnableExif(event)); })); - _subscriptions.add(accountPrefController.shareFolder.listen((event) { - add(_SetShareFolder(event)); - })); } @override @@ -309,11 +305,6 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _transformItems(state.files); } - void _onSetShareFolder(_SetShareFolder ev, Emitter<_State> emit) { - _log.info(ev); - add(const _Reload()); - } - void _onSetError(_SetError ev, Emitter<_State> emit) { _log.info(ev); emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); diff --git a/app/lib/widget/home_photos/state_event.dart b/app/lib/widget/home_photos/state_event.dart index ded809db..29e9ccb0 100644 --- a/app/lib/widget/home_photos/state_event.dart +++ b/app/lib/widget/home_photos/state_event.dart @@ -248,16 +248,6 @@ class _SetEnableExif implements _Event { final bool value; } -@toString -class _SetShareFolder implements _Event { - const _SetShareFolder(this.value); - - @override - String toString() => _$toString(); - - final String value; -} - @toString class _UpdateDateTimeGroup implements _Event { const _UpdateDateTimeGroup(); diff --git a/app/lib/widget/home_photos2.g.dart b/app/lib/widget/home_photos2.g.dart index bf18d3d9..e5ea0a66 100644 --- a/app/lib/widget/home_photos2.g.dart +++ b/app/lib/widget/home_photos2.g.dart @@ -280,13 +280,6 @@ extension _$_SetEnableExifToString on _SetEnableExif { } } -extension _$_SetShareFolderToString on _SetShareFolder { - String _$toString() { - // ignore: unnecessary_string_interpolations - return "_SetShareFolder {value: $value}"; - } -} - extension _$_UpdateDateTimeGroupToString on _UpdateDateTimeGroup { String _$toString() { // ignore: unnecessary_string_interpolations From 88af19ac67d2607c28e02189cb805d2accf9dfc5 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Wed, 31 Jan 2024 01:31:52 +0800 Subject: [PATCH 25/42] Move metadata logic out of photos bloc --- app/lib/controller/account_controller.dart | 22 ++++- app/lib/controller/metadata_controller.dart | 97 +++++++++++++++++++ app/lib/controller/metadata_controller.g.dart | 15 +++ app/lib/controller/session_controller.dart | 2 + app/lib/widget/home_photos/bloc.dart | 53 +--------- app/lib/widget/home_photos/state_event.dart | 16 +-- app/lib/widget/home_photos2.dart | 8 +- app/lib/widget/home_photos2.g.dart | 11 +-- app/lib/widget/my_app.dart | 6 +- 9 files changed, 152 insertions(+), 78 deletions(-) create mode 100644 app/lib/controller/metadata_controller.dart create mode 100644 app/lib/controller/metadata_controller.g.dart diff --git a/app/lib/controller/account_controller.dart b/app/lib/controller/account_controller.dart index 9d761182..20e1e2d1 100644 --- a/app/lib/controller/account_controller.dart +++ b/app/lib/controller/account_controller.dart @@ -3,8 +3,10 @@ import 'package:nc_photos/account.dart'; import 'package:nc_photos/controller/account_pref_controller.dart'; import 'package:nc_photos/controller/collections_controller.dart'; import 'package:nc_photos/controller/files_controller.dart'; +import 'package:nc_photos/controller/metadata_controller.dart'; import 'package:nc_photos/controller/persons_controller.dart'; import 'package:nc_photos/controller/places_controller.dart'; +import 'package:nc_photos/controller/pref_controller.dart'; import 'package:nc_photos/controller/server_controller.dart'; import 'package:nc_photos/controller/session_controller.dart'; import 'package:nc_photos/controller/sharings_controller.dart'; @@ -13,6 +15,10 @@ import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/event/native_event_relay.dart'; class AccountController { + AccountController({ + required this.prefController, + }); + void setCurrentAccount(Account account) { _account = account; @@ -35,6 +41,8 @@ class AccountController { _filesController?.dispose(); _filesController = null; + _metadataController?.dispose(); + _metadataController = null; _nativeEventRelay?.dispose(); _nativeEventRelay = NativeEventRelay(filesController: filesController); } @@ -84,13 +92,22 @@ class AccountController { account: _account!, ); - FilesController get filesController => - _filesController ??= FilesController( + FilesController get filesController => _filesController ??= FilesController( KiwiContainer().resolve(), account: _account!, accountPrefController: accountPrefController, ); + MetadataController get metadataController => + _metadataController ??= MetadataController( + KiwiContainer().resolve(), + account: account, + filesController: filesController, + prefController: prefController, + ); + + PrefController prefController; + Account? _account; CollectionsController? _collectionsController; @@ -103,5 +120,6 @@ class AccountController { PlacesController? _placesController; FilesController? _filesController; + MetadataController? _metadataController; NativeEventRelay? _nativeEventRelay; } diff --git a/app/lib/controller/metadata_controller.dart b/app/lib/controller/metadata_controller.dart new file mode 100644 index 00000000..0b3e8d0e --- /dev/null +++ b/app/lib/controller/metadata_controller.dart @@ -0,0 +1,97 @@ +import 'dart:async'; + +import 'package:logging/logging.dart'; +import 'package:nc_photos/account.dart'; +import 'package:nc_photos/controller/files_controller.dart'; +import 'package:nc_photos/controller/pref_controller.dart'; +import 'package:nc_photos/db/entity_converter.dart'; +import 'package:nc_photos/di_container.dart'; +import 'package:nc_photos/entity/file_descriptor.dart'; +import 'package:nc_photos/entity/file_util.dart' as file_util; +import 'package:nc_photos/service.dart' as service; +import 'package:np_codegen/np_codegen.dart'; + +part 'metadata_controller.g.dart'; + +@npLog +class MetadataController { + MetadataController( + this._c, { + required this.account, + required this.filesController, + required this.prefController, + }) { + _subscriptions.add(filesController.stream.listen(_onFilesEvent)); + _subscriptions.add(prefController.isEnableExif.listen(_onSetEnableExif)); + } + + void dispose() { + for (final s in _subscriptions) { + s.cancel(); + } + } + + /// Normally EXIF task only run once, call this function to make it run again + /// after receiving new files + void scheduleNext() { + _hasStarted = false; + } + + Future _onFilesEvent(FilesStreamEvent ev) async { + _log.info("[_onFilesEvent]"); + if (!prefController.isEnableExif.value) { + // disabled + return; + } + if (ev.data.isNotEmpty && !ev.hasNext) { + // finished querying + if (!_hasStarted) { + await _startMetadataTask(ev.data); + } + } + } + + void _onSetEnableExif(bool value) { + _log.info("[_onSetEnableExif]"); + if (value) { + final filesState = filesController.stream.value; + if (filesState.hasNext || filesState.data.isEmpty) { + _log.info("[_onSetEnableExif] Ignored as data not ready"); + return; + } + _startMetadataTask(filesState.data); + } else { + _stopMetadataTask(); + } + } + + Future _startMetadataTask(List data) async { + _hasStarted = true; + try { + final missingCount = await _c.npDb.countFilesByFileIdsMissingMetadata( + account: account.toDb(), + fileIds: data.map((e) => e.fdId).toList(), + mimes: file_util.supportedImageFormatMimes, + ); + _log.info("[_startMetadataTask] Missing count: $missingCount"); + if (missingCount > 0) { + unawaited(service.startService()); + } + } catch (e, stackTrace) { + _log.shout( + "[_startMetadataTask] Failed starting metadata task", e, stackTrace); + } + } + + void _stopMetadataTask() { + service.stopService(); + } + + final DiContainer _c; + final Account account; + final FilesController filesController; + final PrefController prefController; + + final _subscriptions = []; + var _hasStarted = false; +} diff --git a/app/lib/controller/metadata_controller.g.dart b/app/lib/controller/metadata_controller.g.dart new file mode 100644 index 00000000..506407ee --- /dev/null +++ b/app/lib/controller/metadata_controller.g.dart @@ -0,0 +1,15 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'metadata_controller.dart'; + +// ************************************************************************** +// NpLogGenerator +// ************************************************************************** + +extension _$MetadataControllerNpLog on MetadataController { + // ignore: unused_element + Logger get _log => log; + + static final log = + Logger("controller.metadata_controller.MetadataController"); +} diff --git a/app/lib/controller/session_controller.dart b/app/lib/controller/session_controller.dart index 82d9a68e..c3af94a8 100644 --- a/app/lib/controller/session_controller.dart +++ b/app/lib/controller/session_controller.dart @@ -9,9 +9,11 @@ class SessionController { _hasFiredMetadataTaskController.close(); } + @Deprecated("Use MetadataController") ValueStream get hasFiredMetadataTask => _hasFiredMetadataTaskController.stream; + @Deprecated("Use MetadataController") void setFiredMetadataTask(bool value) { _hasFiredMetadataTaskController.add(value); } diff --git a/app/lib/widget/home_photos/bloc.dart b/app/lib/widget/home_photos/bloc.dart index b1ac1be1..9e4768ae 100644 --- a/app/lib/widget/home_photos/bloc.dart +++ b/app/lib/widget/home_photos/bloc.dart @@ -9,16 +9,16 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { required this.prefController, required this.accountPrefController, required this.collectionsController, - required this.sessionController, required this.syncController, required this.personsController, + required this.metadataController, }) : super(_State.init( zoom: prefController.homePhotosZoomLevel.value, isEnableMemoryCollection: accountPrefController.isEnableMemoryAlbum.value, )) { on<_LoadItems>(_onLoad); - on<_Reload>(_onReload); + on<_RequestRefresh>(_onRequestRefresh); on<_TransformItems>(_onTransformItems); on<_OnItemTransformed>(_onOnItemTransformed); @@ -41,7 +41,6 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { on<_SetEnableMemoryCollection>(_onSetEnableMemoryCollection); on<_SetSortByName>(_onSetSortByName); on<_SetMemoriesRange>(_onSetMemoriesRange); - on<_SetEnableExif>(_onSetEnableExif); on<_UpdateDateTimeGroup>(_onUpdateDateTimeGroup); on<_SetError>(_onSetError); @@ -56,9 +55,6 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _subscriptions.add(prefController.memoriesRange.listen((event) { add(_SetMemoriesRange(event)); })); - _subscriptions.add(prefController.isEnableExif.listen((event) { - add(_SetEnableExif(event)); - })); } @override @@ -118,10 +114,11 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { ); } - void _onReload(_Reload ev, Emitter<_State> emit) { + void _onRequestRefresh(_RequestRefresh ev, Emitter<_State> emit) { _log.info(ev); emit(state.copyWith(syncProgress: const Progress(0))); _syncRemote(); + metadataController.scheduleNext(); } void _onTransformItems(_TransformItems ev, Emitter<_State> emit) { @@ -143,7 +140,6 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { personsController: personsController, personProvider: accountPrefController.personProvider.value, ); - _tryStartMetadataTask(); } void _onSetSelectedItems(_SetSelectedItems ev, Emitter<_State> emit) { @@ -291,15 +287,6 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _transformItems(state.files); } - void _onSetEnableExif(_SetEnableExif ev, Emitter<_State> emit) { - _log.info(ev); - if (ev.value) { - _tryStartMetadataTask(ignoreFired: true); - } else { - _stopMetadataTask(); - } - } - void _onUpdateDateTimeGroup(_UpdateDateTimeGroup ev, Emitter<_State> emit) { _log.info(ev); _transformItems(state.files); @@ -354,45 +341,15 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { emit(state.copyWith(selectedItems: const {})); } - Future _tryStartMetadataTask({ - bool ignoreFired = false, - }) async { - if (state.files.isNotEmpty && - prefController.isEnableExif.value && - (ignoreFired || !sessionController.hasFiredMetadataTask.value)) { - sessionController.setFiredMetadataTask(true); - try { - final missingMetadataCount = - await _c.npDb.countFilesByFileIdsMissingMetadata( - account: account.toDb(), - fileIds: state.files.map((e) => e.fdId).toList(), - mimes: file_util.supportedImageFormatMimes, - ); - _log.info( - "[_tryStartMetadataTask] Missing count: $missingMetadataCount"); - if (missingMetadataCount > 0) { - unawaited(service.startService()); - } - } catch (e, stackTrace) { - _log.shout("[_tryStartMetadataTask] Failed starting metadata task", e, - stackTrace); - } - } - } - - void _stopMetadataTask() { - service.stopService(); - } - final DiContainer _c; final Account account; final FilesController controller; final PrefController prefController; final AccountPrefController accountPrefController; final CollectionsController collectionsController; - final SessionController sessionController; final SyncController syncController; final PersonsController personsController; + final MetadataController metadataController; final _itemTransformerQueue = ComputeQueue<_ItemTransformerArgument, _ItemTransformerResult>(); diff --git a/app/lib/widget/home_photos/state_event.dart b/app/lib/widget/home_photos/state_event.dart index 29e9ccb0..efb372bc 100644 --- a/app/lib/widget/home_photos/state_event.dart +++ b/app/lib/widget/home_photos/state_event.dart @@ -65,9 +65,11 @@ class _LoadItems implements _Event { String toString() => _$toString(); } +/// User explicitly requested to refresh the data, e.g., performed the +/// pull-to-refresh action @toString -class _Reload implements _Event { - const _Reload(); +class _RequestRefresh implements _Event { + const _RequestRefresh(); @override String toString() => _$toString(); @@ -238,16 +240,6 @@ class _SetMemoriesRange implements _Event { final int value; } -@toString -class _SetEnableExif implements _Event { - const _SetEnableExif(this.value); - - @override - String toString() => _$toString(); - - final bool value; -} - @toString class _UpdateDateTimeGroup implements _Event { const _UpdateDateTimeGroup(); diff --git a/app/lib/widget/home_photos2.dart b/app/lib/widget/home_photos2.dart index b04e6a73..b29ce93c 100644 --- a/app/lib/widget/home_photos2.dart +++ b/app/lib/widget/home_photos2.dart @@ -18,11 +18,10 @@ import 'package:nc_photos/controller/account_controller.dart'; import 'package:nc_photos/controller/account_pref_controller.dart'; import 'package:nc_photos/controller/collections_controller.dart'; import 'package:nc_photos/controller/files_controller.dart'; +import 'package:nc_photos/controller/metadata_controller.dart'; import 'package:nc_photos/controller/persons_controller.dart'; import 'package:nc_photos/controller/pref_controller.dart'; -import 'package:nc_photos/controller/session_controller.dart'; import 'package:nc_photos/controller/sync_controller.dart'; -import 'package:nc_photos/db/entity_converter.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/download_handler.dart'; import 'package:nc_photos/entity/collection.dart'; @@ -35,7 +34,6 @@ import 'package:nc_photos/flutter_util.dart' as flutter_util; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/language_util.dart' as language_util; import 'package:nc_photos/progress_util.dart'; -import 'package:nc_photos/service.dart' as service; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme/dimension.dart'; @@ -85,9 +83,9 @@ class HomePhotos2 extends StatelessWidget { prefController: context.read(), accountPrefController: accountController.accountPrefController, collectionsController: accountController.collectionsController, - sessionController: accountController.sessionController, syncController: accountController.syncController, personsController: accountController.personsController, + metadataController: accountController.metadataController, ), child: const _WrappedHomePhotos(), ); @@ -299,7 +297,7 @@ class _BodyState extends State<_Body> { .copyWith(scrollbars: false), child: RefreshIndicator( onRefresh: () async { - _bloc.add(const _Reload()); + _bloc.add(const _RequestRefresh()); var hasNotNull = false; await _bloc.stream.firstWhere((s) { if (s.syncProgress != null) { diff --git a/app/lib/widget/home_photos2.g.dart b/app/lib/widget/home_photos2.g.dart index e5ea0a66..00a1a537 100644 --- a/app/lib/widget/home_photos2.g.dart +++ b/app/lib/widget/home_photos2.g.dart @@ -146,10 +146,10 @@ extension _$_LoadItemsToString on _LoadItems { } } -extension _$_ReloadToString on _Reload { +extension _$_RequestRefreshToString on _RequestRefresh { String _$toString() { // ignore: unnecessary_string_interpolations - return "_Reload {}"; + return "_RequestRefresh {}"; } } @@ -273,13 +273,6 @@ extension _$_SetMemoriesRangeToString on _SetMemoriesRange { } } -extension _$_SetEnableExifToString on _SetEnableExif { - String _$toString() { - // ignore: unnecessary_string_interpolations - return "_SetEnableExif {value: $value}"; - } -} - extension _$_UpdateDateTimeGroupToString on _UpdateDateTimeGroup { String _$toString() { // ignore: unnecessary_string_interpolations diff --git a/app/lib/widget/my_app.dart b/app/lib/widget/my_app.dart index 747ddb40..d2970f30 100644 --- a/app/lib/widget/my_app.dart +++ b/app/lib/widget/my_app.dart @@ -70,10 +70,12 @@ class MyApp extends StatelessWidget { return MultiRepositoryProvider( providers: [ RepositoryProvider( - create: (_) => AccountController(), + create: (_) => PrefController(_c), ), RepositoryProvider( - create: (_) => PrefController(_c), + create: (context) => AccountController( + prefController: context.read(), + ), ), RepositoryProvider( create: (_) => _c.npDb, From 54e8c127b9ecac08130d15260ec1726ab745ac8a Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Wed, 31 Jan 2024 01:35:12 +0800 Subject: [PATCH 26/42] Handle ImageProcessorUploadSuccessEvent --- app/lib/controller/account_controller.dart | 5 +++- app/lib/event/native_event_relay.dart | 29 ++++++++++++++++--- np_platform_image_processor/build.yaml | 12 ++++++++ .../lib/src/event.dart | 8 +++++ .../lib/src/event.g.dart | 15 ++++++++++ np_platform_image_processor/pubspec.yaml | 10 +++++++ 6 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 np_platform_image_processor/build.yaml create mode 100644 np_platform_image_processor/lib/src/event.g.dart diff --git a/app/lib/controller/account_controller.dart b/app/lib/controller/account_controller.dart index 20e1e2d1..cdd66061 100644 --- a/app/lib/controller/account_controller.dart +++ b/app/lib/controller/account_controller.dart @@ -44,7 +44,10 @@ class AccountController { _metadataController?.dispose(); _metadataController = null; _nativeEventRelay?.dispose(); - _nativeEventRelay = NativeEventRelay(filesController: filesController); + _nativeEventRelay = NativeEventRelay( + filesController: filesController, + metadataController: metadataController, + ); } Account get account => _account!; diff --git a/app/lib/event/native_event_relay.dart b/app/lib/event/native_event_relay.dart index f05bcd46..1271058d 100644 --- a/app/lib/event/native_event_relay.dart +++ b/app/lib/event/native_event_relay.dart @@ -2,19 +2,24 @@ import 'dart:async'; import 'package:logging/logging.dart'; import 'package:nc_photos/controller/files_controller.dart'; +import 'package:nc_photos/controller/metadata_controller.dart'; import 'package:nc_photos/event/native_event.dart'; +import 'package:nc_photos/platform/features.dart' as features; import 'package:nc_photos/stream_extension.dart'; import 'package:np_codegen/np_codegen.dart'; +import 'package:np_platform_image_processor/np_platform_image_processor.dart'; import 'package:np_platform_message_relay/np_platform_message_relay.dart'; part 'native_event_relay.g.dart'; +/// Convert native events into actions on the corresponding controllers @npLog class NativeEventRelay { NativeEventRelay({ required this.filesController, + required this.metadataController, }) { - _subscription = MessageRelay.stream.whereType().listen((event) { + _subscriptions.add(MessageRelay.stream.whereType().listen((event) { switch (event.event) { case FileExifUpdatedEvent.id: _onFileExifUpdatedEvent(FileExifUpdatedEvent.fromEvent(event)); @@ -24,11 +29,19 @@ class NativeEventRelay { _log.severe('Unknown event: ${event.event}'); break; } - }); + })); + + if (features.isSupportEnhancement) { + _subscriptions.add(ImageProcessor.stream + .whereType() + .listen(_onImageProcessorUploadSuccessEvent)); + } } void dispose() { - _subscription?.cancel(); + for (final s in _subscriptions) { + s.cancel(); + } } void _onFileExifUpdatedEvent(FileExifUpdatedEvent ev) { @@ -36,6 +49,14 @@ class NativeEventRelay { filesController.applySyncResult(fileExifs: ev.fileIds); } + void _onImageProcessorUploadSuccessEvent( + ImageProcessorUploadSuccessEvent ev) { + _log.info(ev); + filesController.syncRemote(); + metadataController.scheduleNext(); + } + final FilesController filesController; - StreamSubscription? _subscription; + final MetadataController metadataController; + final _subscriptions = []; } diff --git a/np_platform_image_processor/build.yaml b/np_platform_image_processor/build.yaml new file mode 100644 index 00000000..3f712dba --- /dev/null +++ b/np_platform_image_processor/build.yaml @@ -0,0 +1,12 @@ +targets: + $default: + builders: + to_string_build: + options: + formatStringNameMapping: + double: "${$?.toStringAsFixed(3)}" + List: "[length: ${$?.length}]" + Set: "{length: ${$?.length}}" + File: "${$?.path}" + FileDescriptor: "${$?.fdPath}" + useEnumName: true diff --git a/np_platform_image_processor/lib/src/event.dart b/np_platform_image_processor/lib/src/event.dart index 3c85173b..f7a4ddf8 100644 --- a/np_platform_image_processor/lib/src/event.dart +++ b/np_platform_image_processor/lib/src/event.dart @@ -1,3 +1,7 @@ +import 'package:to_string/to_string.dart'; + +part 'event.g.dart'; + abstract class ImageProcessorEvent { static ImageProcessorEvent fromNativeEvent(dynamic ev) { final id = ev["event"]; @@ -10,6 +14,7 @@ abstract class ImageProcessorEvent { } } +@toString class ImageProcessorUploadSuccessEvent implements ImageProcessorEvent { const ImageProcessorUploadSuccessEvent._(); @@ -18,5 +23,8 @@ class ImageProcessorUploadSuccessEvent implements ImageProcessorEvent { return const ImageProcessorUploadSuccessEvent._(); } + @override + String toString() => _$toString(); + static const _id = "ImageProcessorUploadSuccessEvent"; } diff --git a/np_platform_image_processor/lib/src/event.g.dart b/np_platform_image_processor/lib/src/event.g.dart new file mode 100644 index 00000000..fb2854ef --- /dev/null +++ b/np_platform_image_processor/lib/src/event.g.dart @@ -0,0 +1,15 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'event.dart'; + +// ************************************************************************** +// ToStringGenerator +// ************************************************************************** + +extension _$ImageProcessorUploadSuccessEventToString + on ImageProcessorUploadSuccessEvent { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "ImageProcessorUploadSuccessEvent {}"; + } +} diff --git a/np_platform_image_processor/pubspec.yaml b/np_platform_image_processor/pubspec.yaml index 1d98bfc1..8f6e5b1f 100644 --- a/np_platform_image_processor/pubspec.yaml +++ b/np_platform_image_processor/pubspec.yaml @@ -17,6 +17,11 @@ dependencies: path: ../codegen np_platform_raw_image: path: ../np_platform_raw_image + to_string: + git: + url: https://gitlab.com/nkming2/dart-to-string + ref: to_string-1.0.0 + path: to_string dev_dependencies: build_runner: ^2.2.1 @@ -24,6 +29,11 @@ dev_dependencies: path: ../codegen_build np_lints: path: ../np_lints + to_string_build: + git: + url: https://gitlab.com/nkming2/dart-to-string + ref: to_string_build-1.0.0 + path: to_string_build flutter: plugin: From be0caa1c48798bd22c3851dabcc2ef54517a834c Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Wed, 31 Jan 2024 01:36:04 +0800 Subject: [PATCH 27/42] Deprecate pref updated events --- app/lib/entity/pref.dart | 25 ++++--------------------- app/lib/event/event.dart | 2 ++ 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/app/lib/entity/pref.dart b/app/lib/entity/pref.dart index 41b67bee..d5063bc4 100644 --- a/app/lib/entity/pref.dart +++ b/app/lib/entity/pref.dart @@ -2,12 +2,9 @@ import 'dart:async'; import 'dart:convert'; import 'package:collection/collection.dart'; -import 'package:event_bus/event_bus.dart'; -import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/entity/pref/provider/memory.dart'; -import 'package:nc_photos/event/event.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:np_common/type.dart'; @@ -30,14 +27,8 @@ class Pref { } Future _set(PrefKey key, T value, - Future Function(PrefKey key, T value) setFn) async { - if (await setFn(key, value)) { - KiwiContainer().resolve().fire(PrefUpdatedEvent(key, value)); - return true; - } else { - return false; - } - } + Future Function(PrefKey key, T value) setFn) => + setFn(key, value); Future _remove(PrefKey key) => provider.remove(key); @@ -69,16 +60,8 @@ class AccountPref { Future toJson() => provider.toJson(); Future _set(AccountPrefKey key, T value, - Future Function(AccountPrefKey key, T value) setFn) async { - if (await setFn(key, value)) { - KiwiContainer() - .resolve() - .fire(AccountPrefUpdatedEvent(this, key, value)); - return true; - } else { - return false; - } - } + Future Function(AccountPrefKey key, T value) setFn) => + setFn(key, value); Future _remove(AccountPrefKey key) => provider.remove(key); diff --git a/app/lib/event/event.dart b/app/lib/event/event.dart index 9c84221f..126bb1c7 100644 --- a/app/lib/event/event.dart +++ b/app/lib/event/event.dart @@ -37,6 +37,7 @@ class AppEventListener { final _log = Logger("event.event.AppEventListener<${T.runtimeType}>"); } +@Deprecated("not fired anymore, to be removed") class AccountPrefUpdatedEvent { const AccountPrefUpdatedEvent(this.pref, this.key, this.value); @@ -116,6 +117,7 @@ class MetadataTaskStateChangedEvent { final MetadataTaskState state; } +@Deprecated("not fired anymore, to be removed") class PrefUpdatedEvent { PrefUpdatedEvent(this.key, this.value); From 482158c6bff496d44e900e962deeaf22450d1921 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Wed, 31 Jan 2024 01:39:40 +0800 Subject: [PATCH 28/42] Remove unused import --- app/lib/widget/home.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/app/lib/widget/home.dart b/app/lib/widget/home.dart index bce80336..cea64e80 100644 --- a/app/lib/widget/home.dart +++ b/app/lib/widget/home.dart @@ -19,7 +19,6 @@ import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme/dimension.dart'; import 'package:nc_photos/use_case/import_potential_shared_album.dart'; import 'package:nc_photos/widget/home_collections.dart'; -import 'package:nc_photos/widget/home_photos.dart'; import 'package:nc_photos/widget/home_photos2.dart'; import 'package:nc_photos/widget/home_search.dart'; import 'package:np_codegen/np_codegen.dart'; From 5278cda402c390f73eed03cc57b5177ee1a06a74 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sun, 4 Feb 2024 22:29:50 +0800 Subject: [PATCH 29/42] Use controller to remove files in RemoveSelectionHandler --- .../handler/remove_selection_handler.dart | 49 +++++-------------- app/lib/widget/home_photos.dart | 5 +- app/lib/widget/home_search.dart | 6 ++- app/lib/widget/trashbin_browser.dart | 6 ++- app/lib/widget/trashbin_viewer.dart | 7 ++- app/lib/widget/viewer.dart | 6 +-- 6 files changed, 31 insertions(+), 48 deletions(-) diff --git a/app/lib/widget/handler/remove_selection_handler.dart b/app/lib/widget/handler/remove_selection_handler.dart index 3959abcb..6b06dbe5 100644 --- a/app/lib/widget/handler/remove_selection_handler.dart +++ b/app/lib/widget/handler/remove_selection_handler.dart @@ -1,16 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_localizations.dart'; -import 'package:nc_photos/debug_util.dart'; -import 'package:nc_photos/di_container.dart'; +import 'package:nc_photos/controller/files_controller.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/navigation_manager.dart'; import 'package:nc_photos/snack_bar_manager.dart'; -import 'package:nc_photos/use_case/inflate_file_descriptor.dart'; -import 'package:nc_photos/use_case/remove.dart'; import 'package:nc_photos/widget/trashbin_browser.dart'; import 'package:np_codegen/np_codegen.dart'; @@ -18,11 +14,9 @@ part 'remove_selection_handler.g.dart'; @npLog class RemoveSelectionHandler { - RemoveSelectionHandler(this._c) - : assert(require(_c)), - assert(InflateFileDescriptor.require(_c)); - - static bool require(DiContainer c) => true; + const RemoveSelectionHandler({ + required this.filesController, + }); /// Remove [selectedFiles] and return the removed count Future call({ @@ -31,44 +25,25 @@ class RemoveSelectionHandler { bool shouldCleanupAlbum = true, bool isRemoveOpened = false, bool isMoveToTrash = false, - bool shouldShowProcessingText = true, }) async { - final selectedFiles = await InflateFileDescriptor(_c)(account, selection); - final String processingText, successText; + final String successText; final String Function(int) failureText; if (isRemoveOpened) { - processingText = L10n.global().deleteProcessingNotification; successText = L10n.global().deleteSuccessNotification; failureText = (_) => L10n.global().deleteFailureNotification; } else { - processingText = L10n.global() - .deleteSelectedProcessingNotification(selectedFiles.length); successText = L10n.global().deleteSelectedSuccessNotification; failureText = (count) => L10n.global().deleteSelectedFailureNotification(count); } - if (shouldShowProcessingText) { - SnackBarManager().showSnackBar( - SnackBar( - content: Text(processingText), - duration: k.snackBarDurationShort, - ), - canBeReplaced: true, - ); - } var failureCount = 0; - await Remove(KiwiContainer().resolve())( - account, - selectedFiles, - onError: (_, file, e, stackTrace) { - _log.shout( - "[call] Failed while removing file: ${logFilename(file.fdPath)}", - e, - stackTrace); - ++failureCount; + await filesController.remove( + selection, + errorBuilder: (fileIds) { + failureCount = fileIds.length; + return RemoveFailureError(fileIds); }, - shouldCleanUp: shouldCleanupAlbum, ); final trashAction = isMoveToTrash ? SnackBarAction( @@ -93,8 +68,8 @@ class RemoveSelectionHandler { action: trashAction, )); } - return selectedFiles.length - failureCount; + return selection.length - failureCount; } - final DiContainer _c; + final FilesController filesController; } diff --git a/app/lib/widget/home_photos.dart b/app/lib/widget/home_photos.dart index 40bb1ebc..2e15fec7 100644 --- a/app/lib/widget/home_photos.dart +++ b/app/lib/widget/home_photos.dart @@ -472,7 +472,6 @@ class _HomePhotosState extends State } Future _onSelectionDeletePressed(BuildContext context) async { - final c = KiwiContainer().resolve(); final selectedFiles = selectedListItems .whereType() .map((e) => e.file) @@ -480,7 +479,9 @@ class _HomePhotosState extends State setState(() { clearSelectedItems(); }); - await RemoveSelectionHandler(c)( + await RemoveSelectionHandler( + filesController: context.read().filesController, + )( account: widget.account, selection: selectedFiles, isMoveToTrash: true, diff --git a/app/lib/widget/home_search.dart b/app/lib/widget/home_search.dart index d908d52c..af03b86e 100644 --- a/app/lib/widget/home_search.dart +++ b/app/lib/widget/home_search.dart @@ -8,6 +8,7 @@ import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/bloc/search.dart'; +import 'package:nc_photos/controller/account_controller.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/download_handler.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; @@ -492,7 +493,6 @@ class _HomeSearchState extends State } Future _onSelectionDeletePressed(BuildContext context) async { - final c = KiwiContainer().resolve(); final selectedFiles = selectedListItems .whereType() .map((e) => e.file) @@ -500,7 +500,9 @@ class _HomeSearchState extends State setState(() { clearSelectedItems(); }); - await RemoveSelectionHandler(c)( + await RemoveSelectionHandler( + filesController: context.read().filesController, + )( account: widget.account, selection: selectedFiles, isMoveToTrash: true, diff --git a/app/lib/widget/trashbin_browser.dart b/app/lib/widget/trashbin_browser.dart index 5dd05ade..233b28c7 100644 --- a/app/lib/widget/trashbin_browser.dart +++ b/app/lib/widget/trashbin_browser.dart @@ -8,6 +8,7 @@ import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/bloc/ls_trashbin.dart'; +import 'package:nc_photos/controller/account_controller.dart'; import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/file.dart'; @@ -384,8 +385,9 @@ class _TrashbinBrowserState extends State } Future _deleteFiles(List files) async { - final c = KiwiContainer().resolve(); - await RemoveSelectionHandler(c)( + await RemoveSelectionHandler( + filesController: context.read().filesController, + )( account: widget.account, selection: files, shouldCleanupAlbum: false, diff --git a/app/lib/widget/trashbin_viewer.dart b/app/lib/widget/trashbin_viewer.dart index 242216ee..b81dbcf5 100644 --- a/app/lib/widget/trashbin_viewer.dart +++ b/app/lib/widget/trashbin_viewer.dart @@ -1,10 +1,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_localizations.dart'; +import 'package:nc_photos/controller/account_controller.dart'; import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/file.dart'; @@ -315,10 +317,11 @@ class _TrashbinViewerState extends State { } Future _delete(BuildContext context) async { - final c = KiwiContainer().resolve(); final file = widget.streamFiles[_viewerController.currentPage]; _log.info("[_delete] Removing file: ${file.path}"); - final count = await RemoveSelectionHandler(c)( + final count = await RemoveSelectionHandler( + filesController: context.read().filesController, + )( account: widget.account, selection: [file], shouldCleanupAlbum: false, diff --git a/app/lib/widget/viewer.dart b/app/lib/widget/viewer.dart index 4ed2bce3..3fe49b16 100644 --- a/app/lib/widget/viewer.dart +++ b/app/lib/widget/viewer.dart @@ -646,15 +646,15 @@ class _ViewerState extends State void _onDeletePressed(BuildContext context) { final index = _viewerController.currentPage; - final c = KiwiContainer().resolve(); final file = _streamFilesView[index]; _log.info("[_onDeletePressed] Removing file: ${file.fdPath}"); - unawaited(RemoveSelectionHandler(c)( + unawaited(RemoveSelectionHandler( + filesController: context.read().filesController, + )( account: widget.account, selection: [file], isRemoveOpened: true, isMoveToTrash: true, - shouldShowProcessingText: false, )); _removeCurrentItemFromStream(context, index); } From 455aedd22bc8e0433bb6259fd9117e1ee27010c2 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sun, 4 Feb 2024 22:32:11 +0800 Subject: [PATCH 30/42] Remove legacy home photos --- app/lib/bloc/scan_account_dir.dart | 551 ------------ app/lib/bloc/scan_account_dir.g.dart | 47 - app/lib/widget/home_photos.dart | 1182 -------------------------- app/lib/widget/home_photos.g.dart | 14 - 4 files changed, 1794 deletions(-) delete mode 100644 app/lib/bloc/scan_account_dir.dart delete mode 100644 app/lib/bloc/scan_account_dir.g.dart delete mode 100644 app/lib/widget/home_photos.dart delete mode 100644 app/lib/widget/home_photos.g.dart diff --git a/app/lib/bloc/scan_account_dir.dart b/app/lib/bloc/scan_account_dir.dart deleted file mode 100644 index fa09fcad..00000000 --- a/app/lib/bloc/scan_account_dir.dart +++ /dev/null @@ -1,551 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:kiwi/kiwi.dart'; -import 'package:logging/logging.dart'; -import 'package:nc_photos/account.dart'; -import 'package:nc_photos/bloc/bloc_util.dart' as bloc_util; -import 'package:nc_photos/bloc/progress.dart'; -import 'package:nc_photos/debug_util.dart'; -import 'package:nc_photos/di_container.dart'; -import 'package:nc_photos/entity/file.dart'; -import 'package:nc_photos/entity/file_descriptor.dart'; -import 'package:nc_photos/entity/file_util.dart' as file_util; -import 'package:nc_photos/entity/pref.dart'; -import 'package:nc_photos/event/event.dart'; -import 'package:nc_photos/event/native_event.dart'; -import 'package:nc_photos/exception.dart'; -import 'package:nc_photos/progress_util.dart'; -import 'package:nc_photos/stream_extension.dart'; -import 'package:nc_photos/throttler.dart'; -import 'package:nc_photos/use_case/ls.dart'; -import 'package:nc_photos/use_case/scan_dir.dart'; -import 'package:nc_photos/use_case/scan_dir_offline.dart'; -import 'package:nc_photos/use_case/sync_dir.dart'; -import 'package:np_codegen/np_codegen.dart'; -import 'package:np_platform_image_processor/np_platform_image_processor.dart'; -import 'package:np_platform_util/np_platform_util.dart'; -import 'package:to_string/to_string.dart'; - -part 'scan_account_dir.g.dart'; - -abstract class ScanAccountDirBlocEvent { - const ScanAccountDirBlocEvent(); -} - -@toString -abstract class ScanAccountDirBlocQueryBase extends ScanAccountDirBlocEvent { - const ScanAccountDirBlocQueryBase({ - this.progressBloc, - }); - - @override - String toString() => _$toString(); - - /// Get notified about the query progress - final ProgressBloc? progressBloc; -} - -class ScanAccountDirBlocQuery extends ScanAccountDirBlocQueryBase { - const ScanAccountDirBlocQuery({ - super.progressBloc, - }); -} - -class ScanAccountDirBlocRefresh extends ScanAccountDirBlocQueryBase { - const ScanAccountDirBlocRefresh({ - super.progressBloc, - }); -} - -/// An external event has happened and may affect the state of this bloc -@toString -class _ScanAccountDirBlocExternalEvent extends ScanAccountDirBlocEvent { - const _ScanAccountDirBlocExternalEvent(); - - @override - String toString() => _$toString(); -} - -@toString -abstract class ScanAccountDirBlocState { - const ScanAccountDirBlocState(this.files); - - @override - String toString() => _$toString(); - - final List files; -} - -class ScanAccountDirBlocInit extends ScanAccountDirBlocState { - const ScanAccountDirBlocInit() : super(const []); -} - -class ScanAccountDirBlocLoading extends ScanAccountDirBlocState { - const ScanAccountDirBlocLoading( - List files, { - this.isInitialLoad = false, - }) : super(files); - - final bool isInitialLoad; -} - -class ScanAccountDirBlocSuccess extends ScanAccountDirBlocState { - const ScanAccountDirBlocSuccess(List files) : super(files); -} - -@toString -class ScanAccountDirBlocFailure extends ScanAccountDirBlocState { - const ScanAccountDirBlocFailure(List files, this.exception) - : super(files); - - @override - String toString() => _$toString(); - - final dynamic exception; -} - -/// The state of this bloc is inconsistent. This typically means that the data -/// may have been changed externally -class ScanAccountDirBlocInconsistent extends ScanAccountDirBlocState { - const ScanAccountDirBlocInconsistent(List files) - : super(files); -} - -/// A bloc that return all files under a dir recursively -/// -/// See [ScanDir] -@npLog -class ScanAccountDirBloc - extends Bloc { - ScanAccountDirBloc._(this.account) : super(const ScanAccountDirBlocInit()) { - final c = KiwiContainer().resolve(); - assert(require(c)); - _c = c; - - _fileRemovedEventListener.begin(); - _filePropertyUpdatedEventListener.begin(); - _fileTrashbinRestoredEventListener.begin(); - _fileMovedEventListener.begin(); - _favoriteResyncedEventListener.begin(); - _prefUpdatedEventListener.begin(); - _accountPrefUpdatedEventListener.begin(); - - _nativeFileExifUpdatedListener?.begin(); - _imageProcessorUploadSuccessListener = _imageProcessorUploadSuccessStream - ?.listen(_onImageProcessorUploadSuccessEvent); - - on(_onEvent, transformer: ((events, mapper) { - return events.distinct((a, b) { - // only handle ScanAccountDirBlocQuery - final r = a is ScanAccountDirBlocQuery && b is ScanAccountDirBlocQuery; - if (r) { - _log.fine("[on] Skip identical ScanAccountDirBlocQuery event"); - } - return r; - }).asyncExpand(mapper); - })); - } - - static bool require(DiContainer c) => - DiContainer.has(c, DiType.fileRepo) && - DiContainer.has(c, DiType.touchManager); - - static ScanAccountDirBloc of(Account account) { - final name = - bloc_util.getInstNameForRootAwareAccount("ScanAccountDirBloc", account); - try { - _log.fine("[of] Resolving bloc for '$name'"); - return KiwiContainer().resolve(name); - } catch (_) { - // no created instance for this account, make a new one - _log.info("[of] New bloc instance for account: $account"); - final bloc = ScanAccountDirBloc._(account); - KiwiContainer().registerInstance(bloc, name: name); - return bloc; - } - } - - @override - Future close() { - _fileRemovedEventListener.end(); - _filePropertyUpdatedEventListener.end(); - _fileTrashbinRestoredEventListener.end(); - _fileMovedEventListener.end(); - _favoriteResyncedEventListener.end(); - _prefUpdatedEventListener.end(); - _accountPrefUpdatedEventListener.end(); - - _nativeFileExifUpdatedListener?.end(); - _imageProcessorUploadSuccessListener?.cancel(); - _imageProcessorUploadSuccessListener = null; - - _refreshThrottler.clear(); - return super.close(); - } - - Future _onEvent(ScanAccountDirBlocEvent event, - Emitter emit) async { - _log.info("[_onEvent] $event"); - if (event is ScanAccountDirBlocQueryBase) { - await _onEventQuery(event, emit); - } else if (event is _ScanAccountDirBlocExternalEvent) { - await _onExternalEvent(event, emit); - } - } - - Future _onEventQuery(ScanAccountDirBlocQueryBase ev, - Emitter emit) async { - _log.info("[_onEventQuery] $ev"); - emit(ScanAccountDirBlocLoading(state.files)); - final hasContent = state.files.isNotEmpty; - - final stopwatch = Stopwatch()..start(); - if (!hasContent) { - try { - emit(ScanAccountDirBlocLoading(await _queryOfflineMini(ev))); - } catch (e, stackTrace) { - _log.shout( - "[_onEventQuery] Failed while _queryOfflineMini", e, stackTrace); - } - _log.info( - "[_onEventQuery] Elapsed time (_queryOfflineMini): ${stopwatch.elapsedMilliseconds}ms"); - stopwatch.reset(); - } - final cacheFiles = await _queryOffline(ev); - _log.info( - "[_onEventQuery] Elapsed time (_queryOffline): ${stopwatch.elapsedMilliseconds}ms, ${cacheFiles.length} files"); - if (!hasContent) { - // show something instantly on first load - emit(ScanAccountDirBlocLoading(cacheFiles)); - } - - if (!hasContent && cacheFiles.isEmpty) { - emit(const ScanAccountDirBlocLoading([], isInitialLoad: true)); - } - - stopwatch.reset(); - final bool hasUpdate; - try { - hasUpdate = await _syncOnline( - ev, - onProgressUpdate: (value) { - if (ev.progressBloc?.isClosed == false) { - ev.progressBloc! - .add(ProgressBlocUpdate(value.progress, value.text)); - } - }, - ); - } catch (e, stackTrace) { - _log.shout("[_onEventQuery] Exception while request", e, stackTrace); - emit(ScanAccountDirBlocFailure(cacheFiles, e)); - return; - } - _log.info( - "[_onEventQuery] Elapsed time (_syncOnline): ${stopwatch.elapsedMilliseconds}ms, hasUpdate: $hasUpdate"); - if (hasUpdate) { - // content updated, reload from db - stopwatch.reset(); - final newFiles = await _queryOffline(ev); - _log.info( - "[_onEventQuery] Elapsed time (_queryOffline) 2nd pass: ${stopwatch.elapsedMilliseconds}ms, ${newFiles.length} files"); - emit(ScanAccountDirBlocSuccess(newFiles)); - } else { - emit(ScanAccountDirBlocSuccess(cacheFiles)); - } - } - - Future _syncOnline( - ScanAccountDirBlocQueryBase ev, { - ValueChanged? onProgressUpdate, - }) async { - final settings = AccountPref.of(account); - final shareDir = - File(path: file_util.unstripPath(account, settings.getShareFolderOr())); - bool isShareDirIncluded = false; - - bool hasUpdate = false; - _c.touchManager.clearTouchCache(); - final progress = IntProgress(account.roots.length); - for (final r in account.roots) { - final dirPath = file_util.unstripPath(account, r); - hasUpdate |= await SyncDir(_c)( - account, - dirPath, - onProgressUpdate: (value) { - final merged = progress.progress + progress.step * value.progress; - onProgressUpdate?.call(Progress(merged, value.text)); - }, - ); - isShareDirIncluded |= - file_util.isOrUnderDir(shareDir, File(path: dirPath)); - progress.next(); - } - - if (!isShareDirIncluded) { - _log.info("[_syncOnline] Explicitly scanning share folder"); - hasUpdate |= await SyncDir(_c)( - account, - file_util.unstripPath(account, settings.getShareFolderOr()), - isRecursive: false, - ); - } - return hasUpdate; - } - - Future _onExternalEvent(_ScanAccountDirBlocExternalEvent ev, - Emitter emit) async { - emit(ScanAccountDirBlocInconsistent(state.files)); - } - - void _onFileRemovedEvent(FileRemovedEvent ev) { - if (state is ScanAccountDirBlocInit) { - // no data in this bloc, ignore - return; - } - if (_isFileOfInterest(ev.file)) { - _log.info("[_onFileRemovedEvent] Request refresh"); - _refreshThrottler.trigger( - maxResponceTime: const Duration(seconds: 3), - maxPendingCount: 10, - ); - } - } - - void _onFilePropertyUpdatedEvent(FilePropertyUpdatedEvent ev) { - if (!ev.hasAnyProperties([ - FilePropertyUpdatedEvent.propMetadata, - FilePropertyUpdatedEvent.propIsArchived, - FilePropertyUpdatedEvent.propOverrideDateTime, - FilePropertyUpdatedEvent.propFavorite, - ])) { - // not interested - return; - } - if (state is ScanAccountDirBlocInit) { - // no data in this bloc, ignore - return; - } - if (!_isFileOfInterest(ev.file)) { - return; - } - - _log.info("[_onFilePropertyUpdatedEvent] Request refresh"); - if (ev.hasAnyProperties([ - FilePropertyUpdatedEvent.propIsArchived, - FilePropertyUpdatedEvent.propOverrideDateTime, - FilePropertyUpdatedEvent.propFavorite, - ])) { - _refreshThrottler.trigger( - maxResponceTime: const Duration(seconds: 3), - maxPendingCount: 10, - ); - } else { - _refreshThrottler.trigger( - maxResponceTime: const Duration(seconds: 10), - maxPendingCount: 10, - ); - } - } - - void _onFileTrashbinRestoredEvent(FileTrashbinRestoredEvent ev) { - if (state is ScanAccountDirBlocInit) { - // no data in this bloc, ignore - return; - } - _log.info("[_onFileTrashbinRestoredEvent] Request refresh"); - _refreshThrottler.trigger( - maxResponceTime: const Duration(seconds: 3), - maxPendingCount: 10, - ); - } - - void _onFileMovedEvent(FileMovedEvent ev) { - if (state is ScanAccountDirBlocInit) { - // no data in this bloc, ignore - return; - } - if (_isFileOfInterest(ev.file)) { - _log.info("[_onFileMovedEvent] Request refresh"); - _refreshThrottler.trigger( - maxResponceTime: const Duration(seconds: 3), - maxPendingCount: 10, - ); - } - } - - void _onFavoriteResyncedEvent(FavoriteResyncedEvent ev) { - if (state is ScanAccountDirBlocInit) { - // no data in this bloc, ignore - return; - } - if (ev.account.compareServerIdentity(account)) { - _log.info("[_onFavoriteResyncedEvent] Request refresh"); - _refreshThrottler.trigger( - maxResponceTime: const Duration(seconds: 3), - maxPendingCount: 10, - ); - } - } - - void _onPrefUpdatedEvent(PrefUpdatedEvent ev) { - if (state is ScanAccountDirBlocInit) { - // no data in this bloc, ignore - return; - } - if (ev.key == PrefKey.accounts3) { - _log.info("[_onPrefUpdatedEvent] Request refresh"); - _refreshThrottler.trigger( - maxResponceTime: const Duration(seconds: 3), - maxPendingCount: 10, - ); - } - } - - void _onAccountPrefUpdatedEvent(AccountPrefUpdatedEvent ev) { - if (state is ScanAccountDirBlocInit) { - // no data in this bloc, ignore - return; - } - if (ev.key == AccountPrefKey.shareFolder && - identical(ev.pref, AccountPref.of(account))) { - _log.info("[_onAccountPrefUpdatedEvent] Request refresh"); - _refreshThrottler.trigger( - maxResponceTime: const Duration(seconds: 3), - maxPendingCount: 10, - ); - } - } - - void _onNativeFileExifUpdated(FileExifUpdatedEvent ev) { - _log.info("[_onNativeFileExifUpdated] Request refresh"); - _refreshThrottler.trigger( - maxResponceTime: const Duration(seconds: 3), - maxPendingCount: 10, - ); - } - - void _onImageProcessorUploadSuccessEvent( - ImageProcessorUploadSuccessEvent ev) { - _log.info("[_onImageProcessorUploadSuccessEvent] Request refresh"); - _refreshThrottler.trigger( - maxResponceTime: const Duration(seconds: 3), - maxPendingCount: 10, - ); - } - - /// Query a small amount of files to give an illusion of quick startup - Future> _queryOfflineMini( - ScanAccountDirBlocQueryBase ev) async { - return await ScanDirOfflineMini(_c)( - account, - account.roots - .map((r) => File(path: file_util.unstripPath(account, r))) - .toList(), - scanMiniCount, - isOnlySupportedFormat: true, - ); - } - - Future> _queryOffline( - ScanAccountDirBlocQueryBase ev) async { - final settings = AccountPref.of(account); - final shareDir = - File(path: file_util.unstripPath(account, settings.getShareFolderOr())); - bool isShareDirIncluded = false; - - final files = []; - for (final r in account.roots) { - try { - final dir = File(path: file_util.unstripPath(account, r)); - files.addAll(await ScanDirOffline(_c)(account, dir, - isOnlySupportedFormat: true)); - isShareDirIncluded |= file_util.isOrUnderDir(shareDir, dir); - } catch (e, stackTrace) { - _log.shout( - "[_queryOffline] Failed while ScanDirOffline: ${logFilename(r)}", - e, - stackTrace); - } - } - - if (!isShareDirIncluded) { - _log.info("[_queryOffline] Explicitly scanning share folder"); - try { - final raw = await Ls(_c.fileRepoLocal)(account, shareDir); - files.addAll(raw.where((f) => file_util.isSupportedFormat(f))); - } on CacheNotFoundException catch (_) { - // normal when there's no cache - } catch (e, stackTrace) { - _log.shout( - "[_queryOffline] Failed while ScanDirOffline: ${logFilename(shareDir.path)}", - e, - stackTrace); - } - } - return files; - } - - bool _isFileOfInterest(FileDescriptor file) { - if (!file_util.isSupportedFormat(file)) { - return false; - } - - for (final r in account.roots) { - final dir = File(path: file_util.unstripPath(account, r)); - if (file_util.isUnderDir(file, dir)) { - return true; - } - } - - final settings = AccountPref.of(account); - final shareDir = - File(path: file_util.unstripPath(account, settings.getShareFolderOr())); - if (file_util.isUnderDir(file, shareDir)) { - return true; - } - return false; - } - - late final DiContainer _c; - - final Account account; - - static const scanMiniCount = 100; - - late final _fileRemovedEventListener = - AppEventListener(_onFileRemovedEvent); - late final _filePropertyUpdatedEventListener = - AppEventListener(_onFilePropertyUpdatedEvent); - late final _fileTrashbinRestoredEventListener = - AppEventListener(_onFileTrashbinRestoredEvent); - late final _fileMovedEventListener = - AppEventListener(_onFileMovedEvent); - late final _favoriteResyncedEventListener = - AppEventListener(_onFavoriteResyncedEvent); - late final _prefUpdatedEventListener = - AppEventListener(_onPrefUpdatedEvent); - late final _accountPrefUpdatedEventListener = - AppEventListener(_onAccountPrefUpdatedEvent); - - late final _nativeFileExifUpdatedListener = getRawPlatform() == NpPlatform.web - ? null - : NativeEventListener(_onNativeFileExifUpdated); - - Stream? - get _imageProcessorUploadSuccessStream => getRawPlatform() == - NpPlatform.web - ? null - : ImageProcessor.stream.whereType(); - StreamSubscription? _imageProcessorUploadSuccessListener; - - late final _refreshThrottler = Throttler( - onTriggered: (_) { - add(const _ScanAccountDirBlocExternalEvent()); - }, - logTag: "ScanAccountDirBloc.refresh", - ); - - static final _log = _$ScanAccountDirBlocNpLog.log; -} diff --git a/app/lib/bloc/scan_account_dir.g.dart b/app/lib/bloc/scan_account_dir.g.dart deleted file mode 100644 index 76f200b2..00000000 --- a/app/lib/bloc/scan_account_dir.g.dart +++ /dev/null @@ -1,47 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'scan_account_dir.dart'; - -// ************************************************************************** -// NpLogGenerator -// ************************************************************************** - -extension _$ScanAccountDirBlocNpLog on ScanAccountDirBloc { - // ignore: unused_element - Logger get _log => log; - - static final log = Logger("bloc.scan_account_dir.ScanAccountDirBloc"); -} - -// ************************************************************************** -// ToStringGenerator -// ************************************************************************** - -extension _$ScanAccountDirBlocQueryBaseToString on ScanAccountDirBlocQueryBase { - String _$toString() { - // ignore: unnecessary_string_interpolations - return "${objectRuntimeType(this, "ScanAccountDirBlocQueryBase")} {progressBloc: $progressBloc}"; - } -} - -extension _$_ScanAccountDirBlocExternalEventToString - on _ScanAccountDirBlocExternalEvent { - String _$toString() { - // ignore: unnecessary_string_interpolations - return "_ScanAccountDirBlocExternalEvent {}"; - } -} - -extension _$ScanAccountDirBlocStateToString on ScanAccountDirBlocState { - String _$toString() { - // ignore: unnecessary_string_interpolations - return "${objectRuntimeType(this, "ScanAccountDirBlocState")} {files: [length: ${files.length}]}"; - } -} - -extension _$ScanAccountDirBlocFailureToString on ScanAccountDirBlocFailure { - String _$toString() { - // ignore: unnecessary_string_interpolations - return "ScanAccountDirBlocFailure {files: [length: ${files.length}], exception: $exception}"; - } -} diff --git a/app/lib/widget/home_photos.dart b/app/lib/widget/home_photos.dart deleted file mode 100644 index 2e15fec7..00000000 --- a/app/lib/widget/home_photos.dart +++ /dev/null @@ -1,1182 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:math' as math; -import 'dart:ui'; - -import 'package:collection/collection.dart'; -import 'package:draggable_scrollbar/draggable_scrollbar.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:intl/intl.dart'; -import 'package:kiwi/kiwi.dart'; -import 'package:logging/logging.dart'; -import 'package:nc_photos/account.dart'; -import 'package:nc_photos/app_localizations.dart'; -import 'package:nc_photos/bloc/progress.dart'; -import 'package:nc_photos/bloc/scan_account_dir.dart'; -import 'package:nc_photos/controller/account_controller.dart'; -import 'package:nc_photos/db/entity_converter.dart'; -import 'package:nc_photos/di_container.dart'; -import 'package:nc_photos/download_handler.dart'; -import 'package:nc_photos/entity/collection.dart'; -import 'package:nc_photos/entity/file_descriptor.dart'; -import 'package:nc_photos/entity/file_util.dart' as file_util; -import 'package:nc_photos/entity/pref.dart'; -import 'package:nc_photos/event/event.dart'; -import 'package:nc_photos/exception_util.dart' as exception_util; -import 'package:nc_photos/k.dart' as k; -import 'package:nc_photos/language_util.dart' as language_util; -import 'package:nc_photos/metadata_task_manager.dart'; -import 'package:nc_photos/object_extension.dart'; -import 'package:nc_photos/service.dart' as service; -import 'package:nc_photos/share_handler.dart'; -import 'package:nc_photos/snack_bar_manager.dart'; -import 'package:nc_photos/stream_extension.dart'; -import 'package:nc_photos/theme.dart'; -import 'package:nc_photos/theme/dimension.dart'; -import 'package:nc_photos/throttler.dart'; -import 'package:nc_photos/widget/builder/photo_list_item_builder.dart'; -import 'package:nc_photos/widget/collection_browser.dart'; -import 'package:nc_photos/widget/handler/add_selection_to_collection_handler.dart'; -import 'package:nc_photos/widget/handler/archive_selection_handler.dart'; -import 'package:nc_photos/widget/handler/double_tap_exit_handler.dart'; -import 'package:nc_photos/widget/handler/remove_selection_handler.dart'; -import 'package:nc_photos/widget/home_app_bar.dart'; -import 'package:nc_photos/widget/navigation_bar_blur_filter.dart'; -import 'package:nc_photos/widget/page_visibility_mixin.dart'; -import 'package:nc_photos/widget/photo_list_item.dart'; -import 'package:nc_photos/widget/photo_list_util.dart' as photo_list_util; -import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart'; -import 'package:nc_photos/widget/selection_app_bar.dart'; -import 'package:nc_photos/widget/settings.dart'; -import 'package:nc_photos/widget/viewer.dart'; -import 'package:nc_photos/widget/zoom_menu_button.dart'; -import 'package:np_async/np_async.dart'; -import 'package:np_codegen/np_codegen.dart'; -import 'package:np_common/object_util.dart'; -import 'package:np_platform_image_processor/np_platform_image_processor.dart'; -import 'package:np_platform_util/np_platform_util.dart'; -import 'package:visibility_detector/visibility_detector.dart'; - -part 'home_photos.g.dart'; - -class HomePhotosBackToTopEvent { - const HomePhotosBackToTopEvent(); -} - -class HomePhotos extends StatefulWidget { - const HomePhotos({ - Key? key, - required this.account, - }) : super(key: key); - - @override - createState() => _HomePhotosState(); - - final Account account; -} - -@npLog -class _HomePhotosState extends State - with - SelectableItemStreamListMixin, - RouteAware, - PageVisibilityMixin, - TickerProviderStateMixin { - @override - initState() { - super.initState(); - _thumbZoomLevel = Pref().getHomePhotosZoomLevelOr(0); - _initBloc(); - _web?.onInitState(); - _prefUpdatedListener.begin(); - _imageProcessorUploadSuccessListener = _imageProcessorUploadSuccessStream - ?.listen(_onImageProcessorUploadSuccessEvent); - _onBackToTopListener.begin(); - } - - @override - dispose() { - _onBackToTopListener.end(); - _prefUpdatedListener.end(); - _imageProcessorUploadSuccessListener?.cancel(); - _imageProcessorUploadSuccessListener = null; - _web?.onDispose(); - super.dispose(); - } - - @override - build(BuildContext context) { - return BlocListener( - bloc: _bloc, - listener: (context, state) => _onStateChange(context, state), - child: _buildContent(context), - ); - } - - @override - onItemTap(SelectableItem item, int index) { - item.as()?.run((fileItem) { - Navigator.pushNamed( - context, - Viewer.routeName, - arguments: - ViewerArguments(widget.account, _backingFiles, fileItem.fileIndex), - ); - }); - } - - @override - onVisibilityChanged(VisibilityInfo info, int index, SelectableItem item) { - if (info.visibleFraction >= 0.2) { - _visibleItems.add(_VisibleItem(index, item)); - } else { - _visibleItems.remove(_VisibleItem(index, item)); - } - _visibilityThrottler.trigger( - maxResponceTime: const Duration(milliseconds: 500)); - } - - @override - onBackButtonPressed() async { - if (isSelectionMode) { - return super.onBackButtonPressed(); - } else { - return DoubleTapExitHandler()(); - } - } - - void _initBloc() { - if (_bloc.state is ScanAccountDirBlocInit) { - _log.info("[_initBloc] Initialize bloc"); - _reqQuery(); - } else { - // process the current state - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _onStateChange(context, _bloc.state); - }); - } - }); - } - } - - Widget _buildContent(BuildContext context) { - return LayoutBuilder(builder: (context, constraints) { - final scrollExtent = _getScrollViewExtent(context, constraints); - return Stack( - children: [ - buildItemStreamListOuter( - context, - child: DraggableScrollbar.semicircle( - controller: _scrollController, - overrideMaxScrollExtent: scrollExtent, - // status bar + app bar - topOffset: _calcAppBarExtent(context), - bottomOffset: AppDimension.of(context).homeBottomAppBarHeight, - labelTextBuilder: (_) => _buildScrollLabel(context), - labelPadding: const EdgeInsets.symmetric(horizontal: 40), - backgroundColor: Theme.of(context) - .elevate(Theme.of(context).colorScheme.inverseSurface, 3), - enabled: _isScrollbarVisible, - heightScrollThumb: 60, - child: ScrollConfiguration( - behavior: - ScrollConfiguration.of(context).copyWith(scrollbars: false), - child: RefreshIndicator( - onRefresh: () async { - _onRefreshSelected(context); - await _waitRefresh(); - }, - child: CustomScrollView( - controller: _scrollController, - slivers: [ - _buildAppBar(context), - _web?.buildContent(context), - if (AccountPref.of(widget.account) - .isEnableMemoryAlbumOr(true) && - _smartCollections.isNotEmpty) - _buildSmartAlbumList(context), - BlocBuilder( - bloc: _bloc, - builder: (context, state) { - if (_isInitialSync(state)) { - return _InitialLoadingProgress( - progressBloc: _queryProgressBloc, - ); - } else { - return buildItemStreamList( - maxCrossAxisExtent: _thumbSize.toDouble(), - onMaxExtentChanged: (value) { - setState(() { - _itemListMaxExtent = value; - }); - }, - isEnableVisibilityCallback: true, - ); - } - }, - ), - SliverToBoxAdapter( - child: SizedBox( - height: - AppDimension.of(context).homeBottomAppBarHeight, - ), - ), - ].whereNotNull().toList(), - ), - ), - ), - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: NavigationBarBlurFilter( - height: AppDimension.of(context).homeBottomAppBarHeight, - ), - ), - ], - ); - }); - } - - Widget _buildAppBar(BuildContext context) { - if (isSelectionMode) { - return _buildSelectionAppBar(context); - } else { - return _buildNormalAppBar(context); - } - } - - Widget _buildSelectionAppBar(BuildContext conetxt) { - return SelectionAppBar( - count: selectedListItems.length, - onClosePressed: () { - setState(() { - clearSelectedItems(); - }); - }, - actions: [ - IconButton( - icon: const Icon(Icons.share), - tooltip: L10n.global().shareTooltip, - onPressed: () { - _onSelectionSharePressed(context); - }, - ), - IconButton( - icon: const Icon(Icons.add), - tooltip: L10n.global().addItemToCollectionTooltip, - onPressed: () { - _onSelectionAddToAlbumPressed(context); - }, - ), - PopupMenuButton<_SelectionMenuOption>( - tooltip: MaterialLocalizations.of(context).moreButtonTooltip, - itemBuilder: (context) => [ - PopupMenuItem( - value: _SelectionMenuOption.download, - child: Text(L10n.global().downloadTooltip), - ), - PopupMenuItem( - value: _SelectionMenuOption.archive, - child: Text(L10n.global().archiveTooltip), - ), - PopupMenuItem( - value: _SelectionMenuOption.delete, - child: Text(L10n.global().deleteTooltip), - ), - ], - onSelected: (option) { - _onSelectionMenuSelected(context, option); - }, - ), - ], - ); - } - - Widget _buildNormalAppBar(BuildContext context) { - return BlocBuilder( - bloc: _bloc, - buildWhen: (previous, current) { - if (previous is ScanAccountDirBlocLoading && - current is ScanAccountDirBlocLoading) { - // both loading, check if initial flag changed - return previous.isInitialLoad != current.isInitialLoad; - } else { - // check if any one is loading == state changed from/to loading - return previous is ScanAccountDirBlocLoading || - current is ScanAccountDirBlocLoading; - } - }, - builder: (context, state) { - return HomeSliverAppBar( - account: widget.account, - isShowProgressIcon: !_isInitialSync(state) && - (state is ScanAccountDirBlocLoading || - _buildItemQueue.isProcessing) && - !_isRefreshIndicatorActive, - actions: [ - ZoomMenuButton( - initialZoom: _thumbZoomLevel, - minZoom: -1, - maxZoom: 2, - onZoomChanged: (value) { - _setThumbZoomLevel(value.round()); - Pref().setHomePhotosZoomLevel(_thumbZoomLevel); - }, - ), - ], - ); - }, - ); - } - - Widget _buildSmartAlbumList(BuildContext context) { - return SliverToBoxAdapter( - child: _SmartCollectionList( - account: widget.account, - collections: _smartCollections, - ), - ); - } - - Widget _buildScrollLabel(BuildContext context) { - final firstVisibleItem = _visibleItems - .sorted() - .firstWhereOrNull((e) => e.item is PhotoListFileItem); - final date = - firstVisibleItem?.item.as()?.file.fdDateTime; - if (date != null) { - final text = DateFormat(DateFormat.YEAR_ABBR_MONTH, - Localizations.localeOf(context).languageCode) - .format(date.toLocal()); - return _ScrollLabel( - child: Text(text), - ); - } else { - return const SizedBox(); - } - } - - void _onStateChange(BuildContext context, ScanAccountDirBlocState state) { - if (state is ScanAccountDirBlocInit) { - itemStreamListItems = []; - } else if (state is ScanAccountDirBlocSuccess) { - _transformItems(state.files, isPostSuccess: true); - } else if (state is ScanAccountDirBlocLoading) { - if (state.files.length > ScanAccountDirBloc.scanMiniCount) { - _isScrollbarVisible = true; - } - _transformItems(state.files); - } else if (state is ScanAccountDirBlocFailure) { - _isScrollbarVisible = true; - _transformItems(state.files); - if (isPageVisible()) { - SnackBarManager().showSnackBar(SnackBar( - content: Text(exception_util.toUserString(state.exception)), - duration: k.snackBarDurationNormal, - )); - } - } else if (state is ScanAccountDirBlocInconsistent) { - _reqQuery(); - } - } - - void _onRefreshSelected(BuildContext context) { - context - .read() - .sessionController - .setFiredMetadataTask(false); - _reqRefresh(); - } - - void _onSelectionMenuSelected( - BuildContext context, _SelectionMenuOption option) { - switch (option) { - case _SelectionMenuOption.archive: - _onSelectionArchivePressed(context); - break; - case _SelectionMenuOption.delete: - _onSelectionDeletePressed(context); - break; - case _SelectionMenuOption.download: - _onSelectionDownloadPressed(); - break; - default: - _log.shout("[_onSelectionMenuSelected] Unknown option: $option"); - break; - } - } - - void _onSelectionSharePressed(BuildContext context) { - final c = KiwiContainer().resolve(); - final selected = selectedListItems - .whereType() - .map((e) => e.file) - .toList(); - ShareHandler( - c, - context: context, - clearSelection: () { - setState(() { - clearSelectedItems(); - }); - }, - ).shareFiles(widget.account, selected); - } - - Future _onSelectionAddToAlbumPressed(BuildContext context) { - return const AddSelectionToCollectionHandler()( - context: context, - selection: selectedListItems - .whereType() - .map((e) => e.file) - .toList(), - clearSelection: () { - if (mounted) { - setState(() { - clearSelectedItems(); - }); - } - }, - ); - } - - void _onSelectionDownloadPressed() { - final c = KiwiContainer().resolve(); - final selected = selectedListItems - .whereType() - .map((e) => e.file) - .toList(); - DownloadHandler(c).downloadFiles(widget.account, selected); - setState(() { - clearSelectedItems(); - }); - } - - Future _onSelectionArchivePressed(BuildContext context) async { - final c = KiwiContainer().resolve(); - final selectedFiles = selectedListItems - .whereType() - .map((e) => e.file) - .toList(); - setState(() { - clearSelectedItems(); - }); - await ArchiveSelectionHandler(c)( - account: widget.account, - selection: selectedFiles, - ); - } - - Future _onSelectionDeletePressed(BuildContext context) async { - final selectedFiles = selectedListItems - .whereType() - .map((e) => e.file) - .toList(); - setState(() { - clearSelectedItems(); - }); - await RemoveSelectionHandler( - filesController: context.read().filesController, - )( - account: widget.account, - selection: selectedFiles, - isMoveToTrash: true, - ); - } - - void _onPrefUpdated(PrefUpdatedEvent ev) { - if (ev.key == PrefKey.enableExif) { - if (ev.value == true) { - _tryStartMetadataTask(context, ignoreFired: true); - } else { - _stopMetadataTask(); - } - } else if (ev.key == PrefKey.isPhotosTabSortByName) { - if (_bloc.state is! ScanAccountDirBlocInit) { - _log.info("[_onPrefUpdated] Update view after changing sort option"); - _transformItems(_bloc.state.files); - } - } else if (ev.key == PrefKey.memoriesRange) { - if (_bloc.state is! ScanAccountDirBlocInit) { - _log.info("[_onPrefUpdated] Update view after changing memories"); - _transformItems(_bloc.state.files); - } - } - } - - void _onImageProcessorUploadSuccessEvent( - ImageProcessorUploadSuccessEvent ev) { - _log.info( - "[_onImageProcessorUploadSuccessEvent] Scheduling metadata task after next refresh"); - context - .read() - .sessionController - .setFiredMetadataTask(false); - } - - void _onBackToTop(HomePhotosBackToTopEvent ev) { - _scrollController.jumpTo(0); - } - - Future _tryStartMetadataTask( - BuildContext context, { - bool ignoreFired = false, - }) async { - if (_bloc.state is ScanAccountDirBlocSuccess && - Pref().isEnableExifOr() && - (ignoreFired || - !context - .read() - .sessionController - .hasFiredMetadataTask - .value)) { - try { - final c = KiwiContainer().resolve(); - final missingMetadataCount = - await c.npDb.countFilesByFileIdsMissingMetadata( - account: widget.account.toDb(), - fileIds: _backingFiles.map((e) => e.fdId).toList(), - mimes: file_util.supportedImageFormatMimes, - ); - _log.info( - "[_tryStartMetadataTask] Missing count: $missingMetadataCount"); - if (missingMetadataCount > 0) { - if (_web != null) { - _web!.startMetadataTask(missingMetadataCount); - } else { - unawaited(service.startService()); - } - } - - context - .read() - .sessionController - .setFiredMetadataTask(true); - } catch (e, stackTrace) { - _log.shout("[_tryStartMetadataTask] Failed starting metadata task", e, - stackTrace); - } - } - } - - void _stopMetadataTask() { - if (_web == null) { - service.stopService(); - } - } - - /// Transform a File list to grid items - void _transformItems( - List files, { - bool isSorted = false, - bool isPostSuccess = false, - }) { - _log.info("[_transformItems] Queue ${files.length} items"); - final c = KiwiContainer().resolve(); - final PhotoListItemSorter? sorter; - final PhotoListItemGrouper? grouper; - if (Pref().isPhotosTabSortByNameOr()) { - sorter = isSorted ? null : photoListFilenameSorter; - grouper = null; - } else { - sorter = isSorted ? null : photoListFileDateTimeSorter; - grouper = PhotoListFileDateGrouper(isMonthOnly: _thumbZoomLevel < 0); - } - - _buildItemQueue.addJob( - PhotoListItemBuilderArguments( - widget.account, - files, - sorter: sorter, - grouper: grouper, - smartAlbumConfig: - PhotoListItemSmartAlbumConfig(c.pref.getMemoriesRangeOr()), - shouldShowFavoriteBadge: true, - locale: language_util.getSelectedLocale() ?? - PlatformDispatcher.instance.locale, - ), - buildPhotoListItem, - (result) { - if (mounted) { - setState(() { - _backingFiles = result.backingFiles; - itemStreamListItems = result.listItems; - _smartCollections = result.smartCollections; - - if (isPostSuccess) { - _isScrollbarVisible = true; - context.read().syncController.requestSync( - account: widget.account, - filesController: context.read(), - personsController: context.read(), - personProvider: _accountPrefController.personProvider.value, - ); - _tryStartMetadataTask(context); - } - }); - } - }, - ); - } - - void _reqQuery() { - _bloc.add(ScanAccountDirBlocQuery( - progressBloc: _queryProgressBloc, - )); - } - - void _reqRefresh() { - _bloc.add(const ScanAccountDirBlocRefresh()); - } - - Future _waitRefresh() async { - setState(() { - _isRefreshIndicatorActive = true; - }); - try { - while (true) { - await Future.delayed(const Duration(seconds: 1)); - if (_bloc.state is! ScanAccountDirBlocLoading) { - return; - } - } - } finally { - // To prevent the app bar icon appearing for a very short while - unawaited(Future.delayed(const Duration(seconds: 2)).then((_) { - if (mounted) { - setState(() { - _isRefreshIndicatorActive = false; - }); - } - })); - } - } - - void _setThumbZoomLevel(int level) { - final prevLevel = _thumbZoomLevel; - if ((prevLevel >= 0) != (level >= 0)) { - _thumbZoomLevel = level; - _transformItems(_backingFiles, isSorted: true); - } else { - setState(() { - _thumbZoomLevel = level; - }); - } - } - - /// Return the estimated scroll extent of the custom scroll view, or null - double? _getScrollViewExtent( - BuildContext context, BoxConstraints constraints) { - if (_itemListMaxExtent != null && constraints.hasBoundedHeight) { - final appBarExtent = _calcAppBarExtent(context); - final bottomAppBarExtent = - AppDimension.of(context).homeBottomAppBarHeight; - final metadataTaskHeaderExtent = _web?.getHeaderHeight() ?? 0; - final smartAlbumListHeight = - AccountPref.of(widget.account).isEnableMemoryAlbumOr(true) && - _smartCollections.isNotEmpty - ? _SmartAlbumItem.height - : 0; - // scroll extent = list height - widget viewport height - // + sliver app bar height + bottom app bar height - // + metadata task header height + smart album list height - final scrollExtent = _itemListMaxExtent! - - constraints.maxHeight + - appBarExtent + - bottomAppBarExtent + - metadataTaskHeaderExtent + - smartAlbumListHeight; - _log.info( - "[_getScrollViewExtent] $_itemListMaxExtent - ${constraints.maxHeight} + $appBarExtent + $bottomAppBarExtent + $metadataTaskHeaderExtent + $smartAlbumListHeight = $scrollExtent"); - return scrollExtent; - } else { - return null; - } - } - - bool _isInitialSync(ScanAccountDirBlocState state) => - state is ScanAccountDirBlocLoading && - state.files.isEmpty && - state.isInitialLoad; - - double _calcAppBarExtent(BuildContext context) => - MediaQuery.of(context).padding.top + kToolbarHeight; - - late final _bloc = ScanAccountDirBloc.of(widget.account); - late final _queryProgressBloc = ProgressBloc(); - late final _accountPrefController = - context.read().accountPrefController; - - var _backingFiles = []; - var _smartCollections = []; - - final _buildItemQueue = - ComputeQueue(); - - var _thumbZoomLevel = 0; - int get _thumbSize => photo_list_util.getThumbSize(_thumbZoomLevel); - - final ScrollController _scrollController = ScrollController(); - - double? _itemListMaxExtent; - - final _visibleItems = HashSet<_VisibleItem>(); - late final _visibilityThrottler = Throttler(onTriggered: (_) { - // label text is always 1 frame behind, so we need to update the text for - // the last frame - if (mounted) { - _log.fine("[_visibilityThrottler] Update screen"); - setState(() {}); - } - }); - - late final _prefUpdatedListener = - AppEventListener(_onPrefUpdated); - late final _onBackToTopListener = - AppEventListener(_onBackToTop); - - Stream? - get _imageProcessorUploadSuccessStream => getRawPlatform() == - NpPlatform.web - ? null - : ImageProcessor.stream.whereType(); - StreamSubscription? _imageProcessorUploadSuccessListener; - - late final _Web? _web = - getRawPlatform() == NpPlatform.web ? _Web(this) : null; - - var _isScrollbarVisible = false; - var _isRefreshIndicatorActive = false; -} - -class _Web { - _Web(this.state); - - void onInitState() { - _metadataTaskStateChangedListener.begin(); - _filePropertyUpdatedListener.begin(); - } - - void onDispose() { - _metadataTaskIconController.stop(); - _metadataTaskStateChangedListener.end(); - _filePropertyUpdatedListener.end(); - } - - Widget? buildContent(BuildContext context) { - if (_metadataTaskState != MetadataTaskState.idle) { - return _buildMetadataTaskHeader(context); - } else { - return null; - } - } - - void startMetadataTask(int missingMetadataCount) { - final c = KiwiContainer().resolve(); - MetadataTaskManager().addTask(MetadataTask( - c, state.widget.account, AccountPref.of(state.widget.account))); - _metadataTaskProcessTotalCount = missingMetadataCount; - } - - double getHeaderHeight() { - return _metadataTaskState == MetadataTaskState.idle - ? 0 - : _metadataTaskHeaderHeight; - } - - Widget _buildMetadataTaskHeader(BuildContext context) { - return SliverPersistentHeader( - pinned: true, - floating: false, - delegate: _MetadataTaskHeaderDelegate( - extent: _metadataTaskHeaderHeight, - builder: (context) => Container( - height: double.infinity, - color: Theme.of(context).scaffoldBackgroundColor, - alignment: AlignmentDirectional.centerStart, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row( - children: [ - if (_metadataTaskState == MetadataTaskState.prcoessing) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - _MetadataTaskLoadingIcon( - controller: _metadataTaskIconController, - ), - const SizedBox(width: 4), - Text( - L10n.global().metadataTaskProcessingNotification + - _getMetadataTaskProgressString(), - style: const TextStyle(fontSize: 12), - ), - ], - ) - else if (_metadataTaskState == MetadataTaskState.waitingForWifi) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.sync_problem, - size: 16, - ), - const SizedBox(width: 4), - Text( - L10n.global().metadataTaskPauseNoWiFiNotification, - style: const TextStyle(fontSize: 12), - ), - ], - ) - else if (_metadataTaskState == MetadataTaskState.lowBattery) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.sync_problem, - size: 16, - ), - const SizedBox(width: 4), - Text( - L10n.global().metadataTaskPauseLowBatteryNotification, - style: const TextStyle(fontSize: 12), - ), - ], - ), - Expanded( - child: Container(), - ), - Material( - type: MaterialType.transparency, - child: InkWell( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), - child: Text( - L10n.global().configButtonLabel, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontSize: 12, - ), - ), - ), - onTap: () { - Navigator.of(context).pushNamed(Settings.routeName); - }, - ), - ), - ], - ), - ), - ), - ), - ); - } - - void _onMetadataTaskStateChanged(MetadataTaskStateChangedEvent ev) { - if (ev.state == MetadataTaskState.idle) { - _metadataTaskProcessCount = 0; - } - if (ev.state != _metadataTaskState) { - // ignore: invalid_use_of_protected_member - state.setState(() { - _metadataTaskState = ev.state; - }); - } - } - - void _onFilePropertyUpdated(FilePropertyUpdatedEvent ev) { - if (!ev.hasAnyProperties([ - FilePropertyUpdatedEvent.propMetadata, - FilePropertyUpdatedEvent.propImageLocation, - ])) { - return; - } - // ignore: invalid_use_of_protected_member - state.setState(() { - ++_metadataTaskProcessCount; - }); - } - - String _getMetadataTaskProgressString() { - if (_metadataTaskProcessTotalCount == 0) { - return ""; - } - final clippedCount = - math.min(_metadataTaskProcessCount, _metadataTaskProcessTotalCount - 1); - return " ($clippedCount/$_metadataTaskProcessTotalCount)"; - } - - final _HomePhotosState state; - - late final _metadataTaskStateChangedListener = - AppEventListener( - _onMetadataTaskStateChanged); - var _metadataTaskState = MetadataTaskManager().state; - late final _filePropertyUpdatedListener = - AppEventListener(_onFilePropertyUpdated); - var _metadataTaskProcessCount = 0; - var _metadataTaskProcessTotalCount = 0; - late final _metadataTaskIconController = AnimationController( - upperBound: 2 * math.pi, - duration: const Duration(seconds: 10), - vsync: state, - )..repeat(); - - static const _metadataTaskHeaderHeight = 32.0; -} - -class _MetadataTaskHeaderDelegate extends SliverPersistentHeaderDelegate { - const _MetadataTaskHeaderDelegate({ - required this.extent, - required this.builder, - }); - - @override - build(BuildContext context, double shrinkOffset, bool overlapsContent) { - return builder(context); - } - - @override - get maxExtent => extent; - - @override - get minExtent => maxExtent; - - @override - shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => true; - - final double extent; - final Widget Function(BuildContext context) builder; -} - -class _MetadataTaskLoadingIcon extends AnimatedWidget { - const _MetadataTaskLoadingIcon({ - Key? key, - required AnimationController controller, - }) : super(key: key, listenable: controller); - - @override - build(BuildContext context) { - return Transform.rotate( - angle: -_progress.value, - child: const Icon( - Icons.sync, - size: 16, - ), - ); - } - - Animation get _progress => listenable as Animation; -} - -class _SmartCollectionList extends StatelessWidget { - const _SmartCollectionList({ - required this.account, - required this.collections, - }); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: _SmartAlbumItem.height, - child: ListView.separated( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 8), - itemCount: collections.length, - itemBuilder: (context, index) { - final c = collections[index]; - return _SmartAlbumItem( - account: account, - previewUrl: c.getCoverUrl( - k.photoThumbSize, - k.photoThumbSize, - isKeepAspectRatio: true, - ), - label: c.name, - onTap: () { - Navigator.of(context).pushNamed(CollectionBrowser.routeName, - arguments: CollectionBrowserArguments(c)); - }, - ); - }, - separatorBuilder: (context, index) => const SizedBox(width: 8), - ), - ); - } - - final Account account; - final List collections; -} - -class _SmartAlbumItem extends StatelessWidget { - static const width = 96.0; - static const height = width * 1.15; - - const _SmartAlbumItem({ - Key? key, - required this.account, - required this.previewUrl, - required this.label, - this.onTap, - }) : super(key: key); - - @override - build(BuildContext context) { - return Align( - alignment: AlignmentDirectional.topStart, - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: SizedBox( - width: width, - height: height, - child: Stack( - fit: StackFit.expand, - children: [ - PhotoListImage( - account: account, - previewUrl: previewUrl, - padding: const EdgeInsets.all(0), - ), - Positioned.fill( - child: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.center, - end: Alignment.bottomCenter, - colors: [Colors.transparent, Colors.black87], - ), - ), - ), - ), - Positioned.fill( - child: Align( - alignment: AlignmentDirectional.bottomStart, - child: Padding( - padding: const EdgeInsets.all(4), - child: Text( - label, - style: Theme.of(context).textTheme.labelLarge!.copyWith( - color: Theme.of(context).onDarkSurface, - ), - ), - ), - ), - ), - if (onTap != null) - Positioned.fill( - child: Material( - type: MaterialType.transparency, - child: InkWell( - onTap: onTap, - ), - ), - ), - ], - ), - ), - ), - ); - } - - final Account account; - final String? previewUrl; - final String label; - final VoidCallback? onTap; -} - -enum _SelectionMenuOption { - archive, - delete, - download, -} - -class _VisibleItem implements Comparable<_VisibleItem> { - const _VisibleItem(this.index, this.item); - - @override - operator ==(Object other) => other is _VisibleItem && other.index == index; - - @override - compareTo(_VisibleItem other) => index.compareTo(other.index); - - @override - get hashCode => index.hashCode; - - final int index; - final SelectableItem item; -} - -class _InitialLoadingProgress extends StatelessWidget { - const _InitialLoadingProgress({ - required this.progressBloc, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - bloc: progressBloc, - buildWhen: (previous, current) => previous != current, - builder: (context, state) { - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 56, 16, 0), - child: Center( - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: Theme.of(context).widthLimitedContentMaxWidth, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - L10n.global().initialSyncMessage, - style: Theme.of(context).textTheme.bodyLarge, - ), - const SizedBox(height: 8), - LinearProgressIndicator( - value: state.progress == 0 ? null : state.progress, - ), - const SizedBox(height: 8), - Text( - state.text ?? "", - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ), - ), - ), - ), - ); - }, - ); - } - - final ProgressBloc progressBloc; -} - -class _ScrollLabel extends StatelessWidget { - const _ScrollLabel({ - required this.child, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: DefaultTextStyle( - style: theme.textTheme.titleMedium! - .copyWith(color: theme.colorScheme.onInverseSurface), - child: child, - ), - ); - } - - final Widget child; -} diff --git a/app/lib/widget/home_photos.g.dart b/app/lib/widget/home_photos.g.dart deleted file mode 100644 index 9c22ac33..00000000 --- a/app/lib/widget/home_photos.g.dart +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'home_photos.dart'; - -// ************************************************************************** -// NpLogGenerator -// ************************************************************************** - -extension _$_HomePhotosStateNpLog on _HomePhotosState { - // ignore: unused_element - Logger get _log => log; - - static final log = Logger("widget.home_photos._HomePhotosState"); -} From e77059225b379b1074a65565a798b5eec38b5e6c Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Wed, 7 Feb 2024 01:01:45 +0800 Subject: [PATCH 31/42] Fix removing files does not update collections --- app/lib/controller/account_controller.dart | 1 + .../collection_items_controller.dart | 50 ++++++++++--------- .../controller/collections_controller.dart | 4 ++ app/lib/use_case/remove.dart | 1 + app/lib/widget/collection_browser/bloc.dart | 1 + 5 files changed, 34 insertions(+), 23 deletions(-) diff --git a/app/lib/controller/account_controller.dart b/app/lib/controller/account_controller.dart index cdd66061..7044c68f 100644 --- a/app/lib/controller/account_controller.dart +++ b/app/lib/controller/account_controller.dart @@ -55,6 +55,7 @@ class AccountController { CollectionsController get collectionsController => _collectionsController ??= CollectionsController( KiwiContainer().resolve(), + filesController: filesController, account: _account!, serverController: serverController, ); diff --git a/app/lib/controller/collection_items_controller.dart b/app/lib/controller/collection_items_controller.dart index bf87bcd2..72b503e6 100644 --- a/app/lib/controller/collection_items_controller.dart +++ b/app/lib/controller/collection_items_controller.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:mutex/mutex.dart'; import 'package:nc_photos/account.dart'; +import 'package:nc_photos/controller/files_controller.dart'; import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/collection.dart'; @@ -13,7 +14,6 @@ import 'package:nc_photos/entity/collection/adapter.dart'; import 'package:nc_photos/entity/collection_item.dart'; import 'package:nc_photos/entity/collection_item/new_item.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; -import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/exception_event.dart'; import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/rx_extension.dart'; @@ -46,18 +46,21 @@ class CollectionItemStreamData { class CollectionItemsController { CollectionItemsController( this._c, { + required this.filesController, required this.account, required this.collection, required this.onCollectionUpdated, }) { - _fileRemovedEventListener.begin(); + _subscriptions.add(filesController.stream.listen(_onFilesEvent)); } /// Dispose this controller and release all internal resources /// /// MUST be called void dispose() { - _fileRemovedEventListener.end(); + for (final s in _subscriptions) { + s.cancel(); + } _dataStreamController.close(); } @@ -309,26 +312,29 @@ class CollectionItemsController { } } - void _onFileRemovedEvent(FileRemovedEvent ev) { - // if (account != ev.account) { - // return; - // } - // final newItems = _dataStreamController.value.items.where((e) { - // if (e is CollectionFileItem) { - // return !e.file.compareServerIdentity(ev.file); - // } else { - // return true; - // } - // }).toList(); - // if (newItems.length != _dataStreamController.value.items.length) { - // // item of interest - // _dataStreamController.addWithValue((value) => value.copyWith( - // items: newItems, - // )); - // } + Future _onFilesEvent(FilesStreamEvent ev) async { + if (!_isDataStreamInited || ev.hasNext || collection.isDynamicCollection) { + // clean up only make sense for static albums + return; + } + await _mutex.protect(() async { + final newItems = _dataStreamController.value.items.where((e) { + if (e is CollectionFileItem) { + return ev.dataMap.containsKey(e.file.fdId); + } else { + return true; + } + }).toList(); + if (newItems.length != _dataStreamController.value.items.length) { + _dataStreamController.addWithValue((value) => value.copyWith( + items: newItems, + )); + } + }); } final DiContainer _c; + final FilesController filesController; final Account account; Collection collection; ValueChanged onCollectionUpdated; @@ -341,8 +347,6 @@ class CollectionItemsController { ), ); - late final _fileRemovedEventListener = - AppEventListener(_onFileRemovedEvent); - final _mutex = Mutex(); + final _subscriptions = []; } diff --git a/app/lib/controller/collections_controller.dart b/app/lib/controller/collections_controller.dart index 1d6f7d15..5b6b56a2 100644 --- a/app/lib/controller/collections_controller.dart +++ b/app/lib/controller/collections_controller.dart @@ -6,6 +6,7 @@ import 'package:logging/logging.dart'; import 'package:mutex/mutex.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/controller/collection_items_controller.dart'; +import 'package:nc_photos/controller/files_controller.dart'; import 'package:nc_photos/controller/server_controller.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/collection.dart'; @@ -64,6 +65,7 @@ class CollectionStreamEvent { class CollectionsController { CollectionsController( this._c, { + required this.filesController, required this.account, required this.serverController, }); @@ -309,6 +311,7 @@ class CollectionsController { final k = _CollectionKey(c); _itemControllers[k] ??= CollectionItemsController( _c, + filesController: filesController, account: account, collection: k.collection, onCollectionUpdated: _updateCollection, @@ -347,6 +350,7 @@ class CollectionsController { } final DiContainer _c; + final FilesController filesController; final Account account; final ServerController serverController; diff --git a/app/lib/use_case/remove.dart b/app/lib/use_case/remove.dart index df8cde7d..e226b6b2 100644 --- a/app/lib/use_case/remove.dart +++ b/app/lib/use_case/remove.dart @@ -56,6 +56,7 @@ class Remove { return count; } + // TODO: move to CollectionsController Future _cleanUpAlbums( Account account, List removes) async { final albums = await ListAlbum(_c)(account).whereType().toList(); diff --git a/app/lib/widget/collection_browser/bloc.dart b/app/lib/widget/collection_browser/bloc.dart index 64445168..c9b76c73 100644 --- a/app/lib/widget/collection_browser/bloc.dart +++ b/app/lib/widget/collection_browser/bloc.dart @@ -425,6 +425,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { ); itemsController = CollectionItemsController( _c, + filesController: filesController, account: account, collection: collection, onCollectionUpdated: (_) {}, From 0521e2b5c314df1d85e37a8f6121fb63211081ae Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Wed, 7 Feb 2024 01:08:28 +0800 Subject: [PATCH 32/42] Fix item count in HomeCollections does not consider the runtime count from item controller --- .../collection_items_controller.dart | 10 ++++++ app/lib/widget/home_collections.dart | 14 +++++--- app/lib/widget/home_collections.g.dart | 12 ++++++- app/lib/widget/home_collections/bloc.dart | 36 ++++++++++++++++--- .../widget/home_collections/state_event.dart | 14 ++++++++ app/lib/widget/home_collections/type.dart | 14 +++++--- app/lib/widget/home_collections/view.dart | 5 ++- 7 files changed, 89 insertions(+), 16 deletions(-) diff --git a/app/lib/controller/collection_items_controller.dart b/app/lib/controller/collection_items_controller.dart index 72b503e6..e8e20913 100644 --- a/app/lib/controller/collection_items_controller.dart +++ b/app/lib/controller/collection_items_controller.dart @@ -51,6 +51,13 @@ class CollectionItemsController { required this.collection, required this.onCollectionUpdated, }) { + _countStreamController = BehaviorSubject.seeded(collection.count); + _subscriptions.add(_dataStreamController.stream.listen((event) { + if (!event.hasNext) { + _countStreamController.add(event.items.length); + } + })); + _subscriptions.add(filesController.stream.listen(_onFilesEvent)); } @@ -82,6 +89,8 @@ class CollectionItemsController { /// Peek the stream and return the current value CollectionItemStreamData peekStream() => _dataStreamController.stream.value; + ValueStream get countStream => _countStreamController.stream; + /// Add list of [files] to [collection] Future addFiles(List files) async { final isInited = _isDataStreamInited; @@ -346,6 +355,7 @@ class CollectionItemsController { hasNext: true, ), ); + late final BehaviorSubject _countStreamController; final _mutex = Mutex(); final _subscriptions = []; diff --git a/app/lib/widget/home_collections.dart b/app/lib/widget/home_collections.dart index 8d65efb5..e817d9e1 100644 --- a/app/lib/widget/home_collections.dart +++ b/app/lib/widget/home_collections.dart @@ -191,9 +191,14 @@ class _WrappedHomeCollectionsState extends State<_WrappedHomeCollections> items: state.transformedItems, itemBuilder: (_, __, metadata) { final item = metadata as _Item; - return _ItemView( - account: _bloc.account, - item: item, + return _BlocSelector( + selector: (state) => + state.itemCounts[item.collection.id], + builder: (context, itemCount) => _ItemView( + account: _bloc.account, + item: item, + collectionItemCountOverride: itemCount, + ), ); }, staggeredTileBuilder: (_, __) => @@ -267,7 +272,8 @@ class _WrappedHomeCollectionsState extends State<_WrappedHomeCollections> typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; typedef _BlocListener = BlocListener<_Bloc, _State>; -// typedef _BlocSelector = BlocSelector<_Bloc, _State, T>; +// typedef _BlocListenerT = BlocListenerT<_Bloc, _State, T>; +typedef _BlocSelector = BlocSelector<_Bloc, _State, T>; extension on BuildContext { _Bloc get bloc => read<_Bloc>(); diff --git a/app/lib/widget/home_collections.g.dart b/app/lib/widget/home_collections.g.dart index 95416794..c002b482 100644 --- a/app/lib/widget/home_collections.g.dart +++ b/app/lib/widget/home_collections.g.dart @@ -19,6 +19,7 @@ abstract class $_StateCopyWithWorker { bool? isLoading, List<_Item>? transformedItems, Set<_Item>? selectedItems, + Map? itemCounts, ExceptionEvent? error, ExceptionEvent? removeError}); } @@ -33,6 +34,7 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { dynamic isLoading, dynamic transformedItems, dynamic selectedItems, + dynamic itemCounts, dynamic error = copyWithNull, dynamic removeError = copyWithNull}) { return _State( @@ -42,6 +44,7 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { transformedItems: transformedItems as List<_Item>? ?? that.transformedItems, selectedItems: selectedItems as Set<_Item>? ?? that.selectedItems, + itemCounts: itemCounts as Map? ?? that.itemCounts, error: error == copyWithNull ? that.error : error as ExceptionEvent?, removeError: removeError == copyWithNull ? that.removeError @@ -96,7 +99,7 @@ extension _$_ItemNpLog on _Item { extension _$_StateToString on _State { String _$toString() { // ignore: unnecessary_string_interpolations - return "_State {collections: [length: ${collections.length}], sort: ${sort.name}, isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, error: $error, removeError: $removeError}"; + return "_State {collections: [length: ${collections.length}], sort: ${sort.name}, isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, itemCounts: $itemCounts, error: $error, removeError: $removeError}"; } } @@ -149,6 +152,13 @@ extension _$_SetCollectionSortToString on _SetCollectionSort { } } +extension _$_SetItemCountToString on _SetItemCount { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetItemCount {collection: $collection, value: $value}"; + } +} + extension _$_SetErrorToString on _SetError { String _$toString() { // ignore: unnecessary_string_interpolations diff --git a/app/lib/widget/home_collections/bloc.dart b/app/lib/widget/home_collections/bloc.dart index aba56295..a85e4e6f 100644 --- a/app/lib/widget/home_collections/bloc.dart +++ b/app/lib/widget/home_collections/bloc.dart @@ -16,18 +16,36 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { on<_UpdateCollectionSort>(_onUpdateCollectionSort); on<_SetCollectionSort>(_onSetCollectionSort); + on<_SetItemCount>(_onSetItemCount); on<_SetError>(_onSetError); - _homeAlbumsSortSubscription = - prefController.homeAlbumsSort.distinct().listen((event) { + _subscriptions.add(prefController.homeAlbumsSort.distinct().listen((event) { add(_UpdateCollectionSort(collection_util.CollectionSort.values[event])); - }); + })); + _subscriptions.add(controller.stream.listen((event) { + for (final s in _itemSubscriptions) { + s.cancel(); + } + _itemSubscriptions.clear(); + for (final d in event.data) { + _itemSubscriptions.add(d.controller.countStream.listen((event) { + if (event != null) { + add(_SetItemCount(d.collection, event)); + } + })); + } + })); } @override Future close() { - _homeAlbumsSortSubscription?.cancel(); + for (final s in _itemSubscriptions) { + s.cancel(); + } + for (final s in _subscriptions) { + s.cancel(); + } return super.close(); } @@ -105,6 +123,13 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { prefController.setHomeAlbumsSort(ev.sort.index); } + void _onSetItemCount(_SetItemCount ev, Emitter<_State> emit) { + _log.info(ev); + final next = Map.of(state.itemCounts); + next[ev.collection.id] = ev.value; + emit(state.copyWith(itemCounts: next)); + } + void _onSetError(_SetError ev, Emitter<_State> emit) { _log.info(ev); emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); @@ -122,6 +147,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { final CollectionsController controller; final PrefController prefController; - StreamSubscription? _homeAlbumsSortSubscription; + final _subscriptions = []; + final _itemSubscriptions = []; var _isHandlingError = false; } diff --git a/app/lib/widget/home_collections/state_event.dart b/app/lib/widget/home_collections/state_event.dart index 08f22632..612c8227 100644 --- a/app/lib/widget/home_collections/state_event.dart +++ b/app/lib/widget/home_collections/state_event.dart @@ -9,6 +9,7 @@ class _State { required this.isLoading, required this.transformedItems, required this.selectedItems, + required this.itemCounts, this.error, required this.removeError, }); @@ -20,6 +21,7 @@ class _State { isLoading: false, transformedItems: [], selectedItems: {}, + itemCounts: {}, removeError: null, ); } @@ -32,6 +34,7 @@ class _State { final bool isLoading; final List<_Item> transformedItems; final Set<_Item> selectedItems; + final Map itemCounts; final ExceptionEvent? error; final ExceptionEvent? removeError; @@ -112,6 +115,17 @@ class _SetCollectionSort implements _Event { final collection_util.CollectionSort sort; } +@toString +class _SetItemCount implements _Event { + const _SetItemCount(this.collection, this.value); + + @override + String toString() => _$toString(); + + final Collection collection; + final int value; +} + @toString class _SetError implements _Event { const _SetError(this.error, [this.stackTrace]); diff --git a/app/lib/widget/home_collections/type.dart b/app/lib/widget/home_collections/type.dart index 20737baa..cc5e91a4 100644 --- a/app/lib/widget/home_collections/type.dart +++ b/app/lib/widget/home_collections/type.dart @@ -11,9 +11,6 @@ enum _ItemType { class _Item implements SelectableItemMetadata { _Item(this.collection) : isShared = collection.shares.isNotEmpty || !collection.isOwned { - if (collection.count != null) { - _subtitle = L10n.global().albumSize(collection.count!); - } try { _coverUrl = collection.getCoverUrl(k.coverSize, k.coverSize); } catch (e, stackTrace) { @@ -35,7 +32,15 @@ class _Item implements SelectableItemMetadata { String get name => collection.name; - String? get subtitle => _subtitle; + String? getSubtitle({ + int? itemCountOverride, + }) { + if (collection.count != null) { + return L10n.global().albumSize(itemCountOverride ?? collection.count!); + } else { + return null; + } + } String? get coverUrl => _coverUrl; @@ -64,7 +69,6 @@ class _Item implements SelectableItemMetadata { final Collection collection; final bool isShared; - String? _subtitle; String? _coverUrl; late _ItemType _itemType; } diff --git a/app/lib/widget/home_collections/view.dart b/app/lib/widget/home_collections/view.dart index 13f9d362..bc4caf5d 100644 --- a/app/lib/widget/home_collections/view.dart +++ b/app/lib/widget/home_collections/view.dart @@ -129,6 +129,7 @@ class _ItemView extends StatelessWidget { const _ItemView({ required this.account, required this.item, + this.collectionItemCountOverride, }); @override @@ -152,7 +153,8 @@ class _ItemView extends StatelessWidget { if (item.isShared) { subtitle = "${L10n.global().albumSharedLabel} | "; } - subtitle += item.subtitle ?? ""; + subtitle += + item.getSubtitle(itemCountOverride: collectionItemCountOverride) ?? ""; return CollectionGridItem( cover: _CollectionCover( account: account, @@ -166,6 +168,7 @@ class _ItemView extends StatelessWidget { final Account account; final _Item item; + final int? collectionItemCountOverride; } class _CollectionCover extends StatelessWidget { From 3c35a383c1ddb04f332bb6b475dbe4eabfca4c95 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Wed, 7 Feb 2024 00:54:39 +0800 Subject: [PATCH 33/42] Remove obsolete files --- app/lib/bloc/progress.dart | 57 ------------------------------------ app/lib/bloc/progress.g.dart | 32 -------------------- 2 files changed, 89 deletions(-) delete mode 100644 app/lib/bloc/progress.dart delete mode 100644 app/lib/bloc/progress.g.dart diff --git a/app/lib/bloc/progress.dart b/app/lib/bloc/progress.dart deleted file mode 100644 index c57422b9..00000000 --- a/app/lib/bloc/progress.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:logging/logging.dart'; -import 'package:np_codegen/np_codegen.dart'; -import 'package:to_string/to_string.dart'; - -part 'progress.g.dart'; - -abstract class ProgressBlocEvent { - const ProgressBlocEvent(); -} - -@toString -class ProgressBlocUpdate extends ProgressBlocEvent { - const ProgressBlocUpdate(this.progress, [this.text]); - - @override - String toString() => _$toString(); - - final double progress; - final String? text; -} - -@toString -class ProgressBlocState with EquatableMixin { - const ProgressBlocState(this.progress, this.text); - - @override - String toString() => _$toString(); - - @override - List get props => [progress, text]; - - final double progress; - final String? text; -} - -/// A generic bloc to bubble progress update for some events -@npLog -class ProgressBloc extends Bloc { - ProgressBloc() : super(const ProgressBlocState(0, null)) { - on(_onEvent); - } - - Future _onEvent( - ProgressBlocEvent ev, Emitter emit) async { - _log.info("[_onEvent] $ev"); - if (ev is ProgressBlocUpdate) { - await _onEventUpdate(ev, emit); - } - } - - Future _onEventUpdate( - ProgressBlocUpdate ev, Emitter emit) async { - emit(ProgressBlocState(ev.progress, ev.text)); - } -} diff --git a/app/lib/bloc/progress.g.dart b/app/lib/bloc/progress.g.dart deleted file mode 100644 index c9e133f6..00000000 --- a/app/lib/bloc/progress.g.dart +++ /dev/null @@ -1,32 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'progress.dart'; - -// ************************************************************************** -// NpLogGenerator -// ************************************************************************** - -extension _$ProgressBlocNpLog on ProgressBloc { - // ignore: unused_element - Logger get _log => log; - - static final log = Logger("bloc.progress.ProgressBloc"); -} - -// ************************************************************************** -// ToStringGenerator -// ************************************************************************** - -extension _$ProgressBlocUpdateToString on ProgressBlocUpdate { - String _$toString() { - // ignore: unnecessary_string_interpolations - return "ProgressBlocUpdate {progress: ${progress.toStringAsFixed(3)}, text: $text}"; - } -} - -extension _$ProgressBlocStateToString on ProgressBlocState { - String _$toString() { - // ignore: unnecessary_string_interpolations - return "ProgressBlocState {progress: ${progress.toStringAsFixed(3)}, text: $text}"; - } -} From 91caf39184afd919eeb4ca19f5191f0e17bbe085 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Tue, 13 Feb 2024 19:22:58 +0800 Subject: [PATCH 34/42] Fix db file data source returning file instead of fd from share folder --- app/lib/entity/file/data_source2.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/lib/entity/file/data_source2.dart b/app/lib/entity/file/data_source2.dart index ec6b0df6..bcb805dc 100644 --- a/app/lib/entity/file/data_source2.dart +++ b/app/lib/entity/file/data_source2.dart @@ -208,8 +208,10 @@ class FileNpDbDataSource implements FileDataSource2 { account: account.toDb(), dir: File(path: shareDirPath).toDbKey(), ); - results.addAll(shareDirResults.map((e) => DbFileConverter.fromDb( - account.userId.toCaseInsensitiveString(), e))); + results.addAll(shareDirResults.where((f) => f.isCollection != true).map( + (e) => DbFileConverter.fromDb( + account.userId.toCaseInsensitiveString(), e) + .toDescriptor())); } on DbNotFoundException catch (_) { // normal when there's no cache } catch (e, stackTrace) { From 64b281289b6f510f39fb23b75c65b0893f6ab808 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Tue, 13 Feb 2024 19:24:16 +0800 Subject: [PATCH 35/42] Improve doc --- app/lib/entity/file/repo.dart | 4 ++++ app/lib/widget/collection_browser/bloc.dart | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/lib/entity/file/repo.dart b/app/lib/entity/file/repo.dart index 6ccdc9c1..65db5298 100644 --- a/app/lib/entity/file/repo.dart +++ b/app/lib/entity/file/repo.dart @@ -10,6 +10,8 @@ part 'repo.g.dart'; abstract class FileRepo2 { /// Query all files belonging to [account] /// + /// Returned files are sorted by time in descending order + /// /// Normally the stream should complete with only a single event, but some /// implementation might want to return multiple set of values, say one set of /// cached value and later another set of updated value from a remote source. @@ -127,6 +129,8 @@ class CachedFileRepo implements FileRepo2 { abstract class FileDataSource2 { /// Query all files belonging to [account] + /// + /// Returned files are sorted by time in descending order Stream> getFileDescriptors( Account account, String shareDirPath); diff --git a/app/lib/widget/collection_browser/bloc.dart b/app/lib/widget/collection_browser/bloc.dart index c9b76c73..d51da1da 100644 --- a/app/lib/widget/collection_browser/bloc.dart +++ b/app/lib/widget/collection_browser/bloc.dart @@ -501,18 +501,24 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { ); } + /// Remove file items not recognized by the app List _filterItems( List rawItems, Set? whitelist) { if (whitelist == null) { return rawItems; } - return rawItems.where((e) { + final results = rawItems.where((e) { if (e is CollectionFileItem) { return whitelist.contains(e.file.fdId); } else { return true; } }).toList(); + if (rawItems.length != results.length) { + _log.fine( + "[_filterItems] ${rawItems.length - results.length} items filtered out"); + } + return results; } String? _getCoverUrlByItems() { From 59a5741c5027f6ca2676051ffc6fb6a4fc0cff67 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Tue, 13 Feb 2024 19:29:13 +0800 Subject: [PATCH 36/42] Update test cases --- ...ource_test.dart => data_source2_test.dart} | 162 ++++++++++++++---- app/test/mock_type.dart | 68 ++++++++ app/test/test_util.dart | 4 +- app/test/use_case/remove_album_test.dart | 19 +- app/test/use_case/remove_test.dart | 7 +- 5 files changed, 223 insertions(+), 37 deletions(-) rename app/test/entity/file/{data_source_test.dart => data_source2_test.dart} (66%) diff --git a/app/test/entity/file/data_source_test.dart b/app/test/entity/file/data_source2_test.dart similarity index 66% rename from app/test/entity/file/data_source_test.dart rename to app/test/entity/file/data_source2_test.dart index 4c3cab2f..8f47911f 100644 --- a/app/test/entity/file/data_source_test.dart +++ b/app/test/entity/file/data_source2_test.dart @@ -1,7 +1,8 @@ import 'package:nc_photos/db/entity_converter.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/file.dart'; -import 'package:nc_photos/entity/file/data_source.dart'; +import 'package:nc_photos/entity/file/data_source2.dart'; +import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:np_collection/np_collection.dart'; import 'package:np_common/or_null.dart'; import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat; @@ -10,9 +11,13 @@ import 'package:test/test.dart'; import '../../test_util.dart' as util; void main() { - group("FileSqliteDbDataSource", () { - test("list", _list); - test("listSingle", _listSingle); + group("FileNpDbDataSource", () { + group("getFileDescriptors", () { + test("normal", _getFileDescriptors); + test("multiple account", _getFileDescriptorsMultipleAccount); + test("share folder", _getFileDescriptorsShareFolder); + test("extra share folder", _getFileDescriptorsExtraShareFolder); + }); group("remove", () { test("file", _removeFile); test("empty dir", _removeEmptyDir); @@ -28,14 +33,11 @@ void main() { }); } -/// List a dir +/// Return files of an account /// /// Files: admin/test1.jpg, admin/test/test2.jpg -/// List: admin -/// Expect: admin/test1.jpg -/// List: admin/test -/// Expect: admin/test/test2.jpg -Future _list() async { +/// Expect: admin/test1.jpg, admin/test/test2.jpg +Future _getFileDescriptors() async { final account = util.buildAccount(); final files = (util.FilesBuilder() ..addDir("admin") @@ -55,17 +57,77 @@ Future _list() async { await util.insertDirRelation(c.sqliteDb, account, files[2], [files[3]]); }); - final src = FileSqliteDbDataSource(c); - expect(await src.list(account, files[0]), files.slice(0, 3)); - expect(await src.list(account, files[2]), files.slice(2, 4)); + final src = FileNpDbDataSource(c.npDb); + expect( + await src + .getFileDescriptors(account, file_util.unstripPath(account, "")) + .last, + [files[3].toDescriptor(), files[1].toDescriptor()], + ); } -/// List a single dir +/// Return files of an account /// -/// Expect: throw UnimplementedError -Future _listSingle() async { +/// Files: admin/test1.jpg, admin/test/test2.jpg, user1/test3.jpg +/// Expect: admin/test1.jpg, admin/test/test2.jpg +Future _getFileDescriptorsMultipleAccount() async { final account = util.buildAccount(); - final files = (util.FilesBuilder()..addDir("admin")).build(); + final user1Account = util.buildAccount(userId: "user1"); + final files = (util.FilesBuilder() + ..addDir("admin") + ..addJpeg("admin/test1.jpg") + ..addDir("admin/test") + ..addJpeg("admin/test/test2.jpg")) + .build(); + final user1Files = (util.FilesBuilder(initialFileId: files.length) + ..addDir("user1", ownerId: "user1") + ..addJpeg("user1/test3.jpg")) + .build(); + final c = DiContainer( + npDb: util.buildTestDb(), + ); + addTearDown(() => c.sqliteDb.close()); + await c.sqliteDb.transaction(() async { + await c.sqliteDb.insertAccounts([account.toDb()]); + await c.sqliteDb.insertAccounts([user1Account.toDb()]); + await util.insertFiles(c.sqliteDb, account, files); + await util.insertDirRelation( + c.sqliteDb, account, files[0], files.slice(1, 3)); + await util.insertDirRelation(c.sqliteDb, account, files[2], [files[3]]); + await util.insertFiles(c.sqliteDb, user1Account, user1Files); + await util.insertDirRelation( + c.sqliteDb, user1Account, user1Files[0], [user1Files[1]]); + }); + + final src = FileNpDbDataSource(c.npDb); + expect( + await src + .getFileDescriptors(account, file_util.unstripPath(account, "")) + .last, + [files[3].toDescriptor(), files[1].toDescriptor()], + ); + expect( + await src + .getFileDescriptors( + user1Account, file_util.unstripPath(user1Account, "")) + .last, + [user1Files[1].toDescriptor()], + ); +} + +/// Return files of an account +/// +/// Files: admin/test1/test1.jpg, admin/test2/test2.jpg +/// Expect: admin/test1/test1.jpg +Future _getFileDescriptorsShareFolder() async { + final account = util.buildAccount(roots: ["test1"]); + final files = (util.FilesBuilder() + ..addDir("admin") + ..addDir("admin/test1") + ..addJpeg("admin/test1/test1.jpg") + ..addDir("admin/test2") + ..addJpeg("admin/test2/test2.jpg")) + .build(); final c = DiContainer( npDb: util.buildTestDb(), ); @@ -73,12 +135,54 @@ Future _listSingle() async { await c.sqliteDb.transaction(() async { await c.sqliteDb.insertAccounts([account.toDb()]); await util.insertFiles(c.sqliteDb, account, files); - await util.insertDirRelation(c.sqliteDb, account, files[0], const []); + await util + .insertDirRelation(c.sqliteDb, account, files[0], [files[1], files[3]]); + await util.insertDirRelation(c.sqliteDb, account, files[1], [files[2]]); + await util.insertDirRelation(c.sqliteDb, account, files[3], [files[4]]); }); - final src = FileSqliteDbDataSource(c); - expect(() async => await src.listSingle(account, files[0]), - throwsUnimplementedError); + final src = FileNpDbDataSource(c.npDb); + expect( + await src + .getFileDescriptors(account, file_util.unstripPath(account, "test1")) + .last, + [files[2].toDescriptor()], + ); +} + +/// Return files of an account +/// +/// Files: admin/test1/test1.jpg, admin/test2/test2.jpg +/// Expect: admin/test1/test1.jpg, admin/test2/test2.jpg +Future _getFileDescriptorsExtraShareFolder() async { + final account = util.buildAccount(roots: ["test1"]); + final files = (util.FilesBuilder() + ..addDir("admin") + ..addDir("admin/test1") + ..addJpeg("admin/test1/test1.jpg") + ..addDir("admin/test2") + ..addJpeg("admin/test2/test2.jpg")) + .build(); + final c = DiContainer( + npDb: util.buildTestDb(), + ); + addTearDown(() => c.sqliteDb.close()); + await c.sqliteDb.transaction(() async { + await c.sqliteDb.insertAccounts([account.toDb()]); + await util.insertFiles(c.sqliteDb, account, files); + await util + .insertDirRelation(c.sqliteDb, account, files[0], [files[1], files[3]]); + await util.insertDirRelation(c.sqliteDb, account, files[1], [files[2]]); + await util.insertDirRelation(c.sqliteDb, account, files[3], [files[4]]); + }); + + final src = FileNpDbDataSource(c.npDb); + expect( + await src + .getFileDescriptors(account, file_util.unstripPath(account, "test2")) + .last, + [files[2].toDescriptor(), files[4].toDescriptor()], + ); } /// Remove a file @@ -100,7 +204,7 @@ Future _removeFile() async { await util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]); }); - final src = FileSqliteDbDataSource(c); + final src = FileNpDbDataSource(c.npDb); await src.remove(account, files[1]); expect( await util.listSqliteDbFiles(c.sqliteDb), @@ -128,7 +232,7 @@ Future _removeEmptyDir() async { await util.insertDirRelation(c.sqliteDb, account, files[1], const []); }); - final src = FileSqliteDbDataSource(c); + final src = FileNpDbDataSource(c.npDb); await src.remove(account, files[1]); // parent dir is not updated, parent dir is only updated when syncing with // remote @@ -161,7 +265,7 @@ Future _removeDir() async { await util.insertDirRelation(c.sqliteDb, account, files[1], [files[2]]); }); - final src = FileSqliteDbDataSource(c); + final src = FileNpDbDataSource(c.npDb); await src.remove(account, files[1]); expect( await util.listSqliteDbFiles(c.sqliteDb), @@ -192,7 +296,7 @@ Future _removeDirWithSubDir() async { await util.insertDirRelation(c.sqliteDb, account, files[2], [files[3]]); }); - final src = FileSqliteDbDataSource(c); + final src = FileNpDbDataSource(c.npDb); await src.remove(account, files[1]); expect( await util.listSqliteDbDirs(c.sqliteDb), @@ -225,7 +329,7 @@ Future _updateFileProperty() async { await util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]); }); - final src = FileSqliteDbDataSource(c); + final src = FileNpDbDataSource(c.npDb); await src.updateProperty( account, files[1], @@ -267,7 +371,7 @@ Future _updateMetadata() async { await util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]); }); - final src = FileSqliteDbDataSource(c); + final src = FileNpDbDataSource(c.npDb); await src.updateProperty( account, files[1], @@ -309,7 +413,7 @@ Future _updateAddMetadata() async { await util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]); }); - final src = FileSqliteDbDataSource(c); + final src = FileNpDbDataSource(c.npDb); await src.updateProperty( account, files[1], @@ -355,7 +459,7 @@ Future _updateDeleteMetadata() async { await util.insertDirRelation(c.sqliteDb, account, files[0], [files[1]]); }); - final src = FileSqliteDbDataSource(c); + final src = FileNpDbDataSource(c.npDb); await src.updateProperty( account, files[1], diff --git a/app/test/mock_type.dart b/app/test/mock_type.dart index 48611d1c..b7a94953 100644 --- a/app/test/mock_type.dart +++ b/app/test/mock_type.dart @@ -12,6 +12,7 @@ import 'package:nc_photos/entity/face_recognition_person/repo.dart'; import 'package:nc_photos/entity/favorite.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file/data_source.dart'; +import 'package:nc_photos/entity/file/repo.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/share.dart'; @@ -303,6 +304,72 @@ class MockFileMemoryRepo extends FileRepo { } } +class MockFileDataSource2 implements FileDataSource2 { + @override + Stream> getFileDescriptors( + Account account, String shareDirPath) { + throw UnimplementedError(); + } + + @override + Future remove(Account account, FileDescriptor f) { + throw UnimplementedError(); + } + + @override + Future updateProperty( + Account account, + FileDescriptor f, { + OrNull? metadata, + OrNull? isArchived, + OrNull? overrideDateTime, + bool? favorite, + OrNull? location, + }) { + throw UnimplementedError(); + } +} + +class MockFileMemoryDataSource2 extends MockFileDataSource2 { + MockFileMemoryDataSource2([ + List initialData = const [], + ]) : files = initialData.map((f) => f.copyWith()).toList(); + + @override + Stream> getFileDescriptors( + Account account, String shareDirPath) async* { + yield files.where((f) { + if (account.roots.any((r) => file_util.isOrUnderDirPath( + f.fdPath, file_util.unstripPath(account, r)))) { + return true; + } else if (file_util.isOrUnderDirPath( + f.fdPath, file_util.unstripPath(account, shareDirPath))) { + return true; + } else { + return false; + } + }).toList(); + } + + @override + Future remove(Account account, FileDescriptor file) async { + files.removeWhere((f) => f.compareServerIdentity(file)); + } + + final List files; +} + +/// [FileRepo2] mock that support some ops with an internal List +class MockFileMemoryRepo2 extends BasicFileRepo { + MockFileMemoryRepo2([ + List initialData = const [], + ]) : super(MockFileMemoryDataSource2(initialData)); + + List get files { + return (dataSrc as MockFileMemoryDataSource2).files; + } +} + /// Mock of [ShareRepo] where all methods will throw UnimplementedError class MockShareRepo implements ShareRepo { @override @@ -489,6 +556,7 @@ class MockFaceRecognitionPersonMemoryRepo extension MockDiContainerExtension on DiContainer { MockAlbumMemoryRepo get albumMemoryRepo => albumRepo as MockAlbumMemoryRepo; MockFileMemoryRepo get fileMemoryRepo => fileRepo as MockFileMemoryRepo; + MockFileMemoryRepo2 get fileMemoryRepo2 => fileRepo2 as MockFileMemoryRepo2; MockShareMemoryRepo get shareMemoryRepo => shareRepo as MockShareMemoryRepo; MockShareeMemoryRepo get shareeMemoryRepo => shareeRepo as MockShareeMemoryRepo; diff --git a/app/test/test_util.dart b/app/test/test_util.dart index 9b049a69..e01db815 100644 --- a/app/test/test_util.dart +++ b/app/test/test_util.dart @@ -317,7 +317,7 @@ void initLog() { } Account buildAccount({ - String id = "123456-000000", + String? id, String scheme = "http", String address = "example.com", String userId = "admin", @@ -326,7 +326,7 @@ Account buildAccount({ List roots = const [""], }) => Account( - id: id, + id: id ?? "$userId-000000", scheme: scheme, address: address, userId: userId.toCi(), diff --git a/app/test/use_case/remove_album_test.dart b/app/test/use_case/remove_album_test.dart index 561f2f10..ae590d0c 100644 --- a/app/test/use_case/remove_album_test.dart +++ b/app/test/use_case/remove_album_test.dart @@ -2,6 +2,7 @@ import 'package:event_bus/event_bus.dart'; import 'package:kiwi/kiwi.dart'; import 'package:nc_photos/db/entity_converter.dart'; import 'package:nc_photos/di_container.dart'; +import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/entity/pref/provider/memory.dart'; import 'package:nc_photos/use_case/album/remove_album.dart'; @@ -36,6 +37,7 @@ Future _removeAlbum() async { final c = DiContainer( albumRepo: MockAlbumMemoryRepo([album1, album2]), fileRepo: MockFileMemoryRepo([albumFile1, albumFile2]), + fileRepo2: MockFileMemoryRepo2([albumFile1, albumFile2]), shareRepo: MockShareRepo(), npDb: util.buildTestDb(), pref: Pref.scoped(PrefMemoryProvider()), @@ -44,7 +46,7 @@ Future _removeAlbum() async { await RemoveAlbum(c)( account, c.albumMemoryRepo.findAlbumByPath(albumFile1.path)); - expect(c.fileMemoryRepo.files, [albumFile2]); + expect(c.fileMemoryRepo2.files, [albumFile2.toDescriptor()]); } /// Remove a shared album (admin -> user1) @@ -64,6 +66,7 @@ Future _removeSharedAlbum() async { final c = DiContainer( albumRepo: MockAlbumMemoryRepo([album]), fileRepo: MockFileMemoryRepo([albumFile, ...files]), + fileRepo2: MockFileMemoryRepo2([albumFile, ...files]), shareRepo: MockShareMemoryRepo([ util.buildShare(id: "0", file: albumFile, shareWith: "user1"), util.buildShare(id: "1", file: files[0], shareWith: "user1"), @@ -77,7 +80,7 @@ Future _removeSharedAlbum() async { await RemoveAlbum(c)( account, c.albumMemoryRepo.findAlbumByPath(albumFile.path)); - expect(c.fileMemoryRepo.files, [files[0]]); + expect(c.fileMemoryRepo2.files, [files[0].toDescriptor()]); expect(c.shareMemoryRepo.shares, const []); } @@ -105,6 +108,7 @@ Future _removeSharedAlbumFileInOtherAlbum() async { final c = DiContainer( albumRepo: MockAlbumMemoryRepo(albums), fileRepo: MockFileMemoryRepo([...albumFiles, ...files]), + fileRepo2: MockFileMemoryRepo2([...albumFiles, ...files]), shareRepo: MockShareMemoryRepo([ util.buildShare(id: "0", file: albumFiles[0], shareWith: "user1"), util.buildShare(id: "1", file: files[0], shareWith: "user1"), @@ -119,7 +123,10 @@ Future _removeSharedAlbumFileInOtherAlbum() async { await RemoveAlbum(c)( account, c.albumMemoryRepo.findAlbumByPath(albumFiles[0].path)); - expect(c.fileMemoryRepo.files, [albumFiles[1], files[0]]); + expect( + c.fileMemoryRepo2.files, + [albumFiles[1].toDescriptor(), files[0].toDescriptor()], + ); expect(c.shareMemoryRepo.shares, [ util.buildShare(id: "1", file: files[0], shareWith: "user1"), util.buildShare(id: "2", file: albumFiles[1], shareWith: "user1"), @@ -147,6 +154,7 @@ Future _removeSharedAlbumResyncedFile() async { final c = DiContainer( albumRepo: MockAlbumMemoryRepo([album]), fileRepo: MockFileMemoryRepo([albumFile, ...files, ...user1Files]), + fileRepo2: MockFileMemoryRepo2([albumFile, ...files, ...user1Files]), shareRepo: MockShareMemoryRepo([ util.buildShare(id: "0", file: albumFile, shareWith: "user1"), util.buildShare(id: "1", file: files[0], shareWith: "user1"), @@ -167,6 +175,9 @@ Future _removeSharedAlbumResyncedFile() async { await RemoveAlbum(c)( account, c.albumMemoryRepo.findAlbumByPath(albumFile.path)); - expect(c.fileMemoryRepo.files, [...files, ...user1Files]); + expect( + c.fileMemoryRepo2.files, + [...files, ...user1Files].map((e) => e.toDescriptor()), + ); expect(c.shareMemoryRepo.shares, []); } diff --git a/app/test/use_case/remove_test.dart b/app/test/use_case/remove_test.dart index 94c3cd16..98176085 100644 --- a/app/test/use_case/remove_test.dart +++ b/app/test/use_case/remove_test.dart @@ -6,6 +6,7 @@ import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album/cover_provider.dart'; import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/sort_provider.dart'; +import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/entity/pref/provider/memory.dart'; import 'package:nc_photos/use_case/remove.dart'; @@ -46,6 +47,7 @@ Future _removeFile() async { final c = DiContainer( albumRepo: MockAlbumMemoryRepo(), fileRepo: MockFileMemoryRepo(files), + fileRepo2: MockFileMemoryRepo2(files), shareRepo: MockShareMemoryRepo(), npDb: util.buildTestDb(), pref: Pref.scoped(PrefMemoryProvider()), @@ -57,7 +59,7 @@ Future _removeFile() async { }); await Remove(c)(account, [files[0]]); - expect(c.fileMemoryRepo.files, [files[1]]); + expect(c.fileMemoryRepo2.files, [files[1].toDescriptor()]); } /// Remove a file, skip clean up @@ -72,6 +74,7 @@ Future _removeFileNoCleanUp() async { final c = DiContainer( albumRepo: MockAlbumMemoryRepo(), fileRepo: MockFileMemoryRepo(files), + fileRepo2: MockFileMemoryRepo2(files), shareRepo: MockShareMemoryRepo(), npDb: util.buildTestDb(), pref: Pref.scoped(PrefMemoryProvider()), @@ -83,7 +86,7 @@ Future _removeFileNoCleanUp() async { }); await Remove(c)(account, [files[0]], shouldCleanUp: false); - expect(c.fileMemoryRepo.files, [files[1]]); + expect(c.fileMemoryRepo2.files, [files[1].toDescriptor()]); } /// Remove a file included in an album From 07ee28d0cb30394c0f2de8c04b1e623116038963 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Wed, 14 Feb 2024 00:59:05 +0800 Subject: [PATCH 37/42] List and create share now take file descriptor instead of file --- app/lib/entity/file_util.dart | 13 ++++++++----- app/lib/entity/share.dart | 10 ++++++---- app/lib/entity/share/data_source.dart | 11 ++++++----- app/lib/use_case/create_share.dart | 3 ++- app/lib/use_case/list_share.dart | 14 ++++++-------- 5 files changed, 28 insertions(+), 23 deletions(-) diff --git a/app/lib/entity/file_util.dart b/app/lib/entity/file_util.dart index 9f36e167..2eb5453c 100644 --- a/app/lib/entity/file_util.dart +++ b/app/lib/entity/file_util.dart @@ -70,15 +70,18 @@ String unstripPath(Account account, String strippedPath) { } /// For a path "remote.php/dav/files/foo/bar.jpg", return foo -CiString getUserDirName(File file) { - if (file.path.startsWith("remote.php/dav/files/")) { +CiString getUserDirName(File file) => getUserDirNamePath(file.path); + +/// For a path "remote.php/dav/files/foo/bar.jpg", return foo +CiString getUserDirNamePath(String filePath) { + if (filePath.startsWith("remote.php/dav/files/")) { const beg = "remote.php/dav/files/".length; - final end = file.path.indexOf("/", beg); + final end = filePath.indexOf("/", beg); if (end != -1) { - return file.path.substring(beg, end).toCi(); + return filePath.substring(beg, end).toCi(); } } - throw ArgumentError("Invalid path: ${file.path}"); + throw ArgumentError("Invalid path: $filePath"); } String renameConflict(String filename, int conflictCount) { diff --git a/app/lib/entity/share.dart b/app/lib/entity/share.dart index 13614eda..f6a594d1 100644 --- a/app/lib/entity/share.dart +++ b/app/lib/entity/share.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/entity/file.dart'; +import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:np_string/np_string.dart'; import 'package:path/path.dart' as path_lib; import 'package:to_string/to_string.dart'; @@ -150,7 +151,7 @@ class ShareRepo { /// See [ShareDataSource.list] Future> list( Account account, - File file, { + FileDescriptor file, { bool? isIncludeReshare, }) => dataSrc.list(account, file, isIncludeReshare: isIncludeReshare); @@ -171,7 +172,8 @@ class ShareRepo { dataSrc.reverseListAll(account); /// See [ShareDataSource.create] - Future create(Account account, File file, String shareWith) => + Future create( + Account account, FileDescriptor file, String shareWith) => dataSrc.create(account, file, shareWith); /// See [ShareDataSource.createLink] @@ -193,7 +195,7 @@ abstract class ShareDataSource { /// List all shares from a given file Future> list( Account account, - File file, { + FileDescriptor file, { bool? isIncludeReshare, }); @@ -210,7 +212,7 @@ abstract class ShareDataSource { Future> reverseListAll(Account account); /// Share a file/folder with a user - Future create(Account account, File file, String shareWith); + Future create(Account account, FileDescriptor file, String shareWith); /// Share a file/folder with a share link /// diff --git a/app/lib/entity/share/data_source.dart b/app/lib/entity/share/data_source.dart index 0fdf72fd..7b9a3549 100644 --- a/app/lib/entity/share/data_source.dart +++ b/app/lib/entity/share/data_source.dart @@ -18,12 +18,12 @@ part 'data_source.g.dart'; @npLog class ShareRemoteDataSource implements ShareDataSource { @override - list( + Future> list( Account account, - File file, { + FileDescriptor file, { bool? isIncludeReshare, }) async { - _log.info("[list] ${file.path}"); + _log.info("[list] ${file.fdPath}"); final response = await ApiUtil.fromAccount(account).ocs().filesSharing().shares().get( path: file.strippedPath, @@ -73,8 +73,9 @@ class ShareRemoteDataSource implements ShareDataSource { } @override - create(Account account, File file, String shareWith) async { - _log.info("[create] Share '${file.path}' with '$shareWith'"); + Future create( + Account account, FileDescriptor file, String shareWith) async { + _log.info("[create] Share '${file.fdPath}' with '$shareWith'"); final response = await ApiUtil.fromAccount(account).ocs().filesSharing().shares().post( path: file.strippedPath, diff --git a/app/lib/use_case/create_share.dart b/app/lib/use_case/create_share.dart index 996a1e77..e7917cbf 100644 --- a/app/lib/use_case/create_share.dart +++ b/app/lib/use_case/create_share.dart @@ -1,11 +1,12 @@ import 'package:nc_photos/account.dart'; import 'package:nc_photos/entity/file.dart'; +import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/share.dart'; class CreateUserShare { const CreateUserShare(this.shareRepo); - Future call(Account account, File file, String shareWith) => + Future call(Account account, FileDescriptor file, String shareWith) => shareRepo.create(account, file, shareWith); final ShareRepo shareRepo; diff --git a/app/lib/use_case/list_share.dart b/app/lib/use_case/list_share.dart index d1fc80ed..f53a7a23 100644 --- a/app/lib/use_case/list_share.dart +++ b/app/lib/use_case/list_share.dart @@ -2,7 +2,7 @@ import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/di_container.dart'; -import 'package:nc_photos/entity/file.dart'; +import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/use_case/find_file.dart'; @@ -13,22 +13,20 @@ part 'list_share.g.dart'; /// List all shares from a given file @npLog class ListShare { - ListShare(this._c) : assert(require(_c)); - - static bool require(DiContainer c) => DiContainer.has(c, DiType.shareRepo); + const ListShare(this._c); Future> call( Account account, - File file, { + FileDescriptor file, { bool? isIncludeReshare, }) async { try { - if (file_util.getUserDirName(file) != account.userId) { - file = (await FindFile(_c)(account, [file.fileId!])).first; + if (file_util.getUserDirNamePath(file.fdPath) != account.userId) { + file = (await FindFile(_c)(account, [file.fdId])).first; } } catch (_) { // file not found - _log.warning("[call] File not found in db: ${logFilename(file.path)}"); + _log.warning("[call] File not found in db: ${logFilename(file.fdPath)}"); } return _c.shareRepo.list( account, From 1c999f6fab777bd3d8882a17351f4c39e1b5099a Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Wed, 14 Feb 2024 01:12:04 +0800 Subject: [PATCH 38/42] Migrate album to save files as file descriptors --- app/lib/bloc/list_album_share_outlier.dart | 18 +- app/lib/bloc/list_album_share_outlier.g.dart | 2 +- app/lib/entity/album.dart | 9 +- app/lib/entity/album/cover_provider.dart | 8 +- app/lib/entity/album/data_source2.dart | 5 + app/lib/entity/album/item.dart | 70 +-- app/lib/entity/album/item.g.dart | 2 +- app/lib/entity/album/upgrader.dart | 81 ++- app/lib/entity/album/upgrader.g.dart | 7 + app/lib/entity/collection/adapter/album.dart | 2 +- app/lib/entity/collection/exporter.dart | 1 + app/lib/entity/file_descriptor.dart | 22 +- app/lib/use_case/album/add_file_to_album.dart | 34 +- app/lib/use_case/album/remove_album.dart | 1 - app/lib/use_case/album/remove_from_album.dart | 10 +- .../use_case/album/share_album_with_user.dart | 12 +- .../album/unshare_album_with_user.dart | 1 - .../album/unshare_file_from_album.dart | 10 +- .../nc_album/remove_from_nc_album.dart | 2 +- app/lib/use_case/populate_album.dart | 2 + app/lib/use_case/preprocess_album.dart | 3 +- app/lib/use_case/remove.dart | 10 +- app/lib/use_case/resync_album.dart | 21 +- app/lib/use_case/update_album.dart | 22 +- app/lib/use_case/update_album_time.dart | 3 +- app/lib/use_case/update_auto_album_cover.dart | 4 +- .../widget/album_share_outlier_browser.dart | 35 +- app/test/entity/album/data_source_test.dart | 9 +- app/test/entity/album_test.dart | 512 +++++++++++------- .../entity/album_test/album_upgrader_v9.dart | 498 +++++++++++++++++ app/test/mock_type.dart | 25 +- app/test/test_util.dart | 13 +- app/test/use_case/add_file_to_album_test.dart | 66 ++- app/test/use_case/remove_from_album_test.dart | 54 +- .../use_case/share_album_with_user_test.dart | 54 +- 35 files changed, 1162 insertions(+), 466 deletions(-) create mode 100644 app/test/entity/album_test/album_upgrader_v9.dart diff --git a/app/lib/bloc/list_album_share_outlier.dart b/app/lib/bloc/list_album_share_outlier.dart index efaf26d1..108ebe23 100644 --- a/app/lib/bloc/list_album_share_outlier.dart +++ b/app/lib/bloc/list_album_share_outlier.dart @@ -9,6 +9,7 @@ import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/file.dart'; +import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/use_case/list_share.dart'; @@ -28,12 +29,12 @@ class ListAlbumShareOutlierItem with EquatableMixin { String toString() => _$toString(); @override - get props => [ + List get props => [ file, shareItems, ]; - final File file; + final FileDescriptor file; @Format(r"${$?.toReadableString()}") final List shareItems; } @@ -164,7 +165,6 @@ class ListAlbumShareOutlierBloc extends Bloc { ListAlbumShareOutlierBloc(this._c) : assert(require(_c)), - assert(ListShare.require(_c)), super(ListAlbumShareOutlierBlocInit()) { on(_onEvent); } @@ -282,7 +282,7 @@ class ListAlbumShareOutlierBloc extends Bloc s.userId) .toSet(); _log.info( - "[_processSingleFileItem] Sharees: ${albumSharees.map((s) => managedAlbumSharees.contains(s) ? "(managed)$s" : s).toReadableString()} for file: ${logFilename(fileItem.file.path)}"); + "[_processSingleFileItem] Sharees: ${albumSharees.map((s) => managedAlbumSharees.contains(s) ? "(managed)$s" : s).toReadableString()} for file: ${logFilename(fileItem.file.fdPath)}"); // check all shares (including reshares) against sharees that are managed by // us @@ -320,10 +320,10 @@ class ListAlbumShareOutlierBloc extends Bloc s != account.userId && s != fileItem.file.ownerId) + .where((s) => s != account.userId && s != fileItem.ownerId) .toList(); _log.info( - "[_processSingleFileItem] Missing shares: ${missings.toReadableString()} for file: ${logFilename(fileItem.file.path)}"); + "[_processSingleFileItem] Missing shares: ${missings.toReadableString()} for file: ${logFilename(fileItem.file.fdPath)}"); for (final m in missings) { final as = albumShares[m]!; shareItems.add( @@ -338,14 +338,14 @@ class ListAlbumShareOutlierBloc extends Bloc s.shareWith == e))); } catch (e, stackTrace) { _log.severe( - "[_processSingleFileItem] Failed while processing extra share for file: ${logFilename(fileItem.file.path)}", + "[_processSingleFileItem] Failed while processing extra share for file: ${logFilename(fileItem.file.fdPath)}", e, stackTrace); errors.add(e); diff --git a/app/lib/bloc/list_album_share_outlier.g.dart b/app/lib/bloc/list_album_share_outlier.g.dart index cd087b9c..0867dc2d 100644 --- a/app/lib/bloc/list_album_share_outlier.g.dart +++ b/app/lib/bloc/list_album_share_outlier.g.dart @@ -21,7 +21,7 @@ extension _$ListAlbumShareOutlierBlocNpLog on ListAlbumShareOutlierBloc { extension _$ListAlbumShareOutlierItemToString on ListAlbumShareOutlierItem { String _$toString() { // ignore: unnecessary_string_interpolations - return "ListAlbumShareOutlierItem {file: ${file.path}, shareItems: ${shareItems.toReadableString()}}"; + return "ListAlbumShareOutlierItem {file: ${file.fdPath}, shareItems: ${shareItems.toReadableString()}}"; } } diff --git a/app/lib/entity/album.dart b/app/lib/entity/album.dart index 01282a69..b9bafde2 100644 --- a/app/lib/entity/album.dart +++ b/app/lib/entity/album.dart @@ -101,6 +101,13 @@ class Album with EquatableMixin { return null; } } + if (jsonVersion < 10) { + result = upgraderFactory?.buildV9()?.doJson(result); + if (result == null) { + _log.info("[fromJson] Version $jsonVersion not compatible"); + return null; + } + } if (jsonVersion > version) { _log.warning( "[fromJson] Reading album with newer version: $jsonVersion > $version"); @@ -224,7 +231,7 @@ class Album with EquatableMixin { final int savedVersion; /// versioning of this class, use to upgrade old persisted album - static const version = 9; + static const version = 10; static final _log = _$AlbumNpLog.log; } diff --git a/app/lib/entity/album/cover_provider.dart b/app/lib/entity/album/cover_provider.dart index d280be3c..abeacff6 100644 --- a/app/lib/entity/album/cover_provider.dart +++ b/app/lib/entity/album/cover_provider.dart @@ -4,7 +4,6 @@ import 'package:logging/logging.dart'; import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/provider.dart'; -import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:np_codegen/np_codegen.dart'; @@ -78,11 +77,8 @@ class AlbumAutoCoverProvider extends AlbumCoverProvider { return items .whereType() .map((e) => e.file) - .where((element) => - file_util.isSupportedFormat(element) && - (element.hasPreview ?? false) && - element.fileId != null) - .sorted(compareFileDateTimeDescending) + .where(file_util.isSupportedFormat) + .sorted(compareFileDescriptorDateTimeDescending) .firstOrNull; } diff --git a/app/lib/entity/album/data_source2.dart b/app/lib/entity/album/data_source2.dart index cf755761..dd4ed749 100644 --- a/app/lib/entity/album/data_source2.dart +++ b/app/lib/entity/album/data_source2.dart @@ -147,6 +147,11 @@ class AlbumSqliteDbDataSource2 implements AlbumDataSource2 { if (dbAlbum.version < 9) { dbAlbum = AlbumUpgraderV8(logFilePath: file.path).doDb(dbAlbum)!; } + if (dbAlbum.version < 10) { + dbAlbum = + AlbumUpgraderV9(account: account, logFilePath: file.path) + .doDb(dbAlbum)!; + } return DbAlbumConverter.fromDb(file, dbAlbum); } catch (e, stackTrace) { _log.severe( diff --git a/app/lib/entity/album/item.dart b/app/lib/entity/album/item.dart index f7a8e0bc..94a8ce3f 100644 --- a/app/lib/entity/album/item.dart +++ b/app/lib/entity/album/item.dart @@ -1,9 +1,8 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; -import 'package:nc_photos/entity/file.dart'; +import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:np_codegen/np_codegen.dart'; -import 'package:np_common/or_null.dart'; import 'package:np_common/type.dart'; import 'package:np_string/np_string.dart'; import 'package:to_string/to_string.dart'; @@ -57,11 +56,13 @@ abstract class AlbumItem with EquatableMixin { JsonObj toContentJson(); + bool compareServerIdentity(AlbumItem other); + @override String toString() => _$toString(); @override - get props => [ + List get props => [ addedBy, addedAt, ]; @@ -75,29 +76,19 @@ abstract class AlbumItem with EquatableMixin { @toString class AlbumFileItem extends AlbumItem { AlbumFileItem({ - required CiString addedBy, - required DateTime addedAt, + required super.addedBy, + required super.addedAt, required this.file, - }) : super(addedBy: addedBy, addedAt: addedAt); - - @override - // ignore: hash_and_equals - bool operator ==(Object? other) => equals(other, isDeep: true); - - bool equals(Object? other, {bool isDeep = false}) { - if (other is AlbumFileItem) { - return super == other && (file.equals(other.file, isDeep: isDeep)); - } else { - return false; - } - } + required this.ownerId, + }); factory AlbumFileItem.fromJson( JsonObj json, CiString addedBy, DateTime addedAt) { return AlbumFileItem( addedBy: addedBy, addedAt: addedAt, - file: File.fromJson(json["file"].cast()), + file: FileDescriptor.fromJson(json["file"].cast()), + ownerId: (json["ownerId"] as String).toCi(), ); } @@ -105,37 +96,43 @@ class AlbumFileItem extends AlbumItem { String toString() => _$toString(); @override - toContentJson() { + JsonObj toContentJson() { return { - "file": file.toJson(), + "file": file.toFdJson(), + "ownerId": ownerId.raw, }; } + @override + bool compareServerIdentity(AlbumItem other) => + other is AlbumFileItem && + file.compareServerIdentity(other.file) && + addedBy == other.addedBy && + addedAt == other.addedAt; + AlbumFileItem copyWith({ CiString? addedBy, DateTime? addedAt, - File? file, + FileDescriptor? file, + CiString? ownerId, }) { return AlbumFileItem( addedBy: addedBy ?? this.addedBy, addedAt: addedAt ?? this.addedAt, file: file ?? this.file, + ownerId: ownerId ?? this.ownerId, ); } - AlbumFileItem minimize() => AlbumFileItem( - addedBy: addedBy, - addedAt: addedAt, - file: file.copyWith(metadata: const OrNull(null)), - ); - @override - get props => [ + List get props => [ ...super.props, - // file is handled separately, see [equals] + file, + ownerId, ]; - final File file; + final FileDescriptor file; + final CiString ownerId; static const _type = "file"; } @@ -161,12 +158,19 @@ class AlbumLabelItem extends AlbumItem { String toString() => _$toString(); @override - toContentJson() { + JsonObj toContentJson() { return { "text": text, }; } + @override + bool compareServerIdentity(AlbumItem other) => + other is AlbumLabelItem && + text == other.text && + addedBy == other.addedBy && + addedAt == other.addedAt; + AlbumLabelItem copyWith({ CiString? addedBy, DateTime? addedAt, @@ -180,7 +184,7 @@ class AlbumLabelItem extends AlbumItem { } @override - get props => [ + List get props => [ ...super.props, text, ]; diff --git a/app/lib/entity/album/item.g.dart b/app/lib/entity/album/item.g.dart index 9c3131e9..fe8fdd1a 100644 --- a/app/lib/entity/album/item.g.dart +++ b/app/lib/entity/album/item.g.dart @@ -27,7 +27,7 @@ extension _$AlbumItemToString on AlbumItem { extension _$AlbumFileItemToString on AlbumFileItem { String _$toString() { // ignore: unnecessary_string_interpolations - return "AlbumFileItem {addedBy: $addedBy, addedAt: $addedAt, file: ${file.path}}"; + return "AlbumFileItem {addedBy: $addedBy, addedAt: $addedAt, file: ${file.fdPath}, ownerId: $ownerId}"; } } diff --git a/app/lib/entity/album/upgrader.dart b/app/lib/entity/album/upgrader.dart index aaba2803..e62174e1 100644 --- a/app/lib/entity/album/upgrader.dart +++ b/app/lib/entity/album/upgrader.dart @@ -350,6 +350,64 @@ class AlbumUpgraderV8 implements AlbumUpgrader { final String? logFilePath; } +/// Upgrade v9 Album to v10 +/// +/// In v10, file items are now stored as FileDescriptor instead of File +@npLog +class AlbumUpgraderV9 implements AlbumUpgrader { + const AlbumUpgraderV9({ + required this.account, + this.logFilePath, + }); + + @override + JsonObj? doJson(JsonObj json) { + _log.fine("[doJson] Upgrade v9 Album for file: $logFilePath"); + final result = JsonObj.from(json); + if (result["provider"]["type"] != "static") { + return result; + } + for (final item in (result["provider"]["content"]["items"] as List)) { + if (item["type"] != "file") { + continue; + } + final originalFile = + (item["content"]["file"] as Map).cast(); + item["content"]["file"] = + AlbumUpgraderV8._fileJsonToFileDescriptorJson(originalFile); + item["content"]["ownerId"] = + originalFile["ownerId"] ?? account.userId.raw; + } + return result; + } + + @override + DbAlbum? doDb(DbAlbum dbObj) { + _log.fine("[doDb] Upgrade v9 Album for file: $logFilePath"); + if (dbObj.providerType != "static") { + return dbObj; + } + final content = Map.of(dbObj.providerContent); + for (final item in content["items"] as List) { + if (item["type"] != "file") { + continue; + } + final originalFile = + (item["content"]["file"] as Map).cast(); + item["content"]["file"] = + AlbumUpgraderV8._fileJsonToFileDescriptorJson(originalFile); + item["content"]["ownerId"] = + originalFile["ownerId"] ?? account.userId.raw; + } + return dbObj.copyWith(providerContent: content); + } + + final Account account; + + /// File path for logging only + final String? logFilePath; +} + abstract class AlbumUpgraderFactory { const AlbumUpgraderFactory(); @@ -361,6 +419,7 @@ abstract class AlbumUpgraderFactory { AlbumUpgraderV6? buildV6(); AlbumUpgraderV7? buildV7(); AlbumUpgraderV8? buildV8(); + AlbumUpgraderV9? buildV9(); } class DefaultAlbumUpgraderFactory extends AlbumUpgraderFactory { @@ -371,32 +430,38 @@ class DefaultAlbumUpgraderFactory extends AlbumUpgraderFactory { }); @override - buildV1() => AlbumUpgraderV1(logFilePath: logFilePath); + AlbumUpgraderV1 buildV1() => AlbumUpgraderV1(logFilePath: logFilePath); @override - buildV2() => AlbumUpgraderV2(logFilePath: logFilePath); + AlbumUpgraderV2 buildV2() => AlbumUpgraderV2(logFilePath: logFilePath); @override - buildV3() => AlbumUpgraderV3(logFilePath: logFilePath); + AlbumUpgraderV3 buildV3() => AlbumUpgraderV3(logFilePath: logFilePath); @override - buildV4() => AlbumUpgraderV4(logFilePath: logFilePath); + AlbumUpgraderV4 buildV4() => AlbumUpgraderV4(logFilePath: logFilePath); @override - buildV5() => AlbumUpgraderV5( + AlbumUpgraderV5 buildV5() => AlbumUpgraderV5( account, albumFile: albumFile, logFilePath: logFilePath, ); @override - buildV6() => AlbumUpgraderV6(logFilePath: logFilePath); + AlbumUpgraderV6 buildV6() => AlbumUpgraderV6(logFilePath: logFilePath); @override - buildV7() => AlbumUpgraderV7(logFilePath: logFilePath); + AlbumUpgraderV7 buildV7() => AlbumUpgraderV7(logFilePath: logFilePath); @override - AlbumUpgraderV8? buildV8() => AlbumUpgraderV8(logFilePath: logFilePath); + AlbumUpgraderV8 buildV8() => AlbumUpgraderV8(logFilePath: logFilePath); + + @override + AlbumUpgraderV9 buildV9() => AlbumUpgraderV9( + account: account, + logFilePath: logFilePath, + ); final Account account; final File? albumFile; diff --git a/app/lib/entity/album/upgrader.g.dart b/app/lib/entity/album/upgrader.g.dart index bcc45112..b1db67c1 100644 --- a/app/lib/entity/album/upgrader.g.dart +++ b/app/lib/entity/album/upgrader.g.dart @@ -61,3 +61,10 @@ extension _$AlbumUpgraderV8NpLog on AlbumUpgraderV8 { static final log = Logger("entity.album.upgrader.AlbumUpgraderV8"); } + +extension _$AlbumUpgraderV9NpLog on AlbumUpgraderV9 { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("entity.album.upgrader.AlbumUpgraderV9"); +} diff --git a/app/lib/entity/collection/adapter/album.dart b/app/lib/entity/collection/adapter/album.dart index 4917e47b..5023a497 100644 --- a/app/lib/entity/collection/adapter/album.dart +++ b/app/lib/entity/collection/adapter/album.dart @@ -192,7 +192,7 @@ class CollectionAlbumAdapter implements CollectionAdapter { _provider.album, sharee, onShareFileFailed: (f, e, stackTrace) { - _log.severe("[share] Failed to share file: ${logFilename(f.path)}", e, + _log.severe("[share] Failed to share file: ${logFilename(f.fdPath)}", e, stackTrace); fileFailed = true; }, diff --git a/app/lib/entity/collection/exporter.dart b/app/lib/entity/collection/exporter.dart index 9db4a003..899669dc 100644 --- a/app/lib/entity/collection/exporter.dart +++ b/app/lib/entity/collection/exporter.dart @@ -50,6 +50,7 @@ class CollectionExporter { addedBy: account.userId, addedAt: clock.now().toUtc(), file: f, + ownerId: f.ownerId ?? account.userId, ); } } else if (e is CollectionLabelItem) { diff --git a/app/lib/entity/file_descriptor.dart b/app/lib/entity/file_descriptor.dart index ac5de1c8..cda61fb9 100644 --- a/app/lib/entity/file_descriptor.dart +++ b/app/lib/entity/file_descriptor.dart @@ -55,7 +55,7 @@ class FileDescriptor with EquatableMixin { JsonObj toFdJson() => toJson(this); @override - get props => [ + List get props => [ fdPath, fdId, fdMime, @@ -136,3 +136,23 @@ extension FileDescriptorExtension on FileDescriptor { ); } } + +class FileDescriptorServerIdentityComparator { + const FileDescriptorServerIdentityComparator(this.file); + + @override + bool operator ==(Object other) { + if (other is FileDescriptorServerIdentityComparator) { + return file.compareServerIdentity(other.file); + } else if (other is FileDescriptor) { + return file.compareServerIdentity(other); + } else { + return false; + } + } + + @override + int get hashCode => file.fdId.hashCode; + + final FileDescriptor file; +} diff --git a/app/lib/use_case/album/add_file_to_album.dart b/app/lib/use_case/album/add_file_to_album.dart index ff84878c..50611d4a 100644 --- a/app/lib/use_case/album/add_file_to_album.dart +++ b/app/lib/use_case/album/add_file_to_album.dart @@ -27,7 +27,6 @@ class AddFileToAlbum { static bool require(DiContainer c) => DiContainer.has(c, DiType.albumRepo) && DiContainer.has(c, DiType.shareRepo) && - ListShare.require(c) && PreProcessAlbum.require(c); /// Add list of files to [album] @@ -47,7 +46,8 @@ class AddFileToAlbum { .map((f) => AlbumFileItem( addedBy: account.userId, addedAt: clock.now(), - file: f, + file: f.toDescriptor(), + ownerId: f.ownerId ?? account.userId, )) .where((i) => itemSet.add(OverrideComparator( i, _isItemFileEqual, _getItemHashCode))) @@ -63,18 +63,14 @@ class AddFileToAlbum { ); // UpdateAlbumWithActualItems only persists when there are changes to // several properties, so we can't rely on it - newAlbum = await UpdateAlbumWithActualItems(null)( - account, - newAlbum, - newItems, - ); + newAlbum = + await UpdateAlbumWithActualItems(null)(account, newAlbum, newItems); await UpdateAlbum(_c.albumRepo)(account, newAlbum); if (album.shares?.isNotEmpty == true) { - final newFiles = - addItems.whereType().map((e) => e.file).toList(); - if (newFiles.isNotEmpty) { - await _shareFiles(account, newAlbum, newFiles); + final newFileItems = addItems.whereType().toList(); + if (newFileItems.isNotEmpty) { + await _shareFiles(account, newAlbum, newFileItems); } } @@ -82,7 +78,7 @@ class AddFileToAlbum { } Future _shareFiles( - Account account, Album album, List files) async { + Account account, Album album, List fileItems) async { final albumShares = (album.shares!.map((e) => e.userId).toList() ..add(album.albumFile!.ownerId ?? account.userId)) .where((element) => element != account.userId) @@ -90,30 +86,30 @@ class AddFileToAlbum { if (albumShares.isEmpty) { return; } - for (final f in files) { + for (final i in fileItems) { try { - final fileShares = (await ListShare(_c)(account, f)) + final fileShares = (await ListShare(_c)(account, i.file)) .where((element) => element.shareType == ShareType.user) .map((e) => e.shareWith!) .toSet(); final diffShares = albumShares.difference(fileShares); for (final s in diffShares) { - if (s == f.ownerId) { + if (s == i.ownerId) { // skip files already owned by the target user continue; } try { - await CreateUserShare(_c.shareRepo)(account, f, s.raw); + await CreateUserShare(_c.shareRepo)(account, i.file, s.raw); } catch (e, stackTrace) { _log.shout( - "[_shareFiles] Failed while CreateUserShare: ${logFilename(f.path)}", + "[_shareFiles] Failed while CreateUserShare: ${logFilename(i.file.fdPath)}", e, stackTrace); } } } catch (e, stackTrace) { _log.shout( - "[_shareFiles] Failed while listing shares: ${logFilename(f.path)}", + "[_shareFiles] Failed while listing shares: ${logFilename(i.file.fdPath)}", e, stackTrace); } @@ -133,7 +129,7 @@ bool _isItemFileEqual(AlbumItem a, AlbumItem b) { int _getItemHashCode(AlbumItem a) { if (a is AlbumFileItem) { - return a.file.fileId?.hashCode ?? a.file.path.hashCode; + return a.file.fdId.hashCode; } else { return a.hashCode; } diff --git a/app/lib/use_case/album/remove_album.dart b/app/lib/use_case/album/remove_album.dart index 1fdbd2f6..8318589d 100644 --- a/app/lib/use_case/album/remove_album.dart +++ b/app/lib/use_case/album/remove_album.dart @@ -19,7 +19,6 @@ part 'remove_album.g.dart'; class RemoveAlbum { RemoveAlbum(this._c) : assert(require(_c)), - assert(ListShare.require(_c)), assert(UnshareFileFromAlbum.require(_c)); static bool require(DiContainer c) => diff --git a/app/lib/use_case/album/remove_from_album.dart b/app/lib/use_case/album/remove_from_album.dart index 9716af59..4621f002 100644 --- a/app/lib/use_case/album/remove_from_album.dart +++ b/app/lib/use_case/album/remove_from_album.dart @@ -30,9 +30,6 @@ class RemoveFromAlbum { /// Remove a list of AlbumItems from [album] /// - /// The items are compared with [identical], so it must come from [album] for - /// it to work - /// /// If [shouldUnshare] is false, files will not be unshared after removing /// from the album Future call( @@ -65,7 +62,8 @@ class RemoveFromAlbum { .toList(); final provider = album.provider as AlbumStaticProvider; final newItems = provider.items - .where((element) => !filtered.containsIdentical(element)) + .where((e) => + !filtered.containsIf(e, (a, b) => a.compareServerIdentity(b))) .toList(); var newAlbum = album.copyWith( provider: AlbumStaticProvider.of(album).copyWith( @@ -108,7 +106,7 @@ class RemoveFromAlbum { isNeedUpdate = true; break; } - if (fileItem.file.bestDateTime == newAlbum.provider.latestItemTime) { + if (fileItem.file.fdDateTime == newAlbum.provider.latestItemTime) { isNeedUpdate = true; break; } @@ -130,7 +128,7 @@ class RemoveFromAlbum { } Future _unshareFiles( - Account account, Album album, List files) async { + Account account, Album album, List files) async { final albumShares = (album.shares!.map((e) => e.userId).toList() ..add(album.albumFile!.ownerId ?? account.userId)) .where((element) => element != account.userId) diff --git a/app/lib/use_case/album/share_album_with_user.dart b/app/lib/use_case/album/share_album_with_user.dart index 484440a7..8fc128d6 100644 --- a/app/lib/use_case/album/share_album_with_user.dart +++ b/app/lib/use_case/album/share_album_with_user.dart @@ -4,7 +4,7 @@ import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/provider.dart'; -import 'package:nc_photos/entity/file.dart'; +import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/sharee.dart'; import 'package:nc_photos/use_case/create_share.dart'; @@ -25,7 +25,7 @@ class ShareAlbumWithUser { Account account, Album album, Sharee sharee, { - ErrorWithValueHandler? onShareFileFailed, + ErrorWithValueHandler? onShareFileFailed, }) async { assert(album.provider is AlbumStaticProvider); final newShares = (album.shares ?? []) @@ -56,12 +56,12 @@ class ShareAlbumWithUser { Account account, Album album, CiString shareWith, { - ErrorWithValueHandler? onShareFileFailed, + ErrorWithValueHandler? onShareFileFailed, }) async { final files = AlbumStaticProvider.of(album) .items .whereType() - .where((item) => item.file.ownerId != shareWith) + .where((item) => item.ownerId != shareWith) .map((e) => e.file); try { await CreateUserShare(shareRepo)( @@ -74,12 +74,12 @@ class ShareAlbumWithUser { onShareFileFailed?.call(album.albumFile!, e, stackTrace); } for (final f in files) { - _log.info("[_createFileShares] Sharing '${f.path}' with '$shareWith'"); + _log.info("[_createFileShares] Sharing '${f.fdPath}' with '$shareWith'"); try { await CreateUserShare(shareRepo)(account, f, shareWith.raw); } catch (e, stackTrace) { _log.severe( - "[_createFileShares] Failed sharing file '${logFilename(f.path)}' with '$shareWith'", + "[_createFileShares] Failed sharing file '${logFilename(f.fdPath)}' with '$shareWith'", e, stackTrace); onShareFileFailed?.call(f, e, stackTrace); diff --git a/app/lib/use_case/album/unshare_album_with_user.dart b/app/lib/use_case/album/unshare_album_with_user.dart index d6dca056..00456631 100644 --- a/app/lib/use_case/album/unshare_album_with_user.dart +++ b/app/lib/use_case/album/unshare_album_with_user.dart @@ -21,7 +21,6 @@ part 'unshare_album_with_user.g.dart'; class UnshareAlbumWithUser { UnshareAlbumWithUser(this._c) : assert(require(_c)), - assert(ListShare.require(_c)), assert(UnshareFileFromAlbum.require(_c)); static bool require(DiContainer c) => diff --git a/app/lib/use_case/album/unshare_file_from_album.dart b/app/lib/use_case/album/unshare_file_from_album.dart index 8a5983ea..0c49c069 100644 --- a/app/lib/use_case/album/unshare_file_from_album.dart +++ b/app/lib/use_case/album/unshare_file_from_album.dart @@ -5,7 +5,6 @@ import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/provider.dart'; -import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/stream_extension.dart'; @@ -22,8 +21,7 @@ part 'unshare_file_from_album.g.dart'; class UnshareFileFromAlbum { UnshareFileFromAlbum(this._c) : assert(require(_c)), - assert(ListAlbum.require(_c)), - assert(ListShare.require(_c)); + assert(ListAlbum.require(_c)); static bool require(DiContainer c) => DiContainer.has(c, DiType.shareRepo); @@ -34,7 +32,7 @@ class UnshareFileFromAlbum { Future call( Account account, Album album, - List files, + List files, List unshareWith, { ErrorWithValueHandler? onUnshareFileFailed, }) async { @@ -57,7 +55,7 @@ class UnshareFileFromAlbum { exclusiveShares.addAll( shares.where((element) => unshareWith.contains(element.shareWith))); } catch (e, stackTrace) { - _log.severe("[call] Failed while ListShare: '${logFilename(f.path)}'", + _log.severe("[call] Failed while ListShare: '${logFilename(f.fdPath)}'", e, stackTrace); } } @@ -77,7 +75,7 @@ class UnshareFileFromAlbum { // remove files shared as part of this other shared album exclusiveShares.removeWhere((s) => sharesOfInterest.any((i) => i.userId == s.shareWith) && - albumFiles.any((f) => f.fileId == s.itemSource)); + albumFiles.any((f) => f.fdId == s.itemSource)); } _log.fine("[call] Post-filter shares: $exclusiveShares"); diff --git a/app/lib/use_case/nc_album/remove_from_nc_album.dart b/app/lib/use_case/nc_album/remove_from_nc_album.dart index d9c6bb98..6939aaa1 100644 --- a/app/lib/use_case/nc_album/remove_from_nc_album.dart +++ b/app/lib/use_case/nc_album/remove_from_nc_album.dart @@ -45,7 +45,7 @@ class RemoveFromNcAlbum { var count = fileItems.length; await Remove(_c)( account, - fileItems.map((e) => e.file.toFile()).toList(), + fileItems.map((e) => e.file).toList(), onError: (i, f, e, stackTrace) { --count; try { diff --git a/app/lib/use_case/populate_album.dart b/app/lib/use_case/populate_album.dart index 6b020a49..26ef6d1e 100644 --- a/app/lib/use_case/populate_album.dart +++ b/app/lib/use_case/populate_album.dart @@ -55,6 +55,7 @@ class PopulateAlbum { addedBy: account.userId, addedAt: clock.now(), file: f, + ownerId: f.ownerId ?? account.userId, ))); } } @@ -72,6 +73,7 @@ class PopulateAlbum { addedBy: account.userId, addedAt: clock.now(), file: f, + ownerId: f.ownerId ?? account.userId, ))); return products; } diff --git a/app/lib/use_case/preprocess_album.dart b/app/lib/use_case/preprocess_album.dart index c8d4c534..90aee799 100644 --- a/app/lib/use_case/preprocess_album.dart +++ b/app/lib/use_case/preprocess_album.dart @@ -14,8 +14,7 @@ import 'package:nc_photos/use_case/resync_album.dart'; class PreProcessAlbum { PreProcessAlbum(this._c) : assert(require(_c)), - assert(PopulateAlbum.require(_c)), - assert(ResyncAlbum.require(_c)); + assert(PopulateAlbum.require(_c)); static bool require(DiContainer c) => true; diff --git a/app/lib/use_case/remove.dart b/app/lib/use_case/remove.dart index e226b6b2..e6ef30c2 100644 --- a/app/lib/use_case/remove.dart +++ b/app/lib/use_case/remove.dart @@ -7,7 +7,6 @@ import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/provider.dart'; -import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/stream_extension.dart'; @@ -61,7 +60,7 @@ class Remove { Account account, List removes) async { final albums = await ListAlbum(_c)(account).whereType().toList(); // figure out which files need to be unshared with whom - final unshares = >{}; + final unshares = >{}; // clean up only make sense for static albums for (final a in albums.where((a) => a.provider is AlbumStaticProvider)) { try { @@ -69,22 +68,21 @@ class Remove { final itemsToRemove = provider.items .whereType() .where((i) => - (i.file.isOwned(account.userId) || - i.addedBy == account.userId) && + (i.ownerId == account.userId || i.addedBy == account.userId) && removes.any((r) => r.compareServerIdentity(i.file))) .toList(); if (itemsToRemove.isEmpty) { continue; } for (final i in itemsToRemove) { - final key = FileServerIdentityComparator(i.file); + final key = FileDescriptorServerIdentityComparator(i.file); final value = (a.shares?.map((s) => s.userId).toList() ?? []) ..add(a.albumFile!.ownerId!) ..remove(account.userId); (unshares[key] ??= {}).addAll(value); } _log.fine( - "[_cleanUpAlbums] Removing from album '${a.name}': ${itemsToRemove.map((e) => e.file.path).toReadableString()}"); + "[_cleanUpAlbums] Removing from album '${a.name}': ${itemsToRemove.map((e) => e.file.fdPath).toReadableString()}"); // skip unsharing as we'll handle it ourselves await RemoveFromAlbum(_c)(account, a, itemsToRemove, shouldUnshare: false); diff --git a/app/lib/use_case/resync_album.dart b/app/lib/use_case/resync_album.dart index 556efd97..ff68d5dd 100644 --- a/app/lib/use_case/resync_album.dart +++ b/app/lib/use_case/resync_album.dart @@ -6,7 +6,7 @@ import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/provider.dart'; -import 'package:nc_photos/use_case/find_file.dart'; +import 'package:nc_photos/use_case/find_file_descriptor.dart'; import 'package:np_codegen/np_codegen.dart'; part 'resync_album.g.dart'; @@ -14,9 +14,7 @@ part 'resync_album.g.dart'; /// Resync files inside an album with the file db @npLog class ResyncAlbum { - ResyncAlbum(this._c) : assert(require(_c)); - - static bool require(DiContainer c) => true; + const ResyncAlbum(this._c); Future> call(Account account, Album album) async { _log.info("[call] Resync album: ${album.name}"); @@ -26,11 +24,11 @@ class ResyncAlbum { } final items = AlbumStaticProvider.of(album).items; - final files = await FindFile(_c)( + final files = await FindFileDescriptor(_c)( account, items .whereType() - .map((i) => i.file.fileId) + .map((i) => i.file.fdId) .whereNotNull() .toList(), onFileNotFound: (_) {}, @@ -40,19 +38,18 @@ class ResyncAlbum { return items.map((i) { if (i is AlbumFileItem) { try { - if (i.file.fileId! == nextFile?.fileId) { - final newItem = i.copyWith( - file: nextFile, - ); + if (i.file.fdId == nextFile?.fdId) { + final newItem = i.copyWith(file: nextFile); nextFile = fileIt.moveNext() ? fileIt.current : null; return newItem; } else { - _log.warning("[call] File not found: ${logFilename(i.file.path)}"); + _log.warning( + "[call] File not found: ${logFilename(i.file.fdPath)}"); return i; } } catch (e, stackTrace) { _log.shout( - "[call] Failed syncing file in album: ${logFilename(i.file.path)}", + "[call] Failed syncing file in album: ${logFilename(i.file.fdPath)}", e, stackTrace); return i; diff --git a/app/lib/use_case/update_album.dart b/app/lib/use_case/update_album.dart index 5cf0f065..6d8818ba 100644 --- a/app/lib/use_case/update_album.dart +++ b/app/lib/use_case/update_album.dart @@ -1,11 +1,9 @@ import 'package:nc_photos/account.dart'; import 'package:nc_photos/entity/album.dart'; -import 'package:nc_photos/entity/album/item.dart'; -import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/exception.dart'; class UpdateAlbum { - UpdateAlbum(this.albumRepo); + const UpdateAlbum(this.albumRepo); Future call(Account account, Album album) async { if (album.savedVersion > Album.version) { @@ -13,23 +11,7 @@ class UpdateAlbum { throw AlbumDowngradeException( "Not allowed to downgrade album '${album.name}'"); } - final provider = album.provider; - if (provider is AlbumStaticProvider) { - await albumRepo.update( - account, - album.copyWith( - provider: provider.copyWith( - items: _minimizeItems(provider.items), - ), - ), - ); - } else { - await albumRepo.update(account, album); - } - } - - List _minimizeItems(List items) { - return items.map((e) => e is AlbumFileItem ? e.minimize() : e).toList(); + await albumRepo.update(account, album); } final AlbumRepo albumRepo; diff --git a/app/lib/use_case/update_album_time.dart b/app/lib/use_case/update_album_time.dart index b3a17088..f8614ec5 100644 --- a/app/lib/use_case/update_album_time.dart +++ b/app/lib/use_case/update_album_time.dart @@ -2,7 +2,6 @@ import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/sort_provider.dart'; -import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:np_common/or_null.dart'; @@ -49,7 +48,7 @@ class UpdateAlbumTime { .map((e) => e.file) .where((element) => file_util.isSupportedFormat(element)) .first; - latestItemTime = latestFile.bestDateTime; + latestItemTime = latestFile.fdDateTime; } catch (_) { latestItemTime = null; } diff --git a/app/lib/use_case/update_auto_album_cover.dart b/app/lib/use_case/update_auto_album_cover.dart index b51f287c..3c0b080e 100644 --- a/app/lib/use_case/update_auto_album_cover.dart +++ b/app/lib/use_case/update_auto_album_cover.dart @@ -45,8 +45,8 @@ class UpdateAutoAlbumCover { final coverFile = sortedItems .whereType() .map((e) => e.file) - .where((element) => file_util.isSupportedFormat(element)) - .firstWhere((element) => element.hasPreview ?? false); + .where(file_util.isSupportedFormat) + .first; // cache the result for later use if ((album.coverProvider as AlbumAutoCoverProvider) .coverFile diff --git a/app/lib/widget/album_share_outlier_browser.dart b/app/lib/widget/album_share_outlier_browser.dart index c8806bb3..f1a90814 100644 --- a/app/lib/widget/album_share_outlier_browser.dart +++ b/app/lib/widget/album_share_outlier_browser.dart @@ -7,7 +7,6 @@ import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/bloc/list_album_share_outlier.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/album.dart'; -import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/share.dart'; @@ -190,7 +189,7 @@ class _AlbumShareOutlierBrowserState extends State { Widget _buildMissingShareeItem( BuildContext context, _MissingShareeItem item) { final Widget trailing; - switch (_getItemStatus(item.file.path, item.shareWith)) { + switch (_getItemStatus(item.file.fdPath, item.shareWith)) { case null: trailing = _buildFixButton( onPressed: () { @@ -223,7 +222,7 @@ class _AlbumShareOutlierBrowserState extends State { Widget _buildExtraShareItem(BuildContext context, _ExtraShareItem item) { final Widget trailing; - switch (_getItemStatus(item.file.path, item.share.shareWith!)) { + switch (_getItemStatus(item.file.fdPath, item.share.shareWith!)) { case null: trailing = _buildFixButton( onPressed: () { @@ -254,7 +253,7 @@ class _AlbumShareOutlierBrowserState extends State { ); } - Widget _buildFileThumbnail(File file) { + Widget _buildFileThumbnail(FileDescriptor file) { if (file_util.isAlbumFile(widget.account, file)) { return const SizedBox.square( dimension: 56, @@ -270,8 +269,8 @@ class _AlbumShareOutlierBrowserState extends State { } } - String _buildFilename(File file) { - if (widget.album.albumFile?.path.equalsIgnoreCase(file.path) == true) { + String _buildFilename(FileDescriptor file) { + if (widget.album.albumFile?.path.equalsIgnoreCase(file.fdPath) == true) { return widget.album.name; } else { return file.filename; @@ -326,9 +325,9 @@ class _AlbumShareOutlierBrowserState extends State { // select only items that are not fixed/being fixed final items = _items.where((i) { if (i is _MissingShareeItem) { - return _getItemStatus(i.file.path, i.shareWith) == null; + return _getItemStatus(i.file.fdPath, i.shareWith) == null; } else if (i is _ExtraShareItem) { - return _getItemStatus(i.file.path, i.share.shareWith!) == null; + return _getItemStatus(i.file.fdPath, i.share.shareWith!) == null; } else { // ? return false; @@ -337,10 +336,10 @@ class _AlbumShareOutlierBrowserState extends State { setState(() { for (final i in items) { if (i is _MissingShareeItem) { - _setItemStatus(i.file.path, i.shareWith, _ItemStatus.processing); + _setItemStatus(i.file.fdPath, i.shareWith, _ItemStatus.processing); } else if (i is _ExtraShareItem) { _setItemStatus( - i.file.path, i.share.shareWith!, _ItemStatus.processing); + i.file.fdPath, i.share.shareWith!, _ItemStatus.processing); } } }); @@ -372,14 +371,14 @@ class _AlbumShareOutlierBrowserState extends State { Future _fixMissingSharee(_MissingShareeItem item) async { final shareRepo = ShareRepo(ShareRemoteDataSource()); setState(() { - _setItemStatus(item.file.path, item.shareWith, _ItemStatus.processing); + _setItemStatus(item.file.fdPath, item.shareWith, _ItemStatus.processing); }); try { await CreateUserShare(shareRepo)( widget.account, item.file, item.shareWith.raw); if (mounted) { setState(() { - _setItemStatus(item.file.path, item.shareWith, _ItemStatus.fixed); + _setItemStatus(item.file.fdPath, item.shareWith, _ItemStatus.fixed); }); } } catch (e, stackTrace) { @@ -391,7 +390,7 @@ class _AlbumShareOutlierBrowserState extends State { )); if (mounted) { setState(() { - _removeItemStatus(item.file.path, item.shareWith); + _removeItemStatus(item.file.fdPath, item.shareWith); }); } } @@ -401,14 +400,14 @@ class _AlbumShareOutlierBrowserState extends State { final shareRepo = ShareRepo(ShareRemoteDataSource()); setState(() { _setItemStatus( - item.file.path, item.share.shareWith!, _ItemStatus.processing); + item.file.fdPath, item.share.shareWith!, _ItemStatus.processing); }); try { await RemoveShare(shareRepo)(widget.account, item.share); if (mounted) { setState(() { _setItemStatus( - item.file.path, item.share.shareWith!, _ItemStatus.fixed); + item.file.fdPath, item.share.shareWith!, _ItemStatus.fixed); }); } } catch (e, stackTrace) { @@ -419,7 +418,7 @@ class _AlbumShareOutlierBrowserState extends State { )); if (mounted) { setState(() { - _removeItemStatus(item.file.path, item.share.shareWith!); + _removeItemStatus(item.file.fdPath, item.share.shareWith!); }); } } @@ -466,7 +465,7 @@ abstract class _ListItem { class _ExtraShareItem extends _ListItem { const _ExtraShareItem(this.file, this.share); - final File file; + final FileDescriptor file; final Share share; } @@ -474,7 +473,7 @@ class _MissingShareeItem extends _ListItem { const _MissingShareeItem( this.file, this.shareWith, this.shareWithDisplayName); - final File file; + final FileDescriptor file; final CiString shareWith; final String? shareWithDisplayName; } diff --git a/app/test/entity/album/data_source_test.dart b/app/test/entity/album/data_source_test.dart index ed500444..30124b8f 100644 --- a/app/test/entity/album/data_source_test.dart +++ b/app/test/entity/album/data_source_test.dart @@ -178,7 +178,8 @@ Future _dbUpdateExisting() async { AlbumFileItem( addedBy: "admin".toCi(), addedAt: DateTime.utc(2021, 2, 3, 4, 5, 6), - file: files[1], + file: files[1].toDescriptor(), + ownerId: "admin".toCi(), ), ], ), @@ -258,7 +259,8 @@ Future _dbUpdateShares() async { AlbumFileItem( addedBy: "admin".toCi(), addedAt: DateTime.utc(2021, 2, 3, 4, 5, 6), - file: files[1], + file: files[1].toDescriptor(), + ownerId: "admin".toCi(), ), ], ), @@ -320,7 +322,8 @@ Future _dbUpdateDeleteShares() async { AlbumFileItem( addedBy: "admin".toCi(), addedAt: DateTime.utc(2021, 2, 3, 4, 5, 6), - file: files[1], + file: files[1].toDescriptor(), + ownerId: "admin".toCi(), ), ], ), diff --git a/app/test/entity/album_test.dart b/app/test/entity/album_test.dart index fbb4fd5b..9f2c19ac 100644 --- a/app/test/entity/album_test.dart +++ b/app/test/entity/album_test.dart @@ -16,6 +16,7 @@ import 'package:test/test.dart'; import '../test_util.dart' as util; part 'album_test/album_upgrader_v8.dart'; +part 'album_test/album_upgrader_v9.dart'; void main() { group("Album", () { @@ -93,74 +94,7 @@ void main() { }); group("AlbumStaticProvider", () { - test("AlbumFileItem", () { - final json = { - "version": Album.version, - "lastUpdated": "2020-01-02T03:04:05.678901Z", - "name": "", - "provider": { - "type": "static", - "content": { - "items": [ - { - "type": "file", - "content": { - "file": { - "path": "remote.php/dav/files/admin/test1.jpg", - }, - }, - "addedBy": "admin", - "addedAt": "2020-01-02T03:04:05.678901Z", - }, - { - "type": "file", - "content": { - "file": { - "path": "remote.php/dav/files/admin/test2.jpg", - }, - }, - "addedBy": "admin", - "addedAt": "2020-01-02T03:04:05.678901Z", - }, - ], - }, - }, - "coverProvider": { - "type": "auto", - "content": {}, - }, - "sortProvider": { - "type": "null", - "content": {}, - }, - }; - expect( - Album.fromJson( - json, - upgraderFactory: const _NullAlbumUpgraderFactory(), - ), - Album( - lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), - name: "", - provider: AlbumStaticProvider( - items: [ - AlbumFileItem( - addedBy: "admin".toCi(), - addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), - file: File(path: "remote.php/dav/files/admin/test1.jpg"), - ), - AlbumFileItem( - addedBy: "admin".toCi(), - addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), - file: File(path: "remote.php/dav/files/admin/test2.jpg"), - ), - ], - ), - coverProvider: const AlbumAutoCoverProvider(), - sortProvider: const AlbumNullSortProvider(), - )); - }); - + test("AlbumFileItem", _fromJsonStaticProviderFileItem); test("AlbumLabelItem", () { final json = { "version": Album.version, @@ -455,69 +389,7 @@ void main() { }); group("AlbumStaticProvider", () { - test("AlbumFileItem", () { - final album = Album( - lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), - name: "", - provider: AlbumStaticProvider( - items: [ - AlbumFileItem( - addedBy: "admin".toCi(), - addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), - file: File(path: "remote.php/dav/files/admin/test1.jpg"), - ), - AlbumFileItem( - addedBy: "admin".toCi(), - addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), - file: File(path: "remote.php/dav/files/admin/test2.jpg"), - ), - ], - ), - coverProvider: const AlbumAutoCoverProvider(), - sortProvider: const AlbumNullSortProvider(), - ); - expect(album.toRemoteJson(), { - "version": Album.version, - "lastUpdated": "2020-01-02T03:04:05.678901Z", - "name": "", - "provider": { - "type": "static", - "content": { - "items": [ - { - "type": "file", - "content": { - "file": { - "path": "remote.php/dav/files/admin/test1.jpg", - }, - }, - "addedBy": "admin", - "addedAt": "2020-01-02T03:04:05.678901Z", - }, - { - "type": "file", - "content": { - "file": { - "path": "remote.php/dav/files/admin/test2.jpg", - }, - }, - "addedBy": "admin", - "addedAt": "2020-01-02T03:04:05.678901Z", - }, - ], - }, - }, - "coverProvider": { - "type": "auto", - "content": {}, - }, - "sortProvider": { - "type": "null", - "content": {}, - }, - }); - }); - + test("AlbumFileItem", _toRemoteJsonStaticProviderFileItem); test("AlbumLabelItem", () { final album = Album( lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), @@ -751,69 +623,7 @@ void main() { }); group("AlbumStaticProvider", () { - test("AlbumFileItem", () { - final album = Album( - lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), - name: "", - provider: AlbumStaticProvider( - items: [ - AlbumFileItem( - addedBy: "admin".toCi(), - addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), - file: File(path: "remote.php/dav/files/admin/test1.jpg"), - ), - AlbumFileItem( - addedBy: "admin".toCi(), - addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), - file: File(path: "remote.php/dav/files/admin/test2.jpg"), - ), - ], - ), - coverProvider: const AlbumAutoCoverProvider(), - sortProvider: const AlbumNullSortProvider(), - ); - expect(album.toAppDbJson(), { - "version": Album.version, - "lastUpdated": "2020-01-02T03:04:05.678901Z", - "name": "", - "provider": { - "type": "static", - "content": { - "items": [ - { - "type": "file", - "content": { - "file": { - "path": "remote.php/dav/files/admin/test1.jpg", - }, - }, - "addedBy": "admin", - "addedAt": "2020-01-02T03:04:05.678901Z", - }, - { - "type": "file", - "content": { - "file": { - "path": "remote.php/dav/files/admin/test2.jpg", - }, - }, - "addedBy": "admin", - "addedAt": "2020-01-02T03:04:05.678901Z", - }, - ], - }, - }, - "coverProvider": { - "type": "auto", - "content": {}, - }, - "sortProvider": { - "type": "null", - "content": {}, - }, - }); - }); - + test("AlbumFileItem", _toDbJsonStaticProviderFileItem); test("AlbumLabelItem", () { final album = Album( lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), @@ -1829,6 +1639,304 @@ void main() { }); }); }); + + group("AlbumUpgraderV9", () { + group("doJson", () { + test("non static provider", _upgradeV9JsonNonStatic); + group("static provider", () { + test("normal", _upgradeV9JsonStaticNormal); + test("w/o ownerId", _upgradeV9JsonStaticNoOwnerId); + test("other ownerId", _upgradeV9JsonStaticOtherOwnerId); + }); + }); + group("doDb", () { + test("non static provider", _upgradeV9DbNonStatic); + group("static provider", () { + test("normal", _upgradeV9DbStaticNormal); + test("w/o ownerId", _upgradeV9DbStaticNoOwnerId); + test("other ownerId", _upgradeV9DbStaticOtherOwnerId); + }); + }); + }); + }); +} + +void _fromJsonStaticProviderFileItem() { + final json = { + "version": Album.version, + "lastUpdated": "2020-01-02T03:04:05.678901Z", + "name": "", + "provider": { + "type": "static", + "content": { + "items": [ + { + "type": "file", + "content": { + "file": { + "fdPath": "remote.php/dav/files/admin/test1.jpg", + "fdId": 1, + "fdMime": "image/jpeg", + "fdIsArchived": false, + "fdIsFavorite": false, + "fdDateTime": "2020-01-02T03:04:05.678901Z", + }, + "ownerId": "admin", + }, + "addedBy": "admin", + "addedAt": "2020-01-02T03:04:05.678901Z", + }, + { + "type": "file", + "content": { + "file": { + "fdPath": "remote.php/dav/files/admin/test2.jpg", + "fdId": 2, + "fdMime": "image/jpeg", + "fdIsArchived": false, + "fdIsFavorite": false, + "fdDateTime": "2020-01-01T03:04:05.678901Z", + }, + "ownerId": "admin", + }, + "addedBy": "admin", + "addedAt": "2020-01-02T03:04:05.678901Z", + }, + ], + }, + }, + "coverProvider": { + "type": "auto", + "content": {}, + }, + "sortProvider": { + "type": "null", + "content": {}, + }, + }; + expect( + Album.fromJson( + json, + upgraderFactory: const _NullAlbumUpgraderFactory(), + ), + Album( + lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), + name: "", + provider: AlbumStaticProvider( + items: [ + AlbumFileItem( + addedBy: "admin".toCi(), + addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), + file: FileDescriptor( + fdPath: "remote.php/dav/files/admin/test1.jpg", + fdId: 1, + fdMime: "image/jpeg", + fdIsArchived: false, + fdIsFavorite: false, + fdDateTime: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), + ), + ownerId: "admin".toCi(), + ), + AlbumFileItem( + addedBy: "admin".toCi(), + addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), + file: FileDescriptor( + fdPath: "remote.php/dav/files/admin/test2.jpg", + fdId: 2, + fdMime: "image/jpeg", + fdIsArchived: false, + fdIsFavorite: false, + fdDateTime: DateTime.utc(2020, 1, 1, 3, 4, 5, 678, 901), + ), + ownerId: "admin".toCi(), + ), + ], + ), + coverProvider: const AlbumAutoCoverProvider(), + sortProvider: const AlbumNullSortProvider(), + ), + ); +} + +void _toRemoteJsonStaticProviderFileItem() { + final album = Album( + lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), + name: "", + provider: AlbumStaticProvider( + items: [ + AlbumFileItem( + addedBy: "admin".toCi(), + addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), + file: FileDescriptor( + fdPath: "remote.php/dav/files/admin/test1.jpg", + fdId: 1, + fdMime: "image/jpeg", + fdIsArchived: false, + fdIsFavorite: false, + fdDateTime: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), + ), + ownerId: "admin".toCi(), + ), + AlbumFileItem( + addedBy: "admin".toCi(), + addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), + file: FileDescriptor( + fdPath: "remote.php/dav/files/admin/test2.jpg", + fdId: 2, + fdMime: "image/jpeg", + fdIsArchived: false, + fdIsFavorite: false, + fdDateTime: DateTime.utc(2020, 1, 1, 3, 4, 5, 678, 901), + ), + ownerId: "admin".toCi(), + ), + ], + ), + coverProvider: const AlbumAutoCoverProvider(), + sortProvider: const AlbumNullSortProvider(), + ); + expect(album.toRemoteJson(), { + "version": Album.version, + "lastUpdated": "2020-01-02T03:04:05.678901Z", + "name": "", + "provider": { + "type": "static", + "content": { + "items": [ + { + "type": "file", + "content": { + "file": { + "fdPath": "remote.php/dav/files/admin/test1.jpg", + "fdId": 1, + "fdMime": "image/jpeg", + "fdIsArchived": false, + "fdIsFavorite": false, + "fdDateTime": "2020-01-02T03:04:05.678901Z", + }, + "ownerId": "admin", + }, + "addedBy": "admin", + "addedAt": "2020-01-02T03:04:05.678901Z", + }, + { + "type": "file", + "content": { + "file": { + "fdPath": "remote.php/dav/files/admin/test2.jpg", + "fdId": 2, + "fdMime": "image/jpeg", + "fdIsArchived": false, + "fdIsFavorite": false, + "fdDateTime": "2020-01-01T03:04:05.678901Z", + }, + "ownerId": "admin", + }, + "addedBy": "admin", + "addedAt": "2020-01-02T03:04:05.678901Z", + }, + ], + }, + }, + "coverProvider": { + "type": "auto", + "content": {}, + }, + "sortProvider": { + "type": "null", + "content": {}, + }, + }); +} + +void _toDbJsonStaticProviderFileItem() { + final album = Album( + lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), + name: "", + provider: AlbumStaticProvider( + items: [ + AlbumFileItem( + addedBy: "admin".toCi(), + addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), + file: FileDescriptor( + fdPath: "remote.php/dav/files/admin/test1.jpg", + fdId: 1, + fdMime: "image/jpeg", + fdIsArchived: false, + fdIsFavorite: false, + fdDateTime: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), + ), + ownerId: "admin".toCi(), + ), + AlbumFileItem( + addedBy: "admin".toCi(), + addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), + file: FileDescriptor( + fdPath: "remote.php/dav/files/admin/test2.jpg", + fdId: 2, + fdMime: "image/jpeg", + fdIsArchived: false, + fdIsFavorite: false, + fdDateTime: DateTime.utc(2020, 1, 1, 3, 4, 5, 678, 901), + ), + ownerId: "admin".toCi(), + ), + ], + ), + coverProvider: const AlbumAutoCoverProvider(), + sortProvider: const AlbumNullSortProvider(), + ); + expect(album.toAppDbJson(), { + "version": Album.version, + "lastUpdated": "2020-01-02T03:04:05.678901Z", + "name": "", + "provider": { + "type": "static", + "content": { + "items": [ + { + "type": "file", + "content": { + "file": { + "fdPath": "remote.php/dav/files/admin/test1.jpg", + "fdId": 1, + "fdMime": "image/jpeg", + "fdIsArchived": false, + "fdIsFavorite": false, + "fdDateTime": "2020-01-02T03:04:05.678901Z", + }, + "ownerId": "admin", + }, + "addedBy": "admin", + "addedAt": "2020-01-02T03:04:05.678901Z", + }, + { + "type": "file", + "content": { + "file": { + "fdPath": "remote.php/dav/files/admin/test2.jpg", + "fdId": 2, + "fdMime": "image/jpeg", + "fdIsArchived": false, + "fdIsFavorite": false, + "fdDateTime": "2020-01-01T03:04:05.678901Z", + }, + "ownerId": "admin", + }, + "addedBy": "admin", + "addedAt": "2020-01-02T03:04:05.678901Z", + }, + ], + }, + }, + "coverProvider": { + "type": "auto", + "content": {}, + }, + "sortProvider": { + "type": "null", + "content": {}, + }, }); } @@ -1958,19 +2066,21 @@ class _NullAlbumUpgraderFactory extends AlbumUpgraderFactory { const _NullAlbumUpgraderFactory(); @override - buildV1() => null; + AlbumUpgraderV1? buildV1() => null; @override - buildV2() => null; + AlbumUpgraderV2? buildV2() => null; @override - buildV3() => null; + AlbumUpgraderV3? buildV3() => null; @override - buildV4() => null; + AlbumUpgraderV4? buildV4() => null; @override - buildV5() => null; + AlbumUpgraderV5? buildV5() => null; @override - buildV6() => null; + AlbumUpgraderV6? buildV6() => null; @override - buildV7() => null; + AlbumUpgraderV7? buildV7() => null; @override AlbumUpgraderV8? buildV8() => null; + @override + AlbumUpgraderV9? buildV9() => null; } diff --git a/app/test/entity/album_test/album_upgrader_v9.dart b/app/test/entity/album_test/album_upgrader_v9.dart new file mode 100644 index 00000000..98b311e3 --- /dev/null +++ b/app/test/entity/album_test/album_upgrader_v9.dart @@ -0,0 +1,498 @@ +part of '../album_test.dart'; + +void _upgradeV9JsonNonStatic() { + final account = util.buildAccount(); + final json = { + "version": 9, + "lastUpdated": "2020-01-02T03:04:05.678901Z", + "provider": { + "type": "tag", + "content": { + "tags": [], + }, + }, + "coverProvider": { + "type": "auto", + "content": {}, + }, + "sortProvider": { + "type": "null", + "content": {}, + }, + "albumFile": { + "path": "remote.php/dav/files/admin/test1.json", + }, + }; + expect(AlbumUpgraderV9(account: account).doJson(json), json); +} + +void _upgradeV9JsonStaticNormal() { + final account = util.buildAccount(); + final json = { + "version": 9, + "lastUpdated": "2020-01-02T03:04:05.678901Z", + "provider": { + "type": "static", + "content": { + "items": [ + { + "type": "file", + "content": { + "file": { + "path": "remote.php/dav/files/admin/test1.jpg", + "fileId": 1, + "contentLength": 12345, + "contentType": "image/jpeg", + "lastModified": "2020-01-02T03:04:05.678901Z", + "ownerId": "admin", + }, + }, + }, + ], + }, + }, + "coverProvider": { + "type": "auto", + "content": {}, + }, + "sortProvider": { + "type": "null", + "content": {}, + }, + "albumFile": { + "path": "remote.php/dav/files/admin/test1.json", + }, + }; + expect( + AlbumUpgraderV9(account: account).doJson(json), + { + "version": 9, + "lastUpdated": "2020-01-02T03:04:05.678901Z", + "provider": { + "type": "static", + "content": { + "items": [ + { + "type": "file", + "content": { + "file": { + "fdPath": "remote.php/dav/files/admin/test1.jpg", + "fdId": 1, + "fdMime": "image/jpeg", + "fdIsArchived": false, + "fdIsFavorite": false, + "fdDateTime": "2020-01-02T03:04:05.678901Z", + }, + "ownerId": "admin", + }, + }, + ], + }, + }, + "coverProvider": { + "type": "auto", + "content": {}, + }, + "sortProvider": { + "type": "null", + "content": {}, + }, + "albumFile": { + "path": "remote.php/dav/files/admin/test1.json", + }, + }, + ); +} + +void _upgradeV9JsonStaticNoOwnerId() { + final account = util.buildAccount(); + final json = { + "version": 9, + "lastUpdated": "2020-01-02T03:04:05.678901Z", + "provider": { + "type": "static", + "content": { + "items": [ + { + "type": "file", + "content": { + "file": { + "path": "remote.php/dav/files/admin/test1.jpg", + "fileId": 1, + "contentLength": 12345, + "contentType": "image/jpeg", + "lastModified": "2020-01-02T03:04:05.678901Z", + }, + }, + }, + ], + }, + }, + "coverProvider": { + "type": "auto", + "content": {}, + }, + "sortProvider": { + "type": "null", + "content": {}, + }, + "albumFile": { + "path": "remote.php/dav/files/admin/test1.json", + }, + }; + expect( + AlbumUpgraderV9(account: account).doJson(json), + { + "version": 9, + "lastUpdated": "2020-01-02T03:04:05.678901Z", + "provider": { + "type": "static", + "content": { + "items": [ + { + "type": "file", + "content": { + "file": { + "fdPath": "remote.php/dav/files/admin/test1.jpg", + "fdId": 1, + "fdMime": "image/jpeg", + "fdIsArchived": false, + "fdIsFavorite": false, + "fdDateTime": "2020-01-02T03:04:05.678901Z", + }, + "ownerId": "admin", + }, + }, + ], + }, + }, + "coverProvider": { + "type": "auto", + "content": {}, + }, + "sortProvider": { + "type": "null", + "content": {}, + }, + "albumFile": { + "path": "remote.php/dav/files/admin/test1.json", + }, + }, + ); +} + +void _upgradeV9JsonStaticOtherOwnerId() { + final account = util.buildAccount(); + final json = { + "version": 9, + "lastUpdated": "2020-01-02T03:04:05.678901Z", + "provider": { + "type": "static", + "content": { + "items": [ + { + "type": "file", + "content": { + "file": { + "path": "remote.php/dav/files/admin/test1.jpg", + "fileId": 1, + "contentLength": 12345, + "contentType": "image/jpeg", + "lastModified": "2020-01-02T03:04:05.678901Z", + "ownerId": "user1", + }, + }, + }, + ], + }, + }, + "coverProvider": { + "type": "auto", + "content": {}, + }, + "sortProvider": { + "type": "null", + "content": {}, + }, + "albumFile": { + "path": "remote.php/dav/files/admin/test1.json", + }, + }; + expect( + AlbumUpgraderV9(account: account).doJson(json), + { + "version": 9, + "lastUpdated": "2020-01-02T03:04:05.678901Z", + "provider": { + "type": "static", + "content": { + "items": [ + { + "type": "file", + "content": { + "file": { + "fdPath": "remote.php/dav/files/admin/test1.jpg", + "fdId": 1, + "fdMime": "image/jpeg", + "fdIsArchived": false, + "fdIsFavorite": false, + "fdDateTime": "2020-01-02T03:04:05.678901Z", + }, + "ownerId": "user1", + }, + }, + ], + }, + }, + "coverProvider": { + "type": "auto", + "content": {}, + }, + "sortProvider": { + "type": "null", + "content": {}, + }, + "albumFile": { + "path": "remote.php/dav/files/admin/test1.json", + }, + }, + ); +} + +void _upgradeV9DbNonStatic() { + final account = util.buildAccount(); + final dbObj = DbAlbum( + fileId: 1, + fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5", + version: 9, + lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5), + name: "test1", + providerType: "tag", + providerContent: {"tags": []}, + coverProviderType: "auto", + coverProviderContent: {}, + sortProviderType: "null", + sortProviderContent: {}, + shares: [], + ); + expect( + AlbumUpgraderV9(account: account).doDb(dbObj), + DbAlbum( + fileId: 1, + fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5", + version: 9, + lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5), + name: "test1", + providerType: "tag", + providerContent: {"tags": []}, + coverProviderType: "auto", + coverProviderContent: {}, + sortProviderType: "null", + sortProviderContent: {}, + shares: [], + ), + ); +} + +void _upgradeV9DbStaticNormal() { + final account = util.buildAccount(); + final dbObj = DbAlbum( + fileId: 1, + fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5", + version: 9, + lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5), + name: "test1", + providerType: "static", + providerContent: { + "items": [ + { + "type": "file", + "content": { + "file": { + "path": "remote.php/dav/files/admin/test1.jpg", + "fileId": 2, + "contentLength": 12345, + "contentType": "image/jpeg", + "lastModified": "2020-01-02T03:04:05.678901Z", + "ownerId": "admin", + }, + }, + }, + ], + }, + coverProviderType: "auto", + coverProviderContent: {}, + sortProviderType: "null", + sortProviderContent: {}, + shares: [], + ); + expect( + AlbumUpgraderV9(account: account).doDb(dbObj), + DbAlbum( + fileId: 1, + fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5", + version: 9, + lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5), + name: "test1", + providerType: "static", + providerContent: { + "items": [ + { + "type": "file", + "content": { + "file": { + "fdPath": "remote.php/dav/files/admin/test1.jpg", + "fdId": 2, + "fdMime": "image/jpeg", + "fdIsArchived": false, + "fdIsFavorite": false, + "fdDateTime": "2020-01-02T03:04:05.678901Z", + }, + "ownerId": "admin", + }, + }, + ] + }, + coverProviderType: "auto", + coverProviderContent: {}, + sortProviderType: "null", + sortProviderContent: {}, + shares: [], + ), + ); +} + +void _upgradeV9DbStaticNoOwnerId() { + final account = util.buildAccount(); + final dbObj = DbAlbum( + fileId: 1, + fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5", + version: 9, + lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5), + name: "test1", + providerType: "static", + providerContent: { + "items": [ + { + "type": "file", + "content": { + "file": { + "path": "remote.php/dav/files/admin/test1.jpg", + "fileId": 2, + "contentLength": 12345, + "contentType": "image/jpeg", + "lastModified": "2020-01-02T03:04:05.678901Z", + }, + }, + }, + ], + }, + coverProviderType: "auto", + coverProviderContent: {}, + sortProviderType: "null", + sortProviderContent: {}, + shares: [], + ); + expect( + AlbumUpgraderV9(account: account).doDb(dbObj), + DbAlbum( + fileId: 1, + fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5", + version: 9, + lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5), + name: "test1", + providerType: "static", + providerContent: { + "items": [ + { + "type": "file", + "content": { + "file": { + "fdPath": "remote.php/dav/files/admin/test1.jpg", + "fdId": 2, + "fdMime": "image/jpeg", + "fdIsArchived": false, + "fdIsFavorite": false, + "fdDateTime": "2020-01-02T03:04:05.678901Z", + }, + "ownerId": "admin", + }, + }, + ] + }, + coverProviderType: "auto", + coverProviderContent: {}, + sortProviderType: "null", + sortProviderContent: {}, + shares: [], + ), + ); +} + +void _upgradeV9DbStaticOtherOwnerId() { + final account = util.buildAccount(); + final dbObj = DbAlbum( + fileId: 1, + fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5", + version: 9, + lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5), + name: "test1", + providerType: "static", + providerContent: { + "items": [ + { + "type": "file", + "content": { + "file": { + "path": "remote.php/dav/files/admin/test1.jpg", + "fileId": 2, + "contentLength": 12345, + "contentType": "image/jpeg", + "lastModified": "2020-01-02T03:04:05.678901Z", + "ownerId": "user1", + }, + }, + }, + ], + }, + coverProviderType: "auto", + coverProviderContent: {}, + sortProviderType: "null", + sortProviderContent: {}, + shares: [], + ); + expect( + AlbumUpgraderV9(account: account).doDb(dbObj), + DbAlbum( + fileId: 1, + fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5", + version: 9, + lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5), + name: "test1", + providerType: "static", + providerContent: { + "items": [ + { + "type": "file", + "content": { + "file": { + "fdPath": "remote.php/dav/files/admin/test1.jpg", + "fdId": 2, + "fdMime": "image/jpeg", + "fdIsArchived": false, + "fdIsFavorite": false, + "fdDateTime": "2020-01-02T03:04:05.678901Z", + }, + "ownerId": "user1", + }, + }, + ] + }, + coverProviderType: "auto", + coverProviderContent: {}, + sortProviderType: "null", + sortProviderContent: {}, + shares: [], + ), + ); +} diff --git a/app/test/mock_type.dart b/app/test/mock_type.dart index b7a94953..2e49bbd3 100644 --- a/app/test/mock_type.dart +++ b/app/test/mock_type.dart @@ -57,13 +57,13 @@ class MockAlbumMemoryRepo extends MockAlbumRepo { ]) : albums = initialData.map((a) => a.copyWith()).toList(); @override - get(Account account, File albumFile) async { + Future get(Account account, File albumFile) async { return albums.firstWhere((element) => element.albumFile?.compareServerIdentity(albumFile) == true); } @override - getAll(Account account, List albumFiles) async* { + Stream getAll(Account account, List albumFiles) async* { final results = await waitOr( albumFiles.map((f) => get(account, f)), (error, stackTrace) => ExceptionEvent(error, stackTrace), @@ -74,7 +74,7 @@ class MockAlbumMemoryRepo extends MockAlbumRepo { } @override - update(Account account, Album album) async { + Future update(Account account, Album album) async { final i = albums.indexWhere((element) => element.albumFile?.compareServerIdentity(album.albumFile!) == true); albums[i] = album; @@ -373,7 +373,7 @@ class MockFileMemoryRepo2 extends BasicFileRepo { /// Mock of [ShareRepo] where all methods will throw UnimplementedError class MockShareRepo implements ShareRepo { @override - Future create(Account account, File file, String shareWith) { + Future create(Account account, FileDescriptor file, String shareWith) { throw UnimplementedError(); } @@ -393,7 +393,7 @@ class MockShareRepo implements ShareRepo { @override Future> list( Account account, - File file, { + FileDescriptor file, { bool? isIncludeReshare, }) { throw UnimplementedError(); @@ -429,13 +429,13 @@ class MockShareMemoryRepo extends MockShareRepo { } @override - list( + Future> list( Account account, - File file, { + FileDescriptor file, { bool? isIncludeReshare, }) async { return shares.where((s) { - if (s.itemSource != file.fileId) { + if (s.itemSource != file.fdId) { return false; } else if (isIncludeReshare == true || s.uidOwner == account.userId) { return true; @@ -446,18 +446,19 @@ class MockShareMemoryRepo extends MockShareRepo { } @override - create(Account account, File file, String shareWith) async { + Future create( + Account account, FileDescriptor file, String shareWith) async { final share = Share( id: (_id++).toString(), shareType: ShareType.user, stime: DateTime.utc(2020, 1, 2, 3, 4, 5), uidOwner: account.userId, displaynameOwner: account.username2, - uidFileOwner: file.ownerId!, + uidFileOwner: account.userId, path: file.strippedPath, itemType: ShareItemType.file, - mimeType: file.contentType ?? "", - itemSource: file.fileId!, + mimeType: file.fdMime ?? "", + itemSource: file.fdId, shareWith: shareWith.toCi(), shareWithDisplayName: shareWith, ); diff --git a/app/test/test_util.dart b/app/test/test_util.dart index e01db815..0d160d14 100644 --- a/app/test/test_util.dart +++ b/app/test/test_util.dart @@ -201,8 +201,7 @@ class AlbumBuilder { Album build() { final latestFileItem = items .whereType() - .stableSorted( - (a, b) => a.file.lastModified!.compareTo(b.file.lastModified!)) + .stableSorted((a, b) => a.file.fdDateTime.compareTo(b.file.fdDateTime)) .reversed .firstOrNull; return Album( @@ -210,7 +209,7 @@ class AlbumBuilder { name: name, provider: AlbumStaticProvider( items: items, - latestItemTime: latestFileItem?.file.lastModified, + latestItemTime: latestFileItem?.file.fdDateTime, ), coverProvider: cover == null ? AlbumAutoCoverProvider(coverFile: latestFileItem?.file) @@ -233,15 +232,17 @@ class AlbumBuilder { /// If [isCover] is true, the coverProvider of the album will become /// [AlbumManualCoverProvider] void addFileItem( - File file, { + FileDescriptor file, { String addedBy = "admin", DateTime? addedAt, bool isCover = false, + String? ownerId, }) { final fileItem = AlbumFileItem( file: file, addedBy: addedBy.toCi(), - addedAt: addedAt ?? file.lastModified!, + addedAt: addedAt ?? file.fdDateTime, + ownerId: ownerId?.toCi() ?? file.as()?.ownerId ?? addedBy.toCi(), ); items.add(fileItem); if (isCover) { @@ -272,7 +273,7 @@ class AlbumBuilder { final String ownerId; final items = []; - File? cover; + FileDescriptor? cover; final shares = []; } diff --git a/app/test/use_case/add_file_to_album_test.dart b/app/test/use_case/add_file_to_album_test.dart index d48868ba..867a1cc1 100644 --- a/app/test/use_case/add_file_to_album_test.dart +++ b/app/test/use_case/add_file_to_album_test.dart @@ -76,14 +76,13 @@ Future _addFile() async { AlbumFileItem( addedBy: "admin".toCi(), addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5), - file: file, - ).minimize(), + file: file.toDescriptor(), + ownerId: "admin".toCi(), + ), ], latestItemTime: DateTime.utc(2020, 1, 2, 3, 4, 5), ), - coverProvider: AlbumAutoCoverProvider( - coverFile: file, - ), + coverProvider: AlbumAutoCoverProvider(coverFile: file.toDescriptor()), sortProvider: const AlbumNullSortProvider(), albumFile: albumFile, ), @@ -104,13 +103,13 @@ Future _addExistingFile() async { lastModified: DateTime.utc(2019, 1, 2, 3, 4, 5), )) .build(); + final oldFile = files[0].toDescriptor(); final album = (util.AlbumBuilder() ..addFileItem( - files[0], + oldFile, addedAt: clock.now().toUtc(), )) .build(); - final oldFile = files[0]; final newFile = files[0].copyWith(); final albumFile = album.albumFile!; final c = DiContainer( @@ -142,12 +141,15 @@ Future _addExistingFile() async { AlbumFileItem( addedBy: "admin".toCi(), addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5), - file: files[0], + file: files[0].toDescriptor(), + ownerId: "admin".toCi(), ), ], latestItemTime: DateTime.utc(2019, 1, 2, 3, 4, 5), ), - coverProvider: AlbumAutoCoverProvider(coverFile: files[0]), + coverProvider: AlbumAutoCoverProvider( + coverFile: files[0].toDescriptor(), + ), sortProvider: const AlbumNullSortProvider(), albumFile: albumFile, ), @@ -156,23 +158,27 @@ Future _addExistingFile() async { // when there's a conflict, it's guaranteed that the original file in the // album is kept and the incoming file dropped expect( - identical( - AlbumStaticProvider.of(c.albumMemoryRepo.albums[0]) - .items - .whereType() - .first - .file, - oldFile), - true); + identical( + AlbumStaticProvider.of(c.albumMemoryRepo.albums[0]) + .items + .whereType() + .first + .file, + oldFile, + ), + true, + ); expect( - identical( - AlbumStaticProvider.of(c.albumMemoryRepo.albums[0]) - .items - .whereType() - .first - .file, - newFile), - false); + identical( + AlbumStaticProvider.of(c.albumMemoryRepo.albums[0]) + .items + .whereType() + .first + .file, + newFile, + ), + false, + ); }); } @@ -189,7 +195,8 @@ Future _addExistingSharedFile() async { final user1Files = [ files[0].copyWith(path: "remote.php/dav/files/user1/test1.jpg") ]; - final album = (util.AlbumBuilder()..addFileItem(files[0])).build(); + final album = + (util.AlbumBuilder()..addFileItem(files[0].toDescriptor())).build(); final albumFile = album.albumFile!; final c = DiContainer( fileRepo: MockFileMemoryRepo(), @@ -222,12 +229,15 @@ Future _addExistingSharedFile() async { AlbumFileItem( addedBy: "admin".toCi(), addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5), - file: files[0], + file: files[0].toDescriptor(), + ownerId: "admin".toCi(), ), ], latestItemTime: DateTime.utc(2020, 1, 2, 3, 4, 5), ), - coverProvider: AlbumAutoCoverProvider(coverFile: files[0]), + coverProvider: AlbumAutoCoverProvider( + coverFile: files[0].toDescriptor(), + ), sortProvider: const AlbumNullSortProvider(), albumFile: albumFile, ), diff --git a/app/test/use_case/remove_from_album_test.dart b/app/test/use_case/remove_from_album_test.dart index 45fe8f2e..808d0165 100644 --- a/app/test/use_case/remove_from_album_test.dart +++ b/app/test/use_case/remove_from_album_test.dart @@ -6,6 +6,7 @@ import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album/cover_provider.dart'; import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/sort_provider.dart'; +import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/use_case/album/remove_from_album.dart'; import 'package:np_common/or_null.dart'; import 'package:np_db_sqlite/np_db_sqlite_compat.dart' as compat; @@ -45,7 +46,8 @@ Future _removeLastFile() async { final account = util.buildAccount(); final files = (util.FilesBuilder(initialFileId: 1)..addJpeg("admin/test1.jpg")).build(); - final album = (util.AlbumBuilder()..addFileItem(files[0])).build(); + final album = + (util.AlbumBuilder()..addFileItem(files[0].toDescriptor())).build(); final file1 = files[0]; final fileItem1 = util.AlbumBuilder.fileItemsOf(album)[0]; final albumFile = album.albumFile!; @@ -94,9 +96,9 @@ Future _remove1OfNFiles() async { ..addJpeg("admin/test3.jpg")) .build(); final album = (util.AlbumBuilder() - ..addFileItem(files[0]) - ..addFileItem(files[1]) - ..addFileItem(files[2])) + ..addFileItem(files[0].toDescriptor()) + ..addFileItem(files[1].toDescriptor()) + ..addFileItem(files[2].toDescriptor())) .build(); final fileItems = util.AlbumBuilder.fileItemsOf(album); final albumFile = album.albumFile!; @@ -126,10 +128,12 @@ Future _remove1OfNFiles() async { lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5), name: "test", provider: AlbumStaticProvider( - items: [fileItems[1].minimize(), fileItems[2].minimize()], + items: [fileItems[1], fileItems[2]], latestItemTime: files[2].lastModified, ), - coverProvider: AlbumAutoCoverProvider(coverFile: files[2]), + coverProvider: AlbumAutoCoverProvider( + coverFile: files[2].toDescriptor(), + ), sortProvider: const AlbumNullSortProvider(), albumFile: albumFile, ), @@ -151,9 +155,9 @@ Future _removeLatestOfNFiles() async { lastModified: DateTime.utc(2020, 1, 2, 3, 4, 6))) .build(); final album = (util.AlbumBuilder() - ..addFileItem(files[0]) - ..addFileItem(files[1]) - ..addFileItem(files[2])) + ..addFileItem(files[0].toDescriptor()) + ..addFileItem(files[1].toDescriptor()) + ..addFileItem(files[2].toDescriptor())) .build(); final fileItems = util.AlbumBuilder.fileItemsOf(album); final albumFile = album.albumFile!; @@ -183,10 +187,12 @@ Future _removeLatestOfNFiles() async { lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5), name: "test", provider: AlbumStaticProvider( - items: [fileItems[1].minimize(), fileItems[2].minimize()], + items: [fileItems[1], fileItems[2]], latestItemTime: files[1].lastModified, ), - coverProvider: AlbumAutoCoverProvider(coverFile: files[1]), + coverProvider: AlbumAutoCoverProvider( + coverFile: files[1].toDescriptor(), + ), sortProvider: const AlbumNullSortProvider(), albumFile: albumFile, ), @@ -205,9 +211,9 @@ Future _removeManualCoverFile() async { ..addJpeg("admin/test3.jpg")) .build(); final album = (util.AlbumBuilder() - ..addFileItem(files[0], isCover: true) - ..addFileItem(files[1]) - ..addFileItem(files[2])) + ..addFileItem(files[0].toDescriptor(), isCover: true) + ..addFileItem(files[1].toDescriptor()) + ..addFileItem(files[2].toDescriptor())) .build(); final fileItems = util.AlbumBuilder.fileItemsOf(album); final albumFile = album.albumFile!; @@ -237,10 +243,12 @@ Future _removeManualCoverFile() async { lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5), name: "test", provider: AlbumStaticProvider( - items: [fileItems[1].minimize(), fileItems[2].minimize()], + items: [fileItems[1], fileItems[2]], latestItemTime: files[2].lastModified, ), - coverProvider: AlbumAutoCoverProvider(coverFile: files[2]), + coverProvider: AlbumAutoCoverProvider( + coverFile: files[2].toDescriptor(), + ), sortProvider: const AlbumNullSortProvider(), albumFile: albumFile, ), @@ -256,7 +264,7 @@ Future _removeFromSharedAlbumOwned() async { final files = (util.FilesBuilder(initialFileId: 1)..addJpeg("admin/test1.jpg")).build(); final album = (util.AlbumBuilder() - ..addFileItem(files[0]) + ..addFileItem(files[0].toDescriptor()) ..addShare("user1")) .build(); final file1 = files[0]; @@ -297,7 +305,7 @@ Future _removeFromSharedAlbumOwnedWithOtherShare() async { ..addJpeg("user1/test1.jpg", ownerId: "user1")) .build(); final album = (util.AlbumBuilder() - ..addFileItem(files[0], addedBy: "user1") + ..addFileItem(files[0].toDescriptor(), addedBy: "user1") ..addShare("user1") ..addShare("user2")) .build(); @@ -349,7 +357,7 @@ Future _removeFromSharedAlbumOwnedLeaveExtraShare() async { final files = (util.FilesBuilder(initialFileId: 1)..addJpeg("admin/test1.jpg")).build(); final album = (util.AlbumBuilder() - ..addFileItem(files[0]) + ..addFileItem(files[0].toDescriptor()) ..addShare("user1")) .build(); final file1 = files[0]; @@ -392,12 +400,12 @@ Future _removeFromSharedAlbumOwnedFileInOtherAlbum() async { final files = (util.FilesBuilder(initialFileId: 2)..addJpeg("admin/test1.jpg")).build(); final album1 = (util.AlbumBuilder() - ..addFileItem(files[0]) + ..addFileItem(files[0].toDescriptor()) ..addShare("user1") ..addShare("user2")) .build(); final album2 = (util.AlbumBuilder.ofId(albumId: 1) - ..addFileItem(files[0]) + ..addFileItem(files[0].toDescriptor()) ..addShare("user1")) .build(); final album1fileItems = util.AlbumBuilder.fileItemsOf(album1); @@ -440,7 +448,7 @@ Future _removeFromSharedAlbumNotOwned() async { final files = (util.FilesBuilder(initialFileId: 1)..addJpeg("admin/test1.jpg")).build(); final album = (util.AlbumBuilder(ownerId: "user1") - ..addFileItem(files[0]) + ..addFileItem(files[0].toDescriptor()) ..addShare("admin") ..addShare("user2")) .build(); @@ -489,7 +497,7 @@ Future _removeFromSharedAlbumNotOwnedWithOwnerShare() async { final files = (util.FilesBuilder(initialFileId: 1)..addJpeg("admin/test1.jpg")).build(); final album = (util.AlbumBuilder(ownerId: "user1") - ..addFileItem(files[0]) + ..addFileItem(files[0].toDescriptor()) ..addShare("admin") ..addShare("user2")) .build(); diff --git a/app/test/use_case/share_album_with_user_test.dart b/app/test/use_case/share_album_with_user_test.dart index c6dc62ce..9b80301c 100644 --- a/app/test/use_case/share_album_with_user_test.dart +++ b/app/test/use_case/share_album_with_user_test.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:event_bus/event_bus.dart'; import 'package:kiwi/kiwi.dart'; import 'package:nc_photos/use_case/album/share_album_with_user.dart'; @@ -97,37 +98,30 @@ Future _shareWithFile() async { /// Expect: share (admin -> user1) added to album's shares list; /// new shares (admin -> user1) are created for the album json Future _shareWithFileOwnedByUser() async { - final account = util.buildAccount(); - final files = (util.FilesBuilder(initialFileId: 1) - ..addJpeg("admin/test1.jpg", ownerId: "user1")) - .build(); - final album = (util.AlbumBuilder()..addFileItem(files[0])).build(); - final albumFile = album.albumFile!; - final albumRepo = MockAlbumMemoryRepo([album]); - final shareRepo = MockShareMemoryRepo(); + await withClock(Clock.fixed(DateTime.utc(2020, 1, 2, 3, 4, 5)), () async { + final account = util.buildAccount(); + final files = (util.FilesBuilder(initialFileId: 1) + ..addJpeg("admin/test1.jpg", ownerId: "user1")) + .build(); + final album = (util.AlbumBuilder()..addFileItem(files[0])).build(); + final albumFile = album.albumFile!; + final albumRepo = MockAlbumMemoryRepo([album]); + final shareRepo = MockShareMemoryRepo(); - await ShareAlbumWithUser(shareRepo, albumRepo)( - account, - albumRepo.findAlbumByPath(albumFile.path), - util.buildSharee(shareWith: "user1".toCi()), - ); - expect( - albumRepo - .findAlbumByPath(albumFile.path) - .shares - ?.map((s) => s.copyWith( - // we need to set a known value to sharedAt - sharedAt: OrNull(DateTime.utc(2020, 1, 2, 3, 4, 5)), - )) - .toList(), - [util.buildAlbumShare(userId: "user1")], - ); - expect( - shareRepo.shares, - [ - util.buildShare(id: "0", file: albumFile, shareWith: "user1"), - ], - ); + await ShareAlbumWithUser(shareRepo, albumRepo)( + account, + albumRepo.findAlbumByPath(albumFile.path), + util.buildSharee(shareWith: "user1".toCi()), + ); + expect( + albumRepo.findAlbumByPath(albumFile.path).shares?.toList(), + [util.buildAlbumShare(userId: "user1")], + ); + expect( + shareRepo.shares, + [util.buildShare(id: "0", file: albumFile, shareWith: "user1")], + ); + }); } /// Share a shared album (admin -> user1) with a user (user2) From c588ab9c6d21c752bf11fa6619950bb59a5a0741 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sat, 17 Feb 2024 01:57:48 +0800 Subject: [PATCH 39/42] Update collection items when file updated in files controller --- .../collection_items_controller.dart | 31 ++++++++------ app/lib/entity/album/item.dart | 16 +------- app/lib/entity/album/item.g.dart | 41 +++++++++++++++++++ app/lib/entity/collection_item.dart | 4 ++ .../collection_item/album_item_adapter.dart | 9 ++++ .../entity/collection_item/basic_item.dart | 7 ++++ .../nc_album_item_adapter.dart | 7 ++++ app/lib/entity/collection_item/new_item.dart | 7 ++++ 8 files changed, 96 insertions(+), 26 deletions(-) diff --git a/app/lib/controller/collection_items_controller.dart b/app/lib/controller/collection_items_controller.dart index e8e20913..7739c721 100644 --- a/app/lib/controller/collection_items_controller.dart +++ b/app/lib/controller/collection_items_controller.dart @@ -327,18 +327,25 @@ class CollectionItemsController { return; } await _mutex.protect(() async { - final newItems = _dataStreamController.value.items.where((e) { - if (e is CollectionFileItem) { - return ev.dataMap.containsKey(e.file.fdId); - } else { - return true; - } - }).toList(); - if (newItems.length != _dataStreamController.value.items.length) { - _dataStreamController.addWithValue((value) => value.copyWith( - items: newItems, - )); - } + final newItems = _dataStreamController.value.items + .map((e) { + if (e is CollectionFileItem) { + final file = ev.dataMap[e.file.fdId]; + if (file == null) { + // removed + return null; + } else { + return e.copyWith(file: file); + } + } else { + return e; + } + }) + .whereNotNull() + .toList(); + _dataStreamController.addWithValue((value) => value.copyWith( + items: newItems, + )); }); } diff --git a/app/lib/entity/album/item.dart b/app/lib/entity/album/item.dart index 94a8ce3f..f136022d 100644 --- a/app/lib/entity/album/item.dart +++ b/app/lib/entity/album/item.dart @@ -1,3 +1,4 @@ +import 'package:copy_with/copy_with.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; @@ -73,6 +74,7 @@ abstract class AlbumItem with EquatableMixin { static final _log = _$AlbumItemNpLog.log; } +@genCopyWith @toString class AlbumFileItem extends AlbumItem { AlbumFileItem({ @@ -110,20 +112,6 @@ class AlbumFileItem extends AlbumItem { addedBy == other.addedBy && addedAt == other.addedAt; - AlbumFileItem copyWith({ - CiString? addedBy, - DateTime? addedAt, - FileDescriptor? file, - CiString? ownerId, - }) { - return AlbumFileItem( - addedBy: addedBy ?? this.addedBy, - addedAt: addedAt ?? this.addedAt, - file: file ?? this.file, - ownerId: ownerId ?? this.ownerId, - ); - } - @override List get props => [ ...super.props, diff --git a/app/lib/entity/album/item.g.dart b/app/lib/entity/album/item.g.dart index fe8fdd1a..12bb28f5 100644 --- a/app/lib/entity/album/item.g.dart +++ b/app/lib/entity/album/item.g.dart @@ -2,6 +2,47 @@ part of 'item.dart'; +// ************************************************************************** +// CopyWithLintRuleGenerator +// ************************************************************************** + +// ignore_for_file: library_private_types_in_public_api, duplicate_ignore + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class $AlbumFileItemCopyWithWorker { + AlbumFileItem call( + {CiString? addedBy, + DateTime? addedAt, + FileDescriptor? file, + CiString? ownerId}); +} + +class _$AlbumFileItemCopyWithWorkerImpl + implements $AlbumFileItemCopyWithWorker { + _$AlbumFileItemCopyWithWorkerImpl(this.that); + + @override + AlbumFileItem call( + {dynamic addedBy, dynamic addedAt, dynamic file, dynamic ownerId}) { + return AlbumFileItem( + addedBy: addedBy as CiString? ?? that.addedBy, + addedAt: addedAt as DateTime? ?? that.addedAt, + file: file as FileDescriptor? ?? that.file, + ownerId: ownerId as CiString? ?? that.ownerId); + } + + final AlbumFileItem that; +} + +extension $AlbumFileItemCopyWith on AlbumFileItem { + $AlbumFileItemCopyWithWorker get copyWith => _$copyWith; + $AlbumFileItemCopyWithWorker get _$copyWith => + _$AlbumFileItemCopyWithWorkerImpl(this); +} + // ************************************************************************** // NpLogGenerator // ************************************************************************** diff --git a/app/lib/entity/collection_item.dart b/app/lib/entity/collection_item.dart index 376c66b0..d8b5e5a2 100644 --- a/app/lib/entity/collection_item.dart +++ b/app/lib/entity/collection_item.dart @@ -8,6 +8,10 @@ abstract class CollectionItem { abstract class CollectionFileItem implements CollectionItem { const CollectionFileItem(); + CollectionFileItem copyWith({ + FileDescriptor? file, + }); + FileDescriptor get file; } diff --git a/app/lib/entity/collection_item/album_item_adapter.dart b/app/lib/entity/collection_item/album_item_adapter.dart index 8071c359..59fd77be 100644 --- a/app/lib/entity/collection_item/album_item_adapter.dart +++ b/app/lib/entity/collection_item/album_item_adapter.dart @@ -24,6 +24,15 @@ class CollectionFileItemAlbumAdapter extends CollectionFileItem with AlbumAdaptedCollectionItem { const CollectionFileItemAlbumAdapter(this.item); + @override + CollectionFileItemAlbumAdapter copyWith({ + FileDescriptor? file, + }) { + return CollectionFileItemAlbumAdapter(item.copyWith( + file: file, + )); + } + @override String toString() => _$toString(); diff --git a/app/lib/entity/collection_item/basic_item.dart b/app/lib/entity/collection_item/basic_item.dart index 3411264e..0d5e7a7e 100644 --- a/app/lib/entity/collection_item/basic_item.dart +++ b/app/lib/entity/collection_item/basic_item.dart @@ -9,6 +9,13 @@ part 'basic_item.g.dart'; class BasicCollectionFileItem implements CollectionFileItem { const BasicCollectionFileItem(this.file); + @override + BasicCollectionFileItem copyWith({ + FileDescriptor? file, + }) { + return BasicCollectionFileItem(file ?? this.file); + } + @override String toString() => _$toString(); diff --git a/app/lib/entity/collection_item/nc_album_item_adapter.dart b/app/lib/entity/collection_item/nc_album_item_adapter.dart index a497b1df..aee0abc7 100644 --- a/app/lib/entity/collection_item/nc_album_item_adapter.dart +++ b/app/lib/entity/collection_item/nc_album_item_adapter.dart @@ -9,6 +9,13 @@ part 'nc_album_item_adapter.g.dart'; class CollectionFileItemNcAlbumItemAdapter extends CollectionFileItem { const CollectionFileItemNcAlbumItemAdapter(this.item, [this.localFile]); + @override + CollectionFileItemNcAlbumItemAdapter copyWith({ + FileDescriptor? file, + }) { + return CollectionFileItemNcAlbumItemAdapter(item, file ?? this.file); + } + @override String toString() => _$toString(); diff --git a/app/lib/entity/collection_item/new_item.dart b/app/lib/entity/collection_item/new_item.dart index efd4e95a..9d75fd05 100644 --- a/app/lib/entity/collection_item/new_item.dart +++ b/app/lib/entity/collection_item/new_item.dart @@ -14,6 +14,13 @@ abstract class NewCollectionItem implements CollectionItem {} class NewCollectionFileItem implements CollectionFileItem, NewCollectionItem { const NewCollectionFileItem(this.file); + @override + NewCollectionFileItem copyWith({ + FileDescriptor? file, + }) { + return NewCollectionFileItem(file ?? this.file); + } + @override String toString() => _$toString(); From def24946e21e7848060ae6323401d5ae13e7c22a Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sun, 25 Feb 2024 12:09:57 +0800 Subject: [PATCH 40/42] Optimize pref streams where the first value should be ignored --- app/lib/bloc/home_search_suggestion.dart | 2 +- .../controller/account_pref_controller.dart | 15 +- .../controller/account_pref_controller.g.dart | 31 ++++ app/lib/controller/files_controller.dart | 8 +- app/lib/controller/metadata_controller.dart | 5 +- app/lib/controller/persons_controller.dart | 2 +- app/lib/controller/pref_controller.dart | 81 ++++------- app/lib/controller/pref_controller.g.dart | 130 +++++++++++++++++ app/lib/theme.dart | 2 +- app/lib/widget/archive_browser/bloc.dart | 2 +- app/lib/widget/collection_browser/bloc.dart | 2 +- app/lib/widget/collection_browser/view.dart | 8 +- app/lib/widget/home.dart | 4 +- app/lib/widget/home_app_bar.dart | 1 + app/lib/widget/home_collections/bloc.dart | 2 +- app/lib/widget/home_photos/bloc.dart | 19 +-- app/lib/widget/my_app/bloc.dart | 20 +-- app/lib/widget/search_landing.dart | 1 + app/lib/widget/settings/account/bloc.dart | 12 +- app/lib/widget/settings/collection/bloc.dart | 4 +- app/lib/widget/settings/enhancement/bloc.dart | 8 +- app/lib/widget/settings/language/bloc.dart | 8 +- app/lib/widget/settings/metadata/bloc.dart | 8 +- app/lib/widget/settings/misc/bloc.dart | 8 +- app/lib/widget/settings/photos/bloc.dart | 12 +- app/lib/widget/settings/theme/bloc.dart | 12 +- app/lib/widget/settings/viewer/bloc.dart | 12 +- app/lib/widget/sharing_browser/bloc.dart | 2 +- codegen/lib/np_codegen.dart | 1 + .../src/np_subject_accessor_annotations.dart | 9 ++ codegen_build/build.yaml | 2 +- codegen_build/lib/builder.dart | 4 + .../src/np_subject_accessor_generator.dart | 132 ++++++++++++++++++ codegen_build/pubspec.yaml | 9 +- .../test/np_subject_accessor_test.dart | 117 ++++++++++++++++ .../test/src/np_subject_accessor.dart | 7 + 36 files changed, 550 insertions(+), 152 deletions(-) create mode 100644 codegen/lib/src/np_subject_accessor_annotations.dart create mode 100644 codegen_build/lib/src/np_subject_accessor_generator.dart create mode 100644 codegen_build/test/np_subject_accessor_test.dart create mode 100644 codegen_build/test/src/np_subject_accessor.dart diff --git a/app/lib/bloc/home_search_suggestion.dart b/app/lib/bloc/home_search_suggestion.dart index 35dc0ae7..49e09490 100644 --- a/app/lib/bloc/home_search_suggestion.dart +++ b/app/lib/bloc/home_search_suggestion.dart @@ -219,7 +219,7 @@ class HomeSearchSuggestionBloc } try { final persons = await ListPerson(_c)( - account, accountPrefController.personProvider.value) + account, accountPrefController.personProviderValue) .last; product.addAll(persons.map((t) => _PersonSearcheable(t))); _log.info("[_onEventPreloadData] Loaded ${persons.length} people"); diff --git a/app/lib/controller/account_pref_controller.dart b/app/lib/controller/account_pref_controller.dart index e11fec47..1b6aa39a 100644 --- a/app/lib/controller/account_pref_controller.dart +++ b/app/lib/controller/account_pref_controller.dart @@ -8,6 +8,7 @@ import 'package:rxdart/rxdart.dart'; part 'account_pref_controller.g.dart'; @npLog +@npSubjectAccessor class AccountPrefController { AccountPrefController({ required this.account, @@ -20,34 +21,24 @@ class AccountPrefController { _isEnableMemoryAlbumController.close(); } - ValueStream get shareFolder => _shareFolderController.stream; - Future setShareFolder(String value) => _set( controller: _shareFolderController, setter: (pref, value) => pref.setShareFolder(value), value: value, ); - ValueStream get accountLabel => _accountLabelController.stream; - Future setAccountLabel(String? value) => _set( controller: _accountLabelController, setter: (pref, value) => pref.setAccountLabel(value), value: value, ); - ValueStream get personProvider => - _personProviderController.stream; - Future setPersonProvider(PersonProvider value) => _set( controller: _personProviderController, setter: (pref, value) => pref.setPersonProvider(value.index), value: value, ); - ValueStream get isEnableMemoryAlbum => - _isEnableMemoryAlbumController.stream; - Future setEnableMemoryAlbum(bool value) => _set( controller: _isEnableMemoryAlbumController, setter: (pref, value) => pref.setEnableMemoryAlbum(value), @@ -76,12 +67,16 @@ class AccountPrefController { final Account account; final AccountPref _accountPref; + @npSubjectAccessor late final _shareFolderController = BehaviorSubject.seeded(_accountPref.getShareFolderOr("")); + @npSubjectAccessor late final _accountLabelController = BehaviorSubject.seeded(_accountPref.getAccountLabel()); + @npSubjectAccessor late final _personProviderController = BehaviorSubject.seeded( PersonProvider.fromValue(_accountPref.getPersonProviderOr())); + @npSubjectAccessor late final _isEnableMemoryAlbumController = BehaviorSubject.seeded(_accountPref.isEnableMemoryAlbumOr(true)); } diff --git a/app/lib/controller/account_pref_controller.g.dart b/app/lib/controller/account_pref_controller.g.dart index 410d6cbe..acdd0deb 100644 --- a/app/lib/controller/account_pref_controller.g.dart +++ b/app/lib/controller/account_pref_controller.g.dart @@ -13,3 +13,34 @@ extension _$AccountPrefControllerNpLog on AccountPrefController { static final log = Logger("controller.account_pref_controller.AccountPrefController"); } + +// ************************************************************************** +// NpSubjectAccessorGenerator +// ************************************************************************** + +extension $AccountPrefControllerNpSubjectAccessor on AccountPrefController { + // _shareFolderController + ValueStream get shareFolder => _shareFolderController.stream; + Stream get shareFolderNew => shareFolder.skip(1); + Stream get shareFolderChange => shareFolder.distinct().skip(1); + String get shareFolderValue => _shareFolderController.value; +// _accountLabelController + ValueStream get accountLabel => _accountLabelController.stream; + Stream get accountLabelNew => accountLabel.skip(1); + Stream get accountLabelChange => accountLabel.distinct().skip(1); + String? get accountLabelValue => _accountLabelController.value; +// _personProviderController + ValueStream get personProvider => + _personProviderController.stream; + Stream get personProviderNew => personProvider.skip(1); + Stream get personProviderChange => + personProvider.distinct().skip(1); + PersonProvider get personProviderValue => _personProviderController.value; +// _isEnableMemoryAlbumController + ValueStream get isEnableMemoryAlbum => + _isEnableMemoryAlbumController.stream; + Stream get isEnableMemoryAlbumNew => isEnableMemoryAlbum.skip(1); + Stream get isEnableMemoryAlbumChange => + isEnableMemoryAlbum.distinct().skip(1); + bool get isEnableMemoryAlbumValue => _isEnableMemoryAlbumController.value; +} diff --git a/app/lib/controller/files_controller.dart b/app/lib/controller/files_controller.dart index 1befcbd9..64357591 100644 --- a/app/lib/controller/files_controller.dart +++ b/app/lib/controller/files_controller.dart @@ -43,7 +43,7 @@ class FilesController { required this.account, required this.accountPrefController, }) { - _subscriptions.add(accountPrefController.shareFolder.listen((event) { + _subscriptions.add(accountPrefController.shareFolderChange.listen((event) { // sync remote if share folder is modified if (_isDataStreamInited) { syncRemote(); @@ -84,7 +84,7 @@ class FilesController { try { final shareDir = File( path: file_util.unstripPath( - account, accountPrefController.shareFolder.value), + account, accountPrefController.shareFolderValue), ); var isShareDirIncluded = false; @@ -320,7 +320,7 @@ class FilesController { final completer = Completer(); ListFile(_c)( account, - file_util.unstripPath(account, accountPrefController.shareFolder.value), + file_util.unstripPath(account, accountPrefController.shareFolderValue), ).listen( (ev) { lastData = _convertListResultsToEvent(ev, hasNext: true); @@ -338,7 +338,7 @@ class FilesController { final completer = Completer(); ListFile(_c)( account, - file_util.unstripPath(account, accountPrefController.shareFolder.value), + file_util.unstripPath(account, accountPrefController.shareFolderValue), ).listen( (ev) { results = ev; diff --git a/app/lib/controller/metadata_controller.dart b/app/lib/controller/metadata_controller.dart index 0b3e8d0e..729c89e8 100644 --- a/app/lib/controller/metadata_controller.dart +++ b/app/lib/controller/metadata_controller.dart @@ -22,7 +22,8 @@ class MetadataController { required this.prefController, }) { _subscriptions.add(filesController.stream.listen(_onFilesEvent)); - _subscriptions.add(prefController.isEnableExif.listen(_onSetEnableExif)); + _subscriptions + .add(prefController.isEnableExifChange.listen(_onSetEnableExif)); } void dispose() { @@ -39,7 +40,7 @@ class MetadataController { Future _onFilesEvent(FilesStreamEvent ev) async { _log.info("[_onFilesEvent]"); - if (!prefController.isEnableExif.value) { + if (!prefController.isEnableExifValue) { // disabled return; } diff --git a/app/lib/controller/persons_controller.dart b/app/lib/controller/persons_controller.dart index 6d5e36f9..3e9e683c 100644 --- a/app/lib/controller/persons_controller.dart +++ b/app/lib/controller/persons_controller.dart @@ -71,7 +71,7 @@ class PersonsController { _personStreamContorller.add(lastData); final completer = Completer(); ListPerson(_c.withLocalRepo())( - account, accountPrefController.personProvider.value) + account, accountPrefController.personProviderValue) .listen( (results) { lastData = PersonStreamEvent( diff --git a/app/lib/controller/pref_controller.dart b/app/lib/controller/pref_controller.dart index 86bb00d3..d86e0031 100644 --- a/app/lib/controller/pref_controller.dart +++ b/app/lib/controller/pref_controller.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/pref.dart'; -import 'package:nc_photos/language_util.dart' as language_util; +import 'package:nc_photos/language_util.dart'; import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/size.dart'; import 'package:np_codegen/np_codegen.dart'; @@ -14,134 +14,94 @@ import 'package:rxdart/rxdart.dart'; part 'pref_controller.g.dart'; @npLog +@npSubjectAccessor class PrefController { PrefController(this._c); - ValueStream get language => - _languageController.stream; - - Future setAppLanguage(language_util.AppLanguage value) => - _set( + Future setAppLanguage(AppLanguage value) => _set( controller: _languageController, setter: (pref, value) => pref.setLanguage(value.langId), value: value, ); - ValueStream get homePhotosZoomLevel => - _homePhotosZoomLevelController.stream; - Future setHomePhotosZoomLevel(int value) => _set( controller: _homePhotosZoomLevelController, setter: (pref, value) => pref.setHomePhotosZoomLevel(value), value: value, ); - ValueStream get albumBrowserZoomLevel => - _albumBrowserZoomLevelController.stream; - Future setAlbumBrowserZoomLevel(int value) => _set( controller: _albumBrowserZoomLevelController, setter: (pref, value) => pref.setAlbumBrowserZoomLevel(value), value: value, ); - ValueStream get homeAlbumsSort => _homeAlbumsSortController.stream; - Future setHomeAlbumsSort(int value) => _set( controller: _homeAlbumsSortController, setter: (pref, value) => pref.setHomeAlbumsSort(value), value: value, ); - ValueStream get isEnableExif => _isEnableExifController.stream; - Future setEnableExif(bool value) => _set( controller: _isEnableExifController, setter: (pref, value) => pref.setEnableExif(value), value: value, ); - ValueStream get shouldProcessExifWifiOnly => - _shouldProcessExifWifiOnlyController.stream; - Future setProcessExifWifiOnly(bool value) => _set( controller: _shouldProcessExifWifiOnlyController, setter: (pref, value) => pref.setProcessExifWifiOnly(value), value: value, ); - ValueStream get memoriesRange => _memoriesRangeController.stream; - Future setMemoriesRange(int value) => _set( controller: _memoriesRangeController, setter: (pref, value) => pref.setMemoriesRange(value), value: value, ); - ValueStream get isPhotosTabSortByName => - _isPhotosTabSortByNameController.stream; - Future setPhotosTabSortByName(bool value) => _set( controller: _isPhotosTabSortByNameController, setter: (pref, value) => pref.setPhotosTabSortByName(value), value: value, ); - ValueStream get viewerScreenBrightness => - _viewerScreenBrightnessController.stream; - Future setViewerScreenBrightness(int value) => _set( controller: _viewerScreenBrightnessController, setter: (pref, value) => pref.setViewerScreenBrightness(value), value: value, ); - ValueStream get isViewerForceRotation => - _isViewerForceRotationController.stream; - Future setViewerForceRotation(bool value) => _set( controller: _isViewerForceRotationController, setter: (pref, value) => pref.setViewerForceRotation(value), value: value, ); - ValueStream get gpsMapProvider => - _gpsMapProviderController.stream; - Future setGpsMapProvider(GpsMapProvider value) => _set( controller: _gpsMapProviderController, setter: (pref, value) => pref.setGpsMapProvider(value.index), value: value, ); - ValueStream get isAlbumBrowserShowDate => - _isAlbumBrowserShowDateController.stream; - Future setAlbumBrowserShowDate(bool value) => _set( controller: _isAlbumBrowserShowDateController, setter: (pref, value) => pref.setAlbumBrowserShowDate(value), value: value, ); - ValueStream get isDoubleTapExit => _isDoubleTapExitController.stream; - Future setDoubleTapExit(bool value) => _set( controller: _isDoubleTapExitController, setter: (pref, value) => pref.setDoubleTapExit(value), value: value, ); - ValueStream get isSaveEditResultToServer => - _isSaveEditResultToServerController.stream; - Future setSaveEditResultToServer(bool value) => _set( controller: _isSaveEditResultToServerController, setter: (pref, value) => pref.setSaveEditResultToServer(value), value: value, ); - ValueStream get enhanceMaxSize => _enhanceMaxSizeController.stream; - Future setEnhanceMaxSize(SizeInt value) => _set( controller: _enhanceMaxSizeController, setter: (pref, value) async { @@ -154,34 +114,24 @@ class PrefController { value: value, ); - ValueStream get isDarkTheme => _isDarkThemeController.stream; - Future setDarkTheme(bool value) => _set( controller: _isDarkThemeController, setter: (pref, value) => pref.setDarkTheme(value), value: value, ); - ValueStream get isFollowSystemTheme => - _isFollowSystemThemeController.stream; - Future setFollowSystemTheme(bool value) => _set( controller: _isFollowSystemThemeController, setter: (pref, value) => pref.setFollowSystemTheme(value), value: value, ); - ValueStream get isUseBlackInDarkTheme => - _isUseBlackInDarkThemeController.stream; - Future setUseBlackInDarkTheme(bool value) => _set( controller: _isUseBlackInDarkThemeController, setter: (pref, value) => pref.setUseBlackInDarkTheme(value), value: value, ); - ValueStream get seedColor => _seedColorController.stream; - Future setSeedColor(Color? value) => _setOrRemove( controller: _seedColorController, setter: (pref, value) => pref.setSeedColor(value.withAlpha(0xFF).value), @@ -235,51 +185,70 @@ class PrefController { } } - static language_util.AppLanguage _langIdToAppLanguage(int langId) { + static AppLanguage _langIdToAppLanguage(int langId) { try { - return language_util.supportedLanguages[langId]!; + return supportedLanguages[langId]!; } catch (_) { - return language_util.supportedLanguages[0]!; + return supportedLanguages[0]!; } } final DiContainer _c; + @npSubjectAccessor late final _languageController = BehaviorSubject.seeded(_langIdToAppLanguage(_c.pref.getLanguageOr(0))); + @npSubjectAccessor late final _homePhotosZoomLevelController = BehaviorSubject.seeded(_c.pref.getHomePhotosZoomLevelOr(0)); + @npSubjectAccessor late final _albumBrowserZoomLevelController = BehaviorSubject.seeded(_c.pref.getAlbumBrowserZoomLevelOr(0)); + @npSubjectAccessor late final _homeAlbumsSortController = BehaviorSubject.seeded(_c.pref.getHomeAlbumsSortOr(0)); + @npSubjectAccessor late final _isEnableExifController = BehaviorSubject.seeded(_c.pref.isEnableExifOr(true)); + @npSubjectAccessor late final _shouldProcessExifWifiOnlyController = BehaviorSubject.seeded(_c.pref.shouldProcessExifWifiOnlyOr(true)); + @npSubjectAccessor late final _memoriesRangeController = BehaviorSubject.seeded(_c.pref.getMemoriesRangeOr(2)); + @npSubjectAccessor late final _isPhotosTabSortByNameController = BehaviorSubject.seeded(_c.pref.isPhotosTabSortByNameOr(false)); + @npSubjectAccessor late final _viewerScreenBrightnessController = BehaviorSubject.seeded(_c.pref.getViewerScreenBrightnessOr(-1)); + @npSubjectAccessor late final _isViewerForceRotationController = BehaviorSubject.seeded(_c.pref.isViewerForceRotationOr(false)); + @npSubjectAccessor late final _gpsMapProviderController = BehaviorSubject.seeded( GpsMapProvider.values[_c.pref.getGpsMapProviderOr(0)]); + @npSubjectAccessor late final _isAlbumBrowserShowDateController = BehaviorSubject.seeded(_c.pref.isAlbumBrowserShowDateOr(false)); + @npSubjectAccessor late final _isDoubleTapExitController = BehaviorSubject.seeded(_c.pref.isDoubleTapExitOr(false)); + @npSubjectAccessor late final _isSaveEditResultToServerController = BehaviorSubject.seeded(_c.pref.isSaveEditResultToServerOr(true)); + @npSubjectAccessor late final _enhanceMaxSizeController = BehaviorSubject.seeded( SizeInt(_c.pref.getEnhanceMaxWidthOr(), _c.pref.getEnhanceMaxHeightOr())); + @npSubjectAccessor late final _isDarkThemeController = BehaviorSubject.seeded(_c.pref.isDarkThemeOr(false)); + @npSubjectAccessor late final _isFollowSystemThemeController = BehaviorSubject.seeded(_c.pref.isFollowSystemThemeOr(false)); + @npSubjectAccessor late final _isUseBlackInDarkThemeController = BehaviorSubject.seeded(_c.pref.isUseBlackInDarkThemeOr(false)); + @NpSubjectAccessor(type: "Color?") late final _seedColorController = BehaviorSubject.seeded(_c.pref.getSeedColor()?.run(Color.new)); } diff --git a/app/lib/controller/pref_controller.g.dart b/app/lib/controller/pref_controller.g.dart index fc2a8e28..b064dc4d 100644 --- a/app/lib/controller/pref_controller.g.dart +++ b/app/lib/controller/pref_controller.g.dart @@ -12,3 +12,133 @@ extension _$PrefControllerNpLog on PrefController { static final log = Logger("controller.pref_controller.PrefController"); } + +// ************************************************************************** +// NpSubjectAccessorGenerator +// ************************************************************************** + +extension $PrefControllerNpSubjectAccessor on PrefController { + // _languageController + ValueStream get language => _languageController.stream; + Stream get languageNew => language.skip(1); + Stream get languageChange => language.distinct().skip(1); + AppLanguage get languageValue => _languageController.value; +// _homePhotosZoomLevelController + ValueStream get homePhotosZoomLevel => + _homePhotosZoomLevelController.stream; + Stream get homePhotosZoomLevelNew => homePhotosZoomLevel.skip(1); + Stream get homePhotosZoomLevelChange => + homePhotosZoomLevel.distinct().skip(1); + int get homePhotosZoomLevelValue => _homePhotosZoomLevelController.value; +// _albumBrowserZoomLevelController + ValueStream get albumBrowserZoomLevel => + _albumBrowserZoomLevelController.stream; + Stream get albumBrowserZoomLevelNew => albumBrowserZoomLevel.skip(1); + Stream get albumBrowserZoomLevelChange => + albumBrowserZoomLevel.distinct().skip(1); + int get albumBrowserZoomLevelValue => _albumBrowserZoomLevelController.value; +// _homeAlbumsSortController + ValueStream get homeAlbumsSort => _homeAlbumsSortController.stream; + Stream get homeAlbumsSortNew => homeAlbumsSort.skip(1); + Stream get homeAlbumsSortChange => homeAlbumsSort.distinct().skip(1); + int get homeAlbumsSortValue => _homeAlbumsSortController.value; +// _isEnableExifController + ValueStream get isEnableExif => _isEnableExifController.stream; + Stream get isEnableExifNew => isEnableExif.skip(1); + Stream get isEnableExifChange => isEnableExif.distinct().skip(1); + bool get isEnableExifValue => _isEnableExifController.value; +// _shouldProcessExifWifiOnlyController + ValueStream get shouldProcessExifWifiOnly => + _shouldProcessExifWifiOnlyController.stream; + Stream get shouldProcessExifWifiOnlyNew => + shouldProcessExifWifiOnly.skip(1); + Stream get shouldProcessExifWifiOnlyChange => + shouldProcessExifWifiOnly.distinct().skip(1); + bool get shouldProcessExifWifiOnlyValue => + _shouldProcessExifWifiOnlyController.value; +// _memoriesRangeController + ValueStream get memoriesRange => _memoriesRangeController.stream; + Stream get memoriesRangeNew => memoriesRange.skip(1); + Stream get memoriesRangeChange => memoriesRange.distinct().skip(1); + int get memoriesRangeValue => _memoriesRangeController.value; +// _isPhotosTabSortByNameController + ValueStream get isPhotosTabSortByName => + _isPhotosTabSortByNameController.stream; + Stream get isPhotosTabSortByNameNew => isPhotosTabSortByName.skip(1); + Stream get isPhotosTabSortByNameChange => + isPhotosTabSortByName.distinct().skip(1); + bool get isPhotosTabSortByNameValue => _isPhotosTabSortByNameController.value; +// _viewerScreenBrightnessController + ValueStream get viewerScreenBrightness => + _viewerScreenBrightnessController.stream; + Stream get viewerScreenBrightnessNew => viewerScreenBrightness.skip(1); + Stream get viewerScreenBrightnessChange => + viewerScreenBrightness.distinct().skip(1); + int get viewerScreenBrightnessValue => + _viewerScreenBrightnessController.value; +// _isViewerForceRotationController + ValueStream get isViewerForceRotation => + _isViewerForceRotationController.stream; + Stream get isViewerForceRotationNew => isViewerForceRotation.skip(1); + Stream get isViewerForceRotationChange => + isViewerForceRotation.distinct().skip(1); + bool get isViewerForceRotationValue => _isViewerForceRotationController.value; +// _gpsMapProviderController + ValueStream get gpsMapProvider => + _gpsMapProviderController.stream; + Stream get gpsMapProviderNew => gpsMapProvider.skip(1); + Stream get gpsMapProviderChange => + gpsMapProvider.distinct().skip(1); + GpsMapProvider get gpsMapProviderValue => _gpsMapProviderController.value; +// _isAlbumBrowserShowDateController + ValueStream get isAlbumBrowserShowDate => + _isAlbumBrowserShowDateController.stream; + Stream get isAlbumBrowserShowDateNew => isAlbumBrowserShowDate.skip(1); + Stream get isAlbumBrowserShowDateChange => + isAlbumBrowserShowDate.distinct().skip(1); + bool get isAlbumBrowserShowDateValue => + _isAlbumBrowserShowDateController.value; +// _isDoubleTapExitController + ValueStream get isDoubleTapExit => _isDoubleTapExitController.stream; + Stream get isDoubleTapExitNew => isDoubleTapExit.skip(1); + Stream get isDoubleTapExitChange => isDoubleTapExit.distinct().skip(1); + bool get isDoubleTapExitValue => _isDoubleTapExitController.value; +// _isSaveEditResultToServerController + ValueStream get isSaveEditResultToServer => + _isSaveEditResultToServerController.stream; + Stream get isSaveEditResultToServerNew => + isSaveEditResultToServer.skip(1); + Stream get isSaveEditResultToServerChange => + isSaveEditResultToServer.distinct().skip(1); + bool get isSaveEditResultToServerValue => + _isSaveEditResultToServerController.value; +// _enhanceMaxSizeController + ValueStream get enhanceMaxSize => _enhanceMaxSizeController.stream; + Stream get enhanceMaxSizeNew => enhanceMaxSize.skip(1); + Stream get enhanceMaxSizeChange => enhanceMaxSize.distinct().skip(1); + SizeInt get enhanceMaxSizeValue => _enhanceMaxSizeController.value; +// _isDarkThemeController + ValueStream get isDarkTheme => _isDarkThemeController.stream; + Stream get isDarkThemeNew => isDarkTheme.skip(1); + Stream get isDarkThemeChange => isDarkTheme.distinct().skip(1); + bool get isDarkThemeValue => _isDarkThemeController.value; +// _isFollowSystemThemeController + ValueStream get isFollowSystemTheme => + _isFollowSystemThemeController.stream; + Stream get isFollowSystemThemeNew => isFollowSystemTheme.skip(1); + Stream get isFollowSystemThemeChange => + isFollowSystemTheme.distinct().skip(1); + bool get isFollowSystemThemeValue => _isFollowSystemThemeController.value; +// _isUseBlackInDarkThemeController + ValueStream get isUseBlackInDarkTheme => + _isUseBlackInDarkThemeController.stream; + Stream get isUseBlackInDarkThemeNew => isUseBlackInDarkTheme.skip(1); + Stream get isUseBlackInDarkThemeChange => + isUseBlackInDarkTheme.distinct().skip(1); + bool get isUseBlackInDarkThemeValue => _isUseBlackInDarkThemeController.value; +// _seedColorController + ValueStream get seedColor => _seedColorController.stream; + Stream get seedColorNew => seedColor.skip(1); + Stream get seedColorChange => seedColor.distinct().skip(1); + Color? get seedColorValue => _seedColorController.value; +} diff --git a/app/lib/theme.dart b/app/lib/theme.dart index ac313d8c..a3324a37 100644 --- a/app/lib/theme.dart +++ b/app/lib/theme.dart @@ -112,7 +112,7 @@ ThemeData buildDarkTheme(BuildContext context, [ColorScheme? dynamicScheme]) { } Color? getSeedColor(BuildContext context) { - return context.read().seedColor.value; + return context.read().seedColorValue; } ColorScheme _getColorScheme( diff --git a/app/lib/widget/archive_browser/bloc.dart b/app/lib/widget/archive_browser/bloc.dart index 1c7e1b1f..270d5a03 100644 --- a/app/lib/widget/archive_browser/bloc.dart +++ b/app/lib/widget/archive_browser/bloc.dart @@ -7,7 +7,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { required this.controller, required this.prefController, }) : super(_State.init( - zoom: prefController.albumBrowserZoomLevel.value, + zoom: prefController.albumBrowserZoomLevelValue, )) { on<_LoadItems>(_onLoad); on<_TransformItems>(_onTransformItems); diff --git a/app/lib/widget/collection_browser/bloc.dart b/app/lib/widget/collection_browser/bloc.dart index d51da1da..35b5b502 100644 --- a/app/lib/widget/collection_browser/bloc.dart +++ b/app/lib/widget/collection_browser/bloc.dart @@ -15,7 +15,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { super(_State.init( collection: collection, coverUrl: _getCoverUrl(collection), - zoom: prefController.albumBrowserZoomLevel.value, + zoom: prefController.albumBrowserZoomLevelValue, )) { _initItemController(collection); diff --git a/app/lib/widget/collection_browser/view.dart b/app/lib/widget/collection_browser/view.dart index 16c11849..963b2a3b 100644 --- a/app/lib/widget/collection_browser/view.dart +++ b/app/lib/widget/collection_browser/view.dart @@ -102,8 +102,8 @@ class _EditContentList extends StatelessWidget { @override Widget build(BuildContext context) { return StreamBuilder( - stream: context.read().albumBrowserZoomLevel, - initialData: context.read().albumBrowserZoomLevel.value, + stream: context.read().albumBrowserZoomLevelChange, + initialData: context.read().albumBrowserZoomLevelValue, builder: (_, zoomLevel) { if (zoomLevel.hasError) { context.addEvent( @@ -159,9 +159,9 @@ class _UnmodifiableEditContentList extends StatelessWidget { sliver: SliverOpacity( opacity: .25, sliver: StreamBuilder( - stream: context.read().albumBrowserZoomLevel, + stream: context.read().albumBrowserZoomLevelChange, initialData: - context.read().albumBrowserZoomLevel.value, + context.read().albumBrowserZoomLevelValue, builder: (_, zoomLevel) { if (zoomLevel.hasError) { context.addEvent(_SetMessage( diff --git a/app/lib/widget/home.dart b/app/lib/widget/home.dart index cea64e80..4641f59b 100644 --- a/app/lib/widget/home.dart +++ b/app/lib/widget/home.dart @@ -8,6 +8,7 @@ import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/controller/account_controller.dart'; +import 'package:nc_photos/controller/account_pref_controller.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album/data_source.dart'; @@ -187,8 +188,7 @@ class _HomeState extends State with TickerProviderStateMixin { context .read() .accountPrefController - .shareFolder - .value, + .shareFolderValue, ); } catch (e, stacktrace) { _log.shout( diff --git a/app/lib/widget/home_app_bar.dart b/app/lib/widget/home_app_bar.dart index 8f17fbe1..ff8e8536 100644 --- a/app/lib/widget/home_app_bar.dart +++ b/app/lib/widget/home_app_bar.dart @@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/api/api_util.dart' as api_util; import 'package:nc_photos/controller/account_controller.dart'; +import 'package:nc_photos/controller/account_pref_controller.dart'; import 'package:nc_photos/stream_util.dart'; import 'package:nc_photos/theme.dart'; import 'package:nc_photos/widget/account_picker_dialog.dart'; diff --git a/app/lib/widget/home_collections/bloc.dart b/app/lib/widget/home_collections/bloc.dart index a85e4e6f..eb739cc5 100644 --- a/app/lib/widget/home_collections/bloc.dart +++ b/app/lib/widget/home_collections/bloc.dart @@ -20,7 +20,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { on<_SetError>(_onSetError); - _subscriptions.add(prefController.homeAlbumsSort.distinct().listen((event) { + _subscriptions.add(prefController.homeAlbumsSortChange.listen((event) { add(_UpdateCollectionSort(collection_util.CollectionSort.values[event])); })); _subscriptions.add(controller.stream.listen((event) { diff --git a/app/lib/widget/home_photos/bloc.dart b/app/lib/widget/home_photos/bloc.dart index 9e4768ae..a2e1e4a6 100644 --- a/app/lib/widget/home_photos/bloc.dart +++ b/app/lib/widget/home_photos/bloc.dart @@ -13,9 +13,9 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { required this.personsController, required this.metadataController, }) : super(_State.init( - zoom: prefController.homePhotosZoomLevel.value, + zoom: prefController.homePhotosZoomLevelValue, isEnableMemoryCollection: - accountPrefController.isEnableMemoryAlbum.value, + accountPrefController.isEnableMemoryAlbumValue, )) { on<_LoadItems>(_onLoad); on<_RequestRefresh>(_onRequestRefresh); @@ -46,13 +46,14 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { on<_SetError>(_onSetError); _subscriptions - .add(accountPrefController.isEnableMemoryAlbum.listen((event) { + .add(accountPrefController.isEnableMemoryAlbumChange.listen((event) { add(_SetEnableMemoryCollection(event)); })); - _subscriptions.add(prefController.isPhotosTabSortByName.listen((event) { + _subscriptions + .add(prefController.isPhotosTabSortByNameChange.listen((event) { add(_SetSortByName(event)); })); - _subscriptions.add(prefController.memoriesRange.listen((event) { + _subscriptions.add(prefController.memoriesRangeChange.listen((event) { add(_SetMemoriesRange(event)); })); } @@ -138,7 +139,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { account: account, filesController: controller, personsController: personsController, - personProvider: accountPrefController.personProvider.value, + personProvider: accountPrefController.personProviderValue, ); } @@ -303,11 +304,11 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _ItemTransformerArgument( account: account, files: files, - sort: prefController.isPhotosTabSortByName.value + sort: prefController.isPhotosTabSortByNameValue ? _ItemSort.filename : _ItemSort.dateTime, - isGroupByDay: prefController.homePhotosZoomLevel.value >= 0, - memoriesDayRange: prefController.memoriesRange.value, + isGroupByDay: prefController.homePhotosZoomLevelValue >= 0, + memoriesDayRange: prefController.memoriesRangeValue, locale: language_util.getSelectedLocale() ?? PlatformDispatcher.instance.locale, ), diff --git a/app/lib/widget/my_app/bloc.dart b/app/lib/widget/my_app/bloc.dart index 01fd5a2f..cf2b110c 100644 --- a/app/lib/widget/my_app/bloc.dart +++ b/app/lib/widget/my_app/bloc.dart @@ -5,11 +5,11 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _Bloc({ required this.prefController, }) : super(_State( - language: prefController.language.value, - isDarkTheme: prefController.isDarkTheme.value, - isFollowSystemTheme: prefController.isFollowSystemTheme.value, - isUseBlackInDarkTheme: prefController.isUseBlackInDarkTheme.value, - seedColor: prefController.seedColor.value?.value, + language: prefController.languageValue, + isDarkTheme: prefController.isDarkThemeValue, + isFollowSystemTheme: prefController.isFollowSystemThemeValue, + isUseBlackInDarkTheme: prefController.isUseBlackInDarkThemeValue, + seedColor: prefController.seedColorValue?.value, )) { on<_Init>(_onInit); } @@ -21,23 +21,23 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _log.info(ev); await Future.wait([ emit.forEachIgnoreError( - prefController.language, + prefController.languageChange, onData: (data) => state.copyWith(language: data), ), emit.forEachIgnoreError( - prefController.isDarkTheme, + prefController.isDarkThemeChange, onData: (data) => state.copyWith(isDarkTheme: data), ), emit.forEachIgnoreError( - prefController.isFollowSystemTheme, + prefController.isFollowSystemThemeChange, onData: (data) => state.copyWith(isFollowSystemTheme: data), ), emit.forEachIgnoreError( - prefController.isUseBlackInDarkTheme, + prefController.isUseBlackInDarkThemeChange, onData: (data) => state.copyWith(isUseBlackInDarkTheme: data), ), emit.forEachIgnoreError( - prefController.seedColor, + prefController.seedColorChange, onData: (data) => state.copyWith(seedColor: data?.value), ), ]); diff --git a/app/lib/widget/search_landing.dart b/app/lib/widget/search_landing.dart index 327b5ccc..7a1fb696 100644 --- a/app/lib/widget/search_landing.dart +++ b/app/lib/widget/search_landing.dart @@ -9,6 +9,7 @@ import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/bloc_util.dart'; import 'package:nc_photos/controller/account_controller.dart'; +import 'package:nc_photos/controller/account_pref_controller.dart'; import 'package:nc_photos/controller/persons_controller.dart'; import 'package:nc_photos/controller/places_controller.dart'; import 'package:nc_photos/entity/collection/builder.dart'; diff --git a/app/lib/widget/settings/account/bloc.dart b/app/lib/widget/settings/account/bloc.dart index a50e5839..f615da42 100644 --- a/app/lib/widget/settings/account/bloc.dart +++ b/app/lib/widget/settings/account/bloc.dart @@ -10,13 +10,13 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { }) : _c = container, super(_State.init( account: account, - label: accountPrefController.accountLabel.value, - shareFolder: accountPrefController.shareFolder.value, - personProvider: accountPrefController.personProvider.value, + label: accountPrefController.accountLabelValue, + shareFolder: accountPrefController.shareFolderValue, + personProvider: accountPrefController.personProviderValue, )) { on<_SetLabel>(_onSetLabel); on<_OnUpdateLabel>(_onOnUpdateLabel); - _subscriptions.add(accountPrefController.accountLabel.listen( + _subscriptions.add(accountPrefController.accountLabelChange.listen( (event) { add(_OnUpdateLabel(event)); }, @@ -30,7 +30,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { on<_SetShareFolder>(_onSetShareFolder); on<_OnUpdateShareFolder>(_onOnUpdateShareFolder); - _subscriptions.add(accountPrefController.shareFolder.listen( + _subscriptions.add(accountPrefController.shareFolderChange.listen( (event) { add(_OnUpdateShareFolder(event)); }, @@ -41,7 +41,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { on<_SetPersonProvider>(_onSetPersonProvider); on<_OnUpdatePersonProvider>(_onOnUpdatePersonProvider); - _subscriptions.add(accountPrefController.personProvider.listen( + _subscriptions.add(accountPrefController.personProviderChange.listen( (event) { add(_OnUpdatePersonProvider(event)); }, diff --git a/app/lib/widget/settings/collection/bloc.dart b/app/lib/widget/settings/collection/bloc.dart index 8f295c68..e23d98ba 100644 --- a/app/lib/widget/settings/collection/bloc.dart +++ b/app/lib/widget/settings/collection/bloc.dart @@ -5,7 +5,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _Bloc({ required this.prefController, }) : super(_State( - isBrowserShowDate: prefController.isAlbumBrowserShowDate.value, + isBrowserShowDate: prefController.isAlbumBrowserShowDateValue, )) { on<_Init>(_onInit); on<_SetBrowserShowDate>(_onSetBrowserShowDate); @@ -17,7 +17,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { Future _onInit(_Init ev, Emitter<_State> emit) async { _log.info(ev); return emit.forEach( - prefController.isAlbumBrowserShowDate, + prefController.isAlbumBrowserShowDateChange, onData: (data) => state.copyWith(isBrowserShowDate: data), onError: (e, stackTrace) { _log.severe("[_onInit] Uncaught exception", e, stackTrace); diff --git a/app/lib/widget/settings/enhancement/bloc.dart b/app/lib/widget/settings/enhancement/bloc.dart index d95597d7..c153f10d 100644 --- a/app/lib/widget/settings/enhancement/bloc.dart +++ b/app/lib/widget/settings/enhancement/bloc.dart @@ -6,8 +6,8 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { required this.prefController, }) : super(_State( isSaveEditResultToServer: - prefController.isSaveEditResultToServer.value, - maxSize: prefController.enhanceMaxSize.value, + prefController.isSaveEditResultToServerValue, + maxSize: prefController.enhanceMaxSizeValue, )) { on<_Init>(_onInit); on<_SetSaveEditResultToServer>(_onSetSaveEditResultToServer); @@ -21,7 +21,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _log.info(ev); await Future.wait([ emit.forEach( - prefController.isSaveEditResultToServer, + prefController.isSaveEditResultToServerChange, onData: (data) => state.copyWith(isSaveEditResultToServer: data), onError: (e, stackTrace) { _log.severe("[_onInit] Uncaught exception", e, stackTrace); @@ -29,7 +29,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { }, ), emit.forEach( - prefController.enhanceMaxSize, + prefController.enhanceMaxSizeChange, onData: (data) => state.copyWith(maxSize: data), onError: (e, stackTrace) { _log.severe("[_onInit] Uncaught exception", e, stackTrace); diff --git a/app/lib/widget/settings/language/bloc.dart b/app/lib/widget/settings/language/bloc.dart index aa75f298..9f91383c 100644 --- a/app/lib/widget/settings/language/bloc.dart +++ b/app/lib/widget/settings/language/bloc.dart @@ -5,7 +5,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _Bloc({ required this.prefController, }) : super(_State.init( - selected: prefController.language.value, + selected: prefController.languageValue, )) { on<_Init>(_onInit); on<_SelectLanguage>(_onSelectLanguage); @@ -31,10 +31,8 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { Future _onInit(_Init ev, Emitter<_State> emit) { _log.info(ev); return emit.forEach( - prefController.language, - onData: (data) => state.copyWith( - selected: data, - ), + prefController.languageChange, + onData: (data) => state.copyWith(selected: data), onError: (e, stackTrace) { _log.severe("[_onInit] Uncaught exception", e, stackTrace); return state.copyWith( diff --git a/app/lib/widget/settings/metadata/bloc.dart b/app/lib/widget/settings/metadata/bloc.dart index 139dfaa7..473c86c0 100644 --- a/app/lib/widget/settings/metadata/bloc.dart +++ b/app/lib/widget/settings/metadata/bloc.dart @@ -5,8 +5,8 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _Bloc({ required this.prefController, }) : super(_State( - isEnable: prefController.isEnableExif.value, - isWifiOnly: prefController.shouldProcessExifWifiOnly.value, + isEnable: prefController.isEnableExifValue, + isWifiOnly: prefController.shouldProcessExifWifiOnlyValue, )) { on<_Init>(_onInit); on<_SetEnable>(_onSetEnable); @@ -20,7 +20,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _log.info(ev); await Future.wait([ emit.forEach( - prefController.isEnableExif, + prefController.isEnableExifChange, onData: (data) => state.copyWith(isEnable: data), onError: (e, stackTrace) { _log.severe("[_onInit] Uncaught exception", e, stackTrace); @@ -28,7 +28,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { }, ), emit.forEach( - prefController.shouldProcessExifWifiOnly, + prefController.shouldProcessExifWifiOnlyChange, onData: (data) => state.copyWith(isWifiOnly: data), onError: (e, stackTrace) { _log.severe("[_onInit] Uncaught exception", e, stackTrace); diff --git a/app/lib/widget/settings/misc/bloc.dart b/app/lib/widget/settings/misc/bloc.dart index 5ea0f10a..3bf2ad42 100644 --- a/app/lib/widget/settings/misc/bloc.dart +++ b/app/lib/widget/settings/misc/bloc.dart @@ -5,8 +5,8 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _Bloc({ required this.prefController, }) : super(_State( - isPhotosTabSortByName: prefController.isPhotosTabSortByName.value, - isDoubleTapExit: prefController.isDoubleTapExit.value, + isPhotosTabSortByName: prefController.isPhotosTabSortByNameValue, + isDoubleTapExit: prefController.isDoubleTapExitValue, )) { on<_Init>(_onInit); on<_SetPhotosTabSortByName>(_onSetPhotosTabSortByName); @@ -20,7 +20,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _log.info(ev); await Future.wait([ emit.forEach( - prefController.isPhotosTabSortByName, + prefController.isPhotosTabSortByNameChange, onData: (data) => state.copyWith(isPhotosTabSortByName: data), onError: (e, stackTrace) { _log.severe("[_onInit] Uncaught exception", e, stackTrace); @@ -28,7 +28,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { }, ), emit.forEach( - prefController.isDoubleTapExit, + prefController.isDoubleTapExitChange, onData: (data) => state.copyWith(isDoubleTapExit: data), onError: (e, stackTrace) { _log.severe("[_onInit] Uncaught exception", e, stackTrace); diff --git a/app/lib/widget/settings/photos/bloc.dart b/app/lib/widget/settings/photos/bloc.dart index f1827025..a8ea495b 100644 --- a/app/lib/widget/settings/photos/bloc.dart +++ b/app/lib/widget/settings/photos/bloc.dart @@ -6,9 +6,9 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { required this.prefController, required this.accountPrefController, }) : super(_State( - isEnableMemories: accountPrefController.isEnableMemoryAlbum.value, - isPhotosTabSortByName: prefController.isPhotosTabSortByName.value, - memoriesRange: prefController.memoriesRange.value, + isEnableMemories: accountPrefController.isEnableMemoryAlbumValue, + isPhotosTabSortByName: prefController.isPhotosTabSortByNameValue, + memoriesRange: prefController.memoriesRangeValue, )) { on<_Init>(_onInit); on<_SetEnableMemories>(_onSetEnableMemories); @@ -22,7 +22,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _log.info(ev); await Future.wait([ emit.forEach( - accountPrefController.isEnableMemoryAlbum, + accountPrefController.isEnableMemoryAlbumChange, onData: (data) => state.copyWith(isEnableMemories: data), onError: (e, stackTrace) { _log.severe("[_onInit] Uncaught exception", e, stackTrace); @@ -30,7 +30,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { }, ), emit.forEach( - prefController.isPhotosTabSortByName, + prefController.isPhotosTabSortByNameChange, onData: (data) => state.copyWith(isPhotosTabSortByName: data), onError: (e, stackTrace) { _log.severe("[_onInit] Uncaught exception", e, stackTrace); @@ -38,7 +38,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { }, ), emit.forEach( - prefController.memoriesRange, + prefController.memoriesRangeChange, onData: (data) => state.copyWith(memoriesRange: data), onError: (e, stackTrace) { _log.severe("[_onInit] Uncaught exception", e, stackTrace); diff --git a/app/lib/widget/settings/theme/bloc.dart b/app/lib/widget/settings/theme/bloc.dart index 1ba9a765..ccc852f4 100644 --- a/app/lib/widget/settings/theme/bloc.dart +++ b/app/lib/widget/settings/theme/bloc.dart @@ -5,9 +5,9 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _Bloc({ required this.prefController, }) : super(_State( - isFollowSystemTheme: prefController.isFollowSystemTheme.value, - isUseBlackInDarkTheme: prefController.isUseBlackInDarkTheme.value, - seedColor: prefController.seedColor.value?.value, + isFollowSystemTheme: prefController.isFollowSystemThemeValue, + isUseBlackInDarkTheme: prefController.isUseBlackInDarkThemeValue, + seedColor: prefController.seedColorValue?.value, )) { on<_Init>(_onInit); on<_SetFollowSystemTheme>(_onSetFollowSystemTheme); @@ -22,7 +22,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _log.info(ev); await Future.wait([ emit.forEach( - prefController.isFollowSystemTheme, + prefController.isFollowSystemThemeChange, onData: (data) => state.copyWith(isFollowSystemTheme: data), onError: (e, stackTrace) { _log.severe("[_onInit] Uncaught exception", e, stackTrace); @@ -30,7 +30,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { }, ), emit.forEach( - prefController.isUseBlackInDarkTheme, + prefController.isUseBlackInDarkThemeChange, onData: (data) => state.copyWith(isUseBlackInDarkTheme: data), onError: (e, stackTrace) { _log.severe("[_onInit] Uncaught exception", e, stackTrace); @@ -38,7 +38,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { }, ), emit.forEach( - prefController.seedColor, + prefController.seedColorChange, onData: (data) => state.copyWith(seedColor: data?.value), onError: (e, stackTrace) { _log.severe("[_onInit] Uncaught exception", e, stackTrace); diff --git a/app/lib/widget/settings/viewer/bloc.dart b/app/lib/widget/settings/viewer/bloc.dart index 05f146e9..22c64def 100644 --- a/app/lib/widget/settings/viewer/bloc.dart +++ b/app/lib/widget/settings/viewer/bloc.dart @@ -5,9 +5,9 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _Bloc({ required this.prefController, }) : super(_State( - screenBrightness: prefController.viewerScreenBrightness.value, - isForceRotation: prefController.isViewerForceRotation.value, - gpsMapProvider: prefController.gpsMapProvider.value, + screenBrightness: prefController.viewerScreenBrightnessValue, + isForceRotation: prefController.isViewerForceRotationValue, + gpsMapProvider: prefController.gpsMapProviderValue, )) { on<_Init>(_onInit); on<_SetScreenBrightness>(_onSetScreenBrightness); @@ -22,7 +22,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _log.info(ev); await Future.wait([ emit.forEach( - prefController.viewerScreenBrightness, + prefController.viewerScreenBrightnessChange, onData: (data) => state.copyWith(screenBrightness: data), onError: (e, stackTrace) { _log.severe("[_onInit] Uncaught exception", e, stackTrace); @@ -30,7 +30,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { }, ), emit.forEach( - prefController.isViewerForceRotation, + prefController.isViewerForceRotationChange, onData: (data) => state.copyWith(isForceRotation: data), onError: (e, stackTrace) { _log.severe("[_onInit] Uncaught exception", e, stackTrace); @@ -38,7 +38,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { }, ), emit.forEach( - prefController.gpsMapProvider, + prefController.gpsMapProviderChange, onData: (data) => state.copyWith(gpsMapProvider: data), onError: (e, stackTrace) { _log.severe("[_onInit] Uncaught exception", e, stackTrace); diff --git a/app/lib/widget/sharing_browser/bloc.dart b/app/lib/widget/sharing_browser/bloc.dart index dd437f0e..702d9204 100644 --- a/app/lib/widget/sharing_browser/bloc.dart +++ b/app/lib/widget/sharing_browser/bloc.dart @@ -70,7 +70,7 @@ class _Bloc extends Bloc<_Event, _State> { ); try { return await ImportPotentialSharedAlbum(c)( - account, accountPrefController.shareFolder.value); + account, accountPrefController.shareFolderValue); } catch (e, stackTrace) { _log.shout( "[_importPotentialSharedAlbum] Failed while ImportPotentialSharedAlbum", diff --git a/codegen/lib/np_codegen.dart b/codegen/lib/np_codegen.dart index ed3786f5..e5002329 100644 --- a/codegen/lib/np_codegen.dart +++ b/codegen/lib/np_codegen.dart @@ -2,3 +2,4 @@ library np_codegen; export 'src/drift_table_sort_annotations.dart'; export 'src/np_log_annotations.dart'; +export 'src/np_subject_accessor_annotations.dart'; diff --git a/codegen/lib/src/np_subject_accessor_annotations.dart b/codegen/lib/src/np_subject_accessor_annotations.dart new file mode 100644 index 00000000..2da15ba2 --- /dev/null +++ b/codegen/lib/src/np_subject_accessor_annotations.dart @@ -0,0 +1,9 @@ +class NpSubjectAccessor { + const NpSubjectAccessor({ + this.type, + }); + + final String? type; +} + +const npSubjectAccessor = NpSubjectAccessor(); diff --git a/codegen_build/build.yaml b/codegen_build/build.yaml index 546cdcaa..67d470c0 100644 --- a/codegen_build/build.yaml +++ b/codegen_build/build.yaml @@ -1,7 +1,7 @@ builders: np_log_build: import: "package:np_codegen_build/builder.dart" - builder_factories: ["driftTableSortBuilder", "npLogBuilder"] + builder_factories: ["driftTableSortBuilder", "npLogBuilder", "npSubjectAccessorBuilder"] # The `partId` argument to `SharedPartBuilder` is "some_cool_builder" build_extensions: {".dart": [".np_codegen.g.part"]} auto_apply: dependents diff --git a/codegen_build/lib/builder.dart b/codegen_build/lib/builder.dart index 69bb72da..f8a4b794 100644 --- a/codegen_build/lib/builder.dart +++ b/codegen_build/lib/builder.dart @@ -1,6 +1,7 @@ import 'package:build/build.dart'; import 'package:np_codegen_build/src/drift_table_sort_generator.dart'; import 'package:np_codegen_build/src/np_log_generator.dart'; +import 'package:np_codegen_build/src/np_subject_accessor_generator.dart'; import 'package:source_gen/source_gen.dart'; Builder driftTableSortBuilder(BuilderOptions options) => @@ -8,3 +9,6 @@ Builder driftTableSortBuilder(BuilderOptions options) => Builder npLogBuilder(BuilderOptions options) => SharedPartBuilder([const NpLogGenerator()], "np_log"); + +Builder npSubjectAccessorBuilder(BuilderOptions options) => SharedPartBuilder( + [const NpSubjectAccessorGenerator()], "np_subject_accessor"); diff --git a/codegen_build/lib/src/np_subject_accessor_generator.dart b/codegen_build/lib/src/np_subject_accessor_generator.dart new file mode 100644 index 00000000..516a0dd8 --- /dev/null +++ b/codegen_build/lib/src/np_subject_accessor_generator.dart @@ -0,0 +1,132 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:build/build.dart'; +import 'package:np_codegen/np_codegen.dart'; +import 'package:source_gen/source_gen.dart'; + +class NpSubjectAccessorGenerator + extends GeneratorForAnnotation { + const NpSubjectAccessorGenerator(); + + @override + Future generateForAnnotatedElement( + Element element, + ConstantReader annotation, + BuildStep buildStep, + ) async { + if (element is! ClassElement) { + // not class, probably field + return null; + } + final clazz = element; + final fields = await _getFields(buildStep.resolver, clazz); + return """ +extension \$${clazz.name}NpSubjectAccessor on ${clazz.name} { + ${_buildBody(fields)} +} +"""; + } + + String _buildBody(List<_FieldMeta> fields) { + final results = []; + for (final f in fields) { + results + ..add("// ${f.fullname}") + ..add( + "ValueStream<${f.typeStr}> get ${f.name} => ${f.fullname}.stream;") + ..add("Stream<${f.typeStr}> get ${f.name}New => ${f.name}.skip(1);") + ..add( + "Stream<${f.typeStr}> get ${f.name}Change => ${f.name}.distinct().skip(1);") + ..add("${f.typeStr} get ${f.name}Value => ${f.fullname}.value;"); + } + return results.join("\n"); + } + + Future> _getFields( + Resolver resolver, ClassElement clazz) async { + const typeChecker = TypeChecker.fromRuntime(NpSubjectAccessor); + final data = <_FieldMeta>[]; + for (final f in clazz.fields.where(typeChecker.hasAnnotationOf)) { + // final annotation = typeChecker.annotationsOf(f).first; + // final type = annotation.getField("type")!.toTypeValue()!; + final parseName = _parseName(f); + final parseType = await _parseTypeString(resolver, f); + data.add(_FieldMeta( + name: parseName.name, + fullname: parseName.fullname, + typeStr: parseType.typeStr, + )); + } + return data; + } + + _NameParseResult _parseName(FieldElement field) { + var name = field.name; + if (name.startsWith("_")) { + name = name.substring(1); + } + if (name.endsWith("Controller")) { + name = name.substring(0, name.length - 10); + } + return _NameParseResult(name: name, fullname: field.name); + } + + Future<_TypeParseResult> _parseTypeString( + Resolver resolver, FieldElement field) async { + String? typeStr; + if (const TypeChecker.fromRuntime(NpSubjectAccessor) + .hasAnnotationOf(field)) { + final annotation = const TypeChecker.fromRuntime(NpSubjectAccessor) + .annotationsOf(field) + .first; + final type = annotation.getField("type")?.toStringValue(); + typeStr = type; + } + + if (typeStr == null) { + final astNode = await resolver.astNodeFor(field, resolve: true); + typeStr = (astNode! as VariableDeclaration) + .initializer! + .staticType! + .getDisplayString(withNullability: true); + if (typeStr.startsWith("BehaviorSubject<")) { + typeStr = typeStr.substring(16, typeStr.length - 1); + } + if (typeStr == "InvalidType") { + throw UnsupportedError( + "Type can't be parsed, please specify the type in annotation: ${field.name}"); + } + } + return _TypeParseResult(typeStr: typeStr); + } +} + +class _NameParseResult { + const _NameParseResult({ + required this.name, + required this.fullname, + }); + + final String name; + final String fullname; +} + +class _TypeParseResult { + const _TypeParseResult({ + required this.typeStr, + }); + + final String typeStr; +} + +class _FieldMeta { + const _FieldMeta({ + required this.name, + required this.fullname, + required this.typeStr, + }); + + final String name; + final String fullname; + final String typeStr; +} diff --git a/codegen_build/pubspec.yaml b/codegen_build/pubspec.yaml index 141a9adb..fd1b4813 100644 --- a/codegen_build/pubspec.yaml +++ b/codegen_build/pubspec.yaml @@ -16,12 +16,13 @@ dependencies: dev_dependencies: build_test: ^2.1.5 - np_lints: - path: ../np_lints - path: any - test: any code_gen_tester: git: url: https://github.com/rrousselGit/functional_widget ref: v0.10.0 path: packages/code_gen_tester + np_lints: + path: ../np_lints + path: any + rxdart: any + test: any diff --git a/codegen_build/test/np_subject_accessor_test.dart b/codegen_build/test/np_subject_accessor_test.dart new file mode 100644 index 00000000..9d39f474 --- /dev/null +++ b/codegen_build/test/np_subject_accessor_test.dart @@ -0,0 +1,117 @@ +import 'dart:io'; + +import 'package:build/build.dart'; +import 'package:build_test/build_test.dart'; +import 'package:np_codegen_build/src/np_subject_accessor_generator.dart'; +import 'package:path/path.dart' as p; +import 'package:source_gen/source_gen.dart'; +import 'package:test/test.dart'; + +void main() { + _resolveCompilationUnit("test/src/np_subject_accessor.dart"); + tearDown(() { + // Increment this after each test so the next test has it's own package + _pkgCacheCount++; + }); + + group("NpSubjectAccessor", () { + test("empty", () async { + final src = _genSrc(""" +@npSubjectAccessor +class Empty {} +"""); + final expected = _genExpected(r""" +extension $EmptyNpSubjectAccessor on Empty {} +"""); + return _buildTest(src, expected); + }); + + test("int", () async { + final src = _genSrc(""" +@npSubjectAccessor +class IntTest { + @npSubjectAccessor + final _barController = BehaviorSubject.seeded(1); +} +"""); + final expected = _genExpected(r""" +extension $IntTestNpSubjectAccessor on IntTest { + // _barController + ValueStream get bar => _barController.stream; + Stream get barNew => bar.skip(1); + Stream get barChange => bar.distinct().skip(1); + int get barValue => _barController.value; +} +"""); + return _buildTest(src, expected); + }); + + test("int nullable", () async { + final src = _genSrc(""" +@npSubjectAccessor +class IntNullableTest { + @npSubjectAccessor + final _barController = BehaviorSubject.seeded(1); +} +"""); + final expected = _genExpected(r""" +extension $IntNullableTestNpSubjectAccessor on IntNullableTest { + // _barController + ValueStream get bar => _barController.stream; + Stream get barNew => bar.skip(1); + Stream get barChange => bar.distinct().skip(1); + int? get barValue => _barController.value; +} +"""); + return _buildTest(src, expected); + }); + }); +} + +String _genSrc(String src) { + return """ +import 'package:np_codegen/np_codegen.dart'; +import 'package:rxdart/rxdart.dart'; +part 'test.g.dart'; + +$src +"""; +} + +String _genExpected(String src) { + return """// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'test.dart'; + +// ************************************************************************** +// NpSubjectAccessorGenerator +// ************************************************************************** + +$src"""; +} + +Future _buildTest(String src, String expected) { + return testBuilder( + PartBuilder([const NpSubjectAccessorGenerator()], ".g.dart"), + {"$_pkgName|lib/test.dart": src}, + generateFor: {'$_pkgName|lib/test.dart'}, + outputs: {"$_pkgName|lib/test.g.dart": decodedMatches(expected)}, + ); +} + +// Taken from source_gen_test, unclear why this is needed... +Future _resolveCompilationUnit(String filePath) async { + final assetId = AssetId.parse('a|lib/${p.basename(filePath)}'); + final files = + Directory(p.dirname(filePath)).listSync().whereType().toList(); + + final fileMap = Map.fromEntries(files.map( + (f) => MapEntry('a|lib/${p.basename(f.path)}', f.readAsStringSync()))); + + await resolveSources(fileMap, (item) async { + return await item.libraryFor(assetId); + }, resolverFor: 'a|lib/${p.basename(filePath)}'); +} + +String get _pkgName => 'pkg$_pkgCacheCount'; +int _pkgCacheCount = 1; diff --git a/codegen_build/test/src/np_subject_accessor.dart b/codegen_build/test/src/np_subject_accessor.dart new file mode 100644 index 00000000..825286ef --- /dev/null +++ b/codegen_build/test/src/np_subject_accessor.dart @@ -0,0 +1,7 @@ +// ignore_for_file: unused_import + +import 'package:np_codegen/np_codegen.dart'; +import 'package:rxdart/rxdart.dart'; + +@npSubjectAccessor +class Foo {} From 177d3c51fb1a8352304ec7eb9823f6dfb3c039b1 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sun, 25 Feb 2024 12:22:58 +0800 Subject: [PATCH 41/42] Don't reload files if nothing changes in remote sync --- app/lib/controller/files_controller.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/lib/controller/files_controller.dart b/app/lib/controller/files_controller.dart index 64357591..a64ec4d1 100644 --- a/app/lib/controller/files_controller.dart +++ b/app/lib/controller/files_controller.dart @@ -90,9 +90,10 @@ class FilesController { _c.touchManager.clearTouchCache(); final progress = IntProgress(account.roots.length); + var hasChange = false; for (final r in account.roots) { final dirPath = file_util.unstripPath(account, r); - await SyncDir(_c)( + hasChange |= await SyncDir(_c)( account, dirPath, onProgressUpdate: (value) { @@ -107,10 +108,13 @@ class FilesController { if (!isShareDirIncluded) { _log.info("[syncRemote] Explicitly scanning share folder"); - await SyncDir(_c)(account, shareDir.path, isRecursive: false); + hasChange |= + await SyncDir(_c)(account, shareDir.path, isRecursive: false); + } + if (hasChange) { + // load the synced content to stream + unawaited(_reload()); } - // load the synced content to stream - unawaited(_reload()); } finally { _isSyncing = false; } From 22ebcebfce0a3bdc4416aaf4bb5af8e29e1f0324 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sun, 25 Feb 2024 12:49:06 +0800 Subject: [PATCH 42/42] Optimize timezone conversion speed in HomePhotos --- app/lib/widget/home_photos/bloc.dart | 6 ++-- app/lib/widget/photo_list_util.dart | 54 +++++++++++++++++++--------- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/app/lib/widget/home_photos/bloc.dart b/app/lib/widget/home_photos/bloc.dart index a2e1e4a6..7492d59e 100644 --- a/app/lib/widget/home_photos/bloc.dart +++ b/app/lib/widget/home_photos/bloc.dart @@ -385,6 +385,7 @@ _ItemTransformerResult _buildItem(_ItemTransformerArgument arg) { ) : null; + final tzOffset = clock.now().timeZoneOffset; final transformed = <_Item>[]; for (int i = 0; i < sortedFiles.length; ++i) { final file = sortedFiles[i]; @@ -392,12 +393,13 @@ _ItemTransformerResult _buildItem(_ItemTransformerArgument arg) { if (item == null) { continue; } - final date = dateHelper?.onFile(file); + final localDate = file.fdDateTime.add(tzOffset); + final date = dateHelper?.onFile(file, localDate: localDate); if (date != null) { transformed.add(_DateItem(date: date, isMonthOnly: !arg.isGroupByDay)); } transformed.add(item); - memoryCollectionHelper?.addFile(file); + memoryCollectionHelper?.addFile(file, localDate: localDate); } final memoryCollections = memoryCollectionHelper ?.build((year) => L10n.of(arg.locale).memoryAlbumName(today.year - year)); diff --git a/app/lib/widget/photo_list_util.dart b/app/lib/widget/photo_list_util.dart index bf71a50c..f8b9335b 100644 --- a/app/lib/widget/photo_list_util.dart +++ b/app/lib/widget/photo_list_util.dart @@ -15,15 +15,20 @@ part 'photo_list_util.g.dart'; class DateGroupHelper { DateGroupHelper({ required this.isMonthOnly, - }); + }) : _tzOffset = clock.now().timeZoneOffset; - DateTime? onFile(FileDescriptor file) { - final newDate = file.fdDateTime.toLocal(); - if (newDate.year != _currentDate?.year || - newDate.month != _currentDate?.month || - (!isMonthOnly && newDate.day != _currentDate?.day)) { - _currentDate = newDate; - return newDate; + DateTime? onFile( + FileDescriptor file, { + DateTime? localDate, + }) { + // toLocal is way too slow + // final localDate = file.fdDateTime.toLocal(); + localDate ??= file.fdDateTime.add(_tzOffset); + if (localDate.year != _currentDate?.year || + localDate.month != _currentDate?.month || + (!isMonthOnly && localDate.day != _currentDate?.day)) { + _currentDate = localDate; + return localDate; } else { return null; } @@ -31,6 +36,7 @@ class DateGroupHelper { final bool isMonthOnly; DateTime? _currentDate; + final Duration _tzOffset; } /// Build memory collection from files @@ -42,20 +48,33 @@ class MemoryCollectionHelper { this.account, { DateTime? today, required int dayRange, - }) : today = (today?.toLocal() ?? clock.now()).toMidnight(), - dayRange = math.max(dayRange, 0); + }) : _tzOffset = clock.now().timeZoneOffset, + // today = (today?.toLocal() ?? clock.now()).toMidnight(), + dayRange = math.max(dayRange, 0) { + this.today = (today ?? clock.now()).toUtc().add(_tzOffset).toMidnight(); + } - void addFile(FileDescriptor f) { - final date = f.fdDateTime.toLocal().toMidnight(); - final diff = today.difference(date).inDays; + void addFile( + FileDescriptor f, { + DateTime? localDate, + }) { + // too slow + // final localDate = f.fdDateTime.toLocal().toMidnight(); + localDate = (localDate ?? f.fdDateTime.add(_tzOffset)).toMidnight(); + final diff = today.difference(localDate).inDays; if (diff < 300) { return; } for (final dy in [0, -1, 1]) { - if (today.copyWith(year: date.year + dy).difference(date).abs().inDays <= + if (today + .copyWith(year: localDate.year + dy) + .difference(localDate) + .abs() + .inDays <= dayRange) { - _log.fine("[addFile] Add file (${f.fdDateTime}) to ${date.year + dy}"); - _addFileToYear(f, date.year + dy); + _log.fine( + "[addFile] Add file (${f.fdDateTime}) to ${localDate.year + dy}"); + _addFileToYear(f, localDate.year + dy); break; } } @@ -96,8 +115,9 @@ class MemoryCollectionHelper { } final Account account; - final DateTime today; + late final DateTime today; final int dayRange; + final Duration _tzOffset; final _data = {}; }