mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-24 10:28:50 +01:00
Regression: set/unset album cover
This commit is contained in:
parent
3d05d01b0b
commit
3ccf302553
29 changed files with 653 additions and 218 deletions
|
@ -9,6 +9,8 @@ import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/entity/collection.dart';
|
import 'package:nc_photos/entity/collection.dart';
|
||||||
import 'package:nc_photos/entity/collection_item.dart';
|
import 'package:nc_photos/entity/collection_item.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||||
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
|
import 'package:nc_photos/or_null.dart';
|
||||||
import 'package:nc_photos/rx_extension.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/create_collection.dart';
|
||||||
import 'package:nc_photos/use_case/collection/edit_collection.dart';
|
import 'package:nc_photos/use_case/collection/edit_collection.dart';
|
||||||
|
@ -157,6 +159,7 @@ class CollectionsController {
|
||||||
String? name,
|
String? name,
|
||||||
List<CollectionItem>? items,
|
List<CollectionItem>? items,
|
||||||
CollectionItemSort? itemSort,
|
CollectionItemSort? itemSort,
|
||||||
|
OrNull<FileDescriptor>? cover,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final c = await _mutex.protect(() async {
|
final c = await _mutex.protect(() async {
|
||||||
|
@ -166,6 +169,7 @@ class CollectionsController {
|
||||||
name: name,
|
name: name,
|
||||||
items: items,
|
items: items,
|
||||||
itemSort: itemSort,
|
itemSort: itemSort,
|
||||||
|
cover: cover,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
_updateCollection(c, items);
|
_updateCollection(c, items);
|
||||||
|
|
|
@ -94,6 +94,13 @@ class Album with EquatableMixin {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (jsonVersion < 9) {
|
||||||
|
result = upgraderFactory?.buildV8()?.call(result);
|
||||||
|
if (result == null) {
|
||||||
|
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (jsonVersion > version) {
|
if (jsonVersion > version) {
|
||||||
_log.warning(
|
_log.warning(
|
||||||
"[fromJson] Reading album with newer version: $jsonVersion > $version");
|
"[fromJson] Reading album with newer version: $jsonVersion > $version");
|
||||||
|
@ -217,7 +224,7 @@ class Album with EquatableMixin {
|
||||||
final int savedVersion;
|
final int savedVersion;
|
||||||
|
|
||||||
/// versioning of this class, use to upgrade old persisted album
|
/// versioning of this class, use to upgrade old persisted album
|
||||||
static const version = 8;
|
static const version = 9;
|
||||||
|
|
||||||
static final _log = _$AlbumNpLog.log;
|
static final _log = _$AlbumNpLog.log;
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,13 +122,14 @@ class AlbumAutoCoverProvider extends AlbumCoverProvider {
|
||||||
/// Cover picked by user
|
/// Cover picked by user
|
||||||
@toString
|
@toString
|
||||||
class AlbumManualCoverProvider extends AlbumCoverProvider {
|
class AlbumManualCoverProvider extends AlbumCoverProvider {
|
||||||
AlbumManualCoverProvider({
|
const AlbumManualCoverProvider({
|
||||||
required this.coverFile,
|
required this.coverFile,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory AlbumManualCoverProvider.fromJson(JsonObj json) {
|
factory AlbumManualCoverProvider.fromJson(JsonObj json) {
|
||||||
return AlbumManualCoverProvider(
|
return AlbumManualCoverProvider(
|
||||||
coverFile: File.fromJson(json["coverFile"].cast<String, dynamic>()),
|
coverFile:
|
||||||
|
FileDescriptor.fromJson(json["coverFile"].cast<String, dynamic>()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,21 +137,21 @@ class AlbumManualCoverProvider extends AlbumCoverProvider {
|
||||||
String toString() => _$toString();
|
String toString() => _$toString();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
getCover(Album album) => coverFile;
|
FileDescriptor? getCover(Album album) => coverFile;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
get props => [
|
List<Object?> get props => [
|
||||||
coverFile,
|
coverFile,
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_toContentJson() {
|
JsonObj _toContentJson() {
|
||||||
return {
|
return {
|
||||||
"coverFile": coverFile.toJson(),
|
"coverFile": coverFile.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
final File coverFile;
|
final FileDescriptor coverFile;
|
||||||
|
|
||||||
static const _type = "manual";
|
static const _type = "manual";
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ extension _$AlbumAutoCoverProviderToString on AlbumAutoCoverProvider {
|
||||||
extension _$AlbumManualCoverProviderToString on AlbumManualCoverProvider {
|
extension _$AlbumManualCoverProviderToString on AlbumManualCoverProvider {
|
||||||
String _$toString() {
|
String _$toString() {
|
||||||
// ignore: unnecessary_string_interpolations
|
// ignore: unnecessary_string_interpolations
|
||||||
return "AlbumManualCoverProvider {coverFile: ${coverFile.path}}";
|
return "AlbumManualCoverProvider {coverFile: ${coverFile.fdPath}}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
import 'package:clock/clock.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/entity/exif.dart';
|
import 'package:nc_photos/entity/exif.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
|
import 'package:nc_photos/object_extension.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
import 'package:np_common/ci_string.dart';
|
import 'package:np_common/ci_string.dart';
|
||||||
import 'package:np_common/type.dart';
|
import 'package:np_common/type.dart';
|
||||||
|
@ -238,6 +240,49 @@ class AlbumUpgraderV7 implements AlbumUpgrader {
|
||||||
final String? logFilePath;
|
final String? logFilePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Upgrade v8 Album to v9
|
||||||
|
@npLog
|
||||||
|
class AlbumUpgraderV8 implements AlbumUpgrader {
|
||||||
|
const AlbumUpgraderV8({
|
||||||
|
this.logFilePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
JsonObj? call(JsonObj json) {
|
||||||
|
_log.fine("[call] Upgrade v8 Album for file: $logFilePath");
|
||||||
|
final result = JsonObj.from(json);
|
||||||
|
try {
|
||||||
|
if (result["coverProvider"]["type"] != "manual") {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
final content = (result["coverProvider"]["content"]["coverFile"] as Map)
|
||||||
|
.cast<String, dynamic>();
|
||||||
|
final fd = {
|
||||||
|
"fdPath": content["path"],
|
||||||
|
"fdId": content["fileId"],
|
||||||
|
"fdMime": content["contentType"],
|
||||||
|
"fdIsArchived": content["isArchived"] ?? false,
|
||||||
|
"fdIsFavorite": content["isFavorite"] ?? false,
|
||||||
|
"fdDateTime": content["overrideDateTime"] ??
|
||||||
|
(content["metadata"]?["exif"]?["DateTimeOriginal"] as String?)?.run(
|
||||||
|
(d) =>
|
||||||
|
Exif.dateTimeFormat.parse(d).toUtc().toIso8601String()) ??
|
||||||
|
content["lastModified"] ??
|
||||||
|
clock.now().toUtc().toIso8601String(),
|
||||||
|
};
|
||||||
|
result["coverProvider"]["content"]["coverFile"] = fd;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
// this upgrade is not a must, if it failed then just leave it and it'll
|
||||||
|
// be upgraded the next time the album is saved
|
||||||
|
_log.shout("[call] Failed while upgrade", e, stackTrace);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// File path for logging only
|
||||||
|
final String? logFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
abstract class AlbumUpgraderFactory {
|
abstract class AlbumUpgraderFactory {
|
||||||
const AlbumUpgraderFactory();
|
const AlbumUpgraderFactory();
|
||||||
|
|
||||||
|
@ -248,6 +293,7 @@ abstract class AlbumUpgraderFactory {
|
||||||
AlbumUpgraderV5? buildV5();
|
AlbumUpgraderV5? buildV5();
|
||||||
AlbumUpgraderV6? buildV6();
|
AlbumUpgraderV6? buildV6();
|
||||||
AlbumUpgraderV7? buildV7();
|
AlbumUpgraderV7? buildV7();
|
||||||
|
AlbumUpgraderV8? buildV8();
|
||||||
}
|
}
|
||||||
|
|
||||||
class DefaultAlbumUpgraderFactory extends AlbumUpgraderFactory {
|
class DefaultAlbumUpgraderFactory extends AlbumUpgraderFactory {
|
||||||
|
@ -282,6 +328,9 @@ class DefaultAlbumUpgraderFactory extends AlbumUpgraderFactory {
|
||||||
@override
|
@override
|
||||||
buildV7() => AlbumUpgraderV7(logFilePath: logFilePath);
|
buildV7() => AlbumUpgraderV7(logFilePath: logFilePath);
|
||||||
|
|
||||||
|
@override
|
||||||
|
AlbumUpgraderV8? buildV8() => AlbumUpgraderV8(logFilePath: logFilePath);
|
||||||
|
|
||||||
final Account account;
|
final Account account;
|
||||||
final File? albumFile;
|
final File? albumFile;
|
||||||
|
|
||||||
|
|
|
@ -54,3 +54,10 @@ extension _$AlbumUpgraderV7NpLog on AlbumUpgraderV7 {
|
||||||
|
|
||||||
static final log = Logger("entity.album.upgrader.AlbumUpgraderV7");
|
static final log = Logger("entity.album.upgrader.AlbumUpgraderV7");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension _$AlbumUpgraderV8NpLog on AlbumUpgraderV8 {
|
||||||
|
// ignore: unused_element
|
||||||
|
Logger get _log => log;
|
||||||
|
|
||||||
|
static final log = Logger("entity.album.upgrader.AlbumUpgraderV8");
|
||||||
|
}
|
||||||
|
|
|
@ -60,6 +60,8 @@ enum CollectionCapability {
|
||||||
rename,
|
rename,
|
||||||
// text labels
|
// text labels
|
||||||
labelItem,
|
labelItem,
|
||||||
|
// set the cover image
|
||||||
|
manualCover,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provide the actual content of a collection
|
/// Provide the actual content of a collection
|
||||||
|
@ -80,6 +82,10 @@ abstract class CollectionContentProvider {
|
||||||
DateTime get lastModified;
|
DateTime get lastModified;
|
||||||
|
|
||||||
/// Return the capabilities of the collection
|
/// Return the capabilities of the collection
|
||||||
|
///
|
||||||
|
/// Notice that the capabilities returned here represent all the capabilities
|
||||||
|
/// that this implementation supports. In practice there may be extra runtime
|
||||||
|
/// requirements that mask some of them (e.g., user permissions)
|
||||||
List<CollectionCapability> get capabilities;
|
List<CollectionCapability> get capabilities;
|
||||||
|
|
||||||
/// Return the sort type
|
/// Return the sort type
|
||||||
|
|
|
@ -16,6 +16,7 @@ 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/new_item.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
|
import 'package:nc_photos/or_null.dart';
|
||||||
import 'package:np_common/type.dart';
|
import 'package:np_common/type.dart';
|
||||||
|
|
||||||
abstract class CollectionAdapter {
|
abstract class CollectionAdapter {
|
||||||
|
@ -51,13 +52,11 @@ abstract class CollectionAdapter {
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Edit this collection
|
/// Edit this collection
|
||||||
///
|
|
||||||
/// [name] and [items] are optional params and if not null, set the value to
|
|
||||||
/// this collection
|
|
||||||
Future<Collection> edit({
|
Future<Collection> edit({
|
||||||
String? name,
|
String? name,
|
||||||
List<CollectionItem>? items,
|
List<CollectionItem>? items,
|
||||||
CollectionItemSort? itemSort,
|
CollectionItemSort? itemSort,
|
||||||
|
OrNull<FileDescriptor>? cover,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Remove [items] from this collection and return the removed count
|
/// Remove [items] from this collection and return the removed count
|
||||||
|
@ -70,10 +69,16 @@ abstract class CollectionAdapter {
|
||||||
/// Convert a [NewCollectionItem] to an adapted one
|
/// Convert a [NewCollectionItem] to an adapted one
|
||||||
Future<CollectionItem> adaptToNewItem(NewCollectionItem original);
|
Future<CollectionItem> adaptToNewItem(NewCollectionItem original);
|
||||||
|
|
||||||
bool isItemsRemovable(List<CollectionItem> items);
|
bool isItemRemovable(CollectionItem item);
|
||||||
|
|
||||||
/// Remove this collection
|
/// Remove this collection
|
||||||
Future<void> remove();
|
Future<void> remove();
|
||||||
|
|
||||||
|
/// Return if this capability is allowed
|
||||||
|
bool isPermitted(CollectionCapability capability);
|
||||||
|
|
||||||
|
/// Return if the cover of this collection has been manually set
|
||||||
|
bool isManualCover();
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class CollectionItemAdapter {
|
abstract class CollectionItemAdapter {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
import 'package:nc_photos/entity/album/cover_provider.dart';
|
||||||
import 'package:nc_photos/entity/album/item.dart';
|
import 'package:nc_photos/entity/album/item.dart';
|
||||||
import 'package:nc_photos/entity/album/provider.dart';
|
import 'package:nc_photos/entity/album/provider.dart';
|
||||||
import 'package:nc_photos/entity/collection.dart';
|
import 'package:nc_photos/entity/collection.dart';
|
||||||
|
@ -17,6 +18,7 @@ import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
import 'package:nc_photos/iterable_extension.dart';
|
import 'package:nc_photos/iterable_extension.dart';
|
||||||
import 'package:nc_photos/object_extension.dart';
|
import 'package:nc_photos/object_extension.dart';
|
||||||
|
import 'package:nc_photos/or_null.dart';
|
||||||
import 'package:nc_photos/use_case/album/add_file_to_album.dart';
|
import 'package:nc_photos/use_case/album/add_file_to_album.dart';
|
||||||
import 'package:nc_photos/use_case/album/edit_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_album.dart';
|
||||||
|
@ -75,8 +77,9 @@ class CollectionAlbumAdapter implements CollectionAdapter {
|
||||||
String? name,
|
String? name,
|
||||||
List<CollectionItem>? items,
|
List<CollectionItem>? items,
|
||||||
CollectionItemSort? itemSort,
|
CollectionItemSort? itemSort,
|
||||||
|
OrNull<FileDescriptor>? cover,
|
||||||
}) async {
|
}) async {
|
||||||
assert(name != null || items != null || itemSort != null);
|
assert(name != null || items != null || itemSort != null || cover != null);
|
||||||
final newItems = items?.run((items) => items
|
final newItems = items?.run((items) => items
|
||||||
.map((e) {
|
.map((e) {
|
||||||
if (e is AlbumAdaptedCollectionItem) {
|
if (e is AlbumAdaptedCollectionItem) {
|
||||||
|
@ -101,6 +104,7 @@ class CollectionAlbumAdapter implements CollectionAdapter {
|
||||||
name: name,
|
name: name,
|
||||||
items: newItems,
|
items: newItems,
|
||||||
itemSort: itemSort,
|
itemSort: itemSort,
|
||||||
|
cover: cover,
|
||||||
);
|
);
|
||||||
return collection.copyWith(
|
return collection.copyWith(
|
||||||
name: name,
|
name: name,
|
||||||
|
@ -185,18 +189,39 @@ class CollectionAlbumAdapter implements CollectionAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool isItemsRemovable(List<CollectionItem> items) {
|
bool isItemRemovable(CollectionItem item) {
|
||||||
if (_provider.album.albumFile!.isOwned(account.userId)) {
|
if (_provider.album.provider is! AlbumStaticProvider) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (_provider.album.albumFile?.isOwned(account.userId) == true) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return items
|
if (item is! AlbumAdaptedCollectionItem) {
|
||||||
.whereType<AlbumAdaptedCollectionItem>()
|
_log.warning("[isItemRemovable] Unknown item type: ${item.runtimeType}");
|
||||||
.any((e) => e.albumItem.addedBy == account.userId);
|
return true;
|
||||||
|
}
|
||||||
|
return item.albumItem.addedBy == account.userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> remove() => RemoveAlbum(_c)(account, _provider.album);
|
Future<void> remove() => RemoveAlbum(_c)(account, _provider.album);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isPermitted(CollectionCapability capability) {
|
||||||
|
if (!_provider.capabilities.contains(capability)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (_provider.album.albumFile?.isOwned(account.userId) == true) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return _provider.guestCapabilities.contains(capability);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isManualCover() =>
|
||||||
|
_provider.album.coverProvider is AlbumManualCoverProvider;
|
||||||
|
|
||||||
final DiContainer _c;
|
final DiContainer _c;
|
||||||
final Account account;
|
final Account account;
|
||||||
final Collection collection;
|
final Collection collection;
|
||||||
|
|
|
@ -48,6 +48,10 @@ class CollectionLocationGroupAdapter
|
||||||
throw UnsupportedError("Operation not supported");
|
throw UnsupportedError("Operation not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isPermitted(CollectionCapability capability) =>
|
||||||
|
_provider.capabilities.contains(capability);
|
||||||
|
|
||||||
final DiContainer _c;
|
final DiContainer _c;
|
||||||
final Account account;
|
final Account account;
|
||||||
final Collection collection;
|
final Collection collection;
|
||||||
|
|
|
@ -13,6 +13,7 @@ import 'package:nc_photos/entity/collection_item/util.dart';
|
||||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
import 'package:nc_photos/entity/nc_album.dart';
|
import 'package:nc_photos/entity/nc_album.dart';
|
||||||
import 'package:nc_photos/object_extension.dart';
|
import 'package:nc_photos/object_extension.dart';
|
||||||
|
import 'package:nc_photos/or_null.dart';
|
||||||
import 'package:nc_photos/use_case/find_file_descriptor.dart';
|
import 'package:nc_photos/use_case/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/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/edit_nc_album.dart';
|
||||||
|
@ -75,9 +76,10 @@ class CollectionNcAlbumAdapter implements CollectionAdapter {
|
||||||
String? name,
|
String? name,
|
||||||
List<CollectionItem>? items,
|
List<CollectionItem>? items,
|
||||||
CollectionItemSort? itemSort,
|
CollectionItemSort? itemSort,
|
||||||
|
OrNull<FileDescriptor>? cover,
|
||||||
}) async {
|
}) async {
|
||||||
assert(name != null);
|
assert(name != null);
|
||||||
if (items != null || itemSort != null) {
|
if (items != null || itemSort != null || cover != null) {
|
||||||
_log.warning(
|
_log.warning(
|
||||||
"[edit] Nextcloud album does not support editing item or sort");
|
"[edit] Nextcloud album does not support editing item or sort");
|
||||||
}
|
}
|
||||||
|
@ -131,13 +133,18 @@ class CollectionNcAlbumAdapter implements CollectionAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool isItemsRemovable(List<CollectionItem> items) {
|
bool isItemRemovable(CollectionItem item) => true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> remove() => RemoveNcAlbum(_c)(account, _provider.album);
|
Future<void> remove() => RemoveNcAlbum(_c)(account, _provider.album);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isPermitted(CollectionCapability capability) =>
|
||||||
|
_provider.capabilities.contains(capability);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isManualCover() => false;
|
||||||
|
|
||||||
Future<NcAlbum> _syncRemote() async {
|
Future<NcAlbum> _syncRemote() async {
|
||||||
final remote = await ListNcAlbum(_c)(account).last;
|
final remote = await ListNcAlbum(_c)(account).last;
|
||||||
return remote.firstWhere((e) => e.compareIdentity(_provider.album));
|
return remote.firstWhere((e) => e.compareIdentity(_provider.album));
|
||||||
|
|
|
@ -50,6 +50,10 @@ class CollectionPersonAdapter
|
||||||
throw UnsupportedError("Operation not supported");
|
throw UnsupportedError("Operation not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isPermitted(CollectionCapability capability) =>
|
||||||
|
_provider.capabilities.contains(capability);
|
||||||
|
|
||||||
final DiContainer _c;
|
final DiContainer _c;
|
||||||
final Account account;
|
final Account account;
|
||||||
final Collection collection;
|
final Collection collection;
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:nc_photos/entity/collection/adapter.dart';
|
||||||
import 'package:nc_photos/entity/collection_item.dart';
|
import 'package:nc_photos/entity/collection_item.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
|
import 'package:nc_photos/or_null.dart';
|
||||||
import 'package:np_common/type.dart';
|
import 'package:np_common/type.dart';
|
||||||
|
|
||||||
/// A read-only collection that does not support modifying its items
|
/// A read-only collection that does not support modifying its items
|
||||||
|
@ -22,6 +23,7 @@ mixin CollectionReadOnlyAdapter implements CollectionAdapter {
|
||||||
String? name,
|
String? name,
|
||||||
List<CollectionItem>? items,
|
List<CollectionItem>? items,
|
||||||
CollectionItemSort? itemSort,
|
CollectionItemSort? itemSort,
|
||||||
|
OrNull<FileDescriptor>? cover,
|
||||||
}) {
|
}) {
|
||||||
throw UnsupportedError("Operation not supported");
|
throw UnsupportedError("Operation not supported");
|
||||||
}
|
}
|
||||||
|
@ -36,7 +38,8 @@ mixin CollectionReadOnlyAdapter implements CollectionAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool isItemsRemovable(List<CollectionItem> items) {
|
bool isItemRemovable(CollectionItem item) => false;
|
||||||
return false;
|
|
||||||
}
|
@override
|
||||||
|
bool isManualCover() => false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,10 @@ class CollectionTagAdapter
|
||||||
throw UnsupportedError("Operation not supported");
|
throw UnsupportedError("Operation not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isPermitted(CollectionCapability capability) =>
|
||||||
|
_provider.capabilities.contains(capability);
|
||||||
|
|
||||||
final DiContainer _c;
|
final DiContainer _c;
|
||||||
final Account account;
|
final Account account;
|
||||||
final Collection collection;
|
final Collection collection;
|
||||||
|
|
|
@ -44,6 +44,7 @@ class CollectionAlbumProvider implements CollectionContentProvider {
|
||||||
List<CollectionCapability> get capabilities => [
|
List<CollectionCapability> get capabilities => [
|
||||||
CollectionCapability.sort,
|
CollectionCapability.sort,
|
||||||
CollectionCapability.rename,
|
CollectionCapability.rename,
|
||||||
|
CollectionCapability.manualCover,
|
||||||
if (album.provider is AlbumStaticProvider) ...[
|
if (album.provider is AlbumStaticProvider) ...[
|
||||||
CollectionCapability.manualItem,
|
CollectionCapability.manualItem,
|
||||||
CollectionCapability.manualSort,
|
CollectionCapability.manualSort,
|
||||||
|
@ -51,6 +52,14 @@ class CollectionAlbumProvider implements CollectionContentProvider {
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Capabilities when this album is shared to this user by someone else
|
||||||
|
List<CollectionCapability> get guestCapabilities => [
|
||||||
|
if (album.provider is AlbumStaticProvider) ...[
|
||||||
|
CollectionCapability.manualItem,
|
||||||
|
CollectionCapability.labelItem,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
CollectionItemSort get itemSort => album.sortProvider.toCollectionItemSort();
|
CollectionItemSort get itemSort => album.sortProvider.toCollectionItemSort();
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
|
import 'package:to_string/to_string.dart';
|
||||||
|
|
||||||
|
part 'or_null.g.dart';
|
||||||
|
|
||||||
/// To hold optional arguments that themselves could be null
|
/// To hold optional arguments that themselves could be null
|
||||||
|
@toString
|
||||||
class OrNull<T> {
|
class OrNull<T> {
|
||||||
OrNull(this.obj);
|
OrNull(this.obj);
|
||||||
|
|
||||||
|
@ -6,5 +11,8 @@ class OrNull<T> {
|
||||||
/// null, false will still be returned
|
/// null, false will still be returned
|
||||||
static bool isSetNull(OrNull? x) => x != null && x.obj == null;
|
static bool isSetNull(OrNull? x) => x != null && x.obj == null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
final T? obj;
|
final T? obj;
|
||||||
}
|
}
|
||||||
|
|
14
app/lib/or_null.g.dart
Normal file
14
app/lib/or_null.g.dart
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'or_null.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// ToStringGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
extension _$OrNullToString on OrNull {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "OrNull {obj: $obj}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,10 +2,13 @@ import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/entity/album.dart';
|
import 'package:nc_photos/entity/album.dart';
|
||||||
|
import 'package:nc_photos/entity/album/cover_provider.dart';
|
||||||
import 'package:nc_photos/entity/album/item.dart';
|
import 'package:nc_photos/entity/album/item.dart';
|
||||||
import 'package:nc_photos/entity/album/provider.dart';
|
import 'package:nc_photos/entity/album/provider.dart';
|
||||||
import 'package:nc_photos/entity/album/sort_provider.dart';
|
import 'package:nc_photos/entity/album/sort_provider.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||||
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
|
import 'package:nc_photos/or_null.dart';
|
||||||
import 'package:nc_photos/use_case/update_album.dart';
|
import 'package:nc_photos/use_case/update_album.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
|
||||||
|
@ -22,9 +25,10 @@ class EditAlbum {
|
||||||
String? name,
|
String? name,
|
||||||
List<AlbumItem>? items,
|
List<AlbumItem>? items,
|
||||||
CollectionItemSort? itemSort,
|
CollectionItemSort? itemSort,
|
||||||
|
OrNull<FileDescriptor>? cover,
|
||||||
}) async {
|
}) async {
|
||||||
_log.info(
|
_log.info(
|
||||||
"[call] Edit album ${album.name}, name: $name, items: $items, itemSort: $itemSort");
|
"[call] Edit album ${album.name}, name: $name, items: $items, itemSort: $itemSort, cover: $cover");
|
||||||
var newAlbum = album;
|
var newAlbum = album;
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
newAlbum = newAlbum.copyWith(name: name);
|
newAlbum = newAlbum.copyWith(name: name);
|
||||||
|
@ -43,6 +47,17 @@ class EditAlbum {
|
||||||
sortProvider: AlbumSortProvider.fromCollectionItemSort(itemSort),
|
sortProvider: AlbumSortProvider.fromCollectionItemSort(itemSort),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (cover != null) {
|
||||||
|
if (cover.obj == null) {
|
||||||
|
newAlbum = newAlbum.copyWith(
|
||||||
|
coverProvider: AlbumAutoCoverProvider(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
newAlbum = newAlbum.copyWith(
|
||||||
|
coverProvider: AlbumManualCoverProvider(coverFile: cover.obj!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (identical(newAlbum, album)) {
|
if (identical(newAlbum, album)) {
|
||||||
return album;
|
return album;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import 'package:nc_photos/entity/collection.dart';
|
||||||
import 'package:nc_photos/entity/collection/adapter.dart';
|
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||||
import 'package:nc_photos/entity/collection_item.dart';
|
import 'package:nc_photos/entity/collection_item.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||||
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
|
import 'package:nc_photos/or_null.dart';
|
||||||
|
|
||||||
class EditCollection {
|
class EditCollection {
|
||||||
const EditCollection(this._c);
|
const EditCollection(this._c);
|
||||||
|
@ -15,6 +17,7 @@ class EditCollection {
|
||||||
/// - Rename (set [name])
|
/// - Rename (set [name])
|
||||||
/// - Add text label(s) (set [items])
|
/// - Add text label(s) (set [items])
|
||||||
/// - Sort [items] (set [items] and/or [itemSort])
|
/// - Sort [items] (set [items] and/or [itemSort])
|
||||||
|
/// - Set album [cover]
|
||||||
///
|
///
|
||||||
/// \* To add files to a collection, use [AddFileToCollection] instead
|
/// \* To add files to a collection, use [AddFileToCollection] instead
|
||||||
Future<Collection> call(
|
Future<Collection> call(
|
||||||
|
@ -23,11 +26,13 @@ class EditCollection {
|
||||||
String? name,
|
String? name,
|
||||||
List<CollectionItem>? items,
|
List<CollectionItem>? items,
|
||||||
CollectionItemSort? itemSort,
|
CollectionItemSort? itemSort,
|
||||||
|
OrNull<FileDescriptor>? cover,
|
||||||
}) =>
|
}) =>
|
||||||
CollectionAdapter.of(_c, account, collection).edit(
|
CollectionAdapter.of(_c, account, collection).edit(
|
||||||
name: name,
|
name: name,
|
||||||
items: items,
|
items: items,
|
||||||
itemSort: itemSort,
|
itemSort: itemSort,
|
||||||
|
cover: cover,
|
||||||
);
|
);
|
||||||
|
|
||||||
final DiContainer _c;
|
final DiContainer _c;
|
||||||
|
|
|
@ -353,8 +353,7 @@ class _AlbumBrowserState extends State<AlbumBrowser>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Navigator.pushNamed(context, Viewer.routeName,
|
Navigator.pushNamed(context, Viewer.routeName,
|
||||||
arguments: ViewerArguments(widget.account, _backingFiles, fileIndex,
|
arguments: ViewerArguments(widget.account, _backingFiles, fileIndex));
|
||||||
album: _album));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onSharePressed(BuildContext context) async {
|
Future<void> _onSharePressed(BuildContext context) async {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import 'package:bloc_concurrency/bloc_concurrency.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
|
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
|
||||||
import 'package:clock/clock.dart';
|
import 'package:clock/clock.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:copy_with/copy_with.dart';
|
import 'package:copy_with/copy_with.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
@ -37,6 +36,7 @@ import 'package:nc_photos/flutter_util.dart' as flutter_util;
|
||||||
import 'package:nc_photos/k.dart' as k;
|
import 'package:nc_photos/k.dart' as k;
|
||||||
import 'package:nc_photos/np_api_util.dart';
|
import 'package:nc_photos/np_api_util.dart';
|
||||||
import 'package:nc_photos/object_extension.dart';
|
import 'package:nc_photos/object_extension.dart';
|
||||||
|
import 'package:nc_photos/or_null.dart';
|
||||||
import 'package:nc_photos/snack_bar_manager.dart';
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
import 'package:nc_photos/use_case/archive_file.dart';
|
import 'package:nc_photos/use_case/archive_file.dart';
|
||||||
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
|
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
|
||||||
|
@ -227,8 +227,10 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser>
|
||||||
if (!state.isEditMode) {
|
if (!state.isEditMode) {
|
||||||
return const _ContentList();
|
return const _ContentList();
|
||||||
} else {
|
} else {
|
||||||
if (state.collection.capabilities
|
if (context
|
||||||
.contains(CollectionCapability.manualSort)) {
|
.read<_Bloc>()
|
||||||
|
.isCollectionCapabilityPermitted(
|
||||||
|
CollectionCapability.manualSort)) {
|
||||||
return const _EditContentList();
|
return const _EditContentList();
|
||||||
} else {
|
} else {
|
||||||
return const _UnmodifiableEditContentList();
|
return const _UnmodifiableEditContentList();
|
||||||
|
@ -314,16 +316,18 @@ class _ContentList extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final bloc = context.read<_Bloc>();
|
||||||
return StreamBuilder<int>(
|
return StreamBuilder<int>(
|
||||||
stream: context.read<PrefController>().albumBrowserZoomLevel,
|
stream: context.read<PrefController>().albumBrowserZoomLevel,
|
||||||
initialData: context.read<PrefController>().albumBrowserZoomLevel.value,
|
initialData: context.read<PrefController>().albumBrowserZoomLevel.value,
|
||||||
builder: (_, zoomLevel) {
|
builder: (_, zoomLevel) {
|
||||||
if (zoomLevel.hasError) {
|
if (zoomLevel.hasError) {
|
||||||
context.read<_Bloc>().add(
|
bloc.add(
|
||||||
_SetMessage(L10n.global().writePreferenceFailureNotification));
|
_SetMessage(L10n.global().writePreferenceFailureNotification));
|
||||||
}
|
}
|
||||||
return _BlocBuilder(
|
return _BlocBuilder(
|
||||||
buildWhen: (previous, current) =>
|
buildWhen: (previous, current) =>
|
||||||
|
previous.collection != current.collection ||
|
||||||
previous.transformedItems != current.transformedItems ||
|
previous.transformedItems != current.transformedItems ||
|
||||||
previous.selectedItems != current.selectedItems,
|
previous.selectedItems != current.selectedItems,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
@ -336,9 +340,7 @@ class _ContentList extends StatelessWidget {
|
||||||
staggeredTileBuilder: (_, item) => item.staggeredTile,
|
staggeredTileBuilder: (_, item) => item.staggeredTile,
|
||||||
selectedItems: state.selectedItems,
|
selectedItems: state.selectedItems,
|
||||||
onSelectionChange: (_, selected) {
|
onSelectionChange: (_, selected) {
|
||||||
context
|
bloc.add(_SetSelectedItems(items: selected.cast()));
|
||||||
.read<_Bloc>()
|
|
||||||
.add(_SetSelectedItems(items: selected.cast()));
|
|
||||||
},
|
},
|
||||||
onItemTap: (context, index, _) {
|
onItemTap: (context, index, _) {
|
||||||
final actualIndex = index -
|
final actualIndex = index -
|
||||||
|
@ -349,12 +351,19 @@ class _ContentList extends StatelessWidget {
|
||||||
Navigator.of(context).pushNamed(
|
Navigator.of(context).pushNamed(
|
||||||
Viewer.routeName,
|
Viewer.routeName,
|
||||||
arguments: ViewerArguments(
|
arguments: ViewerArguments(
|
||||||
context.read<_Bloc>().account,
|
bloc.account,
|
||||||
state.transformedItems
|
state.transformedItems
|
||||||
.whereType<_FileItem>()
|
.whereType<_FileItem>()
|
||||||
.map((e) => e.file)
|
.map((e) => e.file)
|
||||||
.toList(),
|
.toList(),
|
||||||
actualIndex,
|
actualIndex,
|
||||||
|
fromCollection: ViewerCollectionData(
|
||||||
|
state.collection,
|
||||||
|
state.transformedItems
|
||||||
|
.whereType<_ActualItem>()
|
||||||
|
.map((e) => e.original)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -383,8 +392,8 @@ class _EditContentList extends StatelessWidget {
|
||||||
buildWhen: (previous, current) =>
|
buildWhen: (previous, current) =>
|
||||||
previous.editTransformedItems != current.editTransformedItems,
|
previous.editTransformedItems != current.editTransformedItems,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.collection.capabilities
|
if (context.read<_Bloc>().isCollectionCapabilityPermitted(
|
||||||
.contains(CollectionCapability.manualSort)) {
|
CollectionCapability.manualSort)) {
|
||||||
return DraggableItemList<_Item>(
|
return DraggableItemList<_Item>(
|
||||||
maxCrossAxisExtent: photo_list_util
|
maxCrossAxisExtent: photo_list_util
|
||||||
.getThumbSize(zoomLevel.requireData)
|
.getThumbSize(zoomLevel.requireData)
|
||||||
|
|
|
@ -216,6 +216,13 @@ extension _$_CancelEditToString on _CancelEdit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension _$_UnsetCoverToString on _UnsetCover {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_UnsetCover {}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension _$_SetSelectedItemsToString on _SetSelectedItems {
|
extension _$_SetSelectedItemsToString on _SetSelectedItems {
|
||||||
String _$toString() {
|
String _$toString() {
|
||||||
// ignore: unnecessary_string_interpolations
|
// ignore: unnecessary_string_interpolations
|
||||||
|
|
|
@ -5,25 +5,19 @@ class _AppBar extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// capability can't be changed once the collection is created
|
final c = KiwiContainer().resolve<DiContainer>();
|
||||||
final capabilities = context.read<_Bloc>().state.collection.capabilities;
|
return _BlocBuilder(
|
||||||
return SliverAppBar(
|
|
||||||
floating: true,
|
|
||||||
expandedHeight: 160,
|
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
|
||||||
background: const _AppBarCover(),
|
|
||||||
title: _BlocBuilder(
|
|
||||||
buildWhen: (previous, current) =>
|
buildWhen: (previous, current) =>
|
||||||
previous.collection.name != current.collection.name,
|
previous.items != current.items ||
|
||||||
builder: (context, state) => Text(
|
previous.collection != current.collection,
|
||||||
state.collection.name,
|
builder: (context, state) {
|
||||||
style: TextStyle(
|
final bloc = context.read<_Bloc>();
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor,
|
final adapter = CollectionAdapter.of(c, bloc.account, state.collection);
|
||||||
),
|
final canRename = adapter.isPermitted(CollectionCapability.rename);
|
||||||
),
|
final canManualCover =
|
||||||
),
|
adapter.isPermitted(CollectionCapability.manualCover);
|
||||||
),
|
|
||||||
actions: [
|
final actions = <Widget>[
|
||||||
ZoomMenuButton(
|
ZoomMenuButton(
|
||||||
initialZoom: 0,
|
initialZoom: 0,
|
||||||
minZoom: 0,
|
minZoom: 0,
|
||||||
|
@ -32,31 +26,48 @@ class _AppBar extends StatelessWidget {
|
||||||
context.read<PrefController>().setAlbumBrowserZoomLevel(value);
|
context.read<PrefController>().setAlbumBrowserZoomLevel(value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (capabilities.contains(CollectionCapability.rename))
|
];
|
||||||
_BlocBuilder(
|
if (state.items.isNotEmpty || canRename) {
|
||||||
buildWhen: (previous, current) => previous.items != current.items,
|
actions.add(PopupMenuButton<_MenuOption>(
|
||||||
builder: (context, state) => PopupMenuButton<_MenuOption>(
|
|
||||||
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
|
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
|
||||||
itemBuilder: (context) {
|
itemBuilder: (_) => [
|
||||||
return [
|
if (canRename)
|
||||||
if (capabilities.contains(CollectionCapability.rename))
|
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: _MenuOption.edit,
|
value: _MenuOption.edit,
|
||||||
child: Text(L10n.global().editTooltip),
|
child: Text(L10n.global().editTooltip),
|
||||||
),
|
),
|
||||||
|
if (canManualCover && adapter.isManualCover())
|
||||||
|
PopupMenuItem(
|
||||||
|
value: _MenuOption.unsetCover,
|
||||||
|
child: Text(L10n.global().unsetAlbumCoverTooltip),
|
||||||
|
),
|
||||||
if (state.items.isNotEmpty)
|
if (state.items.isNotEmpty)
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: _MenuOption.download,
|
value: _MenuOption.download,
|
||||||
child: Text(L10n.global().downloadTooltip),
|
child: Text(L10n.global().downloadTooltip),
|
||||||
),
|
),
|
||||||
];
|
],
|
||||||
},
|
|
||||||
onSelected: (option) {
|
onSelected: (option) {
|
||||||
_onMenuSelected(context, option);
|
_onMenuSelected(context, option);
|
||||||
},
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return SliverAppBar(
|
||||||
|
floating: true,
|
||||||
|
expandedHeight: 160,
|
||||||
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
|
background: const _AppBarCover(),
|
||||||
|
title: Text(
|
||||||
|
state.collection.name,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
actions: actions,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,6 +76,9 @@ class _AppBar extends StatelessWidget {
|
||||||
case _MenuOption.edit:
|
case _MenuOption.edit:
|
||||||
context.read<_Bloc>().add(const _BeginEdit());
|
context.read<_Bloc>().add(const _BeginEdit());
|
||||||
break;
|
break;
|
||||||
|
case _MenuOption.unsetCover:
|
||||||
|
context.read<_Bloc>().add(const _UnsetCover());
|
||||||
|
break;
|
||||||
case _MenuOption.download:
|
case _MenuOption.download:
|
||||||
context.read<_Bloc>().add(const _Download());
|
context.read<_Bloc>().add(const _Download());
|
||||||
break;
|
break;
|
||||||
|
@ -145,8 +159,8 @@ class _SelectionAppBar extends StatelessWidget {
|
||||||
PopupMenuButton<_SelectionMenuOption>(
|
PopupMenuButton<_SelectionMenuOption>(
|
||||||
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
|
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
if (state.collection.capabilities
|
if (context.read<_Bloc>().isCollectionCapabilityPermitted(
|
||||||
.contains(CollectionCapability.manualItem) &&
|
CollectionCapability.manualItem) &&
|
||||||
state.isSelectionRemovable)
|
state.isSelectionRemovable)
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: _SelectionMenuOption.removeFromAlbum,
|
value: _SelectionMenuOption.removeFromAlbum,
|
||||||
|
@ -237,7 +251,11 @@ class _EditAppBar extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final capabilities = context.read<_Bloc>().state.collection.capabilities;
|
final capabilitiesAdapter = CollectionAdapter.of(
|
||||||
|
KiwiContainer().resolve<DiContainer>(),
|
||||||
|
context.read<_Bloc>().account,
|
||||||
|
context.read<_Bloc>().state.collection,
|
||||||
|
);
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
floating: true,
|
floating: true,
|
||||||
expandedHeight: 160,
|
expandedHeight: 160,
|
||||||
|
@ -274,13 +292,13 @@ class _EditAppBar extends StatelessWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (capabilities.contains(CollectionCapability.labelItem))
|
if (capabilitiesAdapter.isPermitted(CollectionCapability.labelItem))
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.text_fields),
|
icon: const Icon(Icons.text_fields),
|
||||||
tooltip: L10n.global().albumAddTextTooltip,
|
tooltip: L10n.global().albumAddTextTooltip,
|
||||||
onPressed: () => _onAddTextPressed(context),
|
onPressed: () => _onAddTextPressed(context),
|
||||||
),
|
),
|
||||||
if (capabilities.contains(CollectionCapability.sort))
|
if (capabilitiesAdapter.isPermitted(CollectionCapability.sort))
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.sort_by_alpha),
|
icon: const Icon(Icons.sort_by_alpha),
|
||||||
tooltip: L10n.global().sortTooltip,
|
tooltip: L10n.global().sortTooltip,
|
||||||
|
@ -354,6 +372,7 @@ class _EditAppBar extends StatelessWidget {
|
||||||
|
|
||||||
enum _MenuOption {
|
enum _MenuOption {
|
||||||
edit,
|
edit,
|
||||||
|
unsetCover,
|
||||||
download,
|
download,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,8 @@ class _Bloc extends Bloc<_Event, _State> implements BlocTag {
|
||||||
on<_DoneEdit>(_onDoneEdit, transformer: concurrent());
|
on<_DoneEdit>(_onDoneEdit, transformer: concurrent());
|
||||||
on<_CancelEdit>(_onCancelEdit);
|
on<_CancelEdit>(_onCancelEdit);
|
||||||
|
|
||||||
|
on<_UnsetCover>(_onUnsetCover);
|
||||||
|
|
||||||
on<_SetSelectedItems>(_onSetSelectedItems);
|
on<_SetSelectedItems>(_onSetSelectedItems);
|
||||||
on<_DownloadSelectedItems>(_onDownloadSelectedItems);
|
on<_DownloadSelectedItems>(_onDownloadSelectedItems);
|
||||||
on<_AddSelectedItemsToCollection>(_onAddSelectedItemsToCollection);
|
on<_AddSelectedItemsToCollection>(_onAddSelectedItemsToCollection);
|
||||||
|
@ -66,12 +68,18 @@ class _Bloc extends Bloc<_Event, _State> implements BlocTag {
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isCollectionCapabilityPermitted(CollectionCapability capability) {
|
||||||
|
return CollectionAdapter.of(_c, account, state.collection)
|
||||||
|
.isPermitted(capability);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get tag => _log.fullName;
|
String get tag => _log.fullName;
|
||||||
|
|
||||||
void _onUpdateCollection(_UpdateCollection ev, Emitter<_State> emit) {
|
void _onUpdateCollection(_UpdateCollection ev, Emitter<_State> emit) {
|
||||||
_log.info("$ev");
|
_log.info("$ev");
|
||||||
emit(state.copyWith(collection: ev.collection));
|
emit(state.copyWith(collection: ev.collection));
|
||||||
|
_updateCover(emit);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onLoad(_LoadItems ev, Emitter<_State> emit) {
|
Future<void> _onLoad(_LoadItems ev, Emitter<_State> emit) {
|
||||||
|
@ -94,15 +102,8 @@ class _Bloc extends Bloc<_Event, _State> implements BlocTag {
|
||||||
void _onTransformItems(_TransformItems ev, Emitter<_State> emit) {
|
void _onTransformItems(_TransformItems ev, Emitter<_State> emit) {
|
||||||
_log.info("$ev");
|
_log.info("$ev");
|
||||||
final result = _transformItems(ev.items, state.collection.itemSort);
|
final result = _transformItems(ev.items, state.collection.itemSort);
|
||||||
var newState = state.copyWith(transformedItems: result.transformed);
|
emit(state.copyWith(transformedItems: result.transformed));
|
||||||
if (state.coverUrl == null) {
|
_updateCover(emit);
|
||||||
// if cover is not managed by the collection, use the first item
|
|
||||||
final cover = _getCoverUrlOnNewItem(result.sorted);
|
|
||||||
if (cover != null) {
|
|
||||||
newState = newState.copyWith(coverUrl: cover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emit(newState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDownload(_Download ev, Emitter<_State> emit) {
|
void _onDownload(_Download ev, Emitter<_State> emit) {
|
||||||
|
@ -128,8 +129,7 @@ class _Bloc extends Bloc<_Event, _State> implements BlocTag {
|
||||||
|
|
||||||
void _onAddLabelToCollection(_AddLabelToCollection ev, Emitter<_State> emit) {
|
void _onAddLabelToCollection(_AddLabelToCollection ev, Emitter<_State> emit) {
|
||||||
_log.info("$ev");
|
_log.info("$ev");
|
||||||
assert(
|
assert(isCollectionCapabilityPermitted(CollectionCapability.labelItem));
|
||||||
state.collection.capabilities.contains(CollectionCapability.labelItem));
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
editItems: [
|
editItems: [
|
||||||
NewCollectionLabelItem(ev.label, clock.now().toUtc()),
|
NewCollectionLabelItem(ev.label, clock.now().toUtc()),
|
||||||
|
@ -149,8 +149,7 @@ class _Bloc extends Bloc<_Event, _State> implements BlocTag {
|
||||||
|
|
||||||
void _onEditManualSort(_EditManualSort ev, Emitter<_State> emit) {
|
void _onEditManualSort(_EditManualSort ev, Emitter<_State> emit) {
|
||||||
_log.info("$ev");
|
_log.info("$ev");
|
||||||
assert(state.collection.capabilities
|
assert(isCollectionCapabilityPermitted(CollectionCapability.manualSort));
|
||||||
.contains(CollectionCapability.manualSort));
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
editSort: CollectionItemSort.manual,
|
editSort: CollectionItemSort.manual,
|
||||||
editItems:
|
editItems:
|
||||||
|
@ -204,15 +203,20 @@ class _Bloc extends Bloc<_Event, _State> implements BlocTag {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onUnsetCover(_UnsetCover ev, Emitter<_State> emit) {
|
||||||
|
_log.info("$ev");
|
||||||
|
collectionsController.edit(state.collection, cover: OrNull(null));
|
||||||
|
}
|
||||||
|
|
||||||
void _onSetSelectedItems(_SetSelectedItems ev, Emitter<_State> emit) {
|
void _onSetSelectedItems(_SetSelectedItems ev, Emitter<_State> emit) {
|
||||||
_log.info("$ev");
|
_log.info("$ev");
|
||||||
|
final adapter = CollectionAdapter.of(_c, account, state.collection);
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
selectedItems: ev.items,
|
selectedItems: ev.items,
|
||||||
isSelectionRemovable: CollectionAdapter.of(_c, account, state.collection)
|
isSelectionRemovable: ev.items
|
||||||
.isItemsRemovable(ev.items
|
|
||||||
.whereType<_ActualItem>()
|
.whereType<_ActualItem>()
|
||||||
.map((e) => e.original)
|
.map((e) => e.original)
|
||||||
.toList()),
|
.any(adapter.isItemRemovable),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -403,13 +407,10 @@ class _Bloc extends Bloc<_Event, _State> implements BlocTag {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _getCoverUrlOnNewItem(List<CollectionItem> sortedItems) {
|
String? _getCoverUrlByItems() {
|
||||||
try {
|
try {
|
||||||
final firstFile =
|
final firstFile =
|
||||||
(sortedItems.firstWhereOrNull((i) => i is CollectionFileItem)
|
state.transformedItems.whereType<_FileItem>().first.file;
|
||||||
as CollectionFileItem?)
|
|
||||||
?.file;
|
|
||||||
if (firstFile != null) {
|
|
||||||
return api_util.getFilePreviewUrlByFileId(
|
return api_util.getFilePreviewUrlByFileId(
|
||||||
account,
|
account,
|
||||||
firstFile.fdId,
|
firstFile.fdId,
|
||||||
|
@ -417,10 +418,10 @@ class _Bloc extends Bloc<_Event, _State> implements BlocTag {
|
||||||
height: k.coverSize,
|
height: k.coverSize,
|
||||||
isKeepAspectRatio: false,
|
isKeepAspectRatio: false,
|
||||||
);
|
);
|
||||||
}
|
} catch (_) {
|
||||||
} catch (_) {}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static String? _getCoverUrl(Collection collection) {
|
static String? _getCoverUrl(Collection collection) {
|
||||||
try {
|
try {
|
||||||
|
@ -430,6 +431,14 @@ class _Bloc extends Bloc<_Event, _State> implements BlocTag {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _updateCover(Emitter<_State> emit) {
|
||||||
|
var coverUrl = _getCoverUrl(state.collection);
|
||||||
|
coverUrl ??= _getCoverUrlByItems();
|
||||||
|
if (coverUrl != state.coverUrl) {
|
||||||
|
emit(state.copyWith(coverUrl: coverUrl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final DiContainer _c;
|
final DiContainer _c;
|
||||||
final Account account;
|
final Account account;
|
||||||
final CollectionsController collectionsController;
|
final CollectionsController collectionsController;
|
||||||
|
|
|
@ -188,6 +188,14 @@ class _CancelEdit implements _Event {
|
||||||
String toString() => _$toString();
|
String toString() => _$toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _UnsetCover implements _Event {
|
||||||
|
const _UnsetCover();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the currently selected items
|
/// Set the currently selected items
|
||||||
@toString
|
@toString
|
||||||
class _SetSelectedItems implements _Event {
|
class _SetSelectedItems implements _Event {
|
||||||
|
|
|
@ -206,8 +206,7 @@ class _SmartAlbumBrowserState extends State<SmartAlbumBrowser>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Navigator.pushNamed(context, Viewer.routeName,
|
Navigator.pushNamed(context, Viewer.routeName,
|
||||||
arguments: ViewerArguments(widget.account, _backingFiles, fileIndex,
|
arguments: ViewerArguments(widget.account, _backingFiles, fileIndex));
|
||||||
album: widget.album));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDownloadPressed() {
|
void _onDownloadPressed() {
|
||||||
|
|
|
@ -5,25 +5,29 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:kiwi/kiwi.dart';
|
import 'package:kiwi/kiwi.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/app_localizations.dart';
|
import 'package:nc_photos/app_localizations.dart';
|
||||||
|
import 'package:nc_photos/controller/account_controller.dart';
|
||||||
|
import 'package:nc_photos/controller/collection_items_controller.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/download_handler.dart';
|
import 'package:nc_photos/download_handler.dart';
|
||||||
import 'package:nc_photos/entity/album.dart';
|
import 'package:nc_photos/entity/collection.dart';
|
||||||
import 'package:nc_photos/entity/album/item.dart';
|
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||||
import 'package:nc_photos/entity/album/provider.dart';
|
import 'package:nc_photos/entity/collection_item.dart';
|
||||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||||
import 'package:nc_photos/flutter_util.dart';
|
import 'package:nc_photos/flutter_util.dart';
|
||||||
import 'package:nc_photos/k.dart' as k;
|
import 'package:nc_photos/k.dart' as k;
|
||||||
import 'package:nc_photos/notified_action.dart';
|
import 'package:nc_photos/notified_action.dart';
|
||||||
|
import 'package:nc_photos/object_extension.dart';
|
||||||
import 'package:nc_photos/platform/features.dart' as features;
|
import 'package:nc_photos/platform/features.dart' as features;
|
||||||
import 'package:nc_photos/pref.dart';
|
import 'package:nc_photos/pref.dart';
|
||||||
import 'package:nc_photos/share_handler.dart';
|
import 'package:nc_photos/share_handler.dart';
|
||||||
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
import 'package:nc_photos/theme.dart';
|
import 'package:nc_photos/theme.dart';
|
||||||
import 'package:nc_photos/use_case/album/remove_from_album.dart';
|
|
||||||
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
|
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
|
||||||
import 'package:nc_photos/use_case/update_property.dart';
|
import 'package:nc_photos/use_case/update_property.dart';
|
||||||
import 'package:nc_photos/widget/disposable.dart';
|
import 'package:nc_photos/widget/disposable.dart';
|
||||||
|
@ -44,18 +48,25 @@ import 'package:np_codegen/np_codegen.dart';
|
||||||
|
|
||||||
part 'viewer.g.dart';
|
part 'viewer.g.dart';
|
||||||
|
|
||||||
|
class ViewerCollectionData {
|
||||||
|
const ViewerCollectionData(this.collection, this.items);
|
||||||
|
|
||||||
|
final Collection collection;
|
||||||
|
final List<CollectionItem> items;
|
||||||
|
}
|
||||||
|
|
||||||
class ViewerArguments {
|
class ViewerArguments {
|
||||||
ViewerArguments(
|
const ViewerArguments(
|
||||||
this.account,
|
this.account,
|
||||||
this.streamFiles,
|
this.streamFiles,
|
||||||
this.startIndex, {
|
this.startIndex, {
|
||||||
this.album,
|
this.fromCollection,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Account account;
|
final Account account;
|
||||||
final List<FileDescriptor> streamFiles;
|
final List<FileDescriptor> streamFiles;
|
||||||
final int startIndex;
|
final int startIndex;
|
||||||
final Album? album;
|
final ViewerCollectionData? fromCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Viewer extends StatefulWidget {
|
class Viewer extends StatefulWidget {
|
||||||
|
@ -73,7 +84,7 @@ class Viewer extends StatefulWidget {
|
||||||
required this.account,
|
required this.account,
|
||||||
required this.streamFiles,
|
required this.streamFiles,
|
||||||
required this.startIndex,
|
required this.startIndex,
|
||||||
this.album,
|
this.fromCollection,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
Viewer.fromArgs(ViewerArguments args, {Key? key})
|
Viewer.fromArgs(ViewerArguments args, {Key? key})
|
||||||
|
@ -82,7 +93,7 @@ class Viewer extends StatefulWidget {
|
||||||
account: args.account,
|
account: args.account,
|
||||||
streamFiles: args.streamFiles,
|
streamFiles: args.streamFiles,
|
||||||
startIndex: args.startIndex,
|
startIndex: args.startIndex,
|
||||||
album: args.album,
|
fromCollection: args.fromCollection,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -92,8 +103,8 @@ class Viewer extends StatefulWidget {
|
||||||
final List<FileDescriptor> streamFiles;
|
final List<FileDescriptor> streamFiles;
|
||||||
final int startIndex;
|
final int startIndex;
|
||||||
|
|
||||||
/// The album these files belongs to, or null
|
/// Data of the collection these files belongs to, or null
|
||||||
final Album? album;
|
final ViewerCollectionData? fromCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@npLog
|
@npLog
|
||||||
|
@ -304,9 +315,11 @@ class _ViewerState extends State<Viewer>
|
||||||
child: ViewerDetailPane(
|
child: ViewerDetailPane(
|
||||||
account: widget.account,
|
account: widget.account,
|
||||||
fd: _streamFilesView[index],
|
fd: _streamFilesView[index],
|
||||||
album: widget.album,
|
fromCollection: widget.fromCollection?.run(
|
||||||
onRemoveFromAlbumPressed:
|
(d) => ViewerSingleCollectionData(
|
||||||
_onRemoveFromAlbumPressed,
|
d.collection, d.items[index])),
|
||||||
|
onRemoveFromCollectionPressed:
|
||||||
|
_onRemoveFromCollectionPressed,
|
||||||
onArchivePressed: _onArchivePressed,
|
onArchivePressed: _onArchivePressed,
|
||||||
onUnarchivePressed: _onUnarchivePressed,
|
onUnarchivePressed: _onUnarchivePressed,
|
||||||
onSlideshowPressed: _onSlideshowPressed,
|
onSlideshowPressed: _onSlideshowPressed,
|
||||||
|
@ -666,30 +679,27 @@ class _ViewerState extends State<Viewer>
|
||||||
_removeCurrentItemFromStream(context, index);
|
_removeCurrentItemFromStream(context, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onRemoveFromAlbumPressed(BuildContext context) {
|
Future<void> _onRemoveFromCollectionPressed(BuildContext context) async {
|
||||||
assert(widget.album!.provider is AlbumStaticProvider);
|
assert(CollectionAdapter.of(KiwiContainer().resolve<DiContainer>(),
|
||||||
|
widget.account, widget.fromCollection!.collection)
|
||||||
|
.isPermitted(CollectionCapability.manualItem));
|
||||||
final index = _viewerController.currentPage;
|
final index = _viewerController.currentPage;
|
||||||
final c = KiwiContainer().resolve<DiContainer>();
|
|
||||||
final file = _streamFilesView[index];
|
final file = _streamFilesView[index];
|
||||||
_log.info("[_onRemoveFromAlbumPressed] Remove file: ${file.fdPath}");
|
_log.info("[_onRemoveFromCollectionPressed] Remove file: ${file.fdPath}");
|
||||||
NotifiedAction(
|
try {
|
||||||
() async {
|
final itemsController = _findCollectionItemsController(context);
|
||||||
final selectedFile =
|
final item = itemsController.stream.value.items
|
||||||
(await InflateFileDescriptor(c)(widget.account, [file])).first;
|
.whereType<CollectionFileItem>()
|
||||||
final thisItem = AlbumStaticProvider.of(widget.album!)
|
.firstWhere((i) => i.file.compareServerIdentity(file));
|
||||||
.items
|
await itemsController.removeItems([item]);
|
||||||
.whereType<AlbumFileItem>()
|
} catch (e, stackTrace) {
|
||||||
.firstWhere((e) => e.file.compareServerIdentity(selectedFile));
|
_log.shout("[_onRemoveFromCollectionPressed] Failed while updating album",
|
||||||
await RemoveFromAlbum(KiwiContainer().resolve<DiContainer>())(
|
e, stackTrace);
|
||||||
widget.account, widget.album!, [thisItem]);
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
},
|
content: Text(L10n.global().removeSelectedFromAlbumFailureNotification),
|
||||||
null,
|
duration: k.snackBarDurationNormal,
|
||||||
L10n.global().removeSelectedFromAlbumSuccessNotification(1),
|
));
|
||||||
failureText: L10n.global().removeSelectedFromAlbumFailureNotification,
|
}
|
||||||
).call().catchError((e, stackTrace) {
|
|
||||||
_log.shout("[_onRemoveFromAlbumPressed] Failed while updating album", e,
|
|
||||||
stackTrace);
|
|
||||||
});
|
|
||||||
_removeCurrentItemFromStream(context, index);
|
_removeCurrentItemFromStream(context, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -829,6 +839,19 @@ class _ViewerState extends State<Viewer>
|
||||||
_isClosingDetailPane = false;
|
_isClosingDetailPane = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CollectionItemsController _findCollectionItemsController(
|
||||||
|
BuildContext context) {
|
||||||
|
return context
|
||||||
|
.read<AccountController>()
|
||||||
|
.collectionsController
|
||||||
|
.stream
|
||||||
|
.value
|
||||||
|
.data
|
||||||
|
.firstWhere((d) =>
|
||||||
|
d.collection.compareIdentity(widget.fromCollection!.collection))
|
||||||
|
.controller;
|
||||||
|
}
|
||||||
|
|
||||||
bool _canSwitchPage() => !_isZoomed;
|
bool _canSwitchPage() => !_isZoomed;
|
||||||
bool _canOpenDetailPane() => !_isZoomed;
|
bool _canOpenDetailPane() => !_isZoomed;
|
||||||
bool _canZoom() => !_isDetailPaneActive;
|
bool _canZoom() => !_isDetailPaneActive;
|
||||||
|
|
|
@ -2,32 +2,32 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:android_intent_plus/android_intent.dart';
|
import 'package:android_intent_plus/android_intent.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:kiwi/kiwi.dart';
|
import 'package:kiwi/kiwi.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/app_localizations.dart';
|
import 'package:nc_photos/app_localizations.dart';
|
||||||
|
import 'package:nc_photos/controller/account_controller.dart';
|
||||||
import 'package:nc_photos/debug_util.dart';
|
import 'package:nc_photos/debug_util.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/double_extension.dart';
|
import 'package:nc_photos/double_extension.dart';
|
||||||
import 'package:nc_photos/entity/album.dart';
|
import 'package:nc_photos/entity/collection.dart';
|
||||||
import 'package:nc_photos/entity/album/cover_provider.dart';
|
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||||
import 'package:nc_photos/entity/album/item.dart';
|
import 'package:nc_photos/entity/collection_item.dart';
|
||||||
import 'package:nc_photos/entity/album/provider.dart';
|
|
||||||
import 'package:nc_photos/entity/exif_extension.dart';
|
import 'package:nc_photos/entity/exif_extension.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
import 'package:nc_photos/k.dart' as k;
|
import 'package:nc_photos/k.dart' as k;
|
||||||
import 'package:nc_photos/location_util.dart' as location_util;
|
import 'package:nc_photos/location_util.dart' as location_util;
|
||||||
import 'package:nc_photos/notified_action.dart';
|
|
||||||
import 'package:nc_photos/object_extension.dart';
|
import 'package:nc_photos/object_extension.dart';
|
||||||
|
import 'package:nc_photos/or_null.dart';
|
||||||
import 'package:nc_photos/platform/features.dart' as features;
|
import 'package:nc_photos/platform/features.dart' as features;
|
||||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||||
import 'package:nc_photos/snack_bar_manager.dart';
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
import 'package:nc_photos/theme.dart';
|
import 'package:nc_photos/theme.dart';
|
||||||
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
|
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
|
||||||
import 'package:nc_photos/use_case/list_file_tag.dart';
|
import 'package:nc_photos/use_case/list_file_tag.dart';
|
||||||
import 'package:nc_photos/use_case/update_album.dart';
|
|
||||||
import 'package:nc_photos/use_case/update_property.dart';
|
import 'package:nc_photos/use_case/update_property.dart';
|
||||||
import 'package:nc_photos/widget/about_geocoding_dialog.dart';
|
import 'package:nc_photos/widget/about_geocoding_dialog.dart';
|
||||||
import 'package:nc_photos/widget/animated_visibility.dart';
|
import 'package:nc_photos/widget/animated_visibility.dart';
|
||||||
|
@ -41,13 +41,20 @@ import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
part 'viewer_detail_pane.g.dart';
|
part 'viewer_detail_pane.g.dart';
|
||||||
|
|
||||||
|
class ViewerSingleCollectionData {
|
||||||
|
const ViewerSingleCollectionData(this.collection, this.item);
|
||||||
|
|
||||||
|
final Collection collection;
|
||||||
|
final CollectionItem item;
|
||||||
|
}
|
||||||
|
|
||||||
class ViewerDetailPane extends StatefulWidget {
|
class ViewerDetailPane extends StatefulWidget {
|
||||||
const ViewerDetailPane({
|
const ViewerDetailPane({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.account,
|
required this.account,
|
||||||
required this.fd,
|
required this.fd,
|
||||||
this.album,
|
this.fromCollection,
|
||||||
required this.onRemoveFromAlbumPressed,
|
required this.onRemoveFromCollectionPressed,
|
||||||
required this.onArchivePressed,
|
required this.onArchivePressed,
|
||||||
required this.onUnarchivePressed,
|
required this.onUnarchivePressed,
|
||||||
this.onSlideshowPressed,
|
this.onSlideshowPressed,
|
||||||
|
@ -59,10 +66,10 @@ class ViewerDetailPane extends StatefulWidget {
|
||||||
final Account account;
|
final Account account;
|
||||||
final FileDescriptor fd;
|
final FileDescriptor fd;
|
||||||
|
|
||||||
/// The album this file belongs to, or null
|
/// Data of the collection this file belongs to, or null
|
||||||
final Album? album;
|
final ViewerSingleCollectionData? fromCollection;
|
||||||
|
|
||||||
final void Function(BuildContext context) onRemoveFromAlbumPressed;
|
final void Function(BuildContext context) onRemoveFromCollectionPressed;
|
||||||
final void Function(BuildContext context) onArchivePressed;
|
final void Function(BuildContext context) onArchivePressed;
|
||||||
final void Function(BuildContext context) onUnarchivePressed;
|
final void Function(BuildContext context) onUnarchivePressed;
|
||||||
final VoidCallback? onSlideshowPressed;
|
final VoidCallback? onSlideshowPressed;
|
||||||
|
@ -148,11 +155,10 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
_DetailPaneButton(
|
_DetailPaneButton(
|
||||||
icon: Icons.remove_outlined,
|
icon: Icons.remove_outlined,
|
||||||
label: L10n.global().removeFromAlbumTooltip,
|
label: L10n.global().removeFromAlbumTooltip,
|
||||||
onPressed: () => widget.onRemoveFromAlbumPressed(context),
|
onPressed: () =>
|
||||||
|
widget.onRemoveFromCollectionPressed(context),
|
||||||
),
|
),
|
||||||
if (widget.album != null &&
|
if (_canSetCover)
|
||||||
widget.album!.albumFile?.isOwned(widget.account.userId) ==
|
|
||||||
true)
|
|
||||||
_DetailPaneButton(
|
_DetailPaneButton(
|
||||||
icon: Icons.photo_album_outlined,
|
icon: Icons.photo_album_outlined,
|
||||||
label: L10n.global().useAsAlbumCoverTooltip,
|
label: L10n.global().useAsAlbumCoverTooltip,
|
||||||
|
@ -384,27 +390,21 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
|
|
||||||
Future<void> _onSetAlbumCoverPressed(BuildContext context) async {
|
Future<void> _onSetAlbumCoverPressed(BuildContext context) async {
|
||||||
assert(_file != null);
|
assert(_file != null);
|
||||||
assert(widget.album != null);
|
assert(widget.fromCollection != null);
|
||||||
_log.info(
|
_log.info(
|
||||||
"[_onSetAlbumCoverPressed] Set '${widget.fd.fdPath}' as album cover for '${widget.album!.name}'");
|
"[_onSetAlbumCoverPressed] Set '${widget.fd.fdPath}' as album cover for '${widget.fromCollection!.collection.name}'");
|
||||||
try {
|
try {
|
||||||
await NotifiedAction(
|
await context.read<AccountController>().collectionsController.edit(
|
||||||
() async {
|
widget.fromCollection!.collection,
|
||||||
await UpdateAlbum(_c.albumRepo)(
|
cover: OrNull(_file!),
|
||||||
widget.account,
|
);
|
||||||
widget.album!.copyWith(
|
|
||||||
coverProvider: AlbumManualCoverProvider(
|
|
||||||
coverFile: _file!,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
},
|
|
||||||
L10n.global().setAlbumCoverProcessingNotification,
|
|
||||||
L10n.global().setAlbumCoverSuccessNotification,
|
|
||||||
failureText: L10n.global().setAlbumCoverFailureNotification,
|
|
||||||
)();
|
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
_log.shout("[_onSetAlbumCoverPressed] Failed while updating album", e,
|
_log.shout("[_onSetAlbumCoverPressed] Failed while updating album", e,
|
||||||
stackTrace);
|
stackTrace);
|
||||||
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
content: Text(L10n.global().setAlbumCoverFailureNotification),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -459,27 +459,6 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _checkCanRemoveFromAlbum() {
|
|
||||||
if (widget.album == null ||
|
|
||||||
widget.album!.provider is! AlbumStaticProvider) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (widget.album!.albumFile?.isOwned(widget.account.userId) == true) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
final thisItem = AlbumStaticProvider.of(widget.album!)
|
|
||||||
.items
|
|
||||||
.whereType<AlbumFileItem>()
|
|
||||||
.firstWhere(
|
|
||||||
(element) => element.file.compareServerIdentity(widget.fd));
|
|
||||||
if (thisItem.addedBy == widget.account.userId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
late final DiContainer _c;
|
late final DiContainer _c;
|
||||||
|
|
||||||
File? _file;
|
File? _file;
|
||||||
|
@ -495,9 +474,17 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
|
|
||||||
final _tags = <String>[];
|
final _tags = <String>[];
|
||||||
|
|
||||||
late final bool _canRemoveFromAlbum = _checkCanRemoveFromAlbum();
|
|
||||||
|
|
||||||
var _shouldBlockGpsMap = true;
|
var _shouldBlockGpsMap = true;
|
||||||
|
|
||||||
|
late final bool _canRemoveFromAlbum = widget.fromCollection?.run((d) =>
|
||||||
|
CollectionAdapter.of(_c, widget.account, d.collection)
|
||||||
|
.isItemRemovable(widget.fromCollection!.item)) ??
|
||||||
|
false;
|
||||||
|
|
||||||
|
late final bool _canSetCover = widget.fromCollection?.run((d) =>
|
||||||
|
CollectionAdapter.of(_c, widget.account, d.collection)
|
||||||
|
.isPermitted(CollectionCapability.manualCover)) ??
|
||||||
|
false;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DetailPaneButton extends StatelessWidget {
|
class _DetailPaneButton extends StatelessWidget {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:clock/clock.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:nc_photos/entity/album.dart';
|
import 'package:nc_photos/entity/album.dart';
|
||||||
import 'package:nc_photos/entity/album/cover_provider.dart';
|
import 'package:nc_photos/entity/album/cover_provider.dart';
|
||||||
|
@ -1758,6 +1759,12 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group("AlbumUpgraderV8", () {
|
||||||
|
test("non manual cover", _upgradeV8NonManualCover);
|
||||||
|
test("manual cover", _upgradeV8ManualCover);
|
||||||
|
test("manual cover (exif time)", _upgradeV8ManualExifTime);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1883,6 +1890,195 @@ void _toAppDbJsonShares() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _upgradeV8NonManualCover() {
|
||||||
|
final json = <String, dynamic>{
|
||||||
|
"version": 8,
|
||||||
|
"lastUpdated": "2020-01-02T03:04:05.678901Z",
|
||||||
|
"provider": <String, dynamic>{
|
||||||
|
"type": "static",
|
||||||
|
"content": <String, dynamic>{
|
||||||
|
"items": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"coverProvider": <String, dynamic>{
|
||||||
|
"type": "memory",
|
||||||
|
"content": <String, dynamic>{
|
||||||
|
"coverFile": <String, dynamic>{
|
||||||
|
"fdPath": "remote.php/dav/files/admin/test1.jpg",
|
||||||
|
"fdId": 1,
|
||||||
|
"fdMime": null,
|
||||||
|
"fdIsArchived": false,
|
||||||
|
"fdIsFavorite": false,
|
||||||
|
"fdDateTime": "2020-01-02T03:04:05.678901Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sortProvider": <String, dynamic>{
|
||||||
|
"type": "null",
|
||||||
|
"content": <String, dynamic>{},
|
||||||
|
},
|
||||||
|
"albumFile": <String, dynamic>{
|
||||||
|
"path": "remote.php/dav/files/admin/test1.json",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(const AlbumUpgraderV8()(json), <String, dynamic>{
|
||||||
|
"version": 8,
|
||||||
|
"lastUpdated": "2020-01-02T03:04:05.678901Z",
|
||||||
|
"provider": <String, dynamic>{
|
||||||
|
"type": "static",
|
||||||
|
"content": <String, dynamic>{
|
||||||
|
"items": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"coverProvider": <String, dynamic>{
|
||||||
|
"type": "memory",
|
||||||
|
"content": <String, dynamic>{
|
||||||
|
"coverFile": <String, dynamic>{
|
||||||
|
"fdPath": "remote.php/dav/files/admin/test1.jpg",
|
||||||
|
"fdId": 1,
|
||||||
|
"fdMime": null,
|
||||||
|
"fdIsArchived": false,
|
||||||
|
"fdIsFavorite": false,
|
||||||
|
"fdDateTime": "2020-01-02T03:04:05.678901Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sortProvider": <String, dynamic>{
|
||||||
|
"type": "null",
|
||||||
|
"content": <String, dynamic>{},
|
||||||
|
},
|
||||||
|
"albumFile": <String, dynamic>{
|
||||||
|
"path": "remote.php/dav/files/admin/test1.json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _upgradeV8ManualCover() {
|
||||||
|
withClock(Clock.fixed(DateTime.utc(2020, 1, 2, 3, 4, 5)), () {
|
||||||
|
final json = <String, dynamic>{
|
||||||
|
"version": 8,
|
||||||
|
"lastUpdated": "2020-01-02T03:04:05.678901Z",
|
||||||
|
"provider": <String, dynamic>{
|
||||||
|
"type": "static",
|
||||||
|
"content": <String, dynamic>{
|
||||||
|
"items": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"coverProvider": <String, dynamic>{
|
||||||
|
"type": "manual",
|
||||||
|
"content": <String, dynamic>{
|
||||||
|
"coverFile": <String, dynamic>{
|
||||||
|
"path": "remote.php/dav/files/admin/test1.jpg",
|
||||||
|
"fileId": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sortProvider": <String, dynamic>{
|
||||||
|
"type": "null",
|
||||||
|
"content": <String, dynamic>{},
|
||||||
|
},
|
||||||
|
"albumFile": <String, dynamic>{
|
||||||
|
"path": "remote.php/dav/files/admin/test1.json",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(const AlbumUpgraderV8()(json), <String, dynamic>{
|
||||||
|
"version": 8,
|
||||||
|
"lastUpdated": "2020-01-02T03:04:05.678901Z",
|
||||||
|
"provider": <String, dynamic>{
|
||||||
|
"type": "static",
|
||||||
|
"content": <String, dynamic>{
|
||||||
|
"items": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"coverProvider": <String, dynamic>{
|
||||||
|
"type": "manual",
|
||||||
|
"content": <String, dynamic>{
|
||||||
|
"coverFile": <String, dynamic>{
|
||||||
|
"fdPath": "remote.php/dav/files/admin/test1.jpg",
|
||||||
|
"fdId": 1,
|
||||||
|
"fdMime": null,
|
||||||
|
"fdIsArchived": false,
|
||||||
|
"fdIsFavorite": false,
|
||||||
|
"fdDateTime": "2020-01-02T03:04:05.000Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sortProvider": <String, dynamic>{
|
||||||
|
"type": "null",
|
||||||
|
"content": <String, dynamic>{},
|
||||||
|
},
|
||||||
|
"albumFile": <String, dynamic>{
|
||||||
|
"path": "remote.php/dav/files/admin/test1.json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _upgradeV8ManualExifTime() {
|
||||||
|
final json = <String, dynamic>{
|
||||||
|
"version": 8,
|
||||||
|
"lastUpdated": "2020-01-02T03:04:05.678901Z",
|
||||||
|
"provider": <String, dynamic>{
|
||||||
|
"type": "static",
|
||||||
|
"content": <String, dynamic>{
|
||||||
|
"items": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"coverProvider": <String, dynamic>{
|
||||||
|
"type": "manual",
|
||||||
|
"content": <String, dynamic>{
|
||||||
|
"coverFile": <String, dynamic>{
|
||||||
|
"path": "remote.php/dav/files/admin/test1.jpg",
|
||||||
|
"fileId": 1,
|
||||||
|
"metadata": <String, dynamic>{
|
||||||
|
"exif": <String, dynamic>{
|
||||||
|
"DateTimeOriginal": "2020:01:02 03:04:05",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sortProvider": <String, dynamic>{
|
||||||
|
"type": "null",
|
||||||
|
"content": <String, dynamic>{},
|
||||||
|
},
|
||||||
|
"albumFile": <String, dynamic>{
|
||||||
|
"path": "remote.php/dav/files/admin/test1.json",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(const AlbumUpgraderV8()(json), <String, dynamic>{
|
||||||
|
"version": 8,
|
||||||
|
"lastUpdated": "2020-01-02T03:04:05.678901Z",
|
||||||
|
"provider": <String, dynamic>{
|
||||||
|
"type": "static",
|
||||||
|
"content": <String, dynamic>{
|
||||||
|
"items": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"coverProvider": <String, dynamic>{
|
||||||
|
"type": "manual",
|
||||||
|
"content": <String, dynamic>{
|
||||||
|
"coverFile": <String, dynamic>{
|
||||||
|
"fdPath": "remote.php/dav/files/admin/test1.jpg",
|
||||||
|
"fdId": 1,
|
||||||
|
"fdMime": null,
|
||||||
|
"fdIsArchived": false,
|
||||||
|
"fdIsFavorite": false,
|
||||||
|
// dart does not provide a way to mock timezone
|
||||||
|
"fdDateTime": DateTime(2020, 1, 2, 3, 4, 5).toUtc().toIso8601String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sortProvider": <String, dynamic>{
|
||||||
|
"type": "null",
|
||||||
|
"content": <String, dynamic>{},
|
||||||
|
},
|
||||||
|
"albumFile": <String, dynamic>{
|
||||||
|
"path": "remote.php/dav/files/admin/test1.json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class _NullAlbumUpgraderFactory extends AlbumUpgraderFactory {
|
class _NullAlbumUpgraderFactory extends AlbumUpgraderFactory {
|
||||||
const _NullAlbumUpgraderFactory();
|
const _NullAlbumUpgraderFactory();
|
||||||
|
|
||||||
|
@ -1900,4 +2096,6 @@ class _NullAlbumUpgraderFactory extends AlbumUpgraderFactory {
|
||||||
buildV6() => null;
|
buildV6() => null;
|
||||||
@override
|
@override
|
||||||
buildV7() => null;
|
buildV7() => null;
|
||||||
|
@override
|
||||||
|
AlbumUpgraderV8? buildV8() => null;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue