소스 검색

refactor: migrate album sort option to provider

shalong-tanwen 1 년 전
부모
커밋
a753832c04

+ 3 - 1
mobile/assets/i18n/en-US.json

@@ -196,9 +196,11 @@
   "library_page_favorites": "Favorites",
   "library_page_new_album": "New album",
   "library_page_sharing": "Sharing",
-  "library_page_sort_created": "Most recently created",
+  "library_page_sort_created": "Created date",
   "library_page_sort_last_modified": "Last modified",
   "library_page_sort_most_recent_photo": "Most recent photo",
+  "library_page_sort_most_oldest_photo": "Oldest photo",
+  "library_page_sort_asset_count": "Number of assets",
   "library_page_sort_title": "Album title",
   "login_disabled": "Login has been disabled",
   "login_form_api_exception": "API exception. Please check the server URL and try again.",

+ 134 - 0
mobile/lib/modules/album/providers/album_sort_options.provider.dart

@@ -0,0 +1,134 @@
+import 'package:collection/collection.dart';
+import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
+import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
+import 'package:immich_mobile/shared/models/album.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+part 'album_sort_options.provider.g.dart';
+
+typedef AlbumSortFn = List<Album> Function(List<Album> albums, bool isReverse);
+
+class _AlbumSortHandlers {
+  const _AlbumSortHandlers._();
+
+  static const AlbumSortFn created = _sortByCreated;
+  static List<Album> _sortByCreated(List<Album> albums, bool isReverse) {
+    final sorted =
+        albums.where((a) => a.isRemote).sortedBy((album) => album.createdAt);
+    return (isReverse ? sorted.reversed : sorted).toList();
+  }
+
+  static const AlbumSortFn title = _sortByTitle;
+  static List<Album> _sortByTitle(List<Album> albums, bool isReverse) {
+    final sorted =
+        albums.where((a) => a.isRemote).sortedBy((album) => album.name);
+    return (isReverse ? sorted.reversed : sorted).toList();
+  }
+
+  static const AlbumSortFn lastModified = _sortByLastModified;
+  static List<Album> _sortByLastModified(List<Album> albums, bool isReverse) {
+    final sorted =
+        albums.where((a) => a.isRemote).sortedBy((album) => album.modifiedAt);
+    return (isReverse ? sorted.reversed : sorted).toList();
+  }
+
+  static const AlbumSortFn assetCount = _sortByAssetCount;
+  static List<Album> _sortByAssetCount(List<Album> albums, bool isReverse) {
+    final sorted = albums
+        .where((a) => a.isRemote)
+        .sorted((a, b) => a.assetCount.compareTo(b.assetCount));
+    return (isReverse ? sorted.reversed : sorted).toList();
+  }
+
+  static const AlbumSortFn mostRecent = _sortByMostRecent;
+  static List<Album> _sortByMostRecent(List<Album> albums, bool isReverse) {
+    final sorted = albums
+        .where((a) => a.isRemote)
+        .sortedBy((album) => album.endDate ?? DateTime(1000))
+        .sorted((a, b) {
+      if (a.endDate == null) return 1;
+      if (b.endDate == null) return -1;
+      return 0;
+    });
+    return (isReverse ? sorted.reversed : sorted).toList();
+  }
+
+  static const AlbumSortFn mostOldest = _sortByMostOldest;
+  static List<Album> _sortByMostOldest(List<Album> albums, bool isReverse) {
+    final sorted = albums
+        .where((a) => a.isRemote)
+        .sortedBy((album) => album.startDate ?? DateTime.now())
+        .sorted((a, b) {
+      if (a.startDate == null) return 1;
+      if (b.startDate == null) return -1;
+      return 0;
+    });
+    return (isReverse ? sorted.reversed : sorted).toList();
+  }
+}
+
+// Store index allows us to re-arrange the values without affecting the saved prefs
+enum AlbumSortMode {
+  title(1, "library_page_sort_title", _AlbumSortHandlers.title),
+  assetCount(4, "library_page_sort_asset_count", _AlbumSortHandlers.assetCount),
+  lastModified(
+    3,
+    "library_page_sort_last_modified",
+    _AlbumSortHandlers.lastModified,
+  ),
+  created(0, "library_page_sort_created", _AlbumSortHandlers.created),
+  mostRecent(
+    2,
+    "library_page_sort_most_recent_photo",
+    _AlbumSortHandlers.mostRecent,
+  ),
+  mostOldest(
+    5,
+    "library_page_sort_most_oldest_photo",
+    _AlbumSortHandlers.mostOldest,
+  );
+
+  final int storeIndex;
+  final String label;
+  final AlbumSortFn sortFn;
+
+  const AlbumSortMode(this.storeIndex, this.label, this.sortFn);
+}
+
+@riverpod
+class AlbumSortFunction extends _$AlbumSortFunction {
+  @override
+  AlbumSortMode build() {
+    final sortOpt = ref
+        .watch(appSettingsServiceProvider)
+        .getSetting(AppSettingsEnum.selectedAlbumSortOrder);
+    return sortOpt < AlbumSortMode.values.length && sortOpt >= 0
+        ? AlbumSortMode.values.firstWhere((e) => e.index == sortOpt)
+        : AlbumSortMode.title;
+  }
+
+  void changeSortMode(AlbumSortMode sortOption) {
+    state = sortOption;
+    ref.watch(appSettingsServiceProvider).setSetting(
+          AppSettingsEnum.selectedAlbumSortOrder,
+          sortOption.storeIndex,
+        );
+  }
+}
+
+@riverpod
+class AlbumSortOrder extends _$AlbumSortOrder {
+  @override
+  bool build() {
+    return ref
+        .watch(appSettingsServiceProvider)
+        .getSetting(AppSettingsEnum.selectedAlbumSortReverse);
+  }
+
+  void changeSortDirection(bool isReverse) {
+    state = isReverse;
+    ref
+        .watch(appSettingsServiceProvider)
+        .setSetting(AppSettingsEnum.selectedAlbumSortReverse, isReverse);
+  }
+}

