From e57c9266764e9217eed00e1b3f875d07c4cf36af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Gro=C3=9F?= Date: Thu, 12 Oct 2023 20:18:54 +0200 Subject: [PATCH] feat(mobile): offer the same album sorting options on mobile as on web (#3804) * Add translations for new album sort options * Support additional album sort options like on web * Update generated code * Fix lint --------- Co-authored-by: Alex --- mobile/assets/i18n/en-US.json | 2 + .../lib/modules/album/views/library_page.dart | 31 ++++ mobile/lib/shared/models/album.dart | 13 ++ mobile/lib/shared/models/album.g.dart | 162 +++++++++++++++--- mobile/lib/shared/services/sync.service.dart | 14 +- 5 files changed, 201 insertions(+), 21 deletions(-) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 0fd20a79bd..e83e1e6677 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -173,6 +173,8 @@ "library_page_sharing": "Sharing", "library_page_sort_created": "Most recently created", "library_page_sort_title": "Album title", + "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_last_modified": "Last modified", "login_disabled": "Login has been disabled", "login_form_api_exception": "API exception. Please check the server URL and try again.", "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", diff --git a/mobile/lib/modules/album/views/library_page.dart b/mobile/lib/modules/album/views/library_page.dart index c22129a501..0eb44b82c5 100644 --- a/mobile/lib/modules/album/views/library_page.dart +++ b/mobile/lib/modules/album/views/library_page.dart @@ -47,6 +47,7 @@ class LibraryPage extends HookConsumerWidget { useState(settings.getSetting(AppSettingsEnum.selectedAlbumSortOrder)); List sortedAlbums() { + // Created. if (selectedAlbumSortOrder.value == 0) { return albums .where((a) => a.isRemote) @@ -54,6 +55,34 @@ class LibraryPage extends HookConsumerWidget { .reversed .toList(); } + // Album title. + if (selectedAlbumSortOrder.value == 1) { + return albums.where((a) => a.isRemote).sortedBy((album) => album.name); + } + // Most recent photo, if unset (e.g. empty album, use modifiedAt / updatedAt). + if (selectedAlbumSortOrder.value == 2) { + return albums + .where((a) => a.isRemote) + .sorted( + (a, b) => a.lastModifiedAssetTimestamp != null && + b.lastModifiedAssetTimestamp != null + ? a.lastModifiedAssetTimestamp! + .compareTo(b.lastModifiedAssetTimestamp!) + : a.modifiedAt.compareTo(b.modifiedAt), + ) + .reversed + .toList(); + } + // Last modified. + if (selectedAlbumSortOrder.value == 3) { + return albums + .where((a) => a.isRemote) + .sortedBy((album) => album.modifiedAt) + .reversed + .toList(); + } + + // Fallback: Album title. return albums.where((a) => a.isRemote).sortedBy((album) => album.name); } @@ -61,6 +90,8 @@ class LibraryPage extends HookConsumerWidget { final options = [ "library_page_sort_created".tr(), "library_page_sort_title".tr(), + "library_page_sort_most_recent_photo".tr(), + "library_page_sort_last_modified".tr(), ]; return PopupMenuButton( diff --git a/mobile/lib/shared/models/album.dart b/mobile/lib/shared/models/album.dart index 486647ad1b..94afe1d76d 100644 --- a/mobile/lib/shared/models/album.dart +++ b/mobile/lib/shared/models/album.dart @@ -18,6 +18,7 @@ class Album { required this.name, required this.createdAt, required this.modifiedAt, + this.lastModifiedAssetTimestamp, required this.shared, }); @@ -29,6 +30,7 @@ class Album { String name; DateTime createdAt; DateTime modifiedAt; + DateTime? lastModifiedAssetTimestamp; bool shared; final IsarLink owner = IsarLink(); final IsarLink thumbnail = IsarLink(); @@ -83,12 +85,21 @@ class Album { @override bool operator ==(other) { if (other is! Album) return false; + + final lastModifiedAssetTimestampIsSetAndEqual = + lastModifiedAssetTimestamp != null && + other.lastModifiedAssetTimestamp != null + ? lastModifiedAssetTimestamp! + .isAtSameMomentAs(other.lastModifiedAssetTimestamp!) + : true; + return id == other.id && remoteId == other.remoteId && localId == other.localId && name == other.name && createdAt.isAtSameMomentAs(other.createdAt) && modifiedAt.isAtSameMomentAs(other.modifiedAt) && + lastModifiedAssetTimestampIsSetAndEqual && shared == other.shared && owner.value == other.owner.value && thumbnail.value == other.thumbnail.value && @@ -105,6 +116,7 @@ class Album { name.hashCode ^ createdAt.hashCode ^ modifiedAt.hashCode ^ + lastModifiedAssetTimestamp.hashCode ^ shared.hashCode ^ owner.value.hashCode ^ thumbnail.value.hashCode ^ @@ -130,6 +142,7 @@ class Album { name: dto.albumName, createdAt: dto.createdAt, modifiedAt: dto.updatedAt, + lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp, shared: dto.shared, ); a.owner.value = await db.users.getById(dto.ownerId); diff --git a/mobile/lib/shared/models/album.g.dart b/mobile/lib/shared/models/album.g.dart index 8d24155657..63f71f380f 100644 --- a/mobile/lib/shared/models/album.g.dart +++ b/mobile/lib/shared/models/album.g.dart @@ -22,28 +22,33 @@ const AlbumSchema = CollectionSchema( name: r'createdAt', type: IsarType.dateTime, ), - r'localId': PropertySchema( + r'lastModifiedAssetTimestamp': PropertySchema( id: 1, + name: r'lastModifiedAssetTimestamp', + type: IsarType.dateTime, + ), + r'localId': PropertySchema( + id: 2, name: r'localId', type: IsarType.string, ), r'modifiedAt': PropertySchema( - id: 2, + id: 3, name: r'modifiedAt', type: IsarType.dateTime, ), r'name': PropertySchema( - id: 3, + id: 4, name: r'name', type: IsarType.string, ), r'remoteId': PropertySchema( - id: 4, + id: 5, name: r'remoteId', type: IsarType.string, ), r'shared': PropertySchema( - id: 5, + id: 6, name: r'shared', type: IsarType.bool, ) @@ -143,11 +148,12 @@ void _albumSerialize( Map> allOffsets, ) { writer.writeDateTime(offsets[0], object.createdAt); - writer.writeString(offsets[1], object.localId); - writer.writeDateTime(offsets[2], object.modifiedAt); - writer.writeString(offsets[3], object.name); - writer.writeString(offsets[4], object.remoteId); - writer.writeBool(offsets[5], object.shared); + writer.writeDateTime(offsets[1], object.lastModifiedAssetTimestamp); + writer.writeString(offsets[2], object.localId); + writer.writeDateTime(offsets[3], object.modifiedAt); + writer.writeString(offsets[4], object.name); + writer.writeString(offsets[5], object.remoteId); + writer.writeBool(offsets[6], object.shared); } Album _albumDeserialize( @@ -158,11 +164,12 @@ Album _albumDeserialize( ) { final object = Album( createdAt: reader.readDateTime(offsets[0]), - localId: reader.readStringOrNull(offsets[1]), - modifiedAt: reader.readDateTime(offsets[2]), - name: reader.readString(offsets[3]), - remoteId: reader.readStringOrNull(offsets[4]), - shared: reader.readBool(offsets[5]), + lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[1]), + localId: reader.readStringOrNull(offsets[2]), + modifiedAt: reader.readDateTime(offsets[3]), + name: reader.readString(offsets[4]), + remoteId: reader.readStringOrNull(offsets[5]), + shared: reader.readBool(offsets[6]), ); object.id = id; return object; @@ -178,14 +185,16 @@ P _albumDeserializeProp

( case 0: return (reader.readDateTime(offset)) as P; case 1: - return (reader.readStringOrNull(offset)) as P; + return (reader.readDateTimeOrNull(offset)) as P; case 2: - return (reader.readDateTime(offset)) as P; - case 3: - return (reader.readString(offset)) as P; - case 4: return (reader.readStringOrNull(offset)) as P; + case 3: + return (reader.readDateTime(offset)) as P; + case 4: + return (reader.readString(offset)) as P; case 5: + return (reader.readStringOrNull(offset)) as P; + case 6: return (reader.readBool(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -520,6 +529,80 @@ extension AlbumQueryFilter on QueryBuilder { }); } + QueryBuilder + lastModifiedAssetTimestampIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'lastModifiedAssetTimestamp', + )); + }); + } + + QueryBuilder + lastModifiedAssetTimestampIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'lastModifiedAssetTimestamp', + )); + }); + } + + QueryBuilder + lastModifiedAssetTimestampEqualTo(DateTime? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'lastModifiedAssetTimestamp', + value: value, + )); + }); + } + + QueryBuilder + lastModifiedAssetTimestampGreaterThan( + DateTime? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'lastModifiedAssetTimestamp', + value: value, + )); + }); + } + + QueryBuilder + lastModifiedAssetTimestampLessThan( + DateTime? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'lastModifiedAssetTimestamp', + value: value, + )); + }); + } + + QueryBuilder + lastModifiedAssetTimestampBetween( + DateTime? lower, + DateTime? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'lastModifiedAssetTimestamp', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + QueryBuilder localIdIsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNull( @@ -1158,6 +1241,19 @@ extension AlbumQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByLastModifiedAssetTimestamp() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.asc); + }); + } + + QueryBuilder + sortByLastModifiedAssetTimestampDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.desc); + }); + } + QueryBuilder sortByLocalId() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'localId', Sort.asc); @@ -1244,6 +1340,19 @@ extension AlbumQuerySortThenBy on QueryBuilder { }); } + QueryBuilder thenByLastModifiedAssetTimestamp() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.asc); + }); + } + + QueryBuilder + thenByLastModifiedAssetTimestampDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.desc); + }); + } + QueryBuilder thenByLocalId() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'localId', Sort.asc); @@ -1312,6 +1421,12 @@ extension AlbumQueryWhereDistinct on QueryBuilder { }); } + QueryBuilder distinctByLastModifiedAssetTimestamp() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'lastModifiedAssetTimestamp'); + }); + } + QueryBuilder distinctByLocalId( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -1359,6 +1474,13 @@ extension AlbumQueryProperty on QueryBuilder { }); } + QueryBuilder + lastModifiedAssetTimestampProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'lastModifiedAssetTimestamp'); + }); + } + QueryBuilder localIdProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'localId'); diff --git a/mobile/lib/shared/services/sync.service.dart b/mobile/lib/shared/services/sync.service.dart index a11bb8eefc..d12a68ffde 100644 --- a/mobile/lib/shared/services/sync.service.dart +++ b/mobile/lib/shared/services/sync.service.dart @@ -282,6 +282,9 @@ class SyncService { if (!_hasAlbumResponseDtoChanged(dto, album)) { return false; } + // loadDetails (/api/album/:id) will not include lastModifiedAssetTimestamp, + // i.e. it will always be null. Save it here. + final originalDto = dto; dto = await loadDetails(dto); if (dto.assetCount != dto.assets.length) { return false; @@ -321,6 +324,7 @@ class SyncService { album.name = dto.albumName; album.shared = dto.shared; album.modifiedAt = dto.updatedAt; + album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp; if (album.thumbnail.value?.remoteId != dto.albumThumbnailAssetId) { album.thumbnail.value = await _db.assets .where() @@ -808,5 +812,13 @@ bool _hasAlbumResponseDtoChanged(AlbumResponseDto dto, Album a) { dto.albumThumbnailAssetId != a.thumbnail.value?.remoteId || dto.shared != a.shared || dto.sharedUsers.length != a.sharedUsers.length || - !dto.updatedAt.isAtSameMomentAs(a.modifiedAt); + !dto.updatedAt.isAtSameMomentAs(a.modifiedAt) || + (dto.lastModifiedAssetTimestamp == null && + a.lastModifiedAssetTimestamp != null) || + (dto.lastModifiedAssetTimestamp != null && + a.lastModifiedAssetTimestamp == null) || + (dto.lastModifiedAssetTimestamp != null && + a.lastModifiedAssetTimestamp != null && + !dto.lastModifiedAssetTimestamp! + .isAtSameMomentAs(a.lastModifiedAssetTimestamp!)); }