mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-24 02:18: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_item.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/use_case/collection/create_collection.dart';
|
||||
import 'package:nc_photos/use_case/collection/edit_collection.dart';
|
||||
|
@ -157,6 +159,7 @@ class CollectionsController {
|
|||
String? name,
|
||||
List<CollectionItem>? items,
|
||||
CollectionItemSort? itemSort,
|
||||
OrNull<FileDescriptor>? cover,
|
||||
}) async {
|
||||
try {
|
||||
final c = await _mutex.protect(() async {
|
||||
|
@ -166,6 +169,7 @@ class CollectionsController {
|
|||
name: name,
|
||||
items: items,
|
||||
itemSort: itemSort,
|
||||
cover: cover,
|
||||
);
|
||||
});
|
||||
_updateCollection(c, items);
|
||||
|
|
|
@ -94,6 +94,13 @@ class Album with EquatableMixin {
|
|||
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) {
|
||||
_log.warning(
|
||||
"[fromJson] Reading album with newer version: $jsonVersion > $version");
|
||||
|
@ -217,7 +224,7 @@ class Album with EquatableMixin {
|
|||
final int savedVersion;
|
||||
|
||||
/// versioning of this class, use to upgrade old persisted album
|
||||
static const version = 8;
|
||||
static const version = 9;
|
||||
|
||||
static final _log = _$AlbumNpLog.log;
|
||||
}
|
||||
|
|
|
@ -122,13 +122,14 @@ class AlbumAutoCoverProvider extends AlbumCoverProvider {
|
|||
/// Cover picked by user
|
||||
@toString
|
||||
class AlbumManualCoverProvider extends AlbumCoverProvider {
|
||||
AlbumManualCoverProvider({
|
||||
const AlbumManualCoverProvider({
|
||||
required this.coverFile,
|
||||
});
|
||||
|
||||
factory AlbumManualCoverProvider.fromJson(JsonObj json) {
|
||||
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();
|
||||
|
||||
@override
|
||||
getCover(Album album) => coverFile;
|
||||
FileDescriptor? getCover(Album album) => coverFile;
|
||||
|
||||
@override
|
||||
get props => [
|
||||
List<Object?> get props => [
|
||||
coverFile,
|
||||
];
|
||||
|
||||
@override
|
||||
_toContentJson() {
|
||||
JsonObj _toContentJson() {
|
||||
return {
|
||||
"coverFile": coverFile.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
final File coverFile;
|
||||
final FileDescriptor coverFile;
|
||||
|
||||
static const _type = "manual";
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ extension _$AlbumAutoCoverProviderToString on AlbumAutoCoverProvider {
|
|||
extension _$AlbumManualCoverProviderToString on AlbumManualCoverProvider {
|
||||
String _$toString() {
|
||||
// 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:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/entity/exif.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_common/ci_string.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
|
@ -238,6 +240,49 @@ class AlbumUpgraderV7 implements AlbumUpgrader {
|
|||
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 {
|
||||
const AlbumUpgraderFactory();
|
||||
|
||||
|
@ -248,6 +293,7 @@ abstract class AlbumUpgraderFactory {
|
|||
AlbumUpgraderV5? buildV5();
|
||||
AlbumUpgraderV6? buildV6();
|
||||
AlbumUpgraderV7? buildV7();
|
||||
AlbumUpgraderV8? buildV8();
|
||||
}
|
||||
|
||||
class DefaultAlbumUpgraderFactory extends AlbumUpgraderFactory {
|
||||
|
@ -282,6 +328,9 @@ class DefaultAlbumUpgraderFactory extends AlbumUpgraderFactory {
|
|||
@override
|
||||
buildV7() => AlbumUpgraderV7(logFilePath: logFilePath);
|
||||
|
||||
@override
|
||||
AlbumUpgraderV8? buildV8() => AlbumUpgraderV8(logFilePath: logFilePath);
|
||||
|
||||
final Account account;
|
||||
final File? albumFile;
|
||||
|
||||
|
|
|
@ -54,3 +54,10 @@ extension _$AlbumUpgraderV7NpLog on 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,
|
||||
// text labels
|
||||
labelItem,
|
||||
// set the cover image
|
||||
manualCover,
|
||||
}
|
||||
|
||||
/// Provide the actual content of a collection
|
||||
|
@ -80,6 +82,10 @@ abstract class CollectionContentProvider {
|
|||
DateTime get lastModified;
|
||||
|
||||
/// Return the capabilities of the collection
|
||||
///
|
||||
/// Notice that the capabilities returned here represent all the capabilities
|
||||
/// that this implementation supports. In practice there may be extra runtime
|
||||
/// requirements that mask some of them (e.g., user permissions)
|
||||
List<CollectionCapability> get capabilities;
|
||||
|
||||
/// Return the sort type
|
||||
|
|
|
@ -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/util.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
|
||||
abstract class CollectionAdapter {
|
||||
|
@ -51,13 +52,11 @@ abstract class CollectionAdapter {
|
|||
});
|
||||
|
||||
/// Edit this collection
|
||||
///
|
||||
/// [name] and [items] are optional params and if not null, set the value to
|
||||
/// this collection
|
||||
Future<Collection> edit({
|
||||
String? name,
|
||||
List<CollectionItem>? items,
|
||||
CollectionItemSort? itemSort,
|
||||
OrNull<FileDescriptor>? cover,
|
||||
});
|
||||
|
||||
/// Remove [items] from this collection and return the removed count
|
||||
|
@ -70,10 +69,16 @@ abstract class CollectionAdapter {
|
|||
/// Convert a [NewCollectionItem] to an adapted one
|
||||
Future<CollectionItem> adaptToNewItem(NewCollectionItem original);
|
||||
|
||||
bool isItemsRemovable(List<CollectionItem> items);
|
||||
bool isItemRemovable(CollectionItem item);
|
||||
|
||||
/// Remove this collection
|
||||
Future<void> remove();
|
||||
|
||||
/// Return if this capability is allowed
|
||||
bool isPermitted(CollectionCapability capability);
|
||||
|
||||
/// Return if the cover of this collection has been manually set
|
||||
bool isManualCover();
|
||||
}
|
||||
|
||||
abstract class CollectionItemAdapter {
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/album/cover_provider.dart';
|
||||
import 'package:nc_photos/entity/album/item.dart';
|
||||
import 'package:nc_photos/entity/album/provider.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
|
@ -17,6 +18,7 @@ import 'package:nc_photos/entity/file.dart';
|
|||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/use_case/album/add_file_to_album.dart';
|
||||
import 'package:nc_photos/use_case/album/edit_album.dart';
|
||||
import 'package:nc_photos/use_case/album/remove_album.dart';
|
||||
|
@ -75,8 +77,9 @@ class CollectionAlbumAdapter implements CollectionAdapter {
|
|||
String? name,
|
||||
List<CollectionItem>? items,
|
||||
CollectionItemSort? itemSort,
|
||||
OrNull<FileDescriptor>? cover,
|
||||
}) async {
|
||||
assert(name != null || items != null || itemSort != null);
|
||||
assert(name != null || items != null || itemSort != null || cover != null);
|
||||
final newItems = items?.run((items) => items
|
||||
.map((e) {
|
||||
if (e is AlbumAdaptedCollectionItem) {
|
||||
|
@ -101,6 +104,7 @@ class CollectionAlbumAdapter implements CollectionAdapter {
|
|||
name: name,
|
||||
items: newItems,
|
||||
itemSort: itemSort,
|
||||
cover: cover,
|
||||
);
|
||||
return collection.copyWith(
|
||||
name: name,
|
||||
|
@ -185,18 +189,39 @@ class CollectionAlbumAdapter implements CollectionAdapter {
|
|||
}
|
||||
|
||||
@override
|
||||
bool isItemsRemovable(List<CollectionItem> items) {
|
||||
if (_provider.album.albumFile!.isOwned(account.userId)) {
|
||||
bool isItemRemovable(CollectionItem item) {
|
||||
if (_provider.album.provider is! AlbumStaticProvider) {
|
||||
return false;
|
||||
}
|
||||
if (_provider.album.albumFile?.isOwned(account.userId) == true) {
|
||||
return true;
|
||||
}
|
||||
return items
|
||||
.whereType<AlbumAdaptedCollectionItem>()
|
||||
.any((e) => e.albumItem.addedBy == account.userId);
|
||||
if (item is! AlbumAdaptedCollectionItem) {
|
||||
_log.warning("[isItemRemovable] Unknown item type: ${item.runtimeType}");
|
||||
return true;
|
||||
}
|
||||
return item.albumItem.addedBy == account.userId;
|
||||
}
|
||||
|
||||
@override
|
||||
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 Account account;
|
||||
final Collection collection;
|
||||
|
|
|
@ -48,6 +48,10 @@ class CollectionLocationGroupAdapter
|
|||
throw UnsupportedError("Operation not supported");
|
||||
}
|
||||
|
||||
@override
|
||||
bool isPermitted(CollectionCapability capability) =>
|
||||
_provider.capabilities.contains(capability);
|
||||
|
||||
final DiContainer _c;
|
||||
final Account account;
|
||||
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/nc_album.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/use_case/find_file_descriptor.dart';
|
||||
import 'package:nc_photos/use_case/nc_album/add_file_to_nc_album.dart';
|
||||
import 'package:nc_photos/use_case/nc_album/edit_nc_album.dart';
|
||||
|
@ -75,9 +76,10 @@ class CollectionNcAlbumAdapter implements CollectionAdapter {
|
|||
String? name,
|
||||
List<CollectionItem>? items,
|
||||
CollectionItemSort? itemSort,
|
||||
OrNull<FileDescriptor>? cover,
|
||||
}) async {
|
||||
assert(name != null);
|
||||
if (items != null || itemSort != null) {
|
||||
if (items != null || itemSort != null || cover != null) {
|
||||
_log.warning(
|
||||
"[edit] Nextcloud album does not support editing item or sort");
|
||||
}
|
||||
|
@ -131,13 +133,18 @@ class CollectionNcAlbumAdapter implements CollectionAdapter {
|
|||
}
|
||||
|
||||
@override
|
||||
bool isItemsRemovable(List<CollectionItem> items) {
|
||||
return true;
|
||||
}
|
||||
bool isItemRemovable(CollectionItem item) => true;
|
||||
|
||||
@override
|
||||
Future<void> remove() => RemoveNcAlbum(_c)(account, _provider.album);
|
||||
|
||||
@override
|
||||
bool isPermitted(CollectionCapability capability) =>
|
||||
_provider.capabilities.contains(capability);
|
||||
|
||||
@override
|
||||
bool isManualCover() => false;
|
||||
|
||||
Future<NcAlbum> _syncRemote() async {
|
||||
final remote = await ListNcAlbum(_c)(account).last;
|
||||
return remote.firstWhere((e) => e.compareIdentity(_provider.album));
|
||||
|
|
|
@ -50,6 +50,10 @@ class CollectionPersonAdapter
|
|||
throw UnsupportedError("Operation not supported");
|
||||
}
|
||||
|
||||
@override
|
||||
bool isPermitted(CollectionCapability capability) =>
|
||||
_provider.capabilities.contains(capability);
|
||||
|
||||
final DiContainer _c;
|
||||
final Account account;
|
||||
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/util.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
|
||||
/// A read-only collection that does not support modifying its items
|
||||
|
@ -22,6 +23,7 @@ mixin CollectionReadOnlyAdapter implements CollectionAdapter {
|
|||
String? name,
|
||||
List<CollectionItem>? items,
|
||||
CollectionItemSort? itemSort,
|
||||
OrNull<FileDescriptor>? cover,
|
||||
}) {
|
||||
throw UnsupportedError("Operation not supported");
|
||||
}
|
||||
|
@ -36,7 +38,8 @@ mixin CollectionReadOnlyAdapter implements CollectionAdapter {
|
|||
}
|
||||
|
||||
@override
|
||||
bool isItemsRemovable(List<CollectionItem> items) {
|
||||
return false;
|
||||
}
|
||||
bool isItemRemovable(CollectionItem item) => false;
|
||||
|
||||
@override
|
||||
bool isManualCover() => false;
|
||||
}
|
||||
|
|
|
@ -37,6 +37,10 @@ class CollectionTagAdapter
|
|||
throw UnsupportedError("Operation not supported");
|
||||
}
|
||||
|
||||
@override
|
||||
bool isPermitted(CollectionCapability capability) =>
|
||||
_provider.capabilities.contains(capability);
|
||||
|
||||
final DiContainer _c;
|
||||
final Account account;
|
||||
final Collection collection;
|
||||
|
|
|
@ -44,6 +44,7 @@ class CollectionAlbumProvider implements CollectionContentProvider {
|
|||
List<CollectionCapability> get capabilities => [
|
||||
CollectionCapability.sort,
|
||||
CollectionCapability.rename,
|
||||
CollectionCapability.manualCover,
|
||||
if (album.provider is AlbumStaticProvider) ...[
|
||||
CollectionCapability.manualItem,
|
||||
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
|
||||
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
|
||||
@toString
|
||||
class OrNull<T> {
|
||||
OrNull(this.obj);
|
||||
|
||||
|
@ -6,5 +11,8 @@ class OrNull<T> {
|
|||
/// null, false will still be returned
|
||||
static bool isSetNull(OrNull? x) => x != null && x.obj == null;
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
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/di_container.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/cover_provider.dart';
|
||||
import 'package:nc_photos/entity/album/item.dart';
|
||||
import 'package:nc_photos/entity/album/provider.dart';
|
||||
import 'package:nc_photos/entity/album/sort_provider.dart';
|
||||
import 'package:nc_photos/entity/collection_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:np_codegen/np_codegen.dart';
|
||||
|
||||
|
@ -22,9 +25,10 @@ class EditAlbum {
|
|||
String? name,
|
||||
List<AlbumItem>? items,
|
||||
CollectionItemSort? itemSort,
|
||||
OrNull<FileDescriptor>? cover,
|
||||
}) async {
|
||||
_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;
|
||||
if (name != null) {
|
||||
newAlbum = newAlbum.copyWith(name: name);
|
||||
|
@ -43,6 +47,17 @@ class EditAlbum {
|
|||
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)) {
|
||||
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_item.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 {
|
||||
const EditCollection(this._c);
|
||||
|
@ -15,6 +17,7 @@ class EditCollection {
|
|||
/// - Rename (set [name])
|
||||
/// - Add text label(s) (set [items])
|
||||
/// - Sort [items] (set [items] and/or [itemSort])
|
||||
/// - Set album [cover]
|
||||
///
|
||||
/// \* To add files to a collection, use [AddFileToCollection] instead
|
||||
Future<Collection> call(
|
||||
|
@ -23,11 +26,13 @@ class EditCollection {
|
|||
String? name,
|
||||
List<CollectionItem>? items,
|
||||
CollectionItemSort? itemSort,
|
||||
OrNull<FileDescriptor>? cover,
|
||||
}) =>
|
||||
CollectionAdapter.of(_c, account, collection).edit(
|
||||
name: name,
|
||||
items: items,
|
||||
itemSort: itemSort,
|
||||
cover: cover,
|
||||
);
|
||||
|
||||
final DiContainer _c;
|
||||
|
|
|
@ -353,8 +353,7 @@ class _AlbumBrowserState extends State<AlbumBrowser>
|
|||
}
|
||||
}
|
||||
Navigator.pushNamed(context, Viewer.routeName,
|
||||
arguments: ViewerArguments(widget.account, _backingFiles, fileIndex,
|
||||
album: _album));
|
||||
arguments: ViewerArguments(widget.account, _backingFiles, fileIndex));
|
||||
}
|
||||
|
||||
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_platform_interface/cached_network_image_platform_interface.dart';
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:copy_with/copy_with.dart';
|
||||
import 'package:flutter/material.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/np_api_util.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/use_case/archive_file.dart';
|
||||
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
|
||||
|
@ -227,8 +227,10 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser>
|
|||
if (!state.isEditMode) {
|
||||
return const _ContentList();
|
||||
} else {
|
||||
if (state.collection.capabilities
|
||||
.contains(CollectionCapability.manualSort)) {
|
||||
if (context
|
||||
.read<_Bloc>()
|
||||
.isCollectionCapabilityPermitted(
|
||||
CollectionCapability.manualSort)) {
|
||||
return const _EditContentList();
|
||||
} else {
|
||||
return const _UnmodifiableEditContentList();
|
||||
|
@ -314,16 +316,18 @@ class _ContentList extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = context.read<_Bloc>();
|
||||
return StreamBuilder<int>(
|
||||
stream: context.read<PrefController>().albumBrowserZoomLevel,
|
||||
initialData: context.read<PrefController>().albumBrowserZoomLevel.value,
|
||||
builder: (_, zoomLevel) {
|
||||
if (zoomLevel.hasError) {
|
||||
context.read<_Bloc>().add(
|
||||
bloc.add(
|
||||
_SetMessage(L10n.global().writePreferenceFailureNotification));
|
||||
}
|
||||
return _BlocBuilder(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.collection != current.collection ||
|
||||
previous.transformedItems != current.transformedItems ||
|
||||
previous.selectedItems != current.selectedItems,
|
||||
builder: (context, state) {
|
||||
|
@ -336,9 +340,7 @@ class _ContentList extends StatelessWidget {
|
|||
staggeredTileBuilder: (_, item) => item.staggeredTile,
|
||||
selectedItems: state.selectedItems,
|
||||
onSelectionChange: (_, selected) {
|
||||
context
|
||||
.read<_Bloc>()
|
||||
.add(_SetSelectedItems(items: selected.cast()));
|
||||
bloc.add(_SetSelectedItems(items: selected.cast()));
|
||||
},
|
||||
onItemTap: (context, index, _) {
|
||||
final actualIndex = index -
|
||||
|
@ -349,12 +351,19 @@ class _ContentList extends StatelessWidget {
|
|||
Navigator.of(context).pushNamed(
|
||||
Viewer.routeName,
|
||||
arguments: ViewerArguments(
|
||||
context.read<_Bloc>().account,
|
||||
bloc.account,
|
||||
state.transformedItems
|
||||
.whereType<_FileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList(),
|
||||
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) =>
|
||||
previous.editTransformedItems != current.editTransformedItems,
|
||||
builder: (context, state) {
|
||||
if (state.collection.capabilities
|
||||
.contains(CollectionCapability.manualSort)) {
|
||||
if (context.read<_Bloc>().isCollectionCapabilityPermitted(
|
||||
CollectionCapability.manualSort)) {
|
||||
return DraggableItemList<_Item>(
|
||||
maxCrossAxisExtent: photo_list_util
|
||||
.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 {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
|
|
|
@ -5,58 +5,69 @@ class _AppBar extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// capability can't be changed once the collection is created
|
||||
final capabilities = context.read<_Bloc>().state.collection.capabilities;
|
||||
return SliverAppBar(
|
||||
floating: true,
|
||||
expandedHeight: 160,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: const _AppBarCover(),
|
||||
title: _BlocBuilder(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.collection.name != current.collection.name,
|
||||
builder: (context, state) => Text(
|
||||
state.collection.name,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor,
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
return _BlocBuilder(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.items != current.items ||
|
||||
previous.collection != current.collection,
|
||||
builder: (context, state) {
|
||||
final bloc = context.read<_Bloc>();
|
||||
final adapter = CollectionAdapter.of(c, bloc.account, state.collection);
|
||||
final canRename = adapter.isPermitted(CollectionCapability.rename);
|
||||
final canManualCover =
|
||||
adapter.isPermitted(CollectionCapability.manualCover);
|
||||
|
||||
final actions = <Widget>[
|
||||
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: [
|
||||
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);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
actions: actions,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -65,6 +76,9 @@ class _AppBar extends StatelessWidget {
|
|||
case _MenuOption.edit:
|
||||
context.read<_Bloc>().add(const _BeginEdit());
|
||||
break;
|
||||
case _MenuOption.unsetCover:
|
||||
context.read<_Bloc>().add(const _UnsetCover());
|
||||
break;
|
||||
case _MenuOption.download:
|
||||
context.read<_Bloc>().add(const _Download());
|
||||
break;
|
||||
|
@ -145,8 +159,8 @@ class _SelectionAppBar extends StatelessWidget {
|
|||
PopupMenuButton<_SelectionMenuOption>(
|
||||
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
|
||||
itemBuilder: (context) => [
|
||||
if (state.collection.capabilities
|
||||
.contains(CollectionCapability.manualItem) &&
|
||||
if (context.read<_Bloc>().isCollectionCapabilityPermitted(
|
||||
CollectionCapability.manualItem) &&
|
||||
state.isSelectionRemovable)
|
||||
PopupMenuItem(
|
||||
value: _SelectionMenuOption.removeFromAlbum,
|
||||
|
@ -237,7 +251,11 @@ class _EditAppBar extends StatelessWidget {
|
|||
|
||||
@override
|
||||
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(
|
||||
floating: true,
|
||||
expandedHeight: 160,
|
||||
|
@ -274,13 +292,13 @@ class _EditAppBar extends StatelessWidget {
|
|||
},
|
||||
),
|
||||
actions: [
|
||||
if (capabilities.contains(CollectionCapability.labelItem))
|
||||
if (capabilitiesAdapter.isPermitted(CollectionCapability.labelItem))
|
||||
IconButton(
|
||||
icon: const Icon(Icons.text_fields),
|
||||
tooltip: L10n.global().albumAddTextTooltip,
|
||||
onPressed: () => _onAddTextPressed(context),
|
||||
),
|
||||
if (capabilities.contains(CollectionCapability.sort))
|
||||
if (capabilitiesAdapter.isPermitted(CollectionCapability.sort))
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sort_by_alpha),
|
||||
tooltip: L10n.global().sortTooltip,
|
||||
|
@ -354,6 +372,7 @@ class _EditAppBar extends StatelessWidget {
|
|||
|
||||
enum _MenuOption {
|
||||
edit,
|
||||
unsetCover,
|
||||
download,
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,8 @@ class _Bloc extends Bloc<_Event, _State> implements BlocTag {
|
|||
on<_DoneEdit>(_onDoneEdit, transformer: concurrent());
|
||||
on<_CancelEdit>(_onCancelEdit);
|
||||
|
||||
on<_UnsetCover>(_onUnsetCover);
|
||||
|
||||
on<_SetSelectedItems>(_onSetSelectedItems);
|
||||
on<_DownloadSelectedItems>(_onDownloadSelectedItems);
|
||||
on<_AddSelectedItemsToCollection>(_onAddSelectedItemsToCollection);
|
||||
|
@ -66,12 +68,18 @@ class _Bloc extends Bloc<_Event, _State> implements BlocTag {
|
|||
return super.close();
|
||||
}
|
||||
|
||||
bool isCollectionCapabilityPermitted(CollectionCapability capability) {
|
||||
return CollectionAdapter.of(_c, account, state.collection)
|
||||
.isPermitted(capability);
|
||||
}
|
||||
|
||||
@override
|
||||
String get tag => _log.fullName;
|
||||
|
||||
void _onUpdateCollection(_UpdateCollection ev, Emitter<_State> emit) {
|
||||
_log.info("$ev");
|
||||
emit(state.copyWith(collection: ev.collection));
|
||||
_updateCover(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) {
|
||||
_log.info("$ev");
|
||||
final result = _transformItems(ev.items, state.collection.itemSort);
|
||||
var newState = state.copyWith(transformedItems: result.transformed);
|
||||
if (state.coverUrl == null) {
|
||||
// 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);
|
||||
emit(state.copyWith(transformedItems: result.transformed));
|
||||
_updateCover(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) {
|
||||
_log.info("$ev");
|
||||
assert(
|
||||
state.collection.capabilities.contains(CollectionCapability.labelItem));
|
||||
assert(isCollectionCapabilityPermitted(CollectionCapability.labelItem));
|
||||
emit(state.copyWith(
|
||||
editItems: [
|
||||
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) {
|
||||
_log.info("$ev");
|
||||
assert(state.collection.capabilities
|
||||
.contains(CollectionCapability.manualSort));
|
||||
assert(isCollectionCapabilityPermitted(CollectionCapability.manualSort));
|
||||
emit(state.copyWith(
|
||||
editSort: CollectionItemSort.manual,
|
||||
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) {
|
||||
_log.info("$ev");
|
||||
final adapter = CollectionAdapter.of(_c, account, state.collection);
|
||||
emit(state.copyWith(
|
||||
selectedItems: ev.items,
|
||||
isSelectionRemovable: CollectionAdapter.of(_c, account, state.collection)
|
||||
.isItemsRemovable(ev.items
|
||||
.whereType<_ActualItem>()
|
||||
.map((e) => e.original)
|
||||
.toList()),
|
||||
isSelectionRemovable: ev.items
|
||||
.whereType<_ActualItem>()
|
||||
.map((e) => e.original)
|
||||
.any(adapter.isItemRemovable),
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -403,23 +407,20 @@ class _Bloc extends Bloc<_Event, _State> implements BlocTag {
|
|||
);
|
||||
}
|
||||
|
||||
String? _getCoverUrlOnNewItem(List<CollectionItem> sortedItems) {
|
||||
String? _getCoverUrlByItems() {
|
||||
try {
|
||||
final firstFile =
|
||||
(sortedItems.firstWhereOrNull((i) => i is CollectionFileItem)
|
||||
as CollectionFileItem?)
|
||||
?.file;
|
||||
if (firstFile != null) {
|
||||
return api_util.getFilePreviewUrlByFileId(
|
||||
account,
|
||||
firstFile.fdId,
|
||||
width: k.coverSize,
|
||||
height: k.coverSize,
|
||||
isKeepAspectRatio: false,
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
state.transformedItems.whereType<_FileItem>().first.file;
|
||||
return api_util.getFilePreviewUrlByFileId(
|
||||
account,
|
||||
firstFile.fdId,
|
||||
width: k.coverSize,
|
||||
height: k.coverSize,
|
||||
isKeepAspectRatio: false,
|
||||
);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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 Account account;
|
||||
final CollectionsController collectionsController;
|
||||
|
|
|
@ -188,6 +188,14 @@ class _CancelEdit implements _Event {
|
|||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
@toString
|
||||
class _UnsetCover implements _Event {
|
||||
const _UnsetCover();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
/// Set the currently selected items
|
||||
@toString
|
||||
class _SetSelectedItems implements _Event {
|
||||
|
|
|
@ -206,8 +206,7 @@ class _SmartAlbumBrowserState extends State<SmartAlbumBrowser>
|
|||
}
|
||||
}
|
||||
Navigator.pushNamed(context, Viewer.routeName,
|
||||
arguments: ViewerArguments(widget.account, _backingFiles, fileIndex,
|
||||
album: widget.album));
|
||||
arguments: ViewerArguments(widget.account, _backingFiles, fileIndex));
|
||||
}
|
||||
|
||||
void _onDownloadPressed() {
|
||||
|
|
|
@ -5,25 +5,29 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.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/download_handler.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/item.dart';
|
||||
import 'package:nc_photos/entity/album/provider.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/flutter_util.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
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/pref.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/use_case/album/remove_from_album.dart';
|
||||
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
|
||||
import 'package:nc_photos/use_case/update_property.dart';
|
||||
import 'package:nc_photos/widget/disposable.dart';
|
||||
|
@ -44,18 +48,25 @@ import 'package:np_codegen/np_codegen.dart';
|
|||
|
||||
part 'viewer.g.dart';
|
||||
|
||||
class ViewerCollectionData {
|
||||
const ViewerCollectionData(this.collection, this.items);
|
||||
|
||||
final Collection collection;
|
||||
final List<CollectionItem> items;
|
||||
}
|
||||
|
||||
class ViewerArguments {
|
||||
ViewerArguments(
|
||||
const ViewerArguments(
|
||||
this.account,
|
||||
this.streamFiles,
|
||||
this.startIndex, {
|
||||
this.album,
|
||||
this.fromCollection,
|
||||
});
|
||||
|
||||
final Account account;
|
||||
final List<FileDescriptor> streamFiles;
|
||||
final int startIndex;
|
||||
final Album? album;
|
||||
final ViewerCollectionData? fromCollection;
|
||||
}
|
||||
|
||||
class Viewer extends StatefulWidget {
|
||||
|
@ -73,7 +84,7 @@ class Viewer extends StatefulWidget {
|
|||
required this.account,
|
||||
required this.streamFiles,
|
||||
required this.startIndex,
|
||||
this.album,
|
||||
this.fromCollection,
|
||||
}) : super(key: key);
|
||||
|
||||
Viewer.fromArgs(ViewerArguments args, {Key? key})
|
||||
|
@ -82,7 +93,7 @@ class Viewer extends StatefulWidget {
|
|||
account: args.account,
|
||||
streamFiles: args.streamFiles,
|
||||
startIndex: args.startIndex,
|
||||
album: args.album,
|
||||
fromCollection: args.fromCollection,
|
||||
);
|
||||
|
||||
@override
|
||||
|
@ -92,8 +103,8 @@ class Viewer extends StatefulWidget {
|
|||
final List<FileDescriptor> streamFiles;
|
||||
final int startIndex;
|
||||
|
||||
/// The album these files belongs to, or null
|
||||
final Album? album;
|
||||
/// Data of the collection these files belongs to, or null
|
||||
final ViewerCollectionData? fromCollection;
|
||||
}
|
||||
|
||||
@npLog
|
||||
|
@ -304,9 +315,11 @@ class _ViewerState extends State<Viewer>
|
|||
child: ViewerDetailPane(
|
||||
account: widget.account,
|
||||
fd: _streamFilesView[index],
|
||||
album: widget.album,
|
||||
onRemoveFromAlbumPressed:
|
||||
_onRemoveFromAlbumPressed,
|
||||
fromCollection: widget.fromCollection?.run(
|
||||
(d) => ViewerSingleCollectionData(
|
||||
d.collection, d.items[index])),
|
||||
onRemoveFromCollectionPressed:
|
||||
_onRemoveFromCollectionPressed,
|
||||
onArchivePressed: _onArchivePressed,
|
||||
onUnarchivePressed: _onUnarchivePressed,
|
||||
onSlideshowPressed: _onSlideshowPressed,
|
||||
|
@ -666,30 +679,27 @@ class _ViewerState extends State<Viewer>
|
|||
_removeCurrentItemFromStream(context, index);
|
||||
}
|
||||
|
||||
void _onRemoveFromAlbumPressed(BuildContext context) {
|
||||
assert(widget.album!.provider is AlbumStaticProvider);
|
||||
Future<void> _onRemoveFromCollectionPressed(BuildContext context) async {
|
||||
assert(CollectionAdapter.of(KiwiContainer().resolve<DiContainer>(),
|
||||
widget.account, widget.fromCollection!.collection)
|
||||
.isPermitted(CollectionCapability.manualItem));
|
||||
final index = _viewerController.currentPage;
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final file = _streamFilesView[index];
|
||||
_log.info("[_onRemoveFromAlbumPressed] Remove file: ${file.fdPath}");
|
||||
NotifiedAction(
|
||||
() async {
|
||||
final selectedFile =
|
||||
(await InflateFileDescriptor(c)(widget.account, [file])).first;
|
||||
final thisItem = AlbumStaticProvider.of(widget.album!)
|
||||
.items
|
||||
.whereType<AlbumFileItem>()
|
||||
.firstWhere((e) => e.file.compareServerIdentity(selectedFile));
|
||||
await RemoveFromAlbum(KiwiContainer().resolve<DiContainer>())(
|
||||
widget.account, widget.album!, [thisItem]);
|
||||
},
|
||||
null,
|
||||
L10n.global().removeSelectedFromAlbumSuccessNotification(1),
|
||||
failureText: L10n.global().removeSelectedFromAlbumFailureNotification,
|
||||
).call().catchError((e, stackTrace) {
|
||||
_log.shout("[_onRemoveFromAlbumPressed] Failed while updating album", e,
|
||||
stackTrace);
|
||||
});
|
||||
_log.info("[_onRemoveFromCollectionPressed] Remove file: ${file.fdPath}");
|
||||
try {
|
||||
final itemsController = _findCollectionItemsController(context);
|
||||
final item = itemsController.stream.value.items
|
||||
.whereType<CollectionFileItem>()
|
||||
.firstWhere((i) => i.file.compareServerIdentity(file));
|
||||
await itemsController.removeItems([item]);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[_onRemoveFromCollectionPressed] Failed while updating album",
|
||||
e, stackTrace);
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(L10n.global().removeSelectedFromAlbumFailureNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
}
|
||||
_removeCurrentItemFromStream(context, index);
|
||||
}
|
||||
|
||||
|
@ -829,6 +839,19 @@ class _ViewerState extends State<Viewer>
|
|||
_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 _canOpenDetailPane() => !_isZoomed;
|
||||
bool _canZoom() => !_isDetailPaneActive;
|
||||
|
|
|
@ -2,32 +2,32 @@ import 'dart:async';
|
|||
|
||||
import 'package:android_intent_plus/android_intent.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.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/di_container.dart';
|
||||
import 'package:nc_photos/double_extension.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/cover_provider.dart';
|
||||
import 'package:nc_photos/entity/album/item.dart';
|
||||
import 'package:nc_photos/entity/album/provider.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||
import 'package:nc_photos/entity/collection_item.dart';
|
||||
import 'package:nc_photos/entity/exif_extension.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
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/or_null.dart';
|
||||
import 'package:nc_photos/platform/features.dart' as features;
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/theme.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/update_album.dart';
|
||||
import 'package:nc_photos/use_case/update_property.dart';
|
||||
import 'package:nc_photos/widget/about_geocoding_dialog.dart';
|
||||
import 'package:nc_photos/widget/animated_visibility.dart';
|
||||
|
@ -41,13 +41,20 @@ import 'package:tuple/tuple.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 {
|
||||
const ViewerDetailPane({
|
||||
Key? key,
|
||||
required this.account,
|
||||
required this.fd,
|
||||
this.album,
|
||||
required this.onRemoveFromAlbumPressed,
|
||||
this.fromCollection,
|
||||
required this.onRemoveFromCollectionPressed,
|
||||
required this.onArchivePressed,
|
||||
required this.onUnarchivePressed,
|
||||
this.onSlideshowPressed,
|
||||
|
@ -59,10 +66,10 @@ class ViewerDetailPane extends StatefulWidget {
|
|||
final Account account;
|
||||
final FileDescriptor fd;
|
||||
|
||||
/// The album this file belongs to, or null
|
||||
final Album? album;
|
||||
/// Data of the collection this file belongs to, or null
|
||||
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) onUnarchivePressed;
|
||||
final VoidCallback? onSlideshowPressed;
|
||||
|
@ -148,11 +155,10 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
_DetailPaneButton(
|
||||
icon: Icons.remove_outlined,
|
||||
label: L10n.global().removeFromAlbumTooltip,
|
||||
onPressed: () => widget.onRemoveFromAlbumPressed(context),
|
||||
onPressed: () =>
|
||||
widget.onRemoveFromCollectionPressed(context),
|
||||
),
|
||||
if (widget.album != null &&
|
||||
widget.album!.albumFile?.isOwned(widget.account.userId) ==
|
||||
true)
|
||||
if (_canSetCover)
|
||||
_DetailPaneButton(
|
||||
icon: Icons.photo_album_outlined,
|
||||
label: L10n.global().useAsAlbumCoverTooltip,
|
||||
|
@ -384,27 +390,21 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
|
||||
Future<void> _onSetAlbumCoverPressed(BuildContext context) async {
|
||||
assert(_file != null);
|
||||
assert(widget.album != null);
|
||||
assert(widget.fromCollection != null);
|
||||
_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 {
|
||||
await NotifiedAction(
|
||||
() async {
|
||||
await UpdateAlbum(_c.albumRepo)(
|
||||
widget.account,
|
||||
widget.album!.copyWith(
|
||||
coverProvider: AlbumManualCoverProvider(
|
||||
coverFile: _file!,
|
||||
),
|
||||
));
|
||||
},
|
||||
L10n.global().setAlbumCoverProcessingNotification,
|
||||
L10n.global().setAlbumCoverSuccessNotification,
|
||||
failureText: L10n.global().setAlbumCoverFailureNotification,
|
||||
)();
|
||||
await context.read<AccountController>().collectionsController.edit(
|
||||
widget.fromCollection!.collection,
|
||||
cover: OrNull(_file!),
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[_onSetAlbumCoverPressed] Failed while updating album", e,
|
||||
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;
|
||||
|
||||
File? _file;
|
||||
|
@ -495,9 +474,17 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
|
||||
final _tags = <String>[];
|
||||
|
||||
late final bool _canRemoveFromAlbum = _checkCanRemoveFromAlbum();
|
||||
|
||||
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 {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:nc_photos/entity/album.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 {
|
||||
const _NullAlbumUpgraderFactory();
|
||||
|
||||
|
@ -1900,4 +2096,6 @@ class _NullAlbumUpgraderFactory extends AlbumUpgraderFactory {
|
|||
buildV6() => null;
|
||||
@override
|
||||
buildV7() => null;
|
||||
@override
|
||||
AlbumUpgraderV8? buildV8() => null;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue