mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-24 10:28:50 +01:00
Merge branch 'collection-rewrite'
This commit is contained in:
commit
23cf6cacca
317 changed files with 18124 additions and 11677 deletions
BIN
app/assets/2.0x/ic_add_collections_outlined_24dp.png
Normal file
BIN
app/assets/2.0x/ic_add_collections_outlined_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 309 B |
BIN
app/assets/2.0x/ic_nextcloud_album.png
Normal file
BIN
app/assets/2.0x/ic_nextcloud_album.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 606 B |
BIN
app/assets/3.0x/ic_add_collections_outlined_24dp.png
Normal file
BIN
app/assets/3.0x/ic_add_collections_outlined_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 362 B |
BIN
app/assets/3.0x/ic_nextcloud_album.png
Normal file
BIN
app/assets/3.0x/ic_nextcloud_album.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 872 B |
BIN
app/assets/ic_add_collections_outlined_24dp.png
Normal file
BIN
app/assets/ic_add_collections_outlined_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 286 B |
BIN
app/assets/ic_nextcloud_album.png
Normal file
BIN
app/assets/ic_nextcloud_album.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 344 B |
|
@ -8,7 +8,7 @@ import 'package:nc_photos/account.dart';
|
|||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:np_api/np_api.dart';
|
||||
import 'package:np_api/np_api.dart' hide NcAlbumItem;
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'api_util.g.dart';
|
||||
|
@ -45,6 +45,11 @@ String getFilePreviewUrlRelative(
|
|||
if (file_util.isTrash(account, file)) {
|
||||
// trashbin does not support preview.png endpoint
|
||||
url = "index.php/apps/files_trashbin/preview?fileId=${file.fdId}";
|
||||
} else if (file_util.isNcAlbumFile(account, file)) {
|
||||
// We can't use the generic file preview url because collaborative albums do
|
||||
// not create a file share for photos not belonging to you, that means you
|
||||
// can only access the file view the Photos API
|
||||
url = "apps/photos/api/v1/preview/${file.fdId}?x=$width&y=$height";
|
||||
} else {
|
||||
url = "index.php/core/preview?fileId=${file.fdId}";
|
||||
}
|
||||
|
@ -76,6 +81,16 @@ String getFilePreviewUrlByFileId(
|
|||
return url;
|
||||
}
|
||||
|
||||
/// Return the preview image URL for [fileId], using the new Photos API in
|
||||
/// Nextcloud 25
|
||||
String getPhotosApiFilePreviewUrlByFileId(
|
||||
Account account,
|
||||
int fileId, {
|
||||
required int width,
|
||||
required int height,
|
||||
}) =>
|
||||
"${account.url}/apps/photos/api/v1/preview/$fileId?x=$width&y=$height";
|
||||
|
||||
String getFileUrl(Account account, FileDescriptor file) {
|
||||
return "${account.url}/${getFileUrlRelative(file)}";
|
||||
}
|
||||
|
|
|
@ -4,7 +4,10 @@ import 'package:logging/logging.dart';
|
|||
import 'package:nc_photos/entity/face.dart';
|
||||
import 'package:nc_photos/entity/favorite.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/nc_album.dart';
|
||||
import 'package:nc_photos/entity/nc_album_item.dart';
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/entity/server_status.dart';
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
import 'package:nc_photos/entity/sharee.dart';
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
|
@ -34,7 +37,6 @@ class ApiFavoriteConverter {
|
|||
}
|
||||
}
|
||||
|
||||
@npLog
|
||||
class ApiFileConverter {
|
||||
static File fromApi(api.File file) {
|
||||
final metadata = file.customProperties?["com.nkming.nc_photos:metadata"]
|
||||
|
@ -78,20 +80,45 @@ class ApiFileConverter {
|
|||
?.run((obj) => ImageLocation.fromJson(jsonDecode(obj))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static String _hrefToPath(String href) {
|
||||
final rawPath = href.trimLeftAny("/");
|
||||
final pos = rawPath.indexOf("remote.php");
|
||||
if (pos == -1) {
|
||||
// what?
|
||||
_log.warning("[_hrefToPath] Unknown href value: $rawPath");
|
||||
return rawPath;
|
||||
} else {
|
||||
return rawPath.substring(pos);
|
||||
}
|
||||
class ApiNcAlbumConverter {
|
||||
static NcAlbum fromApi(api.NcAlbum album) {
|
||||
return NcAlbum(
|
||||
path: _hrefToPath(album.href),
|
||||
lastPhoto: (album.lastPhoto ?? -1) < 0 ? null : album.lastPhoto,
|
||||
nbItems: album.nbItems ?? 0,
|
||||
location: album.location,
|
||||
dateStart: (album.dateRange?["start"] as int?)
|
||||
?.run((d) => DateTime.fromMillisecondsSinceEpoch(d * 1000)),
|
||||
dateEnd: (album.dateRange?["end"] as int?)
|
||||
?.run((d) => DateTime.fromMillisecondsSinceEpoch(d * 1000)),
|
||||
collaborators: album.collaborators
|
||||
.map((c) => NcAlbumCollaborator(
|
||||
id: c.id.toCi(),
|
||||
label: c.label,
|
||||
type: c.type,
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static final _log = _$ApiFileConverterNpLog.log;
|
||||
class ApiNcAlbumItemConverter {
|
||||
static NcAlbumItem fromApi(api.NcAlbumItem item) {
|
||||
return NcAlbumItem(
|
||||
path: _hrefToPath(item.href),
|
||||
fileId: item.fileId!,
|
||||
contentLength: item.contentLength,
|
||||
contentType: item.contentType,
|
||||
etag: item.etag,
|
||||
lastModified: item.lastModified,
|
||||
hasPreview: item.hasPreview,
|
||||
isFavorite: item.favorite,
|
||||
fileMetadataWidth: item.fileMetadataSize?["width"],
|
||||
fileMetadataHeight: item.fileMetadataSize?["height"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ApiPersonConverter {
|
||||
|
@ -153,6 +180,16 @@ class ApiShareeConverter {
|
|||
};
|
||||
}
|
||||
|
||||
class ApiStatusConverter {
|
||||
static ServerStatus fromApi(api.Status status) {
|
||||
return ServerStatus(
|
||||
versionRaw: status.version,
|
||||
versionName: status.versionString,
|
||||
productName: status.productName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ApiTagConverter {
|
||||
static Tag fromApi(api.Tag tag) {
|
||||
return Tag(
|
||||
|
@ -171,3 +208,19 @@ class ApiTaggedFileConverter {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _hrefToPath(String href) {
|
||||
final rawPath = href.trimLeftAny("/");
|
||||
final pos = rawPath.indexOf("remote.php");
|
||||
if (pos == -1) {
|
||||
// what?
|
||||
_$_NpLog.log.warning("[_hrefToPath] Unknown href value: $rawPath");
|
||||
return rawPath;
|
||||
} else {
|
||||
return rawPath.substring(pos);
|
||||
}
|
||||
}
|
||||
|
||||
@npLog
|
||||
// ignore: camel_case_types
|
||||
class _ {}
|
||||
|
|
|
@ -6,9 +6,9 @@ part of 'entity_converter.dart';
|
|||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ApiFileConverterNpLog on ApiFileConverter {
|
||||
extension _$_NpLog on _ {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("api.entity_converter.ApiFileConverter");
|
||||
static final log = Logger("api.entity_converter._");
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import 'package:nc_photos/debug_util.dart';
|
|||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/data_source.dart';
|
||||
import 'package:nc_photos/entity/album/data_source2.dart';
|
||||
import 'package:nc_photos/entity/album/repo2.dart';
|
||||
import 'package:nc_photos/entity/face.dart';
|
||||
import 'package:nc_photos/entity/face/data_source.dart';
|
||||
import 'package:nc_photos/entity/favorite.dart';
|
||||
|
@ -16,6 +18,8 @@ import 'package:nc_photos/entity/file.dart';
|
|||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/entity/local_file.dart';
|
||||
import 'package:nc_photos/entity/local_file/data_source.dart';
|
||||
import 'package:nc_photos/entity/nc_album/data_source.dart';
|
||||
import 'package:nc_photos/entity/nc_album/repo.dart';
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/entity/person/data_source.dart';
|
||||
import 'package:nc_photos/entity/search.dart';
|
||||
|
@ -198,7 +202,12 @@ Future<void> _initDiContainer(InitIsolateType isolateType) async {
|
|||
c.sqliteDb = await _createDb(isolateType);
|
||||
|
||||
c.albumRepo = AlbumRepo(AlbumCachedDataSource(c));
|
||||
c.albumRepoRemote = AlbumRepo(AlbumRemoteDataSource());
|
||||
c.albumRepoLocal = AlbumRepo(AlbumSqliteDbDataSource(c));
|
||||
c.albumRepo2 = CachedAlbumRepo2(
|
||||
const AlbumRemoteDataSource2(), AlbumSqliteDbDataSource2(c.sqliteDb));
|
||||
c.albumRepo2Remote = const BasicAlbumRepo2(AlbumRemoteDataSource2());
|
||||
c.albumRepo2Local = BasicAlbumRepo2(AlbumSqliteDbDataSource2(c.sqliteDb));
|
||||
c.faceRepo = const FaceRepo(FaceRemoteDataSource());
|
||||
c.fileRepo = FileRepo(FileCachedDataSource(c));
|
||||
c.fileRepoRemote = const FileRepo(FileWebdavDataSource());
|
||||
|
@ -214,6 +223,11 @@ Future<void> _initDiContainer(InitIsolateType isolateType) async {
|
|||
c.tagRepoLocal = TagRepo(TagSqliteDbDataSource(c.sqliteDb));
|
||||
c.taggedFileRepo = const TaggedFileRepo(TaggedFileRemoteDataSource());
|
||||
c.searchRepo = SearchRepo(SearchSqliteDbDataSource(c));
|
||||
c.ncAlbumRepo = CachedNcAlbumRepo(
|
||||
const NcAlbumRemoteDataSource(), NcAlbumSqliteDbDataSource(c.sqliteDb));
|
||||
c.ncAlbumRepoRemote = const BasicNcAlbumRepo(NcAlbumRemoteDataSource());
|
||||
c.ncAlbumRepoLocal = BasicNcAlbumRepo(NcAlbumSqliteDbDataSource(c.sqliteDb));
|
||||
|
||||
c.touchManager = TouchManager(c);
|
||||
|
||||
if (platform_k.isAndroid) {
|
||||
|
|
2
app/lib/asset.dart
Normal file
2
app/lib/asset.dart
Normal file
|
@ -0,0 +1,2 @@
|
|||
const icAddCollectionsOutlined24 =
|
||||
"assets/ic_add_collections_outlined_24dp.png";
|
|
@ -4,12 +4,14 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/controller/collections_controller.dart';
|
||||
import 'package:nc_photos/controller/server_controller.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/use_case/list_album.dart';
|
||||
import 'package:nc_photos/use_case/collection/list_collection.dart';
|
||||
import 'package:nc_photos/use_case/list_location_group.dart';
|
||||
import 'package:nc_photos/use_case/list_person.dart';
|
||||
import 'package:nc_photos/use_case/list_tag.dart';
|
||||
|
@ -24,13 +26,13 @@ part 'home_search_suggestion.g.dart';
|
|||
abstract class HomeSearchResult {}
|
||||
|
||||
@toString
|
||||
class HomeSearchAlbumResult implements HomeSearchResult {
|
||||
const HomeSearchAlbumResult(this.album);
|
||||
class HomeSearchCollectionResult implements HomeSearchResult {
|
||||
const HomeSearchCollectionResult(this.collection);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Album album;
|
||||
final Collection collection;
|
||||
}
|
||||
|
||||
@toString
|
||||
|
@ -125,7 +127,8 @@ class HomeSearchSuggestionBlocFailure extends HomeSearchSuggestionBlocState {
|
|||
@npLog
|
||||
class HomeSearchSuggestionBloc
|
||||
extends Bloc<HomeSearchSuggestionBlocEvent, HomeSearchSuggestionBlocState> {
|
||||
HomeSearchSuggestionBloc(this.account)
|
||||
HomeSearchSuggestionBloc(
|
||||
this.account, this.collectionsController, this.serverController)
|
||||
: super(const HomeSearchSuggestionBlocInit()) {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
assert(require(c));
|
||||
|
@ -187,13 +190,21 @@ class HomeSearchSuggestionBloc
|
|||
Emitter<HomeSearchSuggestionBlocState> emit) async {
|
||||
final product = <_Searcheable>[];
|
||||
try {
|
||||
final albums = await ListAlbum(_c)(account)
|
||||
.where((event) => event is Album)
|
||||
var collections = collectionsController
|
||||
.peekStream()
|
||||
.data
|
||||
.map((e) => e.collection)
|
||||
.toList();
|
||||
product.addAll(albums.map((a) => _AlbumSearcheable(a)));
|
||||
_log.info("[_onEventPreloadData] Loaded ${albums.length} albums");
|
||||
if (collections.isEmpty) {
|
||||
collections = await ListCollection(_c,
|
||||
serverController: serverController)(account)
|
||||
.last;
|
||||
}
|
||||
product.addAll(collections.map(_CollectionSearcheable.new));
|
||||
_log.info(
|
||||
"[_onEventPreloadData] Loaded ${collections.length} collections");
|
||||
} catch (e) {
|
||||
_log.warning("[_onEventPreloadData] Failed while ListAlbum", e);
|
||||
_log.warning("[_onEventPreloadData] Failed while ListCollection", e);
|
||||
}
|
||||
try {
|
||||
final tags = await ListTag(_c)(account);
|
||||
|
@ -239,6 +250,8 @@ class HomeSearchSuggestionBloc
|
|||
}
|
||||
|
||||
final Account account;
|
||||
final CollectionsController collectionsController;
|
||||
final ServerController serverController;
|
||||
late final DiContainer _c;
|
||||
|
||||
final _search = Woozy<_Searcheable>(limit: 10);
|
||||
|
@ -249,16 +262,16 @@ abstract class _Searcheable {
|
|||
HomeSearchResult toResult();
|
||||
}
|
||||
|
||||
class _AlbumSearcheable implements _Searcheable {
|
||||
const _AlbumSearcheable(this.album);
|
||||
class _CollectionSearcheable implements _Searcheable {
|
||||
const _CollectionSearcheable(this.collection);
|
||||
|
||||
@override
|
||||
toKeywords() => [album.name.toCi()];
|
||||
toKeywords() => [collection.name.toCi()];
|
||||
|
||||
@override
|
||||
toResult() => HomeSearchAlbumResult(album);
|
||||
toResult() => HomeSearchCollectionResult(collection);
|
||||
|
||||
final Album album;
|
||||
final Collection collection;
|
||||
}
|
||||
|
||||
class _TagSearcheable implements _Searcheable {
|
||||
|
|
|
@ -18,10 +18,10 @@ extension _$HomeSearchSuggestionBlocNpLog on HomeSearchSuggestionBloc {
|
|||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$HomeSearchAlbumResultToString on HomeSearchAlbumResult {
|
||||
extension _$HomeSearchCollectionResultToString on HomeSearchCollectionResult {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "HomeSearchAlbumResult {album: $album}";
|
||||
return "HomeSearchCollectionResult {collection: $collection}";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,338 +0,0 @@
|
|||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.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_util.dart' as file_util;
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/exception_event.dart';
|
||||
import 'package:nc_photos/or_null.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/list_album.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'list_album.g.dart';
|
||||
|
||||
class ListAlbumBlocItem {
|
||||
ListAlbumBlocItem(this.album);
|
||||
|
||||
final Album album;
|
||||
}
|
||||
|
||||
abstract class ListAlbumBlocEvent {
|
||||
const ListAlbumBlocEvent();
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListAlbumBlocQuery extends ListAlbumBlocEvent {
|
||||
const ListAlbumBlocQuery(this.account);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account account;
|
||||
}
|
||||
|
||||
/// An external event has happened and may affect the state of this bloc
|
||||
@toString
|
||||
class _ListAlbumBlocExternalEvent extends ListAlbumBlocEvent {
|
||||
const _ListAlbumBlocExternalEvent();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
@toString
|
||||
abstract class ListAlbumBlocState {
|
||||
const ListAlbumBlocState(this.account, this.items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account? account;
|
||||
final List<ListAlbumBlocItem> items;
|
||||
}
|
||||
|
||||
class ListAlbumBlocInit extends ListAlbumBlocState {
|
||||
const ListAlbumBlocInit() : super(null, const []);
|
||||
}
|
||||
|
||||
class ListAlbumBlocLoading extends ListAlbumBlocState {
|
||||
const ListAlbumBlocLoading(Account? account, List<ListAlbumBlocItem> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
class ListAlbumBlocSuccess extends ListAlbumBlocState {
|
||||
const ListAlbumBlocSuccess(Account? account, List<ListAlbumBlocItem> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListAlbumBlocFailure extends ListAlbumBlocState {
|
||||
const ListAlbumBlocFailure(
|
||||
Account? account, List<ListAlbumBlocItem> items, this.exception)
|
||||
: super(account, items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final dynamic exception;
|
||||
}
|
||||
|
||||
/// The state of this bloc is inconsistent. This typically means that the data
|
||||
/// may have been changed externally
|
||||
class ListAlbumBlocInconsistent extends ListAlbumBlocState {
|
||||
const ListAlbumBlocInconsistent(
|
||||
Account? account, List<ListAlbumBlocItem> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
@npLog
|
||||
class ListAlbumBloc extends Bloc<ListAlbumBlocEvent, ListAlbumBlocState> {
|
||||
/// Constructor
|
||||
///
|
||||
/// If [offlineC] is not null, this [DiContainer] will be used when requesting
|
||||
/// offline contents, otherwise [_c] will be used
|
||||
ListAlbumBloc(
|
||||
this._c, [
|
||||
DiContainer? offlineC,
|
||||
]) : _offlineC = offlineC ?? _c,
|
||||
assert(require(_c)),
|
||||
assert(offlineC == null || require(offlineC)),
|
||||
assert(ListAlbum.require(_c)),
|
||||
assert(offlineC == null || ListAlbum.require(offlineC)),
|
||||
super(const ListAlbumBlocInit()) {
|
||||
_albumUpdatedListener =
|
||||
AppEventListener<AlbumUpdatedEvent>(_onAlbumUpdatedEvent);
|
||||
_fileRemovedListener =
|
||||
AppEventListener<FileRemovedEvent>(_onFileRemovedEvent);
|
||||
_albumCreatedListener =
|
||||
AppEventListener<AlbumCreatedEvent>(_onAlbumCreatedEvent);
|
||||
_albumUpdatedListener.begin();
|
||||
_fileRemovedListener.begin();
|
||||
_albumCreatedListener.begin();
|
||||
_fileMovedListener.begin();
|
||||
_shareCreatedListener.begin();
|
||||
_shareRemovedListener.begin();
|
||||
|
||||
_refreshThrottler = Throttler(
|
||||
onTriggered: (_) {
|
||||
add(const _ListAlbumBlocExternalEvent());
|
||||
},
|
||||
logTag: "ListAlbumBloc.refresh",
|
||||
);
|
||||
|
||||
on<ListAlbumBlocEvent>(_onEvent);
|
||||
}
|
||||
|
||||
static bool require(DiContainer c) => true;
|
||||
|
||||
static ListAlbumBloc of(Account account) {
|
||||
final name = bloc_util.getInstNameForAccount("ListAlbumBloc", account);
|
||||
try {
|
||||
_log.fine("[of] Resolving bloc for '$name'");
|
||||
return KiwiContainer().resolve<ListAlbumBloc>(name);
|
||||
} catch (_) {
|
||||
// no created instance for this account, make a new one
|
||||
_log.info("[of] New bloc instance for account: $account");
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final offlineC = c.copyWith(
|
||||
fileRepo: OrNull(c.fileRepoLocal),
|
||||
albumRepo: OrNull(c.albumRepoLocal),
|
||||
);
|
||||
final bloc = ListAlbumBloc(c, offlineC);
|
||||
KiwiContainer().registerInstance<ListAlbumBloc>(bloc, name: name);
|
||||
return bloc;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
close() {
|
||||
_albumUpdatedListener.end();
|
||||
_fileRemovedListener.end();
|
||||
_albumCreatedListener.end();
|
||||
_fileMovedListener.end();
|
||||
_shareCreatedListener.end();
|
||||
_shareRemovedListener.end();
|
||||
_refreshThrottler.clear();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _onEvent(
|
||||
ListAlbumBlocEvent event, Emitter<ListAlbumBlocState> emit) async {
|
||||
_log.info("[_onEvent] $event");
|
||||
if (event is ListAlbumBlocQuery) {
|
||||
await _onEventQuery(event, emit);
|
||||
} else if (event is _ListAlbumBlocExternalEvent) {
|
||||
await _onExternalEvent(event, emit);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onEventQuery(
|
||||
ListAlbumBlocQuery ev, Emitter<ListAlbumBlocState> emit) async {
|
||||
emit(ListAlbumBlocLoading(ev.account, state.items));
|
||||
bool hasContent = state.items.isNotEmpty;
|
||||
|
||||
if (!hasContent) {
|
||||
// show something instantly on first load
|
||||
final cacheState = await _queryOffline(ev);
|
||||
emit(ListAlbumBlocLoading(ev.account, cacheState.items));
|
||||
hasContent = cacheState.items.isNotEmpty;
|
||||
}
|
||||
|
||||
final newState = await _queryOnline(ev);
|
||||
if (newState is ListAlbumBlocFailure) {
|
||||
emit(ListAlbumBlocFailure(
|
||||
ev.account,
|
||||
newState.items.isNotEmpty ? newState.items : state.items,
|
||||
newState.exception));
|
||||
} else {
|
||||
emit(newState);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onExternalEvent(
|
||||
_ListAlbumBlocExternalEvent ev, Emitter<ListAlbumBlocState> emit) async {
|
||||
emit(ListAlbumBlocInconsistent(state.account, state.items));
|
||||
}
|
||||
|
||||
void _onAlbumUpdatedEvent(AlbumUpdatedEvent ev) {
|
||||
if (state is ListAlbumBlocInit) {
|
||||
// no data in this bloc, ignore
|
||||
return;
|
||||
}
|
||||
if (_isAccountOfInterest(ev.account)) {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onFileRemovedEvent(FileRemovedEvent ev) {
|
||||
if (state is ListAlbumBlocInit) {
|
||||
// no data in this bloc, ignore
|
||||
return;
|
||||
}
|
||||
if (_isAccountOfInterest(ev.account) &&
|
||||
file_util.isAlbumFile(ev.account, ev.file)) {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onFileMovedEvent(FileMovedEvent ev) {
|
||||
if (state is ListAlbumBlocInit) {
|
||||
// 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.getRemoteAlbumsDir(ev.account))) {
|
||||
// moving from/to album dir
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onAlbumCreatedEvent(AlbumCreatedEvent ev) {
|
||||
if (state is ListAlbumBlocInit) {
|
||||
// no data in this bloc, ignore
|
||||
return;
|
||||
}
|
||||
if (_isAccountOfInterest(ev.account)) {
|
||||
add(const _ListAlbumBlocExternalEvent());
|
||||
}
|
||||
}
|
||||
|
||||
void _onShareCreatedEvent(ShareCreatedEvent ev) =>
|
||||
_onShareChanged(ev.account, ev.share);
|
||||
|
||||
void _onShareRemovedEvent(ShareRemovedEvent ev) =>
|
||||
_onShareChanged(ev.account, ev.share);
|
||||
|
||||
void _onShareChanged(Account account, Share share) {
|
||||
if (_isAccountOfInterest(account)) {
|
||||
final webdavPath = file_util.unstripPath(account, share.path);
|
||||
if (webdavPath
|
||||
.startsWith(remote_storage_util.getRemoteAlbumsDir(account))) {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<ListAlbumBlocState> _queryOffline(ListAlbumBlocQuery ev) =>
|
||||
_queryWithAlbumDataSource(_offlineC, ev);
|
||||
|
||||
Future<ListAlbumBlocState> _queryOnline(ListAlbumBlocQuery ev) =>
|
||||
_queryWithAlbumDataSource(_c, ev);
|
||||
|
||||
Future<ListAlbumBlocState> _queryWithAlbumDataSource(
|
||||
DiContainer c, ListAlbumBlocQuery ev) async {
|
||||
try {
|
||||
final albums = <Album>[];
|
||||
final errors = <dynamic>[];
|
||||
await for (final result in ListAlbum(c)(ev.account)) {
|
||||
if (result is ExceptionEvent) {
|
||||
if (result.error is CacheNotFoundException) {
|
||||
_log.info(
|
||||
"[_queryWithAlbumDataSource] Cache not found", result.error);
|
||||
} else {
|
||||
_log.shout("[_queryWithAlbumDataSource] Exception while ListAlbum",
|
||||
result.error, result.stackTrace);
|
||||
}
|
||||
errors.add(result.error);
|
||||
} else if (result is Album) {
|
||||
albums.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
final items = albums.map((e) => ListAlbumBlocItem(e)).toList();
|
||||
if (errors.isEmpty) {
|
||||
return ListAlbumBlocSuccess(ev.account, items);
|
||||
} else {
|
||||
return ListAlbumBlocFailure(ev.account, items, errors.first);
|
||||
}
|
||||
} catch (e, stacktrace) {
|
||||
_log.severe("[_queryWithAlbumDataSource] Exception", e, stacktrace);
|
||||
return ListAlbumBlocFailure(ev.account, [], e);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isAccountOfInterest(Account account) =>
|
||||
state.account == null || state.account!.compareServerIdentity(account);
|
||||
|
||||
final DiContainer _c;
|
||||
final DiContainer _offlineC;
|
||||
|
||||
late AppEventListener<AlbumUpdatedEvent> _albumUpdatedListener;
|
||||
late AppEventListener<FileRemovedEvent> _fileRemovedListener;
|
||||
late AppEventListener<AlbumCreatedEvent> _albumCreatedListener;
|
||||
late final _fileMovedListener =
|
||||
AppEventListener<FileMovedEvent>(_onFileMovedEvent);
|
||||
late final _shareCreatedListener =
|
||||
AppEventListener<ShareCreatedEvent>(_onShareCreatedEvent);
|
||||
late final _shareRemovedListener =
|
||||
AppEventListener<ShareRemovedEvent>(_onShareRemovedEvent);
|
||||
|
||||
late Throttler _refreshThrottler;
|
||||
|
||||
static final _log = _$ListAlbumBlocNpLog.log;
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'list_album.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListAlbumBlocNpLog on ListAlbumBloc {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("bloc.list_album.ListAlbumBloc");
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListAlbumBlocQueryToString on ListAlbumBlocQuery {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListAlbumBlocQuery {account: $account}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_ListAlbumBlocExternalEventToString on _ListAlbumBlocExternalEvent {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_ListAlbumBlocExternalEvent {}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListAlbumBlocStateToString on ListAlbumBlocState {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "${objectRuntimeType(this, "ListAlbumBlocState")} {account: $account, items: [length: ${items.length}]}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListAlbumBlocFailureToString on ListAlbumBlocFailure {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListAlbumBlocFailure {account: $account, items: [length: ${items.length}], exception: $exception}";
|
||||
}
|
||||
}
|
|
@ -1,222 +0,0 @@
|
|||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/throttler.dart';
|
||||
import 'package:nc_photos/use_case/populate_person.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'list_face_file.g.dart';
|
||||
|
||||
abstract class ListFaceFileBlocEvent {
|
||||
const ListFaceFileBlocEvent();
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListFaceFileBlocQuery extends ListFaceFileBlocEvent {
|
||||
const ListFaceFileBlocQuery(this.account, this.person);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account account;
|
||||
final Person person;
|
||||
}
|
||||
|
||||
/// An external event has happened and may affect the state of this bloc
|
||||
@toString
|
||||
class _ListFaceFileBlocExternalEvent extends ListFaceFileBlocEvent {
|
||||
const _ListFaceFileBlocExternalEvent();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
@toString
|
||||
abstract class ListFaceFileBlocState {
|
||||
const ListFaceFileBlocState(this.account, this.items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account? account;
|
||||
final List<File> items;
|
||||
}
|
||||
|
||||
class ListFaceFileBlocInit extends ListFaceFileBlocState {
|
||||
ListFaceFileBlocInit() : super(null, const []);
|
||||
}
|
||||
|
||||
class ListFaceFileBlocLoading extends ListFaceFileBlocState {
|
||||
const ListFaceFileBlocLoading(Account? account, List<File> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
class ListFaceFileBlocSuccess extends ListFaceFileBlocState {
|
||||
const ListFaceFileBlocSuccess(Account? account, List<File> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListFaceFileBlocFailure extends ListFaceFileBlocState {
|
||||
const ListFaceFileBlocFailure(
|
||||
Account? account, List<File> items, this.exception)
|
||||
: super(account, items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Object exception;
|
||||
}
|
||||
|
||||
/// The state of this bloc is inconsistent. This typically means that the data
|
||||
/// may have been changed externally
|
||||
class ListFaceFileBlocInconsistent extends ListFaceFileBlocState {
|
||||
const ListFaceFileBlocInconsistent(Account? account, List<File> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
/// List all people recognized in an account
|
||||
@npLog
|
||||
class ListFaceFileBloc
|
||||
extends Bloc<ListFaceFileBlocEvent, ListFaceFileBlocState> {
|
||||
ListFaceFileBloc(this._c)
|
||||
: assert(require(_c)),
|
||||
assert(PopulatePerson.require(_c)),
|
||||
super(ListFaceFileBlocInit()) {
|
||||
_fileRemovedEventListener.begin();
|
||||
_filePropertyUpdatedEventListener.begin();
|
||||
|
||||
on<ListFaceFileBlocEvent>(_onEvent);
|
||||
}
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.faceRepo);
|
||||
|
||||
@override
|
||||
close() {
|
||||
_fileRemovedEventListener.end();
|
||||
_filePropertyUpdatedEventListener.end();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _onEvent(
|
||||
ListFaceFileBlocEvent event, Emitter<ListFaceFileBlocState> emit) async {
|
||||
_log.info("[_onEvent] $event");
|
||||
if (event is ListFaceFileBlocQuery) {
|
||||
await _onEventQuery(event, emit);
|
||||
} else if (event is _ListFaceFileBlocExternalEvent) {
|
||||
await _onExternalEvent(event, emit);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onEventQuery(
|
||||
ListFaceFileBlocQuery ev, Emitter<ListFaceFileBlocState> emit) async {
|
||||
try {
|
||||
emit(ListFaceFileBlocLoading(ev.account, state.items));
|
||||
emit(ListFaceFileBlocSuccess(ev.account, await _query(ev)));
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[_onEventQuery] Exception while request", e, stackTrace);
|
||||
emit(ListFaceFileBlocFailure(ev.account, state.items, e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onExternalEvent(_ListFaceFileBlocExternalEvent ev,
|
||||
Emitter<ListFaceFileBlocState> emit) async {
|
||||
emit(ListFaceFileBlocInconsistent(state.account, state.items));
|
||||
}
|
||||
|
||||
void _onFileRemovedEvent(FileRemovedEvent ev) {
|
||||
if (state is ListFaceFileBlocInit) {
|
||||
// no data in this bloc, ignore
|
||||
return;
|
||||
}
|
||||
if (_isFileOfInterest(ev.file)) {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onFilePropertyUpdatedEvent(FilePropertyUpdatedEvent ev) {
|
||||
if (!ev.hasAnyProperties([
|
||||
FilePropertyUpdatedEvent.propMetadata,
|
||||
FilePropertyUpdatedEvent.propIsArchived,
|
||||
FilePropertyUpdatedEvent.propOverrideDateTime,
|
||||
FilePropertyUpdatedEvent.propFavorite,
|
||||
])) {
|
||||
// not interested
|
||||
return;
|
||||
}
|
||||
if (state is ListFaceFileBlocInit) {
|
||||
// no data in this bloc, ignore
|
||||
return;
|
||||
}
|
||||
if (!_isFileOfInterest(ev.file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.hasAnyProperties([
|
||||
FilePropertyUpdatedEvent.propIsArchived,
|
||||
FilePropertyUpdatedEvent.propOverrideDateTime,
|
||||
FilePropertyUpdatedEvent.propFavorite,
|
||||
])) {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
} else {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 10),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<File>> _query(ListFaceFileBlocQuery ev) async {
|
||||
final faces = await _c.faceRepo.list(ev.account, ev.person);
|
||||
final files = await PopulatePerson(_c)(ev.account, faces);
|
||||
final rootDirs = ev.account.roots
|
||||
.map((e) => File(path: file_util.unstripPath(ev.account, e)))
|
||||
.toList();
|
||||
return files
|
||||
.where((f) =>
|
||||
file_util.isSupportedFormat(f) &&
|
||||
rootDirs.any((dir) => file_util.isUnderDir(f, dir)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
bool _isFileOfInterest(File file) {
|
||||
if (!file_util.isSupportedFormat(file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (final r in state.account?.roots ?? []) {
|
||||
final dir = File(path: file_util.unstripPath(state.account!, r));
|
||||
if (file_util.isUnderDir(file, dir)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
|
||||
late final _fileRemovedEventListener =
|
||||
AppEventListener<FileRemovedEvent>(_onFileRemovedEvent);
|
||||
late final _filePropertyUpdatedEventListener =
|
||||
AppEventListener<FilePropertyUpdatedEvent>(_onFilePropertyUpdatedEvent);
|
||||
|
||||
late final _refreshThrottler = Throttler(
|
||||
onTriggered: (_) {
|
||||
add(const _ListFaceFileBlocExternalEvent());
|
||||
},
|
||||
logTag: "ListFaceFileBloc.refresh",
|
||||
);
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'list_face_file.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListFaceFileBlocNpLog on ListFaceFileBloc {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("bloc.list_face_file.ListFaceFileBloc");
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListFaceFileBlocQueryToString on ListFaceFileBlocQuery {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListFaceFileBlocQuery {account: $account, person: $person}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_ListFaceFileBlocExternalEventToString
|
||||
on _ListFaceFileBlocExternalEvent {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_ListFaceFileBlocExternalEvent {}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListFaceFileBlocStateToString on ListFaceFileBlocState {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "${objectRuntimeType(this, "ListFaceFileBlocState")} {account: $account, items: [length: ${items.length}]}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListFaceFileBlocFailureToString on ListFaceFileBlocFailure {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListFaceFileBlocFailure {account: $account, items: [length: ${items.length}], exception: $exception}";
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ import 'package:nc_photos/entity/file.dart';
|
|||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:nc_photos/use_case/list_album.dart';
|
||||
import 'package:nc_photos/use_case/album/list_album.dart';
|
||||
import 'package:nc_photos/use_case/ls.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:logging/logging.dart';
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.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/event/event.dart';
|
||||
import 'package:nc_photos/throttler.dart';
|
||||
|
@ -147,7 +148,7 @@ class ListLocationBloc
|
|||
Future<LocationGroupResult> _query(ListLocationBlocQuery ev) =>
|
||||
ListLocationGroup(_c.withLocalRepo())(ev.account);
|
||||
|
||||
bool _isFileOfInterest(File file) {
|
||||
bool _isFileOfInterest(FileDescriptor file) {
|
||||
if (!file_util.isSupportedFormat(file)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,181 +0,0 @@
|
|||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/throttler.dart';
|
||||
import 'package:nc_photos/use_case/list_location_file.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'list_location_file.g.dart';
|
||||
|
||||
abstract class ListLocationFileBlocEvent {
|
||||
const ListLocationFileBlocEvent();
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListLocationFileBlocQuery extends ListLocationFileBlocEvent {
|
||||
const ListLocationFileBlocQuery(this.account, this.place, this.countryCode);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account account;
|
||||
final String? place;
|
||||
final String countryCode;
|
||||
}
|
||||
|
||||
/// An external event has happened and may affect the state of this bloc
|
||||
@toString
|
||||
class _ListLocationFileBlocExternalEvent extends ListLocationFileBlocEvent {
|
||||
const _ListLocationFileBlocExternalEvent();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
@toString
|
||||
abstract class ListLocationFileBlocState {
|
||||
const ListLocationFileBlocState(this.account, this.items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account? account;
|
||||
final List<File> items;
|
||||
}
|
||||
|
||||
class ListLocationFileBlocInit extends ListLocationFileBlocState {
|
||||
ListLocationFileBlocInit() : super(null, const []);
|
||||
}
|
||||
|
||||
class ListLocationFileBlocLoading extends ListLocationFileBlocState {
|
||||
const ListLocationFileBlocLoading(Account? account, List<File> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
class ListLocationFileBlocSuccess extends ListLocationFileBlocState {
|
||||
const ListLocationFileBlocSuccess(Account? account, List<File> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListLocationFileBlocFailure extends ListLocationFileBlocState {
|
||||
const ListLocationFileBlocFailure(
|
||||
Account? account, List<File> items, this.exception)
|
||||
: super(account, items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Object exception;
|
||||
}
|
||||
|
||||
/// The state of this bloc is inconsistent. This typically means that the data
|
||||
/// may have been changed externally
|
||||
class ListLocationFileBlocInconsistent extends ListLocationFileBlocState {
|
||||
const ListLocationFileBlocInconsistent(Account? account, List<File> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
/// List all files associated with a specific tag
|
||||
@npLog
|
||||
class ListLocationFileBloc
|
||||
extends Bloc<ListLocationFileBlocEvent, ListLocationFileBlocState> {
|
||||
ListLocationFileBloc(this._c)
|
||||
: assert(require(_c)),
|
||||
assert(ListLocationFile.require(_c)),
|
||||
super(ListLocationFileBlocInit()) {
|
||||
_fileRemovedEventListener.begin();
|
||||
|
||||
on<ListLocationFileBlocEvent>(_onEvent);
|
||||
}
|
||||
|
||||
static bool require(DiContainer c) =>
|
||||
DiContainer.has(c, DiType.taggedFileRepo);
|
||||
|
||||
@override
|
||||
close() {
|
||||
_fileRemovedEventListener.end();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _onEvent(ListLocationFileBlocEvent event,
|
||||
Emitter<ListLocationFileBlocState> emit) async {
|
||||
_log.info("[_onEvent] $event");
|
||||
if (event is ListLocationFileBlocQuery) {
|
||||
await _onEventQuery(event, emit);
|
||||
} else if (event is _ListLocationFileBlocExternalEvent) {
|
||||
await _onExternalEvent(event, emit);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onEventQuery(ListLocationFileBlocQuery ev,
|
||||
Emitter<ListLocationFileBlocState> emit) async {
|
||||
try {
|
||||
emit(ListLocationFileBlocLoading(ev.account, state.items));
|
||||
emit(ListLocationFileBlocSuccess(ev.account, await _query(ev)));
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[_onEventQuery] Exception while request", e, stackTrace);
|
||||
emit(ListLocationFileBlocFailure(ev.account, state.items, e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onExternalEvent(_ListLocationFileBlocExternalEvent ev,
|
||||
Emitter<ListLocationFileBlocState> emit) async {
|
||||
emit(ListLocationFileBlocInconsistent(state.account, state.items));
|
||||
}
|
||||
|
||||
void _onFileRemovedEvent(FileRemovedEvent ev) {
|
||||
if (state is ListLocationFileBlocInit) {
|
||||
// no data in this bloc, ignore
|
||||
return;
|
||||
}
|
||||
if (_isFileOfInterest(ev.file)) {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<File>> _query(ListLocationFileBlocQuery ev) async {
|
||||
final files = <File>[];
|
||||
for (final r in ev.account.roots) {
|
||||
final dir = File(path: file_util.unstripPath(ev.account, r));
|
||||
files.addAll(await ListLocationFile(_c)(
|
||||
ev.account, dir, ev.place, ev.countryCode));
|
||||
}
|
||||
return files.where((f) => file_util.isSupportedFormat(f)).toList();
|
||||
}
|
||||
|
||||
bool _isFileOfInterest(File file) {
|
||||
if (!file_util.isSupportedFormat(file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (final r in state.account?.roots ?? []) {
|
||||
final dir = File(path: file_util.unstripPath(state.account!, r));
|
||||
if (file_util.isUnderDir(file, dir)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
|
||||
late final _fileRemovedEventListener =
|
||||
AppEventListener<FileRemovedEvent>(_onFileRemovedEvent);
|
||||
|
||||
late final _refreshThrottler = Throttler(
|
||||
onTriggered: (_) {
|
||||
add(const _ListLocationFileBlocExternalEvent());
|
||||
},
|
||||
logTag: "ListLocationFileBloc.refresh",
|
||||
);
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'list_location_file.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListLocationFileBlocNpLog on ListLocationFileBloc {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("bloc.list_location_file.ListLocationFileBloc");
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListLocationFileBlocQueryToString on ListLocationFileBlocQuery {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListLocationFileBlocQuery {account: $account, place: $place, countryCode: $countryCode}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_ListLocationFileBlocExternalEventToString
|
||||
on _ListLocationFileBlocExternalEvent {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_ListLocationFileBlocExternalEvent {}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListLocationFileBlocStateToString on ListLocationFileBlocState {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "${objectRuntimeType(this, "ListLocationFileBlocState")} {account: $account, items: [length: ${items.length}]}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListLocationFileBlocFailureToString on ListLocationFileBlocFailure {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListLocationFileBlocFailure {account: $account, items: [length: ${items.length}], exception: $exception}";
|
||||
}
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
import 'package:nc_photos/entity/share/data_source.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'list_share.g.dart';
|
||||
|
||||
abstract class ListShareBlocEvent {
|
||||
const ListShareBlocEvent();
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListShareBlocQuery extends ListShareBlocEvent {
|
||||
const ListShareBlocQuery(this.account, this.file);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account account;
|
||||
final File file;
|
||||
}
|
||||
|
||||
@toString
|
||||
abstract class ListShareBlocState {
|
||||
const ListShareBlocState(this.account, this.file, this.items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account? account;
|
||||
final File file;
|
||||
final List<Share> items;
|
||||
}
|
||||
|
||||
class ListShareBlocInit extends ListShareBlocState {
|
||||
ListShareBlocInit() : super(null, File(path: ""), const []);
|
||||
}
|
||||
|
||||
class ListShareBlocLoading extends ListShareBlocState {
|
||||
const ListShareBlocLoading(Account? account, File file, List<Share> items)
|
||||
: super(account, file, items);
|
||||
}
|
||||
|
||||
class ListShareBlocSuccess extends ListShareBlocState {
|
||||
const ListShareBlocSuccess(Account? account, File file, List<Share> items)
|
||||
: super(account, file, items);
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListShareBlocFailure extends ListShareBlocState {
|
||||
const ListShareBlocFailure(
|
||||
Account? account, File file, List<Share> items, this.exception)
|
||||
: super(account, file, items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final dynamic exception;
|
||||
}
|
||||
|
||||
/// List all shares from a given file
|
||||
@npLog
|
||||
class ListShareBloc extends Bloc<ListShareBlocEvent, ListShareBlocState> {
|
||||
ListShareBloc() : super(ListShareBlocInit()) {
|
||||
on<ListShareBlocEvent>(_onEvent);
|
||||
}
|
||||
|
||||
Future<void> _onEvent(
|
||||
ListShareBlocEvent event, Emitter<ListShareBlocState> emit) async {
|
||||
_log.info("[_onEvent] $event");
|
||||
if (event is ListShareBlocQuery) {
|
||||
await _onEventQuery(event, emit);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onEventQuery(
|
||||
ListShareBlocQuery ev, Emitter<ListShareBlocState> emit) async {
|
||||
try {
|
||||
emit(ListShareBlocLoading(ev.account, ev.file, state.items));
|
||||
emit(ListShareBlocSuccess(ev.account, ev.file, await _query(ev)));
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[_onEventQuery] Exception while request", e, stackTrace);
|
||||
emit(ListShareBlocFailure(ev.account, ev.file, state.items, e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Share>> _query(ListShareBlocQuery ev) {
|
||||
final shareRepo = ShareRepo(ShareRemoteDataSource());
|
||||
return shareRepo.list(ev.account, ev.file);
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'list_share.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListShareBlocNpLog on ListShareBloc {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("bloc.list_share.ListShareBloc");
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListShareBlocQueryToString on ListShareBlocQuery {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListShareBlocQuery {account: $account, file: ${file.path}}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListShareBlocStateToString on ListShareBlocState {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "${objectRuntimeType(this, "ListShareBlocState")} {account: $account, file: ${file.path}, items: [length: ${items.length}]}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListShareBlocFailureToString on ListShareBlocFailure {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListShareBlocFailure {account: $account, file: ${file.path}, items: [length: ${items.length}], exception: $exception}";
|
||||
}
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.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/entity/sharee.dart';
|
||||
import 'package:nc_photos/entity/sharee/data_source.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'list_sharee.g.dart';
|
||||
|
||||
abstract class ListShareeBlocEvent {
|
||||
const ListShareeBlocEvent();
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListShareeBlocQuery extends ListShareeBlocEvent {
|
||||
const ListShareeBlocQuery(this.account);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account account;
|
||||
}
|
||||
|
||||
@toString
|
||||
abstract class ListShareeBlocState {
|
||||
const ListShareeBlocState(this.account, this.items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account? account;
|
||||
final List<Sharee> items;
|
||||
}
|
||||
|
||||
class ListShareeBlocInit extends ListShareeBlocState {
|
||||
ListShareeBlocInit() : super(null, const []);
|
||||
}
|
||||
|
||||
class ListShareeBlocLoading extends ListShareeBlocState {
|
||||
const ListShareeBlocLoading(Account? account, List<Sharee> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
class ListShareeBlocSuccess extends ListShareeBlocState {
|
||||
const ListShareeBlocSuccess(Account? account, List<Sharee> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListShareeBlocFailure extends ListShareeBlocState {
|
||||
const ListShareeBlocFailure(
|
||||
Account? account, List<Sharee> items, this.exception)
|
||||
: super(account, items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final dynamic exception;
|
||||
}
|
||||
|
||||
/// List all sharees of this account
|
||||
@npLog
|
||||
class ListShareeBloc extends Bloc<ListShareeBlocEvent, ListShareeBlocState> {
|
||||
ListShareeBloc() : super(ListShareeBlocInit()) {
|
||||
on<ListShareeBlocEvent>(_onEvent);
|
||||
}
|
||||
|
||||
static ListShareeBloc of(Account account) {
|
||||
final name = bloc_util.getInstNameForAccount("ListShareeBloc", account);
|
||||
try {
|
||||
_log.fine("[of] Resolving bloc for '$name'");
|
||||
return KiwiContainer().resolve<ListShareeBloc>(name);
|
||||
} catch (_) {
|
||||
// no created instance for this account, make a new one
|
||||
_log.info("[of] New bloc instance for account: $account");
|
||||
final bloc = ListShareeBloc();
|
||||
KiwiContainer().registerInstance<ListShareeBloc>(bloc, name: name);
|
||||
return bloc;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onEvent(
|
||||
ListShareeBlocEvent event, Emitter<ListShareeBlocState> emit) async {
|
||||
_log.info("[_onEvent] $event");
|
||||
if (event is ListShareeBlocQuery) {
|
||||
await _onEventQuery(event, emit);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onEventQuery(
|
||||
ListShareeBlocQuery ev, Emitter<ListShareeBlocState> emit) async {
|
||||
try {
|
||||
emit(ListShareeBlocLoading(ev.account, state.items));
|
||||
emit(ListShareeBlocSuccess(ev.account, await _query(ev)));
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[_onEventQuery] Exception while request", e, stackTrace);
|
||||
emit(ListShareeBlocFailure(ev.account, state.items, e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Sharee>> _query(ListShareeBlocQuery ev) {
|
||||
final shareeRepo = ShareeRepo(ShareeRemoteDataSource());
|
||||
return shareeRepo.list(ev.account);
|
||||
}
|
||||
|
||||
static final _log = _$ListShareeBlocNpLog.log;
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'list_sharee.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListShareeBlocNpLog on ListShareeBloc {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("bloc.list_sharee.ListShareeBloc");
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListShareeBlocQueryToString on ListShareeBlocQuery {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListShareeBlocQuery {account: $account}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListShareeBlocStateToString on ListShareeBlocState {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "${objectRuntimeType(this, "ListShareeBlocState")} {account: $account, items: [length: ${items.length}]}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListShareeBlocFailureToString on ListShareeBlocFailure {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListShareeBlocFailure {account: $account, items: [length: ${items.length}], exception: $exception}";
|
||||
}
|
||||
}
|
|
@ -304,6 +304,7 @@ class ListSharingBloc extends Bloc<ListSharingBlocEvent, ListSharingBlocState> {
|
|||
s,
|
||||
File(
|
||||
path: webdavPath,
|
||||
fileId: s.itemSource,
|
||||
isCollection: true,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -1,226 +0,0 @@
|
|||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/throttler.dart';
|
||||
import 'package:nc_photos/use_case/find_file.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'list_tag_file.g.dart';
|
||||
|
||||
abstract class ListTagFileBlocEvent {
|
||||
const ListTagFileBlocEvent();
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListTagFileBlocQuery extends ListTagFileBlocEvent {
|
||||
const ListTagFileBlocQuery(this.account, this.tag);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account account;
|
||||
final Tag tag;
|
||||
}
|
||||
|
||||
/// An external event has happened and may affect the state of this bloc
|
||||
@toString
|
||||
class _ListTagFileBlocExternalEvent extends ListTagFileBlocEvent {
|
||||
const _ListTagFileBlocExternalEvent();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
@toString
|
||||
abstract class ListTagFileBlocState {
|
||||
const ListTagFileBlocState(this.account, this.items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account? account;
|
||||
final List<File> items;
|
||||
}
|
||||
|
||||
class ListTagFileBlocInit extends ListTagFileBlocState {
|
||||
ListTagFileBlocInit() : super(null, const []);
|
||||
}
|
||||
|
||||
class ListTagFileBlocLoading extends ListTagFileBlocState {
|
||||
const ListTagFileBlocLoading(Account? account, List<File> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
class ListTagFileBlocSuccess extends ListTagFileBlocState {
|
||||
const ListTagFileBlocSuccess(Account? account, List<File> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListTagFileBlocFailure extends ListTagFileBlocState {
|
||||
const ListTagFileBlocFailure(
|
||||
Account? account, List<File> items, this.exception)
|
||||
: super(account, items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Object exception;
|
||||
}
|
||||
|
||||
/// The state of this bloc is inconsistent. This typically means that the data
|
||||
/// may have been changed externally
|
||||
class ListTagFileBlocInconsistent extends ListTagFileBlocState {
|
||||
const ListTagFileBlocInconsistent(Account? account, List<File> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
/// List all files associated with a specific tag
|
||||
@npLog
|
||||
class ListTagFileBloc extends Bloc<ListTagFileBlocEvent, ListTagFileBlocState> {
|
||||
ListTagFileBloc(this._c)
|
||||
: assert(require(_c)),
|
||||
// assert(PopulatePerson.require(_c)),
|
||||
super(ListTagFileBlocInit()) {
|
||||
_fileRemovedEventListener.begin();
|
||||
_filePropertyUpdatedEventListener.begin();
|
||||
|
||||
on<ListTagFileBlocEvent>(_onEvent);
|
||||
}
|
||||
|
||||
static bool require(DiContainer c) =>
|
||||
DiContainer.has(c, DiType.taggedFileRepo);
|
||||
|
||||
@override
|
||||
close() {
|
||||
_fileRemovedEventListener.end();
|
||||
_filePropertyUpdatedEventListener.end();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _onEvent(
|
||||
ListTagFileBlocEvent event, Emitter<ListTagFileBlocState> emit) async {
|
||||
_log.info("[_onEvent] $event");
|
||||
if (event is ListTagFileBlocQuery) {
|
||||
await _onEventQuery(event, emit);
|
||||
} else if (event is _ListTagFileBlocExternalEvent) {
|
||||
await _onExternalEvent(event, emit);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onEventQuery(
|
||||
ListTagFileBlocQuery ev, Emitter<ListTagFileBlocState> emit) async {
|
||||
try {
|
||||
emit(ListTagFileBlocLoading(ev.account, state.items));
|
||||
emit(ListTagFileBlocSuccess(ev.account, await _query(ev)));
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[_onEventQuery] Exception while request", e, stackTrace);
|
||||
emit(ListTagFileBlocFailure(ev.account, state.items, e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onExternalEvent(_ListTagFileBlocExternalEvent ev,
|
||||
Emitter<ListTagFileBlocState> emit) async {
|
||||
emit(ListTagFileBlocInconsistent(state.account, state.items));
|
||||
}
|
||||
|
||||
void _onFileRemovedEvent(FileRemovedEvent ev) {
|
||||
if (state is ListTagFileBlocInit) {
|
||||
// no data in this bloc, ignore
|
||||
return;
|
||||
}
|
||||
if (_isFileOfInterest(ev.file)) {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onFilePropertyUpdatedEvent(FilePropertyUpdatedEvent ev) {
|
||||
if (!ev.hasAnyProperties([
|
||||
FilePropertyUpdatedEvent.propMetadata,
|
||||
FilePropertyUpdatedEvent.propIsArchived,
|
||||
FilePropertyUpdatedEvent.propOverrideDateTime,
|
||||
FilePropertyUpdatedEvent.propFavorite,
|
||||
])) {
|
||||
// not interested
|
||||
return;
|
||||
}
|
||||
if (state is ListTagFileBlocInit) {
|
||||
// no data in this bloc, ignore
|
||||
return;
|
||||
}
|
||||
if (!_isFileOfInterest(ev.file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.hasAnyProperties([
|
||||
FilePropertyUpdatedEvent.propIsArchived,
|
||||
FilePropertyUpdatedEvent.propOverrideDateTime,
|
||||
FilePropertyUpdatedEvent.propFavorite,
|
||||
])) {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
} else {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 10),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<File>> _query(ListTagFileBlocQuery ev) async {
|
||||
final files = <File>[];
|
||||
for (final r in ev.account.roots) {
|
||||
final dir = File(path: file_util.unstripPath(ev.account, r));
|
||||
final taggedFiles =
|
||||
await _c.taggedFileRepo.list(ev.account, dir, [ev.tag]);
|
||||
files.addAll(await FindFile(_c)(
|
||||
ev.account,
|
||||
taggedFiles.map((e) => e.fileId).toList(),
|
||||
onFileNotFound: (id) {
|
||||
_log.warning("[_query] Missing file: $id");
|
||||
},
|
||||
));
|
||||
}
|
||||
return files.where((f) => file_util.isSupportedFormat(f)).toList();
|
||||
}
|
||||
|
||||
bool _isFileOfInterest(File file) {
|
||||
if (!file_util.isSupportedFormat(file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (final r in state.account?.roots ?? []) {
|
||||
final dir = File(path: file_util.unstripPath(state.account!, r));
|
||||
if (file_util.isUnderDir(file, dir)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
|
||||
late final _fileRemovedEventListener =
|
||||
AppEventListener<FileRemovedEvent>(_onFileRemovedEvent);
|
||||
late final _filePropertyUpdatedEventListener =
|
||||
AppEventListener<FilePropertyUpdatedEvent>(_onFilePropertyUpdatedEvent);
|
||||
|
||||
late final _refreshThrottler = Throttler(
|
||||
onTriggered: (_) {
|
||||
add(const _ListTagFileBlocExternalEvent());
|
||||
},
|
||||
logTag: "ListTagFileBloc.refresh",
|
||||
);
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'list_tag_file.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListTagFileBlocNpLog on ListTagFileBloc {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("bloc.list_tag_file.ListTagFileBloc");
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListTagFileBlocQueryToString on ListTagFileBlocQuery {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListTagFileBlocQuery {account: $account, tag: $tag}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_ListTagFileBlocExternalEventToString
|
||||
on _ListTagFileBlocExternalEvent {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_ListTagFileBlocExternalEvent {}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListTagFileBlocStateToString on ListTagFileBlocState {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "${objectRuntimeType(this, "ListTagFileBlocState")} {account: $account, items: [length: ${items.length}]}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListTagFileBlocFailureToString on ListTagFileBlocFailure {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListTagFileBlocFailure {account: $account, items: [length: ${items.length}], exception: $exception}";
|
||||
}
|
||||
}
|
|
@ -481,7 +481,7 @@ class ScanAccountDirBloc
|
|||
return files;
|
||||
}
|
||||
|
||||
bool _isFileOfInterest(File file) {
|
||||
bool _isFileOfInterest(FileDescriptor file) {
|
||||
if (!file_util.isSupportedFormat(file)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:logging/logging.dart';
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.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/search.dart';
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
|
@ -198,7 +199,7 @@ class SearchBloc extends Bloc<SearchBlocEvent, SearchBlocState> {
|
|||
Future<List<File>> _query(SearchBlocQuery ev) =>
|
||||
Search(_c)(ev.account, ev.criteria);
|
||||
|
||||
bool _isFileOfInterest(File file) {
|
||||
bool _isFileOfInterest(FileDescriptor file) {
|
||||
if (!file_util.isSupportedFormat(file)) {
|
||||
return false;
|
||||
}
|
||||
|
|
3
app/lib/bloc_util.dart
Normal file
3
app/lib/bloc_util.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
abstract class BlocTag {
|
||||
String get tag;
|
||||
}
|
32
app/lib/controller/account_controller.dart
Normal file
32
app/lib/controller/account_controller.dart
Normal file
|
@ -0,0 +1,32 @@
|
|||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/controller/collections_controller.dart';
|
||||
import 'package:nc_photos/controller/server_controller.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
|
||||
class AccountController {
|
||||
void setCurrentAccount(Account account) {
|
||||
_account = account;
|
||||
_collectionsController?.dispose();
|
||||
_collectionsController = null;
|
||||
_serverController = null;
|
||||
}
|
||||
|
||||
Account get account => _account!;
|
||||
|
||||
CollectionsController get collectionsController =>
|
||||
_collectionsController ??= CollectionsController(
|
||||
KiwiContainer().resolve<DiContainer>(),
|
||||
account: _account!,
|
||||
serverController: serverController,
|
||||
);
|
||||
|
||||
ServerController get serverController =>
|
||||
_serverController ??= ServerController(
|
||||
account: _account!,
|
||||
);
|
||||
|
||||
Account? _account;
|
||||
CollectionsController? _collectionsController;
|
||||
ServerController? _serverController;
|
||||
}
|
348
app/lib/controller/collection_items_controller.dart
Normal file
348
app/lib/controller/collection_items_controller.dart
Normal file
|
@ -0,0 +1,348 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:copy_with/copy_with.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/new_item.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/exception_event.dart';
|
||||
import 'package:nc_photos/list_extension.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/rx_extension.dart';
|
||||
import 'package:nc_photos/use_case/collection/add_file_to_collection.dart';
|
||||
import 'package:nc_photos/use_case/collection/list_collection_item.dart';
|
||||
import 'package:nc_photos/use_case/collection/remove_from_collection.dart';
|
||||
import 'package:nc_photos/use_case/collection/update_collection_post_load.dart';
|
||||
import 'package:nc_photos/use_case/remove.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
part 'collection_items_controller.g.dart';
|
||||
|
||||
@genCopyWith
|
||||
class CollectionItemStreamData {
|
||||
const CollectionItemStreamData({
|
||||
required this.items,
|
||||
required this.hasNext,
|
||||
});
|
||||
|
||||
final List<CollectionItem> items;
|
||||
|
||||
/// If true, the results are intermediate values and may not represent the
|
||||
/// latest state
|
||||
final bool hasNext;
|
||||
}
|
||||
|
||||
@npLog
|
||||
class CollectionItemsController {
|
||||
CollectionItemsController(
|
||||
this._c, {
|
||||
required this.account,
|
||||
required this.collection,
|
||||
required this.onCollectionUpdated,
|
||||
}) {
|
||||
_fileRemovedEventListener.begin();
|
||||
}
|
||||
|
||||
/// Dispose this controller and release all internal resources
|
||||
///
|
||||
/// MUST be called
|
||||
void dispose() {
|
||||
_fileRemovedEventListener.end();
|
||||
_dataStreamController.close();
|
||||
}
|
||||
|
||||
/// Subscribe to collection items in [collection]
|
||||
///
|
||||
/// The returned stream will emit new list of items whenever there are changes
|
||||
/// to the items (e.g., new item, removed item, etc)
|
||||
///
|
||||
/// 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<CollectionItemStreamData> get stream {
|
||||
if (!_isDataStreamInited) {
|
||||
_isDataStreamInited = true;
|
||||
unawaited(_load());
|
||||
}
|
||||
return _dataStreamController.stream;
|
||||
}
|
||||
|
||||
/// Peek the stream and return the current value
|
||||
CollectionItemStreamData peekStream() => _dataStreamController.stream.value;
|
||||
|
||||
/// Add list of [files] to [collection]
|
||||
Future<void> addFiles(List<FileDescriptor> files) async {
|
||||
final isInited = _isDataStreamInited;
|
||||
final List<FileDescriptor> toAdd;
|
||||
if (isInited) {
|
||||
toAdd = files
|
||||
.where((a) => _dataStreamController.value.items
|
||||
.whereType<CollectionFileItem>()
|
||||
.every((b) => !a.compareServerIdentity(b.file)))
|
||||
.toList();
|
||||
_log.info("[addFiles] Adding ${toAdd.length} non duplicated files");
|
||||
if (toAdd.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_dataStreamController.addWithValue((value) => value.copyWith(
|
||||
items: [
|
||||
...toAdd.map((f) => NewCollectionFileItem(f)),
|
||||
...value.items,
|
||||
],
|
||||
));
|
||||
} else {
|
||||
toAdd = files;
|
||||
_log.info("[addFiles] Adding ${toAdd.length} files");
|
||||
if (toAdd.isEmpty) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ExceptionEvent? error;
|
||||
final failed = <FileDescriptor>[];
|
||||
await _mutex.protect(() async {
|
||||
await AddFileToCollection(_c)(
|
||||
account,
|
||||
collection,
|
||||
toAdd,
|
||||
onError: (f, e, stackTrace) {
|
||||
_log.severe("[addFiles] Exception: ${logFilename(f.strippedPath)}", e,
|
||||
stackTrace);
|
||||
error ??= ExceptionEvent(e, stackTrace);
|
||||
failed.add(f);
|
||||
},
|
||||
onCollectionUpdated: (value) {
|
||||
collection = value;
|
||||
onCollectionUpdated(collection);
|
||||
},
|
||||
);
|
||||
|
||||
if (isInited) {
|
||||
error
|
||||
?.run((e) => _dataStreamController.addError(e.error, e.stackTrace));
|
||||
var finalize = _dataStreamController.value.items.toList();
|
||||
if (failed.isNotEmpty) {
|
||||
// remove failed items
|
||||
finalize.removeWhere((r) {
|
||||
if (r is CollectionFileItem) {
|
||||
return failed.any((f) => r.file.compareServerIdentity(f));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
// convert intermediate items
|
||||
finalize = (await finalize.asyncMap((e) async {
|
||||
try {
|
||||
if (e is NewCollectionFileItem) {
|
||||
return await CollectionAdapter.of(_c, account, collection)
|
||||
.adaptToNewItem(e);
|
||||
} else {
|
||||
return e;
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[addFiles] Item not found in resulting collection: $e",
|
||||
e, stackTrace);
|
||||
return null;
|
||||
}
|
||||
}))
|
||||
.whereNotNull()
|
||||
.toList();
|
||||
_dataStreamController.addWithValue((value) => value.copyWith(
|
||||
items: finalize,
|
||||
));
|
||||
} else if (isInited != _isDataStreamInited) {
|
||||
// stream loaded in between this op, reload
|
||||
unawaited(_load());
|
||||
}
|
||||
});
|
||||
error?.throwMe();
|
||||
}
|
||||
|
||||
/// Remove list of [items] from [collection]
|
||||
///
|
||||
/// The items are compared with [identical], so it's required that all the
|
||||
/// item instances come from the value stream
|
||||
Future<void> removeItems(List<CollectionItem> items) async {
|
||||
final isInited = _isDataStreamInited;
|
||||
if (isInited) {
|
||||
_dataStreamController.addWithValue((value) => value.copyWith(
|
||||
items: value.items
|
||||
.where((a) => !items.any((b) => identical(a, b)))
|
||||
.toList(),
|
||||
));
|
||||
}
|
||||
|
||||
ExceptionEvent? error;
|
||||
final failed = <CollectionItem>[];
|
||||
await _mutex.protect(() async {
|
||||
await RemoveFromCollection(_c)(
|
||||
account,
|
||||
collection,
|
||||
items,
|
||||
onError: (_, item, e, stackTrace) {
|
||||
_log.severe("[removeItems] Exception: $item", e, stackTrace);
|
||||
error ??= ExceptionEvent(e, stackTrace);
|
||||
failed.add(item);
|
||||
},
|
||||
onCollectionUpdated: (value) {
|
||||
collection = value;
|
||||
onCollectionUpdated(collection);
|
||||
},
|
||||
);
|
||||
|
||||
if (isInited) {
|
||||
error
|
||||
?.run((e) => _dataStreamController.addError(e.error, e.stackTrace));
|
||||
if (failed.isNotEmpty) {
|
||||
_dataStreamController.addWithValue((value) => value.copyWith(
|
||||
items: value.items + failed,
|
||||
));
|
||||
}
|
||||
} else if (isInited != _isDataStreamInited) {
|
||||
// stream loaded in between this op, reload
|
||||
unawaited(_load());
|
||||
}
|
||||
});
|
||||
error?.throwMe();
|
||||
}
|
||||
|
||||
/// Delete list of [files] from your server
|
||||
///
|
||||
/// This is a temporary workaround and will be moved away
|
||||
Future<void> deleteItems(List<FileDescriptor> files) async {
|
||||
final isInited = _isDataStreamInited;
|
||||
final List<FileDescriptor> toDelete;
|
||||
List<CollectionFileItem>? toDeleteItem;
|
||||
if (isInited) {
|
||||
final groups = _dataStreamController.value.items.groupListsBy((i) {
|
||||
if (i is CollectionFileItem) {
|
||||
return !files.any((f) => i.file.compareServerIdentity(f));
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
final retain = groups[true] ?? const [];
|
||||
toDeleteItem = groups[false]?.cast<CollectionFileItem>() ?? const [];
|
||||
if (toDeleteItem.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_dataStreamController.addWithValue((value) => value.copyWith(
|
||||
items: retain,
|
||||
));
|
||||
toDelete = toDeleteItem.map((e) => e.file).toList();
|
||||
} else {
|
||||
toDelete = files;
|
||||
}
|
||||
|
||||
ExceptionEvent? error;
|
||||
final failed = <CollectionItem>[];
|
||||
await _mutex.protect(() async {
|
||||
await Remove(_c)(
|
||||
account,
|
||||
toDelete,
|
||||
onError: (i, f, e, stackTrace) {
|
||||
_log.severe("[deleteItems] Exception: ${logFilename(f.strippedPath)}",
|
||||
e, stackTrace);
|
||||
error ??= ExceptionEvent(e, stackTrace);
|
||||
if (isInited) {
|
||||
failed.add(toDeleteItem![i]);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (isInited) {
|
||||
error
|
||||
?.run((e) => _dataStreamController.addError(e.error, e.stackTrace));
|
||||
if (failed.isNotEmpty) {
|
||||
_dataStreamController.addWithValue((value) => value.copyWith(
|
||||
items: value.items + failed,
|
||||
));
|
||||
}
|
||||
} else if (isInited != _isDataStreamInited) {
|
||||
// stream loaded in between this op, reload
|
||||
unawaited(_load());
|
||||
}
|
||||
});
|
||||
error?.throwMe();
|
||||
}
|
||||
|
||||
/// Replace items in the stream, for internal use only
|
||||
void forceReplaceItems(List<CollectionItem> items) {
|
||||
_dataStreamController.addWithValue((v) => v.copyWith(items: items));
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
List<CollectionItem>? items;
|
||||
await for (final r in ListCollectionItem(_c)(account, collection)) {
|
||||
items = r;
|
||||
_dataStreamController.add(CollectionItemStreamData(
|
||||
items: r,
|
||||
hasNext: true,
|
||||
));
|
||||
}
|
||||
if (items != null) {
|
||||
_dataStreamController.add(CollectionItemStreamData(
|
||||
items: items,
|
||||
hasNext: false,
|
||||
));
|
||||
final newCollection =
|
||||
await UpdateCollectionPostLoad(_c)(account, collection, items);
|
||||
if (newCollection != null) {
|
||||
onCollectionUpdated(newCollection);
|
||||
}
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_dataStreamController
|
||||
..addError(e, stackTrace)
|
||||
..addWithValue((v) => v.copyWith(hasNext: false));
|
||||
}
|
||||
}
|
||||
|
||||
void _onFileRemovedEvent(FileRemovedEvent ev) {
|
||||
// if (account != ev.account) {
|
||||
// return;
|
||||
// }
|
||||
// final newItems = _dataStreamController.value.items.where((e) {
|
||||
// if (e is CollectionFileItem) {
|
||||
// return !e.file.compareServerIdentity(ev.file);
|
||||
// } else {
|
||||
// return true;
|
||||
// }
|
||||
// }).toList();
|
||||
// if (newItems.length != _dataStreamController.value.items.length) {
|
||||
// // item of interest
|
||||
// _dataStreamController.addWithValue((value) => value.copyWith(
|
||||
// items: newItems,
|
||||
// ));
|
||||
// }
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
final Account account;
|
||||
Collection collection;
|
||||
ValueChanged<Collection> onCollectionUpdated;
|
||||
|
||||
var _isDataStreamInited = false;
|
||||
final _dataStreamController = BehaviorSubject.seeded(
|
||||
const CollectionItemStreamData(
|
||||
items: [],
|
||||
hasNext: true,
|
||||
),
|
||||
);
|
||||
|
||||
late final _fileRemovedEventListener =
|
||||
AppEventListener<FileRemovedEvent>(_onFileRemovedEvent);
|
||||
|
||||
final _mutex = Mutex();
|
||||
}
|
49
app/lib/controller/collection_items_controller.g.dart
Normal file
49
app/lib/controller/collection_items_controller.g.dart
Normal file
|
@ -0,0 +1,49 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'collection_items_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithLintRuleGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class $CollectionItemStreamDataCopyWithWorker {
|
||||
CollectionItemStreamData call({List<CollectionItem>? items, bool? hasNext});
|
||||
}
|
||||
|
||||
class _$CollectionItemStreamDataCopyWithWorkerImpl
|
||||
implements $CollectionItemStreamDataCopyWithWorker {
|
||||
_$CollectionItemStreamDataCopyWithWorkerImpl(this.that);
|
||||
|
||||
@override
|
||||
CollectionItemStreamData call({dynamic items, dynamic hasNext}) {
|
||||
return CollectionItemStreamData(
|
||||
items: items as List<CollectionItem>? ?? that.items,
|
||||
hasNext: hasNext as bool? ?? that.hasNext);
|
||||
}
|
||||
|
||||
final CollectionItemStreamData that;
|
||||
}
|
||||
|
||||
extension $CollectionItemStreamDataCopyWith on CollectionItemStreamData {
|
||||
$CollectionItemStreamDataCopyWithWorker get copyWith => _$copyWith;
|
||||
$CollectionItemStreamDataCopyWithWorker get _$copyWith =>
|
||||
_$CollectionItemStreamDataCopyWithWorkerImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$CollectionItemsControllerNpLog on CollectionItemsController {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger(
|
||||
"controller.collection_items_controller.CollectionItemsController");
|
||||
}
|
379
app/lib/controller/collections_controller.dart
Normal file
379
app/lib/controller/collections_controller.dart
Normal file
|
@ -0,0 +1,379 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:copy_with/copy_with.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/controller/collection_items_controller.dart';
|
||||
import 'package:nc_photos/controller/server_controller.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/util.dart';
|
||||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/sharee.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/rx_extension.dart';
|
||||
import 'package:nc_photos/use_case/collection/create_collection.dart';
|
||||
import 'package:nc_photos/use_case/collection/edit_collection.dart';
|
||||
import 'package:nc_photos/use_case/collection/import_pending_shared_collection.dart';
|
||||
import 'package:nc_photos/use_case/collection/list_collection.dart';
|
||||
import 'package:nc_photos/use_case/collection/remove_collections.dart';
|
||||
import 'package:nc_photos/use_case/collection/share_collection.dart';
|
||||
import 'package:nc_photos/use_case/collection/unshare_collection.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
part 'collections_controller.g.dart';
|
||||
|
||||
@genCopyWith
|
||||
class CollectionStreamData {
|
||||
const CollectionStreamData({
|
||||
required this.collection,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
final Collection collection;
|
||||
final CollectionItemsController controller;
|
||||
}
|
||||
|
||||
@genCopyWith
|
||||
class CollectionStreamEvent {
|
||||
const CollectionStreamEvent({
|
||||
required this.data,
|
||||
required this.hasNext,
|
||||
});
|
||||
|
||||
CollectionItemsController itemsControllerByCollection(Collection collection) {
|
||||
final i = data.indexWhere((d) => collection.compareIdentity(d.collection));
|
||||
return data[i].controller;
|
||||
}
|
||||
|
||||
final List<CollectionStreamData> data;
|
||||
|
||||
/// If true, the results are intermediate values and may not represent the
|
||||
/// latest state
|
||||
final bool hasNext;
|
||||
}
|
||||
|
||||
@npLog
|
||||
class CollectionsController {
|
||||
CollectionsController(
|
||||
this._c, {
|
||||
required this.account,
|
||||
required this.serverController,
|
||||
});
|
||||
|
||||
void dispose() {
|
||||
_dataStreamController.close();
|
||||
for (final c in _itemControllers.values) {
|
||||
c.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a stream of collections associated with [account]
|
||||
///
|
||||
/// The returned stream will emit new list of collections whenever there are
|
||||
/// changes to the collections (e.g., new collection, removed collection, etc)
|
||||
///
|
||||
/// 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<CollectionStreamEvent> get stream {
|
||||
if (!_isDataStreamInited) {
|
||||
_isDataStreamInited = true;
|
||||
unawaited(_load());
|
||||
}
|
||||
return _dataStreamController.stream;
|
||||
}
|
||||
|
||||
/// Peek the stream and return the current value
|
||||
CollectionStreamEvent peekStream() => _dataStreamController.stream.value;
|
||||
|
||||
/// Reload the data
|
||||
///
|
||||
/// The data will be loaded automatically when the stream is first listened so
|
||||
/// it's not necessary to call this method for that purpose
|
||||
Future<void> reload() async {
|
||||
var results = <Collection>[];
|
||||
final completer = Completer();
|
||||
ListCollection(_c, serverController: serverController)(account).listen(
|
||||
(c) {
|
||||
results = c;
|
||||
},
|
||||
onError: _dataStreamController.addError,
|
||||
onDone: () => completer.complete(),
|
||||
);
|
||||
await completer.future;
|
||||
_dataStreamController.add(CollectionStreamEvent(
|
||||
data: _prepareDataFor(results),
|
||||
hasNext: false,
|
||||
));
|
||||
}
|
||||
|
||||
Future<Collection> createNew(Collection collection) async {
|
||||
// we can't simply add the collection argument to the stream because
|
||||
// the collection may not be a complete workable instance
|
||||
final created = await CreateCollection(_c)(account, collection);
|
||||
_dataStreamController.addWithValue((v) => v.copyWith(
|
||||
data: _prepareDataFor([
|
||||
created,
|
||||
...v.data.map((e) => e.collection),
|
||||
]),
|
||||
));
|
||||
return created;
|
||||
}
|
||||
|
||||
/// Remove [collections] and return the removed count
|
||||
///
|
||||
/// If [onError] is not null, you'll get notified about the errors. The future
|
||||
/// will always complete normally
|
||||
Future<int> remove(
|
||||
List<Collection> collections, {
|
||||
ErrorWithValueHandler<Collection>? onError,
|
||||
}) async {
|
||||
final newData = _dataStreamController.value.data.toList();
|
||||
final toBeRemoved = <CollectionStreamData>[];
|
||||
var failedCount = 0;
|
||||
for (final c in collections) {
|
||||
final i = newData.indexWhere((d) => c.compareIdentity(d.collection));
|
||||
if (i == -1) {
|
||||
_log.warning("[remove] Collection not found: $c");
|
||||
} else {
|
||||
toBeRemoved.add(newData.removeAt(i));
|
||||
}
|
||||
}
|
||||
_dataStreamController.addWithValue((v) => v.copyWith(
|
||||
data: newData,
|
||||
));
|
||||
|
||||
final restore = <CollectionStreamData>[];
|
||||
await _mutex.protect(() async {
|
||||
await RemoveCollections(_c)(
|
||||
account,
|
||||
collections,
|
||||
onError: (c, e, stackTrace) {
|
||||
_log.severe(
|
||||
"[remove] Failed while RemoveCollections: $c", e, stackTrace);
|
||||
final i =
|
||||
toBeRemoved.indexWhere((d) => c.compareIdentity(d.collection));
|
||||
if (i != -1) {
|
||||
restore.add(toBeRemoved.removeAt(i));
|
||||
}
|
||||
++failedCount;
|
||||
onError?.call(c, e, stackTrace);
|
||||
},
|
||||
);
|
||||
});
|
||||
toBeRemoved
|
||||
.map((d) => _CollectionKey(d.collection))
|
||||
.forEach(_itemControllers.remove);
|
||||
if (restore.isNotEmpty) {
|
||||
_log.severe("[remove] Restoring ${restore.length} collections");
|
||||
_dataStreamController.addWithValue((v) => v.copyWith(
|
||||
data: [
|
||||
...restore,
|
||||
...v.data,
|
||||
],
|
||||
));
|
||||
}
|
||||
return collections.length - failedCount;
|
||||
}
|
||||
|
||||
/// See [EditCollection]
|
||||
Future<void> edit(
|
||||
Collection collection, {
|
||||
String? name,
|
||||
List<CollectionItem>? items,
|
||||
CollectionItemSort? itemSort,
|
||||
OrNull<FileDescriptor>? cover,
|
||||
}) async {
|
||||
try {
|
||||
final c = await _mutex.protect(() async {
|
||||
final found = _dataStreamController.value.data.firstWhereOrNull(
|
||||
(ev) => ev.collection.compareIdentity(collection));
|
||||
final item = found?.controller.peekStream();
|
||||
return await EditCollection(_c)(
|
||||
account,
|
||||
collection,
|
||||
name: name,
|
||||
items: items,
|
||||
itemSort: itemSort,
|
||||
cover: cover,
|
||||
knownItems: (item?.items.isEmpty ?? true) ? null : item!.items,
|
||||
);
|
||||
});
|
||||
_updateCollection(c, items);
|
||||
} catch (e, stackTrace) {
|
||||
_dataStreamController.addError(e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> share(Collection collection, Sharee sharee) async {
|
||||
try {
|
||||
Collection? newCollection;
|
||||
final result = await _mutex.protect(() async {
|
||||
return await ShareCollection(_c)(
|
||||
account,
|
||||
collection,
|
||||
sharee,
|
||||
onCollectionUpdated: (c) {
|
||||
newCollection = c;
|
||||
},
|
||||
);
|
||||
});
|
||||
if (newCollection != null) {
|
||||
_updateCollection(newCollection!);
|
||||
}
|
||||
if (result == CollectionShareResult.partial) {
|
||||
_dataStreamController
|
||||
.addError(CollectionPartialShareException(sharee.shareWith.raw));
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_dataStreamController.addError(e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> unshare(Collection collection, CollectionShare share) async {
|
||||
try {
|
||||
Collection? newCollection;
|
||||
final result = await _mutex.protect(() async {
|
||||
return await UnshareCollection(_c)(
|
||||
account,
|
||||
collection,
|
||||
share.userId,
|
||||
onCollectionUpdated: (c) {
|
||||
newCollection = c;
|
||||
},
|
||||
);
|
||||
});
|
||||
if (newCollection != null) {
|
||||
_updateCollection(newCollection!);
|
||||
}
|
||||
if (result == CollectionShareResult.partial) {
|
||||
_dataStreamController
|
||||
.addError(CollectionPartialUnshareException(share.username));
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_dataStreamController.addError(e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
/// See [ImportPendingSharedCollection]
|
||||
Future<Collection?> importPendingSharedCollection(
|
||||
Collection collection) async {
|
||||
try {
|
||||
final newCollection =
|
||||
await ImportPendingSharedCollection(_c)(account, collection);
|
||||
_dataStreamController.addWithValue((v) => v.copyWith(
|
||||
data: _prepareDataFor([
|
||||
newCollection,
|
||||
...v.data.map((e) => e.collection),
|
||||
]),
|
||||
));
|
||||
return newCollection;
|
||||
} catch (e, stackTrace) {
|
||||
_dataStreamController.addError(e, stackTrace);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
var lastData = const CollectionStreamEvent(
|
||||
data: [],
|
||||
hasNext: false,
|
||||
);
|
||||
final completer = Completer();
|
||||
ListCollection(_c, serverController: serverController)(account).listen(
|
||||
(c) {
|
||||
lastData = CollectionStreamEvent(
|
||||
data: _prepareDataFor(c),
|
||||
hasNext: true,
|
||||
);
|
||||
_dataStreamController.add(lastData);
|
||||
},
|
||||
onError: _dataStreamController.addError,
|
||||
onDone: () => completer.complete(),
|
||||
);
|
||||
await completer.future;
|
||||
_dataStreamController.add(lastData.copyWith(hasNext: false));
|
||||
}
|
||||
|
||||
List<CollectionStreamData> _prepareDataFor(List<Collection> collections) {
|
||||
final data = <CollectionStreamData>[];
|
||||
final keys = <_CollectionKey>[];
|
||||
for (final c in collections) {
|
||||
final k = _CollectionKey(c);
|
||||
_itemControllers[k] ??= CollectionItemsController(
|
||||
_c,
|
||||
account: account,
|
||||
collection: k.collection,
|
||||
onCollectionUpdated: _updateCollection,
|
||||
);
|
||||
data.add(CollectionStreamData(
|
||||
collection: c,
|
||||
controller: _itemControllers[k]!,
|
||||
));
|
||||
keys.add(k);
|
||||
}
|
||||
|
||||
final remove =
|
||||
_itemControllers.keys.where((k) => !keys.contains(k)).toList();
|
||||
for (final k in remove) {
|
||||
_itemControllers[k]?.dispose();
|
||||
_itemControllers.remove(k);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
void _updateCollection(Collection collection, [List<CollectionItem>? items]) {
|
||||
_log.info("[_updateCollection] Updating collection: $collection");
|
||||
_dataStreamController.addWithValue((v) => v.copyWith(
|
||||
data: v.data.map((d) {
|
||||
if (d.collection.compareIdentity(collection)) {
|
||||
if (items != null) {
|
||||
d.controller.forceReplaceItems(items);
|
||||
}
|
||||
return d.copyWith(collection: collection);
|
||||
} else {
|
||||
return d;
|
||||
}
|
||||
}).toList(),
|
||||
));
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
final Account account;
|
||||
final ServerController serverController;
|
||||
|
||||
var _isDataStreamInited = false;
|
||||
final _dataStreamController = BehaviorSubject.seeded(
|
||||
const CollectionStreamEvent(
|
||||
data: [],
|
||||
hasNext: true,
|
||||
),
|
||||
);
|
||||
|
||||
final _itemControllers = <_CollectionKey, CollectionItemsController>{};
|
||||
|
||||
final _mutex = Mutex();
|
||||
}
|
||||
|
||||
class _CollectionKey {
|
||||
const _CollectionKey(this.collection);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _CollectionKey &&
|
||||
collection.compareIdentity(other.collection);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => collection.identityHashCode;
|
||||
|
||||
final Collection collection;
|
||||
}
|
75
app/lib/controller/collections_controller.g.dart
Normal file
75
app/lib/controller/collections_controller.g.dart
Normal file
|
@ -0,0 +1,75 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'collections_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithLintRuleGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class $CollectionStreamDataCopyWithWorker {
|
||||
CollectionStreamData call(
|
||||
{Collection? collection, CollectionItemsController? controller});
|
||||
}
|
||||
|
||||
class _$CollectionStreamDataCopyWithWorkerImpl
|
||||
implements $CollectionStreamDataCopyWithWorker {
|
||||
_$CollectionStreamDataCopyWithWorkerImpl(this.that);
|
||||
|
||||
@override
|
||||
CollectionStreamData call({dynamic collection, dynamic controller}) {
|
||||
return CollectionStreamData(
|
||||
collection: collection as Collection? ?? that.collection,
|
||||
controller:
|
||||
controller as CollectionItemsController? ?? that.controller);
|
||||
}
|
||||
|
||||
final CollectionStreamData that;
|
||||
}
|
||||
|
||||
extension $CollectionStreamDataCopyWith on CollectionStreamData {
|
||||
$CollectionStreamDataCopyWithWorker get copyWith => _$copyWith;
|
||||
$CollectionStreamDataCopyWithWorker get _$copyWith =>
|
||||
_$CollectionStreamDataCopyWithWorkerImpl(this);
|
||||
}
|
||||
|
||||
abstract class $CollectionStreamEventCopyWithWorker {
|
||||
CollectionStreamEvent call({List<CollectionStreamData>? data, bool? hasNext});
|
||||
}
|
||||
|
||||
class _$CollectionStreamEventCopyWithWorkerImpl
|
||||
implements $CollectionStreamEventCopyWithWorker {
|
||||
_$CollectionStreamEventCopyWithWorkerImpl(this.that);
|
||||
|
||||
@override
|
||||
CollectionStreamEvent call({dynamic data, dynamic hasNext}) {
|
||||
return CollectionStreamEvent(
|
||||
data: data as List<CollectionStreamData>? ?? that.data,
|
||||
hasNext: hasNext as bool? ?? that.hasNext);
|
||||
}
|
||||
|
||||
final CollectionStreamEvent that;
|
||||
}
|
||||
|
||||
extension $CollectionStreamEventCopyWith on CollectionStreamEvent {
|
||||
$CollectionStreamEventCopyWithWorker get copyWith => _$copyWith;
|
||||
$CollectionStreamEventCopyWithWorker get _$copyWith =>
|
||||
_$CollectionStreamEventCopyWithWorkerImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$CollectionsControllerNpLog on CollectionsController {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log =
|
||||
Logger("controller.collections_controller.CollectionsController");
|
||||
}
|
54
app/lib/controller/pref_controller.dart
Normal file
54
app/lib/controller/pref_controller.dart
Normal file
|
@ -0,0 +1,54 @@
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
part 'pref_controller.g.dart';
|
||||
|
||||
@npLog
|
||||
class PrefController {
|
||||
PrefController(this._c);
|
||||
|
||||
ValueStream<int> get albumBrowserZoomLevel =>
|
||||
_albumBrowserZoomLevelController.stream;
|
||||
|
||||
Future<void> setAlbumBrowserZoomLevel(int value) async {
|
||||
final backup = _albumBrowserZoomLevelController.value;
|
||||
_albumBrowserZoomLevelController.add(value);
|
||||
try {
|
||||
if (!await _c.pref.setAlbumBrowserZoomLevel(value)) {
|
||||
throw StateError("Unknown error");
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[setAlbumBrowserZoomLevel] Failed setting preference", e,
|
||||
stackTrace);
|
||||
_albumBrowserZoomLevelController
|
||||
..addError(e, stackTrace)
|
||||
..add(backup);
|
||||
}
|
||||
}
|
||||
|
||||
ValueStream<int> get homeAlbumsSort => _homeAlbumsSortController.stream;
|
||||
|
||||
Future<void> setHomeAlbumsSort(int value) async {
|
||||
final backup = _homeAlbumsSortController.value;
|
||||
_homeAlbumsSortController.add(value);
|
||||
try {
|
||||
if (!await _c.pref.setHomeAlbumsSort(value)) {
|
||||
throw StateError("Unknown error");
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe(
|
||||
"[setHomeAlbumsSort] Failed setting preference", e, stackTrace);
|
||||
_homeAlbumsSortController
|
||||
..addError(e, stackTrace)
|
||||
..add(backup);
|
||||
}
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
late final _albumBrowserZoomLevelController =
|
||||
BehaviorSubject.seeded(_c.pref.getAlbumBrowserZoomLevelOr(0));
|
||||
late final _homeAlbumsSortController =
|
||||
BehaviorSubject.seeded(_c.pref.getHomeAlbumsSortOr(0));
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'album_picker.dart';
|
||||
part of 'pref_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$_AlbumPickerStateNpLog on _AlbumPickerState {
|
||||
extension _$PrefControllerNpLog on PrefController {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("widget.album_picker._AlbumPickerState");
|
||||
static final log = Logger("controller.pref_controller.PrefController");
|
||||
}
|
61
app/lib/controller/server_controller.dart
Normal file
61
app/lib/controller/server_controller.dart
Normal file
|
@ -0,0 +1,61 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/entity_converter.dart';
|
||||
import 'package:nc_photos/entity/server_status.dart';
|
||||
import 'package:nc_photos/np_api_util.dart';
|
||||
import 'package:np_api/np_api.dart' as api;
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
part 'server_controller.g.dart';
|
||||
|
||||
enum ServerFeature {
|
||||
ncAlbum,
|
||||
}
|
||||
|
||||
@npLog
|
||||
class ServerController {
|
||||
ServerController({
|
||||
required this.account,
|
||||
});
|
||||
|
||||
ValueStream<ServerStatus> get status {
|
||||
if (!_statusStreamContorller.hasValue) {
|
||||
unawaited(_load());
|
||||
}
|
||||
return _statusStreamContorller.stream;
|
||||
}
|
||||
|
||||
bool isSupported(ServerFeature feature) {
|
||||
switch (feature) {
|
||||
case ServerFeature.ncAlbum:
|
||||
return !_statusStreamContorller.hasValue ||
|
||||
_statusStreamContorller.value.majorVersion >= 25;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _load() => _getStatus();
|
||||
|
||||
Future<void> _getStatus() async {
|
||||
try {
|
||||
final response = await ApiUtil.fromAccount(account).status().get();
|
||||
if (!response.isGood) {
|
||||
_log.severe("[_getStatus] Failed requesting server: $response");
|
||||
return;
|
||||
}
|
||||
final apiStatus = await api.StatusParser().parse(response.body);
|
||||
final status = ApiStatusConverter.fromApi(apiStatus);
|
||||
_log.info("[_getStatus] Server status: $status");
|
||||
_statusStreamContorller.add(status);
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[_getStatus] Failed while get", e, stackTrace);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final Account account;
|
||||
|
||||
final _statusStreamContorller = BehaviorSubject<ServerStatus>();
|
||||
}
|
14
app/lib/controller/server_controller.g.dart
Normal file
14
app/lib/controller/server_controller.g.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'server_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ServerControllerNpLog on ServerController {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("controller.server_controller.ServerController");
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/repo2.dart';
|
||||
import 'package:nc_photos/entity/face.dart';
|
||||
import 'package:nc_photos/entity/favorite.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/local_file.dart';
|
||||
import 'package:nc_photos/entity/nc_album/repo.dart';
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/entity/search.dart';
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
|
@ -16,7 +18,11 @@ import 'package:nc_photos/touch_manager.dart';
|
|||
|
||||
enum DiType {
|
||||
albumRepo,
|
||||
albumRepoRemote,
|
||||
albumRepoLocal,
|
||||
albumRepo2,
|
||||
albumRepo2Remote,
|
||||
albumRepo2Local,
|
||||
faceRepo,
|
||||
fileRepo,
|
||||
fileRepoRemote,
|
||||
|
@ -33,6 +39,9 @@ enum DiType {
|
|||
taggedFileRepo,
|
||||
localFileRepo,
|
||||
searchRepo,
|
||||
ncAlbumRepo,
|
||||
ncAlbumRepoRemote,
|
||||
ncAlbumRepoLocal,
|
||||
pref,
|
||||
sqliteDb,
|
||||
touchManager,
|
||||
|
@ -41,7 +50,11 @@ enum DiType {
|
|||
class DiContainer {
|
||||
DiContainer({
|
||||
AlbumRepo? albumRepo,
|
||||
AlbumRepo? albumRepoRemote,
|
||||
AlbumRepo? albumRepoLocal,
|
||||
AlbumRepo2? albumRepo2,
|
||||
AlbumRepo2? albumRepo2Remote,
|
||||
AlbumRepo2? albumRepo2Local,
|
||||
FaceRepo? faceRepo,
|
||||
FileRepo? fileRepo,
|
||||
FileRepo? fileRepoRemote,
|
||||
|
@ -58,11 +71,18 @@ class DiContainer {
|
|||
TaggedFileRepo? taggedFileRepo,
|
||||
LocalFileRepo? localFileRepo,
|
||||
SearchRepo? searchRepo,
|
||||
NcAlbumRepo? ncAlbumRepo,
|
||||
NcAlbumRepo? ncAlbumRepoRemote,
|
||||
NcAlbumRepo? ncAlbumRepoLocal,
|
||||
Pref? pref,
|
||||
sql.SqliteDb? sqliteDb,
|
||||
TouchManager? touchManager,
|
||||
}) : _albumRepo = albumRepo,
|
||||
_albumRepoRemote = albumRepoRemote,
|
||||
_albumRepoLocal = albumRepoLocal,
|
||||
_albumRepo2 = albumRepo2,
|
||||
_albumRepo2Remote = albumRepo2Remote,
|
||||
_albumRepo2Local = albumRepo2Local,
|
||||
_faceRepo = faceRepo,
|
||||
_fileRepo = fileRepo,
|
||||
_fileRepoRemote = fileRepoRemote,
|
||||
|
@ -79,6 +99,9 @@ class DiContainer {
|
|||
_taggedFileRepo = taggedFileRepo,
|
||||
_localFileRepo = localFileRepo,
|
||||
_searchRepo = searchRepo,
|
||||
_ncAlbumRepo = ncAlbumRepo,
|
||||
_ncAlbumRepoRemote = ncAlbumRepoRemote,
|
||||
_ncAlbumRepoLocal = ncAlbumRepoLocal,
|
||||
_pref = pref,
|
||||
_sqliteDb = sqliteDb,
|
||||
_touchManager = touchManager;
|
||||
|
@ -89,8 +112,16 @@ class DiContainer {
|
|||
switch (type) {
|
||||
case DiType.albumRepo:
|
||||
return contianer._albumRepo != null;
|
||||
case DiType.albumRepoRemote:
|
||||
return contianer._albumRepoRemote != null;
|
||||
case DiType.albumRepoLocal:
|
||||
return contianer._albumRepoLocal != null;
|
||||
case DiType.albumRepo2:
|
||||
return contianer._albumRepo2 != null;
|
||||
case DiType.albumRepo2Remote:
|
||||
return contianer._albumRepo2Remote != null;
|
||||
case DiType.albumRepo2Local:
|
||||
return contianer._albumRepo2Local != null;
|
||||
case DiType.faceRepo:
|
||||
return contianer._faceRepo != null;
|
||||
case DiType.fileRepo:
|
||||
|
@ -123,6 +154,12 @@ class DiContainer {
|
|||
return contianer._localFileRepo != null;
|
||||
case DiType.searchRepo:
|
||||
return contianer._searchRepo != null;
|
||||
case DiType.ncAlbumRepo:
|
||||
return contianer._ncAlbumRepo != null;
|
||||
case DiType.ncAlbumRepoRemote:
|
||||
return contianer._ncAlbumRepoRemote != null;
|
||||
case DiType.ncAlbumRepoLocal:
|
||||
return contianer._ncAlbumRepoLocal != null;
|
||||
case DiType.pref:
|
||||
return contianer._pref != null;
|
||||
case DiType.sqliteDb:
|
||||
|
@ -134,6 +171,7 @@ class DiContainer {
|
|||
|
||||
DiContainer copyWith({
|
||||
OrNull<AlbumRepo>? albumRepo,
|
||||
OrNull<AlbumRepo2>? albumRepo2,
|
||||
OrNull<FaceRepo>? faceRepo,
|
||||
OrNull<FileRepo>? fileRepo,
|
||||
OrNull<PersonRepo>? personRepo,
|
||||
|
@ -144,12 +182,14 @@ class DiContainer {
|
|||
OrNull<TaggedFileRepo>? taggedFileRepo,
|
||||
OrNull<LocalFileRepo>? localFileRepo,
|
||||
OrNull<SearchRepo>? searchRepo,
|
||||
OrNull<NcAlbumRepo>? ncAlbumRepo,
|
||||
OrNull<Pref>? pref,
|
||||
OrNull<sql.SqliteDb>? sqliteDb,
|
||||
OrNull<TouchManager>? touchManager,
|
||||
}) {
|
||||
return DiContainer(
|
||||
albumRepo: albumRepo == null ? _albumRepo : albumRepo.obj,
|
||||
albumRepo2: albumRepo2 == null ? _albumRepo2 : albumRepo2.obj,
|
||||
faceRepo: faceRepo == null ? _faceRepo : faceRepo.obj,
|
||||
fileRepo: fileRepo == null ? _fileRepo : fileRepo.obj,
|
||||
personRepo: personRepo == null ? _personRepo : personRepo.obj,
|
||||
|
@ -161,6 +201,7 @@ class DiContainer {
|
|||
taggedFileRepo == null ? _taggedFileRepo : taggedFileRepo.obj,
|
||||
localFileRepo: localFileRepo == null ? _localFileRepo : localFileRepo.obj,
|
||||
searchRepo: searchRepo == null ? _searchRepo : searchRepo.obj,
|
||||
ncAlbumRepo: ncAlbumRepo == null ? _ncAlbumRepo : ncAlbumRepo.obj,
|
||||
pref: pref == null ? _pref : pref.obj,
|
||||
sqliteDb: sqliteDb == null ? _sqliteDb : sqliteDb.obj,
|
||||
touchManager: touchManager == null ? _touchManager : touchManager.obj,
|
||||
|
@ -168,7 +209,11 @@ class DiContainer {
|
|||
}
|
||||
|
||||
AlbumRepo get albumRepo => _albumRepo!;
|
||||
AlbumRepo get albumRepoRemote => _albumRepoRemote!;
|
||||
AlbumRepo get albumRepoLocal => _albumRepoLocal!;
|
||||
AlbumRepo2 get albumRepo2 => _albumRepo2!;
|
||||
AlbumRepo2 get albumRepo2Remote => _albumRepo2Remote!;
|
||||
AlbumRepo2 get albumRepo2Local => _albumRepo2Local!;
|
||||
FaceRepo get faceRepo => _faceRepo!;
|
||||
FileRepo get fileRepo => _fileRepo!;
|
||||
FileRepo get fileRepoRemote => _fileRepoRemote!;
|
||||
|
@ -185,6 +230,9 @@ class DiContainer {
|
|||
TaggedFileRepo get taggedFileRepo => _taggedFileRepo!;
|
||||
LocalFileRepo get localFileRepo => _localFileRepo!;
|
||||
SearchRepo get searchRepo => _searchRepo!;
|
||||
NcAlbumRepo get ncAlbumRepo => _ncAlbumRepo!;
|
||||
NcAlbumRepo get ncAlbumRepoRemote => _ncAlbumRepoRemote!;
|
||||
NcAlbumRepo get ncAlbumRepoLocal => _ncAlbumRepoLocal!;
|
||||
TouchManager get touchManager => _touchManager!;
|
||||
|
||||
sql.SqliteDb get sqliteDb => _sqliteDb!;
|
||||
|
@ -195,11 +243,31 @@ class DiContainer {
|
|||
_albumRepo = v;
|
||||
}
|
||||
|
||||
set albumRepoRemote(AlbumRepo v) {
|
||||
assert(_albumRepoRemote == null);
|
||||
_albumRepoRemote = v;
|
||||
}
|
||||
|
||||
set albumRepoLocal(AlbumRepo v) {
|
||||
assert(_albumRepoLocal == null);
|
||||
_albumRepoLocal = v;
|
||||
}
|
||||
|
||||
set albumRepo2(AlbumRepo2 v) {
|
||||
assert(_albumRepo2 == null);
|
||||
_albumRepo2 = v;
|
||||
}
|
||||
|
||||
set albumRepo2Remote(AlbumRepo2 v) {
|
||||
assert(_albumRepo2Remote == null);
|
||||
_albumRepo2Remote = v;
|
||||
}
|
||||
|
||||
set albumRepo2Local(AlbumRepo2 v) {
|
||||
assert(_albumRepo2Local == null);
|
||||
_albumRepo2Local = v;
|
||||
}
|
||||
|
||||
set faceRepo(FaceRepo v) {
|
||||
assert(_faceRepo == null);
|
||||
_faceRepo = v;
|
||||
|
@ -280,6 +348,21 @@ class DiContainer {
|
|||
_searchRepo = v;
|
||||
}
|
||||
|
||||
set ncAlbumRepo(NcAlbumRepo v) {
|
||||
assert(_ncAlbumRepo == null);
|
||||
_ncAlbumRepo = v;
|
||||
}
|
||||
|
||||
set ncAlbumRepoRemote(NcAlbumRepo v) {
|
||||
assert(_ncAlbumRepoRemote == null);
|
||||
_ncAlbumRepoRemote = v;
|
||||
}
|
||||
|
||||
set ncAlbumRepoLocal(NcAlbumRepo v) {
|
||||
assert(_ncAlbumRepoLocal == null);
|
||||
_ncAlbumRepoLocal = v;
|
||||
}
|
||||
|
||||
set touchManager(TouchManager v) {
|
||||
assert(_touchManager == null);
|
||||
_touchManager = v;
|
||||
|
@ -296,9 +379,13 @@ class DiContainer {
|
|||
}
|
||||
|
||||
AlbumRepo? _albumRepo;
|
||||
AlbumRepo? _albumRepoRemote;
|
||||
// Explicitly request a AlbumRepo backed by local source
|
||||
AlbumRepo? _albumRepoLocal;
|
||||
FaceRepo? _faceRepo;
|
||||
AlbumRepo2? _albumRepo2;
|
||||
AlbumRepo2? _albumRepo2Remote;
|
||||
AlbumRepo2? _albumRepo2Local;
|
||||
FileRepo? _fileRepo;
|
||||
// Explicitly request a FileRepo backed by remote source
|
||||
FileRepo? _fileRepoRemote;
|
||||
|
@ -316,6 +403,9 @@ class DiContainer {
|
|||
TaggedFileRepo? _taggedFileRepo;
|
||||
LocalFileRepo? _localFileRepo;
|
||||
SearchRepo? _searchRepo;
|
||||
NcAlbumRepo? _ncAlbumRepo;
|
||||
NcAlbumRepo? _ncAlbumRepoRemote;
|
||||
NcAlbumRepo? _ncAlbumRepoLocal;
|
||||
TouchManager? _touchManager;
|
||||
|
||||
sql.SqliteDb? _sqliteDb;
|
||||
|
@ -323,14 +413,28 @@ class DiContainer {
|
|||
}
|
||||
|
||||
extension DiContainerExtension on DiContainer {
|
||||
/// Uses remote repo if available
|
||||
///
|
||||
/// Notice that not all repo support this
|
||||
DiContainer withRemoteRepo() => copyWith(
|
||||
albumRepo: OrNull(albumRepoRemote),
|
||||
albumRepo2: OrNull(albumRepo2Remote),
|
||||
fileRepo: OrNull(fileRepoRemote),
|
||||
personRepo: OrNull(personRepoRemote),
|
||||
tagRepo: OrNull(tagRepoRemote),
|
||||
ncAlbumRepo: OrNull(ncAlbumRepoRemote),
|
||||
);
|
||||
|
||||
/// Uses local repo if available
|
||||
///
|
||||
/// Notice that not all repo support this
|
||||
DiContainer withLocalRepo() => copyWith(
|
||||
albumRepo: OrNull(albumRepoLocal),
|
||||
albumRepo2: OrNull(albumRepo2Local),
|
||||
fileRepo: OrNull(fileRepoLocal),
|
||||
personRepo: OrNull(personRepoLocal),
|
||||
tagRepo: OrNull(tagRepoLocal),
|
||||
ncAlbumRepo: OrNull(ncAlbumRepoLocal),
|
||||
);
|
||||
|
||||
DiContainer withLocalAlbumRepo() =>
|
||||
|
|
|
@ -46,49 +46,56 @@ class Album with EquatableMixin {
|
|||
final jsonVersion = json["version"];
|
||||
JsonObj? result = json;
|
||||
if (jsonVersion < 2) {
|
||||
result = upgraderFactory?.buildV1()?.call(result);
|
||||
result = upgraderFactory?.buildV1()?.doJson(result);
|
||||
if (result == null) {
|
||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (jsonVersion < 3) {
|
||||
result = upgraderFactory?.buildV2()?.call(result);
|
||||
result = upgraderFactory?.buildV2()?.doJson(result);
|
||||
if (result == null) {
|
||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (jsonVersion < 4) {
|
||||
result = upgraderFactory?.buildV3()?.call(result);
|
||||
result = upgraderFactory?.buildV3()?.doJson(result);
|
||||
if (result == null) {
|
||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (jsonVersion < 5) {
|
||||
result = upgraderFactory?.buildV4()?.call(result);
|
||||
result = upgraderFactory?.buildV4()?.doJson(result);
|
||||
if (result == null) {
|
||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (jsonVersion < 6) {
|
||||
result = upgraderFactory?.buildV5()?.call(result);
|
||||
result = upgraderFactory?.buildV5()?.doJson(result);
|
||||
if (result == null) {
|
||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (jsonVersion < 7) {
|
||||
result = upgraderFactory?.buildV6()?.call(result);
|
||||
result = upgraderFactory?.buildV6()?.doJson(result);
|
||||
if (result == null) {
|
||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (jsonVersion < 8) {
|
||||
result = upgraderFactory?.buildV7()?.call(result);
|
||||
result = upgraderFactory?.buildV7()?.doJson(result);
|
||||
if (result == null) {
|
||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (jsonVersion < 9) {
|
||||
result = upgraderFactory?.buildV8()?.doJson(result);
|
||||
if (result == null) {
|
||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||
return null;
|
||||
|
@ -217,7 +224,7 @@ class Album with EquatableMixin {
|
|||
final int savedVersion;
|
||||
|
||||
/// versioning of this class, use to upgrade old persisted album
|
||||
static const version = 8;
|
||||
static const version = 9;
|
||||
|
||||
static final _log = _$AlbumNpLog.log;
|
||||
}
|
||||
|
|
|
@ -26,9 +26,6 @@ abstract class AlbumCoverProvider with EquatableMixin {
|
|||
case AlbumManualCoverProvider._type:
|
||||
return AlbumManualCoverProvider.fromJson(
|
||||
content.cast<String, dynamic>());
|
||||
case AlbumMemoryCoverProvider._type:
|
||||
return AlbumMemoryCoverProvider.fromJson(
|
||||
content.cast<String, dynamic>());
|
||||
default:
|
||||
_log.shout("[fromJson] Unknown type: $type");
|
||||
throw ArgumentError.value(type, "type");
|
||||
|
@ -65,7 +62,7 @@ abstract class AlbumCoverProvider with EquatableMixin {
|
|||
/// Cover selected automatically by us
|
||||
@toString
|
||||
class AlbumAutoCoverProvider extends AlbumCoverProvider {
|
||||
AlbumAutoCoverProvider({
|
||||
const AlbumAutoCoverProvider({
|
||||
this.coverFile,
|
||||
});
|
||||
|
||||
|
@ -73,48 +70,48 @@ class AlbumAutoCoverProvider extends AlbumCoverProvider {
|
|||
return AlbumAutoCoverProvider(
|
||||
coverFile: json["coverFile"] == null
|
||||
? null
|
||||
: File.fromJson(json["coverFile"].cast<String, dynamic>()),
|
||||
: FileDescriptor.fromJson(json["coverFile"].cast<String, dynamic>()),
|
||||
);
|
||||
}
|
||||
|
||||
static FileDescriptor? getCoverByItems(List<AlbumItem> items) {
|
||||
return items
|
||||
.whereType<AlbumFileItem>()
|
||||
.map((e) => e.file)
|
||||
.where((element) =>
|
||||
file_util.isSupportedFormat(element) &&
|
||||
(element.hasPreview ?? false) &&
|
||||
element.fileId != null)
|
||||
.sorted(compareFileDateTimeDescending)
|
||||
.firstOrNull;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
getCover(Album album) {
|
||||
if (coverFile == null) {
|
||||
try {
|
||||
// use the latest file as cover
|
||||
return AlbumStaticProvider.of(album)
|
||||
.items
|
||||
.whereType<AlbumFileItem>()
|
||||
.map((e) => e.file)
|
||||
.where((element) =>
|
||||
file_util.isSupportedFormat(element) &&
|
||||
(element.hasPreview ?? false))
|
||||
.sorted(compareFileDateTimeDescending)
|
||||
.first;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
FileDescriptor? getCover(Album album) {
|
||||
if (coverFile == null && album.provider is AlbumStaticProvider) {
|
||||
// use the latest file as cover
|
||||
return getCoverByItems(AlbumStaticProvider.of(album).items);
|
||||
} else {
|
||||
return coverFile;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
get props => [
|
||||
List<Object?> get props => [
|
||||
coverFile,
|
||||
];
|
||||
|
||||
@override
|
||||
_toContentJson() {
|
||||
JsonObj _toContentJson() {
|
||||
return {
|
||||
if (coverFile != null) "coverFile": coverFile!.toJson(),
|
||||
if (coverFile != null) "coverFile": coverFile!.toFdJson(),
|
||||
};
|
||||
}
|
||||
|
||||
final File? coverFile;
|
||||
final FileDescriptor? coverFile;
|
||||
|
||||
static const _type = "auto";
|
||||
}
|
||||
|
@ -122,48 +119,12 @@ class AlbumAutoCoverProvider extends AlbumCoverProvider {
|
|||
/// Cover picked by user
|
||||
@toString
|
||||
class AlbumManualCoverProvider extends AlbumCoverProvider {
|
||||
AlbumManualCoverProvider({
|
||||
const AlbumManualCoverProvider({
|
||||
required this.coverFile,
|
||||
});
|
||||
|
||||
factory AlbumManualCoverProvider.fromJson(JsonObj json) {
|
||||
return AlbumManualCoverProvider(
|
||||
coverFile: File.fromJson(json["coverFile"].cast<String, dynamic>()),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
getCover(Album album) => coverFile;
|
||||
|
||||
@override
|
||||
get props => [
|
||||
coverFile,
|
||||
];
|
||||
|
||||
@override
|
||||
_toContentJson() {
|
||||
return {
|
||||
"coverFile": coverFile.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
final File coverFile;
|
||||
|
||||
static const _type = "manual";
|
||||
}
|
||||
|
||||
/// Cover selected when building a Memory album
|
||||
@toString
|
||||
class AlbumMemoryCoverProvider extends AlbumCoverProvider {
|
||||
AlbumMemoryCoverProvider({
|
||||
required this.coverFile,
|
||||
});
|
||||
|
||||
factory AlbumMemoryCoverProvider.fromJson(JsonObj json) {
|
||||
return AlbumMemoryCoverProvider(
|
||||
coverFile:
|
||||
FileDescriptor.fromJson(json["coverFile"].cast<String, dynamic>()),
|
||||
);
|
||||
|
@ -173,19 +134,21 @@ class AlbumMemoryCoverProvider extends AlbumCoverProvider {
|
|||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
getCover(Album album) => coverFile;
|
||||
FileDescriptor? getCover(Album album) => coverFile;
|
||||
|
||||
@override
|
||||
get props => [
|
||||
List<Object?> get props => [
|
||||
coverFile,
|
||||
];
|
||||
|
||||
@override
|
||||
_toContentJson() => {
|
||||
"coverFile": coverFile.toJson(),
|
||||
};
|
||||
JsonObj _toContentJson() {
|
||||
return {
|
||||
"coverFile": coverFile.toFdJson(),
|
||||
};
|
||||
}
|
||||
|
||||
final FileDescriptor coverFile;
|
||||
|
||||
static const _type = "memory";
|
||||
static const _type = "manual";
|
||||
}
|
||||
|
|
|
@ -20,20 +20,13 @@ extension _$AlbumCoverProviderNpLog on AlbumCoverProvider {
|
|||
extension _$AlbumAutoCoverProviderToString on AlbumAutoCoverProvider {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "AlbumAutoCoverProvider {coverFile: ${coverFile == null ? null : "${coverFile!.path}"}}";
|
||||
return "AlbumAutoCoverProvider {coverFile: ${coverFile == null ? null : "${coverFile!.fdPath}"}}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$AlbumManualCoverProviderToString on AlbumManualCoverProvider {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "AlbumManualCoverProvider {coverFile: ${coverFile.path}}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$AlbumMemoryCoverProviderToString on AlbumMemoryCoverProvider {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "AlbumMemoryCoverProvider {coverFile: ${coverFile.fdPath}}";
|
||||
return "AlbumManualCoverProvider {coverFile: ${coverFile.fdPath}}";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,106 +1,76 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart' as sql;
|
||||
import 'package:kiwi/kiwi.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/album/upgrader.dart';
|
||||
import 'package:nc_photos/entity/album/data_source2.dart';
|
||||
import 'package:nc_photos/entity/album/repo2.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
|
||||
import 'package:nc_photos/entity/sqlite/type_converter.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/exception_event.dart';
|
||||
import 'package:nc_photos/future_util.dart' as future_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:nc_photos/use_case/get_file_binary.dart';
|
||||
import 'package:nc_photos/use_case/ls_single_file.dart';
|
||||
import 'package:nc_photos/use_case/put_file_binary.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
|
||||
part 'data_source.g.dart';
|
||||
|
||||
/// Backward compatibility only, use [AlbumRemoteDataSource2] instead
|
||||
@npLog
|
||||
class AlbumRemoteDataSource implements AlbumDataSource {
|
||||
@override
|
||||
get(Account account, File albumFile) async {
|
||||
_log.info("[get] ${albumFile.path}");
|
||||
const fileRepo = FileRepo(FileWebdavDataSource());
|
||||
final data = await GetFileBinary(fileRepo)(account, albumFile);
|
||||
try {
|
||||
return Album.fromJson(
|
||||
jsonDecode(utf8.decode(data)),
|
||||
upgraderFactory: DefaultAlbumUpgraderFactory(
|
||||
account: account,
|
||||
albumFile: albumFile,
|
||||
logFilePath: albumFile.path,
|
||||
),
|
||||
)!
|
||||
.copyWith(
|
||||
lastUpdated: OrNull(null),
|
||||
albumFile: OrNull(albumFile),
|
||||
);
|
||||
} catch (e, stacktrace) {
|
||||
dynamic d = data;
|
||||
try {
|
||||
d = utf8.decode(data);
|
||||
} catch (_) {}
|
||||
_log.severe("[get] Invalid json data: $d", e, stacktrace);
|
||||
throw const FormatException("Invalid album format");
|
||||
}
|
||||
final albums = await const AlbumRemoteDataSource2().getAlbums(
|
||||
account,
|
||||
[albumFile],
|
||||
onError: (_, error, stackTrace) {
|
||||
Error.throwWithStackTrace(error, stackTrace ?? StackTrace.current);
|
||||
},
|
||||
);
|
||||
return albums.first;
|
||||
}
|
||||
|
||||
@override
|
||||
getAll(Account account, List<File> albumFiles) async* {
|
||||
_log.info(
|
||||
"[getAll] ${albumFiles.map((f) => f.filename).toReadableString()}");
|
||||
final results = await future_util.waitOr(
|
||||
albumFiles.map((f) => get(account, f)),
|
||||
(error, stackTrace) => ExceptionEvent(error, stackTrace),
|
||||
final failed = <String, Map>{};
|
||||
final albums = await const AlbumRemoteDataSource2().getAlbums(
|
||||
account,
|
||||
albumFiles,
|
||||
onError: (v, error, stackTrace) {
|
||||
failed[v.path] = {
|
||||
"file": v,
|
||||
"error": error,
|
||||
"stackTrace": stackTrace,
|
||||
};
|
||||
},
|
||||
);
|
||||
for (final r in results) {
|
||||
yield r;
|
||||
var i = 0;
|
||||
for (final af in albumFiles) {
|
||||
final v = failed[af.path];
|
||||
if (v != null) {
|
||||
yield ExceptionEvent(v["error"], v["stackTrace"]);
|
||||
} else {
|
||||
yield albums[i++];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
create(Account account, Album album) async {
|
||||
_log.info("[create]");
|
||||
final fileName = _makeAlbumFileName();
|
||||
final filePath =
|
||||
"${remote_storage_util.getRemoteAlbumsDir(account)}/$fileName";
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
await PutFileBinary(c.fileRepo)(account, filePath,
|
||||
const Utf8Encoder().convert(jsonEncode(album.toRemoteJson())),
|
||||
shouldCreateMissingDir: true);
|
||||
// query album file
|
||||
final newFile = await LsSingleFile(c)(account, filePath);
|
||||
return album.copyWith(albumFile: OrNull(newFile));
|
||||
return const AlbumRemoteDataSource2().create(account, album);
|
||||
}
|
||||
|
||||
@override
|
||||
update(Account account, Album album) async {
|
||||
_log.info("[update] ${album.albumFile!.path}");
|
||||
const fileRepo = FileRepo(FileWebdavDataSource());
|
||||
await PutFileBinary(fileRepo)(account, album.albumFile!.path,
|
||||
const Utf8Encoder().convert(jsonEncode(album.toRemoteJson())));
|
||||
}
|
||||
|
||||
String _makeAlbumFileName() {
|
||||
// just make up something
|
||||
final timestamp = clock.now().millisecondsSinceEpoch;
|
||||
final random = Random().nextInt(0xFFFFFF);
|
||||
return "${timestamp.toRadixString(16)}-${random.toRadixString(16).padLeft(6, '0')}.nc_album.json";
|
||||
return const AlbumRemoteDataSource2().update(account, album);
|
||||
}
|
||||
}
|
||||
|
||||
/// Backward compatibility only, use [AlbumSqliteDbDataSource2] instead
|
||||
@npLog
|
||||
class AlbumSqliteDbDataSource implements AlbumDataSource {
|
||||
AlbumSqliteDbDataSource(this._c);
|
||||
|
@ -119,64 +89,29 @@ class AlbumSqliteDbDataSource implements AlbumDataSource {
|
|||
getAll(Account account, List<File> albumFiles) async* {
|
||||
_log.info(
|
||||
"[getAll] ${albumFiles.map((f) => f.filename).toReadableString()}");
|
||||
late final List<sql.CompleteFile> dbFiles;
|
||||
late final List<sql.AlbumWithShare> albumWithShares;
|
||||
await _c.sqliteDb.use((db) async {
|
||||
dbFiles = await db.completeFilesByFileIds(
|
||||
albumFiles.map((f) => f.fileId!),
|
||||
appAccount: account,
|
||||
);
|
||||
final query = db.select(db.albums).join([
|
||||
sql.leftOuterJoin(
|
||||
db.albumShares, db.albumShares.album.equalsExp(db.albums.rowId)),
|
||||
])
|
||||
..where(db.albums.file.isIn(dbFiles.map((f) => f.file.rowId)));
|
||||
albumWithShares = await query
|
||||
.map((r) => sql.AlbumWithShare(
|
||||
r.readTable(db.albums), r.readTableOrNull(db.albumShares)))
|
||||
.get();
|
||||
});
|
||||
|
||||
// group entries together
|
||||
final fileRowIdMap = <int, sql.CompleteFile>{};
|
||||
for (var f in dbFiles) {
|
||||
fileRowIdMap[f.file.rowId] = f;
|
||||
}
|
||||
final fileIdMap = <int, Map>{};
|
||||
for (final s in albumWithShares) {
|
||||
final f = fileRowIdMap[s.album.file];
|
||||
if (f == null) {
|
||||
_log.severe("[getAll] File missing for album (rowId: ${s.album.rowId}");
|
||||
final failed = <String, Map>{};
|
||||
final albums = await AlbumSqliteDbDataSource2(_c.sqliteDb).getAlbums(
|
||||
account,
|
||||
albumFiles,
|
||||
onError: (v, error, stackTrace) {
|
||||
failed[v.path] = {
|
||||
"file": v,
|
||||
"error": error,
|
||||
"stackTrace": stackTrace,
|
||||
};
|
||||
},
|
||||
);
|
||||
var i = 0;
|
||||
for (final af in albumFiles) {
|
||||
final v = failed[af.path];
|
||||
if (v != null) {
|
||||
if (v["error"] is CacheNotFoundException) {
|
||||
yield const CacheNotFoundException();
|
||||
} else {
|
||||
yield ExceptionEvent(v["error"], v["stackTrace"]);
|
||||
}
|
||||
} else {
|
||||
if (!fileIdMap.containsKey(f.file.fileId)) {
|
||||
fileIdMap[f.file.fileId] = {
|
||||
"file": f,
|
||||
"album": s.album,
|
||||
};
|
||||
}
|
||||
if (s.share != null) {
|
||||
(fileIdMap[f.file.fileId]!["shares"] ??= <sql.AlbumShare>[])
|
||||
.add(s.share!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort as the input list
|
||||
for (final item in albumFiles.map((f) => fileIdMap[f.fileId])) {
|
||||
if (item == null) {
|
||||
// cache not found
|
||||
yield CacheNotFoundException();
|
||||
} else {
|
||||
try {
|
||||
final f = SqliteFileConverter.fromSql(
|
||||
account.userId.toString(), item["file"]);
|
||||
yield SqliteAlbumConverter.fromSql(
|
||||
item["album"], f, item["shares"] ?? []);
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe(
|
||||
"[getAll] Failed while converting DB entry", e, stackTrace);
|
||||
yield ExceptionEvent(e, stackTrace);
|
||||
}
|
||||
yield albums[i++];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -184,64 +119,22 @@ class AlbumSqliteDbDataSource implements AlbumDataSource {
|
|||
@override
|
||||
create(Account account, Album album) async {
|
||||
_log.info("[create]");
|
||||
throw UnimplementedError();
|
||||
return AlbumSqliteDbDataSource2(_c.sqliteDb).create(account, album);
|
||||
}
|
||||
|
||||
@override
|
||||
update(Account account, Album album) async {
|
||||
_log.info("[update] ${album.albumFile!.path}");
|
||||
await _c.sqliteDb.use((db) async {
|
||||
final rowIds =
|
||||
await db.accountFileRowIdsOf(album.albumFile!, appAccount: account);
|
||||
final insert = SqliteAlbumConverter.toSql(
|
||||
album, rowIds.fileRowId, album.albumFile!.etag!);
|
||||
var rowId = await _updateCache(db, rowIds.fileRowId, insert.album);
|
||||
if (rowId == null) {
|
||||
// new album, need insert
|
||||
_log.info("[update] Insert new album");
|
||||
final insertedAlbum =
|
||||
await db.into(db.albums).insertReturning(insert.album);
|
||||
rowId = insertedAlbum.rowId;
|
||||
} else {
|
||||
await (db.delete(db.albumShares)..where((t) => t.album.equals(rowId)))
|
||||
.go();
|
||||
}
|
||||
if (insert.albumShares.isNotEmpty) {
|
||||
await db.batch((batch) {
|
||||
batch.insertAll(
|
||||
db.albumShares,
|
||||
insert.albumShares.map((s) => s.copyWith(album: sql.Value(rowId!))),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<int?> _updateCache(
|
||||
sql.SqliteDb db, int dbFileRowId, sql.AlbumsCompanion dbAlbum) async {
|
||||
final rowIdQuery = db.selectOnly(db.albums)
|
||||
..addColumns([db.albums.rowId])
|
||||
..where(db.albums.file.equals(dbFileRowId))
|
||||
..limit(1);
|
||||
final rowId =
|
||||
await rowIdQuery.map((r) => r.read(db.albums.rowId)!).getSingleOrNull();
|
||||
if (rowId == null) {
|
||||
// new album
|
||||
return null;
|
||||
}
|
||||
|
||||
await (db.update(db.albums)..where((t) => t.rowId.equals(rowId)))
|
||||
.write(dbAlbum);
|
||||
return rowId;
|
||||
return AlbumSqliteDbDataSource2(_c.sqliteDb).update(account, album);
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
}
|
||||
|
||||
/// Backward compatibility only, use [CachedAlbumRepo2] instead
|
||||
@npLog
|
||||
class AlbumCachedDataSource implements AlbumDataSource {
|
||||
AlbumCachedDataSource(DiContainer c)
|
||||
: _sqliteDbSrc = AlbumSqliteDbDataSource(c);
|
||||
AlbumCachedDataSource(DiContainer c) : sqliteDb = c.sqliteDb;
|
||||
|
||||
@override
|
||||
get(Account account, File albumFile) async {
|
||||
|
@ -251,58 +144,31 @@ class AlbumCachedDataSource implements AlbumDataSource {
|
|||
|
||||
@override
|
||||
getAll(Account account, List<File> albumFiles) async* {
|
||||
var i = 0;
|
||||
await for (final cache in _sqliteDbSrc.getAll(account, albumFiles)) {
|
||||
final albumFile = albumFiles[i++];
|
||||
if (_validateCache(cache, albumFile)) {
|
||||
yield cache;
|
||||
} else {
|
||||
// no cache
|
||||
final remote = await _remoteSrc.get(account, albumFile);
|
||||
await _cacheResult(account, remote);
|
||||
yield remote;
|
||||
}
|
||||
final repo = CachedAlbumRepo2(
|
||||
const AlbumRemoteDataSource2(),
|
||||
AlbumSqliteDbDataSource2(sqliteDb),
|
||||
);
|
||||
final albums = await repo.getAlbums(account, albumFiles).last;
|
||||
for (final a in albums) {
|
||||
yield a;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
update(Account account, Album album) async {
|
||||
await _remoteSrc.update(account, album);
|
||||
await _sqliteDbSrc.update(account, album);
|
||||
update(Account account, Album album) {
|
||||
return CachedAlbumRepo2(
|
||||
const AlbumRemoteDataSource2(),
|
||||
AlbumSqliteDbDataSource2(sqliteDb),
|
||||
).update(account, album);
|
||||
}
|
||||
|
||||
@override
|
||||
create(Account account, Album album) => _remoteSrc.create(account, album);
|
||||
|
||||
Future<void> _cacheResult(Account account, Album result) {
|
||||
return _sqliteDbSrc.update(account, result);
|
||||
create(Account account, Album album) {
|
||||
return CachedAlbumRepo2(
|
||||
const AlbumRemoteDataSource2(),
|
||||
AlbumSqliteDbDataSource2(sqliteDb),
|
||||
).create(account, album);
|
||||
}
|
||||
|
||||
bool _validateCache(dynamic cache, File albumFile) {
|
||||
if (cache is Album) {
|
||||
if (cache.albumFile!.etag?.isNotEmpty == true &&
|
||||
cache.albumFile!.etag == albumFile.etag) {
|
||||
// cache is good
|
||||
_log.fine("[_validateCache] etag matched for ${albumFile.path}");
|
||||
return true;
|
||||
} else {
|
||||
_log.info(
|
||||
"[_validateCache] Remote content updated for ${albumFile.path}");
|
||||
return false;
|
||||
}
|
||||
} else if (cache is CacheNotFoundException) {
|
||||
// normal when there's no cache
|
||||
return false;
|
||||
} else if (cache is ExceptionEvent) {
|
||||
_log.shout(
|
||||
"[_validateCache] Cache failure", cache.error, cache.stackTrace);
|
||||
return false;
|
||||
} else {
|
||||
_log.shout("[_validateCache] Unknown type: ${cache.runtimeType}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final _remoteSrc = AlbumRemoteDataSource();
|
||||
final AlbumSqliteDbDataSource _sqliteDbSrc;
|
||||
final sql.SqliteDb sqliteDb;
|
||||
}
|
||||
|
|
252
app/lib/entity/album/data_source2.dart
Normal file
252
app/lib/entity/album/data_source2.dart
Normal file
|
@ -0,0 +1,252 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart' as sql;
|
||||
import 'package:kiwi/kiwi.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/album/repo2.dart';
|
||||
import 'package:nc_photos/entity/album/upgrader.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
|
||||
import 'package:nc_photos/entity/sqlite/type_converter.dart' as sql;
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:nc_photos/use_case/get_file_binary.dart';
|
||||
import 'package:nc_photos/use_case/ls_single_file.dart';
|
||||
import 'package:nc_photos/use_case/put_file_binary.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
|
||||
part 'data_source2.g.dart';
|
||||
|
||||
@npLog
|
||||
class AlbumRemoteDataSource2 implements AlbumDataSource2 {
|
||||
const AlbumRemoteDataSource2();
|
||||
|
||||
@override
|
||||
Future<List<Album>> getAlbums(
|
||||
Account account,
|
||||
List<File> albumFiles, {
|
||||
ErrorWithValueHandler<File>? onError,
|
||||
}) async {
|
||||
final results = await Future.wait(albumFiles.map((f) async {
|
||||
try {
|
||||
return await _getSingle(account, f);
|
||||
} catch (e, stackTrace) {
|
||||
onError?.call(f, e, stackTrace);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
return results.whereNotNull().toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Album> create(Account account, Album album) async {
|
||||
_log.info("[create] ${album.name}");
|
||||
final fileName = _makeAlbumFileName();
|
||||
final filePath =
|
||||
"${remote_storage_util.getRemoteAlbumsDir(account)}/$fileName";
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
await PutFileBinary(c.fileRepo)(
|
||||
account,
|
||||
filePath,
|
||||
const Utf8Encoder().convert(jsonEncode(album.toRemoteJson())),
|
||||
shouldCreateMissingDir: true,
|
||||
);
|
||||
// query album file
|
||||
final newFile = await LsSingleFile(c)(account, filePath);
|
||||
return album.copyWith(albumFile: OrNull(newFile));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> update(Account account, Album album) async {
|
||||
_log.info("[update] ${album.albumFile!.path}");
|
||||
const fileRepo = FileRepo(FileWebdavDataSource());
|
||||
await PutFileBinary(fileRepo)(
|
||||
account,
|
||||
album.albumFile!.path,
|
||||
const Utf8Encoder().convert(jsonEncode(album.toRemoteJson())),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Album> _getSingle(Account account, File albumFile) async {
|
||||
_log.info("[_getSingle] Getting ${albumFile.path}");
|
||||
const fileRepo = FileRepo(FileWebdavDataSource());
|
||||
final data = await GetFileBinary(fileRepo)(account, albumFile);
|
||||
try {
|
||||
final album = Album.fromJson(
|
||||
jsonDecode(utf8.decode(data)),
|
||||
upgraderFactory: DefaultAlbumUpgraderFactory(
|
||||
account: account,
|
||||
albumFile: albumFile,
|
||||
logFilePath: albumFile.path,
|
||||
),
|
||||
);
|
||||
return album!.copyWith(
|
||||
lastUpdated: OrNull(null),
|
||||
albumFile: OrNull(albumFile),
|
||||
);
|
||||
} catch (e, stacktrace) {
|
||||
dynamic d = data;
|
||||
try {
|
||||
d = utf8.decode(data);
|
||||
} catch (_) {}
|
||||
_log.severe("[_getSingle] Invalid json data: $d", e, stacktrace);
|
||||
throw const FormatException("Invalid album format");
|
||||
}
|
||||
}
|
||||
|
||||
String _makeAlbumFileName() {
|
||||
// just make up something
|
||||
final timestamp = clock.now().millisecondsSinceEpoch;
|
||||
final random = Random().nextInt(0xFFFFFF);
|
||||
return "${timestamp.toRadixString(16)}-${random.toRadixString(16).padLeft(6, '0')}.nc_album.json";
|
||||
}
|
||||
}
|
||||
|
||||
@npLog
|
||||
class AlbumSqliteDbDataSource2 implements AlbumDataSource2 {
|
||||
const AlbumSqliteDbDataSource2(this.sqliteDb);
|
||||
|
||||
@override
|
||||
Future<List<Album>> getAlbums(
|
||||
Account account,
|
||||
List<File> albumFiles, {
|
||||
ErrorWithValueHandler<File>? onError,
|
||||
}) async {
|
||||
late final List<sql.CompleteFile> dbFiles;
|
||||
late final List<sql.AlbumWithShare> albumWithShares;
|
||||
await sqliteDb.use((db) async {
|
||||
dbFiles = await db.completeFilesByFileIds(
|
||||
albumFiles.map((f) => f.fileId!),
|
||||
appAccount: account,
|
||||
);
|
||||
final query = db.select(db.albums).join([
|
||||
sql.leftOuterJoin(
|
||||
db.albumShares, db.albumShares.album.equalsExp(db.albums.rowId)),
|
||||
])
|
||||
..where(db.albums.file.isIn(dbFiles.map((f) => f.file.rowId)));
|
||||
albumWithShares = await query
|
||||
.map((r) => sql.AlbumWithShare(
|
||||
r.readTable(db.albums), r.readTableOrNull(db.albumShares)))
|
||||
.get();
|
||||
});
|
||||
|
||||
// group entries together
|
||||
final fileRowIdMap = <int, sql.CompleteFile>{};
|
||||
for (var f in dbFiles) {
|
||||
fileRowIdMap[f.file.rowId] = f;
|
||||
}
|
||||
final fileIdMap = <int, Map>{};
|
||||
for (final s in albumWithShares) {
|
||||
final f = fileRowIdMap[s.album.file];
|
||||
if (f == null) {
|
||||
_log.severe(
|
||||
"[getAlbums] File missing for album (rowId: ${s.album.rowId}");
|
||||
} else {
|
||||
fileIdMap[f.file.fileId] ??= {
|
||||
"file": f,
|
||||
"album": s.album,
|
||||
};
|
||||
if (s.share != null) {
|
||||
(fileIdMap[f.file.fileId]!["shares"] ??= <sql.AlbumShare>[])
|
||||
.add(s.share!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort as the input list
|
||||
return albumFiles
|
||||
.map((f) {
|
||||
final item = fileIdMap[f.fileId];
|
||||
if (item == null) {
|
||||
// cache not found
|
||||
onError?.call(
|
||||
f, const CacheNotFoundException(), StackTrace.current);
|
||||
return null;
|
||||
} else {
|
||||
try {
|
||||
final queriedFile = sql.SqliteFileConverter.fromSql(
|
||||
account.userId.toString(), item["file"]);
|
||||
var dbAlbum = item["album"] as sql.Album;
|
||||
if (dbAlbum.version < 9) {
|
||||
dbAlbum = AlbumUpgraderV8(logFilePath: queriedFile.path)
|
||||
.doDb(dbAlbum)!;
|
||||
}
|
||||
return sql.SqliteAlbumConverter.fromSql(
|
||||
dbAlbum, queriedFile, item["shares"] ?? []);
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[getAlbums] Failed while converting DB entry", e,
|
||||
stackTrace);
|
||||
onError?.call(f, e, stackTrace);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
})
|
||||
.whereNotNull()
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Album> create(Account account, Album album) async {
|
||||
_log.info("[create] ${album.name}");
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> update(Account account, Album album) async {
|
||||
_log.info("[update] ${album.albumFile!.path}");
|
||||
await sqliteDb.use((db) async {
|
||||
final rowIds =
|
||||
await db.accountFileRowIdsOf(album.albumFile!, appAccount: account);
|
||||
final insert = sql.SqliteAlbumConverter.toSql(
|
||||
album, rowIds.fileRowId, album.albumFile!.etag!);
|
||||
var rowId = await _updateCache(db, rowIds.fileRowId, insert.album);
|
||||
if (rowId == null) {
|
||||
// new album, need insert
|
||||
_log.info("[update] Insert new album");
|
||||
final insertedAlbum =
|
||||
await db.into(db.albums).insertReturning(insert.album);
|
||||
rowId = insertedAlbum.rowId;
|
||||
} else {
|
||||
await (db.delete(db.albumShares)..where((t) => t.album.equals(rowId)))
|
||||
.go();
|
||||
}
|
||||
if (insert.albumShares.isNotEmpty) {
|
||||
await db.batch((batch) {
|
||||
batch.insertAll(
|
||||
db.albumShares,
|
||||
insert.albumShares.map((s) => s.copyWith(album: sql.Value(rowId!))),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<int?> _updateCache(
|
||||
sql.SqliteDb db, int dbFileRowId, sql.AlbumsCompanion dbAlbum) async {
|
||||
final rowIdQuery = db.selectOnly(db.albums)
|
||||
..addColumns([db.albums.rowId])
|
||||
..where(db.albums.file.equals(dbFileRowId))
|
||||
..limit(1);
|
||||
final rowId =
|
||||
await rowIdQuery.map((r) => r.read(db.albums.rowId)!).getSingleOrNull();
|
||||
if (rowId == null) {
|
||||
// new album
|
||||
return null;
|
||||
}
|
||||
|
||||
await (db.update(db.albums)..where((t) => t.rowId.equals(rowId)))
|
||||
.write(dbAlbum);
|
||||
return rowId;
|
||||
}
|
||||
|
||||
final sql.SqliteDb sqliteDb;
|
||||
}
|
22
app/lib/entity/album/data_source2.g.dart
Normal file
22
app/lib/entity/album/data_source2.g.dart
Normal file
|
@ -0,0 +1,22 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'data_source2.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$AlbumRemoteDataSource2NpLog on AlbumRemoteDataSource2 {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("entity.album.data_source2.AlbumRemoteDataSource2");
|
||||
}
|
||||
|
||||
extension _$AlbumSqliteDbDataSource2NpLog on AlbumSqliteDbDataSource2 {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log =
|
||||
Logger("entity.album.data_source2.AlbumSqliteDbDataSource2");
|
||||
}
|
|
@ -263,49 +263,3 @@ class AlbumTagProvider extends AlbumDynamicProvider {
|
|||
|
||||
static const _type = "tag";
|
||||
}
|
||||
|
||||
/// Smart albums are created only by the app and not the user
|
||||
abstract class AlbumSmartProvider extends AlbumProviderBase {
|
||||
AlbumSmartProvider({
|
||||
DateTime? latestItemTime,
|
||||
}) : super(latestItemTime: latestItemTime);
|
||||
|
||||
@override
|
||||
AlbumSmartProvider copyWith({
|
||||
OrNull<DateTime>? latestItemTime,
|
||||
}) {
|
||||
// Smart albums do not support copying
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
toContentJson() {
|
||||
// Smart albums do not support saving
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
/// Memory album is created based on dates
|
||||
@ToString(extraParams: r"{bool isDeep = false}")
|
||||
class AlbumMemoryProvider extends AlbumSmartProvider {
|
||||
AlbumMemoryProvider({
|
||||
required this.year,
|
||||
required this.month,
|
||||
required this.day,
|
||||
}) : super(latestItemTime: DateTime(year, month, day));
|
||||
|
||||
@override
|
||||
String toString({bool isDeep = false}) => _$toString(isDeep: isDeep);
|
||||
|
||||
@override
|
||||
get props => [
|
||||
...super.props,
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
];
|
||||
|
||||
final int year;
|
||||
final int month;
|
||||
final int day;
|
||||
}
|
||||
|
|
|
@ -37,10 +37,3 @@ extension _$AlbumTagProviderToString on AlbumTagProvider {
|
|||
return "AlbumTagProvider {latestItemTime: $latestItemTime, tags: ${tags.map((t) => t.displayName).toReadableString()}}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$AlbumMemoryProviderToString on AlbumMemoryProvider {
|
||||
String _$toString({bool isDeep = false}) {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "AlbumMemoryProvider {latestItemTime: $latestItemTime, year: $year, month: $month, day: $day}";
|
||||
}
|
||||
}
|
||||
|
|
152
app/lib/entity/album/repo2.dart
Normal file
152
app/lib/entity/album/repo2.dart
Normal file
|
@ -0,0 +1,152 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
|
||||
part 'repo2.g.dart';
|
||||
|
||||
abstract class AlbumRepo2 {
|
||||
/// Query all [Album]s defined by [albumFiles]
|
||||
Stream<List<Album>> getAlbums(
|
||||
Account account,
|
||||
List<File> albumFiles, {
|
||||
ErrorWithValueHandler<File>? onError,
|
||||
});
|
||||
|
||||
/// Create a new [album]
|
||||
Future<Album> create(Account account, Album album);
|
||||
|
||||
/// Update an [album]
|
||||
Future<void> update(Account account, Album album);
|
||||
}
|
||||
|
||||
class BasicAlbumRepo2 implements AlbumRepo2 {
|
||||
const BasicAlbumRepo2(this.dataSrc);
|
||||
|
||||
@override
|
||||
Stream<List<Album>> getAlbums(
|
||||
Account account,
|
||||
List<File> albumFiles, {
|
||||
ErrorWithValueHandler<File>? onError,
|
||||
}) async* {
|
||||
yield await dataSrc.getAlbums(account, albumFiles, onError: onError);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Album> create(Account account, Album album) =>
|
||||
dataSrc.create(account, album);
|
||||
|
||||
@override
|
||||
Future<void> update(Account account, Album album) =>
|
||||
dataSrc.update(account, album);
|
||||
|
||||
final AlbumDataSource2 dataSrc;
|
||||
}
|
||||
|
||||
@npLog
|
||||
class CachedAlbumRepo2 implements AlbumRepo2 {
|
||||
const CachedAlbumRepo2(this.remoteDataSrc, this.cacheDataSrc);
|
||||
|
||||
@override
|
||||
Stream<List<Album>> getAlbums(
|
||||
Account account,
|
||||
List<File> albumFiles, {
|
||||
ErrorWithValueHandler<File>? onError,
|
||||
}) async* {
|
||||
// get cache
|
||||
final cached = <Album>[];
|
||||
final failed = <File>[];
|
||||
try {
|
||||
cached.addAll(await cacheDataSrc.getAlbums(
|
||||
account,
|
||||
albumFiles,
|
||||
onError: (f, e, stackTrace) {
|
||||
failed.add(f);
|
||||
if (e is CacheNotFoundException) {
|
||||
// not in cache, normal
|
||||
} else {
|
||||
_log.shout("[getAlbums] Cache failure", e, stackTrace);
|
||||
}
|
||||
},
|
||||
));
|
||||
yield cached;
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[getAlbums] Failed while getAlbums", e, stackTrace);
|
||||
}
|
||||
final cachedGroup = cached.groupListsBy((c) {
|
||||
try {
|
||||
return _validateCache(
|
||||
c, albumFiles.firstWhere(c.albumFile!.compareServerIdentity));
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// query remote
|
||||
final outdated = [
|
||||
...failed,
|
||||
...cachedGroup[false]?.map((e) =>
|
||||
albumFiles.firstWhere(e.albumFile!.compareServerIdentity)) ??
|
||||
const <File>[],
|
||||
];
|
||||
final remote =
|
||||
await remoteDataSrc.getAlbums(account, outdated, onError: onError);
|
||||
yield (cachedGroup[true] ?? []) + remote;
|
||||
|
||||
// update cache
|
||||
for (final a in remote) {
|
||||
unawaited(cacheDataSrc.update(account, a));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Album> create(Account account, Album album) =>
|
||||
remoteDataSrc.create(account, album);
|
||||
|
||||
@override
|
||||
Future<void> update(Account account, Album album) async {
|
||||
await remoteDataSrc.update(account, album);
|
||||
try {
|
||||
await cacheDataSrc.update(account, album);
|
||||
} catch (e, stackTrace) {
|
||||
_log.warning("[update] Failed to update cache", e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if the cached album is considered up to date
|
||||
bool _validateCache(Album cache, File albumFile) {
|
||||
if (cache.albumFile!.etag?.isNotEmpty == true &&
|
||||
cache.albumFile!.etag == albumFile.etag) {
|
||||
// cache is good
|
||||
_log.fine("[_validateCache] etag matched for ${albumFile.path}");
|
||||
return true;
|
||||
} else {
|
||||
_log.info(
|
||||
"[_validateCache] Remote content updated for ${albumFile.path}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final AlbumDataSource2 remoteDataSrc;
|
||||
final AlbumDataSource2 cacheDataSrc;
|
||||
}
|
||||
|
||||
abstract class AlbumDataSource2 {
|
||||
/// Query all [Album]s defined by [albumFiles]
|
||||
Future<List<Album>> getAlbums(
|
||||
Account account,
|
||||
List<File> albumFiles, {
|
||||
ErrorWithValueHandler<File>? onError,
|
||||
});
|
||||
|
||||
Future<Album> create(Account account, Album album);
|
||||
|
||||
Future<void> update(Account account, Album album);
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'list_favorite.dart';
|
||||
part of 'repo2.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListFavoriteNpLog on ListFavorite {
|
||||
extension _$CachedAlbumRepo2NpLog on CachedAlbumRepo2 {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("use_case.list_favorite.ListFavorite");
|
||||
static final log = Logger("entity.album.repo2.CachedAlbumRepo2");
|
||||
}
|
|
@ -1,14 +1,12 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/entity/album/item.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/entity/collection_item/album_item_adapter.dart';
|
||||
import 'package:nc_photos/entity/collection_item/sorter.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
part 'sort_provider.g.dart';
|
||||
|
||||
|
@ -33,6 +31,22 @@ abstract class AlbumSortProvider with EquatableMixin {
|
|||
}
|
||||
}
|
||||
|
||||
factory AlbumSortProvider.fromCollectionItemSort(
|
||||
CollectionItemSort itemSort) {
|
||||
switch (itemSort) {
|
||||
case CollectionItemSort.manual:
|
||||
return const AlbumNullSortProvider();
|
||||
case CollectionItemSort.dateAscending:
|
||||
return const AlbumTimeSortProvider(isAscending: true);
|
||||
case CollectionItemSort.dateDescending:
|
||||
return const AlbumTimeSortProvider(isAscending: false);
|
||||
case CollectionItemSort.nameAscending:
|
||||
return const AlbumFilenameSortProvider(isAscending: true);
|
||||
case CollectionItemSort.nameDescending:
|
||||
return const AlbumFilenameSortProvider(isAscending: false);
|
||||
}
|
||||
}
|
||||
|
||||
JsonObj toJson() {
|
||||
String getType() {
|
||||
if (this is AlbumNullSortProvider) {
|
||||
|
@ -53,7 +67,31 @@ abstract class AlbumSortProvider with EquatableMixin {
|
|||
}
|
||||
|
||||
/// Return a sorted copy of [items]
|
||||
List<AlbumItem> sort(List<AlbumItem> items);
|
||||
List<AlbumItem> sort(List<AlbumItem> items) {
|
||||
final type = toCollectionItemSort();
|
||||
final sorter = CollectionSorter.fromSortType(type);
|
||||
return sorter(items.map(AlbumAdaptedCollectionItem.fromItem).toList())
|
||||
.whereType<AlbumAdaptedCollectionItem>()
|
||||
.map((e) => e.albumItem)
|
||||
.toList();
|
||||
}
|
||||
|
||||
CollectionItemSort toCollectionItemSort() {
|
||||
final that = this;
|
||||
if (that is AlbumNullSortProvider) {
|
||||
return CollectionItemSort.manual;
|
||||
} else if (that is AlbumTimeSortProvider) {
|
||||
return that.isAscending
|
||||
? CollectionItemSort.dateAscending
|
||||
: CollectionItemSort.dateDescending;
|
||||
} else if (that is AlbumFilenameSortProvider) {
|
||||
return that.isAscending
|
||||
? CollectionItemSort.nameAscending
|
||||
: CollectionItemSort.nameDescending;
|
||||
} else {
|
||||
throw StateError("Unknown type: ${sort.runtimeType}");
|
||||
}
|
||||
}
|
||||
|
||||
JsonObj _toContentJson();
|
||||
|
||||
|
@ -72,11 +110,6 @@ class AlbumNullSortProvider extends AlbumSortProvider {
|
|||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
sort(List<AlbumItem> items) {
|
||||
return List.from(items);
|
||||
}
|
||||
|
||||
@override
|
||||
get props => [];
|
||||
|
||||
|
@ -124,37 +157,6 @@ class AlbumTimeSortProvider extends AlbumReversibleSortProvider {
|
|||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
sort(List<AlbumItem> items) {
|
||||
DateTime? prevFileTime;
|
||||
return items
|
||||
.map((e) {
|
||||
if (e is AlbumFileItem) {
|
||||
// take the file time
|
||||
prevFileTime = e.file.bestDateTime;
|
||||
}
|
||||
// for non file items, use the sibling file's time
|
||||
return Tuple2(prevFileTime, e);
|
||||
})
|
||||
.stableSorted((x, y) {
|
||||
if (x.item1 == null && y.item1 == null) {
|
||||
return 0;
|
||||
} else if (x.item1 == null) {
|
||||
return -1;
|
||||
} else if (y.item1 == null) {
|
||||
return 1;
|
||||
} else {
|
||||
if (isAscending) {
|
||||
return x.item1!.compareTo(y.item1!);
|
||||
} else {
|
||||
return y.item1!.compareTo(x.item1!);
|
||||
}
|
||||
}
|
||||
})
|
||||
.map((e) => e.item2)
|
||||
.toList();
|
||||
}
|
||||
|
||||
static const _type = "time";
|
||||
}
|
||||
|
||||
|
@ -174,36 +176,5 @@ class AlbumFilenameSortProvider extends AlbumReversibleSortProvider {
|
|||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
sort(List<AlbumItem> items) {
|
||||
String? prevFilename;
|
||||
return items
|
||||
.map((e) {
|
||||
if (e is AlbumFileItem) {
|
||||
// take the file name
|
||||
prevFilename = e.file.filename;
|
||||
}
|
||||
// for non file items, use the sibling file's name
|
||||
return Tuple2(prevFilename, e);
|
||||
})
|
||||
.stableSorted((x, y) {
|
||||
if (x.item1 == null && y.item1 == null) {
|
||||
return 0;
|
||||
} else if (x.item1 == null) {
|
||||
return -1;
|
||||
} else if (y.item1 == null) {
|
||||
return 1;
|
||||
} else {
|
||||
if (isAscending) {
|
||||
return compareNatural(x.item1!, y.item1!);
|
||||
} else {
|
||||
return compareNatural(y.item1!, x.item1!);
|
||||
}
|
||||
}
|
||||
})
|
||||
.map((e) => e.item2)
|
||||
.toList();
|
||||
}
|
||||
|
||||
static const _type = "filename";
|
||||
}
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/entity/exif.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_common/ci_string.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
|
@ -11,7 +16,8 @@ import 'package:tuple/tuple.dart';
|
|||
part 'upgrader.g.dart';
|
||||
|
||||
abstract class AlbumUpgrader {
|
||||
JsonObj? call(JsonObj json);
|
||||
JsonObj? doJson(JsonObj json);
|
||||
sql.Album? doDb(sql.Album dbObj);
|
||||
}
|
||||
|
||||
/// Upgrade v1 Album to v2
|
||||
|
@ -22,14 +28,17 @@ class AlbumUpgraderV1 implements AlbumUpgrader {
|
|||
});
|
||||
|
||||
@override
|
||||
call(JsonObj json) {
|
||||
doJson(JsonObj json) {
|
||||
// v1 album items are corrupted in one of the updates, drop it
|
||||
_log.fine("[call] Upgrade v1 Album for file: $logFilePath");
|
||||
_log.fine("[doJson] Upgrade v1 Album for file: $logFilePath");
|
||||
final result = JsonObj.from(json);
|
||||
result["items"] = [];
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
sql.Album? doDb(sql.Album dbObj) => null;
|
||||
|
||||
/// File path for logging only
|
||||
final String? logFilePath;
|
||||
}
|
||||
|
@ -42,9 +51,9 @@ class AlbumUpgraderV2 implements AlbumUpgrader {
|
|||
});
|
||||
|
||||
@override
|
||||
call(JsonObj json) {
|
||||
doJson(JsonObj json) {
|
||||
// move v2 items to v3 provider
|
||||
_log.fine("[call] Upgrade v2 Album for file: $logFilePath");
|
||||
_log.fine("[doJson] Upgrade v2 Album for file: $logFilePath");
|
||||
final result = JsonObj.from(json);
|
||||
result["provider"] = <String, dynamic>{
|
||||
"type": "static",
|
||||
|
@ -62,6 +71,9 @@ class AlbumUpgraderV2 implements AlbumUpgrader {
|
|||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
sql.Album? doDb(sql.Album dbObj) => null;
|
||||
|
||||
/// File path for logging only
|
||||
final String? logFilePath;
|
||||
}
|
||||
|
@ -74,9 +86,9 @@ class AlbumUpgraderV3 implements AlbumUpgrader {
|
|||
});
|
||||
|
||||
@override
|
||||
call(JsonObj json) {
|
||||
doJson(JsonObj json) {
|
||||
// move v3 items to v4 provider
|
||||
_log.fine("[call] Upgrade v3 Album for file: $logFilePath");
|
||||
_log.fine("[doJson] Upgrade v3 Album for file: $logFilePath");
|
||||
final result = JsonObj.from(json);
|
||||
// add the descending time sort provider
|
||||
result["sortProvider"] = <String, dynamic>{
|
||||
|
@ -88,6 +100,9 @@ class AlbumUpgraderV3 implements AlbumUpgrader {
|
|||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
sql.Album? doDb(sql.Album dbObj) => null;
|
||||
|
||||
/// File path for logging only
|
||||
final String? logFilePath;
|
||||
}
|
||||
|
@ -100,8 +115,8 @@ class AlbumUpgraderV4 implements AlbumUpgrader {
|
|||
});
|
||||
|
||||
@override
|
||||
call(JsonObj json) {
|
||||
_log.fine("[call] Upgrade v4 Album for file: $logFilePath");
|
||||
doJson(JsonObj json) {
|
||||
_log.fine("[doJson] Upgrade v4 Album for file: $logFilePath");
|
||||
final result = JsonObj.from(json);
|
||||
try {
|
||||
if (result["provider"]["type"] != "static") {
|
||||
|
@ -151,11 +166,14 @@ class AlbumUpgraderV4 implements AlbumUpgrader {
|
|||
} catch (e, stackTrace) {
|
||||
// this upgrade is not a must, if it failed then just leave it and it'll
|
||||
// be upgraded the next time the album is saved
|
||||
_log.shout("[call] Failed while upgrade", e, stackTrace);
|
||||
_log.shout("[doJson] Failed while upgrade", e, stackTrace);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
sql.Album? doDb(sql.Album dbObj) => null;
|
||||
|
||||
/// File path for logging only
|
||||
final String? logFilePath;
|
||||
}
|
||||
|
@ -170,8 +188,8 @@ class AlbumUpgraderV5 implements AlbumUpgrader {
|
|||
});
|
||||
|
||||
@override
|
||||
call(JsonObj json) {
|
||||
_log.fine("[call] Upgrade v5 Album for file: $logFilePath");
|
||||
doJson(JsonObj json) {
|
||||
_log.fine("[doJson] Upgrade v5 Album for file: $logFilePath");
|
||||
final result = JsonObj.from(json);
|
||||
try {
|
||||
if (result["provider"]["type"] != "static") {
|
||||
|
@ -192,11 +210,14 @@ class AlbumUpgraderV5 implements AlbumUpgrader {
|
|||
} catch (e, stackTrace) {
|
||||
// this upgrade is not a must, if it failed then just leave it and it'll
|
||||
// be upgraded the next time the album is saved
|
||||
_log.shout("[call] Failed while upgrade", e, stackTrace);
|
||||
_log.shout("[doJson] Failed while upgrade", e, stackTrace);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
sql.Album? doDb(sql.Album dbObj) => null;
|
||||
|
||||
final Account account;
|
||||
final File? albumFile;
|
||||
|
||||
|
@ -212,11 +233,14 @@ class AlbumUpgraderV6 implements AlbumUpgrader {
|
|||
});
|
||||
|
||||
@override
|
||||
call(JsonObj json) {
|
||||
_log.fine("[call] Upgrade v6 Album for file: $logFilePath");
|
||||
doJson(JsonObj json) {
|
||||
_log.fine("[doJson] Upgrade v6 Album for file: $logFilePath");
|
||||
return json;
|
||||
}
|
||||
|
||||
@override
|
||||
sql.Album? doDb(sql.Album dbObj) => null;
|
||||
|
||||
/// File path for logging only
|
||||
final String? logFilePath;
|
||||
}
|
||||
|
@ -229,11 +253,103 @@ class AlbumUpgraderV7 implements AlbumUpgrader {
|
|||
});
|
||||
|
||||
@override
|
||||
call(JsonObj json) {
|
||||
_log.fine("[call] Upgrade v7 Album for file: $logFilePath");
|
||||
doJson(JsonObj json) {
|
||||
_log.fine("[doJson] Upgrade v7 Album for file: $logFilePath");
|
||||
return json;
|
||||
}
|
||||
|
||||
@override
|
||||
sql.Album? doDb(sql.Album dbObj) => null;
|
||||
|
||||
/// File path for logging only
|
||||
final String? logFilePath;
|
||||
}
|
||||
|
||||
/// Upgrade v8 Album to v9
|
||||
@npLog
|
||||
class AlbumUpgraderV8 implements AlbumUpgrader {
|
||||
const AlbumUpgraderV8({
|
||||
this.logFilePath,
|
||||
});
|
||||
|
||||
@override
|
||||
JsonObj? doJson(JsonObj json) {
|
||||
_log.fine("[doJson] Upgrade v8 Album for file: $logFilePath");
|
||||
final result = JsonObj.from(json);
|
||||
if (result["coverProvider"]["type"] == "manual") {
|
||||
final content = (result["coverProvider"]["content"]["coverFile"] as Map)
|
||||
.cast<String, dynamic>();
|
||||
final fd = _fileJsonToFileDescriptorJson(content);
|
||||
// some very old album file may contain files w/o id
|
||||
if (fd["fdId"] != null) {
|
||||
result["coverProvider"]["content"]["coverFile"] = fd;
|
||||
} else {
|
||||
result["coverProvider"]["content"] = {};
|
||||
}
|
||||
} else if (result["coverProvider"]["type"] == "auto") {
|
||||
final content = (result["coverProvider"]["content"]["coverFile"] as Map?)
|
||||
?.cast<String, dynamic>();
|
||||
if (content != null) {
|
||||
final fd = _fileJsonToFileDescriptorJson(content);
|
||||
if (fd["fdId"] != null) {
|
||||
result["coverProvider"]["content"]["coverFile"] = fd;
|
||||
} else {
|
||||
result["coverProvider"]["content"] = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
sql.Album? doDb(sql.Album dbObj) {
|
||||
_log.fine("[doDb] Upgrade v8 Album for file: $logFilePath");
|
||||
if (dbObj.coverProviderType == "manual") {
|
||||
final content = (jsonDecode(dbObj.coverProviderContent) as Map)
|
||||
.cast<String, dynamic>();
|
||||
final converted = _fileJsonToFileDescriptorJson(
|
||||
(content["coverFile"] as Map).cast<String, dynamic>());
|
||||
if (converted["fdId"] != null) {
|
||||
return dbObj.copyWith(
|
||||
coverProviderContent: jsonEncode({"coverFile": converted}),
|
||||
);
|
||||
} else {
|
||||
return dbObj.copyWith(coverProviderContent: "{}");
|
||||
}
|
||||
} else if (dbObj.coverProviderType == "auto") {
|
||||
final content = (jsonDecode(dbObj.coverProviderContent) as Map)
|
||||
.cast<String, dynamic>();
|
||||
if (content["coverFile"] != null) {
|
||||
final converted = _fileJsonToFileDescriptorJson(
|
||||
(content["coverFile"] as Map).cast<String, dynamic>());
|
||||
if (converted["fdId"] != null) {
|
||||
return dbObj.copyWith(
|
||||
coverProviderContent: jsonEncode({"coverFile": converted}),
|
||||
);
|
||||
} else {
|
||||
return dbObj.copyWith(coverProviderContent: "{}");
|
||||
}
|
||||
}
|
||||
}
|
||||
return dbObj;
|
||||
}
|
||||
|
||||
static JsonObj _fileJsonToFileDescriptorJson(JsonObj json) {
|
||||
return {
|
||||
"fdPath": json["path"],
|
||||
"fdId": json["fileId"],
|
||||
"fdMime": json["contentType"],
|
||||
"fdIsArchived": json["isArchived"] ?? false,
|
||||
// File.isFavorite is serialized as int
|
||||
"fdIsFavorite": json["isFavorite"] == 1,
|
||||
"fdDateTime": json["overrideDateTime"] ??
|
||||
(json["metadata"]?["exif"]?["DateTimeOriginal"] as String?)?.run(
|
||||
(d) => Exif.dateTimeFormat.parse(d).toUtc().toIso8601String()) ??
|
||||
json["lastModified"] ??
|
||||
clock.now().toUtc().toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// File path for logging only
|
||||
final String? logFilePath;
|
||||
}
|
||||
|
@ -248,6 +364,7 @@ abstract class AlbumUpgraderFactory {
|
|||
AlbumUpgraderV5? buildV5();
|
||||
AlbumUpgraderV6? buildV6();
|
||||
AlbumUpgraderV7? buildV7();
|
||||
AlbumUpgraderV8? buildV8();
|
||||
}
|
||||
|
||||
class DefaultAlbumUpgraderFactory extends AlbumUpgraderFactory {
|
||||
|
@ -282,6 +399,9 @@ class DefaultAlbumUpgraderFactory extends AlbumUpgraderFactory {
|
|||
@override
|
||||
buildV7() => AlbumUpgraderV7(logFilePath: logFilePath);
|
||||
|
||||
@override
|
||||
AlbumUpgraderV8? buildV8() => AlbumUpgraderV8(logFilePath: logFilePath);
|
||||
|
||||
final Account account;
|
||||
final File? albumFile;
|
||||
|
||||
|
|
|
@ -54,3 +54,10 @@ extension _$AlbumUpgraderV7NpLog on AlbumUpgraderV7 {
|
|||
|
||||
static final log = Logger("entity.album.upgrader.AlbumUpgraderV7");
|
||||
}
|
||||
|
||||
extension _$AlbumUpgraderV8NpLog on AlbumUpgraderV8 {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("entity.album.upgrader.AlbumUpgraderV8");
|
||||
}
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
enum AlbumSort {
|
||||
dateDescending,
|
||||
dateAscending,
|
||||
nameAscending,
|
||||
nameDescending,
|
||||
}
|
||||
|
||||
List<Album> sorted(List<Album> albums, AlbumSort by) {
|
||||
final isAscending = _isSortAscending(by);
|
||||
return albums
|
||||
.map<Tuple2<dynamic, Album>>((e) {
|
||||
switch (by) {
|
||||
case AlbumSort.nameAscending:
|
||||
case AlbumSort.nameDescending:
|
||||
return Tuple2(e.name, e);
|
||||
|
||||
case AlbumSort.dateAscending:
|
||||
case AlbumSort.dateDescending:
|
||||
return Tuple2(e.provider.latestItemTime ?? e.lastUpdated, e);
|
||||
}
|
||||
})
|
||||
.sorted((a, b) {
|
||||
final x = isAscending ? a : b;
|
||||
final y = isAscending ? b : a;
|
||||
final tmp = x.item1.compareTo(y.item1);
|
||||
if (tmp != 0) {
|
||||
return tmp;
|
||||
} else {
|
||||
return x.item2.name.compareTo(y.item2.name);
|
||||
}
|
||||
})
|
||||
.map((e) => e.item2)
|
||||
.toList();
|
||||
}
|
||||
|
||||
bool _isSortAscending(AlbumSort sort) =>
|
||||
sort == AlbumSort.dateAscending || sort == AlbumSort.nameAscending;
|
148
app/lib/entity/collection.dart
Normal file
148
app/lib/entity/collection.dart
Normal file
|
@ -0,0 +1,148 @@
|
|||
import 'package:copy_with/copy_with.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:nc_photos/entity/collection/util.dart';
|
||||
import 'package:nc_photos/entity/collection_item/sorter.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'collection.g.dart';
|
||||
|
||||
/// Describe a group of items
|
||||
@genCopyWith
|
||||
@toString
|
||||
class Collection with EquatableMixin {
|
||||
const Collection({
|
||||
required this.name,
|
||||
required this.contentProvider,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
bool compareIdentity(Collection other) => other.id == id;
|
||||
|
||||
int get identityHashCode => id.hashCode;
|
||||
|
||||
/// A unique id for each collection. The value is divided into two parts in
|
||||
/// the format XXXX-YYY...YYY, where XXXX is a four-character code
|
||||
/// representing the content provider type, and YYY is an implementation
|
||||
/// detail of each providers
|
||||
String get id => "${contentProvider.fourCc}-${contentProvider.id}";
|
||||
|
||||
/// See [CollectionContentProvider.count]
|
||||
int? get count => contentProvider.count;
|
||||
|
||||
/// See [CollectionContentProvider.lastModified]
|
||||
DateTime get lastModified => contentProvider.lastModified;
|
||||
|
||||
/// See [CollectionContentProvider.capabilities]
|
||||
List<CollectionCapability> get capabilities => contentProvider.capabilities;
|
||||
|
||||
/// See [CollectionContentProvider.itemSort]
|
||||
CollectionItemSort get itemSort => contentProvider.itemSort;
|
||||
|
||||
/// See [CollectionContentProvider.shares]
|
||||
List<CollectionShare> get shares => contentProvider.shares;
|
||||
|
||||
/// See [CollectionContentProvider.getCoverUrl]
|
||||
String? getCoverUrl(
|
||||
int width,
|
||||
int height, {
|
||||
bool? isKeepAspectRatio,
|
||||
}) =>
|
||||
contentProvider.getCoverUrl(
|
||||
width,
|
||||
height,
|
||||
isKeepAspectRatio: isKeepAspectRatio,
|
||||
);
|
||||
|
||||
CollectionSorter getSorter() => CollectionSorter.fromSortType(itemSort);
|
||||
|
||||
/// See [CollectionContentProvider.isDynamicCollection]
|
||||
bool get isDynamicCollection => contentProvider.isDynamicCollection;
|
||||
|
||||
/// See [CollectionContentProvider.isPendingSharedAlbum]
|
||||
bool get isPendingSharedAlbum => contentProvider.isPendingSharedAlbum;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
name,
|
||||
contentProvider,
|
||||
];
|
||||
|
||||
final String name;
|
||||
final CollectionContentProvider contentProvider;
|
||||
}
|
||||
|
||||
enum CollectionCapability {
|
||||
// add/remove items
|
||||
manualItem,
|
||||
// sort the items
|
||||
sort,
|
||||
// rearrange item manually
|
||||
manualSort,
|
||||
// can freely rename album
|
||||
rename,
|
||||
// text labels
|
||||
labelItem,
|
||||
// set the cover image
|
||||
manualCover,
|
||||
// share the collection with other user on the same server
|
||||
share,
|
||||
}
|
||||
|
||||
/// Provide the actual content of a collection
|
||||
abstract class CollectionContentProvider with EquatableMixin {
|
||||
const CollectionContentProvider();
|
||||
|
||||
/// Unique FourCC of this provider type
|
||||
String get fourCc;
|
||||
|
||||
/// Return the unique id of this collection
|
||||
String get id;
|
||||
|
||||
/// Return the number of items in this collection, or null if not supported
|
||||
int? get count;
|
||||
|
||||
/// Return the date time of this collection. Generally this is the date time
|
||||
/// of the latest child
|
||||
DateTime get lastModified;
|
||||
|
||||
/// Return the capabilities of the collection
|
||||
///
|
||||
/// Notice that the capabilities returned here represent all the capabilities
|
||||
/// that this implementation supports. In practice there may be extra runtime
|
||||
/// requirements that mask some of them (e.g., user permissions)
|
||||
List<CollectionCapability> get capabilities;
|
||||
|
||||
/// Return the sort type
|
||||
CollectionItemSort get itemSort;
|
||||
|
||||
/// Return list of users who have access to this collection, excluding the
|
||||
/// current user
|
||||
List<CollectionShare> get shares;
|
||||
|
||||
/// Return the URL of the cover image if available
|
||||
///
|
||||
/// The [width] and [height] are provided as a hint only, implementations are
|
||||
/// free to ignore them if it's not supported
|
||||
///
|
||||
/// [isKeepAspectRatio] is only a hint and implementations may ignore it
|
||||
String? getCoverUrl(
|
||||
int width,
|
||||
int height, {
|
||||
bool? isKeepAspectRatio,
|
||||
});
|
||||
|
||||
/// Return whether this is a dynamic collection
|
||||
///
|
||||
/// A collection is defined as a dynamic one when the items are not specified
|
||||
/// explicitly by the user, but rather derived from some conditions
|
||||
bool get isDynamicCollection;
|
||||
|
||||
/// Return whether this is a shared album pending to be added
|
||||
///
|
||||
/// In some implementation, shared album does not immediately get added to the
|
||||
/// collections list
|
||||
bool get isPendingSharedAlbum;
|
||||
}
|
48
app/lib/entity/collection.g.dart
Normal file
48
app/lib/entity/collection.g.dart
Normal file
|
@ -0,0 +1,48 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'collection.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithLintRuleGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class $CollectionCopyWithWorker {
|
||||
Collection call({String? name, CollectionContentProvider? contentProvider});
|
||||
}
|
||||
|
||||
class _$CollectionCopyWithWorkerImpl implements $CollectionCopyWithWorker {
|
||||
_$CollectionCopyWithWorkerImpl(this.that);
|
||||
|
||||
@override
|
||||
Collection call({dynamic name, dynamic contentProvider}) {
|
||||
return Collection(
|
||||
name: name as String? ?? that.name,
|
||||
contentProvider: contentProvider as CollectionContentProvider? ??
|
||||
that.contentProvider);
|
||||
}
|
||||
|
||||
final Collection that;
|
||||
}
|
||||
|
||||
extension $CollectionCopyWith on Collection {
|
||||
$CollectionCopyWithWorker get copyWith => _$copyWith;
|
||||
$CollectionCopyWithWorker get _$copyWith =>
|
||||
_$CollectionCopyWithWorkerImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$CollectionToString on Collection {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "Collection {name: $name, contentProvider: $contentProvider}";
|
||||
}
|
||||
}
|
115
app/lib/entity/collection/adapter.dart
Normal file
115
app/lib/entity/collection/adapter.dart
Normal file
|
@ -0,0 +1,115 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter/album.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter/location_group.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter/memory.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter/nc_album.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter/person.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter/tag.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/album.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/location_group.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/memory.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/nc_album.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/person.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/tag.dart';
|
||||
import 'package:nc_photos/entity/collection/util.dart';
|
||||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/new_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/sharee.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:np_common/ci_string.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
|
||||
abstract class CollectionAdapter {
|
||||
const CollectionAdapter();
|
||||
|
||||
static CollectionAdapter of(
|
||||
DiContainer c, Account account, Collection collection) {
|
||||
switch (collection.contentProvider.runtimeType) {
|
||||
case CollectionAlbumProvider:
|
||||
return CollectionAlbumAdapter(c, account, collection);
|
||||
case CollectionLocationGroupProvider:
|
||||
return CollectionLocationGroupAdapter(c, account, collection);
|
||||
case CollectionMemoryProvider:
|
||||
return CollectionMemoryAdapter(c, account, collection);
|
||||
case CollectionNcAlbumProvider:
|
||||
return CollectionNcAlbumAdapter(c, account, collection);
|
||||
case CollectionPersonProvider:
|
||||
return CollectionPersonAdapter(c, account, collection);
|
||||
case CollectionTagProvider:
|
||||
return CollectionTagAdapter(c, account, collection);
|
||||
default:
|
||||
throw UnsupportedError(
|
||||
"Unknown type: ${collection.contentProvider.runtimeType}");
|
||||
}
|
||||
}
|
||||
|
||||
/// List items inside this collection
|
||||
Stream<List<CollectionItem>> listItem();
|
||||
|
||||
/// Add [files] to this collection and return the added count
|
||||
Future<int> addFiles(
|
||||
List<FileDescriptor> files, {
|
||||
ErrorWithValueHandler<FileDescriptor>? onError,
|
||||
required ValueChanged<Collection> onCollectionUpdated,
|
||||
});
|
||||
|
||||
/// Edit this collection
|
||||
Future<Collection> edit({
|
||||
String? name,
|
||||
List<CollectionItem>? items,
|
||||
CollectionItemSort? itemSort,
|
||||
OrNull<FileDescriptor>? cover,
|
||||
List<CollectionItem>? knownItems,
|
||||
});
|
||||
|
||||
/// Remove [items] from this collection and return the removed count
|
||||
Future<int> removeItems(
|
||||
List<CollectionItem> items, {
|
||||
ErrorWithValueIndexedHandler<CollectionItem>? onError,
|
||||
required ValueChanged<Collection> onCollectionUpdated,
|
||||
});
|
||||
|
||||
/// Share the collection with [sharee]
|
||||
Future<CollectionShareResult> share(
|
||||
Sharee sharee, {
|
||||
required ValueChanged<Collection> onCollectionUpdated,
|
||||
});
|
||||
|
||||
/// Unshare the collection with a user
|
||||
Future<CollectionShareResult> unshare(
|
||||
CiString userId, {
|
||||
required ValueChanged<Collection> onCollectionUpdated,
|
||||
});
|
||||
|
||||
/// Import a pending shared collection and return the resulting collection
|
||||
Future<Collection> importPendingShared();
|
||||
|
||||
/// Convert a [NewCollectionItem] to an adapted one
|
||||
Future<CollectionItem> adaptToNewItem(NewCollectionItem original);
|
||||
|
||||
bool isItemRemovable(CollectionItem item);
|
||||
|
||||
/// Remove this collection
|
||||
Future<void> remove();
|
||||
|
||||
/// Return if this capability is allowed
|
||||
bool isPermitted(CollectionCapability capability);
|
||||
|
||||
/// Return if the cover of this collection has been manually set
|
||||
bool isManualCover();
|
||||
|
||||
/// Called when the collection items belonging to this collection is first
|
||||
/// loaded
|
||||
Future<Collection?> updatePostLoad(List<CollectionItem> items);
|
||||
}
|
||||
|
||||
abstract class CollectionItemAdapter {
|
||||
const CollectionItemAdapter();
|
||||
|
||||
CollectionItem toItem();
|
||||
}
|
83
app/lib/entity/collection/adapter/adapter_mixin.dart
Normal file
83
app/lib/entity/collection/adapter/adapter_mixin.dart
Normal file
|
@ -0,0 +1,83 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||
import 'package:nc_photos/entity/collection/util.dart';
|
||||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/sharee.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:np_common/ci_string.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
|
||||
/// A read-only collection that does not support modifying its items
|
||||
mixin CollectionAdapterReadOnlyTag implements CollectionAdapter {
|
||||
@override
|
||||
Future<int> addFiles(
|
||||
List<FileDescriptor> files, {
|
||||
ErrorWithValueHandler<FileDescriptor>? onError,
|
||||
required ValueChanged<Collection> onCollectionUpdated,
|
||||
}) {
|
||||
throw UnsupportedError("Operation not supported");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Collection> edit({
|
||||
String? name,
|
||||
List<CollectionItem>? items,
|
||||
CollectionItemSort? itemSort,
|
||||
OrNull<FileDescriptor>? cover,
|
||||
List<CollectionItem>? knownItems,
|
||||
}) {
|
||||
throw UnsupportedError("Operation not supported");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> removeItems(
|
||||
List<CollectionItem> items, {
|
||||
ErrorWithValueIndexedHandler<CollectionItem>? onError,
|
||||
required ValueChanged<Collection> onCollectionUpdated,
|
||||
}) {
|
||||
throw UnsupportedError("Operation not supported");
|
||||
}
|
||||
|
||||
@override
|
||||
bool isItemRemovable(CollectionItem item) => false;
|
||||
|
||||
@override
|
||||
bool isManualCover() => false;
|
||||
|
||||
@override
|
||||
Future<Collection?> updatePostLoad(List<CollectionItem> items) =>
|
||||
Future.value(null);
|
||||
}
|
||||
|
||||
mixin CollectionAdapterUnremovableTag implements CollectionAdapter {
|
||||
@override
|
||||
Future<void> remove() {
|
||||
throw UnsupportedError("Operation not supported");
|
||||
}
|
||||
}
|
||||
|
||||
mixin CollectionAdapterUnshareableTag implements CollectionAdapter {
|
||||
@override
|
||||
Future<CollectionShareResult> share(
|
||||
Sharee sharee, {
|
||||
required ValueChanged<Collection> onCollectionUpdated,
|
||||
}) {
|
||||
throw UnsupportedError("Operation not supported");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CollectionShareResult> unshare(
|
||||
CiString userId, {
|
||||
required ValueChanged<Collection> onCollectionUpdated,
|
||||
}) {
|
||||
throw UnsupportedError("Operation not supported");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Collection> importPendingShared() {
|
||||
throw UnsupportedError("Operation not supported");
|
||||
}
|
||||
}
|
318
app/lib/entity/collection/adapter/album.dart
Normal file
318
app/lib/entity/collection/adapter/album.dart
Normal file
|
@ -0,0 +1,318 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album/cover_provider.dart';
|
||||
import 'package:nc_photos/entity/album/item.dart';
|
||||
import 'package:nc_photos/entity/album/provider.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||
import 'package:nc_photos/entity/collection/builder.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/album.dart';
|
||||
import 'package:nc_photos/entity/collection/util.dart';
|
||||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/album_item_adapter.dart';
|
||||
import 'package:nc_photos/entity/collection_item/new_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/sharee.dart';
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/use_case/album/add_file_to_album.dart';
|
||||
import 'package:nc_photos/use_case/album/edit_album.dart';
|
||||
import 'package:nc_photos/use_case/album/remove_album.dart';
|
||||
import 'package:nc_photos/use_case/album/remove_from_album.dart';
|
||||
import 'package:nc_photos/use_case/album/share_album_with_user.dart';
|
||||
import 'package:nc_photos/use_case/album/unshare_album_with_user.dart';
|
||||
import 'package:nc_photos/use_case/import_pending_shared_album.dart';
|
||||
import 'package:nc_photos/use_case/preprocess_album.dart';
|
||||
import 'package:nc_photos/use_case/unimport_shared_album.dart';
|
||||
import 'package:nc_photos/use_case/update_album_with_actual_items.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_common/ci_string.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
part 'album.g.dart';
|
||||
|
||||
@npLog
|
||||
class CollectionAlbumAdapter implements CollectionAdapter {
|
||||
CollectionAlbumAdapter(this._c, this.account, this.collection)
|
||||
: assert(require(_c)),
|
||||
_provider = collection.contentProvider as CollectionAlbumProvider;
|
||||
|
||||
static bool require(DiContainer c) => PreProcessAlbum.require(c);
|
||||
|
||||
@override
|
||||
Stream<List<CollectionItem>> listItem() async* {
|
||||
final items = await PreProcessAlbum(_c)(account, _provider.album);
|
||||
yield items.map<CollectionItem>((i) {
|
||||
if (i is AlbumFileItem) {
|
||||
return CollectionFileItemAlbumAdapter(i);
|
||||
} else if (i is AlbumLabelItem) {
|
||||
return CollectionLabelItemAlbumAdapter(i);
|
||||
} else {
|
||||
_log.shout("[listItem] Unknown item type: ${i.runtimeType}");
|
||||
throw UnimplementedError("Unknown item type: ${i.runtimeType}");
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> addFiles(
|
||||
List<FileDescriptor> files, {
|
||||
ErrorWithValueHandler<FileDescriptor>? onError,
|
||||
required ValueChanged<Collection> onCollectionUpdated,
|
||||
}) async {
|
||||
try {
|
||||
final newAlbum =
|
||||
await AddFileToAlbum(_c)(account, _provider.album, files);
|
||||
onCollectionUpdated(CollectionBuilder.byAlbum(account, newAlbum));
|
||||
return files.length;
|
||||
} catch (e, stackTrace) {
|
||||
for (final f in files) {
|
||||
onError?.call(f, e, stackTrace);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Collection> edit({
|
||||
String? name,
|
||||
List<CollectionItem>? items,
|
||||
CollectionItemSort? itemSort,
|
||||
OrNull<FileDescriptor>? cover,
|
||||
List<CollectionItem>? knownItems,
|
||||
}) async {
|
||||
assert(name != null || items != null || itemSort != null || cover != null);
|
||||
final newItems = items?.run((items) => items
|
||||
.map((e) {
|
||||
if (e is AlbumAdaptedCollectionItem) {
|
||||
return e.albumItem;
|
||||
} else if (e is NewCollectionLabelItem) {
|
||||
// new labels
|
||||
return AlbumLabelItem(
|
||||
addedBy: account.userId,
|
||||
addedAt: e.createdAt,
|
||||
text: e.text,
|
||||
);
|
||||
} else {
|
||||
_log.severe("[edit] Unsupported type: ${e.runtimeType}");
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.whereNotNull()
|
||||
.toList());
|
||||
final newAlbum = await EditAlbum(_c)(
|
||||
account,
|
||||
_provider.album,
|
||||
name: name,
|
||||
items: newItems,
|
||||
itemSort: itemSort,
|
||||
cover: cover,
|
||||
knownItems: knownItems
|
||||
?.whereType<AlbumAdaptedCollectionItem>()
|
||||
.map((e) => e.albumItem)
|
||||
.toList(),
|
||||
);
|
||||
return collection.copyWith(
|
||||
name: name,
|
||||
contentProvider: _provider.copyWith(album: newAlbum),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> removeItems(
|
||||
List<CollectionItem> items, {
|
||||
ErrorWithValueIndexedHandler<CollectionItem>? onError,
|
||||
required ValueChanged<Collection> onCollectionUpdated,
|
||||
}) async {
|
||||
try {
|
||||
final group = items
|
||||
.withIndex()
|
||||
.groupListsBy((e) => e.item2 is AlbumAdaptedCollectionItem);
|
||||
var failed = 0;
|
||||
if (group[true]?.isNotEmpty ?? false) {
|
||||
final newAlbum = await RemoveFromAlbum(_c)(
|
||||
account,
|
||||
_provider.album,
|
||||
group[true]!
|
||||
.map((e) => e.item2)
|
||||
.cast<AlbumAdaptedCollectionItem>()
|
||||
.map((e) => e.albumItem)
|
||||
.toList(),
|
||||
onError: (i, item, e, stackTrace) {
|
||||
++failed;
|
||||
final actualIndex = group[true]![i].item1;
|
||||
try {
|
||||
onError?.call(actualIndex, items[actualIndex], e, stackTrace);
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[removeItems] Unknown error", e, stackTrace);
|
||||
}
|
||||
},
|
||||
);
|
||||
onCollectionUpdated(collection.copyWith(
|
||||
contentProvider: _provider.copyWith(
|
||||
album: newAlbum,
|
||||
),
|
||||
));
|
||||
}
|
||||
for (final pair in (group[false] ?? const <Tuple2<int, int>>[])) {
|
||||
final actualIndex = pair.item1;
|
||||
onError?.call(
|
||||
actualIndex,
|
||||
items[actualIndex],
|
||||
UnsupportedError(
|
||||
"Unsupported item type: ${items[actualIndex].runtimeType}"),
|
||||
StackTrace.current,
|
||||
);
|
||||
}
|
||||
return (group[true] ?? []).length - failed;
|
||||
} catch (e, stackTrace) {
|
||||
for (final pair in items.withIndex()) {
|
||||
onError?.call(pair.item1, pair.item2, e, stackTrace);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CollectionShareResult> share(
|
||||
Sharee sharee, {
|
||||
required ValueChanged<Collection> onCollectionUpdated,
|
||||
}) async {
|
||||
var fileFailed = false;
|
||||
final newAlbum = await ShareAlbumWithUser(_c.shareRepo, _c.albumRepo)(
|
||||
account,
|
||||
_provider.album,
|
||||
sharee,
|
||||
onShareFileFailed: (f, e, stackTrace) {
|
||||
_log.severe("[share] Failed to share file: ${logFilename(f.path)}", e,
|
||||
stackTrace);
|
||||
fileFailed = true;
|
||||
},
|
||||
);
|
||||
onCollectionUpdated(CollectionBuilder.byAlbum(account, newAlbum));
|
||||
return fileFailed
|
||||
? CollectionShareResult.partial
|
||||
: CollectionShareResult.ok;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CollectionShareResult> unshare(
|
||||
CiString userId, {
|
||||
required ValueChanged<Collection> onCollectionUpdated,
|
||||
}) async {
|
||||
var fileFailed = false;
|
||||
final newAlbum = await UnshareAlbumWithUser(_c)(
|
||||
account,
|
||||
_provider.album,
|
||||
userId,
|
||||
onUnshareFileFailed: (f, e, stackTrace) {
|
||||
_log.severe("[unshare] Failed to unshare file: ${logFilename(f.path)}",
|
||||
e, stackTrace);
|
||||
fileFailed = true;
|
||||
},
|
||||
);
|
||||
onCollectionUpdated(CollectionBuilder.byAlbum(account, newAlbum));
|
||||
return fileFailed
|
||||
? CollectionShareResult.partial
|
||||
: CollectionShareResult.ok;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Collection> importPendingShared() async {
|
||||
final newAlbum =
|
||||
await ImportPendingSharedAlbum(_c)(account, _provider.album);
|
||||
return CollectionBuilder.byAlbum(account, newAlbum);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CollectionItem> adaptToNewItem(NewCollectionItem original) async {
|
||||
if (original is NewCollectionFileItem) {
|
||||
final item = AlbumStaticProvider.of(_provider.album)
|
||||
.items
|
||||
.whereType<AlbumFileItem>()
|
||||
.firstWhere((e) => e.file.compareServerIdentity(original.file));
|
||||
return CollectionFileItemAlbumAdapter(item);
|
||||
} else if (original is NewCollectionLabelItem) {
|
||||
final item = AlbumStaticProvider.of(_provider.album)
|
||||
.items
|
||||
.whereType<AlbumLabelItem>()
|
||||
.sorted((a, b) => a.addedAt.compareTo(b.addedAt))
|
||||
.reversed
|
||||
.firstWhere((e) => e.text == original.text);
|
||||
return CollectionLabelItemAlbumAdapter(item);
|
||||
} else {
|
||||
throw UnsupportedError("Unsupported type: ${original.runtimeType}");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool isItemRemovable(CollectionItem item) {
|
||||
if (_provider.album.provider is! AlbumStaticProvider) {
|
||||
return false;
|
||||
}
|
||||
if (_provider.album.albumFile?.isOwned(account.userId) == true) {
|
||||
return true;
|
||||
}
|
||||
if (item is! AlbumAdaptedCollectionItem) {
|
||||
_log.warning("[isItemRemovable] Unknown item type: ${item.runtimeType}");
|
||||
return true;
|
||||
}
|
||||
return item.albumItem.addedBy == account.userId;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove() {
|
||||
if (_provider.album.albumFile?.isOwned(account.userId) == true) {
|
||||
return RemoveAlbum(_c)(account, _provider.album);
|
||||
} else {
|
||||
return UnimportSharedAlbum(_c)(account, _provider.album);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool isPermitted(CollectionCapability capability) {
|
||||
if (!_provider.capabilities.contains(capability)) {
|
||||
return false;
|
||||
}
|
||||
if (_provider.album.albumFile?.isOwned(account.userId) == true) {
|
||||
return true;
|
||||
} else {
|
||||
return _provider.guestCapabilities.contains(capability);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool isManualCover() =>
|
||||
_provider.album.coverProvider is AlbumManualCoverProvider;
|
||||
|
||||
@override
|
||||
Future<Collection?> updatePostLoad(List<CollectionItem> items) async {
|
||||
final album = await UpdateAlbumWithActualItems(_c.albumRepo)(
|
||||
account,
|
||||
_provider.album,
|
||||
items
|
||||
.whereType<AlbumAdaptedCollectionItem>()
|
||||
.map((e) => e.albumItem)
|
||||
.toList(),
|
||||
);
|
||||
if (!identical(album, _provider.album)) {
|
||||
return CollectionBuilder.byAlbum(account, album);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
final Account account;
|
||||
final Collection collection;
|
||||
|
||||
final CollectionAlbumProvider _provider;
|
||||
}
|
15
app/lib/entity/collection/adapter/album.g.dart
Normal file
15
app/lib/entity/collection/adapter/album.g.dart
Normal file
|
@ -0,0 +1,15 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'album.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$CollectionAlbumAdapterNpLog on CollectionAlbumAdapter {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log =
|
||||
Logger("entity.collection.adapter.album.CollectionAlbumAdapter");
|
||||
}
|
58
app/lib/entity/collection/adapter/location_group.dart
Normal file
58
app/lib/entity/collection/adapter/location_group.dart
Normal file
|
@ -0,0 +1,58 @@
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter/adapter_mixin.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/location_group.dart';
|
||||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/basic_item.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/use_case/list_location_file.dart';
|
||||
|
||||
class CollectionLocationGroupAdapter
|
||||
with
|
||||
CollectionAdapterReadOnlyTag,
|
||||
CollectionAdapterUnremovableTag,
|
||||
CollectionAdapterUnshareableTag
|
||||
implements CollectionAdapter {
|
||||
CollectionLocationGroupAdapter(this._c, this.account, this.collection)
|
||||
: assert(require(_c)),
|
||||
_provider =
|
||||
collection.contentProvider as CollectionLocationGroupProvider;
|
||||
|
||||
static bool require(DiContainer c) => ListLocationFile.require(c);
|
||||
|
||||
@override
|
||||
Stream<List<CollectionItem>> listItem() async* {
|
||||
final files = <File>[];
|
||||
for (final r in account.roots) {
|
||||
final dir = File(path: file_util.unstripPath(account, r));
|
||||
files.addAll(await ListLocationFile(_c)(account, dir,
|
||||
_provider.location.place, _provider.location.countryCode));
|
||||
}
|
||||
yield files
|
||||
.where((f) => file_util.isSupportedFormat(f))
|
||||
.map((f) => BasicCollectionFileItem(f))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CollectionItem> adaptToNewItem(CollectionItem original) async {
|
||||
if (original is CollectionFileItem) {
|
||||
return BasicCollectionFileItem(original.file);
|
||||
} else {
|
||||
throw UnsupportedError("Unsupported type: ${original.runtimeType}");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool isPermitted(CollectionCapability capability) =>
|
||||
_provider.capabilities.contains(capability);
|
||||
|
||||
final DiContainer _c;
|
||||
final Account account;
|
||||
final Collection collection;
|
||||
|
||||
final CollectionLocationGroupProvider _provider;
|
||||
}
|
57
app/lib/entity/collection/adapter/memory.dart
Normal file
57
app/lib/entity/collection/adapter/memory.dart
Normal file
|
@ -0,0 +1,57 @@
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter/adapter_mixin.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/memory.dart';
|
||||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/basic_item.dart';
|
||||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/use_case/list_location_file.dart';
|
||||
|
||||
class CollectionMemoryAdapter
|
||||
with
|
||||
CollectionAdapterReadOnlyTag,
|
||||
CollectionAdapterUnremovableTag,
|
||||
CollectionAdapterUnshareableTag
|
||||
implements CollectionAdapter {
|
||||
CollectionMemoryAdapter(this._c, this.account, this.collection)
|
||||
: assert(require(_c)),
|
||||
_provider = collection.contentProvider as CollectionMemoryProvider;
|
||||
|
||||
static bool require(DiContainer c) => ListLocationFile.require(c);
|
||||
|
||||
@override
|
||||
Stream<List<CollectionItem>> listItem() async* {
|
||||
final date = DateTime(_provider.year, _provider.month, _provider.day);
|
||||
final dayRange = _c.pref.getMemoriesRangeOr();
|
||||
final from = date.subtract(Duration(days: dayRange));
|
||||
final to = date.add(Duration(days: dayRange + 1));
|
||||
final files = await FileSqliteDbDataSource(_c).listByDate(
|
||||
account, from.millisecondsSinceEpoch, to.millisecondsSinceEpoch);
|
||||
yield files
|
||||
.where((f) => file_util.isSupportedFormat(f))
|
||||
.map((f) => BasicCollectionFileItem(f))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CollectionItem> adaptToNewItem(CollectionItem original) async {
|
||||
if (original is CollectionFileItem) {
|
||||
return BasicCollectionFileItem(original.file);
|
||||
} else {
|
||||
throw UnsupportedError("Unsupported type: ${original.runtimeType}");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool isPermitted(CollectionCapability capability) =>
|
||||
_provider.capabilities.contains(capability);
|
||||
|
||||
final DiContainer _c;
|
||||
final Account account;
|
||||
final Collection collection;
|
||||
|
||||
final CollectionMemoryProvider _provider;
|
||||
}
|
173
app/lib/entity/collection/adapter/nc_album.dart
Normal file
173
app/lib/entity/collection/adapter/nc_album.dart
Normal file
|
@ -0,0 +1,173 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter/adapter_mixin.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/nc_album.dart';
|
||||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/basic_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/nc_album_item_adapter.dart';
|
||||
import 'package:nc_photos/entity/collection_item/new_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/nc_album.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/use_case/find_file_descriptor.dart';
|
||||
import 'package:nc_photos/use_case/nc_album/add_file_to_nc_album.dart';
|
||||
import 'package:nc_photos/use_case/nc_album/edit_nc_album.dart';
|
||||
import 'package:nc_photos/use_case/nc_album/list_nc_album.dart';
|
||||
import 'package:nc_photos/use_case/nc_album/list_nc_album_item.dart';
|
||||
import 'package:nc_photos/use_case/nc_album/remove_from_nc_album.dart';
|
||||
import 'package:nc_photos/use_case/nc_album/remove_nc_album.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
|
||||
part 'nc_album.g.dart';
|
||||
|
||||
@npLog
|
||||
class CollectionNcAlbumAdapter
|
||||
with CollectionAdapterUnshareableTag
|
||||
implements CollectionAdapter {
|
||||
CollectionNcAlbumAdapter(this._c, this.account, this.collection)
|
||||
: assert(require(_c)),
|
||||
_provider = collection.contentProvider as CollectionNcAlbumProvider;
|
||||
|
||||
static bool require(DiContainer c) =>
|
||||
ListNcAlbumItem.require(c) && FindFileDescriptor.require(c);
|
||||
|
||||
@override
|
||||
Stream<List<CollectionItem>> listItem() {
|
||||
return ListNcAlbumItem(_c)(account, _provider.album)
|
||||
.asyncMap((items) async {
|
||||
final found = await FindFileDescriptor(_c)(
|
||||
account,
|
||||
items.map((e) => e.fileId).toList(),
|
||||
onFileNotFound: (fileId) {
|
||||
// happens when this is a file shared with you
|
||||
_log.warning("[listItem] File not found: $fileId");
|
||||
},
|
||||
);
|
||||
return items.map((i) {
|
||||
final f = found.firstWhereOrNull((e) => e.fdId == i.fileId);
|
||||
return CollectionFileItemNcAlbumItemAdapter(i, f);
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> addFiles(
|
||||
List<FileDescriptor> files, {
|
||||
ErrorWithValueHandler<FileDescriptor>? onError,
|
||||
required ValueChanged<Collection> onCollectionUpdated,
|
||||
}) async {
|
||||
final count = await AddFileToNcAlbum(_c)(account, _provider.album, files,
|
||||
onError: onError);
|
||||
if (count > 0) {
|
||||
try {
|
||||
final newAlbum = await _syncRemote();
|
||||
onCollectionUpdated(collection.copyWith(
|
||||
contentProvider: _provider.copyWith(
|
||||
album: newAlbum,
|
||||
),
|
||||
));
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[addFiles] Failed while _syncRemote", e, stackTrace);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Collection> edit({
|
||||
String? name,
|
||||
List<CollectionItem>? items,
|
||||
CollectionItemSort? itemSort,
|
||||
OrNull<FileDescriptor>? cover,
|
||||
List<CollectionItem>? knownItems,
|
||||
}) async {
|
||||
assert(name != null);
|
||||
if (items != null || itemSort != null || cover != null) {
|
||||
_log.warning(
|
||||
"[edit] Nextcloud album does not support editing item or sort");
|
||||
}
|
||||
final newItems = items?.run((items) => items
|
||||
.map((e) => e is CollectionFileItem ? e.file : null)
|
||||
.whereNotNull()
|
||||
.toList());
|
||||
final newAlbum = await EditNcAlbum(_c)(
|
||||
account,
|
||||
_provider.album,
|
||||
name: name,
|
||||
items: newItems,
|
||||
itemSort: itemSort,
|
||||
);
|
||||
return collection.copyWith(
|
||||
name: name,
|
||||
contentProvider: _provider.copyWith(album: newAlbum),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> removeItems(
|
||||
List<CollectionItem> items, {
|
||||
ErrorWithValueIndexedHandler<CollectionItem>? onError,
|
||||
required ValueChanged<Collection> onCollectionUpdated,
|
||||
}) async {
|
||||
final count = await RemoveFromNcAlbum(_c)(account, _provider.album, items,
|
||||
onError: onError);
|
||||
if (count > 0) {
|
||||
try {
|
||||
final newAlbum = await _syncRemote();
|
||||
onCollectionUpdated(collection.copyWith(
|
||||
contentProvider: _provider.copyWith(
|
||||
album: newAlbum,
|
||||
),
|
||||
));
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[removeItems] Failed while _syncRemote", e, stackTrace);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CollectionItem> adaptToNewItem(NewCollectionItem original) async {
|
||||
if (original is NewCollectionFileItem) {
|
||||
return BasicCollectionFileItem(original.file);
|
||||
} else {
|
||||
throw UnsupportedError("Unsupported type: ${original.runtimeType}");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool isItemRemovable(CollectionItem item) => true;
|
||||
|
||||
@override
|
||||
Future<void> remove() => RemoveNcAlbum(_c)(account, _provider.album);
|
||||
|
||||
@override
|
||||
bool isPermitted(CollectionCapability capability) =>
|
||||
_provider.capabilities.contains(capability);
|
||||
|
||||
@override
|
||||
bool isManualCover() => false;
|
||||
|
||||
@override
|
||||
Future<Collection?> updatePostLoad(List<CollectionItem> items) =>
|
||||
Future.value(null);
|
||||
|
||||
Future<NcAlbum> _syncRemote() async {
|
||||
final remote = await ListNcAlbum(_c)(account).last;
|
||||
return remote.firstWhere((e) => e.compareIdentity(_provider.album));
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
final Account account;
|
||||
final Collection collection;
|
||||
|
||||
final CollectionNcAlbumProvider _provider;
|
||||
}
|
15
app/lib/entity/collection/adapter/nc_album.g.dart
Normal file
15
app/lib/entity/collection/adapter/nc_album.g.dart
Normal file
|
@ -0,0 +1,15 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'nc_album.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$CollectionNcAlbumAdapterNpLog on CollectionNcAlbumAdapter {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log =
|
||||
Logger("entity.collection.adapter.nc_album.CollectionNcAlbumAdapter");
|
||||
}
|
60
app/lib/entity/collection/adapter/person.dart
Normal file
60
app/lib/entity/collection/adapter/person.dart
Normal file
|
@ -0,0 +1,60 @@
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter/adapter_mixin.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/person.dart';
|
||||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/basic_item.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/use_case/list_face.dart';
|
||||
import 'package:nc_photos/use_case/populate_person.dart';
|
||||
|
||||
class CollectionPersonAdapter
|
||||
with
|
||||
CollectionAdapterReadOnlyTag,
|
||||
CollectionAdapterUnremovableTag,
|
||||
CollectionAdapterUnshareableTag
|
||||
implements CollectionAdapter {
|
||||
CollectionPersonAdapter(this._c, this.account, this.collection)
|
||||
: assert(require(_c)),
|
||||
_provider = collection.contentProvider as CollectionPersonProvider;
|
||||
|
||||
static bool require(DiContainer c) =>
|
||||
ListFace.require(c) && PopulatePerson.require(c);
|
||||
|
||||
@override
|
||||
Stream<List<CollectionItem>> listItem() async* {
|
||||
final faces = await ListFace(_c)(account, _provider.person);
|
||||
final files = await PopulatePerson(_c)(account, faces);
|
||||
final rootDirs = account.roots
|
||||
.map((e) => File(path: file_util.unstripPath(account, e)))
|
||||
.toList();
|
||||
yield files
|
||||
.where((f) =>
|
||||
file_util.isSupportedFormat(f) &&
|
||||
rootDirs.any((dir) => file_util.isUnderDir(f, dir)))
|
||||
.map((f) => BasicCollectionFileItem(f))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CollectionItem> adaptToNewItem(CollectionItem original) async {
|
||||
if (original is CollectionFileItem) {
|
||||
return BasicCollectionFileItem(original.file);
|
||||
} else {
|
||||
throw UnsupportedError("Unsupported type: ${original.runtimeType}");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool isPermitted(CollectionCapability capability) =>
|
||||
_provider.capabilities.contains(capability);
|
||||
|
||||
final DiContainer _c;
|
||||
final Account account;
|
||||
final Collection collection;
|
||||
|
||||
final CollectionPersonProvider _provider;
|
||||
}
|
47
app/lib/entity/collection/adapter/tag.dart
Normal file
47
app/lib/entity/collection/adapter/tag.dart
Normal file
|
@ -0,0 +1,47 @@
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter/adapter_mixin.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/tag.dart';
|
||||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/basic_item.dart';
|
||||
import 'package:nc_photos/use_case/list_tagged_file.dart';
|
||||
|
||||
class CollectionTagAdapter
|
||||
with
|
||||
CollectionAdapterReadOnlyTag,
|
||||
CollectionAdapterUnremovableTag,
|
||||
CollectionAdapterUnshareableTag
|
||||
implements CollectionAdapter {
|
||||
CollectionTagAdapter(this._c, this.account, this.collection)
|
||||
: assert(require(_c)),
|
||||
_provider = collection.contentProvider as CollectionTagProvider;
|
||||
|
||||
static bool require(DiContainer c) => true;
|
||||
|
||||
@override
|
||||
Stream<List<CollectionItem>> listItem() async* {
|
||||
final files = await ListTaggedFile(_c)(account, _provider.tags);
|
||||
yield files.map((f) => BasicCollectionFileItem(f)).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CollectionItem> adaptToNewItem(CollectionItem original) async {
|
||||
if (original is CollectionFileItem) {
|
||||
return BasicCollectionFileItem(original.file);
|
||||
} else {
|
||||
throw UnsupportedError("Unsupported type: ${original.runtimeType}");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool isPermitted(CollectionCapability capability) =>
|
||||
_provider.capabilities.contains(capability);
|
||||
|
||||
final DiContainer _c;
|
||||
final Account account;
|
||||
final Collection collection;
|
||||
|
||||
final CollectionTagProvider _provider;
|
||||
}
|
64
app/lib/entity/collection/builder.dart
Normal file
64
app/lib/entity/collection/builder.dart
Normal file
|
@ -0,0 +1,64 @@
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/album.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/location_group.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/nc_album.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/person.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/tag.dart';
|
||||
import 'package:nc_photos/entity/nc_album.dart';
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
import 'package:nc_photos/use_case/list_location_group.dart';
|
||||
|
||||
class CollectionBuilder {
|
||||
static Collection byAlbum(Account account, Album album) {
|
||||
return Collection(
|
||||
name: album.name,
|
||||
contentProvider: CollectionAlbumProvider(
|
||||
account: account,
|
||||
album: album,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Collection byLocationGroup(Account account, LocationGroup location) {
|
||||
return Collection(
|
||||
name: location.place,
|
||||
contentProvider: CollectionLocationGroupProvider(
|
||||
account: account,
|
||||
location: location,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Collection byNcAlbum(Account account, NcAlbum album) {
|
||||
return Collection(
|
||||
name: album.strippedPath,
|
||||
contentProvider: CollectionNcAlbumProvider(
|
||||
account: account,
|
||||
album: album,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Collection byPerson(Account account, Person person) {
|
||||
return Collection(
|
||||
name: person.name,
|
||||
contentProvider: CollectionPersonProvider(
|
||||
account: account,
|
||||
person: person,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Collection byTags(Account account, List<Tag> tags) {
|
||||
return Collection(
|
||||
name: tags.first.displayName,
|
||||
contentProvider: CollectionTagProvider(
|
||||
account: account,
|
||||
tags: tags,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
117
app/lib/entity/collection/content_provider/album.dart
Normal file
117
app/lib/entity/collection/content_provider/album.dart
Normal file
|
@ -0,0 +1,117 @@
|
|||
import 'package:copy_with/copy_with.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/provider.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/util.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'album.g.dart';
|
||||
|
||||
/// Album provided by our app
|
||||
@genCopyWith
|
||||
@toString
|
||||
class CollectionAlbumProvider
|
||||
with EquatableMixin
|
||||
implements CollectionContentProvider {
|
||||
const CollectionAlbumProvider({
|
||||
required this.account,
|
||||
required this.album,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
String get fourCc => "ALBM";
|
||||
|
||||
@override
|
||||
String get id => album.albumFile!.fileId!.toString();
|
||||
|
||||
@override
|
||||
int? get count {
|
||||
if (album.provider is AlbumStaticProvider) {
|
||||
return (album.provider as AlbumStaticProvider).items.length;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
DateTime get lastModified =>
|
||||
album.provider.latestItemTime ?? album.lastUpdated;
|
||||
|
||||
@override
|
||||
List<CollectionCapability> get capabilities => [
|
||||
CollectionCapability.sort,
|
||||
CollectionCapability.rename,
|
||||
CollectionCapability.manualCover,
|
||||
if (album.provider is AlbumStaticProvider) ...[
|
||||
CollectionCapability.manualItem,
|
||||
CollectionCapability.manualSort,
|
||||
CollectionCapability.labelItem,
|
||||
CollectionCapability.share,
|
||||
],
|
||||
];
|
||||
|
||||
/// Capabilities when this album is shared to this user by someone else
|
||||
List<CollectionCapability> get guestCapabilities => [
|
||||
if (album.provider is AlbumStaticProvider) ...[
|
||||
CollectionCapability.manualItem,
|
||||
CollectionCapability.labelItem,
|
||||
],
|
||||
];
|
||||
|
||||
@override
|
||||
CollectionItemSort get itemSort => album.sortProvider.toCollectionItemSort();
|
||||
|
||||
@override
|
||||
List<CollectionShare> get shares =>
|
||||
album.shares
|
||||
?.where((s) => s.userId != account.userId)
|
||||
.map((s) => CollectionShare(
|
||||
userId: s.userId,
|
||||
username: s.displayName ?? s.userId.raw,
|
||||
))
|
||||
.toList() ??
|
||||
const [];
|
||||
|
||||
@override
|
||||
String? getCoverUrl(
|
||||
int width,
|
||||
int height, {
|
||||
bool? isKeepAspectRatio,
|
||||
}) {
|
||||
final fd = album.coverProvider.getCover(album);
|
||||
if (fd == null) {
|
||||
return null;
|
||||
} else {
|
||||
return api_util.getFilePreviewUrlByFileId(
|
||||
account,
|
||||
fd.fdId,
|
||||
width: width,
|
||||
height: height,
|
||||
isKeepAspectRatio: isKeepAspectRatio ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isDynamicCollection => album.provider is! AlbumStaticProvider;
|
||||
|
||||
@override
|
||||
bool get isPendingSharedAlbum =>
|
||||
album.albumFile?.path.startsWith(
|
||||
remote_storage_util.getRemotePendingSharedAlbumsDir(account)) ==
|
||||
true;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [account, album];
|
||||
|
||||
final Account account;
|
||||
final Album album;
|
||||
}
|
48
app/lib/entity/collection/content_provider/album.g.dart
Normal file
48
app/lib/entity/collection/content_provider/album.g.dart
Normal file
|
@ -0,0 +1,48 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'album.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithLintRuleGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class $CollectionAlbumProviderCopyWithWorker {
|
||||
CollectionAlbumProvider call({Account? account, Album? album});
|
||||
}
|
||||
|
||||
class _$CollectionAlbumProviderCopyWithWorkerImpl
|
||||
implements $CollectionAlbumProviderCopyWithWorker {
|
||||
_$CollectionAlbumProviderCopyWithWorkerImpl(this.that);
|
||||
|
||||
@override
|
||||
CollectionAlbumProvider call({dynamic account, dynamic album}) {
|
||||
return CollectionAlbumProvider(
|
||||
account: account as Account? ?? that.account,
|
||||
album: album as Album? ?? that.album);
|
||||
}
|
||||
|
||||
final CollectionAlbumProvider that;
|
||||
}
|
||||
|
||||
extension $CollectionAlbumProviderCopyWith on CollectionAlbumProvider {
|
||||
$CollectionAlbumProviderCopyWithWorker get copyWith => _$copyWith;
|
||||
$CollectionAlbumProviderCopyWithWorker get _$copyWith =>
|
||||
_$CollectionAlbumProviderCopyWithWorkerImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$CollectionAlbumProviderToString on CollectionAlbumProvider {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "CollectionAlbumProvider {account: $account, album: $album}";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/util.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||
import 'package:nc_photos/use_case/list_location_group.dart';
|
||||
|
||||
class CollectionLocationGroupProvider
|
||||
with EquatableMixin
|
||||
implements CollectionContentProvider {
|
||||
const CollectionLocationGroupProvider({
|
||||
required this.account,
|
||||
required this.location,
|
||||
});
|
||||
|
||||
@override
|
||||
String get fourCc => "LOCG";
|
||||
|
||||
@override
|
||||
String get id => location.place;
|
||||
|
||||
@override
|
||||
int? get count => location.count;
|
||||
|
||||
@override
|
||||
DateTime get lastModified => location.latestDateTime;
|
||||
|
||||
@override
|
||||
List<CollectionCapability> get capabilities => [];
|
||||
|
||||
@override
|
||||
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
||||
|
||||
@override
|
||||
List<CollectionShare> get shares => [];
|
||||
|
||||
@override
|
||||
String? getCoverUrl(
|
||||
int width,
|
||||
int height, {
|
||||
bool? isKeepAspectRatio,
|
||||
}) {
|
||||
return api_util.getFilePreviewUrlByFileId(
|
||||
account,
|
||||
location.latestFileId,
|
||||
width: width,
|
||||
height: height,
|
||||
isKeepAspectRatio: isKeepAspectRatio ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isDynamicCollection => true;
|
||||
|
||||
@override
|
||||
bool get isPendingSharedAlbum => false;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [account, location];
|
||||
|
||||
final Account account;
|
||||
final LocationGroup location;
|
||||
}
|
78
app/lib/entity/collection/content_provider/memory.dart
Normal file
78
app/lib/entity/collection/content_provider/memory.dart
Normal file
|
@ -0,0 +1,78 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/util.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'memory.g.dart';
|
||||
|
||||
@toString
|
||||
class CollectionMemoryProvider
|
||||
with EquatableMixin
|
||||
implements CollectionContentProvider {
|
||||
const CollectionMemoryProvider({
|
||||
required this.account,
|
||||
required this.year,
|
||||
required this.month,
|
||||
required this.day,
|
||||
this.cover,
|
||||
});
|
||||
|
||||
@override
|
||||
String get fourCc => "MEMY";
|
||||
|
||||
@override
|
||||
String get id => "$year-$month-$day";
|
||||
|
||||
@override
|
||||
int? get count => null;
|
||||
|
||||
@override
|
||||
DateTime get lastModified => DateTime(year, month, day);
|
||||
|
||||
@override
|
||||
List<CollectionCapability> get capabilities => [];
|
||||
|
||||
@override
|
||||
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
||||
|
||||
@override
|
||||
List<CollectionShare> get shares => [];
|
||||
|
||||
@override
|
||||
String? getCoverUrl(
|
||||
int width,
|
||||
int height, {
|
||||
bool? isKeepAspectRatio,
|
||||
}) {
|
||||
return cover?.run((cover) => api_util.getFilePreviewUrl(
|
||||
account,
|
||||
cover,
|
||||
width: width,
|
||||
height: height,
|
||||
isKeepAspectRatio: isKeepAspectRatio ?? false,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isDynamicCollection => true;
|
||||
|
||||
@override
|
||||
bool get isPendingSharedAlbum => false;
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [account, year, month, day, cover];
|
||||
|
||||
final Account account;
|
||||
final int year;
|
||||
final int month;
|
||||
final int day;
|
||||
final FileDescriptor? cover;
|
||||
}
|
14
app/lib/entity/collection/content_provider/memory.g.dart
Normal file
14
app/lib/entity/collection/content_provider/memory.g.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'memory.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$CollectionMemoryProviderToString on CollectionMemoryProvider {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "CollectionMemoryProvider {account: $account, year: $year, month: $month, day: $day, cover: ${cover == null ? null : "${cover!.fdPath}"}}";
|
||||
}
|
||||
}
|
87
app/lib/entity/collection/content_provider/nc_album.dart
Normal file
87
app/lib/entity/collection/content_provider/nc_album.dart
Normal file
|
@ -0,0 +1,87 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:copy_with/copy_with.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/util.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||
import 'package:nc_photos/entity/nc_album.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'nc_album.g.dart';
|
||||
|
||||
/// Album provided by our app
|
||||
@genCopyWith
|
||||
@toString
|
||||
class CollectionNcAlbumProvider
|
||||
with EquatableMixin
|
||||
implements CollectionContentProvider {
|
||||
const CollectionNcAlbumProvider({
|
||||
required this.account,
|
||||
required this.album,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
String get fourCc => "NC25";
|
||||
|
||||
@override
|
||||
String get id => album.path;
|
||||
|
||||
@override
|
||||
int? get count => album.count;
|
||||
|
||||
@override
|
||||
DateTime get lastModified => album.dateEnd ?? clock.now().toUtc();
|
||||
|
||||
@override
|
||||
List<CollectionCapability> get capabilities => [
|
||||
CollectionCapability.manualItem,
|
||||
CollectionCapability.rename,
|
||||
// CollectionCapability.share,
|
||||
];
|
||||
|
||||
@override
|
||||
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
||||
|
||||
@override
|
||||
List<CollectionShare> get shares => album.collaborators
|
||||
.map((c) => CollectionShare(
|
||||
userId: c.id,
|
||||
username: c.label,
|
||||
))
|
||||
.toList();
|
||||
|
||||
@override
|
||||
String? getCoverUrl(
|
||||
int width,
|
||||
int height, {
|
||||
bool? isKeepAspectRatio,
|
||||
}) {
|
||||
if (album.lastPhoto == null) {
|
||||
return null;
|
||||
} else {
|
||||
return api_util.getPhotosApiFilePreviewUrlByFileId(
|
||||
account,
|
||||
album.lastPhoto!,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isDynamicCollection => false;
|
||||
|
||||
@override
|
||||
bool get isPendingSharedAlbum => false;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [account, album];
|
||||
|
||||
final Account account;
|
||||
final NcAlbum album;
|
||||
}
|
48
app/lib/entity/collection/content_provider/nc_album.g.dart
Normal file
48
app/lib/entity/collection/content_provider/nc_album.g.dart
Normal file
|
@ -0,0 +1,48 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'nc_album.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithLintRuleGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class $CollectionNcAlbumProviderCopyWithWorker {
|
||||
CollectionNcAlbumProvider call({Account? account, NcAlbum? album});
|
||||
}
|
||||
|
||||
class _$CollectionNcAlbumProviderCopyWithWorkerImpl
|
||||
implements $CollectionNcAlbumProviderCopyWithWorker {
|
||||
_$CollectionNcAlbumProviderCopyWithWorkerImpl(this.that);
|
||||
|
||||
@override
|
||||
CollectionNcAlbumProvider call({dynamic account, dynamic album}) {
|
||||
return CollectionNcAlbumProvider(
|
||||
account: account as Account? ?? that.account,
|
||||
album: album as NcAlbum? ?? that.album);
|
||||
}
|
||||
|
||||
final CollectionNcAlbumProvider that;
|
||||
}
|
||||
|
||||
extension $CollectionNcAlbumProviderCopyWith on CollectionNcAlbumProvider {
|
||||
$CollectionNcAlbumProviderCopyWithWorker get copyWith => _$copyWith;
|
||||
$CollectionNcAlbumProviderCopyWithWorker get _$copyWith =>
|
||||
_$CollectionNcAlbumProviderCopyWithWorkerImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$CollectionNcAlbumProviderToString on CollectionNcAlbumProvider {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "CollectionNcAlbumProvider {account: $account, album: $album}";
|
||||
}
|
||||
}
|
62
app/lib/entity/collection/content_provider/person.dart
Normal file
62
app/lib/entity/collection/content_provider/person.dart
Normal file
|
@ -0,0 +1,62 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/util.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
|
||||
class CollectionPersonProvider
|
||||
with EquatableMixin
|
||||
implements CollectionContentProvider {
|
||||
const CollectionPersonProvider({
|
||||
required this.account,
|
||||
required this.person,
|
||||
});
|
||||
|
||||
@override
|
||||
String get fourCc => "PERS";
|
||||
|
||||
@override
|
||||
String get id => person.name;
|
||||
|
||||
@override
|
||||
int? get count => person.count;
|
||||
|
||||
@override
|
||||
DateTime get lastModified => clock.now().toUtc();
|
||||
|
||||
@override
|
||||
List<CollectionCapability> get capabilities => [];
|
||||
|
||||
@override
|
||||
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
||||
|
||||
@override
|
||||
List<CollectionShare> get shares => [];
|
||||
|
||||
@override
|
||||
String? getCoverUrl(
|
||||
int width,
|
||||
int height, {
|
||||
bool? isKeepAspectRatio,
|
||||
}) {
|
||||
return api_util.getFacePreviewUrl(account, person.thumbFaceId,
|
||||
size: math.max(width, height));
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isDynamicCollection => true;
|
||||
|
||||
@override
|
||||
bool get isPendingSharedAlbum => false;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [account, person];
|
||||
|
||||
final Account account;
|
||||
final Person person;
|
||||
}
|
57
app/lib/entity/collection/content_provider/tag.dart
Normal file
57
app/lib/entity/collection/content_provider/tag.dart
Normal file
|
@ -0,0 +1,57 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/util.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
|
||||
class CollectionTagProvider
|
||||
with EquatableMixin
|
||||
implements CollectionContentProvider {
|
||||
CollectionTagProvider({
|
||||
required this.account,
|
||||
required this.tags,
|
||||
}) : assert(tags.isNotEmpty);
|
||||
|
||||
@override
|
||||
String get fourCc => "TAG-";
|
||||
|
||||
@override
|
||||
String get id => tags.first.displayName;
|
||||
|
||||
@override
|
||||
int? get count => null;
|
||||
|
||||
@override
|
||||
DateTime get lastModified => clock.now().toUtc();
|
||||
|
||||
@override
|
||||
List<CollectionCapability> get capabilities => [];
|
||||
|
||||
@override
|
||||
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
||||
|
||||
@override
|
||||
List<CollectionShare> get shares => [];
|
||||
|
||||
@override
|
||||
String? getCoverUrl(
|
||||
int width,
|
||||
int height, {
|
||||
bool? isKeepAspectRatio,
|
||||
}) =>
|
||||
null;
|
||||
|
||||
@override
|
||||
bool get isDynamicCollection => true;
|
||||
|
||||
@override
|
||||
bool get isPendingSharedAlbum => false;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [account, tags];
|
||||
|
||||
final Account account;
|
||||
final List<Tag> tags;
|
||||
}
|
108
app/lib/entity/collection/exporter.dart
Normal file
108
app/lib/entity/collection/exporter.dart
Normal file
|
@ -0,0 +1,108 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/controller/collections_controller.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/cover_provider.dart';
|
||||
import 'package:nc_photos/entity/album/item.dart';
|
||||
import 'package:nc_photos/entity/album/provider.dart';
|
||||
import 'package:nc_photos/entity/album/sort_provider.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/album.dart';
|
||||
import 'package:nc_photos/entity/collection/content_provider/nc_album.dart';
|
||||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/nc_album.dart';
|
||||
import 'package:nc_photos/use_case/find_file.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
|
||||
part 'exporter.g.dart';
|
||||
|
||||
@npLog
|
||||
class CollectionExporter {
|
||||
const CollectionExporter(this.account, this.collectionsController,
|
||||
this.collection, this.items, this.exportName);
|
||||
|
||||
/// Export as a new collection backed by our client side album
|
||||
Future<Collection> asAlbum() async {
|
||||
final files = await FindFile(KiwiContainer().resolve<DiContainer>())(
|
||||
account,
|
||||
items.whereType<CollectionFileItem>().map((e) => e.file.fdId).toList(),
|
||||
onFileNotFound: (fileId) {
|
||||
_log.severe("[asAlbum] File not found: $fileId");
|
||||
},
|
||||
);
|
||||
final newAlbum = Album(
|
||||
name: exportName,
|
||||
provider: AlbumStaticProvider(
|
||||
items: items
|
||||
.map((e) {
|
||||
if (e is CollectionFileItem) {
|
||||
final f = files
|
||||
.firstWhereOrNull((f) => f.compareServerIdentity(e.file));
|
||||
if (f == null) {
|
||||
return null;
|
||||
} else {
|
||||
return AlbumFileItem(
|
||||
addedBy: account.userId,
|
||||
addedAt: clock.now().toUtc(),
|
||||
file: f,
|
||||
);
|
||||
}
|
||||
} else if (e is CollectionLabelItem) {
|
||||
return AlbumLabelItem(
|
||||
addedBy: account.userId,
|
||||
addedAt: clock.now().toUtc(),
|
||||
text: e.text,
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.whereNotNull()
|
||||
.toList(),
|
||||
latestItemTime: collection.lastModified,
|
||||
),
|
||||
coverProvider: const AlbumAutoCoverProvider(),
|
||||
sortProvider: const AlbumTimeSortProvider(isAscending: false),
|
||||
);
|
||||
var newCollection = Collection(
|
||||
name: exportName,
|
||||
contentProvider: CollectionAlbumProvider(
|
||||
account: account,
|
||||
album: newAlbum,
|
||||
),
|
||||
);
|
||||
return await collectionsController.createNew(newCollection);
|
||||
}
|
||||
|
||||
/// Export as a new collection backed by Nextcloud album
|
||||
Future<Collection> asNcAlbum() async {
|
||||
var newCollection = Collection(
|
||||
name: exportName,
|
||||
contentProvider: CollectionNcAlbumProvider(
|
||||
account: account,
|
||||
album: NcAlbum.createNew(account: account, name: exportName),
|
||||
),
|
||||
);
|
||||
newCollection = await collectionsController.createNew(newCollection);
|
||||
// only files are supported in NcAlbum
|
||||
final newFiles =
|
||||
items.whereType<CollectionFileItem>().map((e) => e.file).toList();
|
||||
final data = collectionsController
|
||||
.peekStream()
|
||||
.data
|
||||
.firstWhere((e) => e.collection.compareIdentity(newCollection));
|
||||
await data.controller.addFiles(newFiles);
|
||||
return newCollection;
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final CollectionsController collectionsController;
|
||||
final Collection collection;
|
||||
final List<CollectionItem> items;
|
||||
final String exportName;
|
||||
}
|
14
app/lib/entity/collection/exporter.g.dart
Normal file
14
app/lib/entity/collection/exporter.g.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'exporter.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$CollectionExporterNpLog on CollectionExporter {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("entity.collection.exporter.CollectionExporter");
|
||||
}
|
70
app/lib/entity/collection/util.dart
Normal file
70
app/lib/entity/collection/util.dart
Normal file
|
@ -0,0 +1,70 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:np_common/ci_string.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
part 'util.g.dart';
|
||||
|
||||
enum CollectionSort {
|
||||
dateDescending,
|
||||
dateAscending,
|
||||
nameAscending,
|
||||
nameDescending;
|
||||
|
||||
bool isAscending() {
|
||||
return this == CollectionSort.dateAscending ||
|
||||
this == CollectionSort.nameAscending;
|
||||
}
|
||||
}
|
||||
|
||||
@toString
|
||||
class CollectionShare with EquatableMixin {
|
||||
const CollectionShare({
|
||||
required this.userId,
|
||||
required this.username,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [userId, username];
|
||||
|
||||
final CiString userId;
|
||||
final String username;
|
||||
}
|
||||
|
||||
enum CollectionShareResult {
|
||||
ok,
|
||||
partial,
|
||||
}
|
||||
|
||||
extension CollectionListExtension on Iterable<Collection> {
|
||||
List<Collection> sortedBy(CollectionSort by) {
|
||||
return map<Tuple2<Comparable, Collection>>((e) {
|
||||
switch (by) {
|
||||
case CollectionSort.nameAscending:
|
||||
case CollectionSort.nameDescending:
|
||||
return Tuple2(e.name.toLowerCase(), e);
|
||||
|
||||
case CollectionSort.dateAscending:
|
||||
case CollectionSort.dateDescending:
|
||||
return Tuple2(e.contentProvider.lastModified, e);
|
||||
}
|
||||
})
|
||||
.sorted((a, b) {
|
||||
final x = by.isAscending() ? a : b;
|
||||
final y = by.isAscending() ? b : a;
|
||||
final tmp = x.item1.compareTo(y.item1);
|
||||
if (tmp != 0) {
|
||||
return tmp;
|
||||
} else {
|
||||
return x.item2.name.compareTo(y.item2.name);
|
||||
}
|
||||
})
|
||||
.map((e) => e.item2)
|
||||
.toList();
|
||||
}
|
||||
}
|
14
app/lib/entity/collection/util.g.dart
Normal file
14
app/lib/entity/collection/util.g.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'util.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$CollectionShareToString on CollectionShare {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "CollectionShare {userId: $userId, username: $username}";
|
||||
}
|
||||
}
|
22
app/lib/entity/collection_item.dart
Normal file
22
app/lib/entity/collection_item.dart
Normal file
|
@ -0,0 +1,22 @@
|
|||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
|
||||
/// An item in a [Collection]
|
||||
abstract class CollectionItem {
|
||||
const CollectionItem();
|
||||
}
|
||||
|
||||
abstract class CollectionFileItem implements CollectionItem {
|
||||
const CollectionFileItem();
|
||||
|
||||
FileDescriptor get file;
|
||||
}
|
||||
|
||||
abstract class CollectionLabelItem implements CollectionItem {
|
||||
const CollectionLabelItem();
|
||||
|
||||
/// An object used to identify this instance
|
||||
///
|
||||
/// [id] should be unique and stable
|
||||
Object get id;
|
||||
String get text;
|
||||
}
|
57
app/lib/entity/collection_item/album_item_adapter.dart
Normal file
57
app/lib/entity/collection_item/album_item_adapter.dart
Normal file
|
@ -0,0 +1,57 @@
|
|||
import 'package:nc_photos/entity/album/item.dart';
|
||||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'album_item_adapter.g.dart';
|
||||
|
||||
mixin AlbumAdaptedCollectionItem on CollectionItem {
|
||||
static AlbumAdaptedCollectionItem fromItem(AlbumItem item) {
|
||||
if (item is AlbumFileItem) {
|
||||
return CollectionFileItemAlbumAdapter(item);
|
||||
} else if (item is AlbumLabelItem) {
|
||||
return CollectionLabelItemAlbumAdapter(item);
|
||||
} else {
|
||||
throw ArgumentError("Unknown type: ${item.runtimeType}");
|
||||
}
|
||||
}
|
||||
|
||||
AlbumItem get albumItem;
|
||||
}
|
||||
|
||||
@toString
|
||||
class CollectionFileItemAlbumAdapter extends CollectionFileItem
|
||||
with AlbumAdaptedCollectionItem {
|
||||
const CollectionFileItemAlbumAdapter(this.item);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
FileDescriptor get file => item.file;
|
||||
|
||||
@override
|
||||
AlbumItem get albumItem => item;
|
||||
|
||||
final AlbumFileItem item;
|
||||
}
|
||||
|
||||
@toString
|
||||
class CollectionLabelItemAlbumAdapter extends CollectionLabelItem
|
||||
with AlbumAdaptedCollectionItem {
|
||||
const CollectionLabelItemAlbumAdapter(this.item);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
Object get id => item.addedAt;
|
||||
|
||||
@override
|
||||
String get text => item.text;
|
||||
|
||||
@override
|
||||
AlbumItem get albumItem => item;
|
||||
|
||||
final AlbumLabelItem item;
|
||||
}
|
23
app/lib/entity/collection_item/album_item_adapter.g.dart
Normal file
23
app/lib/entity/collection_item/album_item_adapter.g.dart
Normal file
|
@ -0,0 +1,23 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'album_item_adapter.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$CollectionFileItemAlbumAdapterToString
|
||||
on CollectionFileItemAlbumAdapter {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "CollectionFileItemAlbumAdapter {item: $item}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$CollectionLabelItemAlbumAdapterToString
|
||||
on CollectionLabelItemAlbumAdapter {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "CollectionLabelItemAlbumAdapter {item: $item}";
|
||||
}
|
||||
}
|
17
app/lib/entity/collection_item/basic_item.dart
Normal file
17
app/lib/entity/collection_item/basic_item.dart
Normal file
|
@ -0,0 +1,17 @@
|
|||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'basic_item.g.dart';
|
||||
|
||||
/// The basic form of [CollectionFileItem]
|
||||
@toString
|
||||
class BasicCollectionFileItem implements CollectionFileItem {
|
||||
const BasicCollectionFileItem(this.file);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
final FileDescriptor file;
|
||||
}
|
14
app/lib/entity/collection_item/basic_item.g.dart
Normal file
14
app/lib/entity/collection_item/basic_item.g.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'basic_item.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$BasicCollectionFileItemToString on BasicCollectionFileItem {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "BasicCollectionFileItem {file: ${file.fdPath}}";
|
||||
}
|
||||
}
|
20
app/lib/entity/collection_item/nc_album_item_adapter.dart
Normal file
20
app/lib/entity/collection_item/nc_album_item_adapter.dart
Normal file
|
@ -0,0 +1,20 @@
|
|||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/nc_album_item.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'nc_album_item_adapter.g.dart';
|
||||
|
||||
@toString
|
||||
class CollectionFileItemNcAlbumItemAdapter extends CollectionFileItem {
|
||||
const CollectionFileItemNcAlbumItemAdapter(this.item, [this.localFile]);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
FileDescriptor get file => localFile ?? item.toFile();
|
||||
|
||||
final NcAlbumItem item;
|
||||
final FileDescriptor? localFile;
|
||||
}
|
15
app/lib/entity/collection_item/nc_album_item_adapter.g.dart
Normal file
15
app/lib/entity/collection_item/nc_album_item_adapter.g.dart
Normal file
|
@ -0,0 +1,15 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'nc_album_item_adapter.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$CollectionFileItemNcAlbumItemAdapterToString
|
||||
on CollectionFileItemNcAlbumItemAdapter {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "CollectionFileItemNcAlbumItemAdapter {item: $item, localFile: ${localFile == null ? null : "${localFile!.fdPath}"}}";
|
||||
}
|
||||
}
|
42
app/lib/entity/collection_item/new_item.dart
Normal file
42
app/lib/entity/collection_item/new_item.dart
Normal file
|
@ -0,0 +1,42 @@
|
|||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'new_item.g.dart';
|
||||
|
||||
abstract class NewCollectionItem implements CollectionItem {}
|
||||
|
||||
/// A new [CollectionFileItem]
|
||||
///
|
||||
/// This class is for marking an intermediate item that has recently been added
|
||||
/// but not necessarily persisted yet to the provider of this collection
|
||||
@toString
|
||||
class NewCollectionFileItem implements CollectionFileItem, NewCollectionItem {
|
||||
const NewCollectionFileItem(this.file);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
final FileDescriptor file;
|
||||
}
|
||||
|
||||
/// A new [CollectionLabelItem]
|
||||
///
|
||||
/// This class is for marking an intermediate item that has recently been added
|
||||
/// but not necessarily persisted yet to the provider of this collection
|
||||
@toString
|
||||
class NewCollectionLabelItem implements CollectionLabelItem, NewCollectionItem {
|
||||
const NewCollectionLabelItem(this.text, this.createdAt);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
Object get id => createdAt;
|
||||
|
||||
@override
|
||||
final String text;
|
||||
|
||||
final DateTime createdAt;
|
||||
}
|
21
app/lib/entity/collection_item/new_item.g.dart
Normal file
21
app/lib/entity/collection_item/new_item.g.dart
Normal file
|
@ -0,0 +1,21 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'new_item.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$NewCollectionFileItemToString on NewCollectionFileItem {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "NewCollectionFileItem {file: ${file.fdPath}}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$NewCollectionLabelItemToString on NewCollectionLabelItem {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "NewCollectionLabelItem {text: $text, createdAt: $createdAt}";
|
||||
}
|
||||
}
|
148
app/lib/entity/collection_item/sorter.dart
Normal file
148
app/lib/entity/collection_item/sorter.dart
Normal file
|
@ -0,0 +1,148 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/entity/album/sort_provider.dart';
|
||||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
part 'sorter.g.dart';
|
||||
|
||||
abstract class CollectionSorter {
|
||||
const CollectionSorter();
|
||||
|
||||
static CollectionSorter fromSortType(CollectionItemSort type) {
|
||||
switch (type) {
|
||||
case CollectionItemSort.dateDescending:
|
||||
return const CollectionTimeSorter(isAscending: false);
|
||||
case CollectionItemSort.dateAscending:
|
||||
return const CollectionTimeSorter(isAscending: true);
|
||||
case CollectionItemSort.nameAscending:
|
||||
return const CollectionFilenameSorter(isAscending: true);
|
||||
case CollectionItemSort.nameDescending:
|
||||
return const CollectionFilenameSorter(isAscending: false);
|
||||
case CollectionItemSort.manual:
|
||||
return const CollectionNullSorter();
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a sorted copy of [items]
|
||||
List<CollectionItem> call(List<CollectionItem> items);
|
||||
}
|
||||
|
||||
/// Sort provider that does nothing
|
||||
class CollectionNullSorter implements CollectionSorter {
|
||||
const CollectionNullSorter();
|
||||
|
||||
@override
|
||||
List<CollectionItem> call(List<CollectionItem> items) {
|
||||
return List.of(items);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sort based on the time of the files
|
||||
class CollectionTimeSorter implements CollectionSorter {
|
||||
const CollectionTimeSorter({
|
||||
required this.isAscending,
|
||||
});
|
||||
|
||||
@override
|
||||
List<CollectionItem> call(List<CollectionItem> items) {
|
||||
DateTime? prevFileTime;
|
||||
return items
|
||||
.map((e) {
|
||||
if (e is CollectionFileItem) {
|
||||
// take the file time
|
||||
prevFileTime = e.file.fdDateTime;
|
||||
}
|
||||
// for non file items, use the sibling file's time
|
||||
return Tuple2(prevFileTime, e);
|
||||
})
|
||||
.stableSorted((x, y) {
|
||||
if (x.item1 == null && y.item1 == null) {
|
||||
return 0;
|
||||
} else if (x.item1 == null) {
|
||||
return -1;
|
||||
} else if (y.item1 == null) {
|
||||
return 1;
|
||||
} else {
|
||||
if (isAscending) {
|
||||
return x.item1!.compareTo(y.item1!);
|
||||
} else {
|
||||
return y.item1!.compareTo(x.item1!);
|
||||
}
|
||||
}
|
||||
})
|
||||
.map((e) => e.item2)
|
||||
.toList();
|
||||
}
|
||||
|
||||
final bool isAscending;
|
||||
}
|
||||
|
||||
/// Sort based on the name of the files
|
||||
class CollectionFilenameSorter implements CollectionSorter {
|
||||
const CollectionFilenameSorter({
|
||||
required this.isAscending,
|
||||
});
|
||||
|
||||
@override
|
||||
List<CollectionItem> call(List<CollectionItem> items) {
|
||||
String? prevFilename;
|
||||
return items
|
||||
.map((e) {
|
||||
if (e is CollectionFileItem) {
|
||||
// take the file name
|
||||
prevFilename = e.file.filename;
|
||||
}
|
||||
// for non file items, use the sibling file's name
|
||||
return Tuple2(prevFilename, e);
|
||||
})
|
||||
.stableSorted((x, y) {
|
||||
if (x.item1 == null && y.item1 == null) {
|
||||
return 0;
|
||||
} else if (x.item1 == null) {
|
||||
return -1;
|
||||
} else if (y.item1 == null) {
|
||||
return 1;
|
||||
} else {
|
||||
if (isAscending) {
|
||||
return compareNatural(x.item1!, y.item1!);
|
||||
} else {
|
||||
return compareNatural(y.item1!, x.item1!);
|
||||
}
|
||||
}
|
||||
})
|
||||
.map((e) => e.item2)
|
||||
.toList();
|
||||
}
|
||||
|
||||
final bool isAscending;
|
||||
}
|
||||
|
||||
@npLog
|
||||
class CollectionAlbumSortAdapter implements CollectionSorter {
|
||||
const CollectionAlbumSortAdapter(this.sort);
|
||||
|
||||
@override
|
||||
List<CollectionItem> call(List<CollectionItem> items) {
|
||||
final CollectionSorter sorter;
|
||||
if (sort is AlbumNullSortProvider) {
|
||||
sorter = const CollectionNullSorter();
|
||||
} else if (sort is AlbumTimeSortProvider) {
|
||||
sorter = CollectionTimeSorter(
|
||||
isAscending: (sort as AlbumTimeSortProvider).isAscending);
|
||||
} else if (sort is AlbumFilenameSortProvider) {
|
||||
sorter = CollectionFilenameSorter(
|
||||
isAscending: (sort as AlbumFilenameSortProvider).isAscending);
|
||||
} else {
|
||||
_log.shout("[call] Unknown type: ${sort.runtimeType}");
|
||||
throw UnsupportedError("Unknown type: ${sort.runtimeType}");
|
||||
}
|
||||
return sorter(items);
|
||||
}
|
||||
|
||||
final AlbumSortProvider sort;
|
||||
}
|
15
app/lib/entity/collection_item/sorter.g.dart
Normal file
15
app/lib/entity/collection_item/sorter.g.dart
Normal file
|
@ -0,0 +1,15 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'sorter.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$CollectionAlbumSortAdapterNpLog on CollectionAlbumSortAdapter {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log =
|
||||
Logger("entity.collection_item.sorter.CollectionAlbumSortAdapter");
|
||||
}
|
7
app/lib/entity/collection_item/util.dart
Normal file
7
app/lib/entity/collection_item/util.dart
Normal file
|
@ -0,0 +1,7 @@
|
|||
enum CollectionItemSort {
|
||||
dateDescending,
|
||||
dateAscending,
|
||||
nameAscending,
|
||||
nameDescending,
|
||||
manual;
|
||||
}
|
|
@ -390,7 +390,6 @@ class File with EquatableMixin implements FileDescriptor {
|
|||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
JsonObj toJson() {
|
||||
return {
|
||||
"path": path,
|
||||
|
@ -419,6 +418,9 @@ class File with EquatableMixin implements FileDescriptor {
|
|||
};
|
||||
}
|
||||
|
||||
@override
|
||||
JsonObj toFdJson() => FileDescriptor.toJson(this);
|
||||
|
||||
File copyWith({
|
||||
String? path,
|
||||
int? contentLength,
|
||||
|
@ -537,6 +539,15 @@ extension FileExtension on File {
|
|||
);
|
||||
|
||||
bool isOwned(CiString userId) => ownerId == null || ownerId == userId;
|
||||
|
||||
FileDescriptor toDescriptor() => FileDescriptor(
|
||||
fdPath: path,
|
||||
fdId: fileId!,
|
||||
fdMime: contentType,
|
||||
fdIsArchived: isArchived ?? false,
|
||||
fdIsFavorite: isFavorite ?? false,
|
||||
fdDateTime: bestDateTime,
|
||||
);
|
||||
}
|
||||
|
||||
class FileServerIdentityComparator {
|
||||
|
@ -575,7 +586,7 @@ class FileRepo {
|
|||
dataSrc.listMinimal(account, dir);
|
||||
|
||||
/// See [FileDataSource.remove]
|
||||
Future<void> remove(Account account, File file) =>
|
||||
Future<void> remove(Account account, FileDescriptor file) =>
|
||||
dataSrc.remove(account, file);
|
||||
|
||||
/// See [FileDataSource.getBinary]
|
||||
|
@ -659,7 +670,7 @@ abstract class FileDataSource {
|
|||
Future<List<File>> listMinimal(Account account, File dir);
|
||||
|
||||
/// Remove file
|
||||
Future<void> remove(Account account, File f);
|
||||
Future<void> remove(Account account, FileDescriptor f);
|
||||
|
||||
/// Read file as binary array
|
||||
Future<Uint8List> getBinary(Account account, File f);
|
||||
|
|
|
@ -90,10 +90,10 @@ class FileWebdavDataSource implements FileDataSource {
|
|||
}
|
||||
|
||||
@override
|
||||
remove(Account account, File f) async {
|
||||
_log.info("[remove] ${f.path}");
|
||||
remove(Account account, FileDescriptor f) async {
|
||||
_log.info("[remove] ${f.fdPath}");
|
||||
final response =
|
||||
await ApiUtil.fromAccount(account).files().delete(path: f.path);
|
||||
await ApiUtil.fromAccount(account).files().delete(path: f.fdPath);
|
||||
if (!response.isGood) {
|
||||
_log.severe("[remove] Failed requesting server: $response");
|
||||
throw ApiException(
|
||||
|
@ -435,8 +435,8 @@ class FileSqliteDbDataSource implements FileDataSource {
|
|||
}
|
||||
|
||||
@override
|
||||
remove(Account account, File f) {
|
||||
_log.info("[remove] ${f.path}");
|
||||
remove(Account account, FileDescriptor f) {
|
||||
_log.info("[remove] ${f.fdPath}");
|
||||
return FileSqliteCacheRemover(_c)(account, f);
|
||||
}
|
||||
|
||||
|
@ -549,13 +549,20 @@ class FileSqliteDbDataSource implements FileDataSource {
|
|||
}
|
||||
|
||||
@override
|
||||
move(
|
||||
Future<void> move(
|
||||
Account account,
|
||||
File f,
|
||||
String destination, {
|
||||
bool? shouldOverwrite,
|
||||
}) async {
|
||||
// do nothing
|
||||
_log.info("[move] ${f.path} to $destination");
|
||||
await _c.sqliteDb.use((db) async {
|
||||
await db.moveFileByFileId(
|
||||
sql.ByAccount.app(account),
|
||||
f.fileId!,
|
||||
File(path: destination).strippedPathWithEmpty,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -719,7 +726,7 @@ class FileCachedDataSource implements FileDataSource {
|
|||
}
|
||||
|
||||
@override
|
||||
remove(Account account, File f) async {
|
||||
remove(Account account, FileDescriptor f) async {
|
||||
await _remoteSrc.remove(account, f);
|
||||
try {
|
||||
await _sqliteDbSrc.remove(account, f);
|
||||
|
@ -786,7 +793,7 @@ class FileCachedDataSource implements FileDataSource {
|
|||
}
|
||||
|
||||
@override
|
||||
move(
|
||||
Future<void> move(
|
||||
Account account,
|
||||
File f,
|
||||
String destination, {
|
||||
|
@ -794,6 +801,13 @@ class FileCachedDataSource implements FileDataSource {
|
|||
}) async {
|
||||
await _remoteSrc.move(account, f, destination,
|
||||
shouldOverwrite: shouldOverwrite);
|
||||
try {
|
||||
await _sqliteDbSrc.move(account, f, destination);
|
||||
} catch (e, stackTrace) {
|
||||
// ignore cache failure
|
||||
_log.warning(
|
||||
"Failed while move: ${logFilename(f.strippedPath)}", e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -191,9 +191,7 @@ class FileSqliteCacheUpdater {
|
|||
) async {
|
||||
// query list of rowIds for files in [remoteFiles]
|
||||
final rowIds = await db.accountFileRowIdsByFileIds(
|
||||
remoteFiles.map((f) => f.fileId!),
|
||||
sqlAccount: dbAccount,
|
||||
);
|
||||
sql.ByAccount.sql(dbAccount), remoteFiles.map((f) => f.fileId!));
|
||||
final rowIdsMap = Map.fromEntries(rowIds.map((e) => MapEntry(e.fileId, e)));
|
||||
|
||||
final inserts = <sql.CompleteFileCompanion>[];
|
||||
|
@ -359,7 +357,7 @@ class FileSqliteCacheRemover {
|
|||
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
|
||||
|
||||
/// Remove a file/dir from cache
|
||||
Future<void> call(Account account, File f) async {
|
||||
Future<void> call(Account account, FileDescriptor f) async {
|
||||
await _c.sqliteDb.use((db) async {
|
||||
final dbAccount = await db.accountOf(account);
|
||||
final rowIds = await db.accountFileRowIdsOf(f, sqlAccount: dbAccount);
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
import 'package:path/path.dart' as path_lib;
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'file_descriptor.g.dart';
|
||||
|
||||
int compareFileDescriptorDateTimeDescending(
|
||||
FileDescriptor x, FileDescriptor y) {
|
||||
|
@ -13,6 +17,7 @@ int compareFileDescriptorDateTimeDescending(
|
|||
}
|
||||
}
|
||||
|
||||
@toString
|
||||
class FileDescriptor with EquatableMixin {
|
||||
const FileDescriptor({
|
||||
required this.fdPath,
|
||||
|
@ -29,18 +34,23 @@ class FileDescriptor with EquatableMixin {
|
|||
fdMime: json["fdMime"],
|
||||
fdIsArchived: json["fdIsArchived"],
|
||||
fdIsFavorite: json["fdIsFavorite"],
|
||||
fdDateTime: json["fdDateTime"],
|
||||
fdDateTime: DateTime.parse(json["fdDateTime"]),
|
||||
);
|
||||
|
||||
JsonObj toJson() => {
|
||||
"fdPath": fdPath,
|
||||
"fdId": fdId,
|
||||
"fdMime": fdMime,
|
||||
"fdIsArchived": fdIsArchived,
|
||||
"fdIsFavorite": fdIsFavorite,
|
||||
"fdDateTime": fdDateTime,
|
||||
static JsonObj toJson(FileDescriptor that) => {
|
||||
"fdPath": that.fdPath,
|
||||
"fdId": that.fdId,
|
||||
"fdMime": that.fdMime,
|
||||
"fdIsArchived": that.fdIsArchived,
|
||||
"fdIsFavorite": that.fdIsFavorite,
|
||||
"fdDateTime": that.fdDateTime.toUtc().toIso8601String(),
|
||||
};
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
JsonObj toFdJson() => toJson(this);
|
||||
|
||||
@override
|
||||
get props => [
|
||||
fdPath,
|
||||
|
@ -107,4 +117,14 @@ extension FileDescriptorExtension on FileDescriptor {
|
|||
|
||||
/// hashCode to be used with [compareServerIdentity]
|
||||
int get identityHashCode => fdId.hashCode;
|
||||
|
||||
File toFile() {
|
||||
return File(
|
||||
path: fdPath,
|
||||
fileId: fdId,
|
||||
contentType: fdMime,
|
||||
isArchived: fdIsArchived,
|
||||
isFavorite: fdIsFavorite,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
14
app/lib/entity/file_descriptor.g.dart
Normal file
14
app/lib/entity/file_descriptor.g.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'file_descriptor.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$FileDescriptorToString on FileDescriptor {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "FileDescriptor {fdPath: $fdPath, fdId: $fdId, fdMime: $fdMime, fdIsArchived: $fdIsArchived, fdIsFavorite: $fdIsFavorite, fdDateTime: $fdDateTime}";
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import 'package:nc_photos/entity/file.dart';
|
|||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:np_api/np_api.dart' as api;
|
||||
import 'package:np_common/ci_string.dart';
|
||||
import 'package:np_common/string_extension.dart';
|
||||
import 'package:path/path.dart' as path_lib;
|
||||
|
@ -20,8 +21,11 @@ bool isSupportedImageMime(String mime) =>
|
|||
bool isSupportedImageFormat(FileDescriptor file) =>
|
||||
isSupportedImageMime(file.fdMime ?? "");
|
||||
|
||||
bool isSupportedVideoMime(String mime) =>
|
||||
supportedVideoFormatMimes.contains(mime);
|
||||
|
||||
bool isSupportedVideoFormat(FileDescriptor file) =>
|
||||
isSupportedFormat(file) && file.fdMime?.startsWith("video/") == true;
|
||||
isSupportedVideoMime(file.fdMime ?? "");
|
||||
|
||||
bool isMetadataSupportedMime(String mime) =>
|
||||
_metadataSupportedFormatMimes.contains(mime);
|
||||
|
@ -32,21 +36,25 @@ bool isMetadataSupportedFormat(FileDescriptor file) =>
|
|||
bool isTrash(Account account, FileDescriptor file) =>
|
||||
file.fdPath.startsWith(api_util.getTrashbinPath(account));
|
||||
|
||||
bool isAlbumFile(Account account, File file) =>
|
||||
file.path.startsWith(remote_storage_util.getRemoteAlbumsDir(account));
|
||||
bool isAlbumFile(Account account, FileDescriptor file) =>
|
||||
file.fdPath.startsWith(remote_storage_util.getRemoteAlbumsDir(account));
|
||||
|
||||
bool isNcAlbumFile(Account account, FileDescriptor file) =>
|
||||
file.fdPath.startsWith("${api.ApiPhotos.path}/");
|
||||
|
||||
/// Return if [file] is located under [dir]
|
||||
///
|
||||
/// Return false if [file] is [dir] itself (since it's not "under")
|
||||
///
|
||||
/// See [isOrUnderDir]
|
||||
bool isUnderDir(File file, File dir) => file.path.startsWith("${dir.path}/");
|
||||
bool isUnderDir(FileDescriptor file, FileDescriptor dir) =>
|
||||
file.fdPath.startsWith("${dir.fdPath}/");
|
||||
|
||||
/// Return if [file] is [dir] or located under [dir]
|
||||
///
|
||||
/// See [isUnderDir]
|
||||
bool isOrUnderDir(File file, File dir) =>
|
||||
file.path == dir.path || isUnderDir(file, dir);
|
||||
bool isOrUnderDir(FileDescriptor file, FileDescriptor dir) =>
|
||||
file.fdPath == dir.fdPath || isUnderDir(file, dir);
|
||||
|
||||
/// Convert a stripped path to a full path
|
||||
///
|
||||
|
@ -118,6 +126,9 @@ final supportedFormatMimes = [
|
|||
final supportedImageFormatMimes =
|
||||
supportedFormatMimes.where((f) => f.startsWith("image/")).toList();
|
||||
|
||||
final supportedVideoFormatMimes =
|
||||
supportedFormatMimes.where((f) => f.startsWith("video/")).toList();
|
||||
|
||||
const _metadataSupportedFormatMimes = [
|
||||
"image/jpeg",
|
||||
"image/heic",
|
||||
|
|
142
app/lib/entity/nc_album.dart
Normal file
142
app/lib/entity/nc_album.dart
Normal file
|
@ -0,0 +1,142 @@
|
|||
import 'package:copy_with/copy_with.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:np_api/np_api.dart' as api;
|
||||
import 'package:np_common/ci_string.dart';
|
||||
import 'package:np_common/string_extension.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'nc_album.g.dart';
|
||||
|
||||
/// Server-side album since Nextcloud 25
|
||||
@toString
|
||||
@genCopyWith
|
||||
class NcAlbum with EquatableMixin {
|
||||
NcAlbum({
|
||||
required String path,
|
||||
required this.lastPhoto,
|
||||
required this.nbItems,
|
||||
required this.location,
|
||||
required this.dateStart,
|
||||
required this.dateEnd,
|
||||
required this.collaborators,
|
||||
}) : path = path.trimAny("/");
|
||||
|
||||
static NcAlbum createNew({
|
||||
required Account account,
|
||||
required String name,
|
||||
}) {
|
||||
return NcAlbum(
|
||||
path: "${api.ApiPhotos.path}/${account.userId}/albums/$name",
|
||||
lastPhoto: null,
|
||||
nbItems: 0,
|
||||
location: null,
|
||||
dateStart: null,
|
||||
dateEnd: null,
|
||||
collaborators: const [],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
path,
|
||||
lastPhoto,
|
||||
nbItems,
|
||||
location,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
collaborators,
|
||||
];
|
||||
|
||||
final String path;
|
||||
|
||||
/// File ID of the last photo
|
||||
///
|
||||
/// The API will return -1 if there's no photos in the album. It's mapped to
|
||||
/// null here instead
|
||||
final int? lastPhoto;
|
||||
|
||||
/// Items count
|
||||
final int nbItems;
|
||||
final String? location;
|
||||
final DateTime? dateStart;
|
||||
final DateTime? dateEnd;
|
||||
final List<NcAlbumCollaborator> collaborators;
|
||||
}
|
||||
|
||||
extension NcAlbumExtension on NcAlbum {
|
||||
/// Return the path of this file with the DAV part stripped
|
||||
///
|
||||
/// WebDAV file path: remote.php/dav/photos/{userId}/albums/{strippedPath}.
|
||||
/// If this path points to the user's root album path, return "."
|
||||
String get strippedPath {
|
||||
if (!path.startsWith("remote.php/dav/photos/")) {
|
||||
return path;
|
||||
}
|
||||
var begin = "remote.php/dav/photos/".length;
|
||||
begin = path.indexOf("/", begin);
|
||||
if (begin == -1) {
|
||||
return path;
|
||||
}
|
||||
if (path.slice(begin, begin + 7) != "/albums") {
|
||||
return path;
|
||||
}
|
||||
// /albums/
|
||||
begin += 8;
|
||||
final stripped = path.slice(begin);
|
||||
if (stripped.isEmpty) {
|
||||
return ".";
|
||||
} else {
|
||||
return stripped;
|
||||
}
|
||||
}
|
||||
|
||||
String getRenamedPath(String newName) {
|
||||
final i = path.indexOf("albums/");
|
||||
if (i == -1) {
|
||||
throw StateError("Invalid path: $path");
|
||||
}
|
||||
return "${path.substring(0, i + "albums/".length)}$newName";
|
||||
}
|
||||
|
||||
int get count => nbItems;
|
||||
|
||||
bool compareIdentity(NcAlbum other) {
|
||||
return path == other.path;
|
||||
}
|
||||
|
||||
int get identityHashCode => path.hashCode;
|
||||
}
|
||||
|
||||
@toString
|
||||
class NcAlbumCollaborator {
|
||||
const NcAlbumCollaborator({
|
||||
required this.id,
|
||||
required this.label,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
factory NcAlbumCollaborator.fromJson(JsonObj json) => NcAlbumCollaborator(
|
||||
id: CiString(json["id"]),
|
||||
label: json["label"],
|
||||
type: json["type"],
|
||||
);
|
||||
|
||||
JsonObj toJson() => {
|
||||
"id": id.raw,
|
||||
"label": label,
|
||||
"type": type,
|
||||
};
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final CiString id;
|
||||
final String label;
|
||||
// right now it's unclear what this variable represents
|
||||
final int type;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue