From 44957101ddb3ae479e4f352a0f35e6c2a9221ca3 Mon Sep 17 00:00:00 2001 From: bwees Date: Sat, 30 Aug 2025 13:39:21 -0500 Subject: [PATCH 1/9] fix: retain filter and sort options when pulling to refresh --- .../presentation/pages/drift_album.page.dart | 2 +- .../infrastructure/remote_album.provider.dart | 35 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/mobile/lib/presentation/pages/drift_album.page.dart b/mobile/lib/presentation/pages/drift_album.page.dart index 0835c741ad9e2..30269b656f833 100644 --- a/mobile/lib/presentation/pages/drift_album.page.dart +++ b/mobile/lib/presentation/pages/drift_album.page.dart @@ -19,7 +19,7 @@ class DriftAlbumsPage extends ConsumerStatefulWidget { class _DriftAlbumsPageState extends ConsumerState { Future onRefresh() async { - await ref.read(remoteAlbumProvider.notifier).refresh(); + await ref.read(remoteAlbumProvider.notifier).refresh(keepFilters: true); } @override diff --git a/mobile/lib/providers/infrastructure/remote_album.provider.dart b/mobile/lib/providers/infrastructure/remote_album.provider.dart index a48a1c30e4111..e0e2383c5fb63 100644 --- a/mobile/lib/providers/infrastructure/remote_album.provider.dart +++ b/mobile/lib/providers/infrastructure/remote_album.provider.dart @@ -39,6 +39,14 @@ class RemoteAlbumState { class RemoteAlbumNotifier extends Notifier { late RemoteAlbumService _remoteAlbumService; final _logger = Logger('RemoteAlbumNotifier'); + + // values used for filtering and sorting when a refresh occurs + String? _lastUserId; + String? _lastQuery; + QuickFilterMode? _lastFilterMode; + RemoteAlbumSortMode? _lastSortMode; + bool? _lastSortIsReverse; + @override RemoteAlbumState build() { _remoteAlbumService = ref.read(remoteAlbumServiceProvider); @@ -56,14 +64,34 @@ class RemoteAlbumNotifier extends Notifier { } } - Future refresh() async { + Future refresh({bool keepFilters = false}) async { await _getAll(); + + // Restore previous search and filters when pulling to refresh + if (keepFilters) { + if (_lastQuery != null && _lastFilterMode != null) { + searchAlbums(_lastQuery!, _lastUserId, _lastFilterMode!); + } + + if (_lastSortMode != null && _lastSortIsReverse != null) { + await sortFilteredAlbums(_lastSortMode!, isReverse: _lastSortIsReverse!); + } + } else { + _lastQuery = null; + _lastUserId = null; + _lastFilterMode = null; + _lastSortMode = null; + _lastSortIsReverse = null; + } } void searchAlbums(String query, String? userId, [QuickFilterMode filterMode = QuickFilterMode.all]) { final filtered = _remoteAlbumService.searchAlbums(state.albums, query, userId, filterMode); - state = state.copyWith(filteredAlbums: filtered); + + _lastQuery = query; + _lastUserId = userId; + _lastFilterMode = filterMode; } void clearSearch() { @@ -73,6 +101,9 @@ class RemoteAlbumNotifier extends Notifier { Future sortFilteredAlbums(RemoteAlbumSortMode sortMode, {bool isReverse = false}) async { final sortedAlbums = await _remoteAlbumService.sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse); state = state.copyWith(filteredAlbums: sortedAlbums); + + _lastSortMode = sortMode; + _lastSortIsReverse = isReverse; } Future createAlbum({ From a60670597363b6552c7c856031e580ad38dfcf5a Mon Sep 17 00:00:00 2001 From: bwees Date: Sat, 30 Aug 2025 13:43:14 -0500 Subject: [PATCH 2/9] chore: use classes to manage state --- .../infrastructure/remote_album.provider.dart | 37 +++++++++---------- mobile/lib/utils/album_filter.utils.dart | 24 ++++++++++++ 2 files changed, 42 insertions(+), 19 deletions(-) create mode 100644 mobile/lib/utils/album_filter.utils.dart diff --git a/mobile/lib/providers/infrastructure/remote_album.provider.dart b/mobile/lib/providers/infrastructure/remote_album.provider.dart index e0e2383c5fb63..e92ac39d55a7d 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/utils/album_filter.utils.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -41,11 +42,8 @@ class RemoteAlbumNotifier extends Notifier { final _logger = Logger('RemoteAlbumNotifier'); // values used for filtering and sorting when a refresh occurs - String? _lastUserId; - String? _lastQuery; - QuickFilterMode? _lastFilterMode; - RemoteAlbumSortMode? _lastSortMode; - bool? _lastSortIsReverse; + AlbumSortState? _lastSortState; + AlbumFilterState? _lastFilterState; @override RemoteAlbumState build() { @@ -69,19 +67,16 @@ class RemoteAlbumNotifier extends Notifier { // Restore previous search and filters when pulling to refresh if (keepFilters) { - if (_lastQuery != null && _lastFilterMode != null) { - searchAlbums(_lastQuery!, _lastUserId, _lastFilterMode!); + if (_lastFilterState != null) { + searchAlbums(_lastFilterState!.query, _lastFilterState!.userId, _lastFilterState!.filterMode); } - if (_lastSortMode != null && _lastSortIsReverse != null) { - await sortFilteredAlbums(_lastSortMode!, isReverse: _lastSortIsReverse!); + if (_lastSortState != null) { + await sortFilteredAlbums(_lastSortState!.sortMode, isReverse: _lastSortState!.isReverse); } } else { - _lastQuery = null; - _lastUserId = null; - _lastFilterMode = null; - _lastSortMode = null; - _lastSortIsReverse = null; + _lastFilterState = null; + _lastSortState = null; } } @@ -89,9 +84,11 @@ class RemoteAlbumNotifier extends Notifier { final filtered = _remoteAlbumService.searchAlbums(state.albums, query, userId, filterMode); state = state.copyWith(filteredAlbums: filtered); - _lastQuery = query; - _lastUserId = userId; - _lastFilterMode = filterMode; + _lastFilterState = AlbumFilterState( + userId: userId, + query: query, + filterMode: filterMode, + ); } void clearSearch() { @@ -102,8 +99,10 @@ class RemoteAlbumNotifier extends Notifier { final sortedAlbums = await _remoteAlbumService.sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse); state = state.copyWith(filteredAlbums: sortedAlbums); - _lastSortMode = sortMode; - _lastSortIsReverse = isReverse; + _lastSortState = AlbumSortState( + sortMode: sortMode, + isReverse: isReverse, + ); } Future createAlbum({ diff --git a/mobile/lib/utils/album_filter.utils.dart b/mobile/lib/utils/album_filter.utils.dart new file mode 100644 index 0000000000000..5fd77593980a0 --- /dev/null +++ b/mobile/lib/utils/album_filter.utils.dart @@ -0,0 +1,24 @@ +import 'package:immich_mobile/domain/services/remote_album.service.dart'; +import 'package:immich_mobile/models/albums/album_search.model.dart'; + +class AlbumFilterState { + String? userId; + String query; + QuickFilterMode filterMode; + + AlbumFilterState({ + this.userId, + required this.query, + required this.filterMode, + }); +} + +class AlbumSortState { + RemoteAlbumSortMode sortMode; + bool isReverse; + + AlbumSortState({ + required this.sortMode, + this.isReverse = false, + }); +} \ No newline at end of file From 30cefa4b57e58ee5138c9788c49f4e6336fde67c Mon Sep 17 00:00:00 2001 From: bwees Date: Sat, 30 Aug 2025 13:52:29 -0500 Subject: [PATCH 3/9] chore: format --- .../infrastructure/remote_album.provider.dart | 11 ++--------- mobile/lib/utils/album_filter.utils.dart | 13 +++---------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/mobile/lib/providers/infrastructure/remote_album.provider.dart b/mobile/lib/providers/infrastructure/remote_album.provider.dart index e92ac39d55a7d..77ba36097a4a5 100644 --- a/mobile/lib/providers/infrastructure/remote_album.provider.dart +++ b/mobile/lib/providers/infrastructure/remote_album.provider.dart @@ -84,11 +84,7 @@ class RemoteAlbumNotifier extends Notifier { final filtered = _remoteAlbumService.searchAlbums(state.albums, query, userId, filterMode); state = state.copyWith(filteredAlbums: filtered); - _lastFilterState = AlbumFilterState( - userId: userId, - query: query, - filterMode: filterMode, - ); + _lastFilterState = AlbumFilterState(userId: userId, query: query, filterMode: filterMode); } void clearSearch() { @@ -99,10 +95,7 @@ class RemoteAlbumNotifier extends Notifier { final sortedAlbums = await _remoteAlbumService.sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse); state = state.copyWith(filteredAlbums: sortedAlbums); - _lastSortState = AlbumSortState( - sortMode: sortMode, - isReverse: isReverse, - ); + _lastSortState = AlbumSortState(sortMode: sortMode, isReverse: isReverse); } Future createAlbum({ diff --git a/mobile/lib/utils/album_filter.utils.dart b/mobile/lib/utils/album_filter.utils.dart index 5fd77593980a0..8965a60bba9bd 100644 --- a/mobile/lib/utils/album_filter.utils.dart +++ b/mobile/lib/utils/album_filter.utils.dart @@ -6,19 +6,12 @@ class AlbumFilterState { String query; QuickFilterMode filterMode; - AlbumFilterState({ - this.userId, - required this.query, - required this.filterMode, - }); + AlbumFilterState({this.userId, required this.query, required this.filterMode}); } class AlbumSortState { RemoteAlbumSortMode sortMode; bool isReverse; - AlbumSortState({ - required this.sortMode, - this.isReverse = false, - }); -} \ No newline at end of file + AlbumSortState({required this.sortMode, this.isReverse = false}); +} From 173ae30ced51e0f62867d38bdee161bd35affb59 Mon Sep 17 00:00:00 2001 From: bwees Date: Sat, 30 Aug 2025 15:00:45 -0500 Subject: [PATCH 4/9] chore: refactor to keep local state of filter/sorted albums instead of a global filteredAlbums --- .../presentation/pages/drift_album.page.dart | 2 +- .../widgets/album/album_selector.widget.dart | 92 +++++++++++++++---- .../infrastructure/remote_album.provider.dart | 81 ++++++---------- mobile/lib/utils/album_filter.utils.dart | 29 ++++-- 4 files changed, 124 insertions(+), 80 deletions(-) diff --git a/mobile/lib/presentation/pages/drift_album.page.dart b/mobile/lib/presentation/pages/drift_album.page.dart index 30269b656f833..0835c741ad9e2 100644 --- a/mobile/lib/presentation/pages/drift_album.page.dart +++ b/mobile/lib/presentation/pages/drift_album.page.dart @@ -19,7 +19,7 @@ class DriftAlbumsPage extends ConsumerStatefulWidget { class _DriftAlbumsPageState extends ConsumerState { Future onRefresh() async { - await ref.read(remoteAlbumProvider.notifier).refresh(keepFilters: true); + await ref.read(remoteAlbumProvider.notifier).refresh(); } @override diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index e49f2b6804f12..c43f4361d038d 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; @@ -14,11 +15,13 @@ import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.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'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; @@ -39,8 +42,11 @@ class AlbumSelector extends ConsumerStatefulWidget { class _AlbumSelectorState extends ConsumerState { bool isGrid = false; final searchController = TextEditingController(); - QuickFilterMode filterMode = QuickFilterMode.all; final searchFocusNode = FocusNode(); + List albums = []; + + AlbumFilter filter = AlbumFilter(query: "", mode: QuickFilterMode.all); + AlbumSort sort = AlbumSort(mode: RemoteAlbumSortMode.lastModified); @override void initState() { @@ -52,7 +58,7 @@ class _AlbumSelectorState extends ConsumerState { }); searchController.addListener(() { - onSearch(searchController.text, filterMode); + onSearch(searchController.text, filter.mode); }); searchFocusNode.addListener(() { @@ -64,7 +70,9 @@ class _AlbumSelectorState extends ConsumerState { void onSearch(String searchTerm, QuickFilterMode sortMode) { final userId = ref.watch(currentUserProvider)?.id; - ref.read(remoteAlbumProvider.notifier).searchAlbums(searchTerm, userId, sortMode); + filter = filter.copyWith(query: searchTerm, userId: userId, mode: sortMode); + + filterAlbums(); } Future onRefresh() async { @@ -77,17 +85,63 @@ class _AlbumSelectorState extends ConsumerState { }); } - void changeFilter(QuickFilterMode sortMode) { + void changeFilter(QuickFilterMode mode) { + setState(() { + filter = filter.copyWith(mode: mode); + }); + + filterAlbums(); + } + + Future changeSort(AlbumSort sort) async { setState(() { - filterMode = sortMode; + this.sort = sort; }); + + await sortAlbums(); } void clearSearch() { setState(() { - filterMode = QuickFilterMode.all; + filter = filter.copyWith(mode: QuickFilterMode.all, query: null); searchController.clear(); - ref.read(remoteAlbumProvider.notifier).clearSearch(); + }); + + filterAlbums(); + } + + Future sortAlbums() async { + final sorted = await ref.read(remoteAlbumProvider.notifier).sortAlbums( + albums, + sort.mode, + isReverse: sort.isReverse, + ); + + + setState(() { + albums = sorted; + }); + } + + Future filterAlbums() async { + if (filter.query == null) { + setState(() { + albums = ref.read(remoteAlbumProvider).albums; + }); + + sortAlbums(); + return; + } + + final filteredAlbums = ref.read(remoteAlbumProvider.notifier).searchAlbums( + ref.read(remoteAlbumProvider).albums, + filter.query!, + filter.userId, + filter.mode, + ); + + setState(() { + albums = filteredAlbums; }); } @@ -100,26 +154,27 @@ class _AlbumSelectorState extends ConsumerState { @override Widget build(BuildContext context) { - final albums = ref.watch(remoteAlbumProvider.select((s) => s.filteredAlbums)); - final userId = ref.watch(currentUserProvider)?.id; + // refilter and sort when albums change + ref.listen(remoteAlbumProvider.select((state) => state.albums), (_,_) => filterAlbums()); + return MultiSliver( children: [ _SearchBar( searchController: searchController, searchFocusNode: searchFocusNode, onSearch: onSearch, - filterMode: filterMode, + filterMode: filter.mode, onClearSearch: clearSearch, ), _QuickFilterButtonRow( - filterMode: filterMode, + filterMode: filter.mode, onChangeFilter: changeFilter, onSearch: onSearch, searchController: searchController, ), - _QuickSortAndViewMode(isGrid: isGrid, onToggleViewMode: toggleViewMode), + _QuickSortAndViewMode(isGrid: isGrid, onToggleViewMode: toggleViewMode, onSortChanged: changeSort), isGrid ? _AlbumGrid(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected) : _AlbumList(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected), @@ -129,7 +184,9 @@ class _AlbumSelectorState extends ConsumerState { } class _SortButton extends ConsumerStatefulWidget { - const _SortButton(); + const _SortButton(this.onSortChanged); + + final Future Function(AlbumSort) onSortChanged; @override ConsumerState<_SortButton> createState() => _SortButtonState(); @@ -148,14 +205,14 @@ class _SortButtonState extends ConsumerState<_SortButton> { albumSortIsReverse = !albumSortIsReverse; isSorting = true; }); - await ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(sortMode, isReverse: albumSortIsReverse); } else { setState(() { albumSortOption = sortMode; isSorting = true; }); - await ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(sortMode, isReverse: albumSortIsReverse); } + + await widget.onSortChanged.call(AlbumSort(mode: albumSortOption, isReverse: albumSortIsReverse)); setState(() { isSorting = false; @@ -394,10 +451,11 @@ class _QuickFilterButton extends StatelessWidget { } class _QuickSortAndViewMode extends StatelessWidget { - const _QuickSortAndViewMode({required this.isGrid, required this.onToggleViewMode}); + const _QuickSortAndViewMode({required this.isGrid, required this.onToggleViewMode, required this.onSortChanged}); final bool isGrid; final VoidCallback onToggleViewMode; + final Future Function(AlbumSort) onSortChanged; @override Widget build(BuildContext context) { @@ -407,7 +465,7 @@ class _QuickSortAndViewMode extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const _SortButton(), + _SortButton(onSortChanged), 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 77ba36097a4a5..936084b321566 100644 --- a/mobile/lib/providers/infrastructure/remote_album.provider.dart +++ b/mobile/lib/providers/infrastructure/remote_album.provider.dart @@ -13,48 +13,42 @@ import 'album.provider.dart'; class RemoteAlbumState { final List albums; - final List filteredAlbums; - const RemoteAlbumState({required this.albums, List? filteredAlbums}) - : filteredAlbums = filteredAlbums ?? albums; + const RemoteAlbumState({required this.albums}); - RemoteAlbumState copyWith({List? albums, List? filteredAlbums}) { - return RemoteAlbumState(albums: albums ?? this.albums, filteredAlbums: filteredAlbums ?? this.filteredAlbums); + RemoteAlbumState copyWith({List? albums}) { + return RemoteAlbumState(albums: albums ?? this.albums); } @override - String toString() => 'RemoteAlbumState(albums: ${albums.length}, filteredAlbums: ${filteredAlbums.length})'; + String toString() => 'RemoteAlbumState(albums: ${albums.length})'; @override bool operator ==(covariant RemoteAlbumState other) { if (identical(this, other)) return true; final listEquals = const DeepCollectionEquality().equals; - return listEquals(other.albums, albums) && listEquals(other.filteredAlbums, filteredAlbums); + return listEquals(other.albums, albums); } @override - int get hashCode => albums.hashCode ^ filteredAlbums.hashCode; + int get hashCode => albums.hashCode; } class RemoteAlbumNotifier extends Notifier { late RemoteAlbumService _remoteAlbumService; final _logger = Logger('RemoteAlbumNotifier'); - // values used for filtering and sorting when a refresh occurs - AlbumSortState? _lastSortState; - AlbumFilterState? _lastFilterState; - @override RemoteAlbumState build() { _remoteAlbumService = ref.read(remoteAlbumServiceProvider); - return const RemoteAlbumState(albums: [], filteredAlbums: []); + return const RemoteAlbumState(albums: []); } Future> _getAll() async { try { final albums = await _remoteAlbumService.getAll(); - state = state.copyWith(albums: albums, filteredAlbums: albums); + state = state.copyWith(albums: albums); return albums; } catch (error, stack) { _logger.severe('Failed to fetch albums', error, stack); @@ -62,40 +56,25 @@ class RemoteAlbumNotifier extends Notifier { } } - Future refresh({bool keepFilters = false}) async { + Future refresh() async { await _getAll(); - - // Restore previous search and filters when pulling to refresh - if (keepFilters) { - if (_lastFilterState != null) { - searchAlbums(_lastFilterState!.query, _lastFilterState!.userId, _lastFilterState!.filterMode); - } - - if (_lastSortState != null) { - await sortFilteredAlbums(_lastSortState!.sortMode, isReverse: _lastSortState!.isReverse); - } - } else { - _lastFilterState = null; - _lastSortState = null; - } } - void searchAlbums(String query, String? userId, [QuickFilterMode filterMode = QuickFilterMode.all]) { - final filtered = _remoteAlbumService.searchAlbums(state.albums, query, userId, filterMode); - state = state.copyWith(filteredAlbums: filtered); - - _lastFilterState = AlbumFilterState(userId: userId, query: query, filterMode: filterMode); + List searchAlbums( + List albums, + String query, + String? userId, [ + QuickFilterMode filterMode = QuickFilterMode.all, + ]) { + return _remoteAlbumService.searchAlbums(albums, query, userId, filterMode); } - void clearSearch() { - state = state.copyWith(filteredAlbums: state.albums); - } - - Future sortFilteredAlbums(RemoteAlbumSortMode sortMode, {bool isReverse = false}) async { - final sortedAlbums = await _remoteAlbumService.sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse); - state = state.copyWith(filteredAlbums: sortedAlbums); - - _lastSortState = AlbumSortState(sortMode: sortMode, isReverse: isReverse); + Future> sortAlbums( + List albums, + RemoteAlbumSortMode sortMode, { + bool isReverse = false, + }) async { + return await _remoteAlbumService.sortAlbums(albums, sortMode, isReverse: isReverse); } Future createAlbum({ @@ -106,7 +85,7 @@ class RemoteAlbumNotifier extends Notifier { try { final album = await _remoteAlbumService.createAlbum(title: title, description: description, assetIds: assetIds); - state = state.copyWith(albums: [...state.albums, album], filteredAlbums: [...state.filteredAlbums, album]); + state = state.copyWith(albums: [...state.albums, album]); return album; } catch (error, stack) { @@ -137,11 +116,7 @@ class RemoteAlbumNotifier extends Notifier { return album.id == albumId ? updatedAlbum : album; }).toList(); - final updatedFilteredAlbums = state.filteredAlbums.map((album) { - return album.id == albumId ? updatedAlbum : album; - }).toList(); - - state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums); + state = state.copyWith(albums: updatedAlbums); return updatedAlbum; } catch (error, stack) { @@ -162,9 +137,7 @@ class RemoteAlbumNotifier extends Notifier { await _remoteAlbumService.deleteAlbum(albumId); final updatedAlbums = state.albums.where((album) => album.id != albumId).toList(); - final updatedFilteredAlbums = state.filteredAlbums.where((album) => album.id != albumId).toList(); - - state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums); + state = state.copyWith(albums: updatedAlbums); } Future> getAssets(String albumId) { @@ -187,9 +160,7 @@ class RemoteAlbumNotifier extends Notifier { await _remoteAlbumService.removeUser(albumId, userId: userId); final updatedAlbums = state.albums.where((album) => album.id != albumId).toList(); - final updatedFilteredAlbums = state.filteredAlbums.where((album) => album.id != albumId).toList(); - - state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums); + state = state.copyWith(albums: updatedAlbums); } Future setActivityStatus(String albumId, bool enabled) { diff --git a/mobile/lib/utils/album_filter.utils.dart b/mobile/lib/utils/album_filter.utils.dart index 8965a60bba9bd..bd254c221dfbe 100644 --- a/mobile/lib/utils/album_filter.utils.dart +++ b/mobile/lib/utils/album_filter.utils.dart @@ -1,17 +1,32 @@ import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; -class AlbumFilterState { +class AlbumFilter { String? userId; - String query; - QuickFilterMode filterMode; + String? query; + QuickFilterMode mode; - AlbumFilterState({this.userId, required this.query, required this.filterMode}); + AlbumFilter({required this.mode, this.userId, this.query}); + + AlbumFilter copyWith({String? userId, String? query, QuickFilterMode? mode}) { + return AlbumFilter( + userId: userId ?? this.userId, + query: query ?? this.query, + mode: mode ?? this.mode, + ); + } } -class AlbumSortState { - RemoteAlbumSortMode sortMode; +class AlbumSort { + RemoteAlbumSortMode mode; bool isReverse; - AlbumSortState({required this.sortMode, this.isReverse = false}); + AlbumSort({required this.mode, this.isReverse = false}); + + AlbumSort copyWith({RemoteAlbumSortMode? mode, bool? isReverse}) { + return AlbumSort( + mode: mode ?? this.mode, + isReverse: isReverse ?? this.isReverse, + ); + } } From 75860d767aeb1814bbf88579b91f668dbb83a4aa Mon Sep 17 00:00:00 2001 From: bwees Date: Sat, 30 Aug 2025 15:06:02 -0500 Subject: [PATCH 5/9] fix: keep sort when page is navigated away and returned --- .../widgets/album/album_selector.widget.dart | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index c43f4361d038d..882a6dd21045d 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -3,7 +3,6 @@ import 'dart:math'; import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; @@ -15,7 +14,6 @@ import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; @@ -111,12 +109,9 @@ class _AlbumSelectorState extends ConsumerState { } Future sortAlbums() async { - final sorted = await ref.read(remoteAlbumProvider.notifier).sortAlbums( - albums, - sort.mode, - isReverse: sort.isReverse, - ); - + final sorted = await ref + .read(remoteAlbumProvider.notifier) + .sortAlbums(albums, sort.mode, isReverse: sort.isReverse); setState(() { albums = sorted; @@ -133,12 +128,9 @@ class _AlbumSelectorState extends ConsumerState { return; } - final filteredAlbums = ref.read(remoteAlbumProvider.notifier).searchAlbums( - ref.read(remoteAlbumProvider).albums, - filter.query!, - filter.userId, - filter.mode, - ); + final filteredAlbums = ref + .read(remoteAlbumProvider.notifier) + .searchAlbums(ref.read(remoteAlbumProvider).albums, filter.query!, filter.userId, filter.mode); setState(() { albums = filteredAlbums; @@ -157,7 +149,10 @@ class _AlbumSelectorState extends ConsumerState { final userId = ref.watch(currentUserProvider)?.id; // refilter and sort when albums change - ref.listen(remoteAlbumProvider.select((state) => state.albums), (_,_) => filterAlbums()); + ref.listen(remoteAlbumProvider.select((state) => state.albums), (_, _) async { + filterAlbums(); + await sortAlbums(); + }); return MultiSliver( children: [ @@ -211,7 +206,7 @@ class _SortButtonState extends ConsumerState<_SortButton> { isSorting = true; }); } - + await widget.onSortChanged.call(AlbumSort(mode: albumSortOption, isReverse: albumSortIsReverse)); setState(() { From 08bab1a03cd6b8f89a75f8087393bb85a9e1aff6 Mon Sep 17 00:00:00 2001 From: bwees Date: Sat, 30 Aug 2025 15:09:54 -0500 Subject: [PATCH 6/9] chore: lint --- mobile/lib/providers/infrastructure/remote_album.provider.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/mobile/lib/providers/infrastructure/remote_album.provider.dart b/mobile/lib/providers/infrastructure/remote_album.provider.dart index 936084b321566..38ba52dc56c9c 100644 --- a/mobile/lib/providers/infrastructure/remote_album.provider.dart +++ b/mobile/lib/providers/infrastructure/remote_album.provider.dart @@ -5,7 +5,6 @@ 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/utils/album_filter.utils.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; From 9574841e7d4a82b39ff7b94e5baaaefaa9238835 Mon Sep 17 00:00:00 2001 From: bwees Date: Sat, 30 Aug 2025 16:48:36 -0500 Subject: [PATCH 7/9] chore: format why is autoformat not working --- mobile/lib/utils/album_filter.utils.dart | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/mobile/lib/utils/album_filter.utils.dart b/mobile/lib/utils/album_filter.utils.dart index bd254c221dfbe..02142b1571faa 100644 --- a/mobile/lib/utils/album_filter.utils.dart +++ b/mobile/lib/utils/album_filter.utils.dart @@ -9,11 +9,7 @@ class AlbumFilter { AlbumFilter({required this.mode, this.userId, this.query}); AlbumFilter copyWith({String? userId, String? query, QuickFilterMode? mode}) { - return AlbumFilter( - userId: userId ?? this.userId, - query: query ?? this.query, - mode: mode ?? this.mode, - ); + return AlbumFilter(userId: userId ?? this.userId, query: query ?? this.query, mode: mode ?? this.mode); } } @@ -24,9 +20,6 @@ class AlbumSort { AlbumSort({required this.mode, this.isReverse = false}); AlbumSort copyWith({RemoteAlbumSortMode? mode, bool? isReverse}) { - return AlbumSort( - mode: mode ?? this.mode, - isReverse: isReverse ?? this.isReverse, - ); + return AlbumSort(mode: mode ?? this.mode, isReverse: isReverse ?? this.isReverse); } } From 7a69555718c79d5586d9b58e03770373920d7257 Mon Sep 17 00:00:00 2001 From: bwees Date: Sun, 31 Aug 2025 22:54:35 -0500 Subject: [PATCH 8/9] fix: default sort direction state --- .../lib/presentation/widgets/album/album_selector.widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index 882a6dd21045d..4ff87855575de 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -44,7 +44,7 @@ class _AlbumSelectorState extends ConsumerState { List albums = []; AlbumFilter filter = AlbumFilter(query: "", mode: QuickFilterMode.all); - AlbumSort sort = AlbumSort(mode: RemoteAlbumSortMode.lastModified); + AlbumSort sort = AlbumSort(mode: RemoteAlbumSortMode.lastModified, isReverse: true); @override void initState() { From 94ab0a09b707a71173e80e6dddfc918b1106bf4d Mon Sep 17 00:00:00 2001 From: bwees Date: Sun, 31 Aug 2025 23:10:25 -0500 Subject: [PATCH 9/9] fix: search clears sorting we have to cache our sorted albums since sorting is very computationally expensive and cannot be run on every keystroke. For searches, instead of pulling from the list of albums, we now pull from the cached sorted list and then filter which is then shown to the user --- .../widgets/album/album_selector.widget.dart | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index 4ff87855575de..ce98728089423 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -41,7 +41,8 @@ class _AlbumSelectorState extends ConsumerState { bool isGrid = false; final searchController = TextEditingController(); final searchFocusNode = FocusNode(); - List albums = []; + List sortedAlbums = []; + List shownAlbums = []; AlbumFilter filter = AlbumFilter(query: "", mode: QuickFilterMode.all); AlbumSort sort = AlbumSort(mode: RemoteAlbumSortMode.lastModified, isReverse: true); @@ -66,9 +67,9 @@ class _AlbumSelectorState extends ConsumerState { }); } - void onSearch(String searchTerm, QuickFilterMode sortMode) { + void onSearch(String searchTerm, QuickFilterMode filterMode) { final userId = ref.watch(currentUserProvider)?.id; - filter = filter.copyWith(query: searchTerm, userId: userId, mode: sortMode); + filter = filter.copyWith(query: searchTerm, userId: userId, mode: filterMode); filterAlbums(); } @@ -111,29 +112,32 @@ class _AlbumSelectorState extends ConsumerState { Future sortAlbums() async { final sorted = await ref .read(remoteAlbumProvider.notifier) - .sortAlbums(albums, sort.mode, isReverse: sort.isReverse); + .sortAlbums(ref.read(remoteAlbumProvider).albums, sort.mode, isReverse: sort.isReverse); setState(() { - albums = sorted; + sortedAlbums = sorted; }); + + // we need to re-filter the albums after sorting + // so shownAlbums gets updated + filterAlbums(); } Future filterAlbums() async { if (filter.query == null) { setState(() { - albums = ref.read(remoteAlbumProvider).albums; + shownAlbums = sortedAlbums; }); - sortAlbums(); return; } final filteredAlbums = ref .read(remoteAlbumProvider.notifier) - .searchAlbums(ref.read(remoteAlbumProvider).albums, filter.query!, filter.userId, filter.mode); + .searchAlbums(sortedAlbums, filter.query!, filter.userId, filter.mode); setState(() { - albums = filteredAlbums; + shownAlbums = filteredAlbums; }); } @@ -150,7 +154,6 @@ class _AlbumSelectorState extends ConsumerState { // refilter and sort when albums change ref.listen(remoteAlbumProvider.select((state) => state.albums), (_, _) async { - filterAlbums(); await sortAlbums(); }); @@ -171,8 +174,8 @@ class _AlbumSelectorState extends ConsumerState { ), _QuickSortAndViewMode(isGrid: isGrid, onToggleViewMode: toggleViewMode, onSortChanged: changeSort), isGrid - ? _AlbumGrid(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected) - : _AlbumList(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected), + ? _AlbumGrid(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected) + : _AlbumList(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected), ], ); }