Overhaul Remove to handle shared album properly

This commit is contained in:
Ming Ming 2021-12-02 18:47:37 +08:00
parent 7ff21fa66e
commit 962e7d1e17
14 changed files with 588 additions and 254 deletions

View file

@ -500,6 +500,26 @@ extension FileExtension on File {
static final _log = Logger("entity.file.FileExtension"); static final _log = Logger("entity.file.FileExtension");
} }
class FileServerIdentityComparator {
const FileServerIdentityComparator(this.file);
@override
operator ==(Object other) {
if (other is FileServerIdentityComparator) {
return file.compareServerIdentity(other.file);
} else if (other is File) {
return file.compareServerIdentity(other);
} else {
return false;
}
}
@override
get hashCode => file.fileId?.hashCode ?? file.path.hashCode;
final File file;
}
class FileRepo { class FileRepo {
const FileRepo(this.dataSrc); const FileRepo(this.dataSrc);

View file

@ -26,7 +26,7 @@ class TouchTokenManager {
"[setRemoteToken] Set remote token for file '${file.path}': $token"); "[setRemoteToken] Set remote token for file '${file.path}': $token");
final path = _getRemotePath(account, file); final path = _getRemotePath(account, file);
if (token == null) { if (token == null) {
return Remove(fileRepo, null)(account, file); return Remove(fileRepo, null, null, null, null)(account, [file]);
} else { } else {
return PutFileBinary(fileRepo)( return PutFileBinary(fileRepo)(
account, path, const Utf8Encoder().convert(token), account, path, const Utf8Encoder().convert(token),

View file

@ -2,116 +2,127 @@ import 'package:event_bus/event_bus.dart';
import 'package:kiwi/kiwi.dart'; import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_db.dart';
import 'package:nc_photos/ci_string.dart';
import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/throttler.dart'; import 'package:nc_photos/pref.dart';
import 'package:nc_photos/use_case/find_file.dart';
import 'package:nc_photos/use_case/list_album.dart'; import 'package:nc_photos/use_case/list_album.dart';
import 'package:nc_photos/use_case/update_album.dart'; import 'package:nc_photos/use_case/list_share.dart';
import 'package:nc_photos/use_case/remove_from_album.dart';
import 'package:nc_photos/use_case/remove_share.dart';
class Remove { class Remove {
Remove(this.fileRepo, this.albumRepo); const Remove(
this.fileRepo, this.albumRepo, this.shareRepo, this.appDb, this.pref)
: assert(albumRepo == null ||
(shareRepo != null && appDb != null && pref != null));
/// Remove a file /// Remove files
Future<void> call(Account account, File file) async { Future<void> call(
await fileRepo.remove(account, file); Account account,
if (albumRepo != null) { List<File> files, {
_log.info("[call] Skip albums cleanup as albumRepo == null"); void Function(File file, Object error, StackTrace stackTrace)?
_CleanUpAlbums()(_CleanUpAlbumsData(fileRepo, albumRepo!, account, file)); onRemoveFileFailed,
}) async {
// need to cleanup first, otherwise we can't unshare the files
if (albumRepo == null) {
_log.info("[call] Skip album cleanup as albumRepo == null");
} else {
await _cleanUpAlbums(account, files);
} }
KiwiContainer().resolve<EventBus>().fire(FileRemovedEvent(account, file)); for (final f in files) {
} try {
await fileRepo.remove(account, f);
final FileRepo fileRepo; KiwiContainer().resolve<EventBus>().fire(FileRemovedEvent(account, f));
final AlbumRepo? albumRepo; } catch (e, stackTrace) {
_log.severe("[call] Failed while remove: ${logFilename(f.path)}", e,
static final _log = Logger("use_case.remove.Remove"); stackTrace);
} onRemoveFileFailed?.call(f, e, stackTrace);
}
class _CleanUpAlbumsData {
_CleanUpAlbumsData(this.fileRepo, this.albumRepo, this.account, this.file);
final FileRepo fileRepo;
final AlbumRepo albumRepo;
final Account account;
final File file;
}
class _CleanUpAlbums {
factory _CleanUpAlbums() {
_inst ??= _CleanUpAlbums._();
return _inst!;
}
_CleanUpAlbums._() {
_throttler = Throttler<_CleanUpAlbumsData>(
onTriggered: (data) {
_onTriggered(data);
},
logTag: "remove._CleanUpAlbums",
);
}
void call(_CleanUpAlbumsData data) {
_throttler.trigger(
maxResponceTime: const Duration(seconds: 3),
maxPendingCount: 10,
data: data,
);
}
void _onTriggered(List<_CleanUpAlbumsData> data) async {
for (final pair in data.groupBy(key: (e) => e.account)) {
final list = pair.item2;
await _cleanUp(list.first.fileRepo, list.first.albumRepo,
list.first.account, list.map((e) => e.file).toList());
} }
} }
/// Clean up for a single account Future<void> _cleanUpAlbums(Account account, List<File> removes) async {
Future<void> _cleanUp(FileRepo fileRepo, AlbumRepo albumRepo, Account account, final albums = await ListAlbum(fileRepo, albumRepo!)(account)
List<File> removes) async { .where((event) => event is Album)
final albums = (await ListAlbum(fileRepo, albumRepo)(account) .cast<Album>()
.where((event) => event is Album) .toList();
.toList()) // figure out which files need to be unshared with whom
.cast<Album>(); final unshares = <FileServerIdentityComparator, Set<CiString>>{};
// clean up only make sense for static albums // clean up only make sense for static albums
for (final a for (final a in albums.where((a) => a.provider is AlbumStaticProvider)) {
in albums.where((element) => element.provider is AlbumStaticProvider)) {
try { try {
final provider = AlbumStaticProvider.of(a); final provider = AlbumStaticProvider.of(a);
if (provider.items.whereType<AlbumFileItem>().any((element) => final itemsToRemove = provider.items
removes.containsIf(element.file, (a, b) => a.path == b.path))) { .whereType<AlbumFileItem>()
final newItems = provider.items.where((element) { .where((i) =>
if (element is AlbumFileItem) { (i.file.isOwned(account.username) ||
return !removes.containsIf( i.addedBy == account.username) &&
element.file, (a, b) => a.path == b.path); removes.any((r) => r.compareServerIdentity(i.file)))
} else { .toList();
return true; if (itemsToRemove.isEmpty) {
} continue;
}).toList();
await UpdateAlbum(albumRepo)(
account,
a.copyWith(
provider: AlbumStaticProvider.of(a).copyWith(
items: newItems,
),
));
} }
for (final i in itemsToRemove) {
final key = FileServerIdentityComparator(i.file);
final value = (a.shares?.map((s) => s.userId).toList() ?? [])
..add(a.albumFile!.ownerId!)
..remove(account.username);
(unshares[key] ??= <CiString>{}).addAll(value);
}
_log.fine(
"[_cleanUpAlbums] Removing from album '${a.name}': ${itemsToRemove.map((e) => e.file.path).toReadableString()}");
// skip unsharing as we'll handle it ourselves
await RemoveFromAlbum(albumRepo!, null, null, appDb!)(
account, a, itemsToRemove);
} catch (e, stacktrace) { } catch (e, stacktrace) {
_log.shout( _log.shout(
"[_cleanUpAlbums] Failed while updating album", e, stacktrace); "[_cleanUpAlbums] Failed while updating album", e, stacktrace);
// continue to next album // continue to next album
} }
} }
for (final e in unshares.entries) {
try {
var file = e.key.file;
if (file_util.getUserDirName(file) != account.username) {
try {
file = await FindFile(appDb!)(account, file.fileId!);
} catch (_) {
// file not found
_log.warning(
"[_cleanUpAlbums] File not found in db: ${logFilename(file.path)}");
}
}
final shares = await ListShare(shareRepo!)(account, file);
for (final s in shares.where((s) => e.value.contains(s.shareWith))) {
try {
await RemoveShare(shareRepo!)(account, s);
} catch (e, stackTrace) {
_log.severe(
"[_cleanUpAlbums] Failed while RemoveShare: $s", e, stackTrace);
}
}
} catch (e, stackTrace) {
_log.shout("[_cleanUpAlbums] Failed", e, stackTrace);
}
}
} }
late final Throttler<_CleanUpAlbumsData> _throttler; final FileRepo fileRepo;
final AlbumRepo? albumRepo;
final ShareRepo? shareRepo;
final AppDb? appDb;
final Pref? pref;
static final _log = Logger("use_case.remove"); static final _log = Logger("use_case.remove.Remove");
static _CleanUpAlbums? _inst;
} }

View file

@ -35,7 +35,7 @@ class RemoveAlbum {
} }
// you can't add an album to another album, so passing null here can save // you can't add an album to another album, so passing null here can save
// a few queries // a few queries
await Remove(fileRepo, null)(account, album.albumFile!); await Remove(fileRepo, null, null, null, null)(account, [album.albumFile!]);
} }
Future<void> _unshareFiles(Account account, Album album) async { Future<void> _unshareFiles(Account account, Album album) async {

View file

@ -14,8 +14,13 @@ import 'package:nc_photos/use_case/update_album.dart';
import 'package:nc_photos/use_case/update_album_with_actual_items.dart'; import 'package:nc_photos/use_case/update_album_with_actual_items.dart';
class RemoveFromAlbum { class RemoveFromAlbum {
/// Constructor
///
/// If [shareRepo] and [fileRepo] are null, files will not be unshared after
/// removing from the album
const RemoveFromAlbum( const RemoveFromAlbum(
this.albumRepo, this.shareRepo, this.fileRepo, this.appDb); this.albumRepo, this.shareRepo, this.fileRepo, this.appDb)
: assert(shareRepo == null || fileRepo != null);
/// Remove a list of AlbumItems from [album] /// Remove a list of AlbumItems from [album]
/// ///
@ -37,11 +42,15 @@ class RemoveFromAlbum {
newAlbum = await _fixAlbumPostRemove(account, newAlbum, items); newAlbum = await _fixAlbumPostRemove(account, newAlbum, items);
await UpdateAlbum(albumRepo)(account, newAlbum); await UpdateAlbum(albumRepo)(account, newAlbum);
if (album.shares?.isNotEmpty == true) { if (shareRepo == null) {
final removeFiles = _log.info("[call] Skip unsharing files as shareRepo == null");
items.whereType<AlbumFileItem>().map((e) => e.file).toList(); } else {
if (removeFiles.isNotEmpty) { if (album.shares?.isNotEmpty == true) {
await _unshareFiles(account, newAlbum, removeFiles); final removeFiles =
items.whereType<AlbumFileItem>().map((e) => e.file).toList();
if (removeFiles.isNotEmpty) {
await _unshareFiles(account, newAlbum, removeFiles);
}
} }
} }
@ -93,14 +102,14 @@ class RemoveFromAlbum {
.where((element) => element != account.username) .where((element) => element != account.username)
.toList(); .toList();
if (albumShares.isNotEmpty) { if (albumShares.isNotEmpty) {
await UnshareFileFromAlbum(shareRepo, fileRepo, albumRepo)( await UnshareFileFromAlbum(shareRepo!, fileRepo!, albumRepo)(
account, album, files, albumShares); account, album, files, albumShares);
} }
} }
final AlbumRepo albumRepo; final AlbumRepo albumRepo;
final ShareRepo shareRepo; final ShareRepo? shareRepo;
final FileRepo fileRepo; final FileRepo? fileRepo;
final AppDb appDb; final AppDb appDb;
static final _log = Logger("use_case.remove_from_album.RemoveFromAlbum"); static final _log = Logger("use_case.remove_from_album.RemoveFromAlbum");

View file

@ -17,6 +17,8 @@ import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/share/data_source.dart';
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/iterable_extension.dart';
@ -419,31 +421,30 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
final fileRepo = FileRepo(FileCachedDataSource(AppDb())); final fileRepo = FileRepo(FileCachedDataSource(AppDb()));
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb())); final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb()));
final shareRepo = ShareRepo(ShareRemoteDataSource());
final successes = <_FileListItem>[]; final successes = <_FileListItem>[];
final failures = <_FileListItem>[];
for (final item in selected) {
try {
await Remove(fileRepo, albumRepo)(widget.account, item.file);
successes.add(item);
} catch (e, stacktrace) {
_log.shout(
"[_onSelectionDeletePressed] Failed while removing file" +
(shouldLogFileName ? ": ${item.file.path}" : ""),
e,
stacktrace);
failures.add(item);
}
}
if (failures.isEmpty) { await Remove(fileRepo, albumRepo, shareRepo, AppDb(), Pref())(
widget.account,
selected.map((e) => e.file).toList(),
onRemoveFileFailed: (file, e, stackTrace) {
_log.shout(
"[_onSelectionDeletePressed] Failed while removing file: ${logFilename(file.path)}",
e,
stackTrace);
successes.removeWhere((item) => item.file.compareServerIdentity(file));
},
);
if (successes.length == selected.length) {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().deleteSelectedSuccessNotification), content: Text(L10n.global().deleteSelectedSuccessNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
} else { } else {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text( content: Text(L10n.global().deleteSelectedFailureNotification(
L10n.global().deleteSelectedFailureNotification(failures.length)), selected.length - successes.length)),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
} }

View file

@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_db.dart';
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/share/data_source.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/pref.dart';
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/use_case/remove.dart';
class RemoveSelectionHandler {
/// Remove [selectedFiles] and return the removed count
Future<int> call({
required Account account,
required List<File> selectedFiles,
bool shouldCleanupAlbum = true,
bool isRemoveOpened = false,
}) async {
final String processingText, 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);
}
SnackBarManager().showSnackBar(
SnackBar(
content: Text(processingText),
duration: k.snackBarDurationShort,
),
canBeReplaced: true,
);
final fileRepo = FileRepo(FileCachedDataSource(AppDb()));
final albumRepo =
shouldCleanupAlbum ? AlbumRepo(AlbumCachedDataSource(AppDb())) : null;
final shareRepo =
shouldCleanupAlbum ? ShareRepo(ShareRemoteDataSource()) : null;
var failureCount = 0;
await Remove(fileRepo, albumRepo, shareRepo, AppDb(), Pref())(
account,
selectedFiles,
onRemoveFileFailed: (file, e, stackTrace) {
_log.shout(
"[call] Failed while removing file: ${logFilename(file.path)}",
e,
stackTrace);
++failureCount;
},
);
if (failureCount == 0) {
SnackBarManager().showSnackBar(SnackBar(
content: Text(successText),
duration: k.snackBarDurationNormal,
));
} else {
SnackBarManager().showSnackBar(SnackBar(
content: Text(failureText(failureCount)),
duration: k.snackBarDurationNormal,
));
}
return selectedFiles.length - failureCount;
}
static final _log =
Logger("widget.handler.remove_selection_handler.RemoveSelectionHandler");
}

