nc-photos/app/lib/bloc/list_sharing.dart
2023-05-24 01:13:23 +08:00

424 lines
13 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/bloc/bloc_util.dart' as bloc_util;
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:nc_photos/throttler.dart';
import 'package:nc_photos/use_case/find_file.dart';
import 'package:nc_photos/use_case/list_share_with_me.dart';
import 'package:nc_photos/use_case/ls.dart';
import 'package:nc_photos/use_case/ls_single_file.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:path/path.dart' as path_lib;
import 'package:to_string/to_string.dart';
part 'list_sharing.g.dart';
abstract class ListSharingItem {
const ListSharingItem(this.share);
final Share share;
}
class ListSharingFile extends ListSharingItem {
const ListSharingFile(Share share, this.file) : super(share);
final File file;
}
class ListSharingAlbum extends ListSharingItem {
const ListSharingAlbum(Share share, this.album) : super(share);
final Album album;
}
abstract class ListSharingBlocEvent {
const ListSharingBlocEvent();
}
@toString
class ListSharingBlocQuery extends ListSharingBlocEvent {
const ListSharingBlocQuery(this.account);
@override
String toString() => _$toString();
final Account account;
}
@toString
class _ListSharingBlocShareRemoved extends ListSharingBlocEvent {
const _ListSharingBlocShareRemoved(this.shares);
@override
String toString() => _$toString();
@Format(r"${$?.toReadableString()}")
final List<Share> shares;
}
@toString
class _ListSharingBlocPendingSharedAlbumMoved extends ListSharingBlocEvent {
const _ListSharingBlocPendingSharedAlbumMoved(
this.account, this.file, this.destination);
@override
String toString() => _$toString();
final Account account;
final File file;
final String destination;
}
@toString
abstract class ListSharingBlocState {
const ListSharingBlocState(this.account, this.items);
@override
String toString() => _$toString();
final Account? account;
final List<ListSharingItem> items;
}
class ListSharingBlocInit extends ListSharingBlocState {
ListSharingBlocInit() : super(null, const []);
}
class ListSharingBlocLoading extends ListSharingBlocState {
const ListSharingBlocLoading(Account? account, List<ListSharingItem> items)
: super(account, items);
}
class ListSharingBlocSuccess extends ListSharingBlocState {
const ListSharingBlocSuccess(Account? account, List<ListSharingItem> items)
: super(account, items);
ListSharingBlocSuccess copyWith({
Account? account,
List<ListSharingItem>? items,
}) =>
ListSharingBlocSuccess(
account ?? this.account,
items ?? List.of(this.items),
);
}
@toString
class ListSharingBlocFailure extends ListSharingBlocState {
const ListSharingBlocFailure(
Account? account, List<ListSharingItem> items, this.exception)
: super(account, items);
@override
String toString() => _$toString();
ListSharingBlocFailure copyWith({
Account? account,
List<ListSharingItem>? items,
dynamic exception,
}) =>
ListSharingBlocFailure(
account ?? this.account,
items ?? List.of(this.items),
exception ?? this.exception,
);
final dynamic exception;
}
/// List shares to be shown in [SharingBrowser]
@npLog
class ListSharingBloc extends Bloc<ListSharingBlocEvent, ListSharingBlocState> {
ListSharingBloc(this._c)
: assert(require(_c)),
assert(FindFile.require(_c)),
assert(ListShareWithMe.require(_c)),
assert(LsSingleFile.require(_c)),
super(ListSharingBlocInit()) {
_shareRemovedListener.begin();
_fileMovedEventListener.begin();
_refreshThrottler = Throttler<Share>(
onTriggered: (shares) {
add(_ListSharingBlocShareRemoved(shares));
},
logTag: "ListSharingBloc.refresh",
);
on<ListSharingBlocEvent>(_onEvent);
}
static bool require(DiContainer c) =>
DiContainer.has(c, DiType.albumRepo) &&
DiContainer.has(c, DiType.fileRepo) &&
DiContainer.has(c, DiType.shareRepo);
static ListSharingBloc of(Account account) {
final name =
bloc_util.getInstNameForRootAwareAccount("ListSharingBloc", account);
try {
_log.fine("[of] Resolving bloc for '$name'");
return KiwiContainer().resolve<ListSharingBloc>(name);
} catch (_) {
// no created instance for this account, make a new one
_log.info("[of] New bloc instance for account: $account");
final bloc = ListSharingBloc(KiwiContainer().resolve<DiContainer>());
KiwiContainer().registerInstance<ListSharingBloc>(bloc, name: name);
return bloc;
}
}
@override
close() {
_shareRemovedListener.end();
_fileMovedEventListener.end();
_refreshThrottler.clear();
return super.close();
}
Future<void> _onEvent(
ListSharingBlocEvent event, Emitter<ListSharingBlocState> emit) async {
_log.info("[_onEvent] $event");
if (event is ListSharingBlocQuery) {
await _onEventQuery(event, emit);
} else if (event is _ListSharingBlocShareRemoved) {
await _onEventShareRemoved(event, emit);
} else if (event is _ListSharingBlocPendingSharedAlbumMoved) {
await _onEventPendingSharedAlbumMoved(event, emit);
}
}
Future<void> _onEventQuery(
ListSharingBlocQuery ev, Emitter<ListSharingBlocState> emit) async {
try {
emit(ListSharingBlocLoading(ev.account, state.items));
emit(ListSharingBlocSuccess(ev.account, await _query(ev)));
} catch (e, stackTrace) {
_log.severe("[_onEventQuery] Exception while request", e, stackTrace);
emit(ListSharingBlocFailure(ev.account, state.items, e));
}
}
Future<void> _onEventShareRemoved(_ListSharingBlocShareRemoved ev,
Emitter<ListSharingBlocState> emit) async {
if (state is! ListSharingBlocSuccess && state is! ListSharingBlocFailure) {
return;
}
final newItems =
state.items.where((i) => !ev.shares.contains(i.share)).toList();
// i love hacks :)
emit((state as dynamic).copyWith(
items: newItems,
) as ListSharingBlocState);
}
Future<void> _onEventPendingSharedAlbumMoved(
_ListSharingBlocPendingSharedAlbumMoved ev,
Emitter<ListSharingBlocState> emit) async {
if (state.items.isEmpty) {
return;
}
try {
emit(ListSharingBlocLoading(ev.account, state.items));
final items = List.of(state.items);
items.removeWhere(
(i) => i is ListSharingAlbum && i.share.path == ev.file.strippedPath);
final newShares =
await ListShareWithMe(_c)(ev.account, File(path: ev.destination));
final newAlbumFile = await LsSingleFile(_c)(ev.account, ev.destination);
final newAlbum = await _c.albumRepo.get(ev.account, newAlbumFile);
for (final s in newShares) {
items.add(ListSharingAlbum(s, newAlbum));
}
emit(ListSharingBlocSuccess(ev.account, items));
} catch (e, stackTrace) {
_log.severe("[_onEventPendingSharedAlbumMoved] Exception while request",
e, stackTrace);
emit(ListSharingBlocFailure(ev.account, state.items, e));
}
}
void _onShareRemovedEvent(ShareRemovedEvent ev) {
if (_isAccountOfInterest(ev.account)) {
_refreshThrottler.trigger(
maxResponceTime: const Duration(seconds: 3),
maxPendingCount: 10,
data: ev.share,
);
}
}
void _onFileMovedEvent(FileMovedEvent ev) {
if (state is ListSharingBlocInit) {
// no data in this bloc, ignore
return;
}
if (_isAccountOfInterest(ev.account)) {
if (ev.destination
.startsWith(remote_storage_util.getRemoteAlbumsDir(ev.account)) &&
ev.file.path.startsWith(remote_storage_util
.getRemotePendingSharedAlbumsDir(ev.account))) {
// moving from/to pending dir
add(_ListSharingBlocPendingSharedAlbumMoved(
ev.account, ev.file, ev.destination));
}
}
}
Future<List<ListSharingItem>> _query(ListSharingBlocQuery ev) async {
final sharedAlbumFiles = await Ls(_c.fileRepo)(
ev.account,
File(
path: remote_storage_util.getRemoteAlbumsDir(ev.account),
));
return (await Future.wait([
_querySharesByMe(ev, sharedAlbumFiles),
_querySharesWithMe(ev, sharedAlbumFiles),
]))
.reduce((value, element) => value + element);
}
Future<List<ListSharingItem>> _querySharesByMe(
ListSharingBlocQuery ev, List<File> sharedAlbumFiles) async {
final shares = await _c.shareRepo.listAll(ev.account);
final futures = shares.map((s) async {
final webdavPath = file_util.unstripPath(ev.account, s.path);
// include link share dirs
if (s.itemType == ShareItemType.folder) {
if (webdavPath.startsWith(
remote_storage_util.getRemoteLinkSharesDir(ev.account))) {
return ListSharingFile(
s,
File(
path: webdavPath,
fileId: s.itemSource,
isCollection: true,
),
);
}
}
// include shared albums
if (path_lib.dirname(webdavPath) ==
remote_storage_util.getRemoteAlbumsDir(ev.account)) {
try {
final file = sharedAlbumFiles
.firstWhere((element) => element.fileId == s.itemSource);
return await _querySharedAlbum(ev, s, file);
} catch (e, stackTrace) {
_log.severe(
"[_querySharesWithMe] Shared album not found: ${s.itemSource}",
e,
stackTrace);
return null;
}
}
if (!file_util.isSupportedMime(s.mimeType)) {
return null;
}
// show only link shares
if (s.url == null) {
return null;
}
if (ev.account.roots
.every((r) => r.isNotEmpty && !s.path.startsWith("$r/"))) {
// ignore files not under root dirs
return null;
}
try {
final file = (await FindFile(_c)(ev.account, [s.itemSource])).first;
return ListSharingFile(s, file);
} catch (e, stackTrace) {
_log.severe("[_querySharesByMe] File not found: ${s.itemSource}", e,
stackTrace);
return null;
}
});
return (await Future.wait(futures)).whereType<ListSharingItem>().toList();
}
Future<List<ListSharingItem>> _querySharesWithMe(
ListSharingBlocQuery ev, List<File> sharedAlbumFiles) async {
final pendingSharedAlbumFiles = await Ls(_c.fileRepo)(
ev.account,
File(
path: remote_storage_util.getRemotePendingSharedAlbumsDir(ev.account),
));
final shares = await _c.shareRepo.reverseListAll(ev.account);
final futures = shares.map((s) async {
final webdavPath = file_util.unstripPath(ev.account, s.path);
// include pending shared albums
if (path_lib.dirname(webdavPath) ==
remote_storage_util.getRemotePendingSharedAlbumsDir(ev.account)) {
try {
final file = pendingSharedAlbumFiles
.firstWhere((element) => element.fileId == s.itemSource);
return await _querySharedAlbum(ev, s, file);
} catch (e, stackTrace) {
_log.severe(
"[_querySharesWithMe] Pending shared album not found: ${s.itemSource}",
e,
stackTrace);
return null;
}
}
// include shared albums
if (path_lib.dirname(webdavPath) ==
remote_storage_util.getRemoteAlbumsDir(ev.account)) {
try {
final file = sharedAlbumFiles
.firstWhere((element) => element.fileId == s.itemSource);
return await _querySharedAlbum(ev, s, file);
} catch (e, stackTrace) {
_log.severe(
"[_querySharesWithMe] Shared album not found: ${s.itemSource}",
e,
stackTrace);
return null;
}
}
});
return (await Future.wait(futures)).whereType<ListSharingItem>().toList();
}
Future<ListSharingItem?> _querySharedAlbum(
ListSharingBlocQuery ev, Share share, File albumFile) async {
try {
final album = await _c.albumRepo.get(ev.account, albumFile);
return ListSharingAlbum(share, album);
} catch (e, stackTrace) {
_log.shout(
"[_querySharedAlbum] Failed while getting album", e, stackTrace);
return null;
}
}
bool _isAccountOfInterest(Account account) =>
state.account == null || state.account!.compareServerIdentity(account);
final DiContainer _c;
late final _shareRemovedListener =
AppEventListener<ShareRemovedEvent>(_onShareRemovedEvent);
late final _fileMovedEventListener =
AppEventListener<FileMovedEvent>(_onFileMovedEvent);
late Throttler _refreshThrottler;
static final _log = _$ListSharingBlocNpLog.log;
}