Refactor: revamp sharing browser code

This commit is contained in:
Ming Ming 2023-09-12 01:01:47 +08:00
parent 1404717574
commit b4cd90d6d4
13 changed files with 973 additions and 680 deletions

View file

@ -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;
}

View file

@ -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}";
}
}

View file

@ -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/server_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/di_container.dart';
@ -23,6 +24,8 @@ class AccountController {
_syncController = null;
_sessionController?.dispose();
_sessionController = null;
_sharingsController?.dispose();
_sharingsController = null;
}
Account get account => _account!;
@ -58,6 +61,12 @@ class AccountController {
SessionController get sessionController =>
_sessionController ??= SessionController();
SharingsController get sharingsController =>
_sharingsController ??= SharingsController(
KiwiContainer().resolve(),
account: _account!,
);
Account? _account;
CollectionsController? _collectionsController;
ServerController? _serverController;
@ -65,4 +74,5 @@ class AccountController {
PersonsController? _personsController;
SyncController? _syncController;
SessionController? _sessionController;
SharingsController? _sharingsController;
}

View 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;
}

View 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");
}

View 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;
}

View 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");
}

View file

@ -191,6 +191,7 @@ class _WrappedAppState extends State<_WrappedApp>
PeopleBrowser.routeName: PeopleBrowser.buildRoute,
EnhancementSettings.routeName: EnhancementSettings.buildRoute,
Settings.routeName: Settings.buildRoute,
SharingBrowser.routeName: SharingBrowser.buildRoute,
};
Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
@ -208,7 +209,6 @@ class _WrappedAppState extends State<_WrappedApp>
route ??= _handleTrashbinBrowserRoute(settings);
route ??= _handleTrashbinViewerRoute(settings);
route ??= _handleSlideshowViewerRoute(settings);
route ??= _handleSharingBrowserRoute(settings);
route ??= _handleSharedFileViewerRoute(settings);
route ??= _handleAlbumShareOutlierBrowserRoute(settings);
route ??= _handleShareFolderPickerRoute(settings);
@ -379,20 +379,6 @@ class _WrappedAppState extends State<_WrappedApp>
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) {
try {
if (settings.name == SharedFileViewer.routeName &&

View file

@ -1,4 +1,7 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:copy_with/copy_with.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
@ -6,7 +9,9 @@ import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.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/entity/album.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/pref.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/k.dart' as k;
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/empty_list_indicator.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:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
import 'package:np_common/or_null.dart';
import 'package:np_ui/np_ui.dart';
import 'package:to_string/to_string.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 {
SharingBrowserArguments(this.account);
@ -37,39 +53,44 @@ class SharingBrowserArguments {
}
/// Show a list of all shares associated with this account
class SharingBrowser extends StatefulWidget {
class SharingBrowser extends StatelessWidget {
static const routeName = "/sharing-browser";
static Route buildRoute(SharingBrowserArguments args) => MaterialPageRoute(
builder: (context) => SharingBrowser.fromArgs(args),
static Route buildRoute() => MaterialPageRoute(
builder: (_) => const SharingBrowser(),
);
const SharingBrowser({
Key? key,
required this.account,
}) : super(key: key);
SharingBrowser.fromArgs(SharingBrowserArguments args, {Key? key})
: this(
key: key,
account: args.account,
);
const SharingBrowser({super.key});
@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
class _SharingBrowserState extends State<SharingBrowser> {
class _WrappedSharingBrowserState extends State<_WrappedSharingBrowser>
with RouteAware, PageVisibilityMixin {
@override
initState() {
super.initState();
_importPotentialSharedAlbum().whenComplete(() {
_initBloc();
});
AccountPref.of(widget.account).run((obj) {
_bloc.add(const _Init());
AccountPref.of(_bloc.account).run((obj) {
if (obj.hasNewSharedAlbumOr()) {
obj.setNewSharedAlbum(false);
}
@ -77,67 +98,82 @@ class _SharingBrowserState extends State<SharingBrowser> {
}
@override
build(BuildContext context) {
return Scaffold(
body: BlocListener<ListSharingBloc, ListSharingBlocState>(
bloc: _bloc,
listener: (context, state) => _onStateChange(context, state),
child: BlocBuilder<ListSharingBloc, ListSharingBlocState>(
bloc: _bloc,
builder: (context, state) => _buildContent(context, state),
Widget build(BuildContext context) {
return MultiBlocListener(
listeners: [
_BlocListener(
listenWhen: (previous, current) => previous.items != current.items,
listener: (context, state) {
_bloc.add(_TransformItems(state.items));
},
),
_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() {
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();
}
late final _bloc = context.read<_Bloc>();
}
Widget _buildContent(BuildContext context, ListSharingBlocState state) {
if ((state is ListSharingBlocSuccess || state is ListSharingBlocFailure) &&
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(),
),
],
);
}
}
class _AppBar extends StatelessWidget {
const _AppBar();
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(
children: [
AppBar(
@ -153,115 +189,63 @@ class _SharingBrowserState extends State<SharingBrowser> {
],
);
}
}
Widget _buildFileItem(BuildContext context, List<ListSharingItem> shares) {
assert(shares.first is ListSharingFile);
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(),
));
},
);
}
class _ContentList extends StatelessWidget {
const _ContentList();
Widget _buildAlbumItem(BuildContext context, List<ListSharingItem> shares) {
assert(shares.first is ListSharingAlbum);
final item = shares.first as ListSharingAlbum;
return _AlbumTile(
account: widget.account,
item: item,
onTap: () => _onAlbumShareItemTap(context, item),
);
}
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),
@override
Widget build(BuildContext context) {
return _BlocBuilder(
buildWhen: (previous, current) =>
previous.transformedItems != current.transformedItems,
builder: (_, state) => SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) =>
_buildItem(context, state.transformedItems[index]),
childCount: state.transformedItems.length,
),
),
);
}
void _transformItems(List<ListSharingItem> items) {
// group shares of the same file
final map = <String, List<ListSharingItem>>{};
for (final i in items) {
final isSharedByMe = (i.share.uidOwner == widget.account.userId);
final groupKey = "${i.share.path}?$isSharedByMe";
map[groupKey] ??= <ListSharingItem>[];
map[groupKey]!.add(i);
Widget _buildItem(BuildContext context, _Item data) {
if (data is _FileShareItem) {
return _buildFileItem(context, data);
} else if (data is _AlbumShareItem) {
return _buildAlbumItem(context, data);
} else {
throw ArgumentError("Unknown item type: ${data.runtimeType}");
}
// 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() {
_bloc.add(ListSharingBlocQuery(widget.account));
Widget _buildFileItem(BuildContext context, _FileShareItem item) {
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 {
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())),
Widget _buildAlbumItem(BuildContext context, _AlbumShareItem item) {
return _AlbumTile(
account: item.account,
item: item,
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 {
@ -274,7 +258,7 @@ class _ListTile extends StatelessWidget {
});
@override
build(BuildContext context) {
Widget build(BuildContext context) {
return UnboundedListTile(
leading: leading,
title: Text(
@ -305,9 +289,9 @@ class _FileTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final dateStr = _getDateFormat(context).format(item.share.stime.toLocal());
final dateStr = _getDateFormat(context).format(item.sharedTime!.toLocal());
return _ListTile(
leading: item.share.itemType == ShareItemType.folder
leading: item.shares.first.itemType == ShareItemType.folder
? const SizedBox(
height: _leadingSize,
width: _leadingSize,
@ -320,18 +304,18 @@ class _FileTile extends StatelessWidget {
dimension: _leadingSize,
errorBuilder: (_) => const Icon(Icons.folder, size: 32),
),
label: item.share.filename,
description: item.share.uidOwner == account.userId
label: item.name,
description: item.sharedBy == null
? L10n.global().fileLastSharedDescription(dateStr)
: L10n.global().fileLastSharedByOthersDescription(
item.share.displaynameOwner, dateStr),
: L10n.global()
.fileLastSharedByOthersDescription(item.sharedBy!, dateStr),
trailing: isLinkShare ? const Icon(Icons.link) : null,
onTap: onTap,
);
}
final Account account;
final ListSharingFile item;
final _FileShareItem item;
final bool isLinkShare;
final VoidCallback? onTap;
}
@ -345,7 +329,7 @@ class _AlbumTile extends StatelessWidget {
@override
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);
return _ListTile(
leading: cover == null
@ -361,17 +345,17 @@ class _AlbumTile extends StatelessWidget {
errorBuilder: (_) => const Icon(Icons.photo_album, size: 32),
),
label: item.album.name,
description: item.share.uidOwner == account.userId
description: item.sharedBy == null
? L10n.global().fileLastSharedDescription(dateStr)
: L10n.global().albumLastSharedByOthersDescription(
item.share.displaynameOwner, dateStr),
: L10n.global()
.albumLastSharedByOthersDescription(item.sharedBy!, dateStr),
trailing: const Icon(Icons.photo_album_outlined),
onTap: onTap,
);
}
final Account account;
final ListSharingAlbum item;
final _AlbumShareItem item;
final VoidCallback? onTap;
}

View file

@ -2,13 +2,105 @@
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
// **************************************************************************
extension _$_SharingBrowserStateNpLog on _SharingBrowserState {
extension _$_WrappedSharingBrowserStateNpLog on _WrappedSharingBrowserState {
// ignore: unused_element
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}";
}
}

View 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;
}

View 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;
}

View 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;
}