+ 42 - 0
mobile/lib/modules/album/providers/album_sort_options.provider.g.dart

@@ -0,0 +1,42 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'album_sort_options.provider.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$albumSortFunctionHash() => r'19a199c30181c19bb8f834f3105b27e6b0fb92f4';
+
+/// See also [AlbumSortFunction].
+@ProviderFor(AlbumSortFunction)
+final albumSortFunctionProvider =
+    AutoDisposeNotifierProvider<AlbumSortFunction, AlbumSortMode>.internal(
+  AlbumSortFunction.new,
+  name: r'albumSortFunctionProvider',
+  debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+      ? null
+      : _$albumSortFunctionHash,
+  dependencies: null,
+  allTransitiveDependencies: null,
+);
+
+typedef _$AlbumSortFunction = AutoDisposeNotifier<AlbumSortMode>;
+String _$albumSortOrderHash() => r'573dea45b4519e69386fc7104c72522e35713440';
+
+/// See also [AlbumSortOrder].
+@ProviderFor(AlbumSortOrder)
+final albumSortOrderProvider =
+    AutoDisposeNotifierProvider<AlbumSortOrder, bool>.internal(
+  AlbumSortOrder.new,
+  name: r'albumSortOrderProvider',
+  debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+      ? null
+      : _$albumSortOrderHash,
+  dependencies: null,
+  allTransitiveDependencies: null,
+);
+
+typedef _$AlbumSortOrder = AutoDisposeNotifier<bool>;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

+ 34 - 74
mobile/lib/modules/album/views/library_page.dart

@@ -1,15 +1,12 @@
-import 'package:collection/collection.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/providers/album.provider.dart';
+import 'package:immich_mobile/modules/album/providers/album_sort_options.provider.dart';
 import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
 import 'package:immich_mobile/routing/router.dart';
-import 'package:immich_mobile/shared/models/album.dart';
-import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
-import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
 