View file

@ -15,7 +15,6 @@ import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc/scan_account_dir.dart'; import 'package:nc_photos/bloc/scan_account_dir.dart';
import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/download_handler.dart'; import 'package:nc_photos/download_handler.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
@ -30,9 +29,9 @@ import 'package:nc_photos/primitive.dart';
import 'package:nc_photos/share_handler.dart'; import 'package:nc_photos/share_handler.dart';
import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme.dart';
import 'package:nc_photos/use_case/remove.dart';
import 'package:nc_photos/use_case/update_property.dart'; import 'package:nc_photos/use_case/update_property.dart';
import 'package:nc_photos/widget/handler/add_selection_to_album_handler.dart'; import 'package:nc_photos/widget/handler/add_selection_to_album_handler.dart';
import 'package:nc_photos/widget/handler/remove_selection_handler.dart';
import 'package:nc_photos/widget/home_app_bar.dart'; import 'package:nc_photos/widget/home_app_bar.dart';
import 'package:nc_photos/widget/measure.dart'; import 'package:nc_photos/widget/measure.dart';
import 'package:nc_photos/widget/page_visibility_mixin.dart'; import 'package:nc_photos/widget/page_visibility_mixin.dart';
@ -466,26 +465,10 @@ class _HomePhotosState extends State<HomePhotos>
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });
final fileRepo = FileRepo(FileCachedDataSource(AppDb())); await RemoveSelectionHandler()(
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb())); account: widget.account,
await NotifiedListAction<File>( selectedFiles: selectedFiles,
list: selectedFiles, );
action: (file) async {
await Remove(fileRepo, albumRepo)(widget.account, file);
},
processingText: L10n.global()
.deleteSelectedProcessingNotification(selectedFiles.length),
successText: L10n.global().deleteSelectedSuccessNotification,
getFailureText: (failures) =>
L10n.global().deleteSelectedFailureNotification(failures.length),
onActionError: (file, e, stackTrace) {
_log.shout(
"[_onSelectionDeletePressed] Failed while removing file" +
(shouldLogFileName ? ": ${file.path}" : ""),
e,
stackTrace);
},
)();
} }
void _onMetadataTaskStateChanged(MetadataTaskStateChangedEvent ev) { void _onMetadataTaskStateChanged(MetadataTaskStateChangedEvent ev) {

View file

@ -13,7 +13,6 @@ import 'package:nc_photos/bloc/list_face.dart';
import 'package:nc_photos/cache_manager_util.dart'; import 'package:nc_photos/cache_manager_util.dart';
import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/download_handler.dart'; import 'package:nc_photos/download_handler.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/face.dart'; import 'package:nc_photos/entity/face.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/entity/file/data_source.dart';
@ -30,9 +29,9 @@ import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme.dart';
import 'package:nc_photos/throttler.dart'; import 'package:nc_photos/throttler.dart';
import 'package:nc_photos/use_case/populate_person.dart'; import 'package:nc_photos/use_case/populate_person.dart';
import 'package:nc_photos/use_case/remove.dart';
import 'package:nc_photos/use_case/update_property.dart'; import 'package:nc_photos/use_case/update_property.dart';
import 'package:nc_photos/widget/handler/add_selection_to_album_handler.dart'; import 'package:nc_photos/widget/handler/add_selection_to_album_handler.dart';
import 'package:nc_photos/widget/handler/remove_selection_handler.dart';
import 'package:nc_photos/widget/photo_list_item.dart'; import 'package:nc_photos/widget/photo_list_item.dart';
import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart'; import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart';
import 'package:nc_photos/widget/selection_app_bar.dart'; import 'package:nc_photos/widget/selection_app_bar.dart';
@ -405,26 +404,10 @@ class _PersonBrowserState extends State<PersonBrowser>
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });
final fileRepo = FileRepo(FileCachedDataSource(AppDb())); await RemoveSelectionHandler()(
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb())); account: widget.account,
await NotifiedListAction<File>( selectedFiles: selectedFiles,
list: selectedFiles, );
action: (file) async {
await Remove(fileRepo, albumRepo)(widget.account, file);
},
processingText: L10n.global()
.deleteSelectedProcessingNotification(selectedFiles.length),
successText: L10n.global().deleteSelectedSuccessNotification,
getFailureText: (failures) =>
L10n.global().deleteSelectedFailureNotification(failures.length),
onActionError: (file, e, stackTrace) {
_log.shout(
"[_onSelectionDeletePressed] Failed while removing file" +
(shouldLogFileName ? ": ${file.path}" : ""),
e,
stackTrace);
},
)();
} }
void _onFilePropertyUpdated(FilePropertyUpdatedEvent ev) { void _onFilePropertyUpdated(FilePropertyUpdatedEvent ev) {

View file

@ -278,11 +278,9 @@ class _SharedFileViewerState extends State<SharedFileViewer> {
} }
final fileRepo = FileRepo(FileCachedDataSource(AppDb())); final fileRepo = FileRepo(FileCachedDataSource(AppDb()));
return Remove(fileRepo, null)( return Remove(fileRepo, null, null, null, null)(
widget.account, widget.account,
widget.file.copyWith( [widget.file.copyWith(path: dirPath)],
path: dirPath,
),
); );
} }

View file

@ -6,7 +6,6 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util; import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/app_db.dart';
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc/ls_trashbin.dart'; import 'package:nc_photos/bloc/ls_trashbin.dart';
import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/debug_util.dart';
@ -19,9 +18,9 @@ import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/pref.dart'; import 'package:nc_photos/pref.dart';
import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme.dart';
import 'package:nc_photos/use_case/remove.dart';
import 'package:nc_photos/use_case/restore_trashbin.dart'; import 'package:nc_photos/use_case/restore_trashbin.dart';
import 'package:nc_photos/widget/empty_list_indicator.dart'; import 'package:nc_photos/widget/empty_list_indicator.dart';
import 'package:nc_photos/widget/handler/remove_selection_handler.dart';
import 'package:nc_photos/widget/photo_list_item.dart'; import 'package:nc_photos/widget/photo_list_item.dart';
import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart'; import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart';
import 'package:nc_photos/widget/selection_app_bar.dart'; import 'package:nc_photos/widget/selection_app_bar.dart';
@ -395,37 +394,11 @@ class _TrashbinBrowserState extends State<TrashbinBrowser>
} }
Future<void> _deleteFiles(List<File> files) async { Future<void> _deleteFiles(List<File> files) async {
SnackBarManager().showSnackBar(SnackBar( await RemoveSelectionHandler()(
content: Text( account: widget.account,
L10n.global().deleteSelectedProcessingNotification(files.length)), selectedFiles: files,
duration: k.snackBarDurationShort, shouldCleanupAlbum: false,
)); );
final fileRepo = FileRepo(FileCachedDataSource(AppDb()));
final failures = <File>[];
for (final f in files) {
try {
await Remove(fileRepo, null)(widget.account, f);
} catch (e, stacktrace) {
_log.shout(
"[_deleteFiles] Failed while removing file" +
(shouldLogFileName ? ": ${f.path}" : ""),
e,
stacktrace);
failures.add(f);
}
}
if (failures.isEmpty) {
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().deleteSelectedSuccessNotification),
duration: k.snackBarDurationNormal,
));
} else {
SnackBarManager().showSnackBar(SnackBar(
content: Text(
L10n.global().deleteSelectedFailureNotification(failures.length)),
duration: k.snackBarDurationNormal,
));
}
} }
void _reqQuery() { void _reqQuery() {

View file

@ -13,8 +13,8 @@ import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme.dart';
import 'package:nc_photos/use_case/remove.dart';
import 'package:nc_photos/use_case/restore_trashbin.dart'; import 'package:nc_photos/use_case/restore_trashbin.dart';
import 'package:nc_photos/widget/handler/remove_selection_handler.dart';
import 'package:nc_photos/widget/horizontal_page_viewer.dart'; import 'package:nc_photos/widget/horizontal_page_viewer.dart';
import 'package:nc_photos/widget/image_viewer.dart'; import 'package:nc_photos/widget/image_viewer.dart';
import 'package:nc_photos/widget/video_viewer.dart'; import 'package:nc_photos/widget/video_viewer.dart';
@ -310,34 +310,14 @@ class _TrashbinViewerState extends State<TrashbinViewer> {
Future<void> _delete(BuildContext context) async { Future<void> _delete(BuildContext context) async {
final file = widget.streamFiles[_viewerController.currentPage]; final file = widget.streamFiles[_viewerController.currentPage];
_log.info("[_delete] Removing file: ${file.path}"); _log.info("[_delete] Removing file: ${file.path}");
var controller = SnackBarManager().showSnackBar(SnackBar( final count = await RemoveSelectionHandler()(
content: Text(L10n.global().deleteProcessingNotification), account: widget.account,
duration: k.snackBarDurationShort, selectedFiles: [file],
)); shouldCleanupAlbum: false,
controller?.closed.whenComplete(() { isRemoveOpened: true,
controller = null; );
}); if (count > 0) {
try {
final fileRepo = FileRepo(FileCachedDataSource(AppDb()));
await Remove(fileRepo, null)(widget.account, file);
controller?.close();
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().deleteSuccessNotification),
duration: k.snackBarDurationNormal,
));
Navigator.of(context).pop(); Navigator.of(context).pop();
} catch (e, stacktrace) {
_log.shout(
"[_delete] Failed while remove" +
(shouldLogFileName ? ": ${file.path}" : ""),
e,
stacktrace);
controller?.close();
SnackBarManager().showSnackBar(SnackBar(
content: Text("${L10n.global().deleteFailureNotification}: "
"${exception_util.toUserString(e)}"),
duration: k.snackBarDurationNormal,
));
} }
} }

