Abstract album sort behavior

This commit is contained in:
Ming Ming 2021-07-08 02:40:43 +08:00
parent 2e82360071
commit 4fb6c022d2
8 changed files with 450 additions and 7 deletions

View file

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

View file

@ -10,6 +10,7 @@ import 'package:nc_photos/app_db.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/album/upgrader.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart';
@ -34,6 +35,7 @@ class Album with EquatableMixin {
@required String name,
@required this.provider,
@required this.coverProvider,
@required this.sortProvider,
this.albumFile,
}) : this.lastUpdated = (lastUpdated ?? DateTime.now()).toUtc(),
this.name = name ?? "";
@ -42,6 +44,7 @@ class Album with EquatableMixin {
Map<String, dynamic> json, {
AlbumUpgraderV1 upgraderV1,
AlbumUpgraderV2 upgraderV2,
AlbumUpgraderV3 upgraderV3,
}) {
final jsonVersion = json["version"];
if (jsonVersion < 2) {
@ -58,6 +61,13 @@ class Album with EquatableMixin {
return null;
}
}
if (jsonVersion < 4) {
json = upgraderV3?.call(json);
if (json == null) {
_log.info("[fromJson] Version $jsonVersion not compatible");
return null;
}
}
return Album(
lastUpdated: json["lastUpdated"] == null
? null
@ -67,6 +77,8 @@ class Album with EquatableMixin {
AlbumProvider.fromJson(json["provider"].cast<String, dynamic>()),
coverProvider: AlbumCoverProvider.fromJson(
json["coverProvider"].cast<String, dynamic>()),
sortProvider: AlbumSortProvider.fromJson(
json["sortProvider"].cast<String, dynamic>()),
albumFile: json["albumFile"] == null
? null
: File.fromJson(json["albumFile"].cast<String, dynamic>()),
@ -80,6 +92,7 @@ class Album with EquatableMixin {
"name: $name, "
"provider: ${provider.toString(isDeep: isDeep)}, "
"coverProvider: $coverProvider, "
"sortProvider: $sortProvider, "
"albumFile: $albumFile, "
"}";
}
@ -94,6 +107,7 @@ class Album with EquatableMixin {
String name,
AlbumProvider provider,
AlbumCoverProvider coverProvider,
AlbumSortProvider sortProvider,
File albumFile,
}) {
return Album(
@ -101,6 +115,7 @@ class Album with EquatableMixin {
name: name ?? this.name,
provider: provider ?? this.provider,
coverProvider: coverProvider ?? this.coverProvider,
sortProvider: sortProvider??this.sortProvider,
albumFile: albumFile ?? this.albumFile,
);
}
@ -112,6 +127,7 @@ class Album with EquatableMixin {
"name": name,
"provider": provider.toJson(),
"coverProvider": coverProvider.toJson(),
"sortProvider": sortProvider.toJson(),
// ignore albumFile
};
}
@ -123,6 +139,7 @@ class Album with EquatableMixin {
"name": name,
"provider": provider.toJson(),
"coverProvider": coverProvider.toJson(),
"sortProvider": sortProvider.toJson(),
if (albumFile != null) "albumFile": albumFile.toJson(),
};
}
@ -133,6 +150,7 @@ class Album with EquatableMixin {
name,
provider,
coverProvider,
sortProvider,
albumFile,
];
@ -141,6 +159,7 @@ class Album with EquatableMixin {
final AlbumProvider provider;
final AlbumCoverProvider coverProvider;
final AlbumSortProvider sortProvider;
/// How is this album stored on server
///
@ -148,7 +167,7 @@ class Album with EquatableMixin {
final File albumFile;
/// versioning of this class, use to upgrade old persisted album
static const version = 3;
static const version = 4;
}
class AlbumRepo {
@ -201,6 +220,7 @@ class AlbumRemoteDataSource implements AlbumDataSource {
jsonDecode(utf8.decode(data)),
upgraderV1: AlbumUpgraderV1(),
upgraderV2: AlbumUpgraderV2(),
upgraderV3: AlbumUpgraderV3(),
).copyWith(albumFile: albumFile);
} catch (e, stacktrace) {
dynamic d = data;

View file

@ -0,0 +1,152 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/iterable_extension.dart';
import 'package:tuple/tuple.dart';
abstract class AlbumSortProvider with EquatableMixin {
const AlbumSortProvider();
factory AlbumSortProvider.fromJson(Map<String, dynamic> json) {
final type = json["type"];
final content = json["content"];
switch (type) {
case AlbumNullSortProvider._type:
return AlbumNullSortProvider.fromJson(content.cast<String, dynamic>());
case AlbumTimeSortProvider._type:
return AlbumTimeSortProvider.fromJson(content.cast<String, dynamic>());
default:
_log.shout("[fromJson] Unknown type: $type");
throw ArgumentError.value(type, "type");
}
}
Map<String, dynamic> toJson() {
String getType() {
if (this is AlbumNullSortProvider) {
return AlbumNullSortProvider._type;
} else if (this is AlbumTimeSortProvider) {
return AlbumTimeSortProvider._type;
} else {
throw StateError("Unknwon subtype");
}
}
return {
"type": getType(),
"content": _toContentJson(),
};
}
/// Return a sorted copy of [items]
List<AlbumItem> sort(List<AlbumItem> items);
Map<String, dynamic> _toContentJson();
static final _log = Logger("entity.album.sort_provider.AlbumSortProvider");
}
/// Sort provider that does nothing
class AlbumNullSortProvider extends AlbumSortProvider {
const AlbumNullSortProvider();
factory AlbumNullSortProvider.fromJson(Map<String, dynamic> json) {
return AlbumNullSortProvider();
}
@override
toString() {
return "$runtimeType {"
"}";
}
@override
sort(List<AlbumItem> items) {
return List.from(items);
}
@override
get props => [];
@override
_toContentJson() {
return {};
}
static const _type = "null";
}
abstract class AlbumReversibleSortProvider extends AlbumSortProvider {
const AlbumReversibleSortProvider({
@required this.isAscending,
});
@override
toString() {
return "$runtimeType {"
"isAscending: $isAscending, "
"}";
}
@override
get props => [
isAscending,
];
@override
_toContentJson() {
return {
"isAscending": isAscending,
};
}
final bool isAscending;
}
/// Sort based on the time of the files
class AlbumTimeSortProvider extends AlbumReversibleSortProvider {
const AlbumTimeSortProvider({
bool isAscending,
}) : super(isAscending: isAscending);
factory AlbumTimeSortProvider.fromJson(Map<String, dynamic> json) {
return AlbumTimeSortProvider(
isAscending: json["isAscending"] ?? true,
);
}
@override
toString() {
return "$runtimeType {"
"super: ${super.toString()}, "
"}";
}
@override
sort(List<AlbumItem> items) {
DateTime prevFileTime;
return items
.map((e) {
if (e is AlbumFileItem) {
// take the file time
prevFileTime = e.file.bestDateTime;
}
// for non file items, use the sibling file's time
return Tuple2(
prevFileTime ?? DateTime.fromMillisecondsSinceEpoch(0), e);
})
.sorted((x, y) {
if (isAscending) {
return x.item1.compareTo(y.item1);
} else {
return y.item1.compareTo(x.item1);
}
})
.map((e) => e.item2)
.toList();
}
static const _type = "time";
}

View file

@ -55,3 +55,29 @@ class AlbumUpgraderV2 implements AlbumUpgrader {
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV2");
}
/// Upgrade v3 Album to v4
class AlbumUpgraderV3 implements AlbumUpgrader {
AlbumUpgraderV3({
this.logFilePath,
});
Map<String, dynamic> call(Map<String, dynamic> json) {
// move v3 items to v4 provider
_log.fine("[call] Upgrade v3 Album for file: $logFilePath");
final result = Map<String, dynamic>.from(json);
// add the descending time sort provider
result["sortProvider"] = <String, dynamic>{
"type": "time",
"content": {
"isAscending": false,
},
};
return result;
}
/// File path for logging only
final String logFilePath;
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV3");
}

View file

@ -10,6 +10,7 @@ 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/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/exception_util.dart' as exception_util;
@ -233,6 +234,7 @@ class _AlbumImporterState extends State<AlbumImporter> {
dirs: [p],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: AlbumTimeSortProvider(isAscending: false),
);
_log.info("[_onImportPressed] Creating dir album: $album");

View file

@ -9,10 +9,10 @@ import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/list_extension.dart';
import 'package:nc_photos/snack_bar_manager.dart';
@ -248,11 +248,11 @@ class _AlbumViewerState extends State<AlbumViewer>
}
void _transformItems() {
_backingFiles = _getAlbumItemsOf(_album)
_backingFiles = _album.sortProvider.sort(_getAlbumItemsOf(_album))
.whereType<AlbumFileItem>()
.map((e) => e.file)
.where((element) => file_util.isSupportedFormat(element))
.sorted(compareFileDateTimeDescending);
.toList();
itemStreamListItems = () sync* {
for (int i = 0; i < _backingFiles.length; ++i) {

View file

@ -6,6 +6,7 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/cover_provider.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/use_case/create_album.dart';
import 'package:nc_photos/widget/album_dir_picker.dart';
@ -120,6 +121,7 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
items: const [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: AlbumTimeSortProvider(isAscending: false),
);
_log.info("[_onOkPressed] Creating static album: $album");
final albumRepo = AlbumRepo(AlbumCachedDataSource());
@ -147,6 +149,7 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
dirs: value,
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: AlbumTimeSortProvider(isAscending: false),
);
_log.info("[_onOkPressed] Creating dir album: $album");
final albumRepo = AlbumRepo(AlbumCachedDataSource());

View file

@ -2,6 +2,7 @@ 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/album/upgrader.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:test/test.dart';
@ -23,6 +24,10 @@ void main() {
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
};
expect(
Album.fromJson(json),
@ -33,6 +38,7 @@ void main() {
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: AlbumNullSortProvider(),
));
});
@ -51,6 +57,10 @@ void main() {
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
};
expect(
Album.fromJson(json),
@ -61,6 +71,7 @@ void main() {
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: AlbumNullSortProvider(),
));
});
@ -96,6 +107,10 @@ void main() {
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
};
expect(
Album.fromJson(json),
@ -113,6 +128,7 @@ void main() {
],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: AlbumNullSortProvider(),
));
});
@ -135,6 +151,10 @@ void main() {
},
},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
};
expect(
Album.fromJson(json),
@ -149,6 +169,44 @@ void main() {
path: "remote.php/dav/files/admin/test1.jpg",
),
),
sortProvider: AlbumNullSortProvider(),
));
});
test("AlbumTimeSortProvider", () {
final json = <String, dynamic>{
"version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "time",
"content": <String, dynamic>{
"isAscending": true,
},
},
};
expect(
Album.fromJson(json),
Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
provider: AlbumStaticProvider(
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: AlbumTimeSortProvider(
isAscending: true,
),
));
});
@ -166,6 +224,10 @@ void main() {
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
"albumFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
},
@ -179,6 +241,7 @@ void main() {
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: AlbumNullSortProvider(),
albumFile: File(path: "remote.php/dav/files/admin/test1.jpg"),
));
});
@ -193,6 +256,7 @@ void main() {
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: AlbumNullSortProvider(),
);
expect(album.toRemoteJson(), <String, dynamic>{
"version": Album.version,
@ -208,6 +272,10 @@ void main() {
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
});
});
@ -219,6 +287,7 @@ void main() {
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: AlbumNullSortProvider(),
);
expect(album.toRemoteJson(), <String, dynamic>{
"version": Album.version,
@ -234,6 +303,10 @@ void main() {
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
});
});
@ -252,6 +325,7 @@ void main() {
],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: AlbumNullSortProvider(),
);
expect(album.toRemoteJson(), <String, dynamic>{
"version": Album.version,
@ -284,6 +358,10 @@ void main() {
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
});
});
@ -296,6 +374,7 @@ void main() {
),
coverProvider: AlbumAutoCoverProvider(
coverFile: File(path: "remote.php/dav/files/admin/test1.jpg")),
sortProvider: AlbumNullSortProvider(),
);
expect(album.toRemoteJson(), <String, dynamic>{
"version": Album.version,
@ -315,12 +394,14 @@ void main() {
},
},
},
});
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
});
});
group("toAppDbJson", () {
test("lastUpdated", () {
test("AlbumTimeSortProvider", () {
final album = Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
@ -328,6 +409,9 @@ void main() {
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: AlbumTimeSortProvider(
isAscending: true,
),
);
expect(album.toAppDbJson(), <String, dynamic>{
"version": Album.version,
@ -343,6 +427,45 @@ void main() {
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "time",
"content": <String, dynamic>{
"isAscending": true,
},
},
});
});
});
group("toAppDbJson", () {
test("lastUpdated", () {
final album = Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
provider: AlbumStaticProvider(
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: AlbumNullSortProvider(),
);
expect(album.toAppDbJson(), <String, dynamic>{
"version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
});
});
@ -354,6 +477,7 @@ void main() {
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: AlbumNullSortProvider(),
);
expect(album.toAppDbJson(), <String, dynamic>{
"version": Album.version,
@ -369,6 +493,10 @@ void main() {
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
});
});
@ -387,6 +515,7 @@ void main() {
],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: AlbumNullSortProvider(),
);
expect(album.toAppDbJson(), <String, dynamic>{
"version": Album.version,
@ -419,6 +548,10 @@ void main() {
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
});
});
@ -434,6 +567,7 @@ void main() {
path: "remote.php/dav/files/admin/test1.jpg",
),
),
sortProvider: AlbumNullSortProvider(),
);
expect(album.toAppDbJson(), <String, dynamic>{
"version": Album.version,
@ -453,6 +587,45 @@ void main() {
},
},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
});
});
test("AlbumTimeSortProvider", () {
final album = Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
provider: AlbumStaticProvider(
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: AlbumTimeSortProvider(
isAscending: true,
),
);
expect(album.toAppDbJson(), <String, dynamic>{
"version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "time",
"content": <String, dynamic>{
"isAscending": true,
},
},
});
});
@ -464,6 +637,7 @@ void main() {
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: AlbumNullSortProvider(),
albumFile: File(path: "remote.php/dav/files/admin/test1.jpg"),
);
expect(album.toAppDbJson(), <String, dynamic>{
@ -480,6 +654,10 @@ void main() {
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
"albumFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
},
@ -560,5 +738,66 @@ void main() {
},
});
});
test("AlbumUpgraderV3", () {
final json = <String, dynamic>{
"version": 3,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [
<String, dynamic>{
"type": "file",
"content": <String, dynamic>{
"file": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
},
},
},
],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{},
},
"albumFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.json",
},
};
expect(AlbumUpgraderV3()(json), <String, dynamic>{
"version": 3,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [
<String, dynamic>{
"type": "file",
"content": <String, dynamic>{
"file": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
},
},
},
],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "time",
"content": <String, dynamic>{
"isAscending": false,
},
},
"albumFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.json",
},
});
});
});
}