Support sorting albums by filename

This commit is contained in:
Ming Ming 2022-05-26 12:00:55 +08:00
parent 083263c561
commit 32bef588a1
9 changed files with 521 additions and 1 deletions

View file

@ -98,6 +98,13 @@ class Album with EquatableMixin {
return null;
}
}
if (jsonVersion < 8) {
result = upgraderFactory?.buildV7()?.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");
@ -221,7 +228,7 @@ class Album with EquatableMixin {
final int savedVersion;
/// versioning of this class, use to upgrade old persisted album
static const version = 7;
static const version = 8;
}
class AlbumShare with EquatableMixin {

View file

@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/entity/album/item.dart';
@ -17,6 +18,9 @@ abstract class AlbumSortProvider with EquatableMixin {
return AlbumNullSortProvider.fromJson(content.cast<String, dynamic>());
case AlbumTimeSortProvider._type:
return AlbumTimeSortProvider.fromJson(content.cast<String, dynamic>());
case AlbumFilenameSortProvider._type:
return AlbumFilenameSortProvider.fromJson(
content.cast<String, dynamic>());
default:
_log.shout("[fromJson] Unknown type: $type");
throw ArgumentError.value(type, "type");
@ -29,6 +33,8 @@ abstract class AlbumSortProvider with EquatableMixin {
return AlbumNullSortProvider._type;
} else if (this is AlbumTimeSortProvider) {
return AlbumTimeSortProvider._type;
} else if (this is AlbumFilenameSortProvider) {
return AlbumFilenameSortProvider._type;
} else {
throw StateError("Unknwon subtype");
}
@ -157,3 +163,54 @@ class AlbumTimeSortProvider extends AlbumReversibleSortProvider {
static const _type = "time";
}
/// Sort based on the name of the files
class AlbumFilenameSortProvider extends AlbumReversibleSortProvider {
const AlbumFilenameSortProvider({
required bool isAscending,
}) : super(isAscending: isAscending);
factory AlbumFilenameSortProvider.fromJson(JsonObj json) {
return AlbumFilenameSortProvider(
isAscending: json["isAscending"] ?? true,
);
}
@override
toString() => "$runtimeType {"
"super: ${super.toString()}, "
"}";
@override
sort(List<AlbumItem> items) {
String? prevFilename;
return items
.map((e) {
if (e is AlbumFileItem) {
// take the file name
prevFilename = e.file.filename;
}
// for non file items, use the sibling file's name
return Tuple2(prevFilename, e);
})
.stableSorted((x, y) {
if (x.item1 == null && y.item1 == null) {
return 0;
} else if (x.item1 == null) {
return -1;
} else if (y.item1 == null) {
return 1;
} else {
if (isAscending) {
return compareNatural(x.item1!, y.item1!);
} else {
return compareNatural(y.item1!, x.item1!);
}
}
})
.map((e) => e.item2)
.toList();
}
static const _type = "filename";
}

View file

@ -224,6 +224,24 @@ class AlbumUpgraderV6 implements AlbumUpgrader {
final String? logFilePath;
}
/// Upgrade v7 Album to v8
class AlbumUpgraderV7 implements AlbumUpgrader {
const AlbumUpgraderV7({
this.logFilePath,
});
@override
call(JsonObj json) {
_log.fine("[call] Upgrade v7 Album for file: $logFilePath");
return json;
}
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV7");
/// File path for logging only
final String? logFilePath;
}
abstract class AlbumUpgraderFactory {
const AlbumUpgraderFactory();
@ -233,6 +251,7 @@ abstract class AlbumUpgraderFactory {
AlbumUpgraderV4? buildV4();
AlbumUpgraderV5? buildV5();
AlbumUpgraderV6? buildV6();
AlbumUpgraderV7? buildV7();
}
class DefaultAlbumUpgraderFactory extends AlbumUpgraderFactory {
@ -264,6 +283,9 @@ class DefaultAlbumUpgraderFactory extends AlbumUpgraderFactory {
@override
buildV6() => AlbumUpgraderV6(logFilePath: logFilePath);
@override
buildV7() => AlbumUpgraderV7(logFilePath: logFilePath);
final Account account;
final File? albumFile;

View file

@ -607,6 +607,14 @@
"@sortOptionTimeDescendingLabel": {
"description": "Sort by time, in descending order"
},
"sortOptionFilenameAscendingLabel": "Filename",
"@sortOptionFilenameAscendingLabel": {
"description": "Sort by filename, in ascending order"
},
"sortOptionFilenameDescendingLabel": "Filename (descending)",
"@sortOptionFilenameDescendingLabel": {
"description": "Sort by filename, in descending order"
},
"sortOptionAlbumNameLabel": "Album name",
"@sortOptionAlbumNameLabel": {
"description": "Sort by album name, in ascending order"

View file

@ -27,6 +27,8 @@
"settingsExperimentalDescription",
"settingsExperimentalPageTitle",
"rootPickerSkipConfirmationDialogContent2",
"sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel",
"sortOptionManualLabel",
"helpButtonLabel",
"collectionSharingLabel",
@ -135,6 +137,8 @@
"settingsExperimentalPageTitle",
"rootPickerSkipConfirmationDialogContent2",
"timeSecondInputHint",
"sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel",
"sortOptionManualLabel",
"helpButtonLabel",
"slideshowTooltip",
@ -275,6 +279,8 @@
"captureLogSuccessNotification",
"rootPickerSkipConfirmationDialogContent2",
"timeSecondInputHint",
"sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel",
"sortOptionAlbumNameLabel",
"sortOptionAlbumNameDescendingLabel",
"sortOptionManualLabel",
@ -412,6 +418,8 @@
"settingsMiscellaneousPageTitle",
"settingsPhotosTabSortByNameTitle",
"rootPickerSkipConfirmationDialogContent2",
"sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel",
"helpButtonLabel",
"backgroundServiceStopping",
"metadataTaskPauseLowBatteryNotification",
@ -433,6 +441,8 @@
"settingsMiscellaneousTitle",
"settingsMiscellaneousPageTitle",
"settingsPhotosTabSortByNameTitle",
"sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel",
"enhanceSuperResolution4xTitle",
"enhanceStyleTransferTitle"
],
@ -446,6 +456,8 @@
"settingsMiscellaneousTitle",
"settingsMiscellaneousPageTitle",
"settingsPhotosTabSortByNameTitle",
"sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel",
"helpTooltip",
"helpButtonLabel",
"removeFromAlbumTooltip",
@ -471,6 +483,8 @@
"settingsMiscellaneousTitle",
"settingsMiscellaneousPageTitle",
"settingsPhotosTabSortByNameTitle",
"sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel",
"createCollectionTooltip",
"createCollectionDialogAlbumLabel",
"createCollectionDialogAlbumDescription",
@ -514,6 +528,8 @@
"settingsMiscellaneousTitle",
"settingsMiscellaneousPageTitle",
"settingsPhotosTabSortByNameTitle",
"sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel",
"enhanceTooltip",
"enhanceButtonLabel",
"enhanceIntroDialogTitle",
@ -536,6 +552,8 @@
"settingsMiscellaneousTitle",
"settingsMiscellaneousPageTitle",
"settingsPhotosTabSortByNameTitle",
"sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel",
"enhanceTooltip",
"enhanceButtonLabel",
"enhanceIntroDialogTitle",
@ -558,6 +576,8 @@
"settingsMiscellaneousTitle",
"settingsMiscellaneousPageTitle",
"settingsPhotosTabSortByNameTitle",
"sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel",
"enhanceTooltip",
"enhanceButtonLabel",
"enhanceIntroDialogTitle",
@ -580,6 +600,8 @@
"settingsMiscellaneousTitle",
"settingsMiscellaneousPageTitle",
"settingsPhotosTabSortByNameTitle",
"sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel",
"enhanceTooltip",
"enhanceButtonLabel",
"enhanceIntroDialogTitle",

View file

@ -575,6 +575,24 @@ class _AlbumBrowserState extends State<AlbumBrowser>
Navigator.of(context).pop();
},
),
FancyOptionPickerItem(
label: L10n.global().sortOptionFilenameAscendingLabel,
isSelected: sortProvider is AlbumFilenameSortProvider &&
sortProvider.isAscending,
onSelect: () {
_onEditSortFilenamePressed();
Navigator.of(context).pop();
},
),
FancyOptionPickerItem(
label: L10n.global().sortOptionFilenameDescendingLabel,
isSelected: sortProvider is AlbumFilenameSortProvider &&
!sortProvider.isAscending,
onSelect: () {
_onEditSortFilenameDescendingPressed();
Navigator.of(context).pop();
},
),
if (sortProvider is AlbumNullSortProvider)
FancyOptionPickerItem(
label: L10n.global().sortOptionManualLabel,
@ -606,6 +624,24 @@ class _AlbumBrowserState extends State<AlbumBrowser>
});
}
void _onEditSortFilenamePressed() {
_editAlbum = _editAlbum!.copyWith(
sortProvider: const AlbumFilenameSortProvider(isAscending: true),
);
setState(() {
_transformItems();
});
}
void _onEditSortFilenameDescendingPressed() {
_editAlbum = _editAlbum!.copyWith(
sortProvider: const AlbumFilenameSortProvider(isAscending: false),
);
setState(() {
_transformItems();
});
}
void _onEditAddTextPressed() {
showDialog<String>(
context: context,

View file

@ -499,6 +499,24 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
Navigator.of(context).pop();
},
),
FancyOptionPickerItem(
label: L10n.global().sortOptionFilenameAscendingLabel,
isSelected: sortProvider is AlbumFilenameSortProvider &&
sortProvider.isAscending,
onSelect: () {
_onEditSortFilenamePressed();
Navigator.of(context).pop();
},
),
FancyOptionPickerItem(
label: L10n.global().sortOptionFilenameDescendingLabel,
isSelected: sortProvider is AlbumFilenameSortProvider &&
!sortProvider.isAscending,
onSelect: () {
_onEditSortFilenameDescendingPressed();
Navigator.of(context).pop();
},
),
],
),
);
@ -522,6 +540,24 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
});
}
void _onEditSortFilenamePressed() {
_editAlbum = _editAlbum!.copyWith(
sortProvider: const AlbumFilenameSortProvider(isAscending: true),
);
setState(() {
_transformItems(_sortedItems);
});
}
void _onEditSortFilenameDescendingPressed() {
_editAlbum = _editAlbum!.copyWith(
sortProvider: const AlbumFilenameSortProvider(isAscending: false),
);
setState(() {
_transformItems(_sortedItems);
});
}
Future<void> _onAlbumUpdatedEvent(AlbumUpdatedEvent ev) async {
if (ev.album.albumFile!.path == _album?.albumFile?.path) {
final album = await _updateAlbumPostPopulate(ev.album, _sortedItems);

View file

@ -27,6 +27,19 @@ void main() {
test("head", _timeNonFileHead);
});
});
group("AlbumFilenameSortProvider", () {
group("AlbumFileItem", () {
test("ascending", _filenameFileAscending);
test("descending", _filenameFileDescending);
test("natural", _filenameFileNatural);
});
group("w/ non AlbumFileItem", () {
test("ascending", _filenameNonFileAscending);
test("descending", _filenameNonFileDescending);
test("head", _filenameNonFileHead);
});
});
}
void _timeFromJson() {
@ -217,3 +230,210 @@ void _timeNonFileHead() {
const sort = AlbumTimeSortProvider(isAscending: true);
expect(sort.sort(items), [items[0], items[2], items[1], items[3]]);
}
/// Sort files by filename
///
/// Expect: items sorted
void _filenameFileAscending() {
final items = (util.FilesBuilder()
..addJpeg(
"admin/test3.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 1),
)
..addJpeg(
"admin/test1.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 0),
)
..addJpeg(
"admin/test2.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 2),
))
.build()
.mapWithIndex((i, f) => AlbumFileItem(
addedBy: CiString("admin"),
addedAt: f.lastModified!,
file: f,
))
.toList();
const sort = AlbumFilenameSortProvider(isAscending: true);
expect(sort.sort(items), [items[1], items[2], items[0]]);
}
/// Sort files by filename, descending
///
/// Expect: items sorted
void _filenameFileDescending() {
final items = (util.FilesBuilder()
..addJpeg(
"admin/test3.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 1),
)
..addJpeg(
"admin/test1.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 0),
)
..addJpeg(
"admin/test2.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 2),
))
.build()
.mapWithIndex((i, f) => AlbumFileItem(
addedBy: CiString("admin"),
addedAt: f.lastModified!,
file: f,
))
.toList();
const sort = AlbumFilenameSortProvider(isAscending: false);
expect(sort.sort(items), [items[0], items[2], items[1]]);
}
/// Sort files by filename
///
/// Expect: items sorted in natural order
void _filenameFileNatural() {
final items = (util.FilesBuilder()
..addJpeg(
"admin/test033_2.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 1),
)
..addJpeg(
"admin/test033_1.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 1),
)
..addJpeg(
"admin/test033_3.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 1),
)
..addJpeg(
"admin/test11.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 0),
)
..addJpeg(
"admin/test2.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 2),
)
..addJpeg(
"admin/test2_999.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 1),
))
.build()
.mapWithIndex((i, f) => AlbumFileItem(
addedBy: CiString("admin"),
addedAt: f.lastModified!,
file: f,
))
.toList();
const sort = AlbumFilenameSortProvider(isAscending: true);
expect(
sort.sort(items),
[items[4], items[5], items[3], items[1], items[0], items[2]],
);
}
/// Sort files + non files by filename
///
/// Expect: file sorted, non file stick with the prev file
void _filenameNonFileAscending() {
final items = (util.FilesBuilder()
..addJpeg(
"admin/test3.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 1),
)
..addJpeg(
"admin/test1.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 0),
)
..addJpeg(
"admin/test2.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 2),
))
.build()
.mapWithIndex<AlbumItem>((i, f) => AlbumFileItem(
addedBy: CiString("admin"),
addedAt: f.lastModified!,
file: f,
))
.toList();
items.insert(
2,
AlbumLabelItem(
addedBy: CiString("admin"),
addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5),
text: "test",
),
);
const sort = AlbumFilenameSortProvider(isAscending: true);
expect(sort.sort(items), [items[1], items[2], items[3], items[0]]);
}
/// Sort files + non files by filename, descending
///
/// Expect: file sorted, non file stick with the prev file
void _filenameNonFileDescending() {
final items = (util.FilesBuilder()
..addJpeg(
"admin/test3.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 1),
)
..addJpeg(
"admin/test1.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 0),
)
..addJpeg(
"admin/test2.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 2),
))
.build()
.mapWithIndex<AlbumItem>((i, f) => AlbumFileItem(
addedBy: CiString("admin"),
addedAt: f.lastModified!,
file: f,
))
.toList();
items.insert(
2,
AlbumLabelItem(
addedBy: CiString("admin"),
addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5),
text: "test",
),
);
const sort = AlbumFilenameSortProvider(isAscending: false);
expect(sort.sort(items), [items[0], items[3], items[1], items[2]]);
}
/// Sort files + non files by filename, with the head being a non file
///
/// Expect: file sorted, non file stick at the head
void _filenameNonFileHead() {
final items = (util.FilesBuilder()
..addJpeg(
"admin/test3.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 1),
)
..addJpeg(
"admin/test1.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 0),
)
..addJpeg(
"admin/test2.jpg",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 2),
))
.build()
.mapWithIndex<AlbumItem>((i, f) => AlbumFileItem(
addedBy: CiString("admin"),
addedAt: f.lastModified!,
file: f,
))
.toList();
items.insert(
0,
AlbumLabelItem(
addedBy: CiString("admin"),
addedAt: DateTime.utc(2020, 1, 2, 3, 4, 5),
text: "test",
),
);
const sort = AlbumFilenameSortProvider(isAscending: true);
expect(sort.sort(items), [items[0], items[2], items[3], items[1]]);
}

View file

@ -292,6 +292,46 @@ void main() {
));
});
test("AlbumFilenameSortProvider", () {
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": "filename",
"content": <String, dynamic>{
"isAscending": true,
},
},
};
expect(
Album.fromJson(
json,
upgraderFactory: const _NullAlbumUpgraderFactory(),
),
Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
provider: AlbumStaticProvider(
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: const AlbumFilenameSortProvider(
isAscending: true,
),
));
});
test("shares", _fromJsonShares);
test("albumFile", () {
@ -580,6 +620,41 @@ void main() {
});
});
test("AlbumFilenameSortProvider", () {
final album = Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
provider: AlbumStaticProvider(
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: const AlbumFilenameSortProvider(
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": "filename",
"content": <String, dynamic>{
"isAscending": true,
},
},
});
});
test("shares", _toRemoteJsonShares);
});
@ -831,6 +906,41 @@ void main() {
});
});
test("AlbumFilenameSortProvider", () {
final album = Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
provider: AlbumStaticProvider(
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: const AlbumFilenameSortProvider(
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": "filename",
"content": <String, dynamic>{
"isAscending": true,
},
},
});
});
test("shares", _toAppDbJsonShares);
test("albumFile", () {
@ -1788,4 +1898,6 @@ class _NullAlbumUpgraderFactory extends AlbumUpgraderFactory {
buildV5() => null;
@override
buildV6() => null;
@override
buildV7() => null;
}