View file

@ -7,23 +7,19 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_db.dart';
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/download_handler.dart'; import 'package:nc_photos/download_handler.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/pref.dart'; import 'package:nc_photos/pref.dart';
import 'package:nc_photos/share_handler.dart'; import 'package:nc_photos/share_handler.dart';
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme.dart';
import 'package:nc_photos/use_case/remove.dart';
import 'package:nc_photos/widget/animated_visibility.dart'; import 'package:nc_photos/widget/animated_visibility.dart';
import 'package:nc_photos/widget/disposable.dart'; import 'package:nc_photos/widget/disposable.dart';
import 'package:nc_photos/widget/handler/remove_selection_handler.dart';
import 'package:nc_photos/widget/horizontal_page_viewer.dart'; import 'package:nc_photos/widget/horizontal_page_viewer.dart';
import 'package:nc_photos/widget/image_viewer.dart'; import 'package:nc_photos/widget/image_viewer.dart';
import 'package:nc_photos/widget/slideshow_dialog.dart'; import 'package:nc_photos/widget/slideshow_dialog.dart';
@ -460,34 +456,13 @@ class _ViewerState extends State<Viewer>
void _onDeletePressed(BuildContext context) async { void _onDeletePressed(BuildContext context) async {
final file = widget.streamFiles[_viewerController.currentPage]; final file = widget.streamFiles[_viewerController.currentPage];
_log.info("[_onDeletePressed] Removing file: ${file.path}"); _log.info("[_onDeletePressed] Removing file: ${file.path}");
var controller = SnackBarManager().showSnackBar(SnackBar( final count = await RemoveSelectionHandler()(
content: Text(L10n.global().deleteProcessingNotification), account: widget.account,
duration: k.snackBarDurationShort, selectedFiles: [file],
)); isRemoveOpened: true,
controller?.closed.whenComplete(() { );
controller = null; if (count > 0) {
});
try {
await Remove(FileRepo(FileCachedDataSource(AppDb())),
AlbumRepo(AlbumCachedDataSource(AppDb())))(widget.account, file);
controller?.close();
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().deleteSuccessNotification),
duration: k.snackBarDurationNormal,
));
Navigator.of(context).pop(); Navigator.of(context).pop();
} catch (e, stacktrace) {
_log.shout(
"[_onDeletePressed] Failed while remove" +
(shouldLogFileName ? ": ${file.path}" : ""),
e,
stacktrace);
controller?.close();
SnackBarManager().showSnackBar(SnackBar(
content: Text("${L10n.global().deleteFailureNotification}: "
"${exception_util.toUserString(e)}"),
duration: k.snackBarDurationNormal,
));
} }
} }

View file

@ -0,0 +1,321 @@
import 'package:event_bus/event_bus.dart';
import 'package:kiwi/kiwi.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/cover_provider.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/or_null.dart';
import 'package:nc_photos/pref.dart';
import 'package:nc_photos/use_case/remove.dart';
import 'package:test/test.dart';
import '../mock_type.dart';
import '../test_util.dart' as util;
void main() {
KiwiContainer().registerInstance<EventBus>(MockEventBus());
group("Remove", () {
test("file", _removeFile);
test("file no clean up", _removeFileNoCleanUp);
group("album", () {
test("file", _removeAlbumFile);
test("file no clean up", _removeAlbumFileNoCleanUp);
});
group("shared album", () {
test("file", _removeSharedAlbumFile);
test("shared file", _removeSharedAlbumSharedFile);
test("file resynced by others", _removeSharedAlbumResyncedFile);
});
});
}
/// Remove a file
///
/// Expect: file deleted
Future<void> _removeFile() async {
final account = util.buildAccount();
final pref = Pref.scoped(PrefMemoryProvider());
final files = (util.FilesBuilder()
..addJpeg("admin/test1.jpg")
..addJpeg("admin/test2.jpg"))
.build();
final appDb = MockAppDb();
await util.fillAppDb(appDb, account, files);
final fileRepo = MockFileMemoryRepo(files);
final albumRepo = MockAlbumMemoryRepo();
final shareRepo = MockShareMemoryRepo();
await Remove(fileRepo, albumRepo, shareRepo, appDb, pref)(
account, [files[0]]);
expect(fileRepo.files, [files[1]]);
}
/// Remove a file, skip clean up
///
/// Expect: file deleted
Future<void> _removeFileNoCleanUp() async {
final account = util.buildAccount();
final files = (util.FilesBuilder()
..addJpeg("admin/test1.jpg")
..addJpeg("admin/test2.jpg"))
.build();
final appDb = MockAppDb();
await util.fillAppDb(appDb, account, files);
final fileRepo = MockFileMemoryRepo(files);
await Remove(fileRepo, null, null, null, null)(account, [files[0]]);
expect(fileRepo.files, [files[1]]);
}
/// Remove a file included in an album
///
/// Expect: file removed from album
Future<void> _removeAlbumFile() async {
final account = util.buildAccount();
final pref = Pref.scoped(PrefMemoryProvider());
final files =
(util.FilesBuilder(initialFileId: 1)..addJpeg("admin/test1.jpg")).build();
final album = (util.AlbumBuilder()..addFileItem(files[0])).build();
final albumFile = album.albumFile!;
final appDb = MockAppDb();
await util.fillAppDb(appDb, account, files);
final fileRepo = MockFileMemoryRepo([albumFile, ...files]);
final albumRepo = MockAlbumMemoryRepo([album]);
final shareRepo = MockShareMemoryRepo();
await Remove(fileRepo, albumRepo, shareRepo, appDb, pref)(
account, [files[0]]);
expect(
albumRepo.albums
.map((e) => e.copyWith(
// we need to set a known value to lastUpdated
lastUpdated: OrNull(DateTime.utc(2020, 1, 2, 3, 4, 5)),
))
.toList(),
[
Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
name: "test",
provider: AlbumStaticProvider(items: []),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: const AlbumNullSortProvider(),
albumFile: albumFile,
),
],
);
}
/// Remove a file included in an album
///
/// Expect: file not removed from album
Future<void> _removeAlbumFileNoCleanUp() async {
final account = util.buildAccount();
final files =
(util.FilesBuilder(initialFileId: 1)..addJpeg("admin/test1.jpg")).build();
final album = (util.AlbumBuilder()..addFileItem(files[0])).build();
final fileItems = util.AlbumBuilder.fileItemsOf(album);
final albumFile = album.albumFile!;
final appDb = MockAppDb();
await util.fillAppDb(appDb, account, files);
final fileRepo = MockFileMemoryRepo([albumFile, ...files]);
final albumRepo = MockAlbumMemoryRepo([album]);
await Remove(fileRepo, null, null, null, null)(account, [files[0]]);
expect(
albumRepo.albums
.map((e) => e.copyWith(
// we need to set a known value to lastUpdated
lastUpdated: OrNull(DateTime.utc(2020, 1, 2, 3, 4, 5)),
))
.toList(),
[
Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
name: "test",
provider: AlbumStaticProvider(
items: fileItems,
latestItemTime: files[0].lastModified,
),
coverProvider: AlbumAutoCoverProvider(coverFile: files[0]),
sortProvider: const AlbumNullSortProvider(),
albumFile: albumFile,
),
],
);
}
/// Remove a file included in a shared album (admin -> user1)
///
/// Expect: file removed from album;
/// file share (admin -> user1) deleted
Future<void> _removeSharedAlbumFile() async {
final account = util.buildAccount();
final pref = Pref.scoped(PrefMemoryProvider());
final files =
(util.FilesBuilder(initialFileId: 1)..addJpeg("admin/test1.jpg")).build();
final album = (util.AlbumBuilder()
..addFileItem(files[0])
..addShare("user1"))
.build();
final albumFile = album.albumFile!;
final appDb = MockAppDb();
await util.fillAppDb(appDb, account, files);
final fileRepo = MockFileMemoryRepo([albumFile, ...files]);
final albumRepo = MockAlbumMemoryRepo([album]);
final shareRepo = MockShareMemoryRepo([
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
util.buildShare(id: "1", file: files[0], shareWith: "user1"),
]);
await Remove(fileRepo, albumRepo, shareRepo, appDb, pref)(
account, [files[0]]);
expect(
albumRepo.albums
.map((e) => e.copyWith(
// we need to set a known value to lastUpdated
lastUpdated: OrNull(DateTime.utc(2020, 1, 2, 3, 4, 5)),
))
.toList(),
[
Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
name: "test",
provider: AlbumStaticProvider(items: []),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: const AlbumNullSortProvider(),
albumFile: albumFile,
shares: [
util.buildAlbumShare(userId: "user1"),
],
),
],
);
expect(
shareRepo.shares,
[util.buildShare(id: "0", file: albumFile, shareWith: "user1")],
);
}
/// Remove a file shared with you (user1 -> admin), added by you to a shared
/// album (admin -> user1, user2)
///
/// Expect: file removed from album;
/// file share (admin -> user2) deleted
Future<void> _removeSharedAlbumSharedFile() async {
final account = util.buildAccount();
final user1Account = util.buildAccount(username: "user1");
final pref = Pref.scoped(PrefMemoryProvider());
final files = (util.FilesBuilder(initialFileId: 1)
..addJpeg("admin/test1.jpg", ownerId: "user1"))
.build();
final user1Files = [
files[0].copyWith(path: "remote.php/dav/files/user1/test1.jpg")
];
final album = (util.AlbumBuilder()
..addFileItem(files[0])
..addShare("user1")
..addShare("user2"))
.build();
final albumFile = album.albumFile!;
final appDb = MockAppDb();
await util.fillAppDb(appDb, account, files);
await util.fillAppDb(appDb, user1Account, user1Files);
final fileRepo = MockFileMemoryRepo([albumFile, ...files, ...user1Files]);
final albumRepo = MockAlbumMemoryRepo([album]);
final shareRepo = MockShareMemoryRepo([
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
util.buildShare(id: "1", file: albumFile, shareWith: "user2"),
util.buildShare(
id: "2", file: user1Files[0], uidOwner: "user1", shareWith: "admin"),
util.buildShare(id: "3", file: files[0], shareWith: "user2"),
]);
await Remove(fileRepo, albumRepo, shareRepo, appDb, pref)(
account, [files[0]]);
expect(
albumRepo.albums
.map((e) => e.copyWith(
// we need to set a known value to lastUpdated
lastUpdated: OrNull(DateTime.utc(2020, 1, 2, 3, 4, 5)),
))
.toList(),
[
Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
name: "test",
provider: AlbumStaticProvider(items: []),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: const AlbumNullSortProvider(),
albumFile: albumFile,
shares: [
util.buildAlbumShare(userId: "user1"),
util.buildAlbumShare(userId: "user2"),
],
),
],
);
expect(
shareRepo.shares,
[
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
util.buildShare(id: "1", file: albumFile, shareWith: "user2"),
util.buildShare(
id: "2", file: user1Files[0], uidOwner: "user1", shareWith: "admin"),
],
);
}
/// Remove a file included in a shared album (admin -> user1), with the album
/// json updated by user1
///
/// Expect: file removed from album;
/// file share (admin -> user1) deleted
Future<void> _removeSharedAlbumResyncedFile() async {
final account = util.buildAccount();
final pref = Pref.scoped(PrefMemoryProvider());
final files =
(util.FilesBuilder(initialFileId: 1)..addJpeg("admin/test1.jpg")).build();
final album = (util.AlbumBuilder()
..addFileItem(files[0]
.copyWith(path: "remote.php/dav/files/user1/share/test1.jpg"))
..addShare("user1"))
.build();
final albumFile = album.albumFile!;
final appDb = MockAppDb();
await util.fillAppDb(appDb, account, files);
final fileRepo = MockFileMemoryRepo([albumFile, ...files]);
final albumRepo = MockAlbumMemoryRepo([album]);
final shareRepo = MockShareMemoryRepo([
util.buildShare(id: "0", file: albumFile, shareWith: "user1"),
util.buildShare(id: "1", file: files[0], shareWith: "user1"),
]);
await Remove(fileRepo, albumRepo, shareRepo, appDb, pref)(
account, [files[0]]);
expect(
albumRepo.albums
.map((e) => e.copyWith(
// we need to set a known value to lastUpdated
lastUpdated: OrNull(DateTime.utc(2020, 1, 2, 3, 4, 5)),
))
.toList(),
[
Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5),
name: "test",
provider: AlbumStaticProvider(items: []),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: const AlbumNullSortProvider(),
albumFile: albumFile,
shares: [
util.buildAlbumShare(userId: "user1"),
],
),
],
);
expect(
shareRepo.shares,
[util.buildShare(id: "0", file: albumFile, shareWith: "user1")],
);
}