mirror of
https://github.com/immich-app/immich.git
synced 2025-09-29 15:31:13 -04:00
fix: retain filter and sort options when pulling to refresh (#21452)
* fix: retain filter and sort options when pulling to refresh * chore: use classes to manage state * chore: format * chore: refactor to keep local state of filter/sorted albums instead of a global filteredAlbums * fix: keep sort when page is navigated away and returned * chore: lint * chore: format why is autoformat not working * fix: default sort direction state * 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
This commit is contained in:
parent
6c178a04dc
commit
bf6211776f
@ -19,6 +19,7 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
|
||||
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 +40,12 @@ class AlbumSelector extends ConsumerStatefulWidget {
|
||||
class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||
bool isGrid = false;
|
||||
final searchController = TextEditingController();
|
||||
QuickFilterMode filterMode = QuickFilterMode.all;
|
||||
final searchFocusNode = FocusNode();
|
||||
List<RemoteAlbum> sortedAlbums = [];
|
||||
List<RemoteAlbum> shownAlbums = [];
|
||||
|
||||
AlbumFilter filter = AlbumFilter(query: "", mode: QuickFilterMode.all);
|
||||
AlbumSort sort = AlbumSort(mode: RemoteAlbumSortMode.lastModified, isReverse: true);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -52,7 +57,7 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||
});
|
||||
|
||||
searchController.addListener(() {
|
||||
onSearch(searchController.text, filterMode);
|
||||
onSearch(searchController.text, filter.mode);
|
||||
});
|
||||
|
||||
searchFocusNode.addListener(() {
|
||||
@ -62,9 +67,11 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||
});
|
||||
}
|
||||
|
||||
void onSearch(String searchTerm, QuickFilterMode sortMode) {
|
||||
void onSearch(String searchTerm, QuickFilterMode filterMode) {
|
||||
final userId = ref.watch(currentUserProvider)?.id;
|
||||
ref.read(remoteAlbumProvider.notifier).searchAlbums(searchTerm, userId, sortMode);
|
||||
filter = filter.copyWith(query: searchTerm, userId: userId, mode: filterMode);
|
||||
|
||||
filterAlbums();
|
||||
}
|
||||
|
||||
Future<void> onRefresh() async {
|
||||
@ -77,17 +84,60 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||
});
|
||||
}
|
||||
|
||||
void changeFilter(QuickFilterMode sortMode) {
|
||||
void changeFilter(QuickFilterMode mode) {
|
||||
setState(() {
|
||||
filterMode = sortMode;
|
||||
filter = filter.copyWith(mode: mode);
|
||||
});
|
||||
|
||||
filterAlbums();
|
||||
}
|
||||
|
||||
Future<void> changeSort(AlbumSort sort) async {
|
||||
setState(() {
|
||||
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<void> sortAlbums() async {
|
||||
final sorted = await ref
|
||||
.read(remoteAlbumProvider.notifier)
|
||||
.sortAlbums(ref.read(remoteAlbumProvider).albums, sort.mode, isReverse: sort.isReverse);
|
||||
|
||||
setState(() {
|
||||
sortedAlbums = sorted;
|
||||
});
|
||||
|
||||
// we need to re-filter the albums after sorting
|
||||
// so shownAlbums gets updated
|
||||
filterAlbums();
|
||||
}
|
||||
|
||||
Future<void> filterAlbums() async {
|
||||
if (filter.query == null) {
|
||||
setState(() {
|
||||
shownAlbums = sortedAlbums;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
final filteredAlbums = ref
|
||||
.read(remoteAlbumProvider.notifier)
|
||||
.searchAlbums(sortedAlbums, filter.query!, filter.userId, filter.mode);
|
||||
|
||||
setState(() {
|
||||
shownAlbums = filteredAlbums;
|
||||
});
|
||||
}
|
||||
|
||||
@ -100,36 +150,41 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||
|
||||
@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), (_, _) async {
|
||||
await sortAlbums();
|
||||
});
|
||||
|
||||
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),
|
||||
? _AlbumGrid(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected)
|
||||
: _AlbumList(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SortButton extends ConsumerStatefulWidget {
|
||||
const _SortButton();
|
||||
const _SortButton(this.onSortChanged);
|
||||
|
||||
final Future<void> Function(AlbumSort) onSortChanged;
|
||||
|
||||
@override
|
||||
ConsumerState<_SortButton> createState() => _SortButtonState();
|
||||
@ -148,15 +203,15 @@ 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 +449,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<void> Function(AlbumSort) onSortChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -407,7 +463,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,
|
||||
|
@ -12,43 +12,42 @@ import 'album.provider.dart';
|
||||
|
||||
class RemoteAlbumState {
|
||||
final List<RemoteAlbum> albums;
|
||||
final List<RemoteAlbum> filteredAlbums;
|
||||
|
||||
const RemoteAlbumState({required this.albums, List<RemoteAlbum>? filteredAlbums})
|
||||
: filteredAlbums = filteredAlbums ?? albums;
|
||||
const RemoteAlbumState({required this.albums});
|
||||
|
||||
RemoteAlbumState copyWith({List<RemoteAlbum>? albums, List<RemoteAlbum>? filteredAlbums}) {
|
||||
return RemoteAlbumState(albums: albums ?? this.albums, filteredAlbums: filteredAlbums ?? this.filteredAlbums);
|
||||
RemoteAlbumState copyWith({List<RemoteAlbum>? 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<RemoteAlbumState> {
|
||||
late RemoteAlbumService _remoteAlbumService;
|
||||
final _logger = Logger('RemoteAlbumNotifier');
|
||||
|
||||
@override
|
||||
RemoteAlbumState build() {
|
||||
_remoteAlbumService = ref.read(remoteAlbumServiceProvider);
|
||||
return const RemoteAlbumState(albums: [], filteredAlbums: []);
|
||||
return const RemoteAlbumState(albums: []);
|
||||
}
|
||||
|
||||
Future<List<RemoteAlbum>> _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);
|
||||
@ -60,19 +59,21 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||
await _getAll();
|
||||
}
|
||||
|
||||
void searchAlbums(String query, String? userId, [QuickFilterMode filterMode = QuickFilterMode.all]) {
|
||||
final filtered = _remoteAlbumService.searchAlbums(state.albums, query, userId, filterMode);
|
||||
|
||||
state = state.copyWith(filteredAlbums: filtered);
|
||||
List<RemoteAlbum> searchAlbums(
|
||||
List<RemoteAlbum> 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<void> sortFilteredAlbums(RemoteAlbumSortMode sortMode, {bool isReverse = false}) async {
|
||||
final sortedAlbums = await _remoteAlbumService.sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse);
|
||||
state = state.copyWith(filteredAlbums: sortedAlbums);
|
||||
Future<List<RemoteAlbum>> sortAlbums(
|
||||
List<RemoteAlbum> albums,
|
||||
RemoteAlbumSortMode sortMode, {
|
||||
bool isReverse = false,
|
||||
}) async {
|
||||
return await _remoteAlbumService.sortAlbums(albums, sortMode, isReverse: isReverse);
|
||||
}
|
||||
|
||||
Future<RemoteAlbum?> createAlbum({
|
||||
@ -83,7 +84,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||
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) {
|
||||
@ -114,11 +115,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||
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) {
|
||||
@ -139,9 +136,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||
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<List<RemoteAsset>> getAssets(String albumId) {
|
||||
@ -164,9 +159,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||
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<void> setActivityStatus(String albumId, bool enabled) {
|
||||
|
25
mobile/lib/utils/album_filter.utils.dart
Normal file
25
mobile/lib/utils/album_filter.utils.dart
Normal file
@ -0,0 +1,25 @@
|
||||
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
|
||||
class AlbumFilter {
|
||||
String? userId;
|
||||
String? query;
|
||||
QuickFilterMode mode;
|
||||
|
||||
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 AlbumSort {
|
||||
RemoteAlbumSortMode mode;
|
||||
bool isReverse;
|
||||
|
||||
AlbumSort({required this.mode, this.isReverse = false});
|
||||
|
||||
AlbumSort copyWith({RemoteAlbumSortMode? mode, bool? isReverse}) {
|
||||
return AlbumSort(mode: mode ?? this.mode, isReverse: isReverse ?? this.isReverse);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user