@@ -21,8 +18,9 @@ class LibraryPage extends HookConsumerWidget {
     final trashEnabled =
         ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
     final albums = ref.watch(albumProvider);
-    var isDarkTheme = context.isDarkTheme;
-    var settings = ref.watch(appSettingsServiceProvider);
+    final isDarkTheme = context.isDarkTheme;
+    final albumSortOption = ref.watch(albumSortFunctionProvider);
+    final albumSortIsReverse = ref.watch(albumSortOrderProvider);
 
     useEffect(
       () {
@@ -32,64 +30,15 @@ class LibraryPage extends HookConsumerWidget {
       [],
     );
 
-    final selectedAlbumSortOrder =
-        useState(settings.getSetting(AppSettingsEnum.selectedAlbumSortOrder));
-
-    List<Album> sortedAlbums() {
-      // Created.
-      if (selectedAlbumSortOrder.value == 0) {
-        return albums
-            .where((a) => a.isRemote)
-            .sortedBy((album) => album.createdAt)
-            .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);
-    }
-
     Widget buildSortButton() {
-      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(
         position: PopupMenuPosition.over,
         itemBuilder: (BuildContext context) {
-          return options.mapIndexed<PopupMenuEntry<int>>((index, option) {
-            final selected = selectedAlbumSortOrder.value == index;
+          return AlbumSortMode.values
+              .map<PopupMenuEntry<AlbumSortMode>>((option) {
+            final selected = albumSortOption == option;
             return PopupMenuItem(
-              value: index,
+              value: option,
               child: Row(
                 children: [
                   Padding(
@@ -101,7 +50,7 @@ class LibraryPage extends HookConsumerWidget {
                     ),
                   ),
                   Text(
-                    option,
+                    option.label.tr(),
                     style: TextStyle(
                       color: selected ? context.primaryColor : null,
                       fontSize: 12.0,
@@ -112,19 +61,31 @@ class LibraryPage extends HookConsumerWidget {
             );
           }).toList();
         },
-        onSelected: (int value) {
-          selectedAlbumSortOrder.value = value;
-          settings.setSetting(AppSettingsEnum.selectedAlbumSortOrder, value);
+        onSelected: (AlbumSortMode value) {
+          final selected = albumSortOption == value;
+          // Switch direction
+          if (selected) {
+            ref
+                .read(albumSortOrderProvider.notifier)
+                .changeSortDirection(!albumSortIsReverse);
+          } else {
+            ref.read(albumSortFunctionProvider.notifier).changeSortMode(value);
+          }
         },
         child: Row(
           children: [
-            Icon(
-              Icons.swap_vert_rounded,
-              size: 18,
-              color: context.primaryColor,
+            Padding(
+              padding: const EdgeInsets.only(right: 5),
+              child: Icon(
+                albumSortIsReverse
+                    ? Icons.arrow_downward_rounded
+                    : Icons.arrow_upward_rounded,
+                size: 14,
+                color: context.primaryColor,
+              ),
             ),
             Text(
-              options[selectedAlbumSortOrder.value],
+              albumSortOption.label.tr(),
               style: context.textTheme.labelLarge?.copyWith(
                 color: context.primaryColor,
               ),
@@ -140,9 +101,8 @@ class LibraryPage extends HookConsumerWidget {
           var cardSize = constraints.maxWidth;
 
           return GestureDetector(
-            onTap: () {
-              context.autoPush(CreateAlbumRoute(isSharedAlbum: false));
-            },
+            onTap: () =>
+                context.autoPush(CreateAlbumRoute(isSharedAlbum: false)),
             child: Padding(
               padding:
                   const EdgeInsets.only(bottom: 32), // Adjust padding to suit
@@ -160,7 +120,7 @@ class LibraryPage extends HookConsumerWidget {
                             : const Color.fromARGB(255, 203, 203, 203),
                       ),
                       color: isDarkTheme ? Colors.grey[900] : Colors.grey[50],
-                      borderRadius: BorderRadius.circular(20),
+                      borderRadius: const BorderRadius.all(Radius.circular(20)),
                     ),
                     child: Center(
                       child: Icon(
@@ -223,7 +183,7 @@ class LibraryPage extends HookConsumerWidget {
       );
     }
 
-    final sorted = sortedAlbums();
+    final sorted = albumSortOption.sortFn(albums, albumSortIsReverse);
 
     final local = albums.where((a) => a.isLocal).toList();
 
@@ -231,7 +191,7 @@ class LibraryPage extends HookConsumerWidget {
       return trashEnabled
           ? InkWell(
               onTap: () => context.autoPush(const TrashRoute()),
-              borderRadius: BorderRadius.circular(12),
+              borderRadius: const BorderRadius.all(Radius.circular(12)),
               child: const Icon(
                 Icons.delete_rounded,
                 size: 25,

+ 5 - 0
mobile/lib/modules/settings/services/app_settings.service.dart

@@ -51,6 +51,11 @@ enum AppSettingsEnum<T> {
   mapIncludeArchived<bool>(StoreKey.mapIncludeArchived, null, false),
   mapRelativeDate<int>(StoreKey.mapRelativeDate, null, 0),
   allowSelfSignedSSLCert<bool>(StoreKey.selfSignedCert, null, false),
+  selectedAlbumSortReverse<bool>(
+    StoreKey.selectedAlbumSortReverse,
+    null,
+    false,
+  ),
   ;
 
   const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);

+ 1 - 0
mobile/lib/shared/models/store.dart

@@ -182,6 +182,7 @@ enum StoreKey<T> {
   mapRelativeDate<int>(119, type: int),
   selfSignedCert<bool>(120, type: bool),
   mapIncludeArchived<bool>(121, type: bool),
+  selectedAlbumSortReverse<bool>(122, type: bool),
   ;
 
   const StoreKey(