浏览代码

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 <alex.tran1502@gmail.com>
Alexander Groß 1 年之前
父节点
当前提交
e57c926676

+ 2 - 0
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.",

+ 31 - 0
mobile/lib/modules/album/views/library_page.dart

@@ -47,6 +47,7 @@ class LibraryPage extends HookConsumerWidget {
         useState(settings.getSetting(AppSettingsEnum.selectedAlbumSortOrder));
 
     List<Album> 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(

+ 13 - 0
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<User> owner = IsarLink<User>();
   final IsarLink<Asset> thumbnail = IsarLink<Asset>();
@@ -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);

+ 141 - 19
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<Type, List<int>> 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<P>(
     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;
+      return (reader.readStringOrNull(offset)) as P;
     case 3:
-      return (reader.readString(offset)) as P;
+      return (reader.readDateTime(offset)) as P;
     case 4:
-      return (reader.readStringOrNull(offset)) as P;
+      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<Album, Album, QFilterCondition> {
     });
   }
 
+  QueryBuilder<Album, Album, QAfterFilterCondition>
+      lastModifiedAssetTimestampIsNull() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(const FilterCondition.isNull(
+        property: r'lastModifiedAssetTimestamp',
+      ));
+    });
+  }
+
+  QueryBuilder<Album, Album, QAfterFilterCondition>
+      lastModifiedAssetTimestampIsNotNull() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(const FilterCondition.isNotNull(
+        property: r'lastModifiedAssetTimestamp',
+      ));
+    });
+  }
+
+  QueryBuilder<Album, Album, QAfterFilterCondition>
+      lastModifiedAssetTimestampEqualTo(DateTime? value) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.equalTo(
+        property: r'lastModifiedAssetTimestamp',
+        value: value,
+      ));
+    });
+  }
+
+  QueryBuilder<Album, Album, QAfterFilterCondition>
+      lastModifiedAssetTimestampGreaterThan(
+    DateTime? value, {
+    bool include = false,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.greaterThan(
+        include: include,
+        property: r'lastModifiedAssetTimestamp',
+        value: value,
+      ));
+    });
+  }
+
+  QueryBuilder<Album, Album, QAfterFilterCondition>
+      lastModifiedAssetTimestampLessThan(
+    DateTime? value, {
+    bool include = false,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.lessThan(
+        include: include,
+        property: r'lastModifiedAssetTimestamp',
+        value: value,
+      ));
+    });
+  }
+
+  QueryBuilder<Album, Album, QAfterFilterCondition>
+      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<Album, Album, QAfterFilterCondition> localIdIsNull() {
     return QueryBuilder.apply(this, (query) {
       return query.addFilterCondition(const FilterCondition.isNull(
@@ -1158,6 +1241,19 @@ extension AlbumQuerySortBy on QueryBuilder<Album, Album, QSortBy> {
     });
   }
 
+  QueryBuilder<Album, Album, QAfterSortBy> sortByLastModifiedAssetTimestamp() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.asc);
+    });
+  }
+
+  QueryBuilder<Album, Album, QAfterSortBy>
+      sortByLastModifiedAssetTimestampDesc() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.desc);
+    });
+  }
+
   QueryBuilder<Album, Album, QAfterSortBy> sortByLocalId() {
     return QueryBuilder.apply(this, (query) {
       return query.addSortBy(r'localId', Sort.asc);
@@ -1244,6 +1340,19 @@ extension AlbumQuerySortThenBy on QueryBuilder<Album, Album, QSortThenBy> {
     });
   }
 
+  QueryBuilder<Album, Album, QAfterSortBy> thenByLastModifiedAssetTimestamp() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.asc);
+    });
+  }
+
+  QueryBuilder<Album, Album, QAfterSortBy>
+      thenByLastModifiedAssetTimestampDesc() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.desc);
+    });
+  }
+
   QueryBuilder<Album, Album, QAfterSortBy> thenByLocalId() {
     return QueryBuilder.apply(this, (query) {
       return query.addSortBy(r'localId', Sort.asc);
@@ -1312,6 +1421,12 @@ extension AlbumQueryWhereDistinct on QueryBuilder<Album, Album, QDistinct> {
     });
   }
 
+  QueryBuilder<Album, Album, QDistinct> distinctByLastModifiedAssetTimestamp() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addDistinctBy(r'lastModifiedAssetTimestamp');
+    });
+  }
+
   QueryBuilder<Album, Album, QDistinct> distinctByLocalId(
       {bool caseSensitive = true}) {
     return QueryBuilder.apply(this, (query) {
@@ -1359,6 +1474,13 @@ extension AlbumQueryProperty on QueryBuilder<Album, Album, QQueryProperty> {
     });
   }
 
+  QueryBuilder<Album, DateTime?, QQueryOperations>
+      lastModifiedAssetTimestampProperty() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addPropertyName(r'lastModifiedAssetTimestamp');
+    });
+  }
+
   QueryBuilder<Album, String?, QQueryOperations> localIdProperty() {
     return QueryBuilder.apply(this, (query) {
       return query.addPropertyName(r'localId');

+ 13 - 1
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!));
 }