diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index d8404db409..a18644cd2a 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -71,6 +71,7 @@ enum StoreKey { readonlyModeEnabled._(138), autoPlayVideo._(139), + albumGridView._(140), // Experimental stuff photoManagerCustomFilter._(1000), diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart index 67e91188e2..68c72255b0 100644 --- a/mobile/lib/domain/services/remote_album.service.dart +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; class RemoteAlbumService { final DriftRemoteAlbumRepository _repository; @@ -32,16 +33,16 @@ class RemoteAlbumService { Future> sortAlbums( List albums, - RemoteAlbumSortMode sortMode, { + AlbumSortMode sortMode, { bool isReverse = false, }) async { final List sorted = switch (sortMode) { - RemoteAlbumSortMode.created => albums.sortedBy((album) => album.createdAt), - RemoteAlbumSortMode.title => albums.sortedBy((album) => album.name), - RemoteAlbumSortMode.lastModified => albums.sortedBy((album) => album.updatedAt), - RemoteAlbumSortMode.assetCount => albums.sortedBy((album) => album.assetCount), - RemoteAlbumSortMode.mostRecent => await _sortByNewestAsset(albums), - RemoteAlbumSortMode.mostOldest => await _sortByOldestAsset(albums), + AlbumSortMode.created => albums.sortedBy((album) => album.createdAt), + AlbumSortMode.title => albums.sortedBy((album) => album.name), + AlbumSortMode.lastModified => albums.sortedBy((album) => album.updatedAt), + AlbumSortMode.assetCount => albums.sortedBy((album) => album.assetCount), + AlbumSortMode.mostRecent => await _sortByNewestAsset(albums), + AlbumSortMode.mostOldest => await _sortByOldestAsset(albums), }; return (isReverse ? sorted.reversed : sorted).toList(); @@ -211,16 +212,3 @@ class RemoteAlbumService { return sorted.reversed.toList(); } } - -enum RemoteAlbumSortMode { - title("library_page_sort_title"), - assetCount("library_page_sort_asset_count"), - lastModified("library_page_sort_last_modified"), - created("library_page_sort_created"), - mostRecent("sort_newest"), - mostOldest("sort_oldest"); - - final String key; - - const RemoteAlbumSortMode(this.key); -} diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index 0d5b9a7636..4110966e57 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -7,7 +7,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; @@ -17,6 +16,9 @@ import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart' import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/album_filter.utils.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; @@ -45,14 +47,28 @@ class _AlbumSelectorState extends ConsumerState { List shownAlbums = []; AlbumFilter filter = AlbumFilter(query: "", mode: QuickFilterMode.all); - AlbumSort sort = AlbumSort(mode: RemoteAlbumSortMode.lastModified, isReverse: true); + AlbumSort sort = AlbumSort(mode: AlbumSortMode.lastModified, isReverse: true); @override void initState() { super.initState(); - // Load albums when component mounts WidgetsBinding.instance.addPostFrameCallback((_) { + final appSettings = ref.read(appSettingsServiceProvider); + final savedSortMode = appSettings.getSetting(AppSettingsEnum.selectedAlbumSortOrder); + final savedIsReverse = appSettings.getSetting(AppSettingsEnum.selectedAlbumSortReverse); + final savedIsGrid = appSettings.getSetting(AppSettingsEnum.albumGridView); + + final albumSortMode = AlbumSortMode.values.firstWhere( + (e) => e.storeIndex == savedSortMode, + orElse: () => AlbumSortMode.lastModified, + ); + + setState(() { + sort = AlbumSort(mode: albumSortMode, isReverse: savedIsReverse); + isGrid = savedIsGrid; + }); + ref.read(remoteAlbumProvider.notifier).refresh(); }); @@ -82,6 +98,7 @@ class _AlbumSelectorState extends ConsumerState { setState(() { isGrid = !isGrid; }); + ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.albumGridView, isGrid); } void changeFilter(QuickFilterMode mode) { @@ -97,6 +114,10 @@ class _AlbumSelectorState extends ConsumerState { this.sort = sort; }); + final appSettings = ref.read(appSettingsServiceProvider); + await appSettings.setSetting(AppSettingsEnum.selectedAlbumSortOrder, sort.mode.storeIndex); + await appSettings.setSetting(AppSettingsEnum.selectedAlbumSortReverse, sort.isReverse); + await sortAlbums(); } @@ -181,6 +202,8 @@ class _AlbumSelectorState extends ConsumerState { onToggleViewMode: toggleViewMode, onSortChanged: changeSort, controller: menuController, + currentSortMode: sort.mode, + currentIsReverse: sort.isReverse, ), isGrid ? _AlbumGrid(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected) @@ -192,21 +215,46 @@ class _AlbumSelectorState extends ConsumerState { } class _SortButton extends ConsumerStatefulWidget { - const _SortButton(this.onSortChanged, {this.controller}); + const _SortButton( + this.onSortChanged, { + required this.initialSortMode, + required this.initialIsReverse, + this.controller, + }); final Future Function(AlbumSort) onSortChanged; final MenuController? controller; + final AlbumSortMode initialSortMode; + final bool initialIsReverse; @override ConsumerState<_SortButton> createState() => _SortButtonState(); } class _SortButtonState extends ConsumerState<_SortButton> { - RemoteAlbumSortMode albumSortOption = RemoteAlbumSortMode.lastModified; - bool albumSortIsReverse = true; + late AlbumSortMode albumSortOption; + late bool albumSortIsReverse; bool isSorting = false; - Future onMenuTapped(RemoteAlbumSortMode sortMode) async { + @override + void initState() { + super.initState(); + albumSortOption = widget.initialSortMode; + albumSortIsReverse = widget.initialIsReverse; + } + + @override + void didUpdateWidget(_SortButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.initialSortMode != widget.initialSortMode || oldWidget.initialIsReverse != widget.initialIsReverse) { + setState(() { + albumSortOption = widget.initialSortMode; + albumSortIsReverse = widget.initialIsReverse; + }); + } + } + + Future onMenuTapped(AlbumSortMode sortMode) async { final selected = albumSortOption == sortMode; // Switch direction if (selected) { @@ -240,7 +288,7 @@ class _SortButtonState extends ConsumerState<_SortButton> { padding: const WidgetStatePropertyAll(EdgeInsets.all(4)), ), consumeOutsideTap: true, - menuChildren: RemoteAlbumSortMode.values + menuChildren: AlbumSortMode.values .map( (sortMode) => MenuItemButton( leadingIcon: albumSortOption == sortMode @@ -269,7 +317,7 @@ class _SortButtonState extends ConsumerState<_SortButton> { ), ), child: Text( - sortMode.key.t(context: context), + sortMode.label.t(context: context), style: context.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, color: albumSortOption == sortMode @@ -298,7 +346,7 @@ class _SortButtonState extends ConsumerState<_SortButton> { : const Icon(Icons.keyboard_arrow_up_rounded), ), Text( - albumSortOption.key.t(context: context), + albumSortOption.label.t(context: context), style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, color: context.colorScheme.onSurface.withAlpha(225), @@ -465,6 +513,8 @@ class _QuickSortAndViewMode extends StatelessWidget { required this.isGrid, required this.onToggleViewMode, required this.onSortChanged, + required this.currentSortMode, + required this.currentIsReverse, this.controller, }); @@ -472,6 +522,8 @@ class _QuickSortAndViewMode extends StatelessWidget { final VoidCallback onToggleViewMode; final MenuController? controller; final Future Function(AlbumSort) onSortChanged; + final AlbumSortMode currentSortMode; + final bool currentIsReverse; @override Widget build(BuildContext context) { @@ -481,7 +533,12 @@ class _QuickSortAndViewMode extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _SortButton(onSortChanged, controller: controller), + _SortButton( + onSortChanged, + controller: controller, + initialSortMode: currentSortMode, + initialIsReverse: currentIsReverse, + ), IconButton( icon: Icon(isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24), onPressed: onToggleViewMode, diff --git a/mobile/lib/providers/infrastructure/remote_album.provider.dart b/mobile/lib/providers/infrastructure/remote_album.provider.dart index 38ba52dc56..e3cffeb093 100644 --- a/mobile/lib/providers/infrastructure/remote_album.provider.dart +++ b/mobile/lib/providers/infrastructure/remote_album.provider.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -70,7 +71,7 @@ class RemoteAlbumNotifier extends Notifier { Future> sortAlbums( List albums, - RemoteAlbumSortMode sortMode, { + AlbumSortMode sortMode, { bool isReverse = false, }) async { return await _remoteAlbumService.sortAlbums(albums, sortMode, isReverse: isReverse); diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index 7149408e8a..fc08193d11 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -51,9 +51,10 @@ enum AppSettingsEnum { enableBackup(StoreKey.enableBackup, null, false), useCellularForUploadVideos(StoreKey.useWifiForUploadVideos, null, false), useCellularForUploadPhotos(StoreKey.useWifiForUploadPhotos, null, false), + readonlyModeEnabled(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false), + albumGridView(StoreKey.albumGridView, "albumGridView", false), backupRequireCharging(StoreKey.backupRequireCharging, null, false), - backupTriggerDelay(StoreKey.backupTriggerDelay, null, 30), - readonlyModeEnabled(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false); + backupTriggerDelay(StoreKey.backupTriggerDelay, null, 30); const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); diff --git a/mobile/lib/utils/album_filter.utils.dart b/mobile/lib/utils/album_filter.utils.dart index 02142b1571..8f9363d4d9 100644 --- a/mobile/lib/utils/album_filter.utils.dart +++ b/mobile/lib/utils/album_filter.utils.dart @@ -1,5 +1,5 @@ -import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; class AlbumFilter { String? userId; @@ -14,12 +14,12 @@ class AlbumFilter { } class AlbumSort { - RemoteAlbumSortMode mode; + AlbumSortMode mode; bool isReverse; AlbumSort({required this.mode, this.isReverse = false}); - AlbumSort copyWith({RemoteAlbumSortMode? mode, bool? isReverse}) { + AlbumSort copyWith({AlbumSortMode? mode, bool? isReverse}) { return AlbumSort(mode: mode ?? this.mode, isReverse: isReverse ?? this.isReverse); } } diff --git a/mobile/test/domain/services/album.service_test.dart b/mobile/test/domain/services/album.service_test.dart index ebd94a9450..b86819536d 100644 --- a/mobile/test/domain/services/album.service_test.dart +++ b/mobile/test/domain/services/album.service_test.dart @@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:mocktail/mocktail.dart'; @@ -76,42 +77,42 @@ void main() { test('should sort correctly based on name', () async { final albums = [albumB, albumA]; - final result = await sut.sortAlbums(albums, RemoteAlbumSortMode.title); + final result = await sut.sortAlbums(albums, AlbumSortMode.title); expect(result, [albumA, albumB]); }); test('should sort correctly based on createdAt', () async { final albums = [albumB, albumA]; - final result = await sut.sortAlbums(albums, RemoteAlbumSortMode.created); + final result = await sut.sortAlbums(albums, AlbumSortMode.created); expect(result, [albumA, albumB]); }); test('should sort correctly based on updatedAt', () async { final albums = [albumB, albumA]; - final result = await sut.sortAlbums(albums, RemoteAlbumSortMode.lastModified); + final result = await sut.sortAlbums(albums, AlbumSortMode.lastModified); expect(result, [albumA, albumB]); }); test('should sort correctly based on assetCount', () async { final albums = [albumB, albumA]; - final result = await sut.sortAlbums(albums, RemoteAlbumSortMode.assetCount); + final result = await sut.sortAlbums(albums, AlbumSortMode.assetCount); expect(result, [albumA, albumB]); }); test('should sort correctly based on newestAssetTimestamp', () async { final albums = [albumB, albumA]; - final result = await sut.sortAlbums(albums, RemoteAlbumSortMode.mostRecent); + final result = await sut.sortAlbums(albums, AlbumSortMode.mostRecent); expect(result, [albumA, albumB]); }); test('should sort correctly based on oldestAssetTimestamp', () async { final albums = [albumB, albumA]; - final result = await sut.sortAlbums(albums, RemoteAlbumSortMode.mostOldest); + final result = await sut.sortAlbums(albums, AlbumSortMode.mostOldest); expect(result, [albumB, albumA]); }); });