mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 08:46:18 +01:00
Rewrite collection support and add NC25 album support
This commit is contained in:
parent
7c798a024d
commit
f3901b5ff0
215 changed files with 12187 additions and 5480 deletions
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_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_nextcloud_album.png
Normal file
BIN
app/assets/ic_nextcloud_album.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 344 B |
|
@ -4,6 +4,7 @@ import 'package:logging/logging.dart';
|
|||
import 'package:nc_photos/entity/face.dart';
|
||||
import 'package:nc_photos/entity/favorite.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/nc_album.dart';
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
import 'package:nc_photos/entity/sharee.dart';
|
||||
|
@ -94,6 +95,22 @@ class ApiFileConverter {
|
|||
static final _log = _$ApiFileConverterNpLog.log;
|
||||
}
|
||||
|
||||
class ApiNcAlbumConverter {
|
||||
static NcAlbum fromApi(api.NcAlbum album) {
|
||||
return NcAlbum(
|
||||
path: album.href,
|
||||
lastPhoto: (album.lastPhoto ?? -1) < 0 ? null : album.lastPhoto,
|
||||
nbItems: album.nbItems ?? 0,
|
||||
location: album.location,
|
||||
dateStart: (album.dateRange?["start"] as int?)
|
||||
?.run((d) => DateTime.fromMillisecondsSinceEpoch(d * 1000)),
|
||||
dateEnd: (album.dateRange?["end"] as int?)
|
||||
?.run((d) => DateTime.fromMillisecondsSinceEpoch(d * 1000)),
|
||||
collaborators: const [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ApiPersonConverter {
|
||||
static Person fromApi(api.Person person) {
|
||||
return Person(
|
||||
|
|
|
@ -8,6 +8,8 @@ import 'package:nc_photos/debug_util.dart';
|
|||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/data_source.dart';
|
||||
import 'package:nc_photos/entity/album/data_source2.dart';
|
||||
import 'package:nc_photos/entity/album/repo2.dart';
|
||||
import 'package:nc_photos/entity/face.dart';
|
||||
import 'package:nc_photos/entity/face/data_source.dart';
|
||||
import 'package:nc_photos/entity/favorite.dart';
|
||||
|
@ -16,6 +18,8 @@ import 'package:nc_photos/entity/file.dart';
|
|||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/entity/local_file.dart';
|
||||
import 'package:nc_photos/entity/local_file/data_source.dart';
|
||||
import 'package:nc_photos/entity/nc_album/data_source.dart';
|
||||
import 'package:nc_photos/entity/nc_album/repo.dart';
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/entity/person/data_source.dart';
|
||||
import 'package:nc_photos/entity/search.dart';
|
||||
|
@ -198,7 +202,12 @@ Future<void> _initDiContainer(InitIsolateType isolateType) async {
|
|||
c.sqliteDb = await _createDb(isolateType);
|
||||
|
||||
c.albumRepo = AlbumRepo(AlbumCachedDataSource(c));
|
||||
c.albumRepoRemote = AlbumRepo(AlbumRemoteDataSource());
|
||||
c.albumRepoLocal = AlbumRepo(AlbumSqliteDbDataSource(c));
|
||||
c.albumRepo2 = CachedAlbumRepo2(
|
||||
const AlbumRemoteDataSource2(), AlbumSqliteDbDataSource2(c.sqliteDb));
|
||||
c.albumRepo2Remote = const BasicAlbumRepo2(AlbumRemoteDataSource2());
|
||||
c.albumRepo2Local = BasicAlbumRepo2(AlbumSqliteDbDataSource2(c.sqliteDb));
|
||||
c.faceRepo = const FaceRepo(FaceRemoteDataSource());
|
||||
c.fileRepo = FileRepo(FileCachedDataSource(c));
|
||||
c.fileRepoRemote = const FileRepo(FileWebdavDataSource());
|
||||
|
@ -214,6 +223,11 @@ Future<void> _initDiContainer(InitIsolateType isolateType) async {
|
|||
c.tagRepoLocal = TagRepo(TagSqliteDbDataSource(c.sqliteDb));
|
||||
c.taggedFileRepo = const TaggedFileRepo(TaggedFileRemoteDataSource());
|
||||
c.searchRepo = SearchRepo(SearchSqliteDbDataSource(c));
|
||||
c.ncAlbumRepo = CachedNcAlbumRepo(
|
||||
const NcAlbumRemoteDataSource(), NcAlbumSqliteDbDataSource(c.sqliteDb));
|
||||
c.ncAlbumRepoRemote = const BasicNcAlbumRepo(NcAlbumRemoteDataSource());
|
||||
c.ncAlbumRepoLocal = BasicNcAlbumRepo(NcAlbumSqliteDbDataSource(c.sqliteDb));
|
||||
|
||||
c.touchManager = TouchManager(c);
|
||||
|
||||
if (platform_k.isAndroid) {
|
||||
|
|
|
@ -4,12 +4,13 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/controller/collections_controller.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/use_case/list_album.dart';
|
||||
import 'package:nc_photos/use_case/collection/list_collection.dart';
|
||||
import 'package:nc_photos/use_case/list_location_group.dart';
|
||||
import 'package:nc_photos/use_case/list_person.dart';
|
||||
import 'package:nc_photos/use_case/list_tag.dart';
|
||||
|
@ -24,13 +25,13 @@ part 'home_search_suggestion.g.dart';
|
|||
abstract class HomeSearchResult {}
|
||||
|
||||
@toString
|
||||
class HomeSearchAlbumResult implements HomeSearchResult {
|
||||
const HomeSearchAlbumResult(this.album);
|
||||
class HomeSearchCollectionResult implements HomeSearchResult {
|
||||
const HomeSearchCollectionResult(this.collection);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Album album;
|
||||
final Collection collection;
|
||||
}
|
||||
|
||||
@toString
|
||||
|
@ -125,7 +126,7 @@ class HomeSearchSuggestionBlocFailure extends HomeSearchSuggestionBlocState {
|
|||
@npLog
|
||||
class HomeSearchSuggestionBloc
|
||||
extends Bloc<HomeSearchSuggestionBlocEvent, HomeSearchSuggestionBlocState> {
|
||||
HomeSearchSuggestionBloc(this.account)
|
||||
HomeSearchSuggestionBloc(this.account, this.collectionsController)
|
||||
: super(const HomeSearchSuggestionBlocInit()) {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
assert(require(c));
|
||||
|
@ -187,13 +188,19 @@ class HomeSearchSuggestionBloc
|
|||
Emitter<HomeSearchSuggestionBlocState> emit) async {
|
||||
final product = <_Searcheable>[];
|
||||
try {
|
||||
final albums = await ListAlbum(_c)(account)
|
||||
.where((event) => event is Album)
|
||||
var collections = collectionsController
|
||||
.peekStream()
|
||||
.data
|
||||
.map((e) => e.collection)
|
||||
.toList();
|
||||
product.addAll(albums.map((a) => _AlbumSearcheable(a)));
|
||||
_log.info("[_onEventPreloadData] Loaded ${albums.length} albums");
|
||||
if (collections.isEmpty) {
|
||||
collections = await ListCollection(_c)(account).last;
|
||||
}
|
||||
product.addAll(collections.map(_CollectionSearcheable.new));
|
||||
_log.info(
|
||||
"[_onEventPreloadData] Loaded ${collections.length} collections");
|
||||
} catch (e) {
|
||||
_log.warning("[_onEventPreloadData] Failed while ListAlbum", e);
|
||||
_log.warning("[_onEventPreloadData] Failed while ListCollection", e);
|
||||
}
|
||||
try {
|
||||
final tags = await ListTag(_c)(account);
|
||||
|
@ -239,6 +246,7 @@ class HomeSearchSuggestionBloc
|
|||
}
|
||||
|
||||
final Account account;
|
||||
final CollectionsController collectionsController;
|
||||
late final DiContainer _c;
|
||||
|
||||
final _search = Woozy<_Searcheable>(limit: 10);
|
||||
|
@ -249,16 +257,16 @@ abstract class _Searcheable {
|
|||
HomeSearchResult toResult();
|
||||
}
|
||||
|
||||
class _AlbumSearcheable implements _Searcheable {
|
||||
const _AlbumSearcheable(this.album);
|
||||
class _CollectionSearcheable implements _Searcheable {
|
||||
const _CollectionSearcheable(this.collection);
|
||||
|
||||
@override
|
||||
toKeywords() => [album.name.toCi()];
|
||||
toKeywords() => [collection.name.toCi()];
|
||||
|
||||
@override
|
||||
toResult() => HomeSearchAlbumResult(album);
|
||||
toResult() => HomeSearchCollectionResult(collection);
|
||||
|
||||
final Album album;
|
||||
final Collection collection;
|
||||
}
|
||||
|
||||
class _TagSearcheable implements _Searcheable {
|
||||
|
|
|
@ -18,10 +18,10 @@ extension _$HomeSearchSuggestionBlocNpLog on HomeSearchSuggestionBloc {
|
|||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$HomeSearchAlbumResultToString on HomeSearchAlbumResult {
|
||||
extension _$HomeSearchCollectionResultToString on HomeSearchCollectionResult {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "HomeSearchAlbumResult {album: $album}";
|
||||
return "HomeSearchCollectionResult {collection: $collection}";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,338 +0,0 @@
|
|||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/bloc/bloc_util.dart' as bloc_util;
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/exception_event.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:nc_photos/throttler.dart';
|
||||
import 'package:nc_photos/use_case/list_album.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'list_album.g.dart';
|
||||
|
||||
class ListAlbumBlocItem {
|
||||
ListAlbumBlocItem(this.album);
|
||||
|
||||
final Album album;
|
||||
}
|
||||
|
||||
abstract class ListAlbumBlocEvent {
|
||||
const ListAlbumBlocEvent();
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListAlbumBlocQuery extends ListAlbumBlocEvent {
|
||||
const ListAlbumBlocQuery(this.account);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account account;
|
||||
}
|
||||
|
||||
/// An external event has happened and may affect the state of this bloc
|
||||
@toString
|
||||
class _ListAlbumBlocExternalEvent extends ListAlbumBlocEvent {
|
||||
const _ListAlbumBlocExternalEvent();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
@toString
|
||||
abstract class ListAlbumBlocState {
|
||||
const ListAlbumBlocState(this.account, this.items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account? account;
|
||||
final List<ListAlbumBlocItem> items;
|
||||
}
|
||||
|
||||
class ListAlbumBlocInit extends ListAlbumBlocState {
|
||||
const ListAlbumBlocInit() : super(null, const []);
|
||||
}
|
||||
|
||||
class ListAlbumBlocLoading extends ListAlbumBlocState {
|
||||
const ListAlbumBlocLoading(Account? account, List<ListAlbumBlocItem> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
class ListAlbumBlocSuccess extends ListAlbumBlocState {
|
||||
const ListAlbumBlocSuccess(Account? account, List<ListAlbumBlocItem> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListAlbumBlocFailure extends ListAlbumBlocState {
|
||||
const ListAlbumBlocFailure(
|
||||
Account? account, List<ListAlbumBlocItem> items, this.exception)
|
||||
: super(account, items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final dynamic exception;
|
||||
}
|
||||
|
||||
/// The state of this bloc is inconsistent. This typically means that the data
|
||||
/// may have been changed externally
|
||||
class ListAlbumBlocInconsistent extends ListAlbumBlocState {
|
||||
const ListAlbumBlocInconsistent(
|
||||
Account? account, List<ListAlbumBlocItem> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
@npLog
|
||||
class ListAlbumBloc extends Bloc<ListAlbumBlocEvent, ListAlbumBlocState> {
|
||||
/// Constructor
|
||||
///
|
||||
/// If [offlineC] is not null, this [DiContainer] will be used when requesting
|
||||
/// offline contents, otherwise [_c] will be used
|
||||
ListAlbumBloc(
|
||||
this._c, [
|
||||
DiContainer? offlineC,
|
||||
]) : _offlineC = offlineC ?? _c,
|
||||
assert(require(_c)),
|
||||
assert(offlineC == null || require(offlineC)),
|
||||
assert(ListAlbum.require(_c)),
|
||||
assert(offlineC == null || ListAlbum.require(offlineC)),
|
||||
super(const ListAlbumBlocInit()) {
|
||||
_albumUpdatedListener =
|
||||
AppEventListener<AlbumUpdatedEvent>(_onAlbumUpdatedEvent);
|
||||
_fileRemovedListener =
|
||||
AppEventListener<FileRemovedEvent>(_onFileRemovedEvent);
|
||||
_albumCreatedListener =
|
||||
AppEventListener<AlbumCreatedEvent>(_onAlbumCreatedEvent);
|
||||
_albumUpdatedListener.begin();
|
||||
_fileRemovedListener.begin();
|
||||
_albumCreatedListener.begin();
|
||||
_fileMovedListener.begin();
|
||||
_shareCreatedListener.begin();
|
||||
_shareRemovedListener.begin();
|
||||
|
||||
_refreshThrottler = Throttler(
|
||||
onTriggered: (_) {
|
||||
add(const _ListAlbumBlocExternalEvent());
|
||||
},
|
||||
logTag: "ListAlbumBloc.refresh",
|
||||
);
|
||||
|
||||
on<ListAlbumBlocEvent>(_onEvent);
|
||||
}
|
||||
|
||||
static bool require(DiContainer c) => true;
|
||||
|
||||
static ListAlbumBloc of(Account account) {
|
||||
final name = bloc_util.getInstNameForAccount("ListAlbumBloc", account);
|
||||
try {
|
||||
_log.fine("[of] Resolving bloc for '$name'");
|
||||
return KiwiContainer().resolve<ListAlbumBloc>(name);
|
||||
} catch (_) {
|
||||
// no created instance for this account, make a new one
|
||||
_log.info("[of] New bloc instance for account: $account");
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final offlineC = c.copyWith(
|
||||
fileRepo: OrNull(c.fileRepoLocal),
|
||||
albumRepo: OrNull(c.albumRepoLocal),
|
||||
);
|
||||
final bloc = ListAlbumBloc(c, offlineC);
|
||||
KiwiContainer().registerInstance<ListAlbumBloc>(bloc, name: name);
|
||||
return bloc;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
close() {
|
||||
_albumUpdatedListener.end();
|
||||
_fileRemovedListener.end();
|
||||
_albumCreatedListener.end();
|
||||
_fileMovedListener.end();
|
||||
_shareCreatedListener.end();
|
||||
_shareRemovedListener.end();
|
||||
_refreshThrottler.clear();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _onEvent(
|
||||
ListAlbumBlocEvent event, Emitter<ListAlbumBlocState> emit) async {
|
||||
_log.info("[_onEvent] $event");
|
||||
if (event is ListAlbumBlocQuery) {
|
||||
await _onEventQuery(event, emit);
|
||||
} else if (event is _ListAlbumBlocExternalEvent) {
|
||||
await _onExternalEvent(event, emit);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onEventQuery(
|
||||
ListAlbumBlocQuery ev, Emitter<ListAlbumBlocState> emit) async {
|
||||
emit(ListAlbumBlocLoading(ev.account, state.items));
|
||||
bool hasContent = state.items.isNotEmpty;
|
||||
|
||||
if (!hasContent) {
|
||||
// show something instantly on first load
|
||||
final cacheState = await _queryOffline(ev);
|
||||
emit(ListAlbumBlocLoading(ev.account, cacheState.items));
|
||||
hasContent = cacheState.items.isNotEmpty;
|
||||
}
|
||||
|
||||
final newState = await _queryOnline(ev);
|
||||
if (newState is ListAlbumBlocFailure) {
|
||||
emit(ListAlbumBlocFailure(
|
||||
ev.account,
|
||||
newState.items.isNotEmpty ? newState.items : state.items,
|
||||
newState.exception));
|
||||
} else {
|
||||
emit(newState);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onExternalEvent(
|
||||
_ListAlbumBlocExternalEvent ev, Emitter<ListAlbumBlocState> emit) async {
|
||||
emit(ListAlbumBlocInconsistent(state.account, state.items));
|
||||
}
|
||||
|
||||
void _onAlbumUpdatedEvent(AlbumUpdatedEvent ev) {
|
||||
if (state is ListAlbumBlocInit) {
|
||||
// no data in this bloc, ignore
|
||||
return;
|
||||
}
|
||||
if (_isAccountOfInterest(ev.account)) {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onFileRemovedEvent(FileRemovedEvent ev) {
|
||||
if (state is ListAlbumBlocInit) {
|
||||
// no data in this bloc, ignore
|
||||
return;
|
||||
}
|
||||
if (_isAccountOfInterest(ev.account) &&
|
||||
file_util.isAlbumFile(ev.account, ev.file)) {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onFileMovedEvent(FileMovedEvent ev) {
|
||||
if (state is ListAlbumBlocInit) {
|
||||
// no data in this bloc, ignore
|
||||
return;
|
||||
}
|
||||
if (_isAccountOfInterest(ev.account)) {
|
||||
if (ev.destination
|
||||
.startsWith(remote_storage_util.getRemoteAlbumsDir(ev.account)) ||
|
||||
ev.file.path
|
||||
.startsWith(remote_storage_util.getRemoteAlbumsDir(ev.account))) {
|
||||
// moving from/to album dir
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onAlbumCreatedEvent(AlbumCreatedEvent ev) {
|
||||
if (state is ListAlbumBlocInit) {
|
||||
// no data in this bloc, ignore
|
||||
return;
|
||||
}
|
||||
if (_isAccountOfInterest(ev.account)) {
|
||||
add(const _ListAlbumBlocExternalEvent());
|
||||
}
|
||||
}
|
||||
|
||||
void _onShareCreatedEvent(ShareCreatedEvent ev) =>
|
||||
_onShareChanged(ev.account, ev.share);
|
||||
|
||||
void _onShareRemovedEvent(ShareRemovedEvent ev) =>
|
||||
_onShareChanged(ev.account, ev.share);
|
||||
|
||||
void _onShareChanged(Account account, Share share) {
|
||||
if (_isAccountOfInterest(account)) {
|
||||
final webdavPath = file_util.unstripPath(account, share.path);
|
||||
if (webdavPath
|
||||
.startsWith(remote_storage_util.getRemoteAlbumsDir(account))) {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<ListAlbumBlocState> _queryOffline(ListAlbumBlocQuery ev) =>
|
||||
_queryWithAlbumDataSource(_offlineC, ev);
|
||||
|
||||
Future<ListAlbumBlocState> _queryOnline(ListAlbumBlocQuery ev) =>
|
||||
_queryWithAlbumDataSource(_c, ev);
|
||||
|
||||
Future<ListAlbumBlocState> _queryWithAlbumDataSource(
|
||||
DiContainer c, ListAlbumBlocQuery ev) async {
|
||||
try {
|
||||
final albums = <Album>[];
|
||||
final errors = <dynamic>[];
|
||||
await for (final result in ListAlbum(c)(ev.account)) {
|
||||
if (result is ExceptionEvent) {
|
||||
if (result.error is CacheNotFoundException) {
|
||||
_log.info(
|
||||
"[_queryWithAlbumDataSource] Cache not found", result.error);
|
||||
} else {
|
||||
_log.shout("[_queryWithAlbumDataSource] Exception while ListAlbum",
|
||||
result.error, result.stackTrace);
|
||||
}
|
||||
errors.add(result.error);
|
||||
} else if (result is Album) {
|
||||
albums.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
final items = albums.map((e) => ListAlbumBlocItem(e)).toList();
|
||||
if (errors.isEmpty) {
|
||||
return ListAlbumBlocSuccess(ev.account, items);
|
||||
} else {
|
||||
return ListAlbumBlocFailure(ev.account, items, errors.first);
|
||||
}
|
||||
} catch (e, stacktrace) {
|
||||
_log.severe("[_queryWithAlbumDataSource] Exception", e, stacktrace);
|
||||
return ListAlbumBlocFailure(ev.account, [], e);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isAccountOfInterest(Account account) =>
|
||||
state.account == null || state.account!.compareServerIdentity(account);
|
||||
|
||||
final DiContainer _c;
|
||||
final DiContainer _offlineC;
|
||||
|
||||
late AppEventListener<AlbumUpdatedEvent> _albumUpdatedListener;
|
||||
late AppEventListener<FileRemovedEvent> _fileRemovedListener;
|
||||
late AppEventListener<AlbumCreatedEvent> _albumCreatedListener;
|
||||
late final _fileMovedListener =
|
||||
AppEventListener<FileMovedEvent>(_onFileMovedEvent);
|
||||
late final _shareCreatedListener =
|
||||
AppEventListener<ShareCreatedEvent>(_onShareCreatedEvent);
|
||||
late final _shareRemovedListener =
|
||||
AppEventListener<ShareRemovedEvent>(_onShareRemovedEvent);
|
||||
|
||||
late Throttler _refreshThrottler;
|
||||
|
||||
static final _log = _$ListAlbumBlocNpLog.log;
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'list_album.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListAlbumBlocNpLog on ListAlbumBloc {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("bloc.list_album.ListAlbumBloc");
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListAlbumBlocQueryToString on ListAlbumBlocQuery {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListAlbumBlocQuery {account: $account}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_ListAlbumBlocExternalEventToString on _ListAlbumBlocExternalEvent {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_ListAlbumBlocExternalEvent {}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListAlbumBlocStateToString on ListAlbumBlocState {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "${objectRuntimeType(this, "ListAlbumBlocState")} {account: $account, items: [length: ${items.length}]}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListAlbumBlocFailureToString on ListAlbumBlocFailure {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListAlbumBlocFailure {account: $account, items: [length: ${items.length}], exception: $exception}";
|
||||
}
|
||||
}
|
|
@ -1,222 +0,0 @@
|
|||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/throttler.dart';
|
||||
import 'package:nc_photos/use_case/populate_person.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'list_face_file.g.dart';
|
||||
|
||||
abstract class ListFaceFileBlocEvent {
|
||||
const ListFaceFileBlocEvent();
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListFaceFileBlocQuery extends ListFaceFileBlocEvent {
|
||||
const ListFaceFileBlocQuery(this.account, this.person);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account account;
|
||||
final Person person;
|
||||
}
|
||||
|
||||
/// An external event has happened and may affect the state of this bloc
|
||||
@toString
|
||||
class _ListFaceFileBlocExternalEvent extends ListFaceFileBlocEvent {
|
||||
const _ListFaceFileBlocExternalEvent();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
@toString
|
||||
abstract class ListFaceFileBlocState {
|
||||
const ListFaceFileBlocState(this.account, this.items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account? account;
|
||||
final List<File> items;
|
||||
}
|
||||
|
||||
class ListFaceFileBlocInit extends ListFaceFileBlocState {
|
||||
ListFaceFileBlocInit() : super(null, const []);
|
||||
}
|
||||
|
||||
class ListFaceFileBlocLoading extends ListFaceFileBlocState {
|
||||
const ListFaceFileBlocLoading(Account? account, List<File> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
class ListFaceFileBlocSuccess extends ListFaceFileBlocState {
|
||||
const ListFaceFileBlocSuccess(Account? account, List<File> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListFaceFileBlocFailure extends ListFaceFileBlocState {
|
||||
const ListFaceFileBlocFailure(
|
||||
Account? account, List<File> items, this.exception)
|
||||
: super(account, items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Object exception;
|
||||
}
|
||||
|
||||
/// The state of this bloc is inconsistent. This typically means that the data
|
||||
/// may have been changed externally
|
||||
class ListFaceFileBlocInconsistent extends ListFaceFileBlocState {
|
||||
const ListFaceFileBlocInconsistent(Account? account, List<File> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
/// List all people recognized in an account
|
||||
@npLog
|
||||
class ListFaceFileBloc
|
||||
extends Bloc<ListFaceFileBlocEvent, ListFaceFileBlocState> {
|
||||
ListFaceFileBloc(this._c)
|
||||
: assert(require(_c)),
|
||||
assert(PopulatePerson.require(_c)),
|
||||
super(ListFaceFileBlocInit()) {
|
||||
_fileRemovedEventListener.begin();
|
||||
_filePropertyUpdatedEventListener.begin();
|
||||
|
||||
on<ListFaceFileBlocEvent>(_onEvent);
|
||||
}
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.faceRepo);
|
||||
|
||||
@override
|
||||
close() {
|
||||
_fileRemovedEventListener.end();
|
||||
_filePropertyUpdatedEventListener.end();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _onEvent(
|
||||
ListFaceFileBlocEvent event, Emitter<ListFaceFileBlocState> emit) async {
|
||||
_log.info("[_onEvent] $event");
|
||||
if (event is ListFaceFileBlocQuery) {
|
||||
await _onEventQuery(event, emit);
|
||||
} else if (event is _ListFaceFileBlocExternalEvent) {
|
||||
await _onExternalEvent(event, emit);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onEventQuery(
|
||||
ListFaceFileBlocQuery ev, Emitter<ListFaceFileBlocState> emit) async {
|
||||
try {
|
||||
emit(ListFaceFileBlocLoading(ev.account, state.items));
|
||||
emit(ListFaceFileBlocSuccess(ev.account, await _query(ev)));
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[_onEventQuery] Exception while request", e, stackTrace);
|
||||
emit(ListFaceFileBlocFailure(ev.account, state.items, e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onExternalEvent(_ListFaceFileBlocExternalEvent ev,
|
||||
Emitter<ListFaceFileBlocState> emit) async {
|
||||
emit(ListFaceFileBlocInconsistent(state.account, state.items));
|
||||
}
|
||||
|
||||
void _onFileRemovedEvent(FileRemovedEvent ev) {
|
||||
if (state is ListFaceFileBlocInit) {
|
||||
// no data in this bloc, ignore
|
||||
return;
|
||||
}
|
||||
if (_isFileOfInterest(ev.file)) {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onFilePropertyUpdatedEvent(FilePropertyUpdatedEvent ev) {
|
||||
if (!ev.hasAnyProperties([
|
||||
FilePropertyUpdatedEvent.propMetadata,
|
||||
FilePropertyUpdatedEvent.propIsArchived,
|
||||
FilePropertyUpdatedEvent.propOverrideDateTime,
|
||||
FilePropertyUpdatedEvent.propFavorite,
|
||||
])) {
|
||||
// not interested
|
||||
return;
|
||||
}
|
||||
if (state is ListFaceFileBlocInit) {
|
||||
// no data in this bloc, ignore
|
||||
return;
|
||||
}
|
||||
if (!_isFileOfInterest(ev.file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.hasAnyProperties([
|
||||
FilePropertyUpdatedEvent.propIsArchived,
|
||||
FilePropertyUpdatedEvent.propOverrideDateTime,
|
||||
FilePropertyUpdatedEvent.propFavorite,
|
||||
])) {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
} else {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 10),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<File>> _query(ListFaceFileBlocQuery ev) async {
|
||||
final faces = await _c.faceRepo.list(ev.account, ev.person);
|
||||
final files = await PopulatePerson(_c)(ev.account, faces);
|
||||
final rootDirs = ev.account.roots
|
||||
.map((e) => File(path: file_util.unstripPath(ev.account, e)))
|
||||
.toList();
|
||||
return files
|
||||
.where((f) =>
|
||||
file_util.isSupportedFormat(f) &&
|
||||
rootDirs.any((dir) => file_util.isUnderDir(f, dir)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
bool _isFileOfInterest(File file) {
|
||||
if (!file_util.isSupportedFormat(file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (final r in state.account?.roots ?? []) {
|
||||
final dir = File(path: file_util.unstripPath(state.account!, r));
|
||||
if (file_util.isUnderDir(file, dir)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
|
||||
late final _fileRemovedEventListener =
|
||||
AppEventListener<FileRemovedEvent>(_onFileRemovedEvent);
|
||||
late final _filePropertyUpdatedEventListener =
|
||||
AppEventListener<FilePropertyUpdatedEvent>(_onFilePropertyUpdatedEvent);
|
||||
|
||||
late final _refreshThrottler = Throttler(
|
||||
onTriggered: (_) {
|
||||
add(const _ListFaceFileBlocExternalEvent());
|
||||
},
|
||||
logTag: "ListFaceFileBloc.refresh",
|
||||
);
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'list_face_file.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListFaceFileBlocNpLog on ListFaceFileBloc {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("bloc.list_face_file.ListFaceFileBloc");
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListFaceFileBlocQueryToString on ListFaceFileBlocQuery {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListFaceFileBlocQuery {account: $account, person: $person}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_ListFaceFileBlocExternalEventToString
|
||||
on _ListFaceFileBlocExternalEvent {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_ListFaceFileBlocExternalEvent {}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListFaceFileBlocStateToString on ListFaceFileBlocState {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "${objectRuntimeType(this, "ListFaceFileBlocState")} {account: $account, items: [length: ${items.length}]}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListFaceFileBlocFailureToString on ListFaceFileBlocFailure {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListFaceFileBlocFailure {account: $account, items: [length: ${items.length}], exception: $exception}";
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ import 'package:nc_photos/entity/file.dart';
|
|||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:nc_photos/use_case/list_album.dart';
|
||||
import 'package:nc_photos/use_case/album/list_album.dart';
|
||||
import 'package:nc_photos/use_case/ls.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:logging/logging.dart';
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/throttler.dart';
|
||||
|
@ -147,7 +148,7 @@ class ListLocationBloc
|
|||
Future<LocationGroupResult> _query(ListLocationBlocQuery ev) =>
|
||||
ListLocationGroup(_c.withLocalRepo())(ev.account);
|
||||
|
||||
bool _isFileOfInterest(File file) {
|
||||
bool _isFileOfInterest(FileDescriptor file) {
|
||||
if (!file_util.isSupportedFormat(file)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,181 +0,0 @@
|
|||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/throttler.dart';
|
||||
import 'package:nc_photos/use_case/list_location_file.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'list_location_file.g.dart';
|
||||
|
||||
abstract class ListLocationFileBlocEvent {
|
||||
const ListLocationFileBlocEvent();
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListLocationFileBlocQuery extends ListLocationFileBlocEvent {
|
||||
const ListLocationFileBlocQuery(this.account, this.place, this.countryCode);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account account;
|
||||
final String? place;
|
||||
final String countryCode;
|
||||
}
|
||||
|
||||
/// An external event has happened and may affect the state of this bloc
|
||||
@toString
|
||||
class _ListLocationFileBlocExternalEvent extends ListLocationFileBlocEvent {
|
||||
const _ListLocationFileBlocExternalEvent();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
@toString
|
||||
abstract class ListLocationFileBlocState {
|
||||
const ListLocationFileBlocState(this.account, this.items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account? account;
|
||||
final List<File> items;
|
||||
}
|
||||
|
||||
class ListLocationFileBlocInit extends ListLocationFileBlocState {
|
||||
ListLocationFileBlocInit() : super(null, const []);
|
||||
}
|
||||
|
||||
class ListLocationFileBlocLoading extends ListLocationFileBlocState {
|
||||
const ListLocationFileBlocLoading(Account? account, List<File> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
class ListLocationFileBlocSuccess extends ListLocationFileBlocState {
|
||||
const ListLocationFileBlocSuccess(Account? account, List<File> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListLocationFileBlocFailure extends ListLocationFileBlocState {
|
||||
const ListLocationFileBlocFailure(
|
||||
Account? account, List<File> items, this.exception)
|
||||
: super(account, items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Object exception;
|
||||
}
|
||||
|
||||
/// The state of this bloc is inconsistent. This typically means that the data
|
||||
/// may have been changed externally
|
||||
class ListLocationFileBlocInconsistent extends ListLocationFileBlocState {
|
||||
const ListLocationFileBlocInconsistent(Account? account, List<File> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
/// List all files associated with a specific tag
|
||||
@npLog
|
||||
class ListLocationFileBloc
|
||||
extends Bloc<ListLocationFileBlocEvent, ListLocationFileBlocState> {
|
||||
ListLocationFileBloc(this._c)
|
||||
: assert(require(_c)),
|
||||
assert(ListLocationFile.require(_c)),
|
||||
super(ListLocationFileBlocInit()) {
|
||||
_fileRemovedEventListener.begin();
|
||||
|
||||
on<ListLocationFileBlocEvent>(_onEvent);
|
||||
}
|
||||
|
||||
static bool require(DiContainer c) =>
|
||||
DiContainer.has(c, DiType.taggedFileRepo);
|
||||
|
||||
@override
|
||||
close() {
|
||||
_fileRemovedEventListener.end();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _onEvent(ListLocationFileBlocEvent event,
|
||||
Emitter<ListLocationFileBlocState> emit) async {
|
||||
_log.info("[_onEvent] $event");
|
||||
if (event is ListLocationFileBlocQuery) {
|
||||
await _onEventQuery(event, emit);
|
||||
} else if (event is _ListLocationFileBlocExternalEvent) {
|
||||
await _onExternalEvent(event, emit);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onEventQuery(ListLocationFileBlocQuery ev,
|
||||
Emitter<ListLocationFileBlocState> emit) async {
|
||||
try {
|
||||
emit(ListLocationFileBlocLoading(ev.account, state.items));
|
||||
emit(ListLocationFileBlocSuccess(ev.account, await _query(ev)));
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[_onEventQuery] Exception while request", e, stackTrace);
|
||||
emit(ListLocationFileBlocFailure(ev.account, state.items, e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onExternalEvent(_ListLocationFileBlocExternalEvent ev,
|
||||
Emitter<ListLocationFileBlocState> emit) async {
|
||||
emit(ListLocationFileBlocInconsistent(state.account, state.items));
|
||||
}
|
||||
|
||||
void _onFileRemovedEvent(FileRemovedEvent ev) {
|
||||
if (state is ListLocationFileBlocInit) {
|
||||
// no data in this bloc, ignore
|
||||
return;
|
||||
}
|
||||
if (_isFileOfInterest(ev.file)) {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<File>> _query(ListLocationFileBlocQuery ev) async {
|
||||
final files = <File>[];
|
||||
for (final r in ev.account.roots) {
|
||||
final dir = File(path: file_util.unstripPath(ev.account, r));
|
||||
files.addAll(await ListLocationFile(_c)(
|
||||
ev.account, dir, ev.place, ev.countryCode));
|
||||
}
|
||||
return files.where((f) => file_util.isSupportedFormat(f)).toList();
|
||||
}
|
||||
|
||||
bool _isFileOfInterest(File file) {
|
||||
if (!file_util.isSupportedFormat(file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (final r in state.account?.roots ?? []) {
|
||||
final dir = File(path: file_util.unstripPath(state.account!, r));
|
||||
if (file_util.isUnderDir(file, dir)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
|
||||
late final _fileRemovedEventListener =
|
||||
AppEventListener<FileRemovedEvent>(_onFileRemovedEvent);
|
||||
|
||||
late final _refreshThrottler = Throttler(
|
||||
onTriggered: (_) {
|
||||
add(const _ListLocationFileBlocExternalEvent());
|
||||
},
|
||||
logTag: "ListLocationFileBloc.refresh",
|
||||
);
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'list_location_file.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListLocationFileBlocNpLog on ListLocationFileBloc {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("bloc.list_location_file.ListLocationFileBloc");
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListLocationFileBlocQueryToString on ListLocationFileBlocQuery {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListLocationFileBlocQuery {account: $account, place: $place, countryCode: $countryCode}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_ListLocationFileBlocExternalEventToString
|
||||
on _ListLocationFileBlocExternalEvent {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_ListLocationFileBlocExternalEvent {}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListLocationFileBlocStateToString on ListLocationFileBlocState {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "${objectRuntimeType(this, "ListLocationFileBlocState")} {account: $account, items: [length: ${items.length}]}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListLocationFileBlocFailureToString on ListLocationFileBlocFailure {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListLocationFileBlocFailure {account: $account, items: [length: ${items.length}], exception: $exception}";
|
||||
}
|
||||
}
|
|
@ -1,226 +0,0 @@
|
|||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/throttler.dart';
|
||||
import 'package:nc_photos/use_case/find_file.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'list_tag_file.g.dart';
|
||||
|
||||
abstract class ListTagFileBlocEvent {
|
||||
const ListTagFileBlocEvent();
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListTagFileBlocQuery extends ListTagFileBlocEvent {
|
||||
const ListTagFileBlocQuery(this.account, this.tag);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account account;
|
||||
final Tag tag;
|
||||
}
|
||||
|
||||
/// An external event has happened and may affect the state of this bloc
|
||||
@toString
|
||||
class _ListTagFileBlocExternalEvent extends ListTagFileBlocEvent {
|
||||
const _ListTagFileBlocExternalEvent();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
@toString
|
||||
abstract class ListTagFileBlocState {
|
||||
const ListTagFileBlocState(this.account, this.items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account? account;
|
||||
final List<File> items;
|
||||
}
|
||||
|
||||
class ListTagFileBlocInit extends ListTagFileBlocState {
|
||||
ListTagFileBlocInit() : super(null, const []);
|
||||
}
|
||||
|
||||
class ListTagFileBlocLoading extends ListTagFileBlocState {
|
||||
const ListTagFileBlocLoading(Account? account, List<File> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
class ListTagFileBlocSuccess extends ListTagFileBlocState {
|
||||
const ListTagFileBlocSuccess(Account? account, List<File> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
@toString
|
||||
class ListTagFileBlocFailure extends ListTagFileBlocState {
|
||||
const ListTagFileBlocFailure(
|
||||
Account? account, List<File> items, this.exception)
|
||||
: super(account, items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Object exception;
|
||||
}
|
||||
|
||||
/// The state of this bloc is inconsistent. This typically means that the data
|
||||
/// may have been changed externally
|
||||
class ListTagFileBlocInconsistent extends ListTagFileBlocState {
|
||||
const ListTagFileBlocInconsistent(Account? account, List<File> items)
|
||||
: super(account, items);
|
||||
}
|
||||
|
||||
/// List all files associated with a specific tag
|
||||
@npLog
|
||||
class ListTagFileBloc extends Bloc<ListTagFileBlocEvent, ListTagFileBlocState> {
|
||||
ListTagFileBloc(this._c)
|
||||
: assert(require(_c)),
|
||||
// assert(PopulatePerson.require(_c)),
|
||||
super(ListTagFileBlocInit()) {
|
||||
_fileRemovedEventListener.begin();
|
||||
_filePropertyUpdatedEventListener.begin();
|
||||
|
||||
on<ListTagFileBlocEvent>(_onEvent);
|
||||
}
|
||||
|
||||
static bool require(DiContainer c) =>
|
||||
DiContainer.has(c, DiType.taggedFileRepo);
|
||||
|
||||
@override
|
||||
close() {
|
||||
_fileRemovedEventListener.end();
|
||||
_filePropertyUpdatedEventListener.end();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _onEvent(
|
||||
ListTagFileBlocEvent event, Emitter<ListTagFileBlocState> emit) async {
|
||||
_log.info("[_onEvent] $event");
|
||||
if (event is ListTagFileBlocQuery) {
|
||||
await _onEventQuery(event, emit);
|
||||
} else if (event is _ListTagFileBlocExternalEvent) {
|
||||
await _onExternalEvent(event, emit);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onEventQuery(
|
||||
ListTagFileBlocQuery ev, Emitter<ListTagFileBlocState> emit) async {
|
||||
try {
|
||||
emit(ListTagFileBlocLoading(ev.account, state.items));
|
||||
emit(ListTagFileBlocSuccess(ev.account, await _query(ev)));
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[_onEventQuery] Exception while request", e, stackTrace);
|
||||
emit(ListTagFileBlocFailure(ev.account, state.items, e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onExternalEvent(_ListTagFileBlocExternalEvent ev,
|
||||
Emitter<ListTagFileBlocState> emit) async {
|
||||
emit(ListTagFileBlocInconsistent(state.account, state.items));
|
||||
}
|
||||
|
||||
void _onFileRemovedEvent(FileRemovedEvent ev) {
|
||||
if (state is ListTagFileBlocInit) {
|
||||
// no data in this bloc, ignore
|
||||
return;
|
||||
}
|
||||
if (_isFileOfInterest(ev.file)) {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onFilePropertyUpdatedEvent(FilePropertyUpdatedEvent ev) {
|
||||
if (!ev.hasAnyProperties([
|
||||
FilePropertyUpdatedEvent.propMetadata,
|
||||
FilePropertyUpdatedEvent.propIsArchived,
|
||||
FilePropertyUpdatedEvent.propOverrideDateTime,
|
||||
FilePropertyUpdatedEvent.propFavorite,
|
||||
])) {
|
||||
// not interested
|
||||
return;
|
||||
}
|
||||
if (state is ListTagFileBlocInit) {
|
||||
// no data in this bloc, ignore
|
||||
return;
|
||||
}
|
||||
if (!_isFileOfInterest(ev.file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.hasAnyProperties([
|
||||
FilePropertyUpdatedEvent.propIsArchived,
|
||||
FilePropertyUpdatedEvent.propOverrideDateTime,
|
||||
FilePropertyUpdatedEvent.propFavorite,
|
||||
])) {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
} else {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 10),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<File>> _query(ListTagFileBlocQuery ev) async {
|
||||
final files = <File>[];
|
||||
for (final r in ev.account.roots) {
|
||||
final dir = File(path: file_util.unstripPath(ev.account, r));
|
||||
final taggedFiles =
|
||||
await _c.taggedFileRepo.list(ev.account, dir, [ev.tag]);
|
||||
files.addAll(await FindFile(_c)(
|
||||
ev.account,
|
||||
taggedFiles.map((e) => e.fileId).toList(),
|
||||
onFileNotFound: (id) {
|
||||
_log.warning("[_query] Missing file: $id");
|
||||
},
|
||||
));
|
||||
}
|
||||
return files.where((f) => file_util.isSupportedFormat(f)).toList();
|
||||
}
|
||||
|
||||
bool _isFileOfInterest(File file) {
|
||||
if (!file_util.isSupportedFormat(file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (final r in state.account?.roots ?? []) {
|
||||
final dir = File(path: file_util.unstripPath(state.account!, r));
|
||||
if (file_util.isUnderDir(file, dir)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
|
||||
late final _fileRemovedEventListener =
|
||||
AppEventListener<FileRemovedEvent>(_onFileRemovedEvent);
|
||||
late final _filePropertyUpdatedEventListener =
|
||||
AppEventListener<FilePropertyUpdatedEvent>(_onFilePropertyUpdatedEvent);
|
||||
|
||||
late final _refreshThrottler = Throttler(
|
||||
onTriggered: (_) {
|
||||
add(const _ListTagFileBlocExternalEvent());
|
||||
},
|
||||
logTag: "ListTagFileBloc.refresh",
|
||||
);
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'list_tag_file.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListTagFileBlocNpLog on ListTagFileBloc {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("bloc.list_tag_file.ListTagFileBloc");
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListTagFileBlocQueryToString on ListTagFileBlocQuery {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListTagFileBlocQuery {account: $account, tag: $tag}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_ListTagFileBlocExternalEventToString
|
||||
on _ListTagFileBlocExternalEvent {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_ListTagFileBlocExternalEvent {}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListTagFileBlocStateToString on ListTagFileBlocState {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "${objectRuntimeType(this, "ListTagFileBlocState")} {account: $account, items: [length: ${items.length}]}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$ListTagFileBlocFailureToString on ListTagFileBlocFailure {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "ListTagFileBlocFailure {account: $account, items: [length: ${items.length}], exception: $exception}";
|
||||
}
|
||||
}
|
|
@ -481,7 +481,7 @@ class ScanAccountDirBloc
|
|||
return files;
|
||||
}
|
||||
|
||||
bool _isFileOfInterest(File file) {
|
||||
bool _isFileOfInterest(FileDescriptor file) {
|
||||
if (!file_util.isSupportedFormat(file)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:logging/logging.dart';
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/entity/search.dart';
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
|
@ -198,7 +199,7 @@ class SearchBloc extends Bloc<SearchBlocEvent, SearchBlocState> {
|
|||
Future<List<File>> _query(SearchBlocQuery ev) =>
|
||||
Search(_c)(ev.account, ev.criteria);
|
||||
|
||||
bool _isFileOfInterest(File file) {
|
||||
bool _isFileOfInterest(FileDescriptor file) {
|
||||
if (!file_util.isSupportedFormat(file)) {
|
||||
return false;
|
||||
}
|
||||
|
|
3
app/lib/bloc_util.dart
Normal file
3
app/lib/bloc_util.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
abstract class BlocTag {
|
||||
String get tag;
|
||||
}
|
23
app/lib/controller/account_controller.dart
Normal file
23
app/lib/controller/account_controller.dart
Normal file
|
@ -0,0 +1,23 @@
|
|||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/controller/collections_controller.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
|
||||
class AccountController {
|
||||
void setCurrentAccount(Account account) {
|
||||
_account = account;
|
||||
_collectionsController?.dispose();
|
||||
_collectionsController = null;
|
||||
}
|
||||
|
||||
Account get account => _account!;
|
||||
|
||||
CollectionsController get collectionsController =>
|
||||
_collectionsController ??= CollectionsController(
|
||||
KiwiContainer().resolve<DiContainer>(),
|
||||
account: _account!,
|
||||
);
|
||||
|
||||
Account? _account;
|
||||
CollectionsController? _collectionsController;
|
||||
}
|
336
app/lib/controller/collection_items_controller.dart
Normal file
336
app/lib/controller/collection_items_controller.dart
Normal file
|
@ -0,0 +1,336 @@
|
|||
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/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;
|
||||
}
|
||||
|
||||
/// 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());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 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());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 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());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 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,
|
||||
));
|
||||
}
|
||||
} 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");
|
||||
}
|
274
app/lib/controller/collections_controller.dart
Normal file
274
app/lib/controller/collections_controller.dart
Normal file
|
@ -0,0 +1,274 @@
|
|||
import 'dart:async';
|
||||
|
||||
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/di_container.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.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/list_collection.dart';
|
||||
import 'package:nc_photos/use_case/collection/remove_collections.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,
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
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),
|
||||
], shouldRemoveCache: false),
|
||||
));
|
||||
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,
|
||||
}) async {
|
||||
try {
|
||||
final c = await _mutex.protect(() async {
|
||||
return await EditCollection(_c)(
|
||||
account,
|
||||
collection,
|
||||
name: name,
|
||||
items: items,
|
||||
itemSort: itemSort,
|
||||
);
|
||||
});
|
||||
_updateCollection(c, items);
|
||||
} catch (e, stackTrace) {
|
||||
_dataStreamController.addError(e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
var lastData = const CollectionStreamEvent(
|
||||
data: [],
|
||||
hasNext: false,
|
||||
);
|
||||
final completer = Completer();
|
||||
ListCollection(_c)(account).listen(
|
||||
(c) {
|
||||
lastData = CollectionStreamEvent(
|
||||
data: _prepareDataFor(c, shouldRemoveCache: true),
|
||||
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, {
|
||||
required bool shouldRemoveCache,
|
||||
}) {
|
||||
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;
|
||||
|
||||
var _isDataStreamInited = false;
|
||||
final _dataStreamController = BehaviorSubject.seeded(
|
||||
const CollectionStreamEvent(
|
||||
data: [],
|
||||
hasNext: true,
|
||||
),
|
||||
);
|
||||
|
||||
final _itemControllers = <_CollectionKey, CollectionItemsController>{};
|
||||
|
||||
final _mutex = Mutex();
|
||||
}
|
||||
|
||||
class _CollectionKey {
|
||||
const _CollectionKey(this.collection);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _CollectionKey &&
|
||||
collection.compareIdentity(other.collection);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => collection.identityHashCode;
|
||||
|
||||
final Collection collection;
|
||||
}
|
75
app/lib/controller/collections_controller.g.dart
Normal file
75
app/lib/controller/collections_controller.g.dart
Normal file
|
@ -0,0 +1,75 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'collections_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithLintRuleGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class $CollectionStreamDataCopyWithWorker {
|
||||
CollectionStreamData call(
|
||||
{Collection? collection, CollectionItemsController? controller});
|
||||
}
|
||||
|
||||
class _$CollectionStreamDataCopyWithWorkerImpl
|
||||
implements $CollectionStreamDataCopyWithWorker {
|
||||
_$CollectionStreamDataCopyWithWorkerImpl(this.that);
|
||||
|
||||
@override
|
||||
CollectionStreamData call({dynamic collection, dynamic controller}) {
|
||||
return CollectionStreamData(
|
||||
collection: collection as Collection? ?? that.collection,
|
||||
controller:
|
||||
controller as CollectionItemsController? ?? that.controller);
|
||||
}
|
||||
|
||||
final CollectionStreamData that;
|
||||
}
|
||||
|
||||
extension $CollectionStreamDataCopyWith on CollectionStreamData {
|
||||
$CollectionStreamDataCopyWithWorker get copyWith => _$copyWith;
|
||||
$CollectionStreamDataCopyWithWorker get _$copyWith =>
|
||||
_$CollectionStreamDataCopyWithWorkerImpl(this);
|
||||
}
|
||||
|
||||
abstract class $CollectionStreamEventCopyWithWorker {
|
||||
CollectionStreamEvent call({List<CollectionStreamData>? data, bool? hasNext});
|
||||
}
|
||||
|
||||
class _$CollectionStreamEventCopyWithWorkerImpl
|
||||
implements $CollectionStreamEventCopyWithWorker {
|
||||
_$CollectionStreamEventCopyWithWorkerImpl(this.that);
|
||||
|
||||
@override
|
||||
CollectionStreamEvent call({dynamic data, dynamic hasNext}) {
|
||||
return CollectionStreamEvent(
|
||||
data: data as List<CollectionStreamData>? ?? that.data,
|
||||
hasNext: hasNext as bool? ?? that.hasNext);
|
||||
}
|
||||
|
||||
final CollectionStreamEvent that;
|
||||
}
|
||||
|
||||
extension $CollectionStreamEventCopyWith on CollectionStreamEvent {
|
||||
$CollectionStreamEventCopyWithWorker get copyWith => _$copyWith;
|
||||
$CollectionStreamEventCopyWithWorker get _$copyWith =>
|
||||
_$CollectionStreamEventCopyWithWorkerImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$CollectionsControllerNpLog on CollectionsController {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log =
|
||||
Logger("controller.collections_controller.CollectionsController");
|
||||
}
|
54
app/lib/controller/pref_controller.dart
Normal file
54
app/lib/controller/pref_controller.dart
Normal file
|
@ -0,0 +1,54 @@
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
part 'pref_controller.g.dart';
|
||||
|
||||
@npLog
|
||||
class PrefController {
|
||||
PrefController(this._c);
|
||||
|
||||
ValueStream<int> get albumBrowserZoomLevel =>
|
||||
_albumBrowserZoomLevelController.stream;
|
||||
|
||||
Future<void> setAlbumBrowserZoomLevel(int value) async {
|
||||
final backup = _albumBrowserZoomLevelController.value;
|
||||
_albumBrowserZoomLevelController.add(value);
|
||||
try {
|
||||
if (!await _c.pref.setAlbumBrowserZoomLevel(value)) {
|
||||
throw StateError("Unknown error");
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[setAlbumBrowserZoomLevel] Failed setting preference", e,
|
||||
stackTrace);
|
||||
_albumBrowserZoomLevelController
|
||||
..addError(e, stackTrace)
|
||||
..add(backup);
|
||||
}
|
||||
}
|
||||
|
||||
ValueStream<int> get homeAlbumsSort => _homeAlbumsSortController.stream;
|
||||
|
||||
Future<void> setHomeAlbumsSort(int value) async {
|
||||
final backup = _homeAlbumsSortController.value;
|
||||
_homeAlbumsSortController.add(value);
|
||||
try {
|
||||
if (!await _c.pref.setHomeAlbumsSort(value)) {
|
||||
throw StateError("Unknown error");
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe(
|
||||
"[setHomeAlbumsSort] Failed setting preference", e, stackTrace);
|
||||
_homeAlbumsSortController
|
||||
..addError(e, stackTrace)
|
||||
..add(backup);
|
||||
}
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
late final _albumBrowserZoomLevelController =
|
||||
BehaviorSubject.seeded(_c.pref.getAlbumBrowserZoomLevelOr(0));
|
||||
late final _homeAlbumsSortController =
|
||||
BehaviorSubject.seeded(_c.pref.getHomeAlbumsSortOr(0));
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'album_picker.dart';
|
||||
part of 'pref_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$_AlbumPickerStateNpLog on _AlbumPickerState {
|
||||
extension _$PrefControllerNpLog on PrefController {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("widget.album_picker._AlbumPickerState");
|
||||
static final log = Logger("controller.pref_controller.PrefController");
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/repo2.dart';
|
||||
import 'package:nc_photos/entity/face.dart';
|
||||
import 'package:nc_photos/entity/favorite.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/local_file.dart';
|
||||
import 'package:nc_photos/entity/nc_album/repo.dart';
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/entity/search.dart';
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
|
@ -16,7 +18,11 @@ import 'package:nc_photos/touch_manager.dart';
|
|||
|
||||
enum DiType {
|
||||
albumRepo,
|
||||
albumRepoRemote,
|
||||
albumRepoLocal,
|
||||
albumRepo2,
|
||||
albumRepo2Remote,
|
||||
albumRepo2Local,
|
||||
faceRepo,
|
||||
fileRepo,
|
||||
fileRepoRemote,
|
||||
|
@ -33,6 +39,9 @@ enum DiType {
|
|||
taggedFileRepo,
|
||||
localFileRepo,
|
||||
searchRepo,
|
||||
ncAlbumRepo,
|
||||
ncAlbumRepoRemote,
|
||||
ncAlbumRepoLocal,
|
||||
pref,
|
||||
sqliteDb,
|
||||
touchManager,
|
||||
|
@ -41,7 +50,11 @@ enum DiType {
|
|||
class DiContainer {
|
||||
DiContainer({
|
||||
AlbumRepo? albumRepo,
|
||||
AlbumRepo? albumRepoRemote,
|
||||
AlbumRepo? albumRepoLocal,
|
||||
AlbumRepo2? albumRepo2,
|
||||
AlbumRepo2? albumRepo2Remote,
|
||||
AlbumRepo2? albumRepo2Local,
|
||||
FaceRepo? faceRepo,
|
||||
FileRepo? fileRepo,
|
||||
FileRepo? fileRepoRemote,
|
||||
|
@ -58,11 +71,18 @@ class DiContainer {
|
|||
TaggedFileRepo? taggedFileRepo,
|
||||
LocalFileRepo? localFileRepo,
|
||||
SearchRepo? searchRepo,
|
||||
NcAlbumRepo? ncAlbumRepo,
|
||||
NcAlbumRepo? ncAlbumRepoRemote,
|
||||
NcAlbumRepo? ncAlbumRepoLocal,
|
||||
Pref? pref,
|
||||
sql.SqliteDb? sqliteDb,
|
||||
TouchManager? touchManager,
|
||||
}) : _albumRepo = albumRepo,
|
||||
_albumRepoRemote = albumRepoRemote,
|
||||
_albumRepoLocal = albumRepoLocal,
|
||||
_albumRepo2 = albumRepo2,
|
||||
_albumRepo2Remote = albumRepo2Remote,
|
||||
_albumRepo2Local = albumRepo2Local,
|
||||
_faceRepo = faceRepo,
|
||||
_fileRepo = fileRepo,
|
||||
_fileRepoRemote = fileRepoRemote,
|
||||
|
@ -79,6 +99,9 @@ class DiContainer {
|
|||
_taggedFileRepo = taggedFileRepo,
|
||||
_localFileRepo = localFileRepo,
|
||||
_searchRepo = searchRepo,
|
||||
_ncAlbumRepo = ncAlbumRepo,
|
||||
_ncAlbumRepoRemote = ncAlbumRepoRemote,
|
||||
_ncAlbumRepoLocal = ncAlbumRepoLocal,
|
||||
_pref = pref,
|
||||
_sqliteDb = sqliteDb,
|
||||
_touchManager = touchManager;
|
||||
|
@ -89,8 +112,16 @@ class DiContainer {
|
|||
switch (type) {
|
||||
case DiType.albumRepo:
|
||||
return contianer._albumRepo != null;
|
||||
case DiType.albumRepoRemote:
|
||||
return contianer._albumRepoRemote != null;
|
||||
case DiType.albumRepoLocal:
|
||||
return contianer._albumRepoLocal != null;
|
||||
case DiType.albumRepo2:
|
||||
return contianer._albumRepo2 != null;
|
||||
case DiType.albumRepo2Remote:
|
||||
return contianer._albumRepo2Remote != null;
|
||||
case DiType.albumRepo2Local:
|
||||
return contianer._albumRepo2Local != null;
|
||||
case DiType.faceRepo:
|
||||
return contianer._faceRepo != null;
|
||||
case DiType.fileRepo:
|
||||
|
@ -123,6 +154,12 @@ class DiContainer {
|
|||
return contianer._localFileRepo != null;
|
||||
case DiType.searchRepo:
|
||||
return contianer._searchRepo != null;
|
||||
case DiType.ncAlbumRepo:
|
||||
return contianer._ncAlbumRepo != null;
|
||||
case DiType.ncAlbumRepoRemote:
|
||||
return contianer._ncAlbumRepoRemote != null;
|
||||
case DiType.ncAlbumRepoLocal:
|
||||
return contianer._ncAlbumRepoLocal != null;
|
||||
case DiType.pref:
|
||||
return contianer._pref != null;
|
||||
case DiType.sqliteDb:
|
||||
|
@ -134,6 +171,7 @@ class DiContainer {
|
|||
|
||||
DiContainer copyWith({
|
||||
OrNull<AlbumRepo>? albumRepo,
|
||||
OrNull<AlbumRepo2>? albumRepo2,
|
||||
OrNull<FaceRepo>? faceRepo,
|
||||
OrNull<FileRepo>? fileRepo,
|
||||
OrNull<PersonRepo>? personRepo,
|
||||
|
@ -144,12 +182,14 @@ class DiContainer {
|
|||
OrNull<TaggedFileRepo>? taggedFileRepo,
|
||||
OrNull<LocalFileRepo>? localFileRepo,
|
||||
OrNull<SearchRepo>? searchRepo,
|
||||
OrNull<NcAlbumRepo>? ncAlbumRepo,
|
||||
OrNull<Pref>? pref,
|
||||
OrNull<sql.SqliteDb>? sqliteDb,
|
||||
OrNull<TouchManager>? touchManager,
|
||||
}) {
|
||||
return DiContainer(
|
||||
albumRepo: albumRepo == null ? _albumRepo : albumRepo.obj,
|
||||
albumRepo2: albumRepo2 == null ? _albumRepo2 : albumRepo2.obj,
|
||||
faceRepo: faceRepo == null ? _faceRepo : faceRepo.obj,
|
||||
fileRepo: fileRepo == null ? _fileRepo : fileRepo.obj,
|
||||
personRepo: personRepo == null ? _personRepo : personRepo.obj,
|
||||
|
@ -161,6 +201,7 @@ class DiContainer {
|
|||
taggedFileRepo == null ? _taggedFileRepo : taggedFileRepo.obj,
|
||||
localFileRepo: localFileRepo == null ? _localFileRepo : localFileRepo.obj,
|
||||
searchRepo: searchRepo == null ? _searchRepo : searchRepo.obj,
|
||||
ncAlbumRepo: ncAlbumRepo == null ? _ncAlbumRepo : ncAlbumRepo.obj,
|
||||
pref: pref == null ? _pref : pref.obj,
|
||||
sqliteDb: sqliteDb == null ? _sqliteDb : sqliteDb.obj,
|
||||
touchManager: touchManager == null ? _touchManager : touchManager.obj,
|
||||
|
@ -168,7 +209,11 @@ class DiContainer {
|
|||
}
|
||||
|
||||
AlbumRepo get albumRepo => _albumRepo!;
|
||||
AlbumRepo get albumRepoRemote => _albumRepoRemote!;
|
||||
AlbumRepo get albumRepoLocal => _albumRepoLocal!;
|
||||
AlbumRepo2 get albumRepo2 => _albumRepo2!;
|
||||
AlbumRepo2 get albumRepo2Remote => _albumRepo2Remote!;
|
||||
AlbumRepo2 get albumRepo2Local => _albumRepo2Local!;
|
||||
FaceRepo get faceRepo => _faceRepo!;
|
||||
FileRepo get fileRepo => _fileRepo!;
|
||||
FileRepo get fileRepoRemote => _fileRepoRemote!;
|
||||
|
@ -185,6 +230,9 @@ class DiContainer {
|
|||
TaggedFileRepo get taggedFileRepo => _taggedFileRepo!;
|
||||
LocalFileRepo get localFileRepo => _localFileRepo!;
|
||||
SearchRepo get searchRepo => _searchRepo!;
|
||||
NcAlbumRepo get ncAlbumRepo => _ncAlbumRepo!;
|
||||
NcAlbumRepo get ncAlbumRepoRemote => _ncAlbumRepoRemote!;
|
||||
NcAlbumRepo get ncAlbumRepoLocal => _ncAlbumRepoLocal!;
|
||||
TouchManager get touchManager => _touchManager!;
|
||||
|
||||
sql.SqliteDb get sqliteDb => _sqliteDb!;
|
||||
|
@ -195,11 +243,31 @@ class DiContainer {
|
|||
_albumRepo = v;
|
||||
}
|
||||
|
||||
set albumRepoRemote(AlbumRepo v) {
|
||||
assert(_albumRepoRemote == null);
|
||||
_albumRepoRemote = v;
|
||||
}
|
||||
|
||||
set albumRepoLocal(AlbumRepo v) {
|
||||
assert(_albumRepoLocal == null);
|
||||
_albumRepoLocal = v;
|
||||
}
|
||||
|
||||
set albumRepo2(AlbumRepo2 v) {
|
||||
assert(_albumRepo2 == null);
|
||||
_albumRepo2 = v;
|
||||
}
|
||||
|
||||
set albumRepo2Remote(AlbumRepo2 v) {
|
||||
assert(_albumRepo2Remote == null);
|
||||
_albumRepo2Remote = v;
|
||||
}
|
||||
|
||||
set albumRepo2Local(AlbumRepo2 v) {
|
||||
assert(_albumRepo2Local == null);
|
||||
_albumRepo2Local = v;
|
||||
}
|
||||
|
||||
set faceRepo(FaceRepo v) {
|
||||
assert(_faceRepo == null);
|
||||
_faceRepo = v;
|
||||
|
@ -280,6 +348,21 @@ class DiContainer {
|
|||
_searchRepo = v;
|
||||
}
|
||||
|
||||
set ncAlbumRepo(NcAlbumRepo v) {
|
||||
assert(_ncAlbumRepo == null);
|
||||
_ncAlbumRepo = v;
|
||||
}
|
||||
|
||||
set ncAlbumRepoRemote(NcAlbumRepo v) {
|
||||
assert(_ncAlbumRepoRemote == null);
|
||||
_ncAlbumRepoRemote = v;
|
||||
}
|
||||
|
||||
set ncAlbumRepoLocal(NcAlbumRepo v) {
|
||||
assert(_ncAlbumRepoLocal == null);
|
||||
_ncAlbumRepoLocal = v;
|
||||
}
|
||||
|
||||
set touchManager(TouchManager v) {
|
||||
assert(_touchManager == null);
|
||||
_touchManager = v;
|
||||
|
@ -296,9 +379,13 @@ class DiContainer {
|
|||
}
|
||||
|
||||
AlbumRepo? _albumRepo;
|
||||
AlbumRepo? _albumRepoRemote;
|
||||
// Explicitly request a AlbumRepo backed by local source
|
||||
AlbumRepo? _albumRepoLocal;
|
||||
FaceRepo? _faceRepo;
|
||||
AlbumRepo2? _albumRepo2;
|
||||
AlbumRepo2? _albumRepo2Remote;
|
||||
AlbumRepo2? _albumRepo2Local;
|
||||
FileRepo? _fileRepo;
|
||||
// Explicitly request a FileRepo backed by remote source
|
||||
FileRepo? _fileRepoRemote;
|
||||
|
@ -316,6 +403,9 @@ class DiContainer {
|
|||
TaggedFileRepo? _taggedFileRepo;
|
||||
LocalFileRepo? _localFileRepo;
|
||||
SearchRepo? _searchRepo;
|
||||
NcAlbumRepo? _ncAlbumRepo;
|
||||
NcAlbumRepo? _ncAlbumRepoRemote;
|
||||
NcAlbumRepo? _ncAlbumRepoLocal;
|
||||
TouchManager? _touchManager;
|
||||
|
||||
sql.SqliteDb? _sqliteDb;
|
||||
|
@ -323,14 +413,28 @@ class DiContainer {
|
|||
}
|
||||
|
||||
extension DiContainerExtension on DiContainer {
|
||||
/// Uses remote repo if available
|
||||
///
|
||||
/// Notice that not all repo support this
|
||||
DiContainer withRemoteRepo() => copyWith(
|
||||
albumRepo: OrNull(albumRepoRemote),
|
||||
albumRepo2: OrNull(albumRepo2Remote),
|
||||
fileRepo: OrNull(fileRepoRemote),
|
||||
personRepo: OrNull(personRepoRemote),
|
||||
tagRepo: OrNull(tagRepoRemote),
|
||||
ncAlbumRepo: OrNull(ncAlbumRepoRemote),
|
||||
);
|
||||
|
||||
/// Uses local repo if available
|
||||
///
|
||||
/// Notice that not all repo support this
|
||||
DiContainer withLocalRepo() => copyWith(
|
||||
albumRepo: OrNull(albumRepoLocal),
|
||||
albumRepo2: OrNull(albumRepo2Local),
|
||||
fileRepo: OrNull(fileRepoLocal),
|
||||
personRepo: OrNull(personRepoLocal),
|
||||
tagRepo: OrNull(tagRepoLocal),
|
||||
ncAlbumRepo: OrNull(ncAlbumRepoLocal),
|
||||
);
|
||||
|
||||
DiContainer withLocalAlbumRepo() =>
|
||||
|
|
|
@ -1,106 +1,76 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart' as sql;
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/upgrader.dart';
|
||||
import 'package:nc_photos/entity/album/data_source2.dart';
|
||||
import 'package:nc_photos/entity/album/repo2.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
|
||||
import 'package:nc_photos/entity/sqlite/type_converter.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/exception_event.dart';
|
||||
import 'package:nc_photos/future_util.dart' as future_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:nc_photos/use_case/get_file_binary.dart';
|
||||
import 'package:nc_photos/use_case/ls_single_file.dart';
|
||||
import 'package:nc_photos/use_case/put_file_binary.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
|
||||
part 'data_source.g.dart';
|
||||
|
||||
/// Backward compatibility only, use [AlbumRemoteDataSource2] instead
|
||||
@npLog
|
||||
class AlbumRemoteDataSource implements AlbumDataSource {
|
||||
@override
|
||||
get(Account account, File albumFile) async {
|
||||
_log.info("[get] ${albumFile.path}");
|
||||
const fileRepo = FileRepo(FileWebdavDataSource());
|
||||
final data = await GetFileBinary(fileRepo)(account, albumFile);
|
||||
try {
|
||||
return Album.fromJson(
|
||||
jsonDecode(utf8.decode(data)),
|
||||
upgraderFactory: DefaultAlbumUpgraderFactory(
|
||||
account: account,
|
||||
albumFile: albumFile,
|
||||
logFilePath: albumFile.path,
|
||||
),
|
||||
)!
|
||||
.copyWith(
|
||||
lastUpdated: OrNull(null),
|
||||
albumFile: OrNull(albumFile),
|
||||
);
|
||||
} catch (e, stacktrace) {
|
||||
dynamic d = data;
|
||||
try {
|
||||
d = utf8.decode(data);
|
||||
} catch (_) {}
|
||||
_log.severe("[get] Invalid json data: $d", e, stacktrace);
|
||||
throw const FormatException("Invalid album format");
|
||||
}
|
||||
final albums = await const AlbumRemoteDataSource2().getAlbums(
|
||||
account,
|
||||
[albumFile],
|
||||
onError: (_, error, stackTrace) {
|
||||
Error.throwWithStackTrace(error, stackTrace ?? StackTrace.current);
|
||||
},
|
||||
);
|
||||
return albums.first;
|
||||
}
|
||||
|
||||
@override
|
||||
getAll(Account account, List<File> albumFiles) async* {
|
||||
_log.info(
|
||||
"[getAll] ${albumFiles.map((f) => f.filename).toReadableString()}");
|
||||
final results = await future_util.waitOr(
|
||||
albumFiles.map((f) => get(account, f)),
|
||||
(error, stackTrace) => ExceptionEvent(error, stackTrace),
|
||||
final failed = <String, Map>{};
|
||||
final albums = await const AlbumRemoteDataSource2().getAlbums(
|
||||
account,
|
||||
albumFiles,
|
||||
onError: (v, error, stackTrace) {
|
||||
failed[v.path] = {
|
||||
"file": v,
|
||||
"error": error,
|
||||
"stackTrace": stackTrace,
|
||||
};
|
||||
},
|
||||
);
|
||||
for (final r in results) {
|
||||
yield r;
|
||||
var i = 0;
|
||||
for (final af in albumFiles) {
|
||||
final v = failed[af.path];
|
||||
if (v != null) {
|
||||
yield ExceptionEvent(v["error"], v["stackTrace"]);
|
||||
} else {
|
||||
yield albums[i++];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
create(Account account, Album album) async {
|
||||
_log.info("[create]");
|
||||
final fileName = _makeAlbumFileName();
|
||||
final filePath =
|
||||
"${remote_storage_util.getRemoteAlbumsDir(account)}/$fileName";
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
await PutFileBinary(c.fileRepo)(account, filePath,
|
||||
const Utf8Encoder().convert(jsonEncode(album.toRemoteJson())),
|
||||
shouldCreateMissingDir: true);
|
||||
// query album file
|
||||
final newFile = await LsSingleFile(c)(account, filePath);
|
||||
return album.copyWith(albumFile: OrNull(newFile));
|
||||
return const AlbumRemoteDataSource2().create(account, album);
|
||||
}
|
||||
|
||||
@override
|
||||
update(Account account, Album album) async {
|
||||
_log.info("[update] ${album.albumFile!.path}");
|
||||
const fileRepo = FileRepo(FileWebdavDataSource());
|
||||
await PutFileBinary(fileRepo)(account, album.albumFile!.path,
|
||||
const Utf8Encoder().convert(jsonEncode(album.toRemoteJson())));
|
||||
}
|
||||
|
||||
String _makeAlbumFileName() {
|
||||
// just make up something
|
||||
final timestamp = clock.now().millisecondsSinceEpoch;
|
||||
final random = Random().nextInt(0xFFFFFF);
|
||||
return "${timestamp.toRadixString(16)}-${random.toRadixString(16).padLeft(6, '0')}.nc_album.json";
|
||||
return const AlbumRemoteDataSource2().update(account, album);
|
||||
}
|
||||
}
|
||||
|
||||
/// Backward compatibility only, use [AlbumSqliteDbDataSource2] instead
|
||||
@npLog
|
||||
class AlbumSqliteDbDataSource implements AlbumDataSource {
|
||||
AlbumSqliteDbDataSource(this._c);
|
||||
|
@ -119,64 +89,29 @@ class AlbumSqliteDbDataSource implements AlbumDataSource {
|
|||
getAll(Account account, List<File> albumFiles) async* {
|
||||
_log.info(
|
||||
"[getAll] ${albumFiles.map((f) => f.filename).toReadableString()}");
|
||||
late final List<sql.CompleteFile> dbFiles;
|
||||
late final List<sql.AlbumWithShare> albumWithShares;
|
||||
await _c.sqliteDb.use((db) async {
|
||||
dbFiles = await db.completeFilesByFileIds(
|
||||
albumFiles.map((f) => f.fileId!),
|
||||
appAccount: account,
|
||||
);
|
||||
final query = db.select(db.albums).join([
|
||||
sql.leftOuterJoin(
|
||||
db.albumShares, db.albumShares.album.equalsExp(db.albums.rowId)),
|
||||
])
|
||||
..where(db.albums.file.isIn(dbFiles.map((f) => f.file.rowId)));
|
||||
albumWithShares = await query
|
||||
.map((r) => sql.AlbumWithShare(
|
||||
r.readTable(db.albums), r.readTableOrNull(db.albumShares)))
|
||||
.get();
|
||||
});
|
||||
|
||||
// group entries together
|
||||
final fileRowIdMap = <int, sql.CompleteFile>{};
|
||||
for (var f in dbFiles) {
|
||||
fileRowIdMap[f.file.rowId] = f;
|
||||
}
|
||||
final fileIdMap = <int, Map>{};
|
||||
for (final s in albumWithShares) {
|
||||
final f = fileRowIdMap[s.album.file];
|
||||
if (f == null) {
|
||||
_log.severe("[getAll] File missing for album (rowId: ${s.album.rowId}");
|
||||
final failed = <String, Map>{};
|
||||
final albums = await AlbumSqliteDbDataSource2(_c.sqliteDb).getAlbums(
|
||||
account,
|
||||
albumFiles,
|
||||
onError: (v, error, stackTrace) {
|
||||
failed[v.path] = {
|
||||
"file": v,
|
||||
"error": error,
|
||||
"stackTrace": stackTrace,
|
||||
};
|
||||
},
|
||||
);
|
||||
var i = 0;
|
||||
for (final af in albumFiles) {
|
||||
final v = failed[af.path];
|
||||
if (v != null) {
|
||||
if (v["error"] is CacheNotFoundException) {
|
||||
yield const CacheNotFoundException();
|
||||
} else {
|
||||
yield ExceptionEvent(v["error"], v["stackTrace"]);
|
||||
}
|
||||
} else {
|
||||
if (!fileIdMap.containsKey(f.file.fileId)) {
|
||||
fileIdMap[f.file.fileId] = {
|
||||
"file": f,
|
||||
"album": s.album,
|
||||
};
|
||||
}
|
||||
if (s.share != null) {
|
||||
(fileIdMap[f.file.fileId]!["shares"] ??= <sql.AlbumShare>[])
|
||||
.add(s.share!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort as the input list
|
||||
for (final item in albumFiles.map((f) => fileIdMap[f.fileId])) {
|
||||
if (item == null) {
|
||||
// cache not found
|
||||
yield CacheNotFoundException();
|
||||
} else {
|
||||
try {
|
||||
final f = SqliteFileConverter.fromSql(
|
||||
account.userId.toString(), item["file"]);
|
||||
yield SqliteAlbumConverter.fromSql(
|
||||
item["album"], f, item["shares"] ?? []);
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe(
|
||||
"[getAll] Failed while converting DB entry", e, stackTrace);
|
||||
yield ExceptionEvent(e, stackTrace);
|
||||
}
|
||||
yield albums[i++];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -184,64 +119,22 @@ class AlbumSqliteDbDataSource implements AlbumDataSource {
|
|||
@override
|
||||
create(Account account, Album album) async {
|
||||
_log.info("[create]");
|
||||
throw UnimplementedError();
|
||||
return AlbumSqliteDbDataSource2(_c.sqliteDb).create(account, album);
|
||||
}
|
||||
|
||||
@override
|
||||
update(Account account, Album album) async {
|
||||
_log.info("[update] ${album.albumFile!.path}");
|
||||
await _c.sqliteDb.use((db) async {
|
||||
final rowIds =
|
||||
await db.accountFileRowIdsOf(album.albumFile!, appAccount: account);
|
||||
final insert = SqliteAlbumConverter.toSql(
|
||||
album, rowIds.fileRowId, album.albumFile!.etag!);
|
||||
var rowId = await _updateCache(db, rowIds.fileRowId, insert.album);
|
||||
if (rowId == null) {
|
||||
// new album, need insert
|
||||
_log.info("[update] Insert new album");
|
||||
final insertedAlbum =
|
||||
await db.into(db.albums).insertReturning(insert.album);
|
||||
rowId = insertedAlbum.rowId;
|
||||
} else {
|
||||
await (db.delete(db.albumShares)..where((t) => t.album.equals(rowId)))
|
||||
.go();
|
||||
}
|
||||
if (insert.albumShares.isNotEmpty) {
|
||||
await db.batch((batch) {
|
||||
batch.insertAll(
|
||||
db.albumShares,
|
||||
insert.albumShares.map((s) => s.copyWith(album: sql.Value(rowId!))),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<int?> _updateCache(
|
||||
sql.SqliteDb db, int dbFileRowId, sql.AlbumsCompanion dbAlbum) async {
|
||||
final rowIdQuery = db.selectOnly(db.albums)
|
||||
..addColumns([db.albums.rowId])
|
||||
..where(db.albums.file.equals(dbFileRowId))
|
||||
..limit(1);
|
||||
final rowId =
|
||||
await rowIdQuery.map((r) => r.read(db.albums.rowId)!).getSingleOrNull();
|
||||
if (rowId == null) {
|
||||
// new album
|
||||
return null;
|
||||
}
|
||||
|
||||
await (db.update(db.albums)..where((t) => t.rowId.equals(rowId)))
|
||||
.write(dbAlbum);
|
||||
return rowId;
|
||||
return AlbumSqliteDbDataSource2(_c.sqliteDb).update(account, album);
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
}
|
||||
|
||||
/// Backward compatibility only, use [CachedAlbumRepo2] instead
|
||||
@npLog
|
||||
class AlbumCachedDataSource implements AlbumDataSource {
|
||||
AlbumCachedDataSource(DiContainer c)
|
||||
: _sqliteDbSrc = AlbumSqliteDbDataSource(c);
|
||||
AlbumCachedDataSource(DiContainer c) : sqliteDb = c.sqliteDb;
|
||||
|
||||
@override
|
||||
get(Account account, File albumFile) async {
|
||||
|
@ -251,58 +144,31 @@ class AlbumCachedDataSource implements AlbumDataSource {
|
|||
|
||||
@override
|
||||
getAll(Account account, List<File> albumFiles) async* {
|
||||
var i = 0;
|
||||
await for (final cache in _sqliteDbSrc.getAll(account, albumFiles)) {
|
||||
final albumFile = albumFiles[i++];
|
||||
if (_validateCache(cache, albumFile)) {
|
||||
yield cache;
|
||||
} else {
|
||||
// no cache
|
||||
final remote = await _remoteSrc.get(account, albumFile);
|
||||
await _cacheResult(account, remote);
|
||||
yield remote;
|
||||
}
|
||||
final repo = CachedAlbumRepo2(
|
||||
const AlbumRemoteDataSource2(),
|
||||
AlbumSqliteDbDataSource2(sqliteDb),
|
||||
);
|
||||
final albums = await repo.getAlbums(account, albumFiles).last;
|
||||
for (final a in albums) {
|
||||
yield a;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
update(Account account, Album album) async {
|
||||
await _remoteSrc.update(account, album);
|
||||
await _sqliteDbSrc.update(account, album);
|
||||
update(Account account, Album album) {
|
||||
return CachedAlbumRepo2(
|
||||
const AlbumRemoteDataSource2(),
|
||||
AlbumSqliteDbDataSource2(sqliteDb),
|
||||
).update(account, album);
|
||||
}
|
||||
|
||||
@override
|
||||
create(Account account, Album album) => _remoteSrc.create(account, album);
|
||||
|
||||
Future<void> _cacheResult(Account account, Album result) {
|
||||
return _sqliteDbSrc.update(account, result);
|
||||
create(Account account, Album album) {
|
||||
return CachedAlbumRepo2(
|
||||
const AlbumRemoteDataSource2(),
|
||||
AlbumSqliteDbDataSource2(sqliteDb),
|
||||
).create(account, album);
|
||||
}
|
||||
|
||||
bool _validateCache(dynamic cache, File albumFile) {
|
||||
if (cache is Album) {
|
||||
if (cache.albumFile!.etag?.isNotEmpty == true &&
|
||||
cache.albumFile!.etag == albumFile.etag) {
|
||||
// cache is good
|
||||
_log.fine("[_validateCache] etag matched for ${albumFile.path}");
|
||||
return true;
|
||||
} else {
|
||||
_log.info(
|
||||
"[_validateCache] Remote content updated for ${albumFile.path}");
|
||||
return false;
|
||||
}
|
||||
} else if (cache is CacheNotFoundException) {
|
||||
// normal when there's no cache
|
||||
return false;
|
||||
} else if (cache is ExceptionEvent) {
|
||||
_log.shout(
|
||||
"[_validateCache] Cache failure", cache.error, cache.stackTrace);
|
||||
return false;
|
||||
} else {
|
||||
_log.shout("[_validateCache] Unknown type: ${cache.runtimeType}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final _remoteSrc = AlbumRemoteDataSource();
|
||||
final AlbumSqliteDbDataSource _sqliteDbSrc;
|
||||
final sql.SqliteDb sqliteDb;
|
||||
}
|
||||
|
|
247
app/lib/entity/album/data_source2.dart
Normal file
247
app/lib/entity/album/data_source2.dart
Normal file
|
@ -0,0 +1,247 @@
|
|||
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"]);
|
||||
return sql.SqliteAlbumConverter.fromSql(
|
||||
item["album"], 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");
|
||||
}
|
150
app/lib/entity/album/repo2.dart
Normal file
150
app/lib/entity/album/repo2.dart
Normal file
|
@ -0,0 +1,150 @@
|
|||
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) => e.albumFile!) ?? const <File>[],
|
||||
];
|
||||
final remote =
|
||||
await remoteDataSrc.getAlbums(account, outdated, onError: onError);
|
||||
yield (cachedGroup[true] ?? []) + remote;
|
||||
|
||||
// update cache
|
||||
for (final a in remote) {
|
||||
unawaited(cacheDataSrc.update(account, a));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Album> create(Account account, Album album) =>
|
||||
remoteDataSrc.create(account, album);
|
||||
|
||||
@override
|
||||
Future<void> update(Account account, Album album) async {
|
||||
await remoteDataSrc.update(account, album);
|
||||
try {
|
||||
await cacheDataSrc.update(account, album);
|
||||
} catch (e, stackTrace) {
|
||||
_log.warning("[update] Failed to update cache", e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if the cached album is considered up to date
|
||||
bool _validateCache(Album cache, File albumFile) {
|
||||
if (cache.albumFile!.etag?.isNotEmpty == true &&
|
||||
cache.albumFile!.etag == albumFile.etag) {
|
||||
// cache is good
|
||||
_log.fine("[_validateCache] etag matched for ${albumFile.path}");
|
||||
return true;
|
||||
} else {
|
||||
_log.info(
|
||||
"[_validateCache] Remote content updated for ${albumFile.path}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final AlbumDataSource2 remoteDataSrc;
|
||||
final AlbumDataSource2 cacheDataSrc;
|
||||
}
|
||||
|
||||
abstract class AlbumDataSource2 {
|
||||
/// Query all [Album]s defined by [albumFiles]
|
||||
Future<List<Album>> getAlbums(
|
||||
Account account,
|
||||
List<File> albumFiles, {
|
||||
ErrorWithValueHandler<File>? onError,
|
||||
});
|
||||
|
||||
Future<Album> create(Account account, Album album);
|
||||
|
||||
Future<void> update(Account account, Album album);
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'list_favorite.dart';
|
||||
part of 'repo2.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$ListFavoriteNpLog on ListFavorite {
|
||||
extension _$CachedAlbumRepo2NpLog on CachedAlbumRepo2 {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("use_case.list_favorite.ListFavorite");
|
||||
static final log = Logger("entity.album.repo2.CachedAlbumRepo2");
|
||||
}
|
|
@ -1,14 +1,12 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/entity/album/item.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/entity/collection_item/album_item_adapter.dart';
|
||||
import 'package:nc_photos/entity/collection_item/sorter.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
part 'sort_provider.g.dart';
|
||||
|
||||
|
@ -33,6 +31,22 @@ abstract class AlbumSortProvider with EquatableMixin {
|
|||
}
|
||||
}
|
||||
|
||||
factory AlbumSortProvider.fromCollectionItemSort(
|
||||
CollectionItemSort itemSort) {
|
||||
switch (itemSort) {
|
||||
case CollectionItemSort.manual:
|
||||
return const AlbumNullSortProvider();
|
||||
case CollectionItemSort.dateAscending:
|
||||
return const AlbumTimeSortProvider(isAscending: true);
|
||||
case CollectionItemSort.dateDescending:
|
||||
return const AlbumTimeSortProvider(isAscending: false);
|
||||
case CollectionItemSort.nameAscending:
|
||||
return const AlbumFilenameSortProvider(isAscending: true);
|
||||
case CollectionItemSort.nameDescending:
|
||||
return const AlbumFilenameSortProvider(isAscending: false);
|
||||
}
|
||||
}
|
||||
|
||||
JsonObj toJson() {
|
||||
String getType() {
|
||||
if (this is AlbumNullSortProvider) {
|
||||
|
@ -53,7 +67,31 @@ abstract class AlbumSortProvider with EquatableMixin {
|
|||
}
|
||||
|
||||
/// Return a sorted copy of [items]
|
||||
List<AlbumItem> sort(List<AlbumItem> items);
|
||||
List<AlbumItem> sort(List<AlbumItem> items) {
|
||||
final type = toCollectionItemSort();
|
||||
final sorter = CollectionSorter.fromSortType(type);
|
||||
return sorter(items.map(AlbumAdaptedCollectionItem.fromItem).toList())
|
||||
.whereType<AlbumAdaptedCollectionItem>()
|
||||
.map((e) => e.albumItem)
|
||||
.toList();
|
||||
}
|
||||
|
||||
CollectionItemSort toCollectionItemSort() {
|
||||
final that = this;
|
||||
if (that is AlbumNullSortProvider) {
|
||||
return CollectionItemSort.manual;
|
||||
} else if (that is AlbumTimeSortProvider) {
|
||||
return that.isAscending
|
||||
? CollectionItemSort.dateAscending
|
||||
: CollectionItemSort.dateDescending;
|
||||
} else if (that is AlbumFilenameSortProvider) {
|
||||
return that.isAscending
|
||||
? CollectionItemSort.nameAscending
|
||||
: CollectionItemSort.nameDescending;
|
||||
} else {
|
||||
throw StateError("Unknown type: ${sort.runtimeType}");
|
||||
}
|
||||
}
|
||||
|
||||
JsonObj _toContentJson();
|
||||
|
||||
|
@ -72,11 +110,6 @@ class AlbumNullSortProvider extends AlbumSortProvider {
|
|||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
sort(List<AlbumItem> items) {
|
||||
return List.from(items);
|
||||
}
|
||||
|
||||
@override
|
||||
get props => [];
|
||||
|
||||
|
@ -124,37 +157,6 @@ class AlbumTimeSortProvider extends AlbumReversibleSortProvider {
|
|||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
sort(List<AlbumItem> items) {
|
||||
DateTime? prevFileTime;
|
||||
return items
|
||||
.map((e) {
|
||||
if (e is AlbumFileItem) {
|
||||
// take the file time
|
||||
prevFileTime = e.file.bestDateTime;
|
||||
}
|
||||
// for non file items, use the sibling file's time
|
||||
return Tuple2(prevFileTime, e);
|
||||
})
|
||||
.stableSorted((x, y) {
|
||||
if (x.item1 == null && y.item1 == null) {
|
||||
return 0;
|
||||
} else if (x.item1 == null) {
|
||||
return -1;
|
||||
} else if (y.item1 == null) {
|
||||
return 1;
|
||||
} else {
|
||||
if (isAscending) {
|
||||
return x.item1!.compareTo(y.item1!);
|
||||
} else {
|
||||
return y.item1!.compareTo(x.item1!);
|
||||
}
|
||||
}
|
||||
})
|
||||
.map((e) => e.item2)
|
||||
.toList();
|
||||
}
|
||||
|
||||
static const _type = "time";
|
||||
}
|
||||
|
||||
|
@ -174,36 +176,5 @@ class AlbumFilenameSortProvider extends AlbumReversibleSortProvider {
|
|||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
@override
|
||||
sort(List<AlbumItem> items) {
|
||||
String? prevFilename;
|
||||
return items
|
||||
.map((e) {
|
||||
if (e is AlbumFileItem) {
|
||||
// take the file name
|
||||
prevFilename = e.file.filename;
|
||||
}
|
||||
// for non file items, use the sibling file's name
|
||||
return Tuple2(prevFilename, e);
|
||||
})
|
||||
.stableSorted((x, y) {
|
||||
if (x.item1 == null && y.item1 == null) {
|
||||
return 0;
|
||||
} else if (x.item1 == null) {
|
||||
return -1;
|
||||
} else if (y.item1 == null) {
|
||||
return 1;
|
||||
} else {
|
||||
if (isAscending) {
|
||||
return compareNatural(x.item1!, y.item1!);
|
||||
} else {
|
||||
return compareNatural(y.item1!, x.item1!);
|
||||
}
|
||||
}
|
||||
})
|
||||
.map((e) => e.item2)
|
||||
.toList();
|
||||
}
|
||||
|
||||
static const _type = "filename";
|
||||
}
|
||||
|
|
|
@ -1,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;
|
93
app/lib/entity/collection.dart
Normal file
93
app/lib/entity/collection.dart
Normal file
|
@ -0,0 +1,93 @@
|
|||
import 'package:copy_with/copy_with.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 {
|
||||
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.getCoverUrl]
|
||||
String? getCoverUrl(int width, int height) =>
|
||||
contentProvider.getCoverUrl(width, height);
|
||||
|
||||
CollectionSorter getSorter() => CollectionSorter.fromSortType(itemSort);
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
/// Provide the actual content of a collection
|
||||
abstract class CollectionContentProvider {
|
||||
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
|
||||
List<CollectionCapability> get capabilities;
|
||||
|
||||
/// Return the sort type
|
||||
CollectionItemSort get itemSort;
|
||||
|
||||
/// 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
|
||||
String? getCoverUrl(int width, int height);
|
||||
}
|
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}";
|
||||
}
|
||||
}
|
83
app/lib/entity/collection/adapter.dart
Normal file
83
app/lib/entity/collection/adapter.dart
Normal file
|
@ -0,0 +1,83 @@
|
|||
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/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/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_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: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 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
|
||||
///
|
||||
/// [name] and [items] are optional params and if not null, set the value to
|
||||
/// this collection
|
||||
Future<Collection> edit({
|
||||
String? name,
|
||||
List<CollectionItem>? items,
|
||||
CollectionItemSort? itemSort,
|
||||
});
|
||||
|
||||
/// Remove [items] from this collection and return the removed count
|
||||
Future<int> removeItems(
|
||||
List<CollectionItem> items, {
|
||||
ErrorWithValueIndexedHandler<CollectionItem>? onError,
|
||||
required ValueChanged<Collection> onCollectionUpdated,
|
||||
});
|
||||
|
||||
/// Convert a [NewCollectionItem] to an adapted one
|
||||
Future<CollectionItem> adaptToNewItem(NewCollectionItem original);
|
||||
|
||||
bool isItemsRemovable(List<CollectionItem> items);
|
||||
|
||||
/// Remove this collection
|
||||
Future<void> remove();
|
||||
}
|
||||
|
||||
abstract class CollectionItemAdapter {
|
||||
const CollectionItemAdapter();
|
||||
|
||||
CollectionItem toItem();
|
||||
}
|
207
app/lib/entity/collection/adapter/album.dart
Normal file
207
app/lib/entity/collection/adapter/album.dart
Normal file
|
@ -0,0 +1,207 @@
|
|||
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/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_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/iterable_extension.dart';
|
||||
import 'package:nc_photos/object_extension.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/preprocess_album.dart';
|
||||
import 'package:np_codegen/np_codegen.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,
|
||||
}) async {
|
||||
assert(name != null || items != null || itemSort != 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,
|
||||
);
|
||||
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<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 isItemsRemovable(List<CollectionItem> items) {
|
||||
if (_provider.album.albumFile!.isOwned(account.userId)) {
|
||||
return true;
|
||||
}
|
||||
return items
|
||||
.whereType<AlbumAdaptedCollectionItem>()
|
||||
.any((e) => e.albumItem.addedBy == account.userId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove() => RemoveAlbum(_c)(account, _provider.album);
|
||||
|
||||
final DiContainer _c;
|
||||
final Account account;
|
||||
final Collection collection;
|
||||
|
||||
final CollectionAlbumProvider _provider;
|
||||
}
|
||||
|
||||
// class CollectionAlbumItemAdapter implements CollectionItemAdapter {}
|
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");
|
||||
}
|
56
app/lib/entity/collection/adapter/location_group.dart
Normal file
56
app/lib/entity/collection/adapter/location_group.dart
Normal file
|
@ -0,0 +1,56 @@
|
|||
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/read_only_adapter.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 CollectionReadOnlyAdapter
|
||||
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
|
||||
Future<void> remove() {
|
||||
throw UnsupportedError("Operation not supported");
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
final Account account;
|
||||
final Collection collection;
|
||||
|
||||
final CollectionLocationGroupProvider _provider;
|
||||
}
|
151
app/lib/entity/collection/adapter/nc_album.dart
Normal file
151
app/lib/entity/collection/adapter/nc_album.dart
Normal file
|
@ -0,0 +1,151 @@
|
|||
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/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/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/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 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) => FindFileDescriptor(_c)(
|
||||
account,
|
||||
items.map((e) => e.fileId).toList(),
|
||||
onFileNotFound: (fileId) {
|
||||
_log.severe("[listItem] File not found: $fileId");
|
||||
},
|
||||
))
|
||||
.map((files) => files.map(BasicCollectionFileItem.new).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,
|
||||
}) async {
|
||||
assert(name != null);
|
||||
if (items != null || itemSort != 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 isItemsRemovable(List<CollectionItem> items) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove() => RemoveNcAlbum(_c)(account, _provider.album);
|
||||
|
||||
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");
|
||||
}
|
58
app/lib/entity/collection/adapter/person.dart
Normal file
58
app/lib/entity/collection/adapter/person.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/read_only_adapter.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 CollectionReadOnlyAdapter
|
||||
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
|
||||
Future<void> remove() {
|
||||
throw UnsupportedError("Operation not supported");
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
final Account account;
|
||||
final Collection collection;
|
||||
|
||||
final CollectionPersonProvider _provider;
|
||||
}
|
42
app/lib/entity/collection/adapter/read_only_adapter.dart
Normal file
42
app/lib/entity/collection/adapter/read_only_adapter.dart
Normal file
|
@ -0,0 +1,42 @@
|
|||
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_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
|
||||
/// A read-only collection that does not support modifying its items
|
||||
mixin CollectionReadOnlyAdapter 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,
|
||||
}) {
|
||||
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 isItemsRemovable(List<CollectionItem> items) {
|
||||
return false;
|
||||
}
|
||||
}
|
45
app/lib/entity/collection/adapter/tag.dart
Normal file
45
app/lib/entity/collection/adapter/tag.dart
Normal file
|
@ -0,0 +1,45 @@
|
|||
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/read_only_adapter.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 CollectionReadOnlyAdapter
|
||||
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
|
||||
Future<void> remove() {
|
||||
throw UnsupportedError("Operation not supported");
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
75
app/lib/entity/collection/content_provider/album.dart
Normal file
75
app/lib/entity/collection/content_provider/album.dart
Normal file
|
@ -0,0 +1,75 @@
|
|||
import 'package:copy_with/copy_with.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_item/util.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'album.g.dart';
|
||||
|
||||
/// Album provided by our app
|
||||
@genCopyWith
|
||||
@toString
|
||||
class CollectionAlbumProvider 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 => [
|
||||
if (album.provider is AlbumStaticProvider) ...[
|
||||
CollectionCapability.manualItem,
|
||||
CollectionCapability.sort,
|
||||
CollectionCapability.manualSort,
|
||||
CollectionCapability.rename,
|
||||
CollectionCapability.labelItem,
|
||||
],
|
||||
];
|
||||
|
||||
@override
|
||||
CollectionItemSort get itemSort => album.sortProvider.toCollectionItemSort();
|
||||
|
||||
@override
|
||||
String? getCoverUrl(int width, int height) {
|
||||
final fd = album.coverProvider.getCover(album);
|
||||
if (fd == null) {
|
||||
return null;
|
||||
} else {
|
||||
return api_util.getFilePreviewUrlByFileId(
|
||||
account,
|
||||
fd.fdId,
|
||||
width: width,
|
||||
height: height,
|
||||
isKeepAspectRatio: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,44 @@
|
|||
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_item/util.dart';
|
||||
import 'package:nc_photos/use_case/list_location_group.dart';
|
||||
|
||||
class CollectionLocationGroupProvider 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
|
||||
String? getCoverUrl(int width, int height) {
|
||||
return api_util.getFilePreviewUrlByFileId(
|
||||
account,
|
||||
location.latestFileId,
|
||||
width: width,
|
||||
height: height,
|
||||
isKeepAspectRatio: false,
|
||||
);
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final LocationGroup location;
|
||||
}
|
62
app/lib/entity/collection/content_provider/nc_album.dart
Normal file
62
app/lib/entity/collection/content_provider/nc_album.dart
Normal file
|
@ -0,0 +1,62 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:copy_with/copy_with.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_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 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,
|
||||
];
|
||||
|
||||
@override
|
||||
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
||||
|
||||
@override
|
||||
String? getCoverUrl(int width, int height) {
|
||||
if (album.lastPhoto == null) {
|
||||
return null;
|
||||
} else {
|
||||
return api_util.getFilePreviewUrlByFileId(
|
||||
account,
|
||||
album.lastPhoto!,
|
||||
width: width,
|
||||
height: height,
|
||||
isKeepAspectRatio: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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}";
|
||||
}
|
||||
}
|
42
app/lib/entity/collection/content_provider/person.dart
Normal file
42
app/lib/entity/collection/content_provider/person.dart
Normal file
|
@ -0,0 +1,42 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:clock/clock.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_item/util.dart';
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
|
||||
class CollectionPersonProvider 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
|
||||
String? getCoverUrl(int width, int height) {
|
||||
return api_util.getFacePreviewUrl(account, person.thumbFaceId,
|
||||
size: math.max(width, height));
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final Person person;
|
||||
}
|
36
app/lib/entity/collection/content_provider/tag.dart
Normal file
36
app/lib/entity/collection/content_provider/tag.dart
Normal file
|
@ -0,0 +1,36 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
|
||||
class CollectionTagProvider 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
|
||||
String? getCoverUrl(int width, int height) => null;
|
||||
|
||||
final Account account;
|
||||
final List<Tag> tags;
|
||||
}
|
43
app/lib/entity/collection/util.dart
Normal file
43
app/lib/entity/collection/util.dart
Normal file
|
@ -0,0 +1,43 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
enum CollectionSort {
|
||||
dateDescending,
|
||||
dateAscending,
|
||||
nameAscending,
|
||||
nameDescending;
|
||||
|
||||
bool isAscending() {
|
||||
return this == CollectionSort.dateAscending ||
|
||||
this == CollectionSort.nameAscending;
|
||||
}
|
||||
}
|
||||
|
||||
extension CollectionListExtension on List<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();
|
||||
}
|
||||
}
|
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}}";
|
||||
}
|
||||
}
|
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;
|
||||
}
|
|
@ -575,7 +575,7 @@ class FileRepo {
|
|||
dataSrc.listMinimal(account, dir);
|
||||
|
||||
/// See [FileDataSource.remove]
|
||||
Future<void> remove(Account account, File file) =>
|
||||
Future<void> remove(Account account, FileDescriptor file) =>
|
||||
dataSrc.remove(account, file);
|
||||
|
||||
/// See [FileDataSource.getBinary]
|
||||
|
@ -659,7 +659,7 @@ abstract class FileDataSource {
|
|||
Future<List<File>> listMinimal(Account account, File dir);
|
||||
|
||||
/// Remove file
|
||||
Future<void> remove(Account account, File f);
|
||||
Future<void> remove(Account account, FileDescriptor f);
|
||||
|
||||
/// Read file as binary array
|
||||
Future<Uint8List> getBinary(Account account, File f);
|
||||
|
|
|
@ -90,10 +90,10 @@ class FileWebdavDataSource implements FileDataSource {
|
|||
}
|
||||
|
||||
@override
|
||||
remove(Account account, File f) async {
|
||||
_log.info("[remove] ${f.path}");
|
||||
remove(Account account, FileDescriptor f) async {
|
||||
_log.info("[remove] ${f.fdPath}");
|
||||
final response =
|
||||
await ApiUtil.fromAccount(account).files().delete(path: f.path);
|
||||
await ApiUtil.fromAccount(account).files().delete(path: f.fdPath);
|
||||
if (!response.isGood) {
|
||||
_log.severe("[remove] Failed requesting server: $response");
|
||||
throw ApiException(
|
||||
|
@ -435,8 +435,8 @@ class FileSqliteDbDataSource implements FileDataSource {
|
|||
}
|
||||
|
||||
@override
|
||||
remove(Account account, File f) {
|
||||
_log.info("[remove] ${f.path}");
|
||||
remove(Account account, FileDescriptor f) {
|
||||
_log.info("[remove] ${f.fdPath}");
|
||||
return FileSqliteCacheRemover(_c)(account, f);
|
||||
}
|
||||
|
||||
|
@ -719,7 +719,7 @@ class FileCachedDataSource implements FileDataSource {
|
|||
}
|
||||
|
||||
@override
|
||||
remove(Account account, File f) async {
|
||||
remove(Account account, FileDescriptor f) async {
|
||||
await _remoteSrc.remove(account, f);
|
||||
try {
|
||||
await _sqliteDbSrc.remove(account, f);
|
||||
|
|
|
@ -359,7 +359,7 @@ class FileSqliteCacheRemover {
|
|||
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
|
||||
|
||||
/// Remove a file/dir from cache
|
||||
Future<void> call(Account account, File f) async {
|
||||
Future<void> call(Account account, FileDescriptor f) async {
|
||||
await _c.sqliteDb.use((db) async {
|
||||
final dbAccount = await db.accountOf(account);
|
||||
final rowIds = await db.accountFileRowIdsOf(f, sqlAccount: dbAccount);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
import 'package:path/path.dart' as path_lib;
|
||||
|
||||
|
@ -107,4 +108,14 @@ extension FileDescriptorExtension on FileDescriptor {
|
|||
|
||||
/// hashCode to be used with [compareServerIdentity]
|
||||
int get identityHashCode => fdId.hashCode;
|
||||
|
||||
File toFile() {
|
||||
return File(
|
||||
path: fdPath,
|
||||
fileId: fdId,
|
||||
contentType: fdMime,
|
||||
isArchived: fdIsArchived,
|
||||
isFavorite: fdIsFavorite,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,11 @@ bool isSupportedImageMime(String mime) =>
|
|||
bool isSupportedImageFormat(FileDescriptor file) =>
|
||||
isSupportedImageMime(file.fdMime ?? "");
|
||||
|
||||
bool isSupportedVideoMime(String mime) =>
|
||||
supportedVideoFormatMimes.contains(mime);
|
||||
|
||||
bool isSupportedVideoFormat(FileDescriptor file) =>
|
||||
isSupportedFormat(file) && file.fdMime?.startsWith("video/") == true;
|
||||
isSupportedVideoMime(file.fdMime ?? "");
|
||||
|
||||
bool isMetadataSupportedMime(String mime) =>
|
||||
_metadataSupportedFormatMimes.contains(mime);
|
||||
|
@ -32,21 +35,22 @@ bool isMetadataSupportedFormat(FileDescriptor file) =>
|
|||
bool isTrash(Account account, FileDescriptor file) =>
|
||||
file.fdPath.startsWith(api_util.getTrashbinPath(account));
|
||||
|
||||
bool isAlbumFile(Account account, File file) =>
|
||||
file.path.startsWith(remote_storage_util.getRemoteAlbumsDir(account));
|
||||
bool isAlbumFile(Account account, FileDescriptor file) =>
|
||||
file.fdPath.startsWith(remote_storage_util.getRemoteAlbumsDir(account));
|
||||
|
||||
/// Return if [file] is located under [dir]
|
||||
///
|
||||
/// Return false if [file] is [dir] itself (since it's not "under")
|
||||
///
|
||||
/// See [isOrUnderDir]
|
||||
bool isUnderDir(File file, File dir) => file.path.startsWith("${dir.path}/");
|
||||
bool isUnderDir(FileDescriptor file, FileDescriptor dir) =>
|
||||
file.fdPath.startsWith("${dir.fdPath}/");
|
||||
|
||||
/// Return if [file] is [dir] or located under [dir]
|
||||
///
|
||||
/// See [isUnderDir]
|
||||
bool isOrUnderDir(File file, File dir) =>
|
||||
file.path == dir.path || isUnderDir(file, dir);
|
||||
bool isOrUnderDir(FileDescriptor file, FileDescriptor dir) =>
|
||||
file.fdPath == dir.fdPath || isUnderDir(file, dir);
|
||||
|
||||
/// Convert a stripped path to a full path
|
||||
///
|
||||
|
@ -118,6 +122,9 @@ final supportedFormatMimes = [
|
|||
final supportedImageFormatMimes =
|
||||
supportedFormatMimes.where((f) => f.startsWith("image/")).toList();
|
||||
|
||||
final supportedVideoFormatMimes =
|
||||
supportedFormatMimes.where((f) => f.startsWith("video/")).toList();
|
||||
|
||||
const _metadataSupportedFormatMimes = [
|
||||
"image/jpeg",
|
||||
"image/heic",
|
||||
|
|
100
app/lib/entity/nc_album.dart
Normal file
100
app/lib/entity/nc_album.dart
Normal file
|
@ -0,0 +1,100 @@
|
|||
import 'package:copy_with/copy_with.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:np_common/string_extension.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'nc_album.g.dart';
|
||||
|
||||
/// Server-side album since Nextcloud 25
|
||||
@toString
|
||||
@genCopyWith
|
||||
class NcAlbum {
|
||||
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: "remote.php/dav/photos/${account.userId}/albums/$name",
|
||||
lastPhoto: null,
|
||||
nbItems: 0,
|
||||
location: null,
|
||||
dateStart: null,
|
||||
dateEnd: null,
|
||||
collaborators: const [],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
class NcAlbumCollaborator {}
|
69
app/lib/entity/nc_album.g.dart
Normal file
69
app/lib/entity/nc_album.g.dart
Normal file
|
@ -0,0 +1,69 @@
|
|||
// 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 $NcAlbumCopyWithWorker {
|
||||
NcAlbum call(
|
||||
{String? path,
|
||||
int? lastPhoto,
|
||||
int? nbItems,
|
||||
String? location,
|
||||
DateTime? dateStart,
|
||||
DateTime? dateEnd,
|
||||
List<NcAlbumCollaborator>? collaborators});
|
||||
}
|
||||
|
||||
class _$NcAlbumCopyWithWorkerImpl implements $NcAlbumCopyWithWorker {
|
||||
_$NcAlbumCopyWithWorkerImpl(this.that);
|
||||
|
||||
@override
|
||||
NcAlbum call(
|
||||
{dynamic path,
|
||||
dynamic lastPhoto = copyWithNull,
|
||||
dynamic nbItems,
|
||||
dynamic location = copyWithNull,
|
||||
dynamic dateStart = copyWithNull,
|
||||
dynamic dateEnd = copyWithNull,
|
||||
dynamic collaborators}) {
|
||||
return NcAlbum(
|
||||
path: path as String? ?? that.path,
|
||||
lastPhoto:
|
||||
lastPhoto == copyWithNull ? that.lastPhoto : lastPhoto as int?,
|
||||
nbItems: nbItems as int? ?? that.nbItems,
|
||||
location:
|
||||
location == copyWithNull ? that.location : location as String?,
|
||||
dateStart:
|
||||
dateStart == copyWithNull ? that.dateStart : dateStart as DateTime?,
|
||||
dateEnd: dateEnd == copyWithNull ? that.dateEnd : dateEnd as DateTime?,
|
||||
collaborators:
|
||||
collaborators as List<NcAlbumCollaborator>? ?? that.collaborators);
|
||||
}
|
||||
|
||||
final NcAlbum that;
|
||||
}
|
||||
|
||||
extension $NcAlbumCopyWith on NcAlbum {
|
||||
$NcAlbumCopyWithWorker get copyWith => _$copyWith;
|
||||
$NcAlbumCopyWithWorker get _$copyWith => _$NcAlbumCopyWithWorkerImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$NcAlbumToString on NcAlbum {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "NcAlbum {path: $path, lastPhoto: $lastPhoto, nbItems: $nbItems, location: $location, dateStart: $dateStart, dateEnd: $dateEnd, collaborators: [length: ${collaborators.length}]}";
|
||||
}
|
||||
}
|
241
app/lib/entity/nc_album/data_source.dart
Normal file
241
app/lib/entity/nc_album/data_source.dart
Normal file
|
@ -0,0 +1,241 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/entity_converter.dart';
|
||||
import 'package:nc_photos/entity/nc_album.dart';
|
||||
import 'package:nc_photos/entity/nc_album/item.dart';
|
||||
import 'package:nc_photos/entity/nc_album/repo.dart';
|
||||
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
|
||||
import 'package:nc_photos/entity/sqlite/type_converter.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/list_util.dart' as list_util;
|
||||
import 'package:nc_photos/np_api_util.dart';
|
||||
import 'package:np_api/np_api.dart' as api;
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
|
||||
part 'data_source.g.dart';
|
||||
|
||||
@npLog
|
||||
class NcAlbumRemoteDataSource implements NcAlbumDataSource {
|
||||
const NcAlbumRemoteDataSource();
|
||||
|
||||
@override
|
||||
Future<List<NcAlbum>> getAlbums(Account account) async {
|
||||
_log.info("[getAlbums] account: ${account.userId}");
|
||||
final response = await ApiUtil.fromAccount(account)
|
||||
.photos(account.userId.toString())
|
||||
.albums()
|
||||
.propfind(
|
||||
lastPhoto: 1,
|
||||
nbItems: 1,
|
||||
location: 1,
|
||||
dateRange: 1,
|
||||
collaborators: 1,
|
||||
);
|
||||
if (!response.isGood) {
|
||||
_log.severe("[getAlbums] Failed requesting server: $response");
|
||||
throw ApiException(
|
||||
response: response,
|
||||
message: "Server responed with an error: HTTP ${response.statusCode}",
|
||||
);
|
||||
}
|
||||
|
||||
final apiNcAlbums = await api.NcAlbumParser().parse(response.body);
|
||||
return apiNcAlbums
|
||||
.map(ApiNcAlbumConverter.fromApi)
|
||||
.where((a) => a.strippedPath != ".")
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> create(Account account, NcAlbum album) async {
|
||||
_log.info("[create] account: ${account.userId}, album: ${album.path}");
|
||||
final response = await ApiUtil.fromAccount(account)
|
||||
.photos(account.userId.toString())
|
||||
.album(album.strippedPath)
|
||||
.mkcol();
|
||||
if (!response.isGood) {
|
||||
_log.severe("[create] Failed requesting server: $response");
|
||||
throw ApiException(
|
||||
response: response,
|
||||
message: "Server responed with an error: HTTP ${response.statusCode}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove(Account account, NcAlbum album) async {
|
||||
_log.info("[remove] account: ${account.userId}, album: ${album.path}");
|
||||
final response = await ApiUtil.fromAccount(account)
|
||||
.photos(account.userId.toString())
|
||||
.album(album.strippedPath)
|
||||
.delete();
|
||||
if (!response.isGood) {
|
||||
_log.severe("[remove] Failed requesting server: $response");
|
||||
throw ApiException(
|
||||
response: response,
|
||||
message: "Server responed with an error: HTTP ${response.statusCode}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<NcAlbumItem>> getItems(Account account, NcAlbum album) async {
|
||||
_log.info(
|
||||
"[getItems] account: ${account.userId}, album: ${album.strippedPath}");
|
||||
final response = await ApiUtil.fromAccount(account).files().propfind(
|
||||
path: album.path,
|
||||
fileid: 1,
|
||||
);
|
||||
if (!response.isGood) {
|
||||
_log.severe("[getItems] Failed requesting server: $response");
|
||||
throw ApiException(
|
||||
response: response,
|
||||
message: "Server responed with an error: HTTP ${response.statusCode}",
|
||||
);
|
||||
}
|
||||
|
||||
final apiFiles = await api.FileParser().parse(response.body);
|
||||
return apiFiles
|
||||
.where((f) => f.fileId != null)
|
||||
.map(ApiFileConverter.fromApi)
|
||||
.map((f) => NcAlbumItem(f.fileId!))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@npLog
|
||||
class NcAlbumSqliteDbDataSource implements NcAlbumCacheDataSource {
|
||||
const NcAlbumSqliteDbDataSource(this.sqliteDb);
|
||||
|
||||
@override
|
||||
Future<List<NcAlbum>> getAlbums(Account account) async {
|
||||
_log.info("[getAlbums] account: ${account.userId}");
|
||||
final dbAlbums = await sqliteDb.use((db) async {
|
||||
return await db.ncAlbumsByAccount(account: sql.ByAccount.app(account));
|
||||
});
|
||||
return dbAlbums
|
||||
.map((a) {
|
||||
try {
|
||||
return SqliteNcAlbumConverter.fromSql(account.userId.toString(), a);
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe(
|
||||
"[getAlbums] Failed while converting DB entry", e, stackTrace);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.whereNotNull()
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> create(Account account, NcAlbum album) async {
|
||||
_log.info("[create] account: ${account.userId}, album: ${album.path}");
|
||||
await sqliteDb.use((db) async {
|
||||
await db.insertNcAlbum(
|
||||
account: sql.ByAccount.app(account),
|
||||
object: SqliteNcAlbumConverter.toSql(null, album),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove(Account account, NcAlbum album) async {
|
||||
_log.info("[remove] account: ${account.userId}, album: ${album.path}");
|
||||
await sqliteDb.use((db) async {
|
||||
await db.deleteNcAlbumByRelativePath(
|
||||
account: sql.ByAccount.app(account),
|
||||
relativePath: album.strippedPath,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<NcAlbumItem>> getItems(Account account, NcAlbum album) async {
|
||||
_log.info(
|
||||
"[getItems] account: ${account.userId}, album: ${album.strippedPath}");
|
||||
final dbItems = await sqliteDb.use((db) async {
|
||||
return await db.ncAlbumItemsByParentRelativePath(
|
||||
account: sql.ByAccount.app(account),
|
||||
parentRelativePath: album.strippedPath,
|
||||
);
|
||||
});
|
||||
return dbItems.map((i) => NcAlbumItem(i.fileId)).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateAlbumsCache(Account account, List<NcAlbum> remote) async {
|
||||
await sqliteDb.use((db) async {
|
||||
final dbAccount = await db.accountOf(account);
|
||||
final existings = (await db.partialNcAlbumsByAccount(
|
||||
account: sql.ByAccount.sql(dbAccount),
|
||||
columns: [db.ncAlbums.rowId, db.ncAlbums.relativePath],
|
||||
))
|
||||
.whereNotNull()
|
||||
.toList();
|
||||
await db.batch((batch) async {
|
||||
for (final r in remote) {
|
||||
final dbObj = SqliteNcAlbumConverter.toSql(dbAccount, r);
|
||||
final found = existings.indexWhere((e) => e[1] == r.strippedPath);
|
||||
if (found != -1) {
|
||||
// existing record, update it
|
||||
batch.update(
|
||||
db.ncAlbums,
|
||||
dbObj,
|
||||
where: (sql.$NcAlbumsTable t) =>
|
||||
t.rowId.equals(existings[found][0]),
|
||||
);
|
||||
} else {
|
||||
// insert
|
||||
batch.insert(db.ncAlbums, dbObj);
|
||||
}
|
||||
}
|
||||
for (final e in existings
|
||||
.where((e) => !remote.any((r) => r.strippedPath == e[1]))) {
|
||||
batch.deleteWhere(
|
||||
db.ncAlbums,
|
||||
(sql.$NcAlbumsTable t) => t.rowId.equals(e[0]),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateItemsCache(
|
||||
Account account, NcAlbum album, List<NcAlbumItem> remote) async {
|
||||
await sqliteDb.use((db) async {
|
||||
final dbAlbum = await db.ncAlbumByRelativePath(
|
||||
account: sql.ByAccount.app(account),
|
||||
relativePath: album.strippedPath,
|
||||
);
|
||||
final existingItems = await db.ncAlbumItemsByParent(
|
||||
parent: dbAlbum!,
|
||||
);
|
||||
final idDiff = list_util.diff(
|
||||
existingItems.map((e) => e.fileId).sorted((a, b) => a.compareTo(b)),
|
||||
remote.map((e) => e.fileId).sorted((a, b) => a.compareTo(b)),
|
||||
);
|
||||
if (idDiff.onlyInA.isNotEmpty || idDiff.onlyInB.isNotEmpty) {
|
||||
await db.batch((batch) async {
|
||||
for (final id in idDiff.onlyInB) {
|
||||
// new
|
||||
batch.insert(
|
||||
db.ncAlbumItems,
|
||||
SqliteNcAlbumItemConverter.toSql(dbAlbum, id),
|
||||
);
|
||||
}
|
||||
// removed
|
||||
batch.deleteWhere(
|
||||
db.ncAlbumItems,
|
||||
(sql.$NcAlbumItemsTable t) =>
|
||||
t.parent.equals(dbAlbum.rowId) & t.fileId.isIn(idDiff.onlyInA),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final sql.SqliteDb sqliteDb;
|
||||
}
|
23
app/lib/entity/nc_album/data_source.g.dart
Normal file
23
app/lib/entity/nc_album/data_source.g.dart
Normal file
|
@ -0,0 +1,23 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'data_source.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$NcAlbumRemoteDataSourceNpLog on NcAlbumRemoteDataSource {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log =
|
||||
Logger("entity.nc_album.data_source.NcAlbumRemoteDataSource");
|
||||
}
|
||||
|
||||
extension _$NcAlbumSqliteDbDataSourceNpLog on NcAlbumSqliteDbDataSource {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log =
|
||||
Logger("entity.nc_album.data_source.NcAlbumSqliteDbDataSource");
|
||||
}
|
5
app/lib/entity/nc_album/item.dart
Normal file
5
app/lib/entity/nc_album/item.dart
Normal file
|
@ -0,0 +1,5 @@
|
|||
class NcAlbumItem {
|
||||
const NcAlbumItem(this.fileId);
|
||||
|
||||
final int fileId;
|
||||
}
|
140
app/lib/entity/nc_album/repo.dart
Normal file
140
app/lib/entity/nc_album/repo.dart
Normal file
|
@ -0,0 +1,140 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/entity/nc_album.dart';
|
||||
import 'package:nc_photos/entity/nc_album/item.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
|
||||
part 'repo.g.dart';
|
||||
|
||||
abstract class NcAlbumRepo {
|
||||
/// Query all [NcAlbum]s belonging to [account]
|
||||
///
|
||||
/// Normally the stream should complete with only a single event, but some
|
||||
/// implementation might want to return multiple set of values, say one set of
|
||||
/// cached value and later another set of updated value from a remote source.
|
||||
/// In any case, each event is guaranteed to be one complete set of data
|
||||
Stream<List<NcAlbum>> getAlbums(Account account);
|
||||
|
||||
/// Create a new [album]
|
||||
Future<void> create(Account account, NcAlbum album);
|
||||
|
||||
/// Remove [album]
|
||||
Future<void> remove(Account account, NcAlbum album);
|
||||
|
||||
/// Query all items belonging to [album]
|
||||
Stream<List<NcAlbumItem>> getItems(Account account, NcAlbum album);
|
||||
}
|
||||
|
||||
/// A repo that simply relay the call to the backed [NcAlbumDataSource]
|
||||
@npLog
|
||||
class BasicNcAlbumRepo implements NcAlbumRepo {
|
||||
const BasicNcAlbumRepo(this.dataSrc);
|
||||
|
||||
@override
|
||||
Stream<List<NcAlbum>> getAlbums(Account account) async* {
|
||||
yield await dataSrc.getAlbums(account);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> create(Account account, NcAlbum album) =>
|
||||
dataSrc.create(account, album);
|
||||
|
||||
@override
|
||||
Future<void> remove(Account account, NcAlbum album) =>
|
||||
dataSrc.remove(account, album);
|
||||
|
||||
@override
|
||||
Stream<List<NcAlbumItem>> getItems(Account account, NcAlbum album) async* {
|
||||
yield await dataSrc.getItems(account, album);
|
||||
}
|
||||
|
||||
final NcAlbumDataSource dataSrc;
|
||||
}
|
||||
|
||||
/// A repo that manage a remote data source and a cache data source
|
||||
@npLog
|
||||
class CachedNcAlbumRepo implements NcAlbumRepo {
|
||||
const CachedNcAlbumRepo(this.remoteDataSrc, this.cacheDataSrc);
|
||||
|
||||
@override
|
||||
Stream<List<NcAlbum>> getAlbums(Account account) async* {
|
||||
// get cache
|
||||
try {
|
||||
yield await cacheDataSrc.getAlbums(account);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[getAlbums] Cache failure", e, stackTrace);
|
||||
}
|
||||
|
||||
// query remote
|
||||
final remote = await remoteDataSrc.getAlbums(account);
|
||||
yield remote;
|
||||
|
||||
// update cache
|
||||
unawaited(cacheDataSrc.updateAlbumsCache(account, remote));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> create(Account account, NcAlbum album) async {
|
||||
await remoteDataSrc.create(account, album);
|
||||
try {
|
||||
await cacheDataSrc.create(account, album);
|
||||
} catch (e, stackTrace) {
|
||||
_log.warning("[create] Failed to insert cache", e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove(Account account, NcAlbum album) async {
|
||||
await remoteDataSrc.remove(account, album);
|
||||
try {
|
||||
await cacheDataSrc.remove(account, album);
|
||||
} catch (e, stackTrace) {
|
||||
_log.warning("[remove] Failed to remove cache", e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<NcAlbumItem>> getItems(Account account, NcAlbum album) async* {
|
||||
// get cache
|
||||
try {
|
||||
yield await cacheDataSrc.getItems(account, album);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[getItems] Cache failure", e, stackTrace);
|
||||
}
|
||||
|
||||
// query remote
|
||||
final remote = await remoteDataSrc.getItems(account, album);
|
||||
yield remote;
|
||||
|
||||
// update cache
|
||||
await cacheDataSrc.updateItemsCache(account, album, remote);
|
||||
}
|
||||
|
||||
final NcAlbumDataSource remoteDataSrc;
|
||||
final NcAlbumCacheDataSource cacheDataSrc;
|
||||
}
|
||||
|
||||
abstract class NcAlbumDataSource {
|
||||
/// Query all [NcAlbum]s belonging to [account]
|
||||
Future<List<NcAlbum>> getAlbums(Account account);
|
||||
|
||||
/// Create a new [album]
|
||||
Future<void> create(Account account, NcAlbum album);
|
||||
|
||||
/// Remove [album]
|
||||
Future<void> remove(Account account, NcAlbum album);
|
||||
|
||||
/// Query all items belonging to [album]
|
||||
Future<List<NcAlbumItem>> getItems(Account account, NcAlbum album);
|
||||
}
|
||||
|
||||
abstract class NcAlbumCacheDataSource extends NcAlbumDataSource {
|
||||
/// Update cache to match [remote]
|
||||
Future<void> updateAlbumsCache(Account account, List<NcAlbum> remote);
|
||||
|
||||
/// Update cache to match [remote]
|
||||
Future<void> updateItemsCache(
|
||||
Account account, NcAlbum album, List<NcAlbumItem> remote);
|
||||
}
|
21
app/lib/entity/nc_album/repo.g.dart
Normal file
21
app/lib/entity/nc_album/repo.g.dart
Normal file
|
@ -0,0 +1,21 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'repo.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$BasicNcAlbumRepoNpLog on BasicNcAlbumRepo {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("entity.nc_album.repo.BasicNcAlbumRepo");
|
||||
}
|
||||
|
||||
extension _$CachedNcAlbumRepoNpLog on CachedNcAlbumRepo {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("entity.nc_album.repo.CachedNcAlbumRepo");
|
||||
}
|
|
@ -2,7 +2,7 @@ import 'package:drift/drift.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart' as app;
|
||||
import 'package:nc_photos/entity/file.dart' as app;
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart' as app;
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/entity/sqlite/files_query_builder.dart';
|
||||
import 'package:nc_photos/entity/sqlite/isolate_util.dart';
|
||||
|
@ -19,6 +19,7 @@ import 'package:np_codegen/np_codegen.dart';
|
|||
|
||||
part 'database.g.dart';
|
||||
part 'database_extension.dart';
|
||||
part 'database/nc_album_extension.dart';
|
||||
|
||||
// remember to also update the truncate method after adding a new table
|
||||
@npLog
|
||||
|
@ -36,6 +37,8 @@ part 'database_extension.dart';
|
|||
AlbumShares,
|
||||
Tags,
|
||||
Persons,
|
||||
NcAlbums,
|
||||
NcAlbumItems,
|
||||
],
|
||||
)
|
||||
class SqliteDb extends _$SqliteDb {
|
||||
|
|
|
@ -4157,6 +4157,639 @@ class $PersonsTable extends Persons with TableInfo<$PersonsTable, Person> {
|
|||
}
|
||||
}
|
||||
|
||||
class NcAlbum extends DataClass implements Insertable<NcAlbum> {
|
||||
final int rowId;
|
||||
final int account;
|
||||
final String relativePath;
|
||||
final int? lastPhoto;
|
||||
final int nbItems;
|
||||
final String? location;
|
||||
final DateTime? dateStart;
|
||||
final DateTime? dateEnd;
|
||||
NcAlbum(
|
||||
{required this.rowId,
|
||||
required this.account,
|
||||
required this.relativePath,
|
||||
this.lastPhoto,
|
||||
required this.nbItems,
|
||||
this.location,
|
||||
this.dateStart,
|
||||
this.dateEnd});
|
||||
factory NcAlbum.fromData(Map<String, dynamic> data, {String? prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
return NcAlbum(
|
||||
rowId: const IntType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}row_id'])!,
|
||||
account: const IntType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}account'])!,
|
||||
relativePath: const StringType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}relative_path'])!,
|
||||
lastPhoto: const IntType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}last_photo']),
|
||||
nbItems: const IntType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}nb_items'])!,
|
||||
location: const StringType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}location']),
|
||||
dateStart: $NcAlbumsTable.$converter0.mapToDart(const DateTimeType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}date_start'])),
|
||||
dateEnd: $NcAlbumsTable.$converter1.mapToDart(const DateTimeType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}date_end'])),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['row_id'] = Variable<int>(rowId);
|
||||
map['account'] = Variable<int>(account);
|
||||
map['relative_path'] = Variable<String>(relativePath);
|
||||
if (!nullToAbsent || lastPhoto != null) {
|
||||
map['last_photo'] = Variable<int?>(lastPhoto);
|
||||
}
|
||||
map['nb_items'] = Variable<int>(nbItems);
|
||||
if (!nullToAbsent || location != null) {
|
||||
map['location'] = Variable<String?>(location);
|
||||
}
|
||||
if (!nullToAbsent || dateStart != null) {
|
||||
final converter = $NcAlbumsTable.$converter0;
|
||||
map['date_start'] = Variable<DateTime?>(converter.mapToSql(dateStart));
|
||||
}
|
||||
if (!nullToAbsent || dateEnd != null) {
|
||||
final converter = $NcAlbumsTable.$converter1;
|
||||
map['date_end'] = Variable<DateTime?>(converter.mapToSql(dateEnd));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
NcAlbumsCompanion toCompanion(bool nullToAbsent) {
|
||||
return NcAlbumsCompanion(
|
||||
rowId: Value(rowId),
|
||||
account: Value(account),
|
||||
relativePath: Value(relativePath),
|
||||
lastPhoto: lastPhoto == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(lastPhoto),
|
||||
nbItems: Value(nbItems),
|
||||
location: location == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(location),
|
||||
dateStart: dateStart == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(dateStart),
|
||||
dateEnd: dateEnd == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(dateEnd),
|
||||
);
|
||||
}
|
||||
|
||||
factory NcAlbum.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return NcAlbum(
|
||||
rowId: serializer.fromJson<int>(json['rowId']),
|
||||
account: serializer.fromJson<int>(json['account']),
|
||||
relativePath: serializer.fromJson<String>(json['relativePath']),
|
||||
lastPhoto: serializer.fromJson<int?>(json['lastPhoto']),
|
||||
nbItems: serializer.fromJson<int>(json['nbItems']),
|
||||
location: serializer.fromJson<String?>(json['location']),
|
||||
dateStart: serializer.fromJson<DateTime?>(json['dateStart']),
|
||||
dateEnd: serializer.fromJson<DateTime?>(json['dateEnd']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'rowId': serializer.toJson<int>(rowId),
|
||||
'account': serializer.toJson<int>(account),
|
||||
'relativePath': serializer.toJson<String>(relativePath),
|
||||
'lastPhoto': serializer.toJson<int?>(lastPhoto),
|
||||
'nbItems': serializer.toJson<int>(nbItems),
|
||||
'location': serializer.toJson<String?>(location),
|
||||
'dateStart': serializer.toJson<DateTime?>(dateStart),
|
||||
'dateEnd': serializer.toJson<DateTime?>(dateEnd),
|
||||
};
|
||||
}
|
||||
|
||||
NcAlbum copyWith(
|
||||
{int? rowId,
|
||||
int? account,
|
||||
String? relativePath,
|
||||
Value<int?> lastPhoto = const Value.absent(),
|
||||
int? nbItems,
|
||||
Value<String?> location = const Value.absent(),
|
||||
Value<DateTime?> dateStart = const Value.absent(),
|
||||
Value<DateTime?> dateEnd = const Value.absent()}) =>
|
||||
NcAlbum(
|
||||
rowId: rowId ?? this.rowId,
|
||||
account: account ?? this.account,
|
||||
relativePath: relativePath ?? this.relativePath,
|
||||
lastPhoto: lastPhoto.present ? lastPhoto.value : this.lastPhoto,
|
||||
nbItems: nbItems ?? this.nbItems,
|
||||
location: location.present ? location.value : this.location,
|
||||
dateStart: dateStart.present ? dateStart.value : this.dateStart,
|
||||
dateEnd: dateEnd.present ? dateEnd.value : this.dateEnd,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('NcAlbum(')
|
||||
..write('rowId: $rowId, ')
|
||||
..write('account: $account, ')
|
||||
..write('relativePath: $relativePath, ')
|
||||
..write('lastPhoto: $lastPhoto, ')
|
||||
..write('nbItems: $nbItems, ')
|
||||
..write('location: $location, ')
|
||||
..write('dateStart: $dateStart, ')
|
||||
..write('dateEnd: $dateEnd')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(rowId, account, relativePath, lastPhoto,
|
||||
nbItems, location, dateStart, dateEnd);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is NcAlbum &&
|
||||
other.rowId == this.rowId &&
|
||||
other.account == this.account &&
|
||||
other.relativePath == this.relativePath &&
|
||||
other.lastPhoto == this.lastPhoto &&
|
||||
other.nbItems == this.nbItems &&
|
||||
other.location == this.location &&
|
||||
other.dateStart == this.dateStart &&
|
||||
other.dateEnd == this.dateEnd);
|
||||
}
|
||||
|
||||
class NcAlbumsCompanion extends UpdateCompanion<NcAlbum> {
|
||||
final Value<int> rowId;
|
||||
final Value<int> account;
|
||||
final Value<String> relativePath;
|
||||
final Value<int?> lastPhoto;
|
||||
final Value<int> nbItems;
|
||||
final Value<String?> location;
|
||||
final Value<DateTime?> dateStart;
|
||||
final Value<DateTime?> dateEnd;
|
||||
const NcAlbumsCompanion({
|
||||
this.rowId = const Value.absent(),
|
||||
this.account = const Value.absent(),
|
||||
this.relativePath = const Value.absent(),
|
||||
this.lastPhoto = const Value.absent(),
|
||||
this.nbItems = const Value.absent(),
|
||||
this.location = const Value.absent(),
|
||||
this.dateStart = const Value.absent(),
|
||||
this.dateEnd = const Value.absent(),
|
||||
});
|
||||
NcAlbumsCompanion.insert({
|
||||
this.rowId = const Value.absent(),
|
||||
required int account,
|
||||
required String relativePath,
|
||||
this.lastPhoto = const Value.absent(),
|
||||
required int nbItems,
|
||||
this.location = const Value.absent(),
|
||||
this.dateStart = const Value.absent(),
|
||||
this.dateEnd = const Value.absent(),
|
||||
}) : account = Value(account),
|
||||
relativePath = Value(relativePath),
|
||||
nbItems = Value(nbItems);
|
||||
static Insertable<NcAlbum> custom({
|
||||
Expression<int>? rowId,
|
||||
Expression<int>? account,
|
||||
Expression<String>? relativePath,
|
||||
Expression<int?>? lastPhoto,
|
||||
Expression<int>? nbItems,
|
||||
Expression<String?>? location,
|
||||
Expression<DateTime?>? dateStart,
|
||||
Expression<DateTime?>? dateEnd,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (rowId != null) 'row_id': rowId,
|
||||
if (account != null) 'account': account,
|
||||
if (relativePath != null) 'relative_path': relativePath,
|
||||
if (lastPhoto != null) 'last_photo': lastPhoto,
|
||||
if (nbItems != null) 'nb_items': nbItems,
|
||||
if (location != null) 'location': location,
|
||||
if (dateStart != null) 'date_start': dateStart,
|
||||
if (dateEnd != null) 'date_end': dateEnd,
|
||||
});
|
||||
}
|
||||
|
||||
NcAlbumsCompanion copyWith(
|
||||
{Value<int>? rowId,
|
||||
Value<int>? account,
|
||||
Value<String>? relativePath,
|
||||
Value<int?>? lastPhoto,
|
||||
Value<int>? nbItems,
|
||||
Value<String?>? location,
|
||||
Value<DateTime?>? dateStart,
|
||||
Value<DateTime?>? dateEnd}) {
|
||||
return NcAlbumsCompanion(
|
||||
rowId: rowId ?? this.rowId,
|
||||
account: account ?? this.account,
|
||||
relativePath: relativePath ?? this.relativePath,
|
||||
lastPhoto: lastPhoto ?? this.lastPhoto,
|
||||
nbItems: nbItems ?? this.nbItems,
|
||||
location: location ?? this.location,
|
||||
dateStart: dateStart ?? this.dateStart,
|
||||
dateEnd: dateEnd ?? this.dateEnd,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (rowId.present) {
|
||||
map['row_id'] = Variable<int>(rowId.value);
|
||||
}
|
||||
if (account.present) {
|
||||
map['account'] = Variable<int>(account.value);
|
||||
}
|
||||
if (relativePath.present) {
|
||||
map['relative_path'] = Variable<String>(relativePath.value);
|
||||
}
|
||||
if (lastPhoto.present) {
|
||||
map['last_photo'] = Variable<int?>(lastPhoto.value);
|
||||
}
|
||||
if (nbItems.present) {
|
||||
map['nb_items'] = Variable<int>(nbItems.value);
|
||||
}
|
||||
if (location.present) {
|
||||
map['location'] = Variable<String?>(location.value);
|
||||
}
|
||||
if (dateStart.present) {
|
||||
final converter = $NcAlbumsTable.$converter0;
|
||||
map['date_start'] =
|
||||
Variable<DateTime?>(converter.mapToSql(dateStart.value));
|
||||
}
|
||||
if (dateEnd.present) {
|
||||
final converter = $NcAlbumsTable.$converter1;
|
||||
map['date_end'] = Variable<DateTime?>(converter.mapToSql(dateEnd.value));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('NcAlbumsCompanion(')
|
||||
..write('rowId: $rowId, ')
|
||||
..write('account: $account, ')
|
||||
..write('relativePath: $relativePath, ')
|
||||
..write('lastPhoto: $lastPhoto, ')
|
||||
..write('nbItems: $nbItems, ')
|
||||
..write('location: $location, ')
|
||||
..write('dateStart: $dateStart, ')
|
||||
..write('dateEnd: $dateEnd')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class $NcAlbumsTable extends NcAlbums with TableInfo<$NcAlbumsTable, NcAlbum> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$NcAlbumsTable(this.attachedDatabase, [this._alias]);
|
||||
final VerificationMeta _rowIdMeta = const VerificationMeta('rowId');
|
||||
@override
|
||||
late final GeneratedColumn<int?> rowId = GeneratedColumn<int?>(
|
||||
'row_id', aliasedName, false,
|
||||
type: const IntType(),
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: 'PRIMARY KEY AUTOINCREMENT');
|
||||
final VerificationMeta _accountMeta = const VerificationMeta('account');
|
||||
@override
|
||||
late final GeneratedColumn<int?> account = GeneratedColumn<int?>(
|
||||
'account', aliasedName, false,
|
||||
type: const IntType(),
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints: 'REFERENCES accounts (row_id) ON DELETE CASCADE');
|
||||
final VerificationMeta _relativePathMeta =
|
||||
const VerificationMeta('relativePath');
|
||||
@override
|
||||
late final GeneratedColumn<String?> relativePath = GeneratedColumn<String?>(
|
||||
'relative_path', aliasedName, false,
|
||||
type: const StringType(), requiredDuringInsert: true);
|
||||
final VerificationMeta _lastPhotoMeta = const VerificationMeta('lastPhoto');
|
||||
@override
|
||||
late final GeneratedColumn<int?> lastPhoto = GeneratedColumn<int?>(
|
||||
'last_photo', aliasedName, true,
|
||||
type: const IntType(), requiredDuringInsert: false);
|
||||
final VerificationMeta _nbItemsMeta = const VerificationMeta('nbItems');
|
||||
@override
|
||||
late final GeneratedColumn<int?> nbItems = GeneratedColumn<int?>(
|
||||
'nb_items', aliasedName, false,
|
||||
type: const IntType(), requiredDuringInsert: true);
|
||||
final VerificationMeta _locationMeta = const VerificationMeta('location');
|
||||
@override
|
||||
late final GeneratedColumn<String?> location = GeneratedColumn<String?>(
|
||||
'location', aliasedName, true,
|
||||
type: const StringType(), requiredDuringInsert: false);
|
||||
final VerificationMeta _dateStartMeta = const VerificationMeta('dateStart');
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<DateTime, DateTime?> dateStart =
|
||||
GeneratedColumn<DateTime?>('date_start', aliasedName, true,
|
||||
type: const IntType(), requiredDuringInsert: false)
|
||||
.withConverter<DateTime>($NcAlbumsTable.$converter0);
|
||||
final VerificationMeta _dateEndMeta = const VerificationMeta('dateEnd');
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<DateTime, DateTime?> dateEnd =
|
||||
GeneratedColumn<DateTime?>('date_end', aliasedName, true,
|
||||
type: const IntType(), requiredDuringInsert: false)
|
||||
.withConverter<DateTime>($NcAlbumsTable.$converter1);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [
|
||||
rowId,
|
||||
account,
|
||||
relativePath,
|
||||
lastPhoto,
|
||||
nbItems,
|
||||
location,
|
||||
dateStart,
|
||||
dateEnd
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? 'nc_albums';
|
||||
@override
|
||||
String get actualTableName => 'nc_albums';
|
||||
@override
|
||||
VerificationContext validateIntegrity(Insertable<NcAlbum> instance,
|
||||
{bool isInserting = false}) {
|
||||
final context = VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('row_id')) {
|
||||
context.handle(
|
||||
_rowIdMeta, rowId.isAcceptableOrUnknown(data['row_id']!, _rowIdMeta));
|
||||
}
|
||||
if (data.containsKey('account')) {
|
||||
context.handle(_accountMeta,
|
||||
account.isAcceptableOrUnknown(data['account']!, _accountMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_accountMeta);
|
||||
}
|
||||
if (data.containsKey('relative_path')) {
|
||||
context.handle(
|
||||
_relativePathMeta,
|
||||
relativePath.isAcceptableOrUnknown(
|
||||
data['relative_path']!, _relativePathMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_relativePathMeta);
|
||||
}
|
||||
if (data.containsKey('last_photo')) {
|
||||
context.handle(_lastPhotoMeta,
|
||||
lastPhoto.isAcceptableOrUnknown(data['last_photo']!, _lastPhotoMeta));
|
||||
}
|
||||
if (data.containsKey('nb_items')) {
|
||||
context.handle(_nbItemsMeta,
|
||||
nbItems.isAcceptableOrUnknown(data['nb_items']!, _nbItemsMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_nbItemsMeta);
|
||||
}
|
||||
if (data.containsKey('location')) {
|
||||
context.handle(_locationMeta,
|
||||
location.isAcceptableOrUnknown(data['location']!, _locationMeta));
|
||||
}
|
||||
context.handle(_dateStartMeta, const VerificationResult.success());
|
||||
context.handle(_dateEndMeta, const VerificationResult.success());
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {rowId};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [
|
||||
{account, relativePath},
|
||||
];
|
||||
@override
|
||||
NcAlbum map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return NcAlbum.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
}
|
||||
|
||||
@override
|
||||
$NcAlbumsTable createAlias(String alias) {
|
||||
return $NcAlbumsTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
static TypeConverter<DateTime, DateTime> $converter0 =
|
||||
const SqliteDateTimeConverter();
|
||||
static TypeConverter<DateTime, DateTime> $converter1 =
|
||||
const SqliteDateTimeConverter();
|
||||
}
|
||||
|
||||
class NcAlbumItem extends DataClass implements Insertable<NcAlbumItem> {
|
||||
final int rowId;
|
||||
final int parent;
|
||||
final int fileId;
|
||||
NcAlbumItem(
|
||||
{required this.rowId, required this.parent, required this.fileId});
|
||||
factory NcAlbumItem.fromData(Map<String, dynamic> data, {String? prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
return NcAlbumItem(
|
||||
rowId: const IntType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}row_id'])!,
|
||||
parent: const IntType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}parent'])!,
|
||||
fileId: const IntType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}file_id'])!,
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['row_id'] = Variable<int>(rowId);
|
||||
map['parent'] = Variable<int>(parent);
|
||||
map['file_id'] = Variable<int>(fileId);
|
||||
return map;
|
||||
}
|
||||
|
||||
NcAlbumItemsCompanion toCompanion(bool nullToAbsent) {
|
||||
return NcAlbumItemsCompanion(
|
||||
rowId: Value(rowId),
|
||||
parent: Value(parent),
|
||||
fileId: Value(fileId),
|
||||
);
|
||||
}
|
||||
|
||||
factory NcAlbumItem.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return NcAlbumItem(
|
||||
rowId: serializer.fromJson<int>(json['rowId']),
|
||||
parent: serializer.fromJson<int>(json['parent']),
|
||||
fileId: serializer.fromJson<int>(json['fileId']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'rowId': serializer.toJson<int>(rowId),
|
||||
'parent': serializer.toJson<int>(parent),
|
||||
'fileId': serializer.toJson<int>(fileId),
|
||||
};
|
||||
}
|
||||
|
||||
NcAlbumItem copyWith({int? rowId, int? parent, int? fileId}) => NcAlbumItem(
|
||||
rowId: rowId ?? this.rowId,
|
||||
parent: parent ?? this.parent,
|
||||
fileId: fileId ?? this.fileId,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('NcAlbumItem(')
|
||||
..write('rowId: $rowId, ')
|
||||
..write('parent: $parent, ')
|
||||
..write('fileId: $fileId')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(rowId, parent, fileId);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is NcAlbumItem &&
|
||||
other.rowId == this.rowId &&
|
||||
other.parent == this.parent &&
|
||||
other.fileId == this.fileId);
|
||||
}
|
||||
|
||||
class NcAlbumItemsCompanion extends UpdateCompanion<NcAlbumItem> {
|
||||
final Value<int> rowId;
|
||||
final Value<int> parent;
|
||||
final Value<int> fileId;
|
||||
const NcAlbumItemsCompanion({
|
||||
this.rowId = const Value.absent(),
|
||||
this.parent = const Value.absent(),
|
||||
this.fileId = const Value.absent(),
|
||||
});
|
||||
NcAlbumItemsCompanion.insert({
|
||||
this.rowId = const Value.absent(),
|
||||
required int parent,
|
||||
required int fileId,
|
||||
}) : parent = Value(parent),
|
||||
fileId = Value(fileId);
|
||||
static Insertable<NcAlbumItem> custom({
|
||||
Expression<int>? rowId,
|
||||
Expression<int>? parent,
|
||||
Expression<int>? fileId,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (rowId != null) 'row_id': rowId,
|
||||
if (parent != null) 'parent': parent,
|
||||
if (fileId != null) 'file_id': fileId,
|
||||
});
|
||||
}
|
||||
|
||||
NcAlbumItemsCompanion copyWith(
|
||||
{Value<int>? rowId, Value<int>? parent, Value<int>? fileId}) {
|
||||
return NcAlbumItemsCompanion(
|
||||
rowId: rowId ?? this.rowId,
|
||||
parent: parent ?? this.parent,
|
||||
fileId: fileId ?? this.fileId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (rowId.present) {
|
||||
map['row_id'] = Variable<int>(rowId.value);
|
||||
}
|
||||
if (parent.present) {
|
||||
map['parent'] = Variable<int>(parent.value);
|
||||
}
|
||||
if (fileId.present) {
|
||||
map['file_id'] = Variable<int>(fileId.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('NcAlbumItemsCompanion(')
|
||||
..write('rowId: $rowId, ')
|
||||
..write('parent: $parent, ')
|
||||
..write('fileId: $fileId')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class $NcAlbumItemsTable extends NcAlbumItems
|
||||
with TableInfo<$NcAlbumItemsTable, NcAlbumItem> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$NcAlbumItemsTable(this.attachedDatabase, [this._alias]);
|
||||
final VerificationMeta _rowIdMeta = const VerificationMeta('rowId');
|
||||
@override
|
||||
late final GeneratedColumn<int?> rowId = GeneratedColumn<int?>(
|
||||
'row_id', aliasedName, false,
|
||||
type: const IntType(),
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: 'PRIMARY KEY AUTOINCREMENT');
|
||||
final VerificationMeta _parentMeta = const VerificationMeta('parent');
|
||||
@override
|
||||
late final GeneratedColumn<int?> parent = GeneratedColumn<int?>(
|
||||
'parent', aliasedName, false,
|
||||
type: const IntType(),
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints: 'REFERENCES nc_albums (row_id) ON DELETE CASCADE');
|
||||
final VerificationMeta _fileIdMeta = const VerificationMeta('fileId');
|
||||
@override
|
||||
late final GeneratedColumn<int?> fileId = GeneratedColumn<int?>(
|
||||
'file_id', aliasedName, false,
|
||||
type: const IntType(), requiredDuringInsert: true);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [rowId, parent, fileId];
|
||||
@override
|
||||
String get aliasedName => _alias ?? 'nc_album_items';
|
||||
@override
|
||||
String get actualTableName => 'nc_album_items';
|
||||
@override
|
||||
VerificationContext validateIntegrity(Insertable<NcAlbumItem> instance,
|
||||
{bool isInserting = false}) {
|
||||
final context = VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('row_id')) {
|
||||
context.handle(
|
||||
_rowIdMeta, rowId.isAcceptableOrUnknown(data['row_id']!, _rowIdMeta));
|
||||
}
|
||||
if (data.containsKey('parent')) {
|
||||
context.handle(_parentMeta,
|
||||
parent.isAcceptableOrUnknown(data['parent']!, _parentMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_parentMeta);
|
||||
}
|
||||
if (data.containsKey('file_id')) {
|
||||
context.handle(_fileIdMeta,
|
||||
fileId.isAcceptableOrUnknown(data['file_id']!, _fileIdMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_fileIdMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {rowId};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [
|
||||
{parent, fileId},
|
||||
];
|
||||
@override
|
||||
NcAlbumItem map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return NcAlbumItem.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
}
|
||||
|
||||
@override
|
||||
$NcAlbumItemsTable createAlias(String alias) {
|
||||
return $NcAlbumItemsTable(attachedDatabase, alias);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$SqliteDb extends GeneratedDatabase {
|
||||
_$SqliteDb(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e);
|
||||
_$SqliteDb.connect(DatabaseConnection c) : super.connect(c);
|
||||
|
@ -4172,6 +4805,8 @@ abstract class _$SqliteDb extends GeneratedDatabase {
|
|||
late final $AlbumSharesTable albumShares = $AlbumSharesTable(this);
|
||||
late final $TagsTable tags = $TagsTable(this);
|
||||
late final $PersonsTable persons = $PersonsTable(this);
|
||||
late final $NcAlbumsTable ncAlbums = $NcAlbumsTable(this);
|
||||
late final $NcAlbumItemsTable ncAlbumItems = $NcAlbumItemsTable(this);
|
||||
@override
|
||||
Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
|
||||
@override
|
||||
|
@ -4187,7 +4822,9 @@ abstract class _$SqliteDb extends GeneratedDatabase {
|
|||
albums,
|
||||
albumShares,
|
||||
tags,
|
||||
persons
|
||||
persons,
|
||||
ncAlbums,
|
||||
ncAlbumItems
|
||||
];
|
||||
}
|
||||
|
||||
|
|
137
app/lib/entity/sqlite/database/nc_album_extension.dart
Normal file
137
app/lib/entity/sqlite/database/nc_album_extension.dart
Normal file
|
@ -0,0 +1,137 @@
|
|||
part of '../database.dart';
|
||||
|
||||
extension SqliteDbNcAlbumExtension on SqliteDb {
|
||||
Future<List<NcAlbum>> ncAlbumsByAccount({
|
||||
required ByAccount account,
|
||||
}) {
|
||||
assert((account.sqlAccount != null) != (account.appAccount != null));
|
||||
if (account.sqlAccount != null) {
|
||||
final query = select(ncAlbums)
|
||||
..where((t) => t.account.equals(account.sqlAccount!.rowId));
|
||||
return query.get();
|
||||
} else {
|
||||
final query = select(ncAlbums).join([
|
||||
innerJoin(accounts, accounts.rowId.equalsExp(ncAlbums.account),
|
||||
useColumns: false),
|
||||
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
|
||||
useColumns: false),
|
||||
])
|
||||
..where(servers.address.equals(account.appAccount!.url))
|
||||
..where(accounts.userId
|
||||
.equals(account.appAccount!.userId.toCaseInsensitiveString()));
|
||||
return query.map((r) => r.readTable(ncAlbums)).get();
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<List>> partialNcAlbumsByAccount({
|
||||
required ByAccount account,
|
||||
required List<Expression> columns,
|
||||
}) {
|
||||
final query = selectOnly(ncAlbums)..addColumns(columns);
|
||||
if (account.sqlAccount != null) {
|
||||
query.where(ncAlbums.account.equals(account.sqlAccount!.rowId));
|
||||
} else {
|
||||
query.join([
|
||||
innerJoin(accounts, accounts.rowId.equalsExp(ncAlbums.account),
|
||||
useColumns: false),
|
||||
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
|
||||
useColumns: false),
|
||||
])
|
||||
..where(servers.address.equals(account.appAccount!.url))
|
||||
..where(accounts.userId
|
||||
.equals(account.appAccount!.userId.toCaseInsensitiveString()));
|
||||
}
|
||||
return query.map((r) => columns.map((c) => r.read(c)).toList()).get();
|
||||
}
|
||||
|
||||
Future<NcAlbum?> ncAlbumByRelativePath({
|
||||
required ByAccount account,
|
||||
required String relativePath,
|
||||
}) {
|
||||
if (account.sqlAccount != null) {
|
||||
final query = select(ncAlbums)
|
||||
..where((t) => t.account.equals(account.sqlAccount!.rowId))
|
||||
..where((t) => t.relativePath.equals(relativePath));
|
||||
return query.getSingleOrNull();
|
||||
} else {
|
||||
final query = select(ncAlbums).join([
|
||||
innerJoin(accounts, accounts.rowId.equalsExp(ncAlbums.account),
|
||||
useColumns: false),
|
||||
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
|
||||
useColumns: false),
|
||||
])
|
||||
..where(servers.address.equals(account.appAccount!.url))
|
||||
..where(accounts.userId
|
||||
.equals(account.appAccount!.userId.toCaseInsensitiveString()))
|
||||
..where(ncAlbums.relativePath.equals(relativePath));
|
||||
return query.map((r) => r.readTable(ncAlbums)).getSingleOrNull();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> insertNcAlbum({
|
||||
required ByAccount account,
|
||||
required NcAlbumsCompanion object,
|
||||
}) async {
|
||||
final Account dbAccount;
|
||||
if (account.sqlAccount != null) {
|
||||
dbAccount = account.sqlAccount!;
|
||||
} else {
|
||||
dbAccount = await accountOf(account.appAccount!);
|
||||
}
|
||||
await into(ncAlbums).insert(object.copyWith(
|
||||
account: Value(dbAccount.rowId),
|
||||
));
|
||||
}
|
||||
|
||||
/// Delete [NaAlbum] by relativePath
|
||||
///
|
||||
/// Return the number of deleted rows
|
||||
Future<int> deleteNcAlbumByRelativePath({
|
||||
required ByAccount account,
|
||||
required String relativePath,
|
||||
}) async {
|
||||
final Account dbAccount;
|
||||
if (account.sqlAccount != null) {
|
||||
dbAccount = account.sqlAccount!;
|
||||
} else {
|
||||
dbAccount = await accountOf(account.appAccount!);
|
||||
}
|
||||
return await (delete(ncAlbums)
|
||||
..where((t) => t.account.equals(dbAccount.rowId))
|
||||
..where((t) => t.relativePath.equals(relativePath)))
|
||||
.go();
|
||||
}
|
||||
|
||||
Future<List<NcAlbumItem>> ncAlbumItemsByParent({
|
||||
required NcAlbum parent,
|
||||
}) {
|
||||
final query = select(ncAlbumItems)
|
||||
..where((t) => t.parent.equals(parent.rowId));
|
||||
return query.get();
|
||||
}
|
||||
|
||||
Future<List<NcAlbumItem>> ncAlbumItemsByParentRelativePath({
|
||||
required ByAccount account,
|
||||
required String parentRelativePath,
|
||||
}) {
|
||||
final query = select(ncAlbumItems).join([
|
||||
innerJoin(ncAlbums, ncAlbums.rowId.equalsExp(ncAlbumItems.parent),
|
||||
useColumns: false),
|
||||
]);
|
||||
if (account.sqlAccount != null) {
|
||||
query.where(ncAlbums.account.equals(account.sqlAccount!.rowId));
|
||||
} else {
|
||||
query.join([
|
||||
innerJoin(accounts, accounts.rowId.equalsExp(ncAlbums.account),
|
||||
useColumns: false),
|
||||
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
|
||||
useColumns: false),
|
||||
])
|
||||
..where(servers.address.equals(account.appAccount!.url))
|
||||
..where(accounts.userId
|
||||
.equals(account.appAccount!.userId.toCaseInsensitiveString()));
|
||||
}
|
||||
query.where(ncAlbums.relativePath.equals(parentRelativePath));
|
||||
return query.map((r) => r.readTable(ncAlbumItems)).get();
|
||||
}
|
||||
}
|
|
@ -42,6 +42,32 @@ extension FileListExtension on List<app.File> {
|
|||
}
|
||||
}
|
||||
|
||||
class FileDescriptor {
|
||||
const FileDescriptor({
|
||||
required this.relativePath,
|
||||
required this.fileId,
|
||||
required this.contentType,
|
||||
required this.isArchived,
|
||||
required this.isFavorite,
|
||||
required this.bestDateTime,
|
||||
});
|
||||
|
||||
final String relativePath;
|
||||
final int fileId;
|
||||
final String? contentType;
|
||||
final bool? isArchived;
|
||||
final bool? isFavorite;
|
||||
final DateTime bestDateTime;
|
||||
}
|
||||
|
||||
extension FileDescriptorListExtension on List<FileDescriptor> {
|
||||
List<app.FileDescriptor> convertToAppFileDescriptor(app.Account account) {
|
||||
return map((f) =>
|
||||
SqliteFileDescriptorConverter.fromSql(account.userId.toString(), f))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumWithShare {
|
||||
const AlbumWithShare(this.album, this.share);
|
||||
|
||||
|
@ -75,6 +101,20 @@ class AccountFileRowIdsWithFileId {
|
|||
final int fileId;
|
||||
}
|
||||
|
||||
class ByAccount {
|
||||
const ByAccount.sql(Account account) : this._(sqlAccount: account);
|
||||
|
||||
const ByAccount.app(app.Account account) : this._(appAccount: account);
|
||||
|
||||
const ByAccount._({
|
||||
this.sqlAccount,
|
||||
this.appAccount,
|
||||
}) : assert((sqlAccount != null) != (appAccount != null));
|
||||
|
||||
final Account? sqlAccount;
|
||||
final app.Account? appAccount;
|
||||
}
|
||||
|
||||
extension SqliteDbExtension on SqliteDb {
|
||||
/// Start a transaction and run [block]
|
||||
///
|
||||
|
@ -234,7 +274,7 @@ extension SqliteDbExtension on SqliteDb {
|
|||
///
|
||||
/// Only one of [sqlAccount] and [appAccount] must be passed
|
||||
Future<AccountFileRowIds?> accountFileRowIdsOfOrNull(
|
||||
app.File file, {
|
||||
app.FileDescriptor file, {
|
||||
Account? sqlAccount,
|
||||
app.Account? appAccount,
|
||||
}) {
|
||||
|
@ -250,9 +290,9 @@ extension SqliteDbExtension on SqliteDb {
|
|||
} else {
|
||||
q.setAppAccount(appAccount!);
|
||||
}
|
||||
if (file.fileId != null) {
|
||||
q.byFileId(file.fileId!);
|
||||
} else {
|
||||
try {
|
||||
q.byFileId(file.fdId);
|
||||
} catch (_) {
|
||||
q.byRelativePath(file.strippedPathWithEmpty);
|
||||
}
|
||||
return q.build()..limit(1);
|
||||
|
@ -268,7 +308,7 @@ extension SqliteDbExtension on SqliteDb {
|
|||
|
||||
/// See [accountFileRowIdsOfOrNull]
|
||||
Future<AccountFileRowIds> accountFileRowIdsOf(
|
||||
app.File file, {
|
||||
app.FileDescriptor file, {
|
||||
Account? sqlAccount,
|
||||
app.Account? appAccount,
|
||||
}) =>
|
||||
|
@ -398,6 +438,45 @@ extension SqliteDbExtension on SqliteDb {
|
|||
.get();
|
||||
}
|
||||
|
||||
/// Query [FileDescriptor]s by fileId
|
||||
///
|
||||
/// Returned files are NOT guaranteed to be sorted as [fileIds]
|
||||
Future<List<FileDescriptor>> fileDescriptorsByFileIds(
|
||||
ByAccount account, Iterable<int> fileIds) {
|
||||
return fileIds.withPartition((sublist) {
|
||||
final query = queryFiles().run((q) {
|
||||
q.setQueryMode(
|
||||
FilesQueryMode.expression,
|
||||
expressions: [
|
||||
accountFiles.relativePath,
|
||||
files.fileId,
|
||||
files.contentType,
|
||||
accountFiles.isArchived,
|
||||
accountFiles.isFavorite,
|
||||
accountFiles.bestDateTime,
|
||||
],
|
||||
);
|
||||
if (account.sqlAccount != null) {
|
||||
q.setSqlAccount(account.sqlAccount!);
|
||||
} else {
|
||||
q.setAppAccount(account.appAccount!);
|
||||
}
|
||||
q.byFileIds(sublist);
|
||||
return q.build();
|
||||
});
|
||||
return query
|
||||
.map((r) => FileDescriptor(
|
||||
relativePath: r.read(accountFiles.relativePath)!,
|
||||
fileId: r.read(files.fileId)!,
|
||||
contentType: r.read(files.contentType),
|
||||
isArchived: r.read(accountFiles.isArchived),
|
||||
isFavorite: r.read(accountFiles.isFavorite),
|
||||
bestDateTime: r.read(accountFiles.bestDateTime)!,
|
||||
))
|
||||
.get();
|
||||
}, maxByFileIdsSize);
|
||||
}
|
||||
|
||||
Future<List<Tag>> allTags({
|
||||
Account? sqlAccount,
|
||||
app.Account? appAccount,
|
||||
|
@ -553,6 +632,8 @@ extension SqliteDbExtension on SqliteDb {
|
|||
await delete(albumShares).go();
|
||||
await delete(tags).go();
|
||||
await delete(persons).go();
|
||||
await delete(ncAlbums).go();
|
||||
await delete(ncAlbumItems).go();
|
||||
|
||||
// reset the auto increment counter
|
||||
await customStatement("UPDATE sqlite_sequence SET seq=0;");
|
||||
|
|
|
@ -125,6 +125,37 @@ class DirFiles extends Table {
|
|||
get primaryKey => {dir, child};
|
||||
}
|
||||
|
||||
class NcAlbums extends Table {
|
||||
IntColumn get rowId => integer().autoIncrement()();
|
||||
IntColumn get account =>
|
||||
integer().references(Accounts, #rowId, onDelete: KeyAction.cascade)();
|
||||
TextColumn get relativePath => text()();
|
||||
IntColumn get lastPhoto => integer().nullable()();
|
||||
IntColumn get nbItems => integer()();
|
||||
TextColumn get location => text().nullable()();
|
||||
DateTimeColumn get dateStart =>
|
||||
dateTime().map(const SqliteDateTimeConverter()).nullable()();
|
||||
DateTimeColumn get dateEnd =>
|
||||
dateTime().map(const SqliteDateTimeConverter()).nullable()();
|
||||
|
||||
@override
|
||||
List<Set<Column>>? get uniqueKeys => [
|
||||
{account, relativePath},
|
||||
];
|
||||
}
|
||||
|
||||
class NcAlbumItems extends Table {
|
||||
IntColumn get rowId => integer().autoIncrement()();
|
||||
IntColumn get parent =>
|
||||
integer().references(NcAlbums, #rowId, onDelete: KeyAction.cascade)();
|
||||
IntColumn get fileId => integer()();
|
||||
|
||||
@override
|
||||
List<Set<Column>>? get uniqueKeys => [
|
||||
{parent, fileId},
|
||||
];
|
||||
}
|
||||
|
||||
class Albums extends Table {
|
||||
IntColumn get rowId => integer().autoIncrement()();
|
||||
IntColumn get file => integer()
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:nc_photos/entity/album/sort_provider.dart';
|
|||
import 'package:nc_photos/entity/exif.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/nc_album.dart';
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
|
@ -110,6 +111,19 @@ class SqliteAlbumConverter {
|
|||
}
|
||||
}
|
||||
|
||||
class SqliteFileDescriptorConverter {
|
||||
static FileDescriptor fromSql(String userId, sql.FileDescriptor f) {
|
||||
return FileDescriptor(
|
||||
fdPath: "remote.php/dav/files/$userId/${f.relativePath}",
|
||||
fdId: f.fileId,
|
||||
fdMime: f.contentType,
|
||||
fdIsArchived: f.isArchived ?? false,
|
||||
fdIsFavorite: f.isFavorite ?? false,
|
||||
fdDateTime: f.bestDateTime,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SqliteFileConverter {
|
||||
static File fromSql(String userId, sql.CompleteFile f) {
|
||||
final metadata = f.image?.run((obj) => Metadata(
|
||||
|
@ -239,6 +253,43 @@ class SqlitePersonConverter {
|
|||
);
|
||||
}
|
||||
|
||||
class SqliteNcAlbumConverter {
|
||||
static NcAlbum fromSql(String userId, sql.NcAlbum ncAlbum) => NcAlbum(
|
||||
path: "remote.php/dav/photos/$userId/albums/${ncAlbum.relativePath}",
|
||||
lastPhoto: ncAlbum.lastPhoto,
|
||||
nbItems: ncAlbum.nbItems,
|
||||
location: ncAlbum.location,
|
||||
dateStart: ncAlbum.dateStart,
|
||||
dateEnd: ncAlbum.dateEnd,
|
||||
collaborators: [],
|
||||
);
|
||||
|
||||
static sql.NcAlbumsCompanion toSql(sql.Account? dbAccount, NcAlbum ncAlbum) =>
|
||||
sql.NcAlbumsCompanion(
|
||||
account:
|
||||
dbAccount == null ? const Value.absent() : Value(dbAccount.rowId),
|
||||
relativePath: Value(ncAlbum.strippedPath),
|
||||
lastPhoto: Value(ncAlbum.lastPhoto),
|
||||
nbItems: Value(ncAlbum.nbItems),
|
||||
location: Value(ncAlbum.location),
|
||||
dateStart: Value(ncAlbum.dateStart),
|
||||
dateEnd: Value(ncAlbum.dateEnd),
|
||||
);
|
||||
}
|
||||
|
||||
class SqliteNcAlbumItemConverter {
|
||||
static int fromSql(sql.NcAlbumItem item) => item.fileId;
|
||||
|
||||
static sql.NcAlbumItemsCompanion toSql(
|
||||
sql.NcAlbum parent,
|
||||
int fileId,
|
||||
) =>
|
||||
sql.NcAlbumItemsCompanion(
|
||||
parent: Value(parent.rowId),
|
||||
fileId: Value(fileId),
|
||||
);
|
||||
}
|
||||
|
||||
sql.TagsCompanion _convertAppTag(Map map) {
|
||||
final account = map["account"] as sql.Account?;
|
||||
final tag = map["tag"] as Tag;
|
||||
|
|
|
@ -6,6 +6,7 @@ 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/entity/local_file.dart';
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
import 'package:nc_photos/pref.dart';
|
||||
|
@ -78,7 +79,7 @@ class FileRemovedEvent {
|
|||
FileRemovedEvent(this.account, this.file);
|
||||
|
||||
final Account account;
|
||||
final File file;
|
||||
final FileDescriptor file;
|
||||
}
|
||||
|
||||
class FileTrashbinRestoredEvent {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:np_api/np_api.dart';
|
||||
|
||||
class CacheNotFoundException implements Exception {
|
||||
CacheNotFoundException([this.message]);
|
||||
const CacheNotFoundException([this.message]);
|
||||
|
||||
@override
|
||||
toString() {
|
||||
|
@ -94,3 +94,12 @@ class InterruptedException implements Exception {
|
|||
|
||||
final dynamic message;
|
||||
}
|
||||
|
||||
class AlbumItemPermissionException implements Exception {
|
||||
const AlbumItemPermissionException([this.message]);
|
||||
|
||||
@override
|
||||
toString() => "AlbumItemPermissionException: $message";
|
||||
|
||||
final dynamic message;
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ class CustomizableMaterialPageRoute extends MaterialPageRoute {
|
|||
|
||||
String getImageHeroTag(FileDescriptor file) => "imageHero(${file.fdPath})";
|
||||
|
||||
String getCollectionHeroTag(String coverUrl) => "collectionHero($coverUrl)";
|
||||
|
||||
// copied from flutter
|
||||
Widget defaultHeroFlightShuttleBuilder(
|
||||
BuildContext flightContext,
|
||||
|
|
16
app/lib/lazy.dart
Normal file
16
app/lib/lazy.dart
Normal file
|
@ -0,0 +1,16 @@
|
|||
class Lazy<T> {
|
||||
Lazy(this.build);
|
||||
|
||||
T call() {
|
||||
if (build != null) {
|
||||
_value = build!();
|
||||
build = null;
|
||||
}
|
||||
return _value;
|
||||
}
|
||||
|
||||
T get get => call();
|
||||
|
||||
T Function()? build;
|
||||
late final T _value;
|
||||
}
|
|
@ -49,4 +49,8 @@ extension ListExtension<T> on List<T> {
|
|||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
Future<List<U>> asyncMap<U>(Future<U> Function(T element) fn) {
|
||||
return Stream.fromIterable(this).asyncMap(fn).toList();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/app_init.dart' as app_init;
|
||||
import 'package:nc_photos/bloc_util.dart';
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/widget/my_app.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
|
@ -33,8 +34,9 @@ void main() async {
|
|||
@npLog
|
||||
class _BlocObserver extends BlocObserver {
|
||||
@override
|
||||
onChange(BlocBase bloc, Change change) {
|
||||
void onChange(BlocBase bloc, Change change) {
|
||||
super.onChange(bloc, change);
|
||||
_log.finer("${bloc.runtimeType} $change");
|
||||
final tag = bloc is BlocTag ? (bloc as BlocTag).tag : bloc.runtimeType;
|
||||
_log.finer("$tag $change");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ class M3 extends ThemeExtension<M3> {
|
|||
const M3({
|
||||
required this.seed,
|
||||
required this.checkbox,
|
||||
required this.assistChip,
|
||||
required this.filterChip,
|
||||
required this.listTile,
|
||||
});
|
||||
|
@ -15,14 +14,12 @@ class M3 extends ThemeExtension<M3> {
|
|||
M3 copyWith({
|
||||
Color? seed,
|
||||
M3Checkbox? checkbox,
|
||||
M3AssistChip? assistChip,
|
||||
M3FilterChip? filterChip,
|
||||
M3ListTile? listTile,
|
||||
}) =>
|
||||
M3(
|
||||
seed: seed ?? this.seed,
|
||||
checkbox: checkbox ?? this.checkbox,
|
||||
assistChip: assistChip ?? this.assistChip,
|
||||
filterChip: filterChip ?? this.filterChip,
|
||||
listTile: listTile ?? this.listTile,
|
||||
);
|
||||
|
@ -35,7 +32,6 @@ class M3 extends ThemeExtension<M3> {
|
|||
return M3(
|
||||
seed: Color.lerp(seed, other.seed, t)!,
|
||||
checkbox: checkbox.lerp(other.checkbox, t),
|
||||
assistChip: assistChip.lerp(other.assistChip, t),
|
||||
filterChip: filterChip.lerp(other.filterChip, t),
|
||||
listTile: listTile.lerp(other.listTile, t),
|
||||
);
|
||||
|
@ -43,7 +39,6 @@ class M3 extends ThemeExtension<M3> {
|
|||
|
||||
final Color seed;
|
||||
final M3Checkbox checkbox;
|
||||
final M3AssistChip assistChip;
|
||||
final M3FilterChip filterChip;
|
||||
final M3ListTile listTile;
|
||||
}
|
||||
|
@ -82,44 +77,6 @@ class M3CheckboxDisabled {
|
|||
final Color container;
|
||||
}
|
||||
|
||||
class M3AssistChip {
|
||||
const M3AssistChip({
|
||||
required this.enabled,
|
||||
});
|
||||
|
||||
M3AssistChip lerp(M3AssistChip? other, double t) {
|
||||
if (other is! M3AssistChip) {
|
||||
return this;
|
||||
}
|
||||
return M3AssistChip(
|
||||
enabled: enabled.lerp(other.enabled, t),
|
||||
);
|
||||
}
|
||||
|
||||
final M3AssistChipEnabled enabled;
|
||||
}
|
||||
|
||||
class M3AssistChipEnabled {
|
||||
const M3AssistChipEnabled({
|
||||
required this.container,
|
||||
required this.containerElevated,
|
||||
});
|
||||
|
||||
M3AssistChipEnabled lerp(M3AssistChipEnabled? other, double t) {
|
||||
if (other is! M3AssistChipEnabled) {
|
||||
return this;
|
||||
}
|
||||
return M3AssistChipEnabled(
|
||||
container: Color.lerp(container, other.container, t)!,
|
||||
containerElevated:
|
||||
Color.lerp(containerElevated, other.containerElevated, t)!,
|
||||
);
|
||||
}
|
||||
|
||||
final Color container;
|
||||
final Color containerElevated;
|
||||
}
|
||||
|
||||
class M3FilterChip {
|
||||
const M3FilterChip({
|
||||
required this.disabled,
|
||||
|
|
7
app/lib/rx_extension.dart
Normal file
7
app/lib/rx_extension.dart
Normal file
|
@ -0,0 +1,7 @@
|
|||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
extension BehaviorSubjectExtension<T> on BehaviorSubject<T> {
|
||||
void addWithValue(T Function(T value) adder) {
|
||||
add(adder(value));
|
||||
}
|
||||
}
|
|
@ -202,12 +202,6 @@ ThemeData _applyColorScheme(ColorScheme colorScheme, Color seedColor) {
|
|||
container: colorScheme.onSurface.withOpacity(.38),
|
||||
),
|
||||
),
|
||||
assistChip: M3AssistChip(
|
||||
enabled: M3AssistChipEnabled(
|
||||
container: Colors.transparent,
|
||||
containerElevated: colorScheme.surface,
|
||||
),
|
||||
),
|
||||
filterChip: M3FilterChip(
|
||||
disabled: M3FilterChipDisabled(
|
||||
containerSelected: colorScheme.onSurface.withOpacity(.12),
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
|
@ -10,30 +11,31 @@ import 'package:nc_photos/entity/file_descriptor.dart';
|
|||
import 'package:nc_photos/entity/share.dart';
|
||||
import 'package:nc_photos/override_comparator.dart';
|
||||
import 'package:nc_photos/use_case/create_share.dart';
|
||||
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
|
||||
import 'package:nc_photos/use_case/list_share.dart';
|
||||
import 'package:nc_photos/use_case/preprocess_album.dart';
|
||||
import 'package:nc_photos/use_case/update_album.dart';
|
||||
import 'package:nc_photos/use_case/update_album_with_actual_items.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
|
||||
part 'add_to_album.g.dart';
|
||||
part 'add_file_to_album.g.dart';
|
||||
|
||||
@npLog
|
||||
class AddToAlbum {
|
||||
AddToAlbum(this._c)
|
||||
: assert(require(_c)),
|
||||
assert(ListShare.require(_c)),
|
||||
assert(PreProcessAlbum.require(_c));
|
||||
class AddFileToAlbum {
|
||||
AddFileToAlbum(this._c) : assert(require(_c));
|
||||
|
||||
static bool require(DiContainer c) =>
|
||||
DiContainer.has(c, DiType.albumRepo) &&
|
||||
DiContainer.has(c, DiType.shareRepo);
|
||||
DiContainer.has(c, DiType.shareRepo) &&
|
||||
ListShare.require(c) &&
|
||||
PreProcessAlbum.require(c);
|
||||
|
||||
/// Add a list of AlbumItems to [album]
|
||||
/// Add list of files to [album]
|
||||
Future<Album> call(
|
||||
Account account, Album album, List<AlbumItem> items) async {
|
||||
_log.info("[call] Add ${items.length} items to album '${album.name}'");
|
||||
Account account, Album album, List<FileDescriptor> fds) async {
|
||||
_log.info("[call] Add ${fds.length} items to album '${album.name}'");
|
||||
assert(album.provider is AlbumStaticProvider);
|
||||
final files = await InflateFileDescriptor(_c)(account, fds);
|
||||
// resync is needed to work out album cover and latest item
|
||||
final oldItems = await PreProcessAlbum(_c)(account, album);
|
||||
final itemSet = oldItems
|
||||
|
@ -41,7 +43,12 @@ class AddToAlbum {
|
|||
e, _isItemFileEqual, _getItemHashCode))
|
||||
.toSet();
|
||||
// find the items that are not having the same file as any existing ones
|
||||
final addItems = items
|
||||
final addItems = files
|
||||
.map((f) => AlbumFileItem(
|
||||
addedBy: account.userId,
|
||||
addedAt: clock.now(),
|
||||
file: f,
|
||||
))
|
||||
.where((i) => itemSet.add(OverrideComparator<AlbumItem>(
|
||||
i, _isItemFileEqual, _getItemHashCode)))
|
||||
.toList();
|
14
app/lib/use_case/album/add_file_to_album.g.dart
Normal file
14
app/lib/use_case/album/add_file_to_album.g.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'add_file_to_album.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$AddFileToAlbumNpLog on AddFileToAlbum {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("use_case.album.add_file_to_album.AddFileToAlbum");
|
||||
}
|
54
app/lib/use_case/album/edit_album.dart
Normal file
54
app/lib/use_case/album/edit_album.dart
Normal file
|
@ -0,0 +1,54 @@
|
|||
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/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_item/util.dart';
|
||||
import 'package:nc_photos/use_case/update_album.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
|
||||
part 'edit_album.g.dart';
|
||||
|
||||
@npLog
|
||||
class EditAlbum {
|
||||
const EditAlbum(this._c);
|
||||
|
||||
/// Modify an [album]
|
||||
Future<Album> call(
|
||||
Account account,
|
||||
Album album, {
|
||||
String? name,
|
||||
List<AlbumItem>? items,
|
||||
CollectionItemSort? itemSort,
|
||||
}) async {
|
||||
_log.info(
|
||||
"[call] Edit album ${album.name}, name: $name, items: $items, itemSort: $itemSort");
|
||||
var newAlbum = album;
|
||||
if (name != null) {
|
||||
newAlbum = newAlbum.copyWith(name: name);
|
||||
}
|
||||
if (items != null) {
|
||||
if (album.provider is AlbumStaticProvider) {
|
||||
newAlbum = newAlbum.copyWith(
|
||||
provider: (album.provider as AlbumStaticProvider).copyWith(
|
||||
items: items,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (itemSort != null) {
|
||||
newAlbum = newAlbum.copyWith(
|
||||
sortProvider: AlbumSortProvider.fromCollectionItemSort(itemSort),
|
||||
);
|
||||
}
|
||||
if (identical(newAlbum, album)) {
|
||||
return album;
|
||||
}
|
||||
await UpdateAlbum(_c.albumRepo)(account, newAlbum);
|
||||
return newAlbum;
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'add_to_album.dart';
|
||||
part of 'edit_album.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$AddToAlbumNpLog on AddToAlbum {
|
||||
extension _$EditAlbumNpLog on EditAlbum {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("use_case.add_to_album.AddToAlbum");
|
||||
static final log = Logger("use_case.album.edit_album.EditAlbum");
|
||||
}
|
78
app/lib/use_case/album/list_album2.dart
Normal file
78
app/lib/use_case/album/list_album2.dart
Normal file
|
@ -0,0 +1,78 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:nc_photos/use_case/compat/v15.dart';
|
||||
import 'package:nc_photos/use_case/compat/v25.dart';
|
||||
import 'package:nc_photos/use_case/ls.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
|
||||
part 'list_album2.g.dart';
|
||||
|
||||
@npLog
|
||||
class ListAlbum2 {
|
||||
ListAlbum2(this._c) : assert(require(_c));
|
||||
|
||||
static bool require(DiContainer c) =>
|
||||
DiContainer.has(c, DiType.albumRepo) &&
|
||||
DiContainer.has(c, DiType.fileRepo);
|
||||
|
||||
Stream<List<Album>> call(
|
||||
Account account, {
|
||||
ErrorHandler? onError,
|
||||
}) async* {
|
||||
var hasAlbum = false;
|
||||
try {
|
||||
await for (final result in _call(account, onError: onError)) {
|
||||
hasAlbum = true;
|
||||
yield result;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is ApiException && e.response.statusCode == 404) {
|
||||
// no albums
|
||||
return;
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
if (!hasAlbum) {
|
||||
if (await CompatV15.migrateAlbumFiles(account, _c.fileRepo)) {
|
||||
// migrated, try again
|
||||
yield* _call(account);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Stream<List<Album>> _call(
|
||||
Account account, {
|
||||
ErrorHandler? onError,
|
||||
}) async* {
|
||||
final ls = await Ls(_c.fileRepo)(
|
||||
account,
|
||||
File(
|
||||
path: remote_storage_util.getRemoteAlbumsDir(account),
|
||||
));
|
||||
final List<File?> albumFiles =
|
||||
ls.where((element) => element.isCollection != true).toList();
|
||||
// migrate files
|
||||
for (var i = 0; i < albumFiles.length; ++i) {
|
||||
final f = albumFiles[i]!;
|
||||
try {
|
||||
if (CompatV25.isAlbumFileNeedMigration(f)) {
|
||||
albumFiles[i] = await CompatV25.migrateAlbumFile(_c, account, f);
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
onError?.call(e, stackTrace);
|
||||
albumFiles[i] = null;
|
||||
}
|
||||
}
|
||||
yield* _c.albumRepo2.getAlbums(account, albumFiles.whereNotNull().toList());
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'home_albums.dart';
|
||||
part of 'list_album2.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$_HomeAlbumsStateNpLog on _HomeAlbumsState {
|
||||
extension _$ListAlbum2NpLog on ListAlbum2 {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("widget.home_albums._HomeAlbumsState");
|
||||
static final log = Logger("use_case.album.list_album2.ListAlbum2");
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue