mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-24 18:38:48 +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_descriptor.dart';
|
||||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||||
import 'package:nc_photos/exception.dart';
|
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';
|
import 'package:to_string/to_string.dart';
|
||||||
|
|
||||||
part 'api_util.g.dart';
|
part 'api_util.g.dart';
|
||||||
|
@ -45,6 +45,11 @@ String getFilePreviewUrlRelative(
|
||||||
if (file_util.isTrash(account, file)) {
|
if (file_util.isTrash(account, file)) {
|
||||||
// trashbin does not support preview.png endpoint
|
// trashbin does not support preview.png endpoint
|
||||||
url = "index.php/apps/files_trashbin/preview?fileId=${file.fdId}";
|
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 {
|
} else {
|
||||||
url = "index.php/core/preview?fileId=${file.fdId}";
|
url = "index.php/core/preview?fileId=${file.fdId}";
|
||||||
}
|
}
|
||||||
|
@ -76,6 +81,16 @@ String getFilePreviewUrlByFileId(
|
||||||
return url;
|
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) {
|
String getFileUrl(Account account, FileDescriptor file) {
|
||||||
return "${account.url}/${getFileUrlRelative(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/face.dart';
|
||||||
import 'package:nc_photos/entity/favorite.dart';
|
import 'package:nc_photos/entity/favorite.dart';
|
||||||
import 'package:nc_photos/entity/file.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/person.dart';
|
||||||
|
import 'package:nc_photos/entity/server_status.dart';
|
||||||
import 'package:nc_photos/entity/share.dart';
|
import 'package:nc_photos/entity/share.dart';
|
||||||
import 'package:nc_photos/entity/sharee.dart';
|
import 'package:nc_photos/entity/sharee.dart';
|
||||||
import 'package:nc_photos/entity/tag.dart';
|
import 'package:nc_photos/entity/tag.dart';
|
||||||
|
@ -34,7 +37,6 @@ class ApiFavoriteConverter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@npLog
|
|
||||||
class ApiFileConverter {
|
class ApiFileConverter {
|
||||||
static File fromApi(api.File file) {
|
static File fromApi(api.File file) {
|
||||||
final metadata = file.customProperties?["com.nkming.nc_photos:metadata"]
|
final metadata = file.customProperties?["com.nkming.nc_photos:metadata"]
|
||||||
|
@ -78,20 +80,45 @@ class ApiFileConverter {
|
||||||
?.run((obj) => ImageLocation.fromJson(jsonDecode(obj))),
|
?.run((obj) => ImageLocation.fromJson(jsonDecode(obj))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static String _hrefToPath(String href) {
|
class ApiNcAlbumConverter {
|
||||||
final rawPath = href.trimLeftAny("/");
|
static NcAlbum fromApi(api.NcAlbum album) {
|
||||||
final pos = rawPath.indexOf("remote.php");
|
return NcAlbum(
|
||||||
if (pos == -1) {
|
path: _hrefToPath(album.href),
|
||||||
// what?
|
lastPhoto: (album.lastPhoto ?? -1) < 0 ? null : album.lastPhoto,
|
||||||
_log.warning("[_hrefToPath] Unknown href value: $rawPath");
|
nbItems: album.nbItems ?? 0,
|
||||||
return rawPath;
|
location: album.location,
|
||||||
} else {
|
dateStart: (album.dateRange?["start"] as int?)
|
||||||
return rawPath.substring(pos);
|
?.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 {
|
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 {
|
class ApiTagConverter {
|
||||||
static Tag fromApi(api.Tag tag) {
|
static Tag fromApi(api.Tag tag) {
|
||||||
return 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
|
// NpLogGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
extension _$ApiFileConverterNpLog on ApiFileConverter {
|
extension _$_NpLog on _ {
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
Logger get _log => log;
|
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/di_container.dart';
|
||||||
import 'package:nc_photos/entity/album.dart';
|
import 'package:nc_photos/entity/album.dart';
|
||||||
import 'package:nc_photos/entity/album/data_source.dart';
|
import 'package:nc_photos/entity/album/data_source.dart';
|
||||||
|
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.dart';
|
||||||
import 'package:nc_photos/entity/face/data_source.dart';
|
import 'package:nc_photos/entity/face/data_source.dart';
|
||||||
import 'package:nc_photos/entity/favorite.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/file/data_source.dart';
|
||||||
import 'package:nc_photos/entity/local_file.dart';
|
import 'package:nc_photos/entity/local_file.dart';
|
||||||
import 'package:nc_photos/entity/local_file/data_source.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.dart';
|
||||||
import 'package:nc_photos/entity/person/data_source.dart';
|
import 'package:nc_photos/entity/person/data_source.dart';
|
||||||
import 'package:nc_photos/entity/search.dart';
|
import 'package:nc_photos/entity/search.dart';
|
||||||
|
@ -198,7 +202,12 @@ Future<void> _initDiContainer(InitIsolateType isolateType) async {
|
||||||
c.sqliteDb = await _createDb(isolateType);
|
c.sqliteDb = await _createDb(isolateType);
|
||||||
|
|
||||||
c.albumRepo = AlbumRepo(AlbumCachedDataSource(c));
|
c.albumRepo = AlbumRepo(AlbumCachedDataSource(c));
|
||||||
|
c.albumRepoRemote = AlbumRepo(AlbumRemoteDataSource());
|
||||||
c.albumRepoLocal = AlbumRepo(AlbumSqliteDbDataSource(c));
|
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.faceRepo = const FaceRepo(FaceRemoteDataSource());
|
||||||
c.fileRepo = FileRepo(FileCachedDataSource(c));
|
c.fileRepo = FileRepo(FileCachedDataSource(c));
|
||||||
c.fileRepoRemote = const FileRepo(FileWebdavDataSource());
|
c.fileRepoRemote = const FileRepo(FileWebdavDataSource());
|
||||||
|
@ -214,6 +223,11 @@ Future<void> _initDiContainer(InitIsolateType isolateType) async {
|
||||||
c.tagRepoLocal = TagRepo(TagSqliteDbDataSource(c.sqliteDb));
|
c.tagRepoLocal = TagRepo(TagSqliteDbDataSource(c.sqliteDb));
|
||||||
c.taggedFileRepo = const TaggedFileRepo(TaggedFileRemoteDataSource());
|
c.taggedFileRepo = const TaggedFileRepo(TaggedFileRemoteDataSource());
|
||||||
c.searchRepo = SearchRepo(SearchSqliteDbDataSource(c));
|
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);
|
c.touchManager = TouchManager(c);
|
||||||
|
|
||||||
if (platform_k.isAndroid) {
|
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:kiwi/kiwi.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/controller/collections_controller.dart';
|
||||||
|
import 'package:nc_photos/controller/server_controller.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/entity/album.dart';
|
import 'package:nc_photos/entity/collection.dart';
|
||||||
import 'package:nc_photos/entity/person.dart';
|
import 'package:nc_photos/entity/person.dart';
|
||||||
import 'package:nc_photos/entity/tag.dart';
|
import 'package:nc_photos/entity/tag.dart';
|
||||||
import 'package:nc_photos/iterable_extension.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_location_group.dart';
|
||||||
import 'package:nc_photos/use_case/list_person.dart';
|
import 'package:nc_photos/use_case/list_person.dart';
|
||||||
import 'package:nc_photos/use_case/list_tag.dart';
|
import 'package:nc_photos/use_case/list_tag.dart';
|
||||||
|
@ -24,13 +26,13 @@ part 'home_search_suggestion.g.dart';
|
||||||
abstract class HomeSearchResult {}
|
abstract class HomeSearchResult {}
|
||||||
|
|
||||||
@toString
|
@toString
|
||||||
class HomeSearchAlbumResult implements HomeSearchResult {
|
class HomeSearchCollectionResult implements HomeSearchResult {
|
||||||
const HomeSearchAlbumResult(this.album);
|
const HomeSearchCollectionResult(this.collection);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => _$toString();
|
String toString() => _$toString();
|
||||||
|
|
||||||
final Album album;
|
final Collection collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@toString
|
@toString
|
||||||
|
@ -125,7 +127,8 @@ class HomeSearchSuggestionBlocFailure extends HomeSearchSuggestionBlocState {
|
||||||
@npLog
|
@npLog
|
||||||
class HomeSearchSuggestionBloc
|
class HomeSearchSuggestionBloc
|
||||||
extends Bloc<HomeSearchSuggestionBlocEvent, HomeSearchSuggestionBlocState> {
|
extends Bloc<HomeSearchSuggestionBlocEvent, HomeSearchSuggestionBlocState> {
|
||||||
HomeSearchSuggestionBloc(this.account)
|
HomeSearchSuggestionBloc(
|
||||||
|
this.account, this.collectionsController, this.serverController)
|
||||||
: super(const HomeSearchSuggestionBlocInit()) {
|
: super(const HomeSearchSuggestionBlocInit()) {
|
||||||
final c = KiwiContainer().resolve<DiContainer>();
|
final c = KiwiContainer().resolve<DiContainer>();
|
||||||
assert(require(c));
|
assert(require(c));
|
||||||
|
@ -187,13 +190,21 @@ class HomeSearchSuggestionBloc
|
||||||
Emitter<HomeSearchSuggestionBlocState> emit) async {
|
Emitter<HomeSearchSuggestionBlocState> emit) async {
|
||||||
final product = <_Searcheable>[];
|
final product = <_Searcheable>[];
|
||||||
try {
|
try {
|
||||||
final albums = await ListAlbum(_c)(account)
|
var collections = collectionsController
|
||||||
.where((event) => event is Album)
|
.peekStream()
|
||||||
|
.data
|
||||||
|
.map((e) => e.collection)
|
||||||
.toList();
|
.toList();
|
||||||
product.addAll(albums.map((a) => _AlbumSearcheable(a)));
|
if (collections.isEmpty) {
|
||||||
_log.info("[_onEventPreloadData] Loaded ${albums.length} albums");
|
collections = await ListCollection(_c,
|
||||||
|
serverController: serverController)(account)
|
||||||
|
.last;
|
||||||
|
}
|
||||||
|
product.addAll(collections.map(_CollectionSearcheable.new));
|
||||||
|
_log.info(
|
||||||
|
"[_onEventPreloadData] Loaded ${collections.length} collections");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log.warning("[_onEventPreloadData] Failed while ListAlbum", e);
|
_log.warning("[_onEventPreloadData] Failed while ListCollection", e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final tags = await ListTag(_c)(account);
|
final tags = await ListTag(_c)(account);
|
||||||
|
@ -239,6 +250,8 @@ class HomeSearchSuggestionBloc
|
||||||
}
|
}
|
||||||
|
|
||||||
final Account account;
|
final Account account;
|
||||||
|
final CollectionsController collectionsController;
|
||||||
|
final ServerController serverController;
|
||||||
late final DiContainer _c;
|
late final DiContainer _c;
|
||||||
|
|
||||||
final _search = Woozy<_Searcheable>(limit: 10);
|
final _search = Woozy<_Searcheable>(limit: 10);
|
||||||
|
@ -249,16 +262,16 @@ abstract class _Searcheable {
|
||||||
HomeSearchResult toResult();
|
HomeSearchResult toResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AlbumSearcheable implements _Searcheable {
|
class _CollectionSearcheable implements _Searcheable {
|
||||||
const _AlbumSearcheable(this.album);
|
const _CollectionSearcheable(this.collection);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
toKeywords() => [album.name.toCi()];
|
toKeywords() => [collection.name.toCi()];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
toResult() => HomeSearchAlbumResult(album);
|
toResult() => HomeSearchCollectionResult(collection);
|
||||||
|
|
||||||
final Album album;
|
final Collection collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TagSearcheable implements _Searcheable {
|
class _TagSearcheable implements _Searcheable {
|
||||||
|
|
|
@ -18,10 +18,10 @@ extension _$HomeSearchSuggestionBlocNpLog on HomeSearchSuggestionBloc {
|
||||||
// ToStringGenerator
|
// ToStringGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
extension _$HomeSearchAlbumResultToString on HomeSearchAlbumResult {
|
extension _$HomeSearchCollectionResultToString on HomeSearchCollectionResult {
|
||||||
String _$toString() {
|
String _$toString() {
|
||||||
// ignore: unnecessary_string_interpolations
|
// 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/entity/file_util.dart' as file_util;
|
||||||
import 'package:nc_photos/iterable_extension.dart';
|
import 'package:nc_photos/iterable_extension.dart';
|
||||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
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:nc_photos/use_case/ls.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
import 'package:to_string/to_string.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/account.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/entity/file.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/file_util.dart' as file_util;
|
||||||
import 'package:nc_photos/event/event.dart';
|
import 'package:nc_photos/event/event.dart';
|
||||||
import 'package:nc_photos/throttler.dart';
|
import 'package:nc_photos/throttler.dart';
|
||||||
|
@ -147,7 +148,7 @@ class ListLocationBloc
|
||||||
Future<LocationGroupResult> _query(ListLocationBlocQuery ev) =>
|
Future<LocationGroupResult> _query(ListLocationBlocQuery ev) =>
|
||||||
ListLocationGroup(_c.withLocalRepo())(ev.account);
|
ListLocationGroup(_c.withLocalRepo())(ev.account);
|
||||||
|
|
||||||
bool _isFileOfInterest(File file) {
|
bool _isFileOfInterest(FileDescriptor file) {
|
||||||
if (!file_util.isSupportedFormat(file)) {
|
if (!file_util.isSupportedFormat(file)) {
|
||||||
return false;
|
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,
|
s,
|
||||||
File(
|
File(
|
||||||
path: webdavPath,
|
path: webdavPath,
|
||||||
|
fileId: s.itemSource,
|
||||||
isCollection: true,
|
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;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isFileOfInterest(File file) {
|
bool _isFileOfInterest(FileDescriptor file) {
|
||||||
if (!file_util.isSupportedFormat(file)) {
|
if (!file_util.isSupportedFormat(file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/entity/file.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/file_util.dart' as file_util;
|
||||||
import 'package:nc_photos/entity/search.dart';
|
import 'package:nc_photos/entity/search.dart';
|
||||||
import 'package:nc_photos/event/event.dart';
|
import 'package:nc_photos/event/event.dart';
|
||||||
|
@ -198,7 +199,7 @@ class SearchBloc extends Bloc<SearchBlocEvent, SearchBlocState> {
|
||||||
Future<List<File>> _query(SearchBlocQuery ev) =>
|
Future<List<File>> _query(SearchBlocQuery ev) =>
|
||||||
Search(_c)(ev.account, ev.criteria);
|
Search(_c)(ev.account, ev.criteria);
|
||||||
|
|
||||||
bool _isFileOfInterest(File file) {
|
bool _isFileOfInterest(FileDescriptor file) {
|
||||||
if (!file_util.isSupportedFormat(file)) {
|
if (!file_util.isSupportedFormat(file)) {
|
||||||
return false;
|
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
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
part of 'album_picker.dart';
|
part of 'pref_controller.dart';
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// NpLogGenerator
|
// NpLogGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
extension _$_AlbumPickerStateNpLog on _AlbumPickerState {
|
extension _$PrefControllerNpLog on PrefController {
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
Logger get _log => log;
|
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.dart';
|
||||||
|
import 'package:nc_photos/entity/album/repo2.dart';
|
||||||
import 'package:nc_photos/entity/face.dart';
|
import 'package:nc_photos/entity/face.dart';
|
||||||
import 'package:nc_photos/entity/favorite.dart';
|
import 'package:nc_photos/entity/favorite.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/entity/local_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/person.dart';
|
||||||
import 'package:nc_photos/entity/search.dart';
|
import 'package:nc_photos/entity/search.dart';
|
||||||
import 'package:nc_photos/entity/share.dart';
|
import 'package:nc_photos/entity/share.dart';
|
||||||
|
@ -16,7 +18,11 @@ import 'package:nc_photos/touch_manager.dart';
|
||||||
|
|
||||||
enum DiType {
|
enum DiType {
|
||||||
albumRepo,
|
albumRepo,
|
||||||
|
albumRepoRemote,
|
||||||
albumRepoLocal,
|
albumRepoLocal,
|
||||||
|
albumRepo2,
|
||||||
|
albumRepo2Remote,
|
||||||
|
albumRepo2Local,
|
||||||
faceRepo,
|
faceRepo,
|
||||||
fileRepo,
|
fileRepo,
|
||||||
fileRepoRemote,
|
fileRepoRemote,
|
||||||
|
@ -33,6 +39,9 @@ enum DiType {
|
||||||
taggedFileRepo,
|
taggedFileRepo,
|
||||||
localFileRepo,
|
localFileRepo,
|
||||||
searchRepo,
|
searchRepo,
|
||||||
|
ncAlbumRepo,
|
||||||
|
ncAlbumRepoRemote,
|
||||||
|
ncAlbumRepoLocal,
|
||||||
pref,
|
pref,
|
||||||
sqliteDb,
|
sqliteDb,
|
||||||
touchManager,
|
touchManager,
|
||||||
|
@ -41,7 +50,11 @@ enum DiType {
|
||||||
class DiContainer {
|
class DiContainer {
|
||||||
DiContainer({
|
DiContainer({
|
||||||
AlbumRepo? albumRepo,
|
AlbumRepo? albumRepo,
|
||||||
|
AlbumRepo? albumRepoRemote,
|
||||||
AlbumRepo? albumRepoLocal,
|
AlbumRepo? albumRepoLocal,
|
||||||
|
AlbumRepo2? albumRepo2,
|
||||||
|
AlbumRepo2? albumRepo2Remote,
|
||||||
|
AlbumRepo2? albumRepo2Local,
|
||||||
FaceRepo? faceRepo,
|
FaceRepo? faceRepo,
|
||||||
FileRepo? fileRepo,
|
FileRepo? fileRepo,
|
||||||
FileRepo? fileRepoRemote,
|
FileRepo? fileRepoRemote,
|
||||||
|
@ -58,11 +71,18 @@ class DiContainer {
|
||||||
TaggedFileRepo? taggedFileRepo,
|
TaggedFileRepo? taggedFileRepo,
|
||||||
LocalFileRepo? localFileRepo,
|
LocalFileRepo? localFileRepo,
|
||||||
SearchRepo? searchRepo,
|
SearchRepo? searchRepo,
|
||||||
|
NcAlbumRepo? ncAlbumRepo,
|
||||||
|
NcAlbumRepo? ncAlbumRepoRemote,
|
||||||
|
NcAlbumRepo? ncAlbumRepoLocal,
|
||||||
Pref? pref,
|
Pref? pref,
|
||||||
sql.SqliteDb? sqliteDb,
|
sql.SqliteDb? sqliteDb,
|
||||||
TouchManager? touchManager,
|
TouchManager? touchManager,
|
||||||
}) : _albumRepo = albumRepo,
|
}) : _albumRepo = albumRepo,
|
||||||
|
_albumRepoRemote = albumRepoRemote,
|
||||||
_albumRepoLocal = albumRepoLocal,
|
_albumRepoLocal = albumRepoLocal,
|
||||||
|
_albumRepo2 = albumRepo2,
|
||||||
|
_albumRepo2Remote = albumRepo2Remote,
|
||||||
|
_albumRepo2Local = albumRepo2Local,
|
||||||
_faceRepo = faceRepo,
|
_faceRepo = faceRepo,
|
||||||
_fileRepo = fileRepo,
|
_fileRepo = fileRepo,
|
||||||
_fileRepoRemote = fileRepoRemote,
|
_fileRepoRemote = fileRepoRemote,
|
||||||
|
@ -79,6 +99,9 @@ class DiContainer {
|
||||||
_taggedFileRepo = taggedFileRepo,
|
_taggedFileRepo = taggedFileRepo,
|
||||||
_localFileRepo = localFileRepo,
|
_localFileRepo = localFileRepo,
|
||||||
_searchRepo = searchRepo,
|
_searchRepo = searchRepo,
|
||||||
|
_ncAlbumRepo = ncAlbumRepo,
|
||||||
|
_ncAlbumRepoRemote = ncAlbumRepoRemote,
|
||||||
|
_ncAlbumRepoLocal = ncAlbumRepoLocal,
|
||||||
_pref = pref,
|
_pref = pref,
|
||||||
_sqliteDb = sqliteDb,
|
_sqliteDb = sqliteDb,
|
||||||
_touchManager = touchManager;
|
_touchManager = touchManager;
|
||||||
|
@ -89,8 +112,16 @@ class DiContainer {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case DiType.albumRepo:
|
case DiType.albumRepo:
|
||||||
return contianer._albumRepo != null;
|
return contianer._albumRepo != null;
|
||||||
|
case DiType.albumRepoRemote:
|
||||||
|
return contianer._albumRepoRemote != null;
|
||||||
case DiType.albumRepoLocal:
|
case DiType.albumRepoLocal:
|
||||||
return contianer._albumRepoLocal != null;
|
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:
|
case DiType.faceRepo:
|
||||||
return contianer._faceRepo != null;
|
return contianer._faceRepo != null;
|
||||||
case DiType.fileRepo:
|
case DiType.fileRepo:
|
||||||
|
@ -123,6 +154,12 @@ class DiContainer {
|
||||||
return contianer._localFileRepo != null;
|
return contianer._localFileRepo != null;
|
||||||
case DiType.searchRepo:
|
case DiType.searchRepo:
|
||||||
return contianer._searchRepo != null;
|
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:
|
case DiType.pref:
|
||||||
return contianer._pref != null;
|
return contianer._pref != null;
|
||||||
case DiType.sqliteDb:
|
case DiType.sqliteDb:
|
||||||
|
@ -134,6 +171,7 @@ class DiContainer {
|
||||||
|
|
||||||
DiContainer copyWith({
|
DiContainer copyWith({
|
||||||
OrNull<AlbumRepo>? albumRepo,
|
OrNull<AlbumRepo>? albumRepo,
|
||||||
|
OrNull<AlbumRepo2>? albumRepo2,
|
||||||
OrNull<FaceRepo>? faceRepo,
|
OrNull<FaceRepo>? faceRepo,
|
||||||
OrNull<FileRepo>? fileRepo,
|
OrNull<FileRepo>? fileRepo,
|
||||||
OrNull<PersonRepo>? personRepo,
|
OrNull<PersonRepo>? personRepo,
|
||||||
|
@ -144,12 +182,14 @@ class DiContainer {
|
||||||
OrNull<TaggedFileRepo>? taggedFileRepo,
|
OrNull<TaggedFileRepo>? taggedFileRepo,
|
||||||
OrNull<LocalFileRepo>? localFileRepo,
|
OrNull<LocalFileRepo>? localFileRepo,
|
||||||
OrNull<SearchRepo>? searchRepo,
|
OrNull<SearchRepo>? searchRepo,
|
||||||
|
OrNull<NcAlbumRepo>? ncAlbumRepo,
|
||||||
OrNull<Pref>? pref,
|
OrNull<Pref>? pref,
|
||||||
OrNull<sql.SqliteDb>? sqliteDb,
|
OrNull<sql.SqliteDb>? sqliteDb,
|
||||||
OrNull<TouchManager>? touchManager,
|
OrNull<TouchManager>? touchManager,
|
||||||
}) {
|
}) {
|
||||||
return DiContainer(
|
return DiContainer(
|
||||||
albumRepo: albumRepo == null ? _albumRepo : albumRepo.obj,
|
albumRepo: albumRepo == null ? _albumRepo : albumRepo.obj,
|
||||||
|
albumRepo2: albumRepo2 == null ? _albumRepo2 : albumRepo2.obj,
|
||||||
faceRepo: faceRepo == null ? _faceRepo : faceRepo.obj,
|
faceRepo: faceRepo == null ? _faceRepo : faceRepo.obj,
|
||||||
fileRepo: fileRepo == null ? _fileRepo : fileRepo.obj,
|
fileRepo: fileRepo == null ? _fileRepo : fileRepo.obj,
|
||||||
personRepo: personRepo == null ? _personRepo : personRepo.obj,
|
personRepo: personRepo == null ? _personRepo : personRepo.obj,
|
||||||
|
@ -161,6 +201,7 @@ class DiContainer {
|
||||||
taggedFileRepo == null ? _taggedFileRepo : taggedFileRepo.obj,
|
taggedFileRepo == null ? _taggedFileRepo : taggedFileRepo.obj,
|
||||||
localFileRepo: localFileRepo == null ? _localFileRepo : localFileRepo.obj,
|
localFileRepo: localFileRepo == null ? _localFileRepo : localFileRepo.obj,
|
||||||
searchRepo: searchRepo == null ? _searchRepo : searchRepo.obj,
|
searchRepo: searchRepo == null ? _searchRepo : searchRepo.obj,
|
||||||
|
ncAlbumRepo: ncAlbumRepo == null ? _ncAlbumRepo : ncAlbumRepo.obj,
|
||||||
pref: pref == null ? _pref : pref.obj,
|
pref: pref == null ? _pref : pref.obj,
|
||||||
sqliteDb: sqliteDb == null ? _sqliteDb : sqliteDb.obj,
|
sqliteDb: sqliteDb == null ? _sqliteDb : sqliteDb.obj,
|
||||||
touchManager: touchManager == null ? _touchManager : touchManager.obj,
|
touchManager: touchManager == null ? _touchManager : touchManager.obj,
|
||||||
|
@ -168,7 +209,11 @@ class DiContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
AlbumRepo get albumRepo => _albumRepo!;
|
AlbumRepo get albumRepo => _albumRepo!;
|
||||||
|
AlbumRepo get albumRepoRemote => _albumRepoRemote!;
|
||||||
AlbumRepo get albumRepoLocal => _albumRepoLocal!;
|
AlbumRepo get albumRepoLocal => _albumRepoLocal!;
|
||||||
|
AlbumRepo2 get albumRepo2 => _albumRepo2!;
|
||||||
|
AlbumRepo2 get albumRepo2Remote => _albumRepo2Remote!;
|
||||||
|
AlbumRepo2 get albumRepo2Local => _albumRepo2Local!;
|
||||||
FaceRepo get faceRepo => _faceRepo!;
|
FaceRepo get faceRepo => _faceRepo!;
|
||||||
FileRepo get fileRepo => _fileRepo!;
|
FileRepo get fileRepo => _fileRepo!;
|
||||||
FileRepo get fileRepoRemote => _fileRepoRemote!;
|
FileRepo get fileRepoRemote => _fileRepoRemote!;
|
||||||
|
@ -185,6 +230,9 @@ class DiContainer {
|
||||||
TaggedFileRepo get taggedFileRepo => _taggedFileRepo!;
|
TaggedFileRepo get taggedFileRepo => _taggedFileRepo!;
|
||||||
LocalFileRepo get localFileRepo => _localFileRepo!;
|
LocalFileRepo get localFileRepo => _localFileRepo!;
|
||||||
SearchRepo get searchRepo => _searchRepo!;
|
SearchRepo get searchRepo => _searchRepo!;
|
||||||
|
NcAlbumRepo get ncAlbumRepo => _ncAlbumRepo!;
|
||||||
|
NcAlbumRepo get ncAlbumRepoRemote => _ncAlbumRepoRemote!;
|
||||||
|
NcAlbumRepo get ncAlbumRepoLocal => _ncAlbumRepoLocal!;
|
||||||
TouchManager get touchManager => _touchManager!;
|
TouchManager get touchManager => _touchManager!;
|
||||||
|
|
||||||
sql.SqliteDb get sqliteDb => _sqliteDb!;
|
sql.SqliteDb get sqliteDb => _sqliteDb!;
|
||||||
|
@ -195,11 +243,31 @@ class DiContainer {
|
||||||
_albumRepo = v;
|
_albumRepo = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set albumRepoRemote(AlbumRepo v) {
|
||||||
|
assert(_albumRepoRemote == null);
|
||||||
|
_albumRepoRemote = v;
|
||||||
|
}
|
||||||
|
|
||||||
set albumRepoLocal(AlbumRepo v) {
|
set albumRepoLocal(AlbumRepo v) {
|
||||||
assert(_albumRepoLocal == null);
|
assert(_albumRepoLocal == null);
|
||||||
_albumRepoLocal = v;
|
_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) {
|
set faceRepo(FaceRepo v) {
|
||||||
assert(_faceRepo == null);
|
assert(_faceRepo == null);
|
||||||
_faceRepo = v;
|
_faceRepo = v;
|
||||||
|
@ -280,6 +348,21 @@ class DiContainer {
|
||||||
_searchRepo = v;
|
_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) {
|
set touchManager(TouchManager v) {
|
||||||
assert(_touchManager == null);
|
assert(_touchManager == null);
|
||||||
_touchManager = v;
|
_touchManager = v;
|
||||||
|
@ -296,9 +379,13 @@ class DiContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
AlbumRepo? _albumRepo;
|
AlbumRepo? _albumRepo;
|
||||||
|
AlbumRepo? _albumRepoRemote;
|
||||||
// Explicitly request a AlbumRepo backed by local source
|
// Explicitly request a AlbumRepo backed by local source
|
||||||
AlbumRepo? _albumRepoLocal;
|
AlbumRepo? _albumRepoLocal;
|
||||||
FaceRepo? _faceRepo;
|
FaceRepo? _faceRepo;
|
||||||
|
AlbumRepo2? _albumRepo2;
|
||||||
|
AlbumRepo2? _albumRepo2Remote;
|
||||||
|
AlbumRepo2? _albumRepo2Local;
|
||||||
FileRepo? _fileRepo;
|
FileRepo? _fileRepo;
|
||||||
// Explicitly request a FileRepo backed by remote source
|
// Explicitly request a FileRepo backed by remote source
|
||||||
FileRepo? _fileRepoRemote;
|
FileRepo? _fileRepoRemote;
|
||||||
|
@ -316,6 +403,9 @@ class DiContainer {
|
||||||
TaggedFileRepo? _taggedFileRepo;
|
TaggedFileRepo? _taggedFileRepo;
|
||||||
LocalFileRepo? _localFileRepo;
|
LocalFileRepo? _localFileRepo;
|
||||||
SearchRepo? _searchRepo;
|
SearchRepo? _searchRepo;
|
||||||
|
NcAlbumRepo? _ncAlbumRepo;
|
||||||
|
NcAlbumRepo? _ncAlbumRepoRemote;
|
||||||
|
NcAlbumRepo? _ncAlbumRepoLocal;
|
||||||
TouchManager? _touchManager;
|
TouchManager? _touchManager;
|
||||||
|
|
||||||
sql.SqliteDb? _sqliteDb;
|
sql.SqliteDb? _sqliteDb;
|
||||||
|
@ -323,14 +413,28 @@ class DiContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DiContainerExtension on 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
|
/// Uses local repo if available
|
||||||
///
|
///
|
||||||
/// Notice that not all repo support this
|
/// Notice that not all repo support this
|
||||||
DiContainer withLocalRepo() => copyWith(
|
DiContainer withLocalRepo() => copyWith(
|
||||||
albumRepo: OrNull(albumRepoLocal),
|
albumRepo: OrNull(albumRepoLocal),
|
||||||
|
albumRepo2: OrNull(albumRepo2Local),
|
||||||
fileRepo: OrNull(fileRepoLocal),
|
fileRepo: OrNull(fileRepoLocal),
|
||||||
personRepo: OrNull(personRepoLocal),
|
personRepo: OrNull(personRepoLocal),
|
||||||
tagRepo: OrNull(tagRepoLocal),
|
tagRepo: OrNull(tagRepoLocal),
|
||||||
|
ncAlbumRepo: OrNull(ncAlbumRepoLocal),
|
||||||
);
|
);
|
||||||
|
|
||||||
DiContainer withLocalAlbumRepo() =>
|
DiContainer withLocalAlbumRepo() =>
|
||||||
|
|
|
@ -46,49 +46,56 @@ class Album with EquatableMixin {
|
||||||
final jsonVersion = json["version"];
|
final jsonVersion = json["version"];
|
||||||
JsonObj? result = json;
|
JsonObj? result = json;
|
||||||
if (jsonVersion < 2) {
|
if (jsonVersion < 2) {
|
||||||
result = upgraderFactory?.buildV1()?.call(result);
|
result = upgraderFactory?.buildV1()?.doJson(result);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (jsonVersion < 3) {
|
if (jsonVersion < 3) {
|
||||||
result = upgraderFactory?.buildV2()?.call(result);
|
result = upgraderFactory?.buildV2()?.doJson(result);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (jsonVersion < 4) {
|
if (jsonVersion < 4) {
|
||||||
result = upgraderFactory?.buildV3()?.call(result);
|
result = upgraderFactory?.buildV3()?.doJson(result);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (jsonVersion < 5) {
|
if (jsonVersion < 5) {
|
||||||
result = upgraderFactory?.buildV4()?.call(result);
|
result = upgraderFactory?.buildV4()?.doJson(result);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (jsonVersion < 6) {
|
if (jsonVersion < 6) {
|
||||||
result = upgraderFactory?.buildV5()?.call(result);
|
result = upgraderFactory?.buildV5()?.doJson(result);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (jsonVersion < 7) {
|
if (jsonVersion < 7) {
|
||||||
result = upgraderFactory?.buildV6()?.call(result);
|
result = upgraderFactory?.buildV6()?.doJson(result);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (jsonVersion < 8) {
|
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) {
|
if (result == null) {
|
||||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||||
return null;
|
return null;
|
||||||
|
@ -217,7 +224,7 @@ class Album with EquatableMixin {
|
||||||
final int savedVersion;
|
final int savedVersion;
|
||||||
|
|
||||||
/// versioning of this class, use to upgrade old persisted album
|
/// versioning of this class, use to upgrade old persisted album
|
||||||
static const version = 8;
|
static const version = 9;
|
||||||
|
|
||||||
static final _log = _$AlbumNpLog.log;
|
static final _log = _$AlbumNpLog.log;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,9 +26,6 @@ abstract class AlbumCoverProvider with EquatableMixin {
|
||||||
case AlbumManualCoverProvider._type:
|
case AlbumManualCoverProvider._type:
|
||||||
return AlbumManualCoverProvider.fromJson(
|
return AlbumManualCoverProvider.fromJson(
|
||||||
content.cast<String, dynamic>());
|
content.cast<String, dynamic>());
|
||||||
case AlbumMemoryCoverProvider._type:
|
|
||||||
return AlbumMemoryCoverProvider.fromJson(
|
|
||||||
content.cast<String, dynamic>());
|
|
||||||
default:
|
default:
|
||||||
_log.shout("[fromJson] Unknown type: $type");
|
_log.shout("[fromJson] Unknown type: $type");
|
||||||
throw ArgumentError.value(type, "type");
|
throw ArgumentError.value(type, "type");
|
||||||
|
@ -65,7 +62,7 @@ abstract class AlbumCoverProvider with EquatableMixin {
|
||||||
/// Cover selected automatically by us
|
/// Cover selected automatically by us
|
||||||
@toString
|
@toString
|
||||||
class AlbumAutoCoverProvider extends AlbumCoverProvider {
|
class AlbumAutoCoverProvider extends AlbumCoverProvider {
|
||||||
AlbumAutoCoverProvider({
|
const AlbumAutoCoverProvider({
|
||||||
this.coverFile,
|
this.coverFile,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -73,48 +70,48 @@ class AlbumAutoCoverProvider extends AlbumCoverProvider {
|
||||||
return AlbumAutoCoverProvider(
|
return AlbumAutoCoverProvider(
|
||||||
coverFile: json["coverFile"] == null
|
coverFile: json["coverFile"] == null
|
||||||
? 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
|
@override
|
||||||
String toString() => _$toString();
|
String toString() => _$toString();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
getCover(Album album) {
|
FileDescriptor? getCover(Album album) {
|
||||||
if (coverFile == null) {
|
if (coverFile == null && album.provider is AlbumStaticProvider) {
|
||||||
try {
|
|
||||||
// use the latest file as cover
|
// use the latest file as cover
|
||||||
return AlbumStaticProvider.of(album)
|
return getCoverByItems(AlbumStaticProvider.of(album).items);
|
||||||
.items
|
|
||||||
.whereType<AlbumFileItem>()
|
|
||||||
.map((e) => e.file)
|
|
||||||
.where((element) =>
|
|
||||||
file_util.isSupportedFormat(element) &&
|
|
||||||
(element.hasPreview ?? false))
|
|
||||||
.sorted(compareFileDateTimeDescending)
|
|
||||||
.first;
|
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return coverFile;
|
return coverFile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
get props => [
|
List<Object?> get props => [
|
||||||
coverFile,
|
coverFile,
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_toContentJson() {
|
JsonObj _toContentJson() {
|
||||||
return {
|
return {
|
||||||
if (coverFile != null) "coverFile": coverFile!.toJson(),
|
if (coverFile != null) "coverFile": coverFile!.toFdJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
final File? coverFile;
|
final FileDescriptor? coverFile;
|
||||||
|
|
||||||
static const _type = "auto";
|
static const _type = "auto";
|
||||||
}
|
}
|
||||||
|
@ -122,48 +119,12 @@ class AlbumAutoCoverProvider extends AlbumCoverProvider {
|
||||||
/// Cover picked by user
|
/// Cover picked by user
|
||||||
@toString
|
@toString
|
||||||
class AlbumManualCoverProvider extends AlbumCoverProvider {
|
class AlbumManualCoverProvider extends AlbumCoverProvider {
|
||||||
AlbumManualCoverProvider({
|
const AlbumManualCoverProvider({
|
||||||
required this.coverFile,
|
required this.coverFile,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory AlbumManualCoverProvider.fromJson(JsonObj json) {
|
factory AlbumManualCoverProvider.fromJson(JsonObj json) {
|
||||||
return AlbumManualCoverProvider(
|
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:
|
coverFile:
|
||||||
FileDescriptor.fromJson(json["coverFile"].cast<String, dynamic>()),
|
FileDescriptor.fromJson(json["coverFile"].cast<String, dynamic>()),
|
||||||
);
|
);
|
||||||
|
@ -173,19 +134,21 @@ class AlbumMemoryCoverProvider extends AlbumCoverProvider {
|
||||||
String toString() => _$toString();
|
String toString() => _$toString();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
getCover(Album album) => coverFile;
|
FileDescriptor? getCover(Album album) => coverFile;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
get props => [
|
List<Object?> get props => [
|
||||||
coverFile,
|
coverFile,
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_toContentJson() => {
|
JsonObj _toContentJson() {
|
||||||
"coverFile": coverFile.toJson(),
|
return {
|
||||||
|
"coverFile": coverFile.toFdJson(),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
final FileDescriptor coverFile;
|
final FileDescriptor coverFile;
|
||||||
|
|
||||||
static const _type = "memory";
|
static const _type = "manual";
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,20 +20,13 @@ extension _$AlbumCoverProviderNpLog on AlbumCoverProvider {
|
||||||
extension _$AlbumAutoCoverProviderToString on AlbumAutoCoverProvider {
|
extension _$AlbumAutoCoverProviderToString on AlbumAutoCoverProvider {
|
||||||
String _$toString() {
|
String _$toString() {
|
||||||
// ignore: unnecessary_string_interpolations
|
// ignore: unnecessary_string_interpolations
|
||||||
return "AlbumAutoCoverProvider {coverFile: ${coverFile == null ? null : "${coverFile!.path}"}}";
|
return "AlbumAutoCoverProvider {coverFile: ${coverFile == null ? null : "${coverFile!.fdPath}"}}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension _$AlbumManualCoverProviderToString on AlbumManualCoverProvider {
|
extension _$AlbumManualCoverProviderToString on AlbumManualCoverProvider {
|
||||||
String _$toString() {
|
String _$toString() {
|
||||||
// ignore: unnecessary_string_interpolations
|
// ignore: unnecessary_string_interpolations
|
||||||
return "AlbumManualCoverProvider {coverFile: ${coverFile.path}}";
|
return "AlbumManualCoverProvider {coverFile: ${coverFile.fdPath}}";
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension _$AlbumMemoryCoverProviderToString on AlbumMemoryCoverProvider {
|
|
||||||
String _$toString() {
|
|
||||||
// ignore: unnecessary_string_interpolations
|
|
||||||
return "AlbumMemoryCoverProvider {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:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/entity/album.dart';
|
import 'package:nc_photos/entity/album.dart';
|
||||||
import 'package:nc_photos/entity/album/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.dart';
|
||||||
import 'package:nc_photos/entity/file/data_source.dart';
|
|
||||||
import 'package:nc_photos/entity/file_descriptor.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/database.dart' as sql;
|
||||||
import 'package:nc_photos/entity/sqlite/type_converter.dart';
|
|
||||||
import 'package:nc_photos/exception.dart';
|
import 'package:nc_photos/exception.dart';
|
||||||
import 'package:nc_photos/exception_event.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/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';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
|
||||||
part 'data_source.g.dart';
|
part 'data_source.g.dart';
|
||||||
|
|
||||||
|
/// Backward compatibility only, use [AlbumRemoteDataSource2] instead
|
||||||
@npLog
|
@npLog
|
||||||
class AlbumRemoteDataSource implements AlbumDataSource {
|
class AlbumRemoteDataSource implements AlbumDataSource {
|
||||||
@override
|
@override
|
||||||
get(Account account, File albumFile) async {
|
get(Account account, File albumFile) async {
|
||||||
_log.info("[get] ${albumFile.path}");
|
_log.info("[get] ${albumFile.path}");
|
||||||
const fileRepo = FileRepo(FileWebdavDataSource());
|
final albums = await const AlbumRemoteDataSource2().getAlbums(
|
||||||
final data = await GetFileBinary(fileRepo)(account, albumFile);
|
account,
|
||||||
try {
|
[albumFile],
|
||||||
return Album.fromJson(
|
onError: (_, error, stackTrace) {
|
||||||
jsonDecode(utf8.decode(data)),
|
Error.throwWithStackTrace(error, stackTrace ?? StackTrace.current);
|
||||||
upgraderFactory: DefaultAlbumUpgraderFactory(
|
},
|
||||||
account: account,
|
|
||||||
albumFile: albumFile,
|
|
||||||
logFilePath: albumFile.path,
|
|
||||||
),
|
|
||||||
)!
|
|
||||||
.copyWith(
|
|
||||||
lastUpdated: OrNull(null),
|
|
||||||
albumFile: OrNull(albumFile),
|
|
||||||
);
|
);
|
||||||
} catch (e, stacktrace) {
|
return albums.first;
|
||||||
dynamic d = data;
|
|
||||||
try {
|
|
||||||
d = utf8.decode(data);
|
|
||||||
} catch (_) {}
|
|
||||||
_log.severe("[get] Invalid json data: $d", e, stacktrace);
|
|
||||||
throw const FormatException("Invalid album format");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
getAll(Account account, List<File> albumFiles) async* {
|
getAll(Account account, List<File> albumFiles) async* {
|
||||||
_log.info(
|
_log.info(
|
||||||
"[getAll] ${albumFiles.map((f) => f.filename).toReadableString()}");
|
"[getAll] ${albumFiles.map((f) => f.filename).toReadableString()}");
|
||||||
final results = await future_util.waitOr(
|
final failed = <String, Map>{};
|
||||||
albumFiles.map((f) => get(account, f)),
|
final albums = await const AlbumRemoteDataSource2().getAlbums(
|
||||||
(error, stackTrace) => ExceptionEvent(error, stackTrace),
|
account,
|
||||||
|
albumFiles,
|
||||||
|
onError: (v, error, stackTrace) {
|
||||||
|
failed[v.path] = {
|
||||||
|
"file": v,
|
||||||
|
"error": error,
|
||||||
|
"stackTrace": stackTrace,
|
||||||
|
};
|
||||||
|
},
|
||||||
);
|
);
|
||||||
for (final r in results) {
|
var i = 0;
|
||||||
yield r;
|
for (final af in albumFiles) {
|
||||||
|
final v = failed[af.path];
|
||||||
|
if (v != null) {
|
||||||
|
yield ExceptionEvent(v["error"], v["stackTrace"]);
|
||||||
|
} else {
|
||||||
|
yield albums[i++];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
create(Account account, Album album) async {
|
create(Account account, Album album) async {
|
||||||
_log.info("[create]");
|
_log.info("[create]");
|
||||||
final fileName = _makeAlbumFileName();
|
return const AlbumRemoteDataSource2().create(account, album);
|
||||||
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
|
@override
|
||||||
update(Account account, Album album) async {
|
update(Account account, Album album) async {
|
||||||
_log.info("[update] ${album.albumFile!.path}");
|
_log.info("[update] ${album.albumFile!.path}");
|
||||||
const fileRepo = FileRepo(FileWebdavDataSource());
|
return const AlbumRemoteDataSource2().update(account, album);
|
||||||
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";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Backward compatibility only, use [AlbumSqliteDbDataSource2] instead
|
||||||
@npLog
|
@npLog
|
||||||
class AlbumSqliteDbDataSource implements AlbumDataSource {
|
class AlbumSqliteDbDataSource implements AlbumDataSource {
|
||||||
AlbumSqliteDbDataSource(this._c);
|
AlbumSqliteDbDataSource(this._c);
|
||||||
|
@ -119,64 +89,29 @@ class AlbumSqliteDbDataSource implements AlbumDataSource {
|
||||||
getAll(Account account, List<File> albumFiles) async* {
|
getAll(Account account, List<File> albumFiles) async* {
|
||||||
_log.info(
|
_log.info(
|
||||||
"[getAll] ${albumFiles.map((f) => f.filename).toReadableString()}");
|
"[getAll] ${albumFiles.map((f) => f.filename).toReadableString()}");
|
||||||
late final List<sql.CompleteFile> dbFiles;
|
final failed = <String, Map>{};
|
||||||
late final List<sql.AlbumWithShare> albumWithShares;
|
final albums = await AlbumSqliteDbDataSource2(_c.sqliteDb).getAlbums(
|
||||||
await _c.sqliteDb.use((db) async {
|
account,
|
||||||
dbFiles = await db.completeFilesByFileIds(
|
albumFiles,
|
||||||
albumFiles.map((f) => f.fileId!),
|
onError: (v, error, stackTrace) {
|
||||||
appAccount: account,
|
failed[v.path] = {
|
||||||
);
|
"file": v,
|
||||||
final query = db.select(db.albums).join([
|
"error": error,
|
||||||
sql.leftOuterJoin(
|
"stackTrace": stackTrace,
|
||||||
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}");
|
|
||||||
} 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>[])
|
var i = 0;
|
||||||
.add(s.share!);
|
for (final af in albumFiles) {
|
||||||
}
|
final v = failed[af.path];
|
||||||
}
|
if (v != null) {
|
||||||
}
|
if (v["error"] is CacheNotFoundException) {
|
||||||
|
yield const CacheNotFoundException();
|
||||||
// sort as the input list
|
|
||||||
for (final item in albumFiles.map((f) => fileIdMap[f.fileId])) {
|
|
||||||
if (item == null) {
|
|
||||||
// cache not found
|
|
||||||
yield CacheNotFoundException();
|
|
||||||
} else {
|
} else {
|
||||||
try {
|
yield ExceptionEvent(v["error"], v["stackTrace"]);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
yield albums[i++];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -184,64 +119,22 @@ class AlbumSqliteDbDataSource implements AlbumDataSource {
|
||||||
@override
|
@override
|
||||||
create(Account account, Album album) async {
|
create(Account account, Album album) async {
|
||||||
_log.info("[create]");
|
_log.info("[create]");
|
||||||
throw UnimplementedError();
|
return AlbumSqliteDbDataSource2(_c.sqliteDb).create(account, album);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
update(Account account, Album album) async {
|
update(Account account, Album album) async {
|
||||||
_log.info("[update] ${album.albumFile!.path}");
|
_log.info("[update] ${album.albumFile!.path}");
|
||||||
await _c.sqliteDb.use((db) async {
|
return AlbumSqliteDbDataSource2(_c.sqliteDb).update(account, album);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final DiContainer _c;
|
final DiContainer _c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Backward compatibility only, use [CachedAlbumRepo2] instead
|
||||||
@npLog
|
@npLog
|
||||||
class AlbumCachedDataSource implements AlbumDataSource {
|
class AlbumCachedDataSource implements AlbumDataSource {
|
||||||
AlbumCachedDataSource(DiContainer c)
|
AlbumCachedDataSource(DiContainer c) : sqliteDb = c.sqliteDb;
|
||||||
: _sqliteDbSrc = AlbumSqliteDbDataSource(c);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
get(Account account, File albumFile) async {
|
get(Account account, File albumFile) async {
|
||||||
|
@ -251,58 +144,31 @@ class AlbumCachedDataSource implements AlbumDataSource {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
getAll(Account account, List<File> albumFiles) async* {
|
getAll(Account account, List<File> albumFiles) async* {
|
||||||
var i = 0;
|
final repo = CachedAlbumRepo2(
|
||||||
await for (final cache in _sqliteDbSrc.getAll(account, albumFiles)) {
|
const AlbumRemoteDataSource2(),
|
||||||
final albumFile = albumFiles[i++];
|
AlbumSqliteDbDataSource2(sqliteDb),
|
||||||
if (_validateCache(cache, albumFile)) {
|
);
|
||||||
yield cache;
|
final albums = await repo.getAlbums(account, albumFiles).last;
|
||||||
} else {
|
for (final a in albums) {
|
||||||
// no cache
|
yield a;
|
||||||
final remote = await _remoteSrc.get(account, albumFile);
|
|
||||||
await _cacheResult(account, remote);
|
|
||||||
yield remote;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
update(Account account, Album album) async {
|
update(Account account, Album album) {
|
||||||
await _remoteSrc.update(account, album);
|
return CachedAlbumRepo2(
|
||||||
await _sqliteDbSrc.update(account, album);
|
const AlbumRemoteDataSource2(),
|
||||||
|
AlbumSqliteDbDataSource2(sqliteDb),
|
||||||
|
).update(account, album);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
create(Account account, Album album) => _remoteSrc.create(account, album);
|
create(Account account, Album album) {
|
||||||
|
return CachedAlbumRepo2(
|
||||||
Future<void> _cacheResult(Account account, Album result) {
|
const AlbumRemoteDataSource2(),
|
||||||
return _sqliteDbSrc.update(account, result);
|
AlbumSqliteDbDataSource2(sqliteDb),
|
||||||
|
).create(account, album);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _validateCache(dynamic cache, File albumFile) {
|
final sql.SqliteDb sqliteDb;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
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";
|
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()}}";
|
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
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
part of 'list_favorite.dart';
|
part of 'repo2.dart';
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// NpLogGenerator
|
// NpLogGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
extension _$ListFavoriteNpLog on ListFavorite {
|
extension _$CachedAlbumRepo2NpLog on CachedAlbumRepo2 {
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
Logger get _log => log;
|
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:equatable/equatable.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/entity/album/item.dart';
|
import 'package:nc_photos/entity/album/item.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/collection_item/album_item_adapter.dart';
|
||||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
import 'package:nc_photos/entity/collection_item/sorter.dart';
|
||||||
import 'package:nc_photos/iterable_extension.dart';
|
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
import 'package:np_common/type.dart';
|
import 'package:np_common/type.dart';
|
||||||
import 'package:to_string/to_string.dart';
|
import 'package:to_string/to_string.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
|
||||||
|
|
||||||
part 'sort_provider.g.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() {
|
JsonObj toJson() {
|
||||||
String getType() {
|
String getType() {
|
||||||
if (this is AlbumNullSortProvider) {
|
if (this is AlbumNullSortProvider) {
|
||||||
|
@ -53,7 +67,31 @@ abstract class AlbumSortProvider with EquatableMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a sorted copy of [items]
|
/// 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();
|
JsonObj _toContentJson();
|
||||||
|
|
||||||
|
@ -72,11 +110,6 @@ class AlbumNullSortProvider extends AlbumSortProvider {
|
||||||
@override
|
@override
|
||||||
String toString() => _$toString();
|
String toString() => _$toString();
|
||||||
|
|
||||||
@override
|
|
||||||
sort(List<AlbumItem> items) {
|
|
||||||
return List.from(items);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
get props => [];
|
get props => [];
|
||||||
|
|
||||||
|
@ -124,37 +157,6 @@ class AlbumTimeSortProvider extends AlbumReversibleSortProvider {
|
||||||
@override
|
@override
|
||||||
String toString() => _$toString();
|
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";
|
static const _type = "time";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,36 +176,5 @@ class AlbumFilenameSortProvider extends AlbumReversibleSortProvider {
|
||||||
@override
|
@override
|
||||||
String toString() => _$toString();
|
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";
|
static const _type = "filename";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:clock/clock.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/entity/exif.dart';
|
import 'package:nc_photos/entity/exif.dart';
|
||||||
import 'package:nc_photos/entity/file.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_codegen/np_codegen.dart';
|
||||||
import 'package:np_common/ci_string.dart';
|
import 'package:np_common/ci_string.dart';
|
||||||
import 'package:np_common/type.dart';
|
import 'package:np_common/type.dart';
|
||||||
|
@ -11,7 +16,8 @@ import 'package:tuple/tuple.dart';
|
||||||
part 'upgrader.g.dart';
|
part 'upgrader.g.dart';
|
||||||
|
|
||||||
abstract class AlbumUpgrader {
|
abstract class AlbumUpgrader {
|
||||||
JsonObj? call(JsonObj json);
|
JsonObj? doJson(JsonObj json);
|
||||||
|
sql.Album? doDb(sql.Album dbObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Upgrade v1 Album to v2
|
/// Upgrade v1 Album to v2
|
||||||
|
@ -22,14 +28,17 @@ class AlbumUpgraderV1 implements AlbumUpgrader {
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
call(JsonObj json) {
|
doJson(JsonObj json) {
|
||||||
// v1 album items are corrupted in one of the updates, drop it
|
// 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);
|
final result = JsonObj.from(json);
|
||||||
result["items"] = [];
|
result["items"] = [];
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
sql.Album? doDb(sql.Album dbObj) => null;
|
||||||
|
|
||||||
/// File path for logging only
|
/// File path for logging only
|
||||||
final String? logFilePath;
|
final String? logFilePath;
|
||||||
}
|
}
|
||||||
|
@ -42,9 +51,9 @@ class AlbumUpgraderV2 implements AlbumUpgrader {
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
call(JsonObj json) {
|
doJson(JsonObj json) {
|
||||||
// move v2 items to v3 provider
|
// 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);
|
final result = JsonObj.from(json);
|
||||||
result["provider"] = <String, dynamic>{
|
result["provider"] = <String, dynamic>{
|
||||||
"type": "static",
|
"type": "static",
|
||||||
|
@ -62,6 +71,9 @@ class AlbumUpgraderV2 implements AlbumUpgrader {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
sql.Album? doDb(sql.Album dbObj) => null;
|
||||||
|
|
||||||
/// File path for logging only
|
/// File path for logging only
|
||||||
final String? logFilePath;
|
final String? logFilePath;
|
||||||
}
|
}
|
||||||
|
@ -74,9 +86,9 @@ class AlbumUpgraderV3 implements AlbumUpgrader {
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
call(JsonObj json) {
|
doJson(JsonObj json) {
|
||||||
// move v3 items to v4 provider
|
// 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);
|
final result = JsonObj.from(json);
|
||||||
// add the descending time sort provider
|
// add the descending time sort provider
|
||||||
result["sortProvider"] = <String, dynamic>{
|
result["sortProvider"] = <String, dynamic>{
|
||||||
|
@ -88,6 +100,9 @@ class AlbumUpgraderV3 implements AlbumUpgrader {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
sql.Album? doDb(sql.Album dbObj) => null;
|
||||||
|
|
||||||
/// File path for logging only
|
/// File path for logging only
|
||||||
final String? logFilePath;
|
final String? logFilePath;
|
||||||
}
|
}
|
||||||
|
@ -100,8 +115,8 @@ class AlbumUpgraderV4 implements AlbumUpgrader {
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
call(JsonObj json) {
|
doJson(JsonObj json) {
|
||||||
_log.fine("[call] Upgrade v4 Album for file: $logFilePath");
|
_log.fine("[doJson] Upgrade v4 Album for file: $logFilePath");
|
||||||
final result = JsonObj.from(json);
|
final result = JsonObj.from(json);
|
||||||
try {
|
try {
|
||||||
if (result["provider"]["type"] != "static") {
|
if (result["provider"]["type"] != "static") {
|
||||||
|
@ -151,11 +166,14 @@ class AlbumUpgraderV4 implements AlbumUpgrader {
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
// this upgrade is not a must, if it failed then just leave it and it'll
|
// 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
|
// 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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
sql.Album? doDb(sql.Album dbObj) => null;
|
||||||
|
|
||||||
/// File path for logging only
|
/// File path for logging only
|
||||||
final String? logFilePath;
|
final String? logFilePath;
|
||||||
}
|
}
|
||||||
|
@ -170,8 +188,8 @@ class AlbumUpgraderV5 implements AlbumUpgrader {
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
call(JsonObj json) {
|
doJson(JsonObj json) {
|
||||||
_log.fine("[call] Upgrade v5 Album for file: $logFilePath");
|
_log.fine("[doJson] Upgrade v5 Album for file: $logFilePath");
|
||||||
final result = JsonObj.from(json);
|
final result = JsonObj.from(json);
|
||||||
try {
|
try {
|
||||||
if (result["provider"]["type"] != "static") {
|
if (result["provider"]["type"] != "static") {
|
||||||
|
@ -192,11 +210,14 @@ class AlbumUpgraderV5 implements AlbumUpgrader {
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
// this upgrade is not a must, if it failed then just leave it and it'll
|
// 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
|
// 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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
sql.Album? doDb(sql.Album dbObj) => null;
|
||||||
|
|
||||||
final Account account;
|
final Account account;
|
||||||
final File? albumFile;
|
final File? albumFile;
|
||||||
|
|
||||||
|
@ -212,11 +233,14 @@ class AlbumUpgraderV6 implements AlbumUpgrader {
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
call(JsonObj json) {
|
doJson(JsonObj json) {
|
||||||
_log.fine("[call] Upgrade v6 Album for file: $logFilePath");
|
_log.fine("[doJson] Upgrade v6 Album for file: $logFilePath");
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
sql.Album? doDb(sql.Album dbObj) => null;
|
||||||
|
|
||||||
/// File path for logging only
|
/// File path for logging only
|
||||||
final String? logFilePath;
|
final String? logFilePath;
|
||||||
}
|
}
|
||||||
|
@ -229,11 +253,103 @@ class AlbumUpgraderV7 implements AlbumUpgrader {
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
call(JsonObj json) {
|
doJson(JsonObj json) {
|
||||||
_log.fine("[call] Upgrade v7 Album for file: $logFilePath");
|
_log.fine("[doJson] Upgrade v7 Album for file: $logFilePath");
|
||||||
return json;
|
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
|
/// File path for logging only
|
||||||
final String? logFilePath;
|
final String? logFilePath;
|
||||||
}
|
}
|
||||||
|
@ -248,6 +364,7 @@ abstract class AlbumUpgraderFactory {
|
||||||
AlbumUpgraderV5? buildV5();
|
AlbumUpgraderV5? buildV5();
|
||||||
AlbumUpgraderV6? buildV6();
|
AlbumUpgraderV6? buildV6();
|
||||||
AlbumUpgraderV7? buildV7();
|
AlbumUpgraderV7? buildV7();
|
||||||
|
AlbumUpgraderV8? buildV8();
|
||||||
}
|
}
|
||||||
|
|
||||||
class DefaultAlbumUpgraderFactory extends AlbumUpgraderFactory {
|
class DefaultAlbumUpgraderFactory extends AlbumUpgraderFactory {
|
||||||
|
@ -282,6 +399,9 @@ class DefaultAlbumUpgraderFactory extends AlbumUpgraderFactory {
|
||||||
@override
|
@override
|
||||||
buildV7() => AlbumUpgraderV7(logFilePath: logFilePath);
|
buildV7() => AlbumUpgraderV7(logFilePath: logFilePath);
|
||||||
|
|
||||||
|
@override
|
||||||
|
AlbumUpgraderV8? buildV8() => AlbumUpgraderV8(logFilePath: logFilePath);
|
||||||
|
|
||||||
final Account account;
|
final Account account;
|
||||||
final File? albumFile;
|
final File? albumFile;
|
||||||
|
|
||||||
|
|
|
@ -54,3 +54,10 @@ extension _$AlbumUpgraderV7NpLog on AlbumUpgraderV7 {
|
||||||
|
|
||||||
static final log = Logger("entity.album.upgrader.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
|
@override
|
||||||
String toString() => _$toString();
|
String toString() => _$toString();
|
||||||
|
|
||||||
@override
|
|
||||||
JsonObj toJson() {
|
JsonObj toJson() {
|
||||||
return {
|
return {
|
||||||
"path": path,
|
"path": path,
|
||||||
|
@ -419,6 +418,9 @@ class File with EquatableMixin implements FileDescriptor {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
JsonObj toFdJson() => FileDescriptor.toJson(this);
|
||||||
|
|
||||||
File copyWith({
|
File copyWith({
|
||||||
String? path,
|
String? path,
|
||||||
int? contentLength,
|
int? contentLength,
|
||||||
|
@ -537,6 +539,15 @@ extension FileExtension on File {
|
||||||
);
|
);
|
||||||
|
|
||||||
bool isOwned(CiString userId) => ownerId == null || ownerId == userId;
|
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 {
|
class FileServerIdentityComparator {
|
||||||
|
@ -575,7 +586,7 @@ class FileRepo {
|
||||||
dataSrc.listMinimal(account, dir);
|
dataSrc.listMinimal(account, dir);
|
||||||
|
|
||||||
/// See [FileDataSource.remove]
|
/// See [FileDataSource.remove]
|
||||||
Future<void> remove(Account account, File file) =>
|
Future<void> remove(Account account, FileDescriptor file) =>
|
||||||
dataSrc.remove(account, file);
|
dataSrc.remove(account, file);
|
||||||
|
|
||||||
/// See [FileDataSource.getBinary]
|
/// See [FileDataSource.getBinary]
|
||||||
|
@ -659,7 +670,7 @@ abstract class FileDataSource {
|
||||||
Future<List<File>> listMinimal(Account account, File dir);
|
Future<List<File>> listMinimal(Account account, File dir);
|
||||||
|
|
||||||
/// Remove file
|
/// Remove file
|
||||||
Future<void> remove(Account account, File f);
|
Future<void> remove(Account account, FileDescriptor f);
|
||||||
|
|
||||||
/// Read file as binary array
|
/// Read file as binary array
|
||||||
Future<Uint8List> getBinary(Account account, File f);
|
Future<Uint8List> getBinary(Account account, File f);
|
||||||
|
|
|
@ -90,10 +90,10 @@ class FileWebdavDataSource implements FileDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
remove(Account account, File f) async {
|
remove(Account account, FileDescriptor f) async {
|
||||||
_log.info("[remove] ${f.path}");
|
_log.info("[remove] ${f.fdPath}");
|
||||||
final response =
|
final response =
|
||||||
await ApiUtil.fromAccount(account).files().delete(path: f.path);
|
await ApiUtil.fromAccount(account).files().delete(path: f.fdPath);
|
||||||
if (!response.isGood) {
|
if (!response.isGood) {
|
||||||
_log.severe("[remove] Failed requesting server: $response");
|
_log.severe("[remove] Failed requesting server: $response");
|
||||||
throw ApiException(
|
throw ApiException(
|
||||||
|
@ -435,8 +435,8 @@ class FileSqliteDbDataSource implements FileDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
remove(Account account, File f) {
|
remove(Account account, FileDescriptor f) {
|
||||||
_log.info("[remove] ${f.path}");
|
_log.info("[remove] ${f.fdPath}");
|
||||||
return FileSqliteCacheRemover(_c)(account, f);
|
return FileSqliteCacheRemover(_c)(account, f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -549,13 +549,20 @@ class FileSqliteDbDataSource implements FileDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
move(
|
Future<void> move(
|
||||||
Account account,
|
Account account,
|
||||||
File f,
|
File f,
|
||||||
String destination, {
|
String destination, {
|
||||||
bool? shouldOverwrite,
|
bool? shouldOverwrite,
|
||||||
}) async {
|
}) 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
|
@override
|
||||||
|
@ -719,7 +726,7 @@ class FileCachedDataSource implements FileDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
remove(Account account, File f) async {
|
remove(Account account, FileDescriptor f) async {
|
||||||
await _remoteSrc.remove(account, f);
|
await _remoteSrc.remove(account, f);
|
||||||
try {
|
try {
|
||||||
await _sqliteDbSrc.remove(account, f);
|
await _sqliteDbSrc.remove(account, f);
|
||||||
|
@ -786,7 +793,7 @@ class FileCachedDataSource implements FileDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
move(
|
Future<void> move(
|
||||||
Account account,
|
Account account,
|
||||||
File f,
|
File f,
|
||||||
String destination, {
|
String destination, {
|
||||||
|
@ -794,6 +801,13 @@ class FileCachedDataSource implements FileDataSource {
|
||||||
}) async {
|
}) async {
|
||||||
await _remoteSrc.move(account, f, destination,
|
await _remoteSrc.move(account, f, destination,
|
||||||
shouldOverwrite: shouldOverwrite);
|
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
|
@override
|
||||||
|
|
|
@ -191,9 +191,7 @@ class FileSqliteCacheUpdater {
|
||||||
) async {
|
) async {
|
||||||
// query list of rowIds for files in [remoteFiles]
|
// query list of rowIds for files in [remoteFiles]
|
||||||
final rowIds = await db.accountFileRowIdsByFileIds(
|
final rowIds = await db.accountFileRowIdsByFileIds(
|
||||||
remoteFiles.map((f) => f.fileId!),
|
sql.ByAccount.sql(dbAccount), remoteFiles.map((f) => f.fileId!));
|
||||||
sqlAccount: dbAccount,
|
|
||||||
);
|
|
||||||
final rowIdsMap = Map.fromEntries(rowIds.map((e) => MapEntry(e.fileId, e)));
|
final rowIdsMap = Map.fromEntries(rowIds.map((e) => MapEntry(e.fileId, e)));
|
||||||
|
|
||||||
final inserts = <sql.CompleteFileCompanion>[];
|
final inserts = <sql.CompleteFileCompanion>[];
|
||||||
|
@ -359,7 +357,7 @@ class FileSqliteCacheRemover {
|
||||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
|
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
|
||||||
|
|
||||||
/// Remove a file/dir from cache
|
/// 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 {
|
await _c.sqliteDb.use((db) async {
|
||||||
final dbAccount = await db.accountOf(account);
|
final dbAccount = await db.accountOf(account);
|
||||||
final rowIds = await db.accountFileRowIdsOf(f, sqlAccount: dbAccount);
|
final rowIds = await db.accountFileRowIdsOf(f, sqlAccount: dbAccount);
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:np_common/type.dart';
|
import 'package:np_common/type.dart';
|
||||||
import 'package:path/path.dart' as path_lib;
|
import 'package:path/path.dart' as path_lib;
|
||||||
|
import 'package:to_string/to_string.dart';
|
||||||
|
|
||||||
|
part 'file_descriptor.g.dart';
|
||||||
|
|
||||||
int compareFileDescriptorDateTimeDescending(
|
int compareFileDescriptorDateTimeDescending(
|
||||||
FileDescriptor x, FileDescriptor y) {
|
FileDescriptor x, FileDescriptor y) {
|
||||||
|
@ -13,6 +17,7 @@ int compareFileDescriptorDateTimeDescending(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
class FileDescriptor with EquatableMixin {
|
class FileDescriptor with EquatableMixin {
|
||||||
const FileDescriptor({
|
const FileDescriptor({
|
||||||
required this.fdPath,
|
required this.fdPath,
|
||||||
|
@ -29,18 +34,23 @@ class FileDescriptor with EquatableMixin {
|
||||||
fdMime: json["fdMime"],
|
fdMime: json["fdMime"],
|
||||||
fdIsArchived: json["fdIsArchived"],
|
fdIsArchived: json["fdIsArchived"],
|
||||||
fdIsFavorite: json["fdIsFavorite"],
|
fdIsFavorite: json["fdIsFavorite"],
|
||||||
fdDateTime: json["fdDateTime"],
|
fdDateTime: DateTime.parse(json["fdDateTime"]),
|
||||||
);
|
);
|
||||||
|
|
||||||
JsonObj toJson() => {
|
static JsonObj toJson(FileDescriptor that) => {
|
||||||
"fdPath": fdPath,
|
"fdPath": that.fdPath,
|
||||||
"fdId": fdId,
|
"fdId": that.fdId,
|
||||||
"fdMime": fdMime,
|
"fdMime": that.fdMime,
|
||||||
"fdIsArchived": fdIsArchived,
|
"fdIsArchived": that.fdIsArchived,
|
||||||
"fdIsFavorite": fdIsFavorite,
|
"fdIsFavorite": that.fdIsFavorite,
|
||||||
"fdDateTime": fdDateTime,
|
"fdDateTime": that.fdDateTime.toUtc().toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
JsonObj toFdJson() => toJson(this);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
get props => [
|
get props => [
|
||||||
fdPath,
|
fdPath,
|
||||||
|
@ -107,4 +117,14 @@ extension FileDescriptorExtension on FileDescriptor {
|
||||||
|
|
||||||
/// hashCode to be used with [compareServerIdentity]
|
/// hashCode to be used with [compareServerIdentity]
|
||||||
int get identityHashCode => fdId.hashCode;
|
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/entity/file_descriptor.dart';
|
||||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
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/ci_string.dart';
|
||||||
import 'package:np_common/string_extension.dart';
|
import 'package:np_common/string_extension.dart';
|
||||||
import 'package:path/path.dart' as path_lib;
|
import 'package:path/path.dart' as path_lib;
|
||||||
|
@ -20,8 +21,11 @@ bool isSupportedImageMime(String mime) =>
|
||||||
bool isSupportedImageFormat(FileDescriptor file) =>
|
bool isSupportedImageFormat(FileDescriptor file) =>
|
||||||
isSupportedImageMime(file.fdMime ?? "");
|
isSupportedImageMime(file.fdMime ?? "");
|
||||||
|
|
||||||
|
bool isSupportedVideoMime(String mime) =>
|
||||||
|
supportedVideoFormatMimes.contains(mime);
|
||||||
|
|
||||||
bool isSupportedVideoFormat(FileDescriptor file) =>
|
bool isSupportedVideoFormat(FileDescriptor file) =>
|
||||||
isSupportedFormat(file) && file.fdMime?.startsWith("video/") == true;
|
isSupportedVideoMime(file.fdMime ?? "");
|
||||||
|
|
||||||
bool isMetadataSupportedMime(String mime) =>
|
bool isMetadataSupportedMime(String mime) =>
|
||||||
_metadataSupportedFormatMimes.contains(mime);
|
_metadataSupportedFormatMimes.contains(mime);
|
||||||
|
@ -32,21 +36,25 @@ bool isMetadataSupportedFormat(FileDescriptor file) =>
|
||||||
bool isTrash(Account account, FileDescriptor file) =>
|
bool isTrash(Account account, FileDescriptor file) =>
|
||||||
file.fdPath.startsWith(api_util.getTrashbinPath(account));
|
file.fdPath.startsWith(api_util.getTrashbinPath(account));
|
||||||
|
|
||||||
bool isAlbumFile(Account account, File file) =>
|
bool isAlbumFile(Account account, FileDescriptor file) =>
|
||||||
file.path.startsWith(remote_storage_util.getRemoteAlbumsDir(account));
|
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 if [file] is located under [dir]
|
||||||
///
|
///
|
||||||
/// Return false if [file] is [dir] itself (since it's not "under")
|
/// Return false if [file] is [dir] itself (since it's not "under")
|
||||||
///
|
///
|
||||||
/// See [isOrUnderDir]
|
/// 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]
|
/// Return if [file] is [dir] or located under [dir]
|
||||||
///
|
///
|
||||||
/// See [isUnderDir]
|
/// See [isUnderDir]
|
||||||
bool isOrUnderDir(File file, File dir) =>
|
bool isOrUnderDir(FileDescriptor file, FileDescriptor dir) =>
|
||||||
file.path == dir.path || isUnderDir(file, dir);
|
file.fdPath == dir.fdPath || isUnderDir(file, dir);
|
||||||
|
|
||||||
/// Convert a stripped path to a full path
|
/// Convert a stripped path to a full path
|
||||||
///
|
///
|
||||||
|
@ -118,6 +126,9 @@ final supportedFormatMimes = [
|
||||||
final supportedImageFormatMimes =
|
final supportedImageFormatMimes =
|
||||||
supportedFormatMimes.where((f) => f.startsWith("image/")).toList();
|
supportedFormatMimes.where((f) => f.startsWith("image/")).toList();
|
||||||
|
|
||||||
|
final supportedVideoFormatMimes =
|
||||||
|
supportedFormatMimes.where((f) => f.startsWith("video/")).toList();
|
||||||
|
|
||||||
const _metadataSupportedFormatMimes = [
|
const _metadataSupportedFormatMimes = [
|
||||||
"image/jpeg",
|
"image/jpeg",
|
||||||
"image/heic",
|
"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