Merge branch 'files-controller'

This commit is contained in:
Ming Ming 2024-03-17 16:57:02 +08:00
commit 99c8448bde
165 changed files with 6823 additions and 3144 deletions

View file

@ -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<void> _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());

View file

@ -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");

View file

@ -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<Object?> get props => [
file,
shareItems,
];
final File file;
final FileDescriptor file;
@Format(r"${$?.toReadableString()}")
final List<ListAlbumShareOutlierShareItem> shareItems;
}
@ -164,7 +165,6 @@ class ListAlbumShareOutlierBloc extends Bloc<ListAlbumShareOutlierBlocEvent,
ListAlbumShareOutlierBlocState> {
ListAlbumShareOutlierBloc(this._c)
: assert(require(_c)),
assert(ListShare.require(_c)),
super(ListAlbumShareOutlierBlocInit()) {
on<ListAlbumShareOutlierBlocEvent>(_onEvent);
}
@ -282,7 +282,7 @@ class ListAlbumShareOutlierBloc extends Bloc<ListAlbumShareOutlierBlocEvent,
});
} catch (e, stackTrace) {
_log.severe(
"[_processAlbumItems] Failed while _processSingleFile: ${logFilename(fi.file.path)}",
"[_processAlbumItems] Failed while _processSingleFile: ${logFilename(fi.file.fdPath)}",
e,
stackTrace);
errors.add(e);
@ -312,7 +312,7 @@ class ListAlbumShareOutlierBloc extends Bloc<ListAlbumShareOutlierBlocEvent,
.map((s) => 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<ListAlbumShareOutlierBlocEvent,
var missings = managedAlbumSharees
.difference(allSharees)
// Can't share to ourselves or the file owner
.where((s) => 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<ListAlbumShareOutlierBlocEvent,
.toSet();
final extras = ownedSharees.difference(albumSharees);
_log.info(
"[_processSingleFileItem] Extra shares: ${extras.toReadableString()} for file: ${logFilename(fileItem.file.path)}");
"[_processSingleFileItem] Extra shares: ${extras.toReadableString()} for file: ${logFilename(fileItem.file.fdPath)}");
for (final e in extras) {
try {
shareItems.add(ListAlbumShareOutlierExtraShareItem(
shares.firstWhere((s) => 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);

View file

@ -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()}}";
}
}

View file

@ -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<Object?> 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<ProgressBlocEvent, ProgressBlocState> {
ProgressBloc() : super(const ProgressBlocState(0, null)) {
on<ProgressBlocEvent>(_onEvent);
}
Future<void> _onEvent(
ProgressBlocEvent ev, Emitter<ProgressBlocState> emit) async {
_log.info("[_onEvent] $ev");
if (ev is ProgressBlocUpdate) {
await _onEventUpdate(ev, emit);
}
}
Future<void> _onEventUpdate(
ProgressBlocUpdate ev, Emitter<ProgressBlocState> emit) async {
emit(ProgressBlocState(ev.progress, ev.text));
}
}

View file

@ -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<FileDescriptor> files;
}
class ScanAccountDirBlocInit extends ScanAccountDirBlocState {
const ScanAccountDirBlocInit() : super(const []);
}
class ScanAccountDirBlocLoading extends ScanAccountDirBlocState {
const ScanAccountDirBlocLoading(
List<FileDescriptor> files, {
this.isInitialLoad = false,
}) : super(files);
final bool isInitialLoad;
}
class ScanAccountDirBlocSuccess extends ScanAccountDirBlocState {
const ScanAccountDirBlocSuccess(List<FileDescriptor> files) : super(files);
}
@toString
class ScanAccountDirBlocFailure extends ScanAccountDirBlocState {
const ScanAccountDirBlocFailure(List<FileDescriptor> 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<FileDescriptor> files)
: super(files);
}
/// A bloc that return all files under a dir recursively
///
/// See [ScanDir]
@npLog
class ScanAccountDirBloc
extends Bloc<ScanAccountDirBlocEvent, ScanAccountDirBlocState> {
ScanAccountDirBloc._(this.account) : super(const ScanAccountDirBlocInit()) {
final c = KiwiContainer().resolve<DiContainer>();
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<ScanAccountDirBlocEvent>(_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<ScanAccountDirBloc>(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<ScanAccountDirBloc>(bloc, name: name);
return bloc;
}
}
@override
Future<void> 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<void> _onEvent(ScanAccountDirBlocEvent event,
Emitter<ScanAccountDirBlocState> emit) async {
_log.info("[_onEvent] $event");
if (event is ScanAccountDirBlocQueryBase) {
await _onEventQuery(event, emit);
} else if (event is _ScanAccountDirBlocExternalEvent) {
await _onExternalEvent(event, emit);
}
}
Future<void> _onEventQuery(ScanAccountDirBlocQueryBase ev,
Emitter<ScanAccountDirBlocState> 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<bool> _syncOnline(
ScanAccountDirBlocQueryBase ev, {
ValueChanged<Progress>? 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<void> _onExternalEvent(_ScanAccountDirBlocExternalEvent ev,
Emitter<ScanAccountDirBlocState> 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<List<FileDescriptor>> _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<List<FileDescriptor>> _queryOffline(
ScanAccountDirBlocQueryBase ev) async {
final settings = AccountPref.of(account);
final shareDir =
File(path: file_util.unstripPath(account, settings.getShareFolderOr()));
bool isShareDirIncluded = false;
final files = <FileDescriptor>[];
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<FileRemovedEvent>(_onFileRemovedEvent);
late final _filePropertyUpdatedEventListener =
AppEventListener<FilePropertyUpdatedEvent>(_onFilePropertyUpdatedEvent);
late final _fileTrashbinRestoredEventListener =
AppEventListener<FileTrashbinRestoredEvent>(_onFileTrashbinRestoredEvent);
late final _fileMovedEventListener =
AppEventListener<FileMovedEvent>(_onFileMovedEvent);
late final _favoriteResyncedEventListener =
AppEventListener<FavoriteResyncedEvent>(_onFavoriteResyncedEvent);
late final _prefUpdatedEventListener =
AppEventListener<PrefUpdatedEvent>(_onPrefUpdatedEvent);
late final _accountPrefUpdatedEventListener =
AppEventListener<AccountPrefUpdatedEvent>(_onAccountPrefUpdatedEvent);
late final _nativeFileExifUpdatedListener = getRawPlatform() == NpPlatform.web
? null
: NativeEventListener<FileExifUpdatedEvent>(_onNativeFileExifUpdated);
Stream<ImageProcessorUploadSuccessEvent>?
get _imageProcessorUploadSuccessStream => getRawPlatform() ==
NpPlatform.web
? null
: ImageProcessor.stream.whereType<ImageProcessorUploadSuccessEvent>();
StreamSubscription? _imageProcessorUploadSuccessListener;
late final _refreshThrottler = Throttler(
onTriggered: (_) {
add(const _ScanAccountDirBlocExternalEvent());
},
logTag: "ScanAccountDirBloc.refresh",
);
static final _log = _$ScanAccountDirBlocNpLog.log;
}

View file

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

View file

@ -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<B extends StateStreamable<S>, S, T>
extends SingleChildStatelessWidget {
const BlocListenerT({
super.key,
required this.selector,
required this.listener,
});
@override
Widget buildWithChild(BuildContext context, Widget? child) {
return BlocListener<B, S>(
listenWhen: (previous, current) =>
selector(previous) != selector(current),
listener: (context, state) => listener(context, selector(state)),
child: child,
);
}
final BlocWidgetSelector<S, T> 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<State> on Emitter<State> {
onError: (_, __) {},
);
}
extension BlocExtension<E, S> on Bloc<E, S> {
void safeAdd(E event) {
if (!isClosed) {
add(event);
}
}
}

View file

@ -2,17 +2,26 @@ 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/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';
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 {
AccountController({
required this.prefController,
});
void setCurrentAccount(Account account) {
_account = account;
_collectionsController?.dispose();
_collectionsController = null;
_serverController?.dispose();
@ -29,6 +38,16 @@ class AccountController {
_sharingsController = null;
_placesController?.dispose();
_placesController = null;
_filesController?.dispose();
_filesController = null;
_metadataController?.dispose();
_metadataController = null;
_nativeEventRelay?.dispose();
_nativeEventRelay = NativeEventRelay(
filesController: filesController,
metadataController: metadataController,
);
}
Account get account => _account!;
@ -36,6 +55,7 @@ class AccountController {
CollectionsController get collectionsController =>
_collectionsController ??= CollectionsController(
KiwiContainer().resolve<DiContainer>(),
filesController: filesController,
account: _account!,
serverController: serverController,
);
@ -76,7 +96,24 @@ class AccountController {
account: _account!,
);
FilesController get filesController => _filesController ??= FilesController(
KiwiContainer().resolve<DiContainer>(),
account: _account!,
accountPrefController: accountPrefController,
);
MetadataController get metadataController =>
_metadataController ??= MetadataController(
KiwiContainer().resolve(),
account: account,
filesController: filesController,
prefController: prefController,
);
PrefController prefController;
Account? _account;
CollectionsController? _collectionsController;
ServerController? _serverController;
AccountPrefController? _accountPrefController;
@ -85,4 +122,8 @@ class AccountController {
SessionController? _sessionController;
SharingsController? _sharingsController;
PlacesController? _placesController;
FilesController? _filesController;
MetadataController? _metadataController;
NativeEventRelay? _nativeEventRelay;
}

View file

@ -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<String> get shareFolder => _shareFolderController.stream;
Future<void> setShareFolder(String value) => _set<String>(
controller: _shareFolderController,
setter: (pref, value) => pref.setShareFolder(value),
value: value,
);
ValueStream<String?> get accountLabel => _accountLabelController.stream;
Future<void> setAccountLabel(String? value) => _set<String?>(
controller: _accountLabelController,
setter: (pref, value) => pref.setAccountLabel(value),
value: value,
);
ValueStream<PersonProvider> get personProvider =>
_personProviderController.stream;
Future<void> setPersonProvider(PersonProvider value) => _set<PersonProvider>(
controller: _personProviderController,
setter: (pref, value) => pref.setPersonProvider(value.index),
value: value,
);
ValueStream<bool> get isEnableMemoryAlbum =>
_isEnableMemoryAlbumController.stream;
Future<void> setEnableMemoryAlbum(bool value) => _set<bool>(
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));
}

View file

@ -13,3 +13,34 @@ extension _$AccountPrefControllerNpLog on AccountPrefController {
static final log =
Logger("controller.account_pref_controller.AccountPrefController");
}
// **************************************************************************
// NpSubjectAccessorGenerator
// **************************************************************************
extension $AccountPrefControllerNpSubjectAccessor on AccountPrefController {
// _shareFolderController
ValueStream<String> get shareFolder => _shareFolderController.stream;
Stream<String> get shareFolderNew => shareFolder.skip(1);
Stream<String> get shareFolderChange => shareFolder.distinct().skip(1);
String get shareFolderValue => _shareFolderController.value;
// _accountLabelController
ValueStream<String?> get accountLabel => _accountLabelController.stream;
Stream<String?> get accountLabelNew => accountLabel.skip(1);
Stream<String?> get accountLabelChange => accountLabel.distinct().skip(1);
String? get accountLabelValue => _accountLabelController.value;
// _personProviderController
ValueStream<PersonProvider> get personProvider =>
_personProviderController.stream;
Stream<PersonProvider> get personProviderNew => personProvider.skip(1);
Stream<PersonProvider> get personProviderChange =>
personProvider.distinct().skip(1);
PersonProvider get personProviderValue => _personProviderController.value;
// _isEnableMemoryAlbumController
ValueStream<bool> get isEnableMemoryAlbum =>
_isEnableMemoryAlbumController.stream;
Stream<bool> get isEnableMemoryAlbumNew => isEnableMemoryAlbum.skip(1);
Stream<bool> get isEnableMemoryAlbumChange =>
isEnableMemoryAlbum.distinct().skip(1);
bool get isEnableMemoryAlbumValue => _isEnableMemoryAlbumController.value;
}

View file

@ -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,28 @@ class CollectionItemStreamData {
class CollectionItemsController {
CollectionItemsController(
this._c, {
required this.filesController,
required this.account,
required this.collection,
required this.onCollectionUpdated,
}) {
_fileRemovedEventListener.begin();
_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));
}
/// Dispose this controller and release all internal resources
///
/// MUST be called
void dispose() {
_fileRemovedEventListener.end();
for (final s in _subscriptions) {
s.cancel();
}
_dataStreamController.close();
}
@ -79,6 +89,8 @@ class CollectionItemsController {
/// Peek the stream and return the current value
CollectionItemStreamData peekStream() => _dataStreamController.stream.value;
ValueStream<int?> get countStream => _countStreamController.stream;
/// Add list of [files] to [collection]
Future<void> addFiles(List<FileDescriptor> files) async {
final isInited = _isDataStreamInited;
@ -309,26 +321,36 @@ 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<void> _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
.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,
));
});
}
final DiContainer _c;
final FilesController filesController;
final Account account;
Collection collection;
ValueChanged<Collection> onCollectionUpdated;
@ -340,9 +362,8 @@ class CollectionItemsController {
hasNext: true,
),
);
late final _fileRemovedEventListener =
AppEventListener<FileRemovedEvent>(_onFileRemovedEvent);
late final BehaviorSubject<int?> _countStreamController;
final _mutex = Mutex();
final _subscriptions = <StreamSubscription>[];
}

View file

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

View file

@ -0,0 +1,439 @@
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/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';
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';
part 'files_controller.g.dart';
abstract class FilesStreamEvent {
/// All files as a ordered list
List<FileDescriptor> get data;
/// All files as a map with the fileId as key
Map<int, FileDescriptor> get dataMap;
bool get hasNext;
}
@npLog
class FilesController {
FilesController(
this._c, {
required this.account,
required this.accountPrefController,
}) {
_subscriptions.add(accountPrefController.shareFolderChange.listen((event) {
// sync remote if share folder is modified
if (_isDataStreamInited) {
syncRemote();
}
}));
}
void dispose() {
for (final s in _subscriptions) {
s.cancel();
}
_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<FilesStreamEvent> get stream {
if (!_isDataStreamInited) {
_isDataStreamInited = true;
_load();
}
return _dataStreamController.stream;
}
Future<void> 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.shareFolderValue),
);
var isShareDirIncluded = false;
_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);
hasChange |= 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");
hasChange |=
await SyncDir(_c)(account, shareDir.path, isRecursive: false);
}
if (hasChange) {
// load the synced content to stream
unawaited(_reload());
}
} finally {
_isSyncing = false;
}
}
/// Update files property and return number of files updated
Future<void> updateProperty(
List<FileDescriptor> files, {
OrNull<Metadata>? metadata,
OrNull<bool>? isArchived,
OrNull<DateTime>? overrideDateTime,
bool? isFavorite,
OrNull<ImageLocation>? location,
Exception? Function(List<int> fileIds) errorBuilder =
UpdatePropertyFailureError.new,
}) async {
final backups = <int, FileDescriptor>{};
// file ids that need to be queried again to get the correct
// FileDescriptor.fdDateTime
final outdated = <int>[];
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 = <int>[];
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(
"[updateProperty] 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<void> remove(
List<FileDescriptor> files, {
Exception? Function(List<int> fileIds) errorBuilder =
RemoveFailureError.new,
}) async {
final backups = <int, FileDescriptor>{};
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("[remove] File not found: $f");
continue;
}
backups[f.fdId] = original;
}
_dataStreamController
.addWithValue((value) => value.copyWith(files: next));
});
final failures = <int>[];
try {
await Remove(_c)(
account,
files,
onError: (index, value, error, stackTrace) {
_log.severe(
"[remove] Failed while Remove: ${logFilename(value.fdPath)}",
error,
stackTrace);
failures.add(value.fdId);
},
);
} catch (e, stackTrace) {
_log.severe("[remove] 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<void> applySyncResult({
DbSyncIdResult? favorites,
List<int>? 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<int, FileDescriptor> 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<int, FileDescriptor> next, List<FileDescriptor> results) {
for (final f in results) {
next[f.fdId] = f;
}
}
Future<void> _load() async {
var lastData = _FilesStreamEvent(
files: const {},
hasNext: false,
);
final completer = Completer();
ListFile(_c)(
account,
file_util.unstripPath(account, accountPrefController.shareFolderValue),
).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));
}
Future<void> _reload() async {
var results = <FileDescriptor>[];
final completer = Completer();
ListFile(_c)(
account,
file_util.unstripPath(account, accountPrefController.shareFolderValue),
).listen(
(ev) {
results = ev;
},
onError: _dataStreamController.addError,
onDone: () => completer.complete(),
);
await completer.future;
_dataStreamController
.add(_convertListResultsToEvent(results, hasNext: false));
}
_FilesStreamEvent _convertListResultsToEvent(
List<FileDescriptor> 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();
var _isSyncing = false;
final _subscriptions = <StreamSubscription>[];
}
@toString
class UpdatePropertyFailureError implements Exception {
const UpdatePropertyFailureError(this.fileIds);
@override
String toString() => _$toString();
final List<int> fileIds;
}
@toString
class RemoveFailureError implements Exception {
const RemoveFailureError(this.fileIds);
@override
String toString() => _$toString();
final List<int> fileIds;
}
class _FilesStreamEvent implements FilesStreamEvent {
_FilesStreamEvent({
required this.files,
Lazy<List<FileDescriptor>>? dataLazy,
required this.hasNext,
}) {
this.dataLazy = dataLazy ?? (Lazy(() => files.values.toList()));
}
_FilesStreamEvent copyWith({
Map<int, FileDescriptor>? files,
bool? hasNext,
}) {
return _FilesStreamEvent(
files: files ?? this.files,
dataLazy: (files == null) ? dataLazy : null,
hasNext: hasNext ?? this.hasNext,
);
}
@override
List<FileDescriptor> get data => dataLazy();
@override
Map<int, FileDescriptor> get dataMap => files;
final Map<int, FileDescriptor> files;
late final Lazy<List<FileDescriptor>> dataLazy;
/// If true, the results are intermediate values and may not represent the
/// latest state
@override
final bool hasNext;
}

View file

@ -1,32 +1,32 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'progress.dart';
part of 'files_controller.dart';
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$ProgressBlocNpLog on ProgressBloc {
extension _$FilesControllerNpLog on FilesController {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("bloc.progress.ProgressBloc");
static final log = Logger("controller.files_controller.FilesController");
}
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$ProgressBlocUpdateToString on ProgressBlocUpdate {
extension _$UpdatePropertyFailureErrorToString on UpdatePropertyFailureError {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "ProgressBlocUpdate {progress: ${progress.toStringAsFixed(3)}, text: $text}";
return "UpdatePropertyFailureError {fileIds: [length: ${fileIds.length}]}";
}
}
extension _$ProgressBlocStateToString on ProgressBlocState {
extension _$RemoveFailureErrorToString on RemoveFailureError {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "ProgressBlocState {progress: ${progress.toStringAsFixed(3)}, text: $text}";
return "RemoveFailureError {fileIds: [length: ${fileIds.length}]}";
}
}

View file

@ -0,0 +1,98 @@
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.isEnableExifChange.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<void> _onFilesEvent(FilesStreamEvent ev) async {
_log.info("[_onFilesEvent]");
if (!prefController.isEnableExifValue) {
// 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<void> _startMetadataTask(List<FileDescriptor> 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 = <StreamSubscription>[];
var _hasStarted = false;
}

View file

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

View file

@ -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(

View file

@ -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,21 +14,21 @@ import 'package:rxdart/rxdart.dart';
part 'pref_controller.g.dart';
@npLog
@npSubjectAccessor
class PrefController {
PrefController(this._c);
ValueStream<language_util.AppLanguage> get language =>
_languageController.stream;
Future<void> setAppLanguage(language_util.AppLanguage value) =>
_set<language_util.AppLanguage>(
Future<void> setAppLanguage(AppLanguage value) => _set<AppLanguage>(
controller: _languageController,
setter: (pref, value) => pref.setLanguage(value.langId),
value: value,
);
ValueStream<int> get albumBrowserZoomLevel =>
_albumBrowserZoomLevelController.stream;
Future<void> setHomePhotosZoomLevel(int value) => _set<int>(
controller: _homePhotosZoomLevelController,
setter: (pref, value) => pref.setHomePhotosZoomLevel(value),
value: value,
);
Future<void> setAlbumBrowserZoomLevel(int value) => _set<int>(
controller: _albumBrowserZoomLevelController,
@ -36,103 +36,72 @@ class PrefController {
value: value,
);
ValueStream<int> get homeAlbumsSort => _homeAlbumsSortController.stream;
Future<void> setHomeAlbumsSort(int value) => _set<int>(
controller: _homeAlbumsSortController,
setter: (pref, value) => pref.setHomeAlbumsSort(value),
value: value,
);
ValueStream<bool> get isEnableExif => _isEnableExifController.stream;
Future<void> setEnableExif(bool value) => _set<bool>(
controller: _isEnableExifController,
setter: (pref, value) => pref.setEnableExif(value),
value: value,
);
ValueStream<bool> get shouldProcessExifWifiOnly =>
_shouldProcessExifWifiOnlyController.stream;
Future<void> setProcessExifWifiOnly(bool value) => _set<bool>(
controller: _shouldProcessExifWifiOnlyController,
setter: (pref, value) => pref.setProcessExifWifiOnly(value),
value: value,
);
ValueStream<int> get memoriesRange => _memoriesRangeController.stream;
Future<void> setMemoriesRange(int value) => _set<int>(
controller: _memoriesRangeController,
setter: (pref, value) => pref.setMemoriesRange(value),
value: value,
);
ValueStream<bool> get isPhotosTabSortByName =>
_isPhotosTabSortByNameController.stream;
Future<void> setPhotosTabSortByName(bool value) => _set<bool>(
controller: _isPhotosTabSortByNameController,
setter: (pref, value) => pref.setPhotosTabSortByName(value),
value: value,
);
ValueStream<int> get viewerScreenBrightness =>
_viewerScreenBrightnessController.stream;
Future<void> setViewerScreenBrightness(int value) => _set<int>(
controller: _viewerScreenBrightnessController,
setter: (pref, value) => pref.setViewerScreenBrightness(value),
value: value,
);
ValueStream<bool> get isViewerForceRotation =>
_isViewerForceRotationController.stream;
Future<void> setViewerForceRotation(bool value) => _set<bool>(
controller: _isViewerForceRotationController,
setter: (pref, value) => pref.setViewerForceRotation(value),
value: value,
);
ValueStream<GpsMapProvider> get gpsMapProvider =>
_gpsMapProviderController.stream;
Future<void> setGpsMapProvider(GpsMapProvider value) => _set<GpsMapProvider>(
controller: _gpsMapProviderController,
setter: (pref, value) => pref.setGpsMapProvider(value.index),
value: value,
);
ValueStream<bool> get isAlbumBrowserShowDate =>
_isAlbumBrowserShowDateController.stream;
Future<void> setAlbumBrowserShowDate(bool value) => _set<bool>(
controller: _isAlbumBrowserShowDateController,
setter: (pref, value) => pref.setAlbumBrowserShowDate(value),
value: value,
);
ValueStream<bool> get isDoubleTapExit => _isDoubleTapExitController.stream;
Future<void> setDoubleTapExit(bool value) => _set<bool>(
controller: _isDoubleTapExitController,
setter: (pref, value) => pref.setDoubleTapExit(value),
value: value,
);
ValueStream<bool> get isSaveEditResultToServer =>
_isSaveEditResultToServerController.stream;
Future<void> setSaveEditResultToServer(bool value) => _set<bool>(
controller: _isSaveEditResultToServerController,
setter: (pref, value) => pref.setSaveEditResultToServer(value),
value: value,
);
ValueStream<SizeInt> get enhanceMaxSize => _enhanceMaxSizeController.stream;
Future<void> setEnhanceMaxSize(SizeInt value) => _set<SizeInt>(
controller: _enhanceMaxSizeController,
setter: (pref, value) async {
@ -145,34 +114,24 @@ class PrefController {
value: value,
);
ValueStream<bool> get isDarkTheme => _isDarkThemeController.stream;
Future<void> setDarkTheme(bool value) => _set<bool>(
controller: _isDarkThemeController,
setter: (pref, value) => pref.setDarkTheme(value),
value: value,
);
ValueStream<bool> get isFollowSystemTheme =>
_isFollowSystemThemeController.stream;
Future<void> setFollowSystemTheme(bool value) => _set<bool>(
controller: _isFollowSystemThemeController,
setter: (pref, value) => pref.setFollowSystemTheme(value),
value: value,
);
ValueStream<bool> get isUseBlackInDarkTheme =>
_isUseBlackInDarkThemeController.stream;
Future<void> setUseBlackInDarkTheme(bool value) => _set<bool>(
controller: _isUseBlackInDarkThemeController,
setter: (pref, value) => pref.setUseBlackInDarkTheme(value),
value: value,
);
ValueStream<Color?> get seedColor => _seedColorController.stream;
Future<void> setSeedColor(Color? value) => _setOrRemove<Color>(
controller: _seedColorController,
setter: (pref, value) => pref.setSeedColor(value.withAlpha(0xFF).value),
@ -226,49 +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<Color?>.seeded(_c.pref.getSeedColor()?.run(Color.new));
}

View file

@ -12,3 +12,133 @@ extension _$PrefControllerNpLog on PrefController {
static final log = Logger("controller.pref_controller.PrefController");
}
// **************************************************************************
// NpSubjectAccessorGenerator
// **************************************************************************
extension $PrefControllerNpSubjectAccessor on PrefController {
// _languageController
ValueStream<AppLanguage> get language => _languageController.stream;
Stream<AppLanguage> get languageNew => language.skip(1);
Stream<AppLanguage> get languageChange => language.distinct().skip(1);
AppLanguage get languageValue => _languageController.value;
// _homePhotosZoomLevelController
ValueStream<int> get homePhotosZoomLevel =>
_homePhotosZoomLevelController.stream;
Stream<int> get homePhotosZoomLevelNew => homePhotosZoomLevel.skip(1);
Stream<int> get homePhotosZoomLevelChange =>
homePhotosZoomLevel.distinct().skip(1);
int get homePhotosZoomLevelValue => _homePhotosZoomLevelController.value;
// _albumBrowserZoomLevelController
ValueStream<int> get albumBrowserZoomLevel =>
_albumBrowserZoomLevelController.stream;
Stream<int> get albumBrowserZoomLevelNew => albumBrowserZoomLevel.skip(1);
Stream<int> get albumBrowserZoomLevelChange =>
albumBrowserZoomLevel.distinct().skip(1);
int get albumBrowserZoomLevelValue => _albumBrowserZoomLevelController.value;
// _homeAlbumsSortController
ValueStream<int> get homeAlbumsSort => _homeAlbumsSortController.stream;
Stream<int> get homeAlbumsSortNew => homeAlbumsSort.skip(1);
Stream<int> get homeAlbumsSortChange => homeAlbumsSort.distinct().skip(1);
int get homeAlbumsSortValue => _homeAlbumsSortController.value;
// _isEnableExifController
ValueStream<bool> get isEnableExif => _isEnableExifController.stream;
Stream<bool> get isEnableExifNew => isEnableExif.skip(1);
Stream<bool> get isEnableExifChange => isEnableExif.distinct().skip(1);
bool get isEnableExifValue => _isEnableExifController.value;
// _shouldProcessExifWifiOnlyController
ValueStream<bool> get shouldProcessExifWifiOnly =>
_shouldProcessExifWifiOnlyController.stream;
Stream<bool> get shouldProcessExifWifiOnlyNew =>
shouldProcessExifWifiOnly.skip(1);
Stream<bool> get shouldProcessExifWifiOnlyChange =>
shouldProcessExifWifiOnly.distinct().skip(1);
bool get shouldProcessExifWifiOnlyValue =>
_shouldProcessExifWifiOnlyController.value;
// _memoriesRangeController
ValueStream<int> get memoriesRange => _memoriesRangeController.stream;
Stream<int> get memoriesRangeNew => memoriesRange.skip(1);
Stream<int> get memoriesRangeChange => memoriesRange.distinct().skip(1);
int get memoriesRangeValue => _memoriesRangeController.value;
// _isPhotosTabSortByNameController
ValueStream<bool> get isPhotosTabSortByName =>
_isPhotosTabSortByNameController.stream;
Stream<bool> get isPhotosTabSortByNameNew => isPhotosTabSortByName.skip(1);
Stream<bool> get isPhotosTabSortByNameChange =>
isPhotosTabSortByName.distinct().skip(1);
bool get isPhotosTabSortByNameValue => _isPhotosTabSortByNameController.value;
// _viewerScreenBrightnessController
ValueStream<int> get viewerScreenBrightness =>
_viewerScreenBrightnessController.stream;
Stream<int> get viewerScreenBrightnessNew => viewerScreenBrightness.skip(1);
Stream<int> get viewerScreenBrightnessChange =>
viewerScreenBrightness.distinct().skip(1);
int get viewerScreenBrightnessValue =>
_viewerScreenBrightnessController.value;
// _isViewerForceRotationController
ValueStream<bool> get isViewerForceRotation =>
_isViewerForceRotationController.stream;
Stream<bool> get isViewerForceRotationNew => isViewerForceRotation.skip(1);
Stream<bool> get isViewerForceRotationChange =>
isViewerForceRotation.distinct().skip(1);
bool get isViewerForceRotationValue => _isViewerForceRotationController.value;
// _gpsMapProviderController
ValueStream<GpsMapProvider> get gpsMapProvider =>
_gpsMapProviderController.stream;
Stream<GpsMapProvider> get gpsMapProviderNew => gpsMapProvider.skip(1);
Stream<GpsMapProvider> get gpsMapProviderChange =>
gpsMapProvider.distinct().skip(1);
GpsMapProvider get gpsMapProviderValue => _gpsMapProviderController.value;
// _isAlbumBrowserShowDateController
ValueStream<bool> get isAlbumBrowserShowDate =>
_isAlbumBrowserShowDateController.stream;
Stream<bool> get isAlbumBrowserShowDateNew => isAlbumBrowserShowDate.skip(1);
Stream<bool> get isAlbumBrowserShowDateChange =>
isAlbumBrowserShowDate.distinct().skip(1);
bool get isAlbumBrowserShowDateValue =>
_isAlbumBrowserShowDateController.value;
// _isDoubleTapExitController
ValueStream<bool> get isDoubleTapExit => _isDoubleTapExitController.stream;
Stream<bool> get isDoubleTapExitNew => isDoubleTapExit.skip(1);
Stream<bool> get isDoubleTapExitChange => isDoubleTapExit.distinct().skip(1);
bool get isDoubleTapExitValue => _isDoubleTapExitController.value;
// _isSaveEditResultToServerController
ValueStream<bool> get isSaveEditResultToServer =>
_isSaveEditResultToServerController.stream;
Stream<bool> get isSaveEditResultToServerNew =>
isSaveEditResultToServer.skip(1);
Stream<bool> get isSaveEditResultToServerChange =>
isSaveEditResultToServer.distinct().skip(1);
bool get isSaveEditResultToServerValue =>
_isSaveEditResultToServerController.value;
// _enhanceMaxSizeController
ValueStream<SizeInt> get enhanceMaxSize => _enhanceMaxSizeController.stream;
Stream<SizeInt> get enhanceMaxSizeNew => enhanceMaxSize.skip(1);
Stream<SizeInt> get enhanceMaxSizeChange => enhanceMaxSize.distinct().skip(1);
SizeInt get enhanceMaxSizeValue => _enhanceMaxSizeController.value;
// _isDarkThemeController
ValueStream<bool> get isDarkTheme => _isDarkThemeController.stream;
Stream<bool> get isDarkThemeNew => isDarkTheme.skip(1);
Stream<bool> get isDarkThemeChange => isDarkTheme.distinct().skip(1);
bool get isDarkThemeValue => _isDarkThemeController.value;
// _isFollowSystemThemeController
ValueStream<bool> get isFollowSystemTheme =>
_isFollowSystemThemeController.stream;
Stream<bool> get isFollowSystemThemeNew => isFollowSystemTheme.skip(1);
Stream<bool> get isFollowSystemThemeChange =>
isFollowSystemTheme.distinct().skip(1);
bool get isFollowSystemThemeValue => _isFollowSystemThemeController.value;
// _isUseBlackInDarkThemeController
ValueStream<bool> get isUseBlackInDarkTheme =>
_isUseBlackInDarkThemeController.stream;
Stream<bool> get isUseBlackInDarkThemeNew => isUseBlackInDarkTheme.skip(1);
Stream<bool> get isUseBlackInDarkThemeChange =>
isUseBlackInDarkTheme.distinct().skip(1);
bool get isUseBlackInDarkThemeValue => _isUseBlackInDarkThemeController.value;
// _seedColorController
ValueStream<Color?> get seedColor => _seedColorController.stream;
Stream<Color?> get seedColorNew => seedColor.skip(1);
Stream<Color?> get seedColorChange => seedColor.distinct().skip(1);
Color? get seedColorValue => _seedColorController.value;
}

View file

@ -9,9 +9,11 @@ class SessionController {
_hasFiredMetadataTaskController.close();
}
@Deprecated("Use MetadataController")
ValueStream<bool> get hasFiredMetadataTask =>
_hasFiredMetadataTaskController.stream;
@Deprecated("Use MetadataController")
void setFiredMetadataTask(bool value) {
_hasFiredMetadataTaskController.add(value);
}

View file

@ -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<void> requestSync(
Account account, PersonProvider personProvider) async {
Future<void> 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<void> requestResync(
Account account, PersonProvider personProvider) async {
Future<void> 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;

View file

@ -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>? albumRepo,
OrNull<AlbumRepo2>? albumRepo2,
OrNull<FileRepo>? fileRepo,
OrNull<FileRepo2>? fileRepo2,
OrNull<ShareRepo>? shareRepo,
OrNull<ShareeRepo>? shareeRepo,
OrNull<FavoriteRepo>? 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;

View file

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

View file

@ -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<AlbumFileItem>()
.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;
}

View file

@ -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(

View file

@ -1,9 +1,9 @@
import 'package:copy_with/copy_with.dart';
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 +57,13 @@ abstract class AlbumItem with EquatableMixin {
JsonObj toContentJson();
bool compareServerIdentity(AlbumItem other);
@override
String toString() => _$toString();
@override
get props => [
List<Object?> get props => [
addedBy,
addedAt,
];
@ -72,32 +74,23 @@ abstract class AlbumItem with EquatableMixin {
static final _log = _$AlbumItemNpLog.log;
}
@genCopyWith
@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<String, dynamic>()),
file: FileDescriptor.fromJson(json["file"].cast<String, dynamic>()),
ownerId: (json["ownerId"] as String).toCi(),
);
}
@ -105,37 +98,29 @@ class AlbumFileItem extends AlbumItem {
String toString() => _$toString();
@override
toContentJson() {
JsonObj toContentJson() {
return {
"file": file.toJson(),
"file": file.toFdJson(),
"ownerId": ownerId.raw,
};
}
AlbumFileItem copyWith({
CiString? addedBy,
DateTime? addedAt,
File? file,
}) {
return AlbumFileItem(
addedBy: addedBy ?? this.addedBy,
addedAt: addedAt ?? this.addedAt,
file: file ?? this.file,
);
}
AlbumFileItem minimize() => AlbumFileItem(
addedBy: addedBy,
addedAt: addedAt,
file: file.copyWith(metadata: const OrNull(null)),
);
@override
bool compareServerIdentity(AlbumItem other) =>
other is AlbumFileItem &&
file.compareServerIdentity(other.file) &&
addedBy == other.addedBy &&
addedAt == other.addedAt;
@override
get props => [
List<Object?> 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 +146,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 +172,7 @@ class AlbumLabelItem extends AlbumItem {
}
@override
get props => [
List<Object?> get props => [
...super.props,
text,
];

View file

@ -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
// **************************************************************************
@ -27,7 +68,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}";
}
}

View file

@ -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<String, dynamic>();
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<String, dynamic>();
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;

View file

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

View file

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

View file

@ -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) {

View file

@ -8,6 +8,10 @@ abstract class CollectionItem {
abstract class CollectionFileItem implements CollectionItem {
const CollectionFileItem();
CollectionFileItem copyWith({
FileDescriptor? file,
});
FileDescriptor get file;
}

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

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

View file

@ -0,0 +1,230 @@
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<List<FileDescriptor>> getFileDescriptors(
Account account, String shareDirPath) {
throw UnsupportedError("getFileDescriptors not supported");
}
@override
Future<void> updateProperty(
Account account,
FileDescriptor f, {
OrNull<Metadata>? metadata,
OrNull<bool>? isArchived,
OrNull<DateTime>? overrideDateTime,
bool? favorite,
OrNull<ImageLocation>? 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<void> 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<List<FileDescriptor>> 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
Future<void> updateProperty(
Account account,
FileDescriptor f, {
OrNull<Metadata>? metadata,
OrNull<bool>? isArchived,
OrNull<DateTime>? overrideDateTime,
bool? favorite,
OrNull<ImageLocation>? 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<void> remove(Account account, FileDescriptor f) async {
_log.info("[remove] ${f.fdPath}");
await db.deleteFile(
account: account.toDb(),
file: f.toDbKey(),
);
}
Future<List<FileDescriptor>> _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<List<FileDescriptor>> _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.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) {
_log.shout(
"[_getCompleteFileDescriptors] Failed while getFilesByDirKey: ${logFilename(shareDirPath)}",
e,
stackTrace);
}
}
return results;
}
final NpDb db;
static const _partialCount = 100;
}

View file

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

View file

@ -110,20 +110,6 @@ class FileSqliteCacheUpdater {
final DiContainer _c;
}
class FileSqliteCacheRemover {
const FileSqliteCacheRemover(this._c);
/// Remove a file/dir from cache
Future<void> 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);

View file

@ -0,0 +1,148 @@
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]
///
/// 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.
/// In any case, each event is guaranteed to be one complete set of data
Stream<List<FileDescriptor>> getFileDescriptors(
Account account, String shareDirPath);
Future<void> updateProperty(
Account account,
FileDescriptor f, {
OrNull<Metadata>? metadata,
OrNull<bool>? isArchived,
OrNull<DateTime>? overrideDateTime,
bool? favorite,
OrNull<ImageLocation>? location,
});
Future<void> 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<List<FileDescriptor>> getFileDescriptors(
Account account, String shareDirPath) =>
dataSrc.getFileDescriptors(account, shareDirPath);
@override
Future<void> updateProperty(
Account account,
FileDescriptor f, {
OrNull<Metadata>? metadata,
OrNull<bool>? isArchived,
OrNull<DateTime>? overrideDateTime,
bool? favorite,
OrNull<ImageLocation>? location,
}) =>
dataSrc.updateProperty(
account,
f,
metadata: metadata,
isArchived: isArchived,
overrideDateTime: overrideDateTime,
favorite: favorite,
location: location,
);
@override
Future<void> 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<List<FileDescriptor>> getFileDescriptors(
Account account, String shareDirPath) =>
cacheDataSrc.getFileDescriptors(account, shareDirPath);
@override
Future<void> updateProperty(
Account account,
FileDescriptor f, {
OrNull<Metadata>? metadata,
OrNull<bool>? isArchived,
OrNull<DateTime>? overrideDateTime,
bool? favorite,
OrNull<ImageLocation>? 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<void> 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]
///
/// Returned files are sorted by time in descending order
Stream<List<FileDescriptor>> getFileDescriptors(
Account account, String shareDirPath);
Future<void> updateProperty(
Account account,
FileDescriptor f, {
OrNull<Metadata>? metadata,
OrNull<bool>? isArchived,
OrNull<DateTime>? overrideDateTime,
bool? favorite,
OrNull<ImageLocation>? location,
});
Future<void> remove(Account account, FileDescriptor f);
}

View file

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

View file

@ -55,7 +55,7 @@ class FileDescriptor with EquatableMixin {
JsonObj toFdJson() => toJson(this);
@override
get props => [
List<Object?> 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;
}

View file

@ -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]
@ -64,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) {

View file

@ -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<bool> _set<T>(PrefKey key, T value,
Future<bool> Function(PrefKey key, T value) setFn) async {
if (await setFn(key, value)) {
KiwiContainer().resolve<EventBus>().fire(PrefUpdatedEvent(key, value));
return true;
} else {
return false;
}
}
Future<bool> Function(PrefKey key, T value) setFn) =>
setFn(key, value);
Future<bool> _remove(PrefKey key) => provider.remove(key);
@ -69,16 +60,8 @@ class AccountPref {
Future<JsonObj> toJson() => provider.toJson();
Future<bool> _set<T>(AccountPrefKey key, T value,
Future<bool> Function(AccountPrefKey key, T value) setFn) async {
if (await setFn(key, value)) {
KiwiContainer()
.resolve<EventBus>()
.fire(AccountPrefUpdatedEvent(this, key, value));
return true;
} else {
return false;
}
}
Future<bool> Function(AccountPrefKey key, T value) setFn) =>
setFn(key, value);
Future<bool> _remove(AccountPrefKey key) => provider.remove(key);

View file

@ -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<Share>> 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<Share> create(Account account, File file, String shareWith) =>
Future<Share> 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<Share>> list(
Account account,
File file, {
FileDescriptor file, {
bool? isIncludeReshare,
});
@ -210,7 +212,7 @@ abstract class ShareDataSource {
Future<List<Share>> reverseListAll(Account account);
/// Share a file/folder with a user
Future<Share> create(Account account, File file, String shareWith);
Future<Share> create(Account account, FileDescriptor file, String shareWith);
/// Share a file/folder with a share link
///

View file

@ -18,12 +18,12 @@ part 'data_source.g.dart';
@npLog
class ShareRemoteDataSource implements ShareDataSource {
@override
list(
Future<List<Share>> 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<Share> 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,

View file

@ -37,6 +37,7 @@ class AppEventListener<T> {
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);
@ -49,7 +50,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
@ -89,6 +90,7 @@ class ShareRemovedEvent {
final Share share;
}
@Deprecated("not fired anymore, to be removed")
class FavoriteResyncedEvent {
const FavoriteResyncedEvent(this.account);
@ -115,6 +117,7 @@ class MetadataTaskStateChangedEvent {
final MetadataTaskState state;
}
@Deprecated("not fired anymore, to be removed")
class PrefUpdatedEvent {
PrefUpdatedEvent(this.key, this.value);

View file

@ -4,7 +4,11 @@ import 'dart:convert';
import 'package:logging/logging.dart';
import 'package:nc_photos/stream_extension.dart';
import 'package:np_platform_message_relay/np_platform_message_relay.dart';
import 'package:to_string/to_string.dart';
part 'native_event.g.dart';
@Deprecated("See AccountController.NativeEventRelay")
class NativeEventListener<T> {
NativeEventListener(this.listener);
@ -28,7 +32,7 @@ class NativeEventListener<T> {
static final _mappedStream =
MessageRelay.stream.whereType<Message>().map((ev) {
switch (ev.event) {
case FileExifUpdatedEvent._id:
case FileExifUpdatedEvent.id:
return FileExifUpdatedEvent.fromEvent(ev);
default:
@ -43,24 +47,28 @@ class NativeEventListener<T> {
Logger("event.native_event.NativeEventListener<${T.runtimeType}>");
}
@toString
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<int>());
}
Message toEvent() => Message(
_id,
id,
jsonEncode({
"fileIds": fileIds,
}),
);
static const _id = "FileExifUpdatedEvent";
@override
String toString() => _$toString();
static const id = "FileExifUpdatedEvent";
final List<int> fileIds;
}

View file

@ -0,0 +1,14 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'native_event.dart';
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$FileExifUpdatedEventToString on FileExifUpdatedEvent {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "FileExifUpdatedEvent {fileIds: [length: ${fileIds.length}]}";
}
}

View file

@ -0,0 +1,62 @@
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,
}) {
_subscriptions.add(MessageRelay.stream.whereType<Message>().listen((event) {
switch (event.event) {
case FileExifUpdatedEvent.id:
_onFileExifUpdatedEvent(FileExifUpdatedEvent.fromEvent(event));
break;
default:
_log.severe('Unknown event: ${event.event}');
break;
}
}));
if (features.isSupportEnhancement) {
_subscriptions.add(ImageProcessor.stream
.whereType<ImageProcessorUploadSuccessEvent>()
.listen(_onImageProcessorUploadSuccessEvent));
}
}
void dispose() {
for (final s in _subscriptions) {
s.cancel();
}
}
void _onFileExifUpdatedEvent(FileExifUpdatedEvent ev) {
_log.info(ev);
filesController.applySyncResult(fileExifs: ev.fileIds);
}
void _onImageProcessorUploadSuccessEvent(
ImageProcessorUploadSuccessEvent ev) {
_log.info(ev);
filesController.syncRemote();
metadataController.scheduleNext();
}
final FilesController filesController;
final MetadataController metadataController;
final _subscriptions = <StreamSubscription>[];
}

View file

@ -1,14 +1,14 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'home_photos.dart';
part of 'native_event_relay.dart';
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$_HomePhotosStateNpLog on _HomePhotosState {
extension _$NativeEventRelayNpLog on NativeEventRelay {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("widget.home_photos._HomePhotosState");
static final log = Logger("event.native_event_relay.NativeEventRelay");
}

View file

@ -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,

View file

@ -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();

View file

@ -112,7 +112,7 @@ ThemeData buildDarkTheme(BuildContext context, [ColorScheme? dynamicScheme]) {
}
Color? getSeedColor(BuildContext context) {
return context.read<PrefController>().seedColor.value;
return context.read<PrefController>().seedColorValue;
}
ColorScheme _getColorScheme(

View file

@ -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<AlbumItem>(
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<AlbumFileItem>().map((e) => e.file).toList();
if (newFiles.isNotEmpty) {
await _shareFiles(account, newAlbum, newFiles);
final newFileItems = addItems.whereType<AlbumFileItem>().toList();
if (newFileItems.isNotEmpty) {
await _shareFiles(account, newAlbum, newFileItems);
}
}
@ -82,7 +78,7 @@ class AddFileToAlbum {
}
Future<void> _shareFiles(
Account account, Album album, List<File> files) async {
Account account, Album album, List<AlbumFileItem> 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;
}

View file

@ -19,8 +19,6 @@ part 'remove_album.g.dart';
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) =>

View file

@ -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<Album> 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<void> _unshareFiles(
Account account, Album album, List<File> files) async {
Account account, Album album, List<FileDescriptor> files) async {
final albumShares = (album.shares!.map((e) => e.userId).toList()
..add(album.albumFile!.ownerId ?? account.userId))
.where((element) => element != account.userId)

View file

@ -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<File>? onShareFileFailed,
ErrorWithValueHandler<FileDescriptor>? onShareFileFailed,
}) async {
assert(album.provider is AlbumStaticProvider);
final newShares = (album.shares ?? [])
@ -56,12 +56,12 @@ class ShareAlbumWithUser {
Account account,
Album album,
CiString shareWith, {
ErrorWithValueHandler<File>? onShareFileFailed,
ErrorWithValueHandler<FileDescriptor>? onShareFileFailed,
}) async {
final files = AlbumStaticProvider.of(album)
.items
.whereType<AlbumFileItem>()
.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);

View file

@ -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) =>

View file

@ -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<void> call(
Account account,
Album album,
List<File> files,
List<FileDescriptor> files,
List<CiString> unshareWith, {
ErrorWithValueHandler<Share>? 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");

View file

@ -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(

View file

@ -1,11 +1,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/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 +13,14 @@ class CacheFavorite {
/// Cache favorites using results from remote
///
/// Return number of files updated
Future<int> call(Account account, Iterable<int> remoteFileIds) async {
/// Return the fileIds of the affected files
Future<DbSyncIdResult> call(
Account account, Iterable<int> remoteFileIds) async {
_log.info("[call] Cache favorites");
final result = await _c.npDb.syncFavoriteFiles(
return _c.npDb.syncFavoriteFiles(
account: account.toDb(),
favoriteFileIds: remoteFileIds.toList(),
);
final count = result.insert + result.delete + result.update;
if (count > 0) {
KiwiContainer().resolve<EventBus>().fire(FavoriteResyncedEvent(account));
}
return count;
}
final DiContainer _c;

View file

@ -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<Share> call(Account account, File file, String shareWith) =>
Future<Share> call(Account account, FileDescriptor file, String shareWith) =>
shareRepo.create(account, file, shareWith);
final ShareRepo shareRepo;

View file

@ -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<List<FileDescriptor>> call(Account account, String shareDirPath) =>
_c.fileRepo2.getFileDescriptors(account, shareDirPath);
final DiContainer _c;
}

View file

@ -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<List<Share>> 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,

View file

@ -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 {

View file

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

View file

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

View file

@ -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';
@ -24,15 +23,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<int> call(
@ -52,7 +43,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<EventBus>().fire(FileRemovedEvent(account, f));
} catch (e, stackTrace) {
@ -64,11 +55,12 @@ class Remove {
return count;
}
// TODO: move to CollectionsController
Future<void> _cleanUpAlbums(
Account account, List<FileDescriptor> removes) async {
final albums = await ListAlbum(_c)(account).whereType<Album>().toList();
// figure out which files need to be unshared with whom
final unshares = <FileServerIdentityComparator, Set<CiString>>{};
final unshares = <FileDescriptorServerIdentityComparator, Set<CiString>>{};
// clean up only make sense for static albums
for (final a in albums.where((a) => a.provider is AlbumStaticProvider)) {
try {
@ -76,22 +68,21 @@ class Remove {
final itemsToRemove = provider.items
.whereType<AlbumFileItem>()
.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] ??= <CiString>{}).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);

View file

@ -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<List<AlbumItem>> 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<AlbumFileItem>()
.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;

View file

@ -1,21 +1,24 @@
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';
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';
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 +31,11 @@ class StartupSync {
/// Sync in a background isolate
static Future<SyncResult> 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 +49,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 +59,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,15 +79,24 @@ 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<EventBus>();
if (result.syncFavoriteCount > 0) {
eventBus.fire(FavoriteResyncedEvent(account));
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!);
}
if (result.isSyncPersonUpdated) {
personsController.reload();
}
}
@ -89,23 +105,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<String, dynamic>()
.let(DbSyncIdResult.fromJson),
syncTagResult: (json["syncTagResult"] as Map?)
?.cast<String, dynamic>()
.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;
}

View file

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

View file

@ -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<int> call(Account account) async {
Future<DbSyncIdResult> call(Account account) async {
_log.info("[call] Sync favorites with remote");
final remote = await _getRemoteFavoriteFileIds(account);
return await CacheFavorite(_c)(account, remote);

View file

@ -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<void> call(Account account) async {
///
/// Return tagIds of the affected tags
Future<DbSyncIdResult> 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;

View file

@ -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<void> 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<AlbumItem> _minimizeItems(List<AlbumItem> items) {
return items.map((e) => e is AlbumFileItem ? e.minimize() : e).toList();
await albumRepo.update(account, album);
}
final AlbumRepo albumRepo;

View file

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

View file

@ -45,8 +45,8 @@ class UpdateAutoAlbumCover {
final coverFile = sortedItems
.whereType<AlbumFileItem>()
.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

View file

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

View file

@ -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<void> call(
Account account,
File file, {
FileDescriptor file, {
OrNull<Metadata>? metadata,
OrNull<bool>? isArchived,
OrNull<DateTime>? 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>? metadata,
OrNull<bool>? isArchived,
OrNull<DateTime>? overrideDateTime,
bool? favorite,
OrNull<ImageLocation>? 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 {

View file

@ -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<AlbumShareOutlierBrowser> {
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<AlbumShareOutlierBrowser> {
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<AlbumShareOutlierBrowser> {
);
}
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<AlbumShareOutlierBrowser> {
}
}
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<AlbumShareOutlierBrowser> {
// 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<AlbumShareOutlierBrowser> {
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<AlbumShareOutlierBrowser> {
Future<void> _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<AlbumShareOutlierBrowser> {
));
if (mounted) {
setState(() {
_removeItemStatus(item.file.path, item.shareWith);
_removeItemStatus(item.file.fdPath, item.shareWith);
});
}
}
@ -401,14 +400,14 @@ class _AlbumShareOutlierBrowserState extends State<AlbumShareOutlierBrowser> {
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<AlbumShareOutlierBrowser> {
));
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;
}

View file

@ -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<AccountController>();
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<StatefulWidget> createState() => _WrappedArchiveBrowserState();
}
@npLog
class _ArchiveBrowserState extends State<ArchiveBrowser>
with SelectableItemStreamListMixin<ArchiveBrowser> {
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<ScanAccountDirBloc, ScanAccountDirBlocState>(
bloc: _bloc,
listener: (context, state) => _onStateChange(context, state),
child: BlocBuilder<ScanAccountDirBloc, ScanAccountDirBlocState>(
bloc: _bloc,
builder: (context, state) => _buildContent(context, state),
),
),
);
}
@override
onItemTap(SelectableItem item, int index) {
item.as<PhotoListFileItem>()?.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<List<FileDescriptor>>(
selector: (state) => state.files,
listener: (context, files) {
_bloc.add(_TransformItems(files));
},
),
Expanded(
child: EmptyListIndicator(
icon: Icons.archive_outlined,
text: L10n.global().listEmptyText,
),
_BlocListenerT<ExceptionEvent?>(
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<bool>(
selector: (state) => state.selectedItems.isEmpty,
builder: (context, isEmpty) =>
isEmpty ? const _AppBar() : const _SelectionAppBar(),
),
SliverToBoxAdapter(
child: _BlocSelector<bool>(
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<double?>(
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<void> _onSelectionAppBarUnarchivePressed() async {
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global()
.unarchiveSelectedProcessingNotification(selectedListItems.length)),
duration: k.snackBarDurationShort,
));
final selection = selectedListItems
.whereType<PhotoListFileItem>()
.map((e) => e.file)
.toList();
setState(() {
clearSelectedItems();
});
final c = KiwiContainer().resolve<DiContainer>();
final selectedFiles =
await InflateFileDescriptor(c)(widget.account, selection);
final failures = <File>[];
for (final f in selectedFiles) {
try {
await UpdateProperty(c.fileRepo)
.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<FileDescriptor> 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 = <FileDescriptor>[];
final _buildItemQueue =
ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>();
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<T> = BlocListenerT<_Bloc, _State, T>;
typedef _BlocSelector<T> = 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 __ {}

View file

@ -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<FileDescriptor>? 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<FileDescriptor>? ?? 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}";
}
}

View file

@ -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());
},
),
],
),
);
}
}

View file

@ -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.albumBrowserZoomLevelValue,
)) {
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<void> 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<void> _onLoad(_LoadItems ev, Emitter<_State> emit) {
_log.info(ev);
return emit.forEach<FilesStreamEvent>(
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<FileDescriptor> 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 = <StreamSubscription>[];
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;
}
}

View file

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

View file

@ -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<FileDescriptor> files;
}
class _ItemTransformerResult {
const _ItemTransformerResult({
required this.items,
});
final List<_Item> items;
}

View file

@ -0,0 +1,90 @@
part of '../archive_browser.dart';
class _ContentList extends StatelessWidget {
const _ContentList();
@override
Widget build(BuildContext context) {
return _BlocSelector<int>(
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;
}

View file

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

View file

@ -17,6 +17,8 @@ abstract class $_StateCopyWithWorker {
{Collection? collection,
String? coverUrl,
List<CollectionItem>? items,
List<CollectionItem>? rawItems,
Set<int>? itemsWhitelist,
bool? isLoading,
List<_Item>? transformedItems,
Set<_Item>? selectedItems,
@ -45,6 +47,8 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
{dynamic collection,
dynamic coverUrl = copyWithNull,
dynamic items,
dynamic rawItems,
dynamic itemsWhitelist = copyWithNull,
dynamic isLoading,
dynamic transformedItems,
dynamic selectedItems,
@ -68,6 +72,10 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
coverUrl:
coverUrl == copyWithNull ? that.coverUrl : coverUrl as String?,
items: items as List<CollectionItem>? ?? that.items,
rawItems: rawItems as List<CollectionItem>? ?? that.rawItems,
itemsWhitelist: itemsWhitelist == copyWithNull
? that.itemsWhitelist
: itemsWhitelist as Set<int>?,
isLoading: isLoading as bool? ?? that.isLoading,
transformedItems:
transformedItems as List<_Item>? ?? that.transformedItems,
@ -143,7 +151,7 @@ extension _$_BlocNpLog on _Bloc {
extension _$_StateToString on _State {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_State {collection: $collection, coverUrl: $coverUrl, items: [length: ${items.length}], isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, isSelectionRemovable: $isSelectionRemovable, isSelectionManageableFile: $isSelectionManageableFile, isSelectionDeletable: $isSelectionDeletable, isEditMode: $isEditMode, isEditBusy: $isEditBusy, editName: $editName, editItems: ${editItems == null ? null : "[length: ${editItems!.length}]"}, editTransformedItems: ${editTransformedItems == null ? null : "[length: ${editTransformedItems!.length}]"}, editSort: ${editSort == null ? null : "${editSort!.name}"}, isDragging: $isDragging, zoom: $zoom, scale: ${scale == null ? null : "${scale!.toStringAsFixed(3)}"}, importResult: $importResult, error: $error, message: $message}";
return "_State {collection: $collection, coverUrl: $coverUrl, items: [length: ${items.length}], rawItems: [length: ${rawItems.length}], itemsWhitelist: ${itemsWhitelist == null ? null : "{length: ${itemsWhitelist!.length}}"}, isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, isSelectionRemovable: $isSelectionRemovable, isSelectionManageableFile: $isSelectionManageableFile, isSelectionDeletable: $isSelectionDeletable, isEditMode: $isEditMode, isEditBusy: $isEditBusy, editName: $editName, editItems: ${editItems == null ? null : "[length: ${editItems!.length}]"}, editTransformedItems: ${editTransformedItems == null ? null : "[length: ${editTransformedItems!.length}]"}, editSort: ${editSort == null ? null : "${editSort!.name}"}, isDragging: $isDragging, zoom: $zoom, scale: ${scale == null ? null : "${scale!.toStringAsFixed(3)}"}, importResult: $importResult, error: $error, message: $message}";
}
}
@ -338,3 +346,17 @@ extension _$_SetMessageToString on _SetMessage {
return "_SetMessage {message: $message}";
}
}
extension _$_ArchiveFailedErrorToString on _ArchiveFailedError {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_ArchiveFailedError {count: $count}";
}
}
extension _$_RemoveFailedErrorToString on _RemoveFailedError {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_RemoveFailedError {count: $count}";
}
}

View file

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

View file

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

View file

@ -174,3 +174,23 @@ class _DateItem extends _Item {
final DateTime date;
}
@toString
class _ArchiveFailedError implements Exception {
const _ArchiveFailedError(this.count);
@override
String toString() => _$toString();
final int count;
}
@toString
class _RemoveFailedError implements Exception {
const _RemoveFailedError(this.count);
@override
String toString() => _$toString();
final int count;
}

View file

@ -102,8 +102,8 @@ class _EditContentList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: context.read<PrefController>().albumBrowserZoomLevel,
initialData: context.read<PrefController>().albumBrowserZoomLevel.value,
stream: context.read<PrefController>().albumBrowserZoomLevelChange,
initialData: context.read<PrefController>().albumBrowserZoomLevelValue,
builder: (_, zoomLevel) {
if (zoomLevel.hasError) {
context.addEvent(
@ -159,9 +159,9 @@ class _UnmodifiableEditContentList extends StatelessWidget {
sliver: SliverOpacity(
opacity: .25,
sliver: StreamBuilder<int>(
stream: context.read<PrefController>().albumBrowserZoomLevel,
stream: context.read<PrefController>().albumBrowserZoomLevelChange,
initialData:
context.read<PrefController>().albumBrowserZoomLevel.value,
context.read<PrefController>().albumBrowserZoomLevelValue,
builder: (_, zoomLevel) {
if (zoomLevel.hasError) {
context.addEvent(_SetMessage(

View file

@ -30,7 +30,7 @@ class ArchiveSelectionHandler {
return NotifiedListAction<File>(
list: selectedFiles,
action: (file) async {
await UpdateProperty(_c.fileRepo).updateIsArchived(account, file, true);
await UpdateProperty(_c).updateIsArchived(account, file, true);
},
processingText: shouldShowProcessingText
? L10n.global()

View file

@ -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<int> 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<DiContainer>())(
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;
}

View file

@ -30,8 +30,7 @@ class UnarchiveSelectionHandler {
return await NotifiedListAction<File>(
list: selectedFiles,
action: (file) async {
await UpdateProperty(_c.fileRepo)
.updateIsArchived(account, file, false);
await UpdateProperty(_c).updateIsArchived(account, file, false);
},
processingText: shouldShowProcessingText
? L10n.global()

View file

@ -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';
@ -19,7 +20,7 @@ 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';
import 'package:np_common/or_null.dart';
@ -141,9 +142,7 @@ class _HomeState extends State<Home> with TickerProviderStateMixin {
Widget _buildPage(BuildContext context, int index) {
switch (index) {
case 0:
return HomePhotos(
account: widget.account,
);
return const HomePhotos2();
case 1:
return HomeSearch(
@ -163,7 +162,7 @@ class _HomeState extends State<Home> with TickerProviderStateMixin {
if (index == 0) {
KiwiContainer()
.resolve<EventBus>()
.fire(const HomePhotosBackToTopEvent());
.fire(const HomePhotos2BackToTopEvent());
}
return;
}
@ -189,8 +188,7 @@ class _HomeState extends State<Home> with TickerProviderStateMixin {
context
.read<AccountController>()
.accountPrefController
.shareFolder
.value,
.shareFolderValue,
);
} catch (e, stacktrace) {
_log.shout(

View file

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

Some files were not shown because too many files have changed in this diff Show more