Regression: set/unset album cover

This commit is contained in:
Ming Ming 2023-04-18 00:15:29 +08:00
parent 3d05d01b0b
commit 3ccf302553
29 changed files with 653 additions and 218 deletions

View file

@ -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);

View file

@ -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;
} }

View file

@ -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";
} }

View file

@ -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}}";
} }
} }

View file

@ -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;

View file

@ -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");
}

View file

@ -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

View file

@ -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 {

View file

@ -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;

View file

@ -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;

View file

@ -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));

View file

@ -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;

View file

@ -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;
} }

View file

@ -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;

View file

@ -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();

View file

@ -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
View 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}";
}
}

View file

@ -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;
} }

View file

@ -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;

View file

@ -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 {

View file

@ -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)

View file

@ -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

View file

@ -5,58 +5,69 @@ 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( buildWhen: (previous, current) =>
floating: true, previous.items != current.items ||
expandedHeight: 160, previous.collection != current.collection,
flexibleSpace: FlexibleSpaceBar( builder: (context, state) {
background: const _AppBarCover(), final bloc = context.read<_Bloc>();
title: _BlocBuilder( final adapter = CollectionAdapter.of(c, bloc.account, state.collection);
buildWhen: (previous, current) => final canRename = adapter.isPermitted(CollectionCapability.rename);
previous.collection.name != current.collection.name, final canManualCover =
builder: (context, state) => Text( adapter.isPermitted(CollectionCapability.manualCover);
state.collection.name,
style: TextStyle( final actions = <Widget>[
color: Theme.of(context).appBarTheme.foregroundColor, ZoomMenuButton(
initialZoom: 0,
minZoom: 0,
maxZoom: 2,
onZoomChanged: (value) {
context.read<PrefController>().setAlbumBrowserZoomLevel(value);
},
),
];
if (state.items.isNotEmpty || canRename) {
actions.add(PopupMenuButton<_MenuOption>(
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
itemBuilder: (_) => [
if (canRename)
PopupMenuItem(
value: _MenuOption.edit,
child: Text(L10n.global().editTooltip),
),
if (canManualCover && adapter.isManualCover())
PopupMenuItem(
value: _MenuOption.unsetCover,
child: Text(L10n.global().unsetAlbumCoverTooltip),
),
if (state.items.isNotEmpty)
PopupMenuItem(
value: _MenuOption.download,
child: Text(L10n.global().downloadTooltip),
),
],
onSelected: (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,
), );
actions: [ },
ZoomMenuButton(
initialZoom: 0,
minZoom: 0,
maxZoom: 2,
onZoomChanged: (value) {
context.read<PrefController>().setAlbumBrowserZoomLevel(value);
},
),
if (capabilities.contains(CollectionCapability.rename))
_BlocBuilder(
buildWhen: (previous, current) => previous.items != current.items,
builder: (context, state) => PopupMenuButton<_MenuOption>(
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
itemBuilder: (context) {
return [
if (capabilities.contains(CollectionCapability.rename))
PopupMenuItem(
value: _MenuOption.edit,
child: Text(L10n.global().editTooltip),
),
if (state.items.isNotEmpty)
PopupMenuItem(
value: _MenuOption.download,
child: Text(L10n.global().downloadTooltip),
),
];
},
onSelected: (option) {
_onMenuSelected(context, option);
},
),
),
],
); );
} }
@ -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,
} }

View file

@ -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) .any(adapter.isItemRemovable),
.toList()),
)); ));
} }
@ -403,23 +407,20 @@ 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?) return api_util.getFilePreviewUrlByFileId(
?.file; account,
if (firstFile != null) { firstFile.fdId,
return api_util.getFilePreviewUrlByFileId( width: k.coverSize,
account, height: k.coverSize,
firstFile.fdId, isKeepAspectRatio: false,
width: k.coverSize, );
height: k.coverSize, } catch (_) {
isKeepAspectRatio: false, return null;
); }
}
} catch (_) {}
return null;
} }
static String? _getCoverUrl(Collection collection) { static String? _getCoverUrl(Collection collection) {
@ -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;

View file

@ -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 {

View file

@ -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() {

View file

@ -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;

View file

@ -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 {

View file

@ -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;
} }