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");
}
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 {
const FileRepo(this.dataSrc);

View file

@ -26,7 +26,7 @@ class TouchTokenManager {
"[setRemoteToken] Set remote token for file '${file.path}': $token");
final path = _getRemotePath(account, file);
if (token == null) {
return Remove(fileRepo, null)(account, file);
return Remove(fileRepo, null, null, null, null)(account, [file]);
} else {
return PutFileBinary(fileRepo)(
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:logging/logging.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/item.dart';
import 'package:nc_photos/entity/album/provider.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/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/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 {
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
Future<void> call(Account account, File file) async {
await fileRepo.remove(account, file);
if (albumRepo != null) {
_log.info("[call] Skip albums cleanup as albumRepo == null");
_CleanUpAlbums()(_CleanUpAlbumsData(fileRepo, albumRepo!, account, file));
/// Remove files
Future<void> call(
Account account,
List<File> files, {
void Function(File file, Object error, StackTrace stackTrace)?
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));
}
final FileRepo fileRepo;
final AlbumRepo? albumRepo;
static final _log = Logger("use_case.remove.Remove");
}
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());
for (final f in files) {
try {
await fileRepo.remove(account, f);
KiwiContainer().resolve<EventBus>().fire(FileRemovedEvent(account, f));
} catch (e, stackTrace) {
_log.severe("[call] Failed while remove: ${logFilename(f.path)}", e,
stackTrace);
onRemoveFileFailed?.call(f, e, stackTrace);
}
}
}
/// Clean up for a single account
Future<void> _cleanUp(FileRepo fileRepo, AlbumRepo albumRepo, Account account,
List<File> removes) async {
final albums = (await ListAlbum(fileRepo, albumRepo)(account)
.where((event) => event is Album)
.toList())
.cast<Album>();
Future<void> _cleanUpAlbums(Account account, List<File> removes) async {
final albums = await ListAlbum(fileRepo, albumRepo!)(account)
.where((event) => event is Album)
.cast<Album>()
.toList();
// figure out which files need to be unshared with whom
final unshares = <FileServerIdentityComparator, Set<CiString>>{};
// clean up only make sense for static albums
for (final a
in albums.where((element) => element.provider is AlbumStaticProvider)) {
for (final a in albums.where((a) => a.provider is AlbumStaticProvider)) {
try {
final provider = AlbumStaticProvider.of(a);
if (provider.items.whereType<AlbumFileItem>().any((element) =>
removes.containsIf(element.file, (a, b) => a.path == b.path))) {
final newItems = provider.items.where((element) {
if (element is AlbumFileItem) {
return !removes.containsIf(
element.file, (a, b) => a.path == b.path);
} else {
return true;
}
}).toList();
await UpdateAlbum(albumRepo)(
account,
a.copyWith(
provider: AlbumStaticProvider.of(a).copyWith(
items: newItems,
),
));
final itemsToRemove = provider.items
.whereType<AlbumFileItem>()
.where((i) =>
(i.file.isOwned(account.username) ||
i.addedBy == account.username) &&
removes.any((r) => r.compareServerIdentity(i.file)))
.toList();
if (itemsToRemove.isEmpty) {
continue;
}
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) {
_log.shout(
"[_cleanUpAlbums] Failed while updating album", e, stacktrace);
// 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 _CleanUpAlbums? _inst;
static final _log = Logger("use_case.remove.Remove");
}

View file

@ -35,7 +35,7 @@ class RemoveAlbum {
}
// you can't add an album to another album, so passing null here can save
// 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 {

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';
class RemoveFromAlbum {
/// Constructor
///
/// If [shareRepo] and [fileRepo] are null, files will not be unshared after
/// removing from the album
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]
///
@ -37,11 +42,15 @@ class RemoveFromAlbum {
newAlbum = await _fixAlbumPostRemove(account, newAlbum, items);
await UpdateAlbum(albumRepo)(account, newAlbum);
if (album.shares?.isNotEmpty == true) {
final removeFiles =
items.whereType<AlbumFileItem>().map((e) => e.file).toList();
if (removeFiles.isNotEmpty) {
await _unshareFiles(account, newAlbum, removeFiles);
if (shareRepo == null) {
_log.info("[call] Skip unsharing files as shareRepo == null");
} else {
if (album.shares?.isNotEmpty == true) {
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)
.toList();
if (albumShares.isNotEmpty) {
await UnshareFileFromAlbum(shareRepo, fileRepo, albumRepo)(
await UnshareFileFromAlbum(shareRepo!, fileRepo!, albumRepo)(
account, album, files, albumShares);
}
}
final AlbumRepo albumRepo;
final ShareRepo shareRepo;
final FileRepo fileRepo;
final ShareRepo? shareRepo;
final FileRepo? fileRepo;
final AppDb appDb;
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/data_source.dart';
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/exception_util.dart' as exception_util;
import 'package:nc_photos/iterable_extension.dart';
@ -419,31 +421,30 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
final fileRepo = FileRepo(FileCachedDataSource(AppDb()));
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb()));
final shareRepo = ShareRepo(ShareRemoteDataSource());
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(
content: Text(L10n.global().deleteSelectedSuccessNotification),
duration: k.snackBarDurationNormal,
));
} else {
SnackBarManager().showSnackBar(SnackBar(
content: Text(
L10n.global().deleteSelectedFailureNotification(failures.length)),
content: Text(L10n.global().deleteSelectedFailureNotification(
selected.length - successes.length)),
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/debug_util.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/data_source.dart';
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/snack_bar_manager.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/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/measure.dart';
import 'package:nc_photos/widget/page_visibility_mixin.dart';
@ -466,26 +465,10 @@ class _HomePhotosState extends State<HomePhotos>
setState(() {
clearSelectedItems();
});
final fileRepo = FileRepo(FileCachedDataSource(AppDb()));
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb()));
await NotifiedListAction<File>(
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);
},
)();
await RemoveSelectionHandler()(
account: widget.account,
selectedFiles: selectedFiles,
);
}
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/debug_util.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/file.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/throttler.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/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/selectable_item_stream_list_mixin.dart';
import 'package:nc_photos/widget/selection_app_bar.dart';
@ -405,26 +404,10 @@ class _PersonBrowserState extends State<PersonBrowser>
setState(() {
clearSelectedItems();
});
final fileRepo = FileRepo(FileCachedDataSource(AppDb()));
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb()));
await NotifiedListAction<File>(
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);
},
)();
await RemoveSelectionHandler()(
account: widget.account,
selectedFiles: selectedFiles,
);
}
void _onFilePropertyUpdated(FilePropertyUpdatedEvent ev) {

View file

@ -278,11 +278,9 @@ class _SharedFileViewerState extends State<SharedFileViewer> {
}
final fileRepo = FileRepo(FileCachedDataSource(AppDb()));
return Remove(fileRepo, null)(
return Remove(fileRepo, null, null, null, null)(
widget.account,
widget.file.copyWith(
path: dirPath,
),
[widget.file.copyWith(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:nc_photos/account.dart';
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/bloc/ls_trashbin.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/snack_bar_manager.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/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/selectable_item_stream_list_mixin.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 {
SnackBarManager().showSnackBar(SnackBar(
content: Text(
L10n.global().deleteSelectedProcessingNotification(files.length)),
duration: k.snackBarDurationShort,
));
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,
));
}
await RemoveSelectionHandler()(
account: widget.account,
selectedFiles: files,
shouldCleanupAlbum: false,
);
}
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/snack_bar_manager.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/widget/handler/remove_selection_handler.dart';
import 'package:nc_photos/widget/horizontal_page_viewer.dart';
import 'package:nc_photos/widget/image_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 {
final file = widget.streamFiles[_viewerController.currentPage];
_log.info("[_delete] Removing file: ${file.path}");
var controller = SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().deleteProcessingNotification),
duration: k.snackBarDurationShort,
));
controller?.closed.whenComplete(() {
controller = null;
});
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,
));
final count = await RemoveSelectionHandler()(
account: widget.account,
selectedFiles: [file],
shouldCleanupAlbum: false,
isRemoveOpened: true,
);
if (count > 0) {
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: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/download_handler.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/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/pref.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/use_case/remove.dart';
import 'package:nc_photos/widget/animated_visibility.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/image_viewer.dart';
import 'package:nc_photos/widget/slideshow_dialog.dart';
@ -460,34 +456,13 @@ class _ViewerState extends State<Viewer>
void _onDeletePressed(BuildContext context) async {
final file = widget.streamFiles[_viewerController.currentPage];
_log.info("[_onDeletePressed] Removing file: ${file.path}");
var controller = SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().deleteProcessingNotification),
duration: k.snackBarDurationShort,
));
controller?.closed.whenComplete(() {
controller = null;
});
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,
));
final count = await RemoveSelectionHandler()(
account: widget.account,
selectedFiles: [file],
isRemoveOpened: true,
);
if (count > 0) {
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")],
);
}