(Un)Share associated files when (un)sharing an album

This commit is contained in:
Ming Ming 2021-10-18 18:46:06 +08:00
parent b533c9caf7
commit 5b1dcfb515
8 changed files with 387 additions and 51 deletions

View file

@ -40,4 +40,6 @@ class LogCapturer {
static LogCapturer? _inst; static LogCapturer? _inst;
} }
String logFilename(String filename) => shouldLogFileName ? filename : "***";
const bool shouldLogFileName = kDebugMode; const bool shouldLogFileName = kDebugMode;

View file

@ -971,6 +971,42 @@
} }
} }
}, },
"shareAlbumSuccessNotification": "Album shared with {user}",
"@shareAlbumSuccessNotification": {
"description": "Shared an album with another user successfully",
"placeholders": {
"user": {
"example": "Alice"
}
}
},
"shareAlbumSuccessWithErrorNotification": "Album shared with {user}, but failed to share some files",
"@shareAlbumSuccessWithErrorNotification": {
"description": "Shared an album with another user successfully, but some files inside the album cannot be shared",
"placeholders": {
"user": {
"example": "Alice"
}
}
},
"unshareAlbumSuccessNotification": "Album unshared with {user}",
"@unshareAlbumSuccessNotification": {
"description": "Unshared an album with another user successfully",
"placeholders": {
"user": {
"example": "Alice"
}
}
},
"unshareAlbumSuccessWithErrorNotification": "Album unshared with {user}, but failed to unshare some files",
"@unshareAlbumSuccessWithErrorNotification": {
"description": "Unshared an album with another user successfully, but some files inside the album cannot be unshared",
"placeholders": {
"user": {
"example": "Alice"
}
}
},
"fixSharesTooltip": "Fix shares", "fixSharesTooltip": "Fix shares",
"@fixSharesTooltip": { "@fixSharesTooltip": {
"description": "Fix file shares in an album. Due to limitation of the server API, album and its files are shared separately, but they are both needed for shared album to work correctly. This button will attempt to synchronize them" "description": "Fix file shares in an album. Due to limitation of the server API, album and its files are shared separately, but they are both needed for shared album to work correctly. This button will attempt to synchronize them"
@ -979,6 +1015,10 @@
"@fixTooltip": { "@fixTooltip": {
"description": "Fix an issue" "description": "Fix an issue"
}, },
"fixButtonLabel": "FIX",
"@fixButtonLabel":{
"description": "Fix an issue"
},
"fixAllTooltip": "Fix all", "fixAllTooltip": "Fix all",
"@fixAllTooltip": { "@fixAllTooltip": {
"description": "Fix all listed issues" "description": "Fix all listed issues"

View file

@ -25,8 +25,13 @@
"addToCollectionTooltip", "addToCollectionTooltip",
"addToCollectionProcessingNotification", "addToCollectionProcessingNotification",
"addToCollectionSuccessNotification", "addToCollectionSuccessNotification",
"shareAlbumSuccessNotification",
"shareAlbumSuccessWithErrorNotification",
"unshareAlbumSuccessNotification",
"unshareAlbumSuccessWithErrorNotification",
"fixSharesTooltip", "fixSharesTooltip",
"fixTooltip", "fixTooltip",
"fixButtonLabel",
"fixAllTooltip", "fixAllTooltip",
"missingShareDescription", "missingShareDescription",
"extraShareDescription" "extraShareDescription"
@ -72,8 +77,13 @@
"addToCollectionTooltip", "addToCollectionTooltip",
"addToCollectionProcessingNotification", "addToCollectionProcessingNotification",
"addToCollectionSuccessNotification", "addToCollectionSuccessNotification",
"shareAlbumSuccessNotification",
"shareAlbumSuccessWithErrorNotification",
"unshareAlbumSuccessNotification",
"unshareAlbumSuccessWithErrorNotification",
"fixSharesTooltip", "fixSharesTooltip",
"fixTooltip", "fixTooltip",
"fixButtonLabel",
"fixAllTooltip", "fixAllTooltip",
"missingShareDescription", "missingShareDescription",
"extraShareDescription" "extraShareDescription"
@ -174,8 +184,13 @@
"addToCollectionTooltip", "addToCollectionTooltip",
"addToCollectionProcessingNotification", "addToCollectionProcessingNotification",
"addToCollectionSuccessNotification", "addToCollectionSuccessNotification",
"shareAlbumSuccessNotification",
"shareAlbumSuccessWithErrorNotification",
"unshareAlbumSuccessNotification",
"unshareAlbumSuccessWithErrorNotification",
"fixSharesTooltip", "fixSharesTooltip",
"fixTooltip", "fixTooltip",
"fixButtonLabel",
"fixAllTooltip", "fixAllTooltip",
"missingShareDescription", "missingShareDescription",
"extraShareDescription" "extraShareDescription"
@ -207,8 +222,13 @@
"addToCollectionTooltip", "addToCollectionTooltip",
"addToCollectionProcessingNotification", "addToCollectionProcessingNotification",
"addToCollectionSuccessNotification", "addToCollectionSuccessNotification",
"shareAlbumSuccessNotification",
"shareAlbumSuccessWithErrorNotification",
"unshareAlbumSuccessNotification",
"unshareAlbumSuccessWithErrorNotification",
"fixSharesTooltip", "fixSharesTooltip",
"fixTooltip", "fixTooltip",
"fixButtonLabel",
"fixAllTooltip", "fixAllTooltip",
"missingShareDescription", "missingShareDescription",
"extraShareDescription" "extraShareDescription"
@ -289,8 +309,13 @@
"addToCollectionTooltip", "addToCollectionTooltip",
"addToCollectionProcessingNotification", "addToCollectionProcessingNotification",
"addToCollectionSuccessNotification", "addToCollectionSuccessNotification",
"shareAlbumSuccessNotification",
"shareAlbumSuccessWithErrorNotification",
"unshareAlbumSuccessNotification",
"unshareAlbumSuccessWithErrorNotification",
"fixSharesTooltip", "fixSharesTooltip",
"fixTooltip", "fixTooltip",
"fixButtonLabel",
"fixAllTooltip", "fixAllTooltip",
"missingShareDescription", "missingShareDescription",
"extraShareDescription" "extraShareDescription"
@ -344,8 +369,13 @@
"addToCollectionTooltip", "addToCollectionTooltip",
"addToCollectionProcessingNotification", "addToCollectionProcessingNotification",
"addToCollectionSuccessNotification", "addToCollectionSuccessNotification",
"shareAlbumSuccessNotification",
"shareAlbumSuccessWithErrorNotification",
"unshareAlbumSuccessNotification",
"unshareAlbumSuccessWithErrorNotification",
"fixSharesTooltip", "fixSharesTooltip",
"fixTooltip", "fixTooltip",
"fixButtonLabel",
"fixAllTooltip", "fixAllTooltip",
"missingShareDescription", "missingShareDescription",
"extraShareDescription" "extraShareDescription"

View file

@ -0,0 +1,60 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:nc_photos/use_case/ls.dart';
class ListSharedAlbumItem {
const ListSharedAlbumItem(this.share, this.album);
final Share share;
final Album album;
}
class ListSharedAlbum {
const ListSharedAlbum(this.shareRepo, this.fileRepo, this.albumRepo);
/// List albums that are currently shared by you
///
/// If [whereShareWith] is not null, only shares sharing with [whereShareWith]
/// will be returned.
Future<List<ListSharedAlbumItem>> call(
Account account, {
String? whereShareWith,
}) async {
final shares = await shareRepo.listDir(
account, File(path: remote_storage_util.getRemoteAlbumsDir(account)));
final shareGroups = <String, List<Share>>{};
for (final s in shares) {
shareGroups[s.path] ??= <Share>[];
shareGroups[s.path]!.add(s);
}
final files = await Ls(fileRepo)(
account,
File(path: remote_storage_util.getRemoteAlbumsDir(account)),
);
final products = <ListSharedAlbumItem>[];
for (final sg in shareGroups.entries) {
// find the file
final albumFile =
files.firstWhere((element) => element.strippedPath == sg.key);
final album = await albumRepo.get(
account,
albumFile,
);
for (final s in sg.value) {
if (whereShareWith != null && s.shareWith != whereShareWith) {
continue;
}
products.add(ListSharedAlbumItem(s, album));
}
}
return products;
}
final ShareRepo shareRepo;
final FileRepo fileRepo;
final AlbumRepo albumRepo;
}

View file

@ -0,0 +1,44 @@
import 'package:logging/logging.dart';
import 'package:nc_photos/account.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/share.dart';
import 'package:nc_photos/use_case/create_share.dart';
class ShareAlbumWithUser {
ShareAlbumWithUser(this.shareRepo);
Future<void> call(
Account account,
Album album,
String shareWith, {
void Function(File)? onShareFileFailed,
}) async {
assert(album.provider is AlbumStaticProvider);
final files = AlbumStaticProvider.of(album)
.items
.whereType<AlbumFileItem>()
.map((e) => e.file);
await CreateUserShare(shareRepo)(account, album.albumFile!, shareWith);
for (final f in files) {
_log.info("[call] Sharing '${f.path}' with '$shareWith'");
try {
await CreateUserShare(shareRepo)(account, f, shareWith);
} catch (e, stackTrace) {
_log.severe(
"[call] Failed sharing file '${logFilename(f.path)}' with '$shareWith'",
e,
stackTrace);
onShareFileFailed?.call(f);
}
}
}
final ShareRepo shareRepo;
static final _log =
Logger("use_case.share_album_with_user.ShareAlbumWithUser");
}

View file

@ -0,0 +1,84 @@
import 'package:logging/logging.dart';
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/entity/file.dart';
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/share/data_source.dart';
import 'package:nc_photos/use_case/list_share.dart';
import 'package:nc_photos/use_case/list_shared_album.dart';
import 'package:nc_photos/use_case/remove_share.dart';
class UnshareAlbumWithUser {
UnshareAlbumWithUser(this.shareRepo, this.fileRepo, this.albumRepo);
Future<void> call(
Account account,
Album album,
String shareWith, {
void Function(File)? onUnshareFileFailed,
}) async {
assert(album.provider is AlbumStaticProvider);
final shareRepo = ShareRepo(ShareRemoteDataSource());
final sharedItems =
await ListSharedAlbum(shareRepo, fileRepo, albumRepo)(account);
final thisShare = sharedItems
.firstWhere((element) =>
element.share.path == album.albumFile!.strippedPath &&
element.share.shareWith == shareWith)
.share;
final otherSharedAlbums = sharedItems
.where((element) =>
!identical(element.share, thisShare) &&
element.album.provider is AlbumStaticProvider &&
element.share.shareWith == shareWith)
.map((e) => e.album)
.toList();
final unsharingFiles = await _getExclusiveSharedFiles(
account, album, otherSharedAlbums, shareWith);
await RemoveShare(shareRepo)(account, thisShare);
for (final f in unsharingFiles) {
_log.info("[call] Unsharing '${f.path}'");
try {
final shares = await ListShare(shareRepo)(account, f);
final share =
shares.firstWhere((element) => element.shareWith == shareWith);
await RemoveShare(shareRepo)(account, share);
} catch (e, stackTrace) {
_log.severe(
"[call] Failed while RemoveShare: ${f.path}", e, stackTrace);
onUnshareFileFailed?.call(f);
}
}
}
/// Return list of files shared with [shareWith] in [album] but not in
/// [others]
Future<List<File>> _getExclusiveSharedFiles(Account account, Album album,
List<Album> others, String shareWith) async {
var files = AlbumStaticProvider.of(album)
.items
.whereType<AlbumFileItem>()
.map((e) => e.file)
.toList();
for (final a in others) {
// filter out files in a
files = files
.where((f) => !AlbumStaticProvider.of(a)
.items
.whereType<AlbumFileItem>()
.any((element) => element.file.path == f.path))
.toList();
}
return files;
}
final ShareRepo shareRepo;
final FileRepo fileRepo;
final AlbumRepo albumRepo;
static final _log =
Logger("use_case.unshare_album_with_user.UnshareAlbumWithUser");
}

View file

@ -325,7 +325,7 @@ class _AlbumBrowserState extends State<AlbumBrowser>
context: context, context: context,
builder: (_) => ShareAlbumDialog( builder: (_) => ShareAlbumDialog(
account: widget.account, account: widget.account,
file: _album!.albumFile!, album: _album!,
), ),
); );
} }

View file

@ -6,7 +6,9 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc/list_share.dart'; import 'package:nc_photos/bloc/list_share.dart';
import 'package:nc_photos/bloc/list_sharee.dart'; import 'package:nc_photos/bloc/list_sharee.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/share.dart'; import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/share/data_source.dart'; import 'package:nc_photos/entity/share/data_source.dart';
import 'package:nc_photos/entity/sharee.dart'; import 'package:nc_photos/entity/sharee.dart';
@ -14,20 +16,24 @@ import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/iterable_extension.dart';
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/use_case/create_share.dart'; import 'package:nc_photos/theme.dart';
import 'package:nc_photos/use_case/share_album_with_user.dart';
import 'package:nc_photos/use_case/unshare_album_with_user.dart';
import 'package:nc_photos/widget/album_share_outlier_browser.dart';
class ShareAlbumDialog extends StatefulWidget { class ShareAlbumDialog extends StatefulWidget {
const ShareAlbumDialog({ ShareAlbumDialog({
Key? key, Key? key,
required this.account, required this.account,
required this.file, required this.album,
}) : super(key: key); }) : assert(album.albumFile != null),
super(key: key);
@override @override
createState() => _ShareAlbumDialogState(); createState() => _ShareAlbumDialogState();
final Account account; final Account account;
final File file; final Album album;
} }
class _ShareAlbumDialogState extends State<ShareAlbumDialog> { class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
@ -35,22 +41,32 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
initState() { initState() {
super.initState(); super.initState();
_shareeBloc.add(ListShareeBlocQuery(widget.account)); _shareeBloc.add(ListShareeBlocQuery(widget.account));
_shareBloc.add(ListShareBlocQuery(widget.account, widget.file)); _shareBloc.add(ListShareBlocQuery(widget.account, widget.album.albumFile!));
} }
@override @override
build(BuildContext context) { build(BuildContext context) {
return BlocListener<ListShareeBloc, ListShareeBlocState>( return AppTheme(
bloc: _shareeBloc, child: GestureDetector(
listener: (context, shareeState) => onTap: () {
_onListShareeBlocStateChanged(context, shareeState), Navigator.of(context).pop();
child: BlocBuilder<ListShareeBloc, ListShareeBlocState>( },
bloc: _shareeBloc, child: Scaffold(
builder: (_, shareeState) => backgroundColor: Colors.transparent,
BlocBuilder<ListShareBloc, ListShareBlocState>( body: BlocListener<ListShareeBloc, ListShareeBlocState>(
bloc: _shareBloc, bloc: _shareeBloc,
builder: (context, shareState) => listener: (context, shareeState) =>
_buildContent(context, shareeState, shareState), _onListShareeBlocStateChanged(context, shareeState),
child: BlocBuilder<ListShareeBloc, ListShareeBlocState>(
bloc: _shareeBloc,
builder: (_, shareeState) =>
BlocBuilder<ListShareBloc, ListShareBlocState>(
bloc: _shareBloc,
builder: (context, shareState) =>
_buildContent(context, shareeState, shareState),
),
),
),
), ),
), ),
); );
@ -89,13 +105,12 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
Widget _buildItem( Widget _buildItem(
BuildContext context, ListShareBlocState shareState, Sharee sharee) { BuildContext context, ListShareBlocState shareState, Sharee sharee) {
final Share? share; final bool isShared;
if (_overrideSharee.containsKey(sharee.shareWith)) { if (_overrideSharee.containsKey(sharee.shareWith)) {
share = _overrideSharee[sharee.shareWith]; isShared = _overrideSharee[sharee.shareWith]!;
} else { } else {
share = shareState.items isShared = shareState.items
.where((element) => element.shareWith == sharee.shareWith) .any((element) => element.shareWith == sharee.shareWith);
.firstOrNull;
} }
final isProcessing = final isProcessing =
@ -112,7 +127,7 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
); );
} else { } else {
trailing = Checkbox( trailing = Checkbox(
value: share != null, value: isShared,
onChanged: (value) {}, onChanged: (value) {},
); );
} }
@ -125,7 +140,7 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
child: trailing, child: trailing,
), ),
), ),
onPressed: () => _onShareePressed(sharee, share), onPressed: () => _onShareePressed(sharee, isShared),
); );
} }
@ -139,49 +154,110 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
} }
} }
void _onShareePressed(Sharee sharee, Share? share) async { void _onShareePressed(Sharee sharee, bool isShared) async {
final shareRepo = ShareRepo(ShareRemoteDataSource());
setState(() { setState(() {
_processingSharee.add(sharee.shareWith); _processingSharee.add(sharee.shareWith);
}); });
if (share == null) { if (!isShared) {
// create new share // create new share
try { await _createShare(sharee);
final newShare = await CreateUserShare(shareRepo)(
widget.account, widget.file, sharee.shareWith);
_overrideSharee[sharee.shareWith] = newShare;
} catch (e, stackTrace) {
_log.shout("[_onShareePressed] Failed while create", e, stackTrace);
SnackBarManager().showSnackBar(SnackBar(
content: Text(exception_util.toUserString(e)),
duration: k.snackBarDurationNormal,
));
}
} else { } else {
// remove share // remove share
try { await _removeShare(sharee);
await Future.delayed(const Duration(seconds: 3));
await shareRepo.delete(widget.account, share);
_overrideSharee[sharee.shareWith] = null;
} catch (e, stackTrace) {
_log.shout("[_onShareePressed] Failed while delete", e, stackTrace);
SnackBarManager().showSnackBar(SnackBar(
content: Text(exception_util.toUserString(e)),
duration: k.snackBarDurationNormal,
));
}
} }
setState(() { setState(() {
_processingSharee.remove(sharee.shareWith); _processingSharee.remove(sharee.shareWith);
}); });
} }
void _onFixPressed() {
Navigator.of(context).pushNamed(AlbumShareOutlierBrowser.routeName,
arguments:
AlbumShareOutlierBrowserArguments(widget.account, widget.album));
}
Future<void> _createShare(Sharee sharee) async {
final shareRepo = ShareRepo(ShareRemoteDataSource());
var hasFailure = false;
try {
await ShareAlbumWithUser(shareRepo)(
widget.account,
widget.album,
sharee.shareWith,
onShareFileFailed: (_) {
hasFailure = true;
},
);
_overrideSharee[sharee.shareWith] = true;
SnackBarManager().showSnackBar(SnackBar(
content: Text(hasFailure
? L10n.global()
.shareAlbumSuccessWithErrorNotification(sharee.shareWith)
: L10n.global().shareAlbumSuccessNotification(sharee.shareWith)),
action: hasFailure
? SnackBarAction(
label: L10n.global().fixButtonLabel,
textColor: Theme.of(context).colorScheme.secondaryVariant,
onPressed: _onFixPressed,
)
: null,
duration: k.snackBarDurationNormal,
));
} catch (e, stackTrace) {
_log.shout(
"[_createShare] Failed while ShareAlbumWithUser", e, stackTrace);
SnackBarManager().showSnackBar(SnackBar(
content: Text(exception_util.toUserString(e)),
duration: k.snackBarDurationNormal,
));
}
}
Future<void> _removeShare(Sharee sharee) async {
final shareRepo = ShareRepo(ShareRemoteDataSource());
final fileRepo = FileRepo(FileCachedDataSource());
final albumRepo = AlbumRepo(AlbumCachedDataSource());
var hasFailure = false;
try {
await UnshareAlbumWithUser(shareRepo, fileRepo, albumRepo)(
widget.account,
widget.album,
sharee.shareWith,
onUnshareFileFailed: (_) {
hasFailure = true;
},
);
_overrideSharee[sharee.shareWith] = false;
SnackBarManager().showSnackBar(SnackBar(
content: Text(hasFailure
? L10n.global()
.unshareAlbumSuccessWithErrorNotification(sharee.shareWith)
: L10n.global().unshareAlbumSuccessNotification(sharee.shareWith)),
action: hasFailure
? SnackBarAction(
label: L10n.global().fixButtonLabel,
textColor: Theme.of(context).colorScheme.secondaryVariant,
onPressed: _onFixPressed,
)
: null,
duration: k.snackBarDurationNormal,
));
} catch (e, stackTrace) {
_log.shout(
"[_removeShare] Failed while UnshareAlbumWithUser", e, stackTrace);
SnackBarManager().showSnackBar(SnackBar(
content: Text(exception_util.toUserString(e)),
duration: k.snackBarDurationNormal,
));
}
}
final _shareeBloc = ListShareeBloc(); final _shareeBloc = ListShareeBloc();
final _shareBloc = ListShareBloc(); final _shareBloc = ListShareBloc();
final _processingSharee = <String>[]; final _processingSharee = <String>[];
/// Store the modified value of each sharee /// Store the modified value of each sharee
final _overrideSharee = <String, Share?>{}; final _overrideSharee = <String, bool>{};
static final _log = static final _log =
Logger("widget.share_album_dialog._ShareAlbumDialogState"); Logger("widget.share_album_dialog._ShareAlbumDialogState");