mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 08:46:18 +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/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;
|
||||
}
|
||||
|
|
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,
|
||||
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 &&
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
|
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…
Reference in a new issue