mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-03-25 00:14:42 +01:00
Refactor: revamp sharing browser code
This commit is contained in:
parent
1404717574
commit
b4cd90d6d4
13 changed files with 973 additions and 680 deletions
|
@ -1,424 +0,0 @@
|
||||||
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/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:np_collection/np_collection.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(super.share, this.file);
|
|
||||||
|
|
||||||
final File file;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ListSharingAlbum extends ListSharingItem {
|
|
||||||
const ListSharingAlbum(super.share, this.album);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'list_sharing.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// NpLogGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
extension _$ListSharingBlocNpLog on ListSharingBloc {
|
|
||||||
// ignore: unused_element
|
|
||||||
Logger get _log => log;
|
|
||||||
|
|
||||||
static final log = Logger("bloc.list_sharing.ListSharingBloc");
|
|
||||||
}
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// ToStringGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
extension _$ListSharingBlocQueryToString on ListSharingBlocQuery {
|
|
||||||
String _$toString() {
|
|
||||||
// ignore: unnecessary_string_interpolations
|
|
||||||
return "ListSharingBlocQuery {account: $account}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension _$_ListSharingBlocShareRemovedToString
|
|
||||||
on _ListSharingBlocShareRemoved {
|
|
||||||
String _$toString() {
|
|
||||||
// ignore: unnecessary_string_interpolations
|
|
||||||
return "_ListSharingBlocShareRemoved {shares: ${shares.toReadableString()}}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension _$_ListSharingBlocPendingSharedAlbumMovedToString
|
|
||||||
on _ListSharingBlocPendingSharedAlbumMoved {
|
|
||||||
String _$toString() {
|
|
||||||
// ignore: unnecessary_string_interpolations
|
|
||||||
return "_ListSharingBlocPendingSharedAlbumMoved {account: $account, file: ${file.path}, destination: $destination}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension _$ListSharingBlocStateToString on ListSharingBlocState {
|
|
||||||
String _$toString() {
|
|
||||||
// ignore: unnecessary_string_interpolations
|
|
||||||
return "${objectRuntimeType(this, "ListSharingBlocState")} {account: $account, items: [length: ${items.length}]}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension _$ListSharingBlocFailureToString on ListSharingBlocFailure {
|
|
||||||
String _$toString() {
|
|
||||||
// ignore: unnecessary_string_interpolations
|
|
||||||
return "ListSharingBlocFailure {account: $account, items: [length: ${items.length}], exception: $exception}";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,6 +5,7 @@ import 'package:nc_photos/controller/collections_controller.dart';
|
||||||
import 'package:nc_photos/controller/persons_controller.dart';
|
import 'package:nc_photos/controller/persons_controller.dart';
|
||||||
import 'package:nc_photos/controller/server_controller.dart';
|
import 'package:nc_photos/controller/server_controller.dart';
|
||||||
import 'package:nc_photos/controller/session_controller.dart';
|
import 'package:nc_photos/controller/session_controller.dart';
|
||||||
|
import 'package:nc_photos/controller/sharings_controller.dart';
|
||||||
import 'package:nc_photos/controller/sync_controller.dart';
|
import 'package:nc_photos/controller/sync_controller.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
|
||||||
|
@ -23,6 +24,8 @@ class AccountController {
|
||||||
_syncController = null;
|
_syncController = null;
|
||||||
_sessionController?.dispose();
|
_sessionController?.dispose();
|
||||||
_sessionController = null;
|
_sessionController = null;
|
||||||
|
_sharingsController?.dispose();
|
||||||
|
_sharingsController = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Account get account => _account!;
|
Account get account => _account!;
|
||||||
|
@ -58,6 +61,12 @@ class AccountController {
|
||||||
SessionController get sessionController =>
|
SessionController get sessionController =>
|
||||||
_sessionController ??= SessionController();
|
_sessionController ??= SessionController();
|
||||||
|
|
||||||
|
SharingsController get sharingsController =>
|
||||||
|
_sharingsController ??= SharingsController(
|
||||||
|
KiwiContainer().resolve(),
|
||||||
|
account: _account!,
|
||||||
|
);
|
||||||
|
|
||||||
Account? _account;
|
Account? _account;
|
||||||
CollectionsController? _collectionsController;
|
CollectionsController? _collectionsController;
|
||||||
ServerController? _serverController;
|
ServerController? _serverController;
|
||||||
|
@ -65,4 +74,5 @@ class AccountController {
|
||||||
PersonsController? _personsController;
|
PersonsController? _personsController;
|
||||||
SyncController? _syncController;
|
SyncController? _syncController;
|
||||||
SessionController? _sessionController;
|
SessionController? _sessionController;
|
||||||
|
SharingsController? _sharingsController;
|
||||||
}
|
}
|
||||||
|
|
193
app/lib/controller/sharings_controller.dart
Normal file
193
app/lib/controller/sharings_controller.dart
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:copy_with/copy_with.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
import 'package:nc_photos/entity/album.dart';
|
||||||
|
import 'package:nc_photos/entity/file.dart';
|
||||||
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
|
import 'package:nc_photos/entity/share.dart';
|
||||||
|
import 'package:nc_photos/event/event.dart';
|
||||||
|
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||||
|
import 'package:nc_photos/rx_extension.dart';
|
||||||
|
import 'package:nc_photos/use_case/list_share_with_me.dart';
|
||||||
|
import 'package:nc_photos/use_case/list_sharing.dart';
|
||||||
|
import 'package:nc_photos/use_case/ls_single_file.dart';
|
||||||
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
|
part 'sharings_controller.g.dart';
|
||||||
|
|
||||||
|
abstract class SharingStreamData {
|
||||||
|
static SharingStreamData _fromListSharingData(ListSharingData src) {
|
||||||
|
if (src is ListSharingFileData) {
|
||||||
|
return SharingStreamFileData(src.share, src.file);
|
||||||
|
} else if (src is ListSharingAlbumData) {
|
||||||
|
return SharingStreamAlbumData(src.share, src.album);
|
||||||
|
} else {
|
||||||
|
throw ArgumentError("Unsupported type: ${src.runtimeType}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SharingStreamShareData implements SharingStreamData {
|
||||||
|
const SharingStreamShareData(this.share);
|
||||||
|
|
||||||
|
final Share share;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SharingStreamFileData extends SharingStreamShareData {
|
||||||
|
const SharingStreamFileData(super.share, this.file);
|
||||||
|
|
||||||
|
final File file;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SharingStreamAlbumData extends SharingStreamShareData {
|
||||||
|
const SharingStreamAlbumData(super.share, this.album);
|
||||||
|
|
||||||
|
final Album album;
|
||||||
|
}
|
||||||
|
|
||||||
|
@genCopyWith
|
||||||
|
class SharingStreamEvent {
|
||||||
|
const SharingStreamEvent({
|
||||||
|
required this.data,
|
||||||
|
required this.hasNext,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<SharingStreamData> data;
|
||||||
|
|
||||||
|
/// If true, the results are intermediate values and may not represent the
|
||||||
|
/// latest state
|
||||||
|
final bool hasNext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@npLog
|
||||||
|
class SharingsController {
|
||||||
|
SharingsController(
|
||||||
|
this._c, {
|
||||||
|
required this.account,
|
||||||
|
});
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_sharingStreamContorller.close();
|
||||||
|
|
||||||
|
_shareRemovedListener?.end();
|
||||||
|
_fileMovedEventListener?.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a stream of curated shares associated with [account]
|
||||||
|
///
|
||||||
|
/// There's no guarantee that the returned list is always sorted in some ways,
|
||||||
|
/// callers must sort it by themselves if the ordering is important
|
||||||
|
ValueStream<SharingStreamEvent> get stream {
|
||||||
|
if (!_isSharingStreamInited) {
|
||||||
|
_isSharingStreamInited = true;
|
||||||
|
unawaited(_load(isReload: false));
|
||||||
|
}
|
||||||
|
return _sharingStreamContorller.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// In the future we need to get rid of the listeners and this reload function
|
||||||
|
/// and move all manipulations to this controller
|
||||||
|
Future<void> reload() async {
|
||||||
|
if (_isSharingStreamInited) {
|
||||||
|
return _load(isReload: true);
|
||||||
|
} else {
|
||||||
|
_log.warning("[reload] Not inited, ignore");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _load({required bool isReload}) async {
|
||||||
|
var lastData = _sharingStreamContorller.value.copyWith(hasNext: true);
|
||||||
|
_sharingStreamContorller.add(lastData);
|
||||||
|
final completer = Completer();
|
||||||
|
ListSharing(_c)(account).listen(
|
||||||
|
(c) {
|
||||||
|
lastData = SharingStreamEvent(
|
||||||
|
data: c.map(SharingStreamData._fromListSharingData).toList(),
|
||||||
|
hasNext: true,
|
||||||
|
);
|
||||||
|
if (!isReload) {
|
||||||
|
_sharingStreamContorller.add(lastData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: _sharingStreamContorller.addError,
|
||||||
|
onDone: () => completer.complete(),
|
||||||
|
);
|
||||||
|
await completer.future;
|
||||||
|
_sharingStreamContorller.add(lastData.copyWith(hasNext: false));
|
||||||
|
|
||||||
|
_shareRemovedListener =
|
||||||
|
AppEventListener<ShareRemovedEvent>(_onShareRemovedEvent)..begin();
|
||||||
|
_fileMovedEventListener =
|
||||||
|
AppEventListener<FileMovedEvent>(_onFileMovedEvent)..begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onShareRemovedEvent(ShareRemovedEvent ev) {
|
||||||
|
if (!_isAccountOfInterest(ev.account)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_sharingStreamContorller.addWithValue((value) => value.copyWith(
|
||||||
|
data: value.data.where((e) {
|
||||||
|
if (e is SharingStreamShareData) {
|
||||||
|
return e.share.id != ev.share.id;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}).toList(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onFileMovedEvent(FileMovedEvent ev) async {
|
||||||
|
if (!_isAccountOfInterest(ev.account)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ev.destination
|
||||||
|
.startsWith(remote_storage_util.getRemoteAlbumsDir(ev.account)) &&
|
||||||
|
ev.file.path.startsWith(
|
||||||
|
remote_storage_util.getRemotePendingSharedAlbumsDir(ev.account))) {
|
||||||
|
// moving from pending dir to album dir
|
||||||
|
} else if (ev.destination.startsWith(
|
||||||
|
remote_storage_util.getRemotePendingSharedAlbumsDir(ev.account)) &&
|
||||||
|
ev.file.path
|
||||||
|
.startsWith(remote_storage_util.getRemoteAlbumsDir(ev.account))) {
|
||||||
|
// moving from album dir to pending dir
|
||||||
|
} else {
|
||||||
|
// unrelated file
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_log.info("[_onFileMovedEvent] ${ev.file.path} -> ${ev.destination}");
|
||||||
|
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);
|
||||||
|
if (_sharingStreamContorller.isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_sharingStreamContorller.addWithValue((value) => value.copyWith(
|
||||||
|
data: value.data
|
||||||
|
.whereNot((e) =>
|
||||||
|
e is SharingStreamAlbumData &&
|
||||||
|
e.share.path == ev.file.strippedPath)
|
||||||
|
.toList()
|
||||||
|
..addAll(newShares.map((s) => SharingStreamAlbumData(s, newAlbum))),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isAccountOfInterest(Account account) =>
|
||||||
|
this.account.compareServerIdentity(account);
|
||||||
|
|
||||||
|
final DiContainer _c;
|
||||||
|
final Account account;
|
||||||
|
|
||||||
|
var _isSharingStreamInited = false;
|
||||||
|
final _sharingStreamContorller = BehaviorSubject.seeded(
|
||||||
|
const SharingStreamEvent(data: [], hasNext: true),
|
||||||
|
);
|
||||||
|
|
||||||
|
AppEventListener<ShareRemovedEvent>? _shareRemovedListener;
|
||||||
|
AppEventListener<FileMovedEvent>? _fileMovedEventListener;
|
||||||
|
}
|
49
app/lib/controller/sharings_controller.g.dart
Normal file
49
app/lib/controller/sharings_controller.g.dart
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'sharings_controller.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// CopyWithLintRuleGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// CopyWithGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
abstract class $SharingStreamEventCopyWithWorker {
|
||||||
|
SharingStreamEvent call({List<SharingStreamData>? data, bool? hasNext});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _$SharingStreamEventCopyWithWorkerImpl
|
||||||
|
implements $SharingStreamEventCopyWithWorker {
|
||||||
|
_$SharingStreamEventCopyWithWorkerImpl(this.that);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SharingStreamEvent call({dynamic data, dynamic hasNext}) {
|
||||||
|
return SharingStreamEvent(
|
||||||
|
data: data as List<SharingStreamData>? ?? that.data,
|
||||||
|
hasNext: hasNext as bool? ?? that.hasNext);
|
||||||
|
}
|
||||||
|
|
||||||
|
final SharingStreamEvent that;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension $SharingStreamEventCopyWith on SharingStreamEvent {
|
||||||
|
$SharingStreamEventCopyWithWorker get copyWith => _$copyWith;
|
||||||
|
$SharingStreamEventCopyWithWorker get _$copyWith =>
|
||||||
|
_$SharingStreamEventCopyWithWorkerImpl(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// NpLogGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
extension _$SharingsControllerNpLog on SharingsController {
|
||||||
|
// ignore: unused_element
|
||||||
|
Logger get _log => log;
|
||||||
|
|
||||||
|
static final log =
|
||||||
|
Logger("controller.sharings_controller.SharingsController");
|
||||||
|
}
|
206
app/lib/use_case/list_sharing.dart
Normal file
206
app/lib/use_case/list_sharing.dart
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
import 'package:nc_photos/entity/album.dart';
|
||||||
|
import 'package:nc_photos/entity/file.dart';
|
||||||
|
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||||
|
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/find_file.dart';
|
||||||
|
import 'package:nc_photos/use_case/ls.dart';
|
||||||
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
import 'package:path/path.dart' as path_lib;
|
||||||
|
|
||||||
|
part 'list_sharing.g.dart';
|
||||||
|
|
||||||
|
abstract class ListSharingData {}
|
||||||
|
|
||||||
|
class ListSharingFileData implements ListSharingData {
|
||||||
|
const ListSharingFileData(this.share, this.file);
|
||||||
|
|
||||||
|
final Share share;
|
||||||
|
final File file;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListSharingAlbumData implements ListSharingData {
|
||||||
|
const ListSharingAlbumData(this.share, this.album);
|
||||||
|
|
||||||
|
final Share share;
|
||||||
|
final Album album;
|
||||||
|
}
|
||||||
|
|
||||||
|
@npLog
|
||||||
|
class ListSharing {
|
||||||
|
ListSharing(this._c);
|
||||||
|
|
||||||
|
Stream<List<ListSharingData>> call(Account account) async* {
|
||||||
|
final sharedAlbumFiles = await Ls(_c.fileRepo)(
|
||||||
|
account,
|
||||||
|
File(
|
||||||
|
path: remote_storage_util.getRemoteAlbumsDir(account),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final controller = StreamController<List<ListSharingData>>();
|
||||||
|
var byMe = <ListSharingData>[];
|
||||||
|
var isByMeDone = false;
|
||||||
|
var withMe = <ListSharingData>[];
|
||||||
|
var isWithMeDone = false;
|
||||||
|
|
||||||
|
void notify() {
|
||||||
|
controller.add([
|
||||||
|
...byMe,
|
||||||
|
...withMe,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDone() {
|
||||||
|
if (isByMeDone && isWithMeDone) {
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unawaited(_querySharesByMe(account, sharedAlbumFiles).then((value) {
|
||||||
|
byMe = value;
|
||||||
|
notify();
|
||||||
|
}).catchError((e, stackTrace) {
|
||||||
|
controller.addError(e, stackTrace);
|
||||||
|
}).whenComplete(() {
|
||||||
|
isByMeDone = true;
|
||||||
|
onDone();
|
||||||
|
}));
|
||||||
|
unawaited(_querySharesWithMe(account, sharedAlbumFiles).then((value) {
|
||||||
|
withMe = value;
|
||||||
|
notify();
|
||||||
|
}).catchError((e, stackTrace) {
|
||||||
|
controller.addError(e, stackTrace);
|
||||||
|
}).whenComplete(() {
|
||||||
|
isWithMeDone = true;
|
||||||
|
onDone();
|
||||||
|
}));
|
||||||
|
yield* controller.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ListSharingData>> _querySharesByMe(
|
||||||
|
Account account, List<File> sharedAlbumFiles) async {
|
||||||
|
final shares = await _c.shareRepo.listAll(account);
|
||||||
|
final futures = shares.map((s) async {
|
||||||
|
final webdavPath = file_util.unstripPath(account, s.path);
|
||||||
|
// include link share dirs
|
||||||
|
if (s.itemType == ShareItemType.folder) {
|
||||||
|
if (webdavPath
|
||||||
|
.startsWith(remote_storage_util.getRemoteLinkSharesDir(account))) {
|
||||||
|
return ListSharingFileData(
|
||||||
|
s,
|
||||||
|
File(
|
||||||
|
path: webdavPath,
|
||||||
|
fileId: s.itemSource,
|
||||||
|
isCollection: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// include shared albums
|
||||||
|
if (path_lib.dirname(webdavPath) ==
|
||||||
|
remote_storage_util.getRemoteAlbumsDir(account)) {
|
||||||
|
try {
|
||||||
|
final file = sharedAlbumFiles
|
||||||
|
.firstWhere((element) => element.fileId == s.itemSource);
|
||||||
|
return await _querySharedAlbum(account, 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 (account.roots
|
||||||
|
.every((r) => r.isNotEmpty && !s.path.startsWith("$r/"))) {
|
||||||
|
// ignore files not under root dirs
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final file = (await FindFile(_c)(account, [s.itemSource])).first;
|
||||||
|
return ListSharingFileData(s, file);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.severe("[_querySharesByMe] File not found: ${s.itemSource}", e,
|
||||||
|
stackTrace);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return (await Future.wait(futures)).whereNotNull().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ListSharingData>> _querySharesWithMe(
|
||||||
|
Account account, List<File> sharedAlbumFiles) async {
|
||||||
|
final pendingSharedAlbumFiles = await Ls(_c.fileRepo)(
|
||||||
|
account,
|
||||||
|
File(
|
||||||
|
path: remote_storage_util.getRemotePendingSharedAlbumsDir(account),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final shares = await _c.shareRepo.reverseListAll(account);
|
||||||
|
final futures = shares.map((s) async {
|
||||||
|
final webdavPath = file_util.unstripPath(account, s.path);
|
||||||
|
// include pending shared albums
|
||||||
|
if (path_lib.dirname(webdavPath) ==
|
||||||
|
remote_storage_util.getRemotePendingSharedAlbumsDir(account)) {
|
||||||
|
try {
|
||||||
|
final file = pendingSharedAlbumFiles
|
||||||
|
.firstWhere((element) => element.fileId == s.itemSource);
|
||||||
|
return await _querySharedAlbum(account, 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(account)) {
|
||||||
|
try {
|
||||||
|
final file = sharedAlbumFiles
|
||||||
|
.firstWhere((element) => element.fileId == s.itemSource);
|
||||||
|
return await _querySharedAlbum(account, s, file);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.severe(
|
||||||
|
"[_querySharesWithMe] Shared album not found: ${s.itemSource}",
|
||||||
|
e,
|
||||||
|
stackTrace);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return (await Future.wait(futures)).whereNotNull().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ListSharingData?> _querySharedAlbum(
|
||||||
|
Account account, Share share, File albumFile) async {
|
||||||
|
try {
|
||||||
|
final album = await _c.albumRepo.get(account, albumFile);
|
||||||
|
return ListSharingAlbumData(share, album);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.shout(
|
||||||
|
"[_querySharedAlbum] Failed while getting album", e, stackTrace);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final DiContainer _c;
|
||||||
|
}
|
14
app/lib/use_case/list_sharing.g.dart
Normal file
14
app/lib/use_case/list_sharing.g.dart
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'list_sharing.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// NpLogGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
extension _$ListSharingNpLog on ListSharing {
|
||||||
|
// ignore: unused_element
|
||||||
|
Logger get _log => log;
|
||||||
|
|
||||||
|
static final log = Logger("use_case.list_sharing.ListSharing");
|
||||||
|
}
|
|
@ -191,6 +191,7 @@ class _WrappedAppState extends State<_WrappedApp>
|
||||||
PeopleBrowser.routeName: PeopleBrowser.buildRoute,
|
PeopleBrowser.routeName: PeopleBrowser.buildRoute,
|
||||||
EnhancementSettings.routeName: EnhancementSettings.buildRoute,
|
EnhancementSettings.routeName: EnhancementSettings.buildRoute,
|
||||||
Settings.routeName: Settings.buildRoute,
|
Settings.routeName: Settings.buildRoute,
|
||||||
|
SharingBrowser.routeName: SharingBrowser.buildRoute,
|
||||||
};
|
};
|
||||||
|
|
||||||
Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
|
Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
|
||||||
|
@ -208,7 +209,6 @@ class _WrappedAppState extends State<_WrappedApp>
|
||||||
route ??= _handleTrashbinBrowserRoute(settings);
|
route ??= _handleTrashbinBrowserRoute(settings);
|
||||||
route ??= _handleTrashbinViewerRoute(settings);
|
route ??= _handleTrashbinViewerRoute(settings);
|
||||||
route ??= _handleSlideshowViewerRoute(settings);
|
route ??= _handleSlideshowViewerRoute(settings);
|
||||||
route ??= _handleSharingBrowserRoute(settings);
|
|
||||||
route ??= _handleSharedFileViewerRoute(settings);
|
route ??= _handleSharedFileViewerRoute(settings);
|
||||||
route ??= _handleAlbumShareOutlierBrowserRoute(settings);
|
route ??= _handleAlbumShareOutlierBrowserRoute(settings);
|
||||||
route ??= _handleShareFolderPickerRoute(settings);
|
route ??= _handleShareFolderPickerRoute(settings);
|
||||||
|
@ -379,20 +379,6 @@ class _WrappedAppState extends State<_WrappedApp>
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Route<dynamic>? _handleSharingBrowserRoute(RouteSettings settings) {
|
|
||||||
try {
|
|
||||||
if (settings.name == SharingBrowser.routeName &&
|
|
||||||
settings.arguments != null) {
|
|
||||||
final args = settings.arguments as SharingBrowserArguments;
|
|
||||||
return SharingBrowser.buildRoute(args);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
_log.severe(
|
|
||||||
"[_handleSharingBrowserRoute] Failed while handling route", e);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Route<dynamic>? _handleSharedFileViewerRoute(RouteSettings settings) {
|
Route<dynamic>? _handleSharedFileViewerRoute(RouteSettings settings) {
|
||||||
try {
|
try {
|
||||||
if (settings.name == SharedFileViewer.routeName &&
|
if (settings.name == SharedFileViewer.routeName &&
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:copy_with/copy_with.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
@ -6,7 +9,9 @@ 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_localizations.dart';
|
import 'package:nc_photos/app_localizations.dart';
|
||||||
import 'package:nc_photos/bloc/list_sharing.dart';
|
import 'package:nc_photos/controller/account_controller.dart';
|
||||||
|
import 'package:nc_photos/controller/account_pref_controller.dart';
|
||||||
|
import 'package:nc_photos/controller/sharings_controller.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/entity/album.dart';
|
import 'package:nc_photos/entity/album.dart';
|
||||||
import 'package:nc_photos/entity/album/data_source.dart';
|
import 'package:nc_photos/entity/album/data_source.dart';
|
||||||
|
@ -15,6 +20,7 @@ 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/pref.dart';
|
import 'package:nc_photos/entity/pref.dart';
|
||||||
import 'package:nc_photos/entity/share.dart';
|
import 'package:nc_photos/entity/share.dart';
|
||||||
|
import 'package:nc_photos/exception_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/k.dart' as k;
|
import 'package:nc_photos/k.dart' as k;
|
||||||
import 'package:nc_photos/object_extension.dart';
|
import 'package:nc_photos/object_extension.dart';
|
||||||
|
@ -23,12 +29,22 @@ import 'package:nc_photos/use_case/import_potential_shared_album.dart';
|
||||||
import 'package:nc_photos/widget/collection_browser.dart';
|
import 'package:nc_photos/widget/collection_browser.dart';
|
||||||
import 'package:nc_photos/widget/empty_list_indicator.dart';
|
import 'package:nc_photos/widget/empty_list_indicator.dart';
|
||||||
import 'package:nc_photos/widget/network_thumbnail.dart';
|
import 'package:nc_photos/widget/network_thumbnail.dart';
|
||||||
|
import 'package:nc_photos/widget/page_visibility_mixin.dart';
|
||||||
import 'package:nc_photos/widget/shared_file_viewer.dart';
|
import 'package:nc_photos/widget/shared_file_viewer.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
import 'package:np_collection/np_collection.dart';
|
||||||
import 'package:np_common/or_null.dart';
|
import 'package:np_common/or_null.dart';
|
||||||
import 'package:np_ui/np_ui.dart';
|
import 'package:np_ui/np_ui.dart';
|
||||||
|
import 'package:to_string/to_string.dart';
|
||||||
|
|
||||||
part 'sharing_browser.g.dart';
|
part 'sharing_browser.g.dart';
|
||||||
|
part 'sharing_browser/bloc.dart';
|
||||||
|
part 'sharing_browser/state_event.dart';
|
||||||
|
part 'sharing_browser/type.dart';
|
||||||
|
|
||||||
|
typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
|
||||||
|
typedef _BlocListener = BlocListener<_Bloc, _State>;
|
||||||
|
// typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
|
||||||
|
|
||||||
class SharingBrowserArguments {
|
class SharingBrowserArguments {
|
||||||
SharingBrowserArguments(this.account);
|
SharingBrowserArguments(this.account);
|
||||||
|
@ -37,39 +53,44 @@ class SharingBrowserArguments {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show a list of all shares associated with this account
|
/// Show a list of all shares associated with this account
|
||||||
class SharingBrowser extends StatefulWidget {
|
class SharingBrowser extends StatelessWidget {
|
||||||
static const routeName = "/sharing-browser";
|
static const routeName = "/sharing-browser";
|
||||||
|
|
||||||
static Route buildRoute(SharingBrowserArguments args) => MaterialPageRoute(
|
static Route buildRoute() => MaterialPageRoute(
|
||||||
builder: (context) => SharingBrowser.fromArgs(args),
|
builder: (_) => const SharingBrowser(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const SharingBrowser({
|
const SharingBrowser({super.key});
|
||||||
Key? key,
|
|
||||||
required this.account,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
SharingBrowser.fromArgs(SharingBrowserArguments args, {Key? key})
|
|
||||||
: this(
|
|
||||||
key: key,
|
|
||||||
account: args.account,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
createState() => _SharingBrowserState();
|
Widget build(BuildContext context) {
|
||||||
|
final accountController = context.read<AccountController>();
|
||||||
|
return BlocProvider(
|
||||||
|
create: (_) => _Bloc(
|
||||||
|
account: accountController.account,
|
||||||
|
accountPrefController: accountController.accountPrefController,
|
||||||
|
sharingsController: accountController.sharingsController,
|
||||||
|
),
|
||||||
|
child: const _WrappedSharingBrowser(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final Account account;
|
class _WrappedSharingBrowser extends StatefulWidget {
|
||||||
|
const _WrappedSharingBrowser();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _WrappedSharingBrowserState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@npLog
|
@npLog
|
||||||
class _SharingBrowserState extends State<SharingBrowser> {
|
class _WrappedSharingBrowserState extends State<_WrappedSharingBrowser>
|
||||||
|
with RouteAware, PageVisibilityMixin {
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_importPotentialSharedAlbum().whenComplete(() {
|
_bloc.add(const _Init());
|
||||||
_initBloc();
|
AccountPref.of(_bloc.account).run((obj) {
|
||||||
});
|
|
||||||
AccountPref.of(widget.account).run((obj) {
|
|
||||||
if (obj.hasNewSharedAlbumOr()) {
|
if (obj.hasNewSharedAlbumOr()) {
|
||||||
obj.setNewSharedAlbum(false);
|
obj.setNewSharedAlbum(false);
|
||||||
}
|
}
|
||||||
|
@ -77,67 +98,82 @@ class _SharingBrowserState extends State<SharingBrowser> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return MultiBlocListener(
|
||||||
body: BlocListener<ListSharingBloc, ListSharingBlocState>(
|
listeners: [
|
||||||
bloc: _bloc,
|
_BlocListener(
|
||||||
listener: (context, state) => _onStateChange(context, state),
|
listenWhen: (previous, current) => previous.items != current.items,
|
||||||
child: BlocBuilder<ListSharingBloc, ListSharingBlocState>(
|
listener: (context, state) {
|
||||||
bloc: _bloc,
|
_bloc.add(_TransformItems(state.items));
|
||||||
builder: (context, state) => _buildContent(context, state),
|
},
|
||||||
|
),
|
||||||
|
_BlocListener(
|
||||||
|
listenWhen: (previous, current) => previous.error != current.error,
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state.error != null && isPageVisible()) {
|
||||||
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
content: Text(exception_util.toUserString(state.error!.error)),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: Scaffold(
|
||||||
|
body: _BlocBuilder(
|
||||||
|
buildWhen: (previous, current) =>
|
||||||
|
previous.items.isEmpty != current.items.isEmpty ||
|
||||||
|
previous.isLoading != current.isLoading,
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.items.isEmpty && !state.isLoading) {
|
||||||
|
return const _EmptyContentList();
|
||||||
|
} else {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
const _AppBar(),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: _BlocBuilder(
|
||||||
|
buildWhen: (previous, current) =>
|
||||||
|
previous.isLoading != current.isLoading,
|
||||||
|
builder: (context, state) => state.isLoading
|
||||||
|
? const LinearProgressIndicator()
|
||||||
|
: const SizedBox(height: 4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const _ContentList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initBloc() {
|
late final _bloc = context.read<_Bloc>();
|
||||||
if (_bloc.state is ListSharingBlocInit) {
|
}
|
||||||
_log.info("[_initBloc] Initialize bloc");
|
|
||||||
} else {
|
|
||||||
// process the current state
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_onStateChange(context, _bloc.state);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_reqQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context, ListSharingBlocState state) {
|
class _AppBar extends StatelessWidget {
|
||||||
if ((state is ListSharingBlocSuccess || state is ListSharingBlocFailure) &&
|
const _AppBar();
|
||||||
state.items.isEmpty) {
|
|
||||||
return _buildEmptyContent(context);
|
|
||||||
} else {
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverAppBar(
|
|
||||||
title: Text(L10n.global().collectionSharingLabel),
|
|
||||||
floating: true,
|
|
||||||
),
|
|
||||||
SliverList(
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(context, index) => _buildItem(context, _items[index]),
|
|
||||||
childCount: _items.length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (state is ListSharingBlocLoading)
|
|
||||||
const Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: LinearProgressIndicator(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEmptyContent(BuildContext context) {
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverAppBar(
|
||||||
|
title: Text(L10n.global().collectionSharingLabel),
|
||||||
|
floating: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EmptyContentList extends StatelessWidget {
|
||||||
|
const _EmptyContentList();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
AppBar(
|
AppBar(
|
||||||
|
@ -153,115 +189,63 @@ class _SharingBrowserState extends State<SharingBrowser> {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildFileItem(BuildContext context, List<ListSharingItem> shares) {
|
class _ContentList extends StatelessWidget {
|
||||||
assert(shares.first is ListSharingFile);
|
const _ContentList();
|
||||||
final item = shares.first as ListSharingFile;
|
|
||||||
return _FileTile(
|
|
||||||
account: widget.account,
|
|
||||||
item: item,
|
|
||||||
isLinkShare: shares.any((e) => e.share.url?.isNotEmpty == true),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pushNamed(SharedFileViewer.routeName,
|
|
||||||
arguments: SharedFileViewerArguments(
|
|
||||||
widget.account,
|
|
||||||
item.file,
|
|
||||||
shares.map((e) => e.share).toList(),
|
|
||||||
));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAlbumItem(BuildContext context, List<ListSharingItem> shares) {
|
@override
|
||||||
assert(shares.first is ListSharingAlbum);
|
Widget build(BuildContext context) {
|
||||||
final item = shares.first as ListSharingAlbum;
|
return _BlocBuilder(
|
||||||
return _AlbumTile(
|
buildWhen: (previous, current) =>
|
||||||
account: widget.account,
|
previous.transformedItems != current.transformedItems,
|
||||||
item: item,
|
builder: (_, state) => SliverList(
|
||||||
onTap: () => _onAlbumShareItemTap(context, item),
|
delegate: SliverChildBuilderDelegate(
|
||||||
);
|
(context, index) =>
|
||||||
}
|
_buildItem(context, state.transformedItems[index]),
|
||||||
|
childCount: state.transformedItems.length,
|
||||||
Widget _buildItem(BuildContext context, List<ListSharingItem> shares) {
|
),
|
||||||
if (shares.first is ListSharingFile) {
|
|
||||||
return _buildFileItem(context, shares);
|
|
||||||
} else if (shares.first is ListSharingAlbum) {
|
|
||||||
return _buildAlbumItem(context, shares);
|
|
||||||
} else {
|
|
||||||
throw StateError("Unknown item type: ${shares.first.runtimeType}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onStateChange(BuildContext context, ListSharingBlocState state) {
|
|
||||||
if (state is ListSharingBlocInit) {
|
|
||||||
_items = [];
|
|
||||||
} else if (state is ListSharingBlocSuccess ||
|
|
||||||
state is ListSharingBlocLoading) {
|
|
||||||
_transformItems(state.items);
|
|
||||||
} else if (state is ListSharingBlocFailure) {
|
|
||||||
_transformItems(state.items);
|
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
|
||||||
content: Text(exception_util.toUserString(state.exception)),
|
|
||||||
duration: k.snackBarDurationNormal,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onAlbumShareItemTap(BuildContext context, ListSharingAlbum share) {
|
|
||||||
Navigator.of(context).pushNamed(
|
|
||||||
CollectionBrowser.routeName,
|
|
||||||
arguments: CollectionBrowserArguments(
|
|
||||||
CollectionBuilder.byAlbum(widget.account, share.album),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _transformItems(List<ListSharingItem> items) {
|
Widget _buildItem(BuildContext context, _Item data) {
|
||||||
// group shares of the same file
|
if (data is _FileShareItem) {
|
||||||
final map = <String, List<ListSharingItem>>{};
|
return _buildFileItem(context, data);
|
||||||
for (final i in items) {
|
} else if (data is _AlbumShareItem) {
|
||||||
final isSharedByMe = (i.share.uidOwner == widget.account.userId);
|
return _buildAlbumItem(context, data);
|
||||||
final groupKey = "${i.share.path}?$isSharedByMe";
|
} else {
|
||||||
map[groupKey] ??= <ListSharingItem>[];
|
throw ArgumentError("Unknown item type: ${data.runtimeType}");
|
||||||
map[groupKey]!.add(i);
|
|
||||||
}
|
}
|
||||||
// sort the sub-lists
|
|
||||||
for (final list in map.values) {
|
|
||||||
list.sort((a, b) => b.share.stime.compareTo(a.share.stime));
|
|
||||||
}
|
|
||||||
// then sort the map and convert it to list
|
|
||||||
_items = map.entries
|
|
||||||
.sorted((a, b) =>
|
|
||||||
b.value.first.share.stime.compareTo(a.value.first.share.stime))
|
|
||||||
.map((e) => e.value)
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _reqQuery() {
|
Widget _buildFileItem(BuildContext context, _FileShareItem item) {
|
||||||
_bloc.add(ListSharingBlocQuery(widget.account));
|
return _FileTile(
|
||||||
|
account: item.account,
|
||||||
|
item: item,
|
||||||
|
isLinkShare: item.shares.any((e) => e.url?.isNotEmpty == true),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pushNamed(SharedFileViewer.routeName,
|
||||||
|
arguments: SharedFileViewerArguments(
|
||||||
|
item.account, item.file, item.shares));
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Album>> _importPotentialSharedAlbum() async {
|
Widget _buildAlbumItem(BuildContext context, _AlbumShareItem item) {
|
||||||
final c = KiwiContainer().resolve<DiContainer>().copyWith(
|
return _AlbumTile(
|
||||||
// don't want the potential albums to be cached at this moment
|
account: item.account,
|
||||||
fileRepo: const OrNull(FileRepo(FileWebdavDataSource())),
|
item: item,
|
||||||
albumRepo: OrNull(AlbumRepo(AlbumRemoteDataSource())),
|
onTap: () {
|
||||||
|
Navigator.of(context).pushNamed(
|
||||||
|
CollectionBrowser.routeName,
|
||||||
|
arguments: CollectionBrowserArguments(
|
||||||
|
CollectionBuilder.byAlbum(item.account, item.album),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
try {
|
},
|
||||||
return await ImportPotentialSharedAlbum(c)(
|
);
|
||||||
widget.account, AccountPref.of(widget.account));
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
_log.shout(
|
|
||||||
"[_importPotentialSharedAlbum] Failed while ImportPotentialSharedAlbum",
|
|
||||||
e,
|
|
||||||
stackTrace);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
late final _bloc = ListSharingBloc.of(widget.account);
|
|
||||||
|
|
||||||
var _items = <List<ListSharingItem>>[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ListTile extends StatelessWidget {
|
class _ListTile extends StatelessWidget {
|
||||||
|
@ -274,7 +258,7 @@ class _ListTile extends StatelessWidget {
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return UnboundedListTile(
|
return UnboundedListTile(
|
||||||
leading: leading,
|
leading: leading,
|
||||||
title: Text(
|
title: Text(
|
||||||
|
@ -305,9 +289,9 @@ class _FileTile extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final dateStr = _getDateFormat(context).format(item.share.stime.toLocal());
|
final dateStr = _getDateFormat(context).format(item.sharedTime!.toLocal());
|
||||||
return _ListTile(
|
return _ListTile(
|
||||||
leading: item.share.itemType == ShareItemType.folder
|
leading: item.shares.first.itemType == ShareItemType.folder
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
height: _leadingSize,
|
height: _leadingSize,
|
||||||
width: _leadingSize,
|
width: _leadingSize,
|
||||||
|
@ -320,18 +304,18 @@ class _FileTile extends StatelessWidget {
|
||||||
dimension: _leadingSize,
|
dimension: _leadingSize,
|
||||||
errorBuilder: (_) => const Icon(Icons.folder, size: 32),
|
errorBuilder: (_) => const Icon(Icons.folder, size: 32),
|
||||||
),
|
),
|
||||||
label: item.share.filename,
|
label: item.name,
|
||||||
description: item.share.uidOwner == account.userId
|
description: item.sharedBy == null
|
||||||
? L10n.global().fileLastSharedDescription(dateStr)
|
? L10n.global().fileLastSharedDescription(dateStr)
|
||||||
: L10n.global().fileLastSharedByOthersDescription(
|
: L10n.global()
|
||||||
item.share.displaynameOwner, dateStr),
|
.fileLastSharedByOthersDescription(item.sharedBy!, dateStr),
|
||||||
trailing: isLinkShare ? const Icon(Icons.link) : null,
|
trailing: isLinkShare ? const Icon(Icons.link) : null,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Account account;
|
final Account account;
|
||||||
final ListSharingFile item;
|
final _FileShareItem item;
|
||||||
final bool isLinkShare;
|
final bool isLinkShare;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
}
|
}
|
||||||
|
@ -345,7 +329,7 @@ class _AlbumTile extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final dateStr = _getDateFormat(context).format(item.share.stime.toLocal());
|
final dateStr = _getDateFormat(context).format(item.sharedTime!.toLocal());
|
||||||
final cover = item.album.coverProvider.getCover(item.album);
|
final cover = item.album.coverProvider.getCover(item.album);
|
||||||
return _ListTile(
|
return _ListTile(
|
||||||
leading: cover == null
|
leading: cover == null
|
||||||
|
@ -361,17 +345,17 @@ class _AlbumTile extends StatelessWidget {
|
||||||
errorBuilder: (_) => const Icon(Icons.photo_album, size: 32),
|
errorBuilder: (_) => const Icon(Icons.photo_album, size: 32),
|
||||||
),
|
),
|
||||||
label: item.album.name,
|
label: item.album.name,
|
||||||
description: item.share.uidOwner == account.userId
|
description: item.sharedBy == null
|
||||||
? L10n.global().fileLastSharedDescription(dateStr)
|
? L10n.global().fileLastSharedDescription(dateStr)
|
||||||
: L10n.global().albumLastSharedByOthersDescription(
|
: L10n.global()
|
||||||
item.share.displaynameOwner, dateStr),
|
.albumLastSharedByOthersDescription(item.sharedBy!, dateStr),
|
||||||
trailing: const Icon(Icons.photo_album_outlined),
|
trailing: const Icon(Icons.photo_album_outlined),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Account account;
|
final Account account;
|
||||||
final ListSharingAlbum item;
|
final _AlbumShareItem item;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,105 @@
|
||||||
|
|
||||||
part of 'sharing_browser.dart';
|
part of 'sharing_browser.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// CopyWithLintRuleGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// CopyWithGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
abstract class $_StateCopyWithWorker {
|
||||||
|
_State call(
|
||||||
|
{List<SharingStreamData>? items,
|
||||||
|
bool? isLoading,
|
||||||
|
List<_Item>? transformedItems,
|
||||||
|
ExceptionEvent? error});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
|
||||||
|
_$_StateCopyWithWorkerImpl(this.that);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_State call(
|
||||||
|
{dynamic items,
|
||||||
|
dynamic isLoading,
|
||||||
|
dynamic transformedItems,
|
||||||
|
dynamic error = copyWithNull}) {
|
||||||
|
return _State(
|
||||||
|
items: items as List<SharingStreamData>? ?? that.items,
|
||||||
|
isLoading: isLoading as bool? ?? that.isLoading,
|
||||||
|
transformedItems:
|
||||||
|
transformedItems as List<_Item>? ?? that.transformedItems,
|
||||||
|
error: error == copyWithNull ? that.error : error as ExceptionEvent?);
|
||||||
|
}
|
||||||
|
|
||||||
|
final _State that;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension $_StateCopyWith on _State {
|
||||||
|
$_StateCopyWithWorker get copyWith => _$copyWith;
|
||||||
|
$_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this);
|
||||||
|
}
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// NpLogGenerator
|
// NpLogGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
extension _$_SharingBrowserStateNpLog on _SharingBrowserState {
|
extension _$_WrappedSharingBrowserStateNpLog on _WrappedSharingBrowserState {
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
Logger get _log => log;
|
Logger get _log => log;
|
||||||
|
|
||||||
static final log = Logger("widget.sharing_browser._SharingBrowserState");
|
static final log =
|
||||||
|
Logger("widget.sharing_browser._WrappedSharingBrowserState");
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_BlocNpLog on _Bloc {
|
||||||
|
// ignore: unused_element
|
||||||
|
Logger get _log => log;
|
||||||
|
|
||||||
|
static final log = Logger("widget.sharing_browser._Bloc");
|
||||||
|
}
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// ToStringGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
extension _$_StateToString on _State {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_State {items: [length: ${items.length}], isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], error: $error}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_InitToString on _Init {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_Init {}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_TransformItemsToString on _TransformItems {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_TransformItems {items: [length: ${items.length}]}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_ListSharingBlocShareRemovedToString
|
||||||
|
on _ListSharingBlocShareRemoved {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_ListSharingBlocShareRemoved {shares: ${shares.toReadableString()}}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_ListSharingBlocPendingSharedAlbumMovedToString
|
||||||
|
on _ListSharingBlocPendingSharedAlbumMoved {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_ListSharingBlocPendingSharedAlbumMoved {account: $account, file: ${file.path}, destination: $destination}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
86
app/lib/widget/sharing_browser/bloc.dart
Normal file
86
app/lib/widget/sharing_browser/bloc.dart
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
part of '../sharing_browser.dart';
|
||||||
|
|
||||||
|
/// List shares to be shown in [SharingBrowser]
|
||||||
|
@npLog
|
||||||
|
class _Bloc extends Bloc<_Event, _State> {
|
||||||
|
_Bloc({
|
||||||
|
required this.account,
|
||||||
|
required this.accountPrefController,
|
||||||
|
required this.sharingsController,
|
||||||
|
}) : super(_State.init()) {
|
||||||
|
on<_Init>(_onInit);
|
||||||
|
on<_TransformItems>(_onTransformItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onInit(_Init ev, Emitter<_State> emit) async {
|
||||||
|
_log.info(ev);
|
||||||
|
try {
|
||||||
|
await _importPotentialSharedAlbum();
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.severe(
|
||||||
|
"[_onInit] Failed while _importPotentialSharedAlbum", e, stackTrace);
|
||||||
|
}
|
||||||
|
unawaited(sharingsController.reload());
|
||||||
|
return emit.forEach<SharingStreamEvent>(
|
||||||
|
sharingsController.stream,
|
||||||
|
onData: (data) => state.copyWith(
|
||||||
|
items: data.data,
|
||||||
|
isLoading: data.hasNext,
|
||||||
|
),
|
||||||
|
onError: (e, stackTrace) {
|
||||||
|
_log.severe("[_onInit] Uncaught exception", e, stackTrace);
|
||||||
|
return state.copyWith(
|
||||||
|
isLoading: false,
|
||||||
|
error: ExceptionEvent(e, stackTrace),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onTransformItems(
|
||||||
|
_TransformItems ev, Emitter<_State> emit) async {
|
||||||
|
_log.info(ev);
|
||||||
|
// group shares of the same file
|
||||||
|
final map = <String, List<SharingStreamShareData>>{};
|
||||||
|
for (final i in ev.items) {
|
||||||
|
if (i is SharingStreamShareData) {
|
||||||
|
final isSharedByMe = (i.share.uidOwner == account.userId);
|
||||||
|
final groupKey = "${i.share.path}?$isSharedByMe";
|
||||||
|
map[groupKey] ??= <SharingStreamShareData>[];
|
||||||
|
map[groupKey]!.add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final results = <_Item>[];
|
||||||
|
// sort and convert the sub-lists
|
||||||
|
for (final list in map.values) {
|
||||||
|
results.add(_Item.fromSharingStreamData(
|
||||||
|
account, list.sortedBy((e) => e.share.stime).reversed.toList()));
|
||||||
|
}
|
||||||
|
// then sort the list itself
|
||||||
|
emit(state.copyWith(
|
||||||
|
transformedItems: results.sortedBy((e) => e.sortTime).reversed.toList(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Album>> _importPotentialSharedAlbum() async {
|
||||||
|
final c = KiwiContainer().resolve<DiContainer>().copyWith(
|
||||||
|
// don't want the potential albums to be cached at this moment
|
||||||
|
fileRepo: const OrNull(FileRepo(FileWebdavDataSource())),
|
||||||
|
albumRepo: OrNull(AlbumRepo(AlbumRemoteDataSource())),
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
return await ImportPotentialSharedAlbum(c)(
|
||||||
|
account, accountPrefController.raw);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.shout(
|
||||||
|
"[_importPotentialSharedAlbum] Failed while ImportPotentialSharedAlbum",
|
||||||
|
e,
|
||||||
|
stackTrace);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
final AccountPrefController accountPrefController;
|
||||||
|
final SharingsController sharingsController;
|
||||||
|
}
|
71
app/lib/widget/sharing_browser/state_event.dart
Normal file
71
app/lib/widget/sharing_browser/state_event.dart
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
part of '../sharing_browser.dart';
|
||||||
|
|
||||||
|
@genCopyWith
|
||||||
|
@toString
|
||||||
|
class _State {
|
||||||
|
const _State({
|
||||||
|
required this.items,
|
||||||
|
required this.isLoading,
|
||||||
|
required this.transformedItems,
|
||||||
|
this.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory _State.init() => const _State(
|
||||||
|
items: [],
|
||||||
|
isLoading: true,
|
||||||
|
transformedItems: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
final List<SharingStreamData> items;
|
||||||
|
final bool isLoading;
|
||||||
|
final List<_Item> transformedItems;
|
||||||
|
|
||||||
|
final ExceptionEvent? error;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _Event {}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _Init implements _Event {
|
||||||
|
const _Init();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _TransformItems implements _Event {
|
||||||
|
const _TransformItems(this.items);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
final List<SharingStreamData> items;
|
||||||
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _ListSharingBlocShareRemoved implements _Event {
|
||||||
|
const _ListSharingBlocShareRemoved(this.shares);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
@Format(r"${$?.toReadableString()}")
|
||||||
|
final List<Share> shares;
|
||||||
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _ListSharingBlocPendingSharedAlbumMoved implements _Event {
|
||||||
|
const _ListSharingBlocPendingSharedAlbumMoved(
|
||||||
|
this.account, this.file, this.destination);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
final File file;
|
||||||
|
final String destination;
|
||||||
|
}
|
81
app/lib/widget/sharing_browser/type.dart
Normal file
81
app/lib/widget/sharing_browser/type.dart
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
part of '../sharing_browser.dart';
|
||||||
|
|
||||||
|
abstract class _Item {
|
||||||
|
static _Item fromSharingStreamData(
|
||||||
|
Account account, List<SharingStreamData> src) {
|
||||||
|
if (src.first is SharingStreamFileData) {
|
||||||
|
final casted = src.cast<SharingStreamFileData>();
|
||||||
|
return _FileShareItem(
|
||||||
|
account: account,
|
||||||
|
file: casted.first.file,
|
||||||
|
shares: casted.map((e) => e.share).toList(),
|
||||||
|
);
|
||||||
|
} else if (src.first is SharingStreamAlbumData) {
|
||||||
|
final casted = src.cast<SharingStreamAlbumData>();
|
||||||
|
return _AlbumShareItem(
|
||||||
|
account: account,
|
||||||
|
album: casted.first.album,
|
||||||
|
shares: casted.map((e) => e.share).toList(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw ArgumentError("Unknown type: ${src.runtimeType}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get name;
|
||||||
|
String? get sharedBy;
|
||||||
|
DateTime? get sharedTime;
|
||||||
|
DateTime get sortTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FileShareItem implements _Item {
|
||||||
|
const _FileShareItem({
|
||||||
|
required this.account,
|
||||||
|
required this.shares,
|
||||||
|
required this.file,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => shares.first.filename;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get sharedBy => shares.first.uidOwner == account.userId
|
||||||
|
? null
|
||||||
|
: shares.first.displaynameOwner;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTime? get sharedTime => shares.first.stime;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTime get sortTime => shares.first.stime;
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
final List<Share> shares;
|
||||||
|
final File file;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlbumShareItem implements _Item {
|
||||||
|
const _AlbumShareItem({
|
||||||
|
required this.account,
|
||||||
|
required this.shares,
|
||||||
|
required this.album,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => album.name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get sharedBy => shares.first.uidOwner == account.userId
|
||||||
|
? null
|
||||||
|
: shares.first.displaynameOwner;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTime? get sharedTime => shares.first.stime;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTime get sortTime => shares.first.stime;
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
final List<Share> shares;
|
||||||
|
final Album album;
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue