Abstract album content provider

This commit is contained in:
Ming Ming 2021-06-25 00:26:56 +08:00
parent d4785b1f74
commit 0e7f2462b6
13 changed files with 452 additions and 149 deletions

View file

@ -138,6 +138,7 @@ class AppDbAlbumEntry {
Album.fromJson( Album.fromJson(
json["album"].cast<String, dynamic>(), json["album"].cast<String, dynamic>(),
upgraderV1: AlbumUpgraderV1(), upgraderV1: AlbumUpgraderV1(),
upgraderV2: AlbumUpgraderV2(),
), ),
); );
} }

View file

@ -1,4 +1,3 @@
import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
@ -8,6 +7,7 @@ import 'package:idb_sqflite/idb_sqflite.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_db.dart'; import 'package:nc_photos/app_db.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/upgrader.dart'; import 'package:nc_photos/entity/album/upgrader.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/entity/file/data_source.dart';
@ -126,15 +126,15 @@ class Album with EquatableMixin {
Album({ Album({
DateTime lastUpdated, DateTime lastUpdated,
@required String name, @required String name,
@required List<AlbumItem> items, @required this.provider,
this.albumFile, this.albumFile,
}) : this.lastUpdated = (lastUpdated ?? DateTime.now()).toUtc(), }) : this.lastUpdated = (lastUpdated ?? DateTime.now()).toUtc(),
this.name = name ?? "", this.name = name ?? "";
this.items = UnmodifiableListView(items);
factory Album.fromJson( factory Album.fromJson(
Map<String, dynamic> json, { Map<String, dynamic> json, {
AlbumUpgraderV1 upgraderV1, AlbumUpgraderV1 upgraderV1,
AlbumUpgraderV2 upgraderV2,
}) { }) {
final jsonVersion = json["version"]; final jsonVersion = json["version"];
if (jsonVersion < 2) { if (jsonVersion < 2) {
@ -144,14 +144,20 @@ class Album with EquatableMixin {
return null; return null;
} }
} }
if (jsonVersion < 3) {
json = upgraderV2?.call(json);
if (json == null) {
_log.info("[fromJson] Version $jsonVersion not compatible");
return null;
}
}
return Album( return Album(
lastUpdated: json["lastUpdated"] == null lastUpdated: json["lastUpdated"] == null
? null ? null
: DateTime.parse(json["lastUpdated"]), : DateTime.parse(json["lastUpdated"]),
name: json["name"], name: json["name"],
items: (json["items"] as List) provider:
.map((e) => AlbumItem.fromJson(e.cast<String, dynamic>())) AlbumProvider.fromJson(json["provider"].cast<String, dynamic>()),
.toList(),
albumFile: json["albumFile"] == null albumFile: json["albumFile"] == null
? null ? null
: File.fromJson(json["albumFile"].cast<String, dynamic>()), : File.fromJson(json["albumFile"].cast<String, dynamic>()),
@ -160,12 +166,10 @@ class Album with EquatableMixin {
@override @override
toString({bool isDeep = false}) { toString({bool isDeep = false}) {
final itemsStr =
isDeep ? items.toReadableString() : "List {length: ${items.length}}";
return "$runtimeType {" return "$runtimeType {"
"lastUpdated: $lastUpdated, " "lastUpdated: $lastUpdated, "
"name: $name, " "name: $name, "
"items: $itemsStr, " "provider: ${provider.toString(isDeep: isDeep)}, "
"albumFile: $albumFile, " "albumFile: $albumFile, "
"}"; "}";
} }
@ -178,13 +182,13 @@ class Album with EquatableMixin {
Album copyWith({ Album copyWith({
DateTime lastUpdated, DateTime lastUpdated,
String name, String name,
List<AlbumItem> items, AlbumProvider provider,
File albumFile, File albumFile,
}) { }) {
return Album( return Album(
lastUpdated: lastUpdated, lastUpdated: lastUpdated,
name: name ?? this.name, name: name ?? this.name,
items: items ?? this.items, provider: provider ?? this.provider,
albumFile: albumFile ?? this.albumFile, albumFile: albumFile ?? this.albumFile,
); );
} }
@ -194,7 +198,7 @@ class Album with EquatableMixin {
"version": version, "version": version,
"lastUpdated": lastUpdated.toIso8601String(), "lastUpdated": lastUpdated.toIso8601String(),
"name": name, "name": name,
"items": items.map((e) => e.toJson()).toList(), "provider": provider.toJson(),
// ignore albumFile // ignore albumFile
}; };
} }
@ -204,7 +208,7 @@ class Album with EquatableMixin {
"version": version, "version": version,
"lastUpdated": lastUpdated.toIso8601String(), "lastUpdated": lastUpdated.toIso8601String(),
"name": name, "name": name,
"items": items.map((e) => e.toJson()).toList(), "provider": provider.toJson(),
if (albumFile != null) "albumFile": albumFile.toJson(), if (albumFile != null) "albumFile": albumFile.toJson(),
}; };
} }
@ -213,15 +217,14 @@ class Album with EquatableMixin {
get props => [ get props => [
lastUpdated, lastUpdated,
name, name,
items, provider,
albumFile, albumFile,
]; ];
final DateTime lastUpdated; final DateTime lastUpdated;
final String name; final String name;
/// Immutable list of items. Modifying the list will result in an error final AlbumProvider provider;
final List<AlbumItem> items;
/// How is this album stored on server /// How is this album stored on server
/// ///
@ -229,7 +232,7 @@ class Album with EquatableMixin {
final File albumFile; final File albumFile;
/// versioning of this class, use to upgrade old persisted album /// versioning of this class, use to upgrade old persisted album
static const version = 2; static const version = 3;
} }
class AlbumRepo { class AlbumRepo {
@ -281,6 +284,7 @@ class AlbumRemoteDataSource implements AlbumDataSource {
return Album.fromJson( return Album.fromJson(
jsonDecode(utf8.decode(data)), jsonDecode(utf8.decode(data)),
upgraderV1: AlbumUpgraderV1(), upgraderV1: AlbumUpgraderV1(),
upgraderV2: AlbumUpgraderV2(),
).copyWith(albumFile: albumFile); ).copyWith(albumFile: albumFile);
} catch (e, stacktrace) { } catch (e, stacktrace) {
dynamic d = data; dynamic d = data;
@ -343,11 +347,19 @@ class AlbumAppDbDataSource implements AlbumDataSource {
if (results?.isNotEmpty == true) { if (results?.isNotEmpty == true) {
final entries = results final entries = results
.map((e) => AppDbAlbumEntry.fromJson(e.cast<String, dynamic>())); .map((e) => AppDbAlbumEntry.fromJson(e.cast<String, dynamic>()));
final items = entries.map((e) { if (entries.length > 1) {
_log.info("[get] ${e.path}[${e.index}]"); final items = entries.map((e) {
return e.album.items; _log.info("[get] ${e.path}[${e.index}]");
}).reduce((value, element) => value + element); return AlbumStaticProvider.of(e.album).items;
return entries.first.album.copyWith(items: items); }).reduce((value, element) => value + element);
return entries.first.album.copyWith(
provider: AlbumStaticProvider(
items: items,
),
);
} else {
return entries.first.album;
}
} else { } else {
throw CacheNotFoundException("No entry: $path"); throw CacheNotFoundException("No entry: $path");
} }
@ -461,27 +473,37 @@ Future<void> _cacheAlbum(
final range = KeyRange.bound([path, 0], [path, int_util.int32Max]); final range = KeyRange.bound([path, 0], [path, int_util.int32Max]);
// count number of entries for this album // count number of entries for this album
final count = await index.count(range); final count = await index.count(range);
int newCount = 0;
var albumItemLists = // cut large album into smaller pieces, needed to workaround Android DB
partition(album.items, AppDbAlbumEntry.maxDataSize).toList(); // limitation
if (albumItemLists.isEmpty) { final entries = <AppDbAlbumEntry>[];
albumItemLists = [<AlbumItem>[]]; if (album.provider is AlbumStaticProvider) {
var albumItemLists = partition(
AlbumStaticProvider.of(album).items, AppDbAlbumEntry.maxDataSize)
.toList();
if (albumItemLists.isEmpty) {
albumItemLists = [<AlbumItem>[]];
}
entries.addAll(albumItemLists.withIndex().map((pair) => AppDbAlbumEntry(
path,
pair.item1,
album.copyWith(
provider: AlbumStaticProvider(items: pair.item2),
))));
} else {
entries.add(AppDbAlbumEntry(path, 0, album));
} }
for (final pair in albumItemLists.withIndex()) { for (final e in entries) {
_log.info( _log.info("[_cacheAlbum] Caching ${e.path}[${e.index}]");
"[_cacheAlbum] Caching $path[${pair.item1}], length: ${pair.item2.length}"); await store.put(e.toJson(),
await store.put( AppDbAlbumEntry.toPrimaryKey(account, e.album.albumFile, e.index));
AppDbAlbumEntry(path, pair.item1, album.copyWith(items: pair.item2))
.toJson(),
AppDbAlbumEntry.toPrimaryKey(account, album.albumFile, pair.item1),
);
++newCount;
} }
if (count > newCount) {
if (count > entries.length) {
// index is 0-based // index is 0-based
final rmRange = KeyRange.bound([path, newCount], [path, int_util.int32Max]); final rmRange =
KeyRange.bound([path, entries.length], [path, int_util.int32Max]);
final rmKeys = await index final rmKeys = await index
.openKeyCursor(range: rmRange, autoAdvance: true) .openKeyCursor(range: rmRange, autoAdvance: true)
.map((cursor) => cursor.primaryKey) .map((cursor) => cursor.primaryKey)

View file

@ -0,0 +1,85 @@
import 'dart:collection';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/iterable_extension.dart';
abstract class AlbumProvider with EquatableMixin {
const AlbumProvider();
factory AlbumProvider.fromJson(Map<String, dynamic> json) {
final type = json["type"];
final content = json["content"];
switch (type) {
case AlbumStaticProvider._type:
return AlbumStaticProvider.fromJson(content.cast<String, dynamic>());
default:
_log.shout("[fromJson] Unknown type: $type");
throw ArgumentError.value(type, "type");
}
}
Map<String, dynamic> toJson() {
String getType() {
if (this is AlbumStaticProvider) {
return AlbumStaticProvider._type;
} else {
throw StateError("Unknwon subtype");
}
}
return {
"type": getType(),
"content": _toContentJson(),
};
}
@override
toString({bool isDeep = false});
Map<String, dynamic> _toContentJson();
static final _log = Logger("entity.album.provider.AlbumProvider");
}
class AlbumStaticProvider extends AlbumProvider {
AlbumStaticProvider({
@required List<AlbumItem> items,
}) : this.items = UnmodifiableListView(items);
factory AlbumStaticProvider.fromJson(Map<String, dynamic> json) {
return AlbumStaticProvider(
items: (json["items"] as List)
.map((e) => AlbumItem.fromJson(e.cast<String, dynamic>()))
.toList(),
);
}
@override
toString({bool isDeep = false}) {
final itemsStr =
isDeep ? items.toReadableString() : "List {length: ${items.length}}";
return "$runtimeType {"
"items: $itemsStr, "
"}";
}
@override
get props => [
items,
];
@override
_toContentJson() {
return {
"items": items.map((e) => e.toJson()).toList(),
};
}
/// Immutable list of items. Modifying the list will result in an error
final List<AlbumItem> items;
static const _type = "static";
}

View file

@ -23,3 +23,29 @@ class AlbumUpgraderV1 implements AlbumUpgrader {
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV1"); static final _log = Logger("entity.album.upgrader.AlbumUpgraderV1");
} }
/// Upgrade v2 Album to v3
class AlbumUpgraderV2 implements AlbumUpgrader {
AlbumUpgraderV2({
this.logFilePath,
});
Map<String, dynamic> call(Map<String, dynamic> json) {
// move v2 items to v3 provider
_log.fine("[call] Upgrade v2 Album for file: $logFilePath");
final result = Map<String, dynamic>.from(json);
result["provider"] = <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": result["items"],
}
};
result.remove("items");
return result;
}
/// File path for logging only
final String logFilePath;
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV2");
}

View file

@ -3,6 +3,7 @@ import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/use_case/list_album.dart'; import 'package:nc_photos/use_case/list_album.dart';
@ -23,18 +24,27 @@ class Remove {
Future<void> _cleanUpAlbums(Account account, File file) async { Future<void> _cleanUpAlbums(Account account, File file) async {
final albums = await ListAlbum(fileRepo, albumRepo)(account); final albums = await ListAlbum(fileRepo, albumRepo)(account);
for (final a in albums) { // clean up only make sense for static albums
for (final a
in albums.where((element) => element.provider is AlbumStaticProvider)) {
try { try {
if (a.items.any((element) => final provider = AlbumStaticProvider.of(a);
if (provider.items.any((element) =>
element is AlbumFileItem && element.file.path == file.path)) { element is AlbumFileItem && element.file.path == file.path)) {
final newItems = a.items.where((element) { final newItems = provider.items.where((element) {
if (element is AlbumFileItem) { if (element is AlbumFileItem) {
return element.file.path != file.path; return element.file.path != file.path;
} else { } else {
return true; return true;
} }
}).toList(); }).toList();
await UpdateAlbum(albumRepo)(account, a.copyWith(items: newItems)); await UpdateAlbum(albumRepo)(
account,
a.copyWith(
provider: AlbumStaticProvider(
items: newItems,
),
));
} }
} catch (e, stacktrace) { } catch (e, stacktrace) {
_log.shout( _log.shout(

View file

@ -4,18 +4,24 @@ import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_db.dart'; import 'package:nc_photos/app_db.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
/// Resync files inside an album with the file db /// Resync files inside an album with the file db
class ResyncAlbum { class ResyncAlbum {
Future<Album> call(Account account, Album album) async { Future<Album> call(Account account, Album album) async {
if (album.provider is! AlbumStaticProvider) {
_log.warning(
"[call] Resync only make sense for static albums: ${album.name}");
return album;
}
return await AppDb.use((db) async { return await AppDb.use((db) async {
final transaction = final transaction =
db.transaction(AppDb.fileDbStoreName, idbModeReadWrite); db.transaction(AppDb.fileDbStoreName, idbModeReadWrite);
final store = transaction.objectStore(AppDb.fileDbStoreName); final store = transaction.objectStore(AppDb.fileDbStoreName);
final index = store.index(AppDbFileDbEntry.indexName); final index = store.index(AppDbFileDbEntry.indexName);
final newItems = <AlbumItem>[]; final newItems = <AlbumItem>[];
for (final item in album.items) { for (final item in AlbumStaticProvider.of(album).items) {
if (item is AlbumFileItem) { if (item is AlbumFileItem) {
try { try {
newItems.add(await _syncOne(account, item, store, index)); newItems.add(await _syncOne(account, item, store, index));
@ -30,7 +36,7 @@ class ResyncAlbum {
newItems.add(item); newItems.add(item);
} }
} }
return album.copyWith(items: newItems); return album.copyWith(provider: AlbumStaticProvider(items: newItems));
}); });
} }

View file

@ -7,6 +7,7 @@ import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/bloc/list_album.dart'; import 'package:nc_photos/bloc/list_album.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/snack_bar_manager.dart';
@ -139,7 +140,8 @@ class _AlbumPickerDialogState extends State<AlbumPickerDialog> {
void _transformItems(List<Album> albums) { void _transformItems(List<Album> albums) {
_items.clear(); _items.clear();
_items.addAll(albums); _items.addAll(
albums.where((element) => element.provider is AlbumStaticProvider));
} }
void _reqQuery() { void _reqQuery() {

View file

@ -8,6 +8,7 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart'; import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/api/api_util.dart' as api_util; import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;
@ -77,6 +78,7 @@ class _AlbumViewerState extends State<AlbumViewer>
get itemStreamListCellSize => _thumbSize; get itemStreamListCellSize => _thumbSize;
void _initAlbum() { void _initAlbum() {
assert(widget.album.provider is AlbumStaticProvider);
ResyncAlbum()(widget.account, widget.album).then((album) { ResyncAlbum()(widget.account, widget.album).then((album) {
if (_shouldPropagateResyncedAlbum(album)) { if (_shouldPropagateResyncedAlbum(album)) {
UpdateAlbum(AlbumRepo(AlbumCachedDataSource()))(widget.account, album) UpdateAlbum(AlbumRepo(AlbumCachedDataSource()))(widget.account, album)
@ -247,7 +249,7 @@ class _AlbumViewerState extends State<AlbumViewer>
final selectedIndexes = final selectedIndexes =
selectedListItems.map((e) => itemStreamListItems.indexOf(e)).toList(); selectedListItems.map((e) => itemStreamListItems.indexOf(e)).toList();
final selectedFiles = _backingFiles.takeIndex(selectedIndexes).toList(); final selectedFiles = _backingFiles.takeIndex(selectedIndexes).toList();
final newItems = _album.items.where((element) { final newItems = _getAlbumItemsOf(_album).where((element) {
if (element is AlbumFileItem) { if (element is AlbumFileItem) {
return !selectedFiles.any((select) => select.path == element.file.path); return !selectedFiles.any((select) => select.path == element.file.path);
} else { } else {
@ -256,7 +258,9 @@ class _AlbumViewerState extends State<AlbumViewer>
}).toList(); }).toList();
final albumRepo = AlbumRepo(AlbumCachedDataSource()); final albumRepo = AlbumRepo(AlbumCachedDataSource());
final newAlbum = _album.copyWith( final newAlbum = _album.copyWith(
items: newItems, provider: AlbumStaticProvider(
items: newItems,
),
); );
UpdateAlbum(albumRepo)(widget.account, newAlbum).then((_) { UpdateAlbum(albumRepo)(widget.account, newAlbum).then((_) {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
@ -286,7 +290,7 @@ class _AlbumViewerState extends State<AlbumViewer>
} }
void _transformItems() { void _transformItems() {
_backingFiles = _album.items _backingFiles = _getAlbumItemsOf(_album)
.whereType<AlbumFileItem>() .whereType<AlbumFileItem>()
.map((e) => e.file) .map((e) => e.file)
.where((element) => file_util.isSupportedFormat(element)) .where((element) => file_util.isSupportedFormat(element))
@ -320,12 +324,14 @@ class _AlbumViewerState extends State<AlbumViewer>
} }
bool _shouldPropagateResyncedAlbum(Album album) { bool _shouldPropagateResyncedAlbum(Album album) {
if (widget.album.items.length != album.items.length) { final origItems = _getAlbumItemsOf(widget.album);
final resyncItems = _getAlbumItemsOf(album);
if (origItems.length != resyncItems.length) {
_log.info( _log.info(
"[_shouldPropagateResyncedAlbum] Item length differ: ${widget.album.items.length}, ${album.items.length}"); "[_shouldPropagateResyncedAlbum] Item length differ: ${origItems.length}, ${resyncItems.length}");
return true; return true;
} }
for (final z in zip([widget.album.items, album.items])) { for (final z in zip([origItems, resyncItems])) {
final a = z[0], b = z[1]; final a = z[0], b = z[1];
bool isEqual; bool isEqual;
if (a is AlbumFileItem && b is AlbumFileItem) { if (a is AlbumFileItem && b is AlbumFileItem) {
@ -344,6 +350,9 @@ class _AlbumViewerState extends State<AlbumViewer>
return false; return false;
} }
static List<AlbumItem> _getAlbumItemsOf(Album a) =>
AlbumStaticProvider.of(a).items;
int get _thumbSize { int get _thumbSize {
switch (_thumbZoomLevel) { switch (_thumbZoomLevel) {
case 1: case 1:

View file

@ -12,6 +12,7 @@ import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/api/api_util.dart' as api_util; import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/bloc/list_album.dart'; import 'package:nc_photos/bloc/list_album.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
@ -175,10 +176,15 @@ class _HomeAlbumsState extends State<HomeAlbums> {
Widget _buildAlbumItem(BuildContext context, int index) { Widget _buildAlbumItem(BuildContext context, int index) {
final item = _items[index]; final item = _items[index];
var subtitle = "";
if (item.album.provider is AlbumStaticProvider) {
subtitle = AppLocalizations.of(context)
.albumSize(AlbumStaticProvider.of(item.album).items.length);
}
return AlbumGridItem( return AlbumGridItem(
cover: _buildAlbumCover(context, item.album), cover: _buildAlbumCover(context, item.album),
title: item.album.name, title: item.album.name,
subtitle: AppLocalizations.of(context).albumSize(item.album.items.length), subtitle: subtitle,
isSelected: _selectedItems.contains(item), isSelected: _selectedItems.contains(item),
onTap: () => _onItemTap(item), onTap: () => _onItemTap(item),
onLongPress: _isSelectionMode ? null : () => _onItemLongPress(item), onLongPress: _isSelectionMode ? null : () => _onItemLongPress(item),
@ -386,7 +392,8 @@ class _HomeAlbumsState extends State<HomeAlbums> {
final sortedAlbums = albums.map((e) { final sortedAlbums = albums.map((e) {
// find the latest file in this album // find the latest file in this album
try { try {
final lastItem = e.items final lastItem = AlbumStaticProvider.of(e)
.items
.whereType<AlbumFileItem>() .whereType<AlbumFileItem>()
.map((e) => e.file) .map((e) => e.file)
.where((element) => file_util.isSupportedFormat(element)) .where((element) => file_util.isSupportedFormat(element))

View file

@ -12,6 +12,7 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util; import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/bloc/scan_dir.dart'; import 'package:nc_photos/bloc/scan_dir.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
@ -311,6 +312,7 @@ class _HomePhotosState extends State<HomePhotos>
} }
Future<void> _addSelectedToAlbum(BuildContext context, Album album) async { Future<void> _addSelectedToAlbum(BuildContext context, Album album) async {
assert(album.provider is AlbumStaticProvider);
final selected = selectedListItems final selected = selectedListItems
.whereType<_FileListItem>() .whereType<_FileListItem>()
.map((e) => AlbumFileItem(file: e.file)) .map((e) => AlbumFileItem(file: e.file))
@ -320,10 +322,12 @@ class _HomePhotosState extends State<HomePhotos>
await UpdateAlbum(albumRepo)( await UpdateAlbum(albumRepo)(
widget.account, widget.account,
album.copyWith( album.copyWith(
items: makeDistinctAlbumItems([ provider: AlbumStaticProvider(
...album.items, items: makeDistinctAlbumItems([
...selected, ...AlbumStaticProvider.of(album).items,
]), ...selected,
]),
),
)); ));
} catch (e, stacktrace) { } catch (e, stacktrace) {
_log.shout( _log.shout(

View file

@ -4,6 +4,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/use_case/create_album.dart'; import 'package:nc_photos/use_case/create_album.dart';
/// Dialog to create a new album /// Dialog to create a new album
@ -63,7 +64,9 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
_formKey.currentState.save(); _formKey.currentState.save();
final album = Album( final album = Album(
name: _formValue.name, name: _formValue.name,
items: const [], provider: AlbumStaticProvider(
items: const [],
),
); );
_log.info("[_onOkPressed] Creating album: $album"); _log.info("[_onOkPressed] Creating album: $album");
final albumRepo = AlbumRepo(AlbumCachedDataSource()); final albumRepo = AlbumRepo(AlbumCachedDataSource());

View file

@ -9,6 +9,7 @@ import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/double_extension.dart'; import 'package:nc_photos/double_extension.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;
@ -348,10 +349,12 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
} }
Future<void> _addToAlbum(BuildContext context, Album album) async { Future<void> _addToAlbum(BuildContext context, Album album) async {
assert(album.provider is AlbumStaticProvider);
try { try {
final albumRepo = AlbumRepo(AlbumCachedDataSource()); final albumRepo = AlbumRepo(AlbumCachedDataSource());
final newItem = AlbumFileItem(file: widget.file); final newItem = AlbumFileItem(file: widget.file);
if (album.items if (AlbumStaticProvider.of(album)
.items
.whereType<AlbumFileItem>() .whereType<AlbumFileItem>()
.containsIf(newItem, (a, b) => a.file.path == b.file.path)) { .containsIf(newItem, (a, b) => a.file.path == b.file.path)) {
// already added, do nothing // already added, do nothing
@ -366,7 +369,12 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
await UpdateAlbum(albumRepo)( await UpdateAlbum(albumRepo)(
widget.account, widget.account,
album.copyWith( album.copyWith(
items: [...album.items, AlbumFileItem(file: widget.file)], provider: AlbumStaticProvider(
items: [
...AlbumStaticProvider.of(album).items,
AlbumFileItem(file: widget.file),
],
),
)); ));
} catch (e, stacktrace) { } catch (e, stacktrace) {
_log.shout("[_addToAlbum] Failed while updating album", e, stacktrace); _log.shout("[_addToAlbum] Failed while updating album", e, stacktrace);

View file

@ -1,4 +1,5 @@
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/upgrader.dart'; import 'package:nc_photos/entity/album/upgrader.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
@ -10,14 +11,21 @@ void main() {
final json = <String, dynamic>{ final json = <String, dynamic>{
"version": Album.version, "version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z", "lastUpdated": "2020-01-02T03:04:05.678901Z",
"items": [], "provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [],
},
},
}; };
expect( expect(
Album.fromJson(json), Album.fromJson(json),
Album( Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "", name: "",
items: [], provider: AlbumStaticProvider(
items: [],
),
)); ));
}); });
@ -26,54 +34,68 @@ void main() {
"version": Album.version, "version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z", "lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "album", "name": "album",
"items": [], "provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [],
},
},
}; };
expect( expect(
Album.fromJson(json), Album.fromJson(json),
Album( Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "album", name: "album",
items: [], provider: AlbumStaticProvider(
items: [],
),
)); ));
}); });
test("items", () { test("AlbumStaticProvider", () {
final json = <String, dynamic>{ final json = <String, dynamic>{
"version": Album.version, "version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z", "lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "", "name": "",
"items": [ "provider": <String, dynamic>{
<String, dynamic>{ "type": "static",
"type": "file", "content": <String, dynamic>{
"content": <String, dynamic>{ "items": [
"file": <String, dynamic>{ <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg", "type": "file",
"content": <String, dynamic>{
"file": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
},
},
}, },
}, <String, dynamic>{
}, "type": "file",
<String, dynamic>{ "content": <String, dynamic>{
"type": "file", "file": <String, dynamic>{
"content": <String, dynamic>{ "path": "remote.php/dav/files/admin/test2.jpg",
"file": <String, dynamic>{ },
"path": "remote.php/dav/files/admin/test2.jpg", },
}, },
}, ],
}, },
] },
}; };
expect( expect(
Album.fromJson(json), Album.fromJson(json),
Album( Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "", name: "",
items: [ provider: AlbumStaticProvider(
AlbumFileItem( items: [
file: File(path: "remote.php/dav/files/admin/test1.jpg"), AlbumFileItem(
), file: File(path: "remote.php/dav/files/admin/test1.jpg"),
AlbumFileItem( ),
file: File(path: "remote.php/dav/files/admin/test2.jpg"), AlbumFileItem(
), file: File(path: "remote.php/dav/files/admin/test2.jpg"),
], ),
],
),
)); ));
}); });
@ -81,7 +103,12 @@ void main() {
final json = <String, dynamic>{ final json = <String, dynamic>{
"version": Album.version, "version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z", "lastUpdated": "2020-01-02T03:04:05.678901Z",
"items": [], "provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [],
},
},
"albumFile": <String, dynamic>{ "albumFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg", "path": "remote.php/dav/files/admin/test1.jpg",
}, },
@ -91,7 +118,9 @@ void main() {
Album( Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "", name: "",
items: [], provider: AlbumStaticProvider(
items: [],
),
albumFile: File(path: "remote.php/dav/files/admin/test1.jpg"), albumFile: File(path: "remote.php/dav/files/admin/test1.jpg"),
)); ));
}); });
@ -102,13 +131,20 @@ void main() {
final album = Album( final album = Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "", name: "",
items: [], provider: AlbumStaticProvider(
items: [],
),
); );
expect(album.toRemoteJson(), <String, dynamic>{ expect(album.toRemoteJson(), <String, dynamic>{
"version": Album.version, "version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z", "lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "", "name": "",
"items": [], "provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [],
},
},
}); });
}); });
@ -116,51 +152,65 @@ void main() {
final album = Album( final album = Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "album", name: "album",
items: [], provider: AlbumStaticProvider(
items: [],
),
); );
expect(album.toRemoteJson(), <String, dynamic>{ expect(album.toRemoteJson(), <String, dynamic>{
"version": Album.version, "version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z", "lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "album", "name": "album",
"items": [], "provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [],
},
},
}); });
}); });
test("items", () { test("AlbumStaticProvider", () {
final album = Album( final album = Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "", name: "",
items: [ provider: AlbumStaticProvider(
AlbumFileItem( items: [
file: File(path: "remote.php/dav/files/admin/test1.jpg"), AlbumFileItem(
), file: File(path: "remote.php/dav/files/admin/test1.jpg"),
AlbumFileItem( ),
file: File(path: "remote.php/dav/files/admin/test2.jpg"), AlbumFileItem(
), file: File(path: "remote.php/dav/files/admin/test2.jpg"),
], ),
],
),
); );
expect(album.toRemoteJson(), <String, dynamic>{ expect(album.toRemoteJson(), <String, dynamic>{
"version": Album.version, "version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z", "lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "", "name": "",
"items": [ "provider": <String, dynamic>{
<String, dynamic>{ "type": "static",
"type": "file", "content": <String, dynamic>{
"content": <String, dynamic>{ "items": [
"file": <String, dynamic>{ <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg", "type": "file",
"content": <String, dynamic>{
"file": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
},
},
}, },
}, <String, dynamic>{
}, "type": "file",
<String, dynamic>{ "content": <String, dynamic>{
"type": "file", "file": <String, dynamic>{
"content": <String, dynamic>{ "path": "remote.php/dav/files/admin/test2.jpg",
"file": <String, dynamic>{ },
"path": "remote.php/dav/files/admin/test2.jpg", },
}, },
}, ],
}, },
] },
}); });
}); });
}); });
@ -170,13 +220,20 @@ void main() {
final album = Album( final album = Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "", name: "",
items: [], provider: AlbumStaticProvider(
items: [],
),
); );
expect(album.toAppDbJson(), <String, dynamic>{ expect(album.toAppDbJson(), <String, dynamic>{
"version": Album.version, "version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z", "lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "", "name": "",
"items": [], "provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [],
},
},
}); });
}); });
@ -184,51 +241,65 @@ void main() {
final album = Album( final album = Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "album", name: "album",
items: [], provider: AlbumStaticProvider(
items: [],
),
); );
expect(album.toAppDbJson(), <String, dynamic>{ expect(album.toAppDbJson(), <String, dynamic>{
"version": Album.version, "version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z", "lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "album", "name": "album",
"items": [], "provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [],
},
},
}); });
}); });
test("items", () { test("AlbumStaticProvider", () {
final album = Album( final album = Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "", name: "",
items: [ provider: AlbumStaticProvider(
AlbumFileItem( items: [
file: File(path: "remote.php/dav/files/admin/test1.jpg"), AlbumFileItem(
), file: File(path: "remote.php/dav/files/admin/test1.jpg"),
AlbumFileItem( ),
file: File(path: "remote.php/dav/files/admin/test2.jpg"), AlbumFileItem(
), file: File(path: "remote.php/dav/files/admin/test2.jpg"),
], ),
],
),
); );
expect(album.toAppDbJson(), <String, dynamic>{ expect(album.toAppDbJson(), <String, dynamic>{
"version": Album.version, "version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z", "lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "", "name": "",
"items": [ "provider": <String, dynamic>{
<String, dynamic>{ "type": "static",
"type": "file", "content": <String, dynamic>{
"content": <String, dynamic>{ "items": [
"file": <String, dynamic>{ <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg", "type": "file",
"content": <String, dynamic>{
"file": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
},
},
}, },
}, <String, dynamic>{
}, "type": "file",
<String, dynamic>{ "content": <String, dynamic>{
"type": "file", "file": <String, dynamic>{
"content": <String, dynamic>{ "path": "remote.php/dav/files/admin/test2.jpg",
"file": <String, dynamic>{ },
"path": "remote.php/dav/files/admin/test2.jpg", },
}, },
}, ],
}, },
] },
}); });
}); });
@ -236,14 +307,21 @@ void main() {
final album = Album( final album = Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "", name: "",
items: [], provider: AlbumStaticProvider(
items: [],
),
albumFile: File(path: "remote.php/dav/files/admin/test1.jpg"), albumFile: File(path: "remote.php/dav/files/admin/test1.jpg"),
); );
expect(album.toAppDbJson(), <String, dynamic>{ expect(album.toAppDbJson(), <String, dynamic>{
"version": Album.version, "version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z", "lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "", "name": "",
"items": [], "provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [],
},
},
"albumFile": <String, dynamic>{ "albumFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg", "path": "remote.php/dav/files/admin/test1.jpg",
}, },
@ -278,5 +356,47 @@ void main() {
}, },
}); });
}); });
test("AlbumUpgraderV2", () {
final json = <String, dynamic>{
"version": 2,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"items": [
<String, dynamic>{
"type": "file",
"content": <String, dynamic>{
"file": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
},
},
},
],
"albumFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.json",
},
};
expect(AlbumUpgraderV2()(json), <String, dynamic>{
"version": 2,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [
<String, dynamic>{
"type": "file",
"content": <String, dynamic>{
"file": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
},
},
},
],
},
},
"albumFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.json",
},
});
});
}); });
} }