From 354dd3cc3c110150ba4e22ec35a75cb44ea38eb5 Mon Sep 17 00:00:00 2001 From: Luis Nachtigall <31982496+LeLunZ@users.noreply.github.com> Date: Sat, 7 Feb 2026 05:41:37 +0100 Subject: [PATCH] feat(mobile): enhance album sorting functionality with order handling (#24816) * feat: enhance album sorting functionality with effective order handling * mobile: formatting * test: align album sorting order in unit tests with defaultSortOrder * test(mobile): add reverse order validation for album sorting * chore(PR): remove OppositeSortOrder Extension and move it directly into SortOrder enum * refactor: return sorted list directly in album sorting function * refactor: remove sort_order_extensions.dart --- mobile/lib/constants/enums.dart | 9 +++++++- .../domain/services/remote_album.service.dart | 7 ++++-- .../widgets/album/album_selector.widget.dart | 8 +++++-- .../album/album_sort_by_options.provider.dart | 18 +++++++++------ .../domain/services/album.service_test.dart | 22 ++++++++++++++----- 5 files changed, 47 insertions(+), 17 deletions(-) diff --git a/mobile/lib/constants/enums.dart b/mobile/lib/constants/enums.dart index 26c223afad..350f6b80fa 100644 --- a/mobile/lib/constants/enums.dart +++ b/mobile/lib/constants/enums.dart @@ -1,4 +1,11 @@ -enum SortOrder { asc, desc } +enum SortOrder { + asc, + desc; + + SortOrder reverse() { + return this == SortOrder.asc ? SortOrder.desc : SortOrder.asc; + } +} enum TextSearchType { context, filename, description, ocr } diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart index 68c72255b0..0cf3f3e1c1 100644 --- a/mobile/lib/domain/services/remote_album.service.dart +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:collection/collection.dart'; +import 'package:immich_mobile/constants/enums.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/models/user.model.dart'; @@ -36,6 +37,7 @@ class RemoteAlbumService { AlbumSortMode sortMode, { bool isReverse = false, }) async { + // list of albums sorted ascendingly according to the selected sort mode final List sorted = switch (sortMode) { AlbumSortMode.created => albums.sortedBy((album) => album.createdAt), AlbumSortMode.title => albums.sortedBy((album) => album.name), @@ -44,8 +46,9 @@ class RemoteAlbumService { AlbumSortMode.mostRecent => await _sortByNewestAsset(albums), AlbumSortMode.mostOldest => await _sortByOldestAsset(albums), }; + final effectiveOrder = isReverse ? sortMode.defaultOrder.reverse() : sortMode.defaultOrder; - return (isReverse ? sorted.reversed : sorted).toList(); + return (effectiveOrder == SortOrder.asc ? sorted : sorted.reversed).toList(); } List searchAlbums( @@ -209,6 +212,6 @@ class RemoteAlbumService { return aDate.compareTo(bDate); }); - return sorted.reversed.toList(); + return sorted; } } diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index e35fbf7433..8f3cee9215 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -5,6 +5,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.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/extensions/build_context_extensions.dart'; @@ -281,6 +282,8 @@ class _SortButtonState extends ConsumerState<_SortButton> { setState(() { albumSortOption = sortMode; isSorting = true; + // reset sort order to default state when switching option + albumSortIsReverse = false; }); } @@ -293,6 +296,7 @@ class _SortButtonState extends ConsumerState<_SortButton> { @override Widget build(BuildContext context) { + final effectiveOrder = albumSortOption.effectiveOrder(albumSortIsReverse); return MenuAnchor( controller: widget.controller, style: MenuStyle( @@ -307,7 +311,7 @@ class _SortButtonState extends ConsumerState<_SortButton> { .map( (sortMode) => MenuItemButton( leadingIcon: albumSortOption == sortMode - ? albumSortIsReverse + ? effectiveOrder == SortOrder.desc ? Icon( Icons.keyboard_arrow_down, color: albumSortOption == sortMode @@ -355,7 +359,7 @@ class _SortButtonState extends ConsumerState<_SortButton> { children: [ Padding( padding: const EdgeInsets.only(right: 5), - child: albumSortIsReverse + child: effectiveOrder == SortOrder.desc ? Icon(Icons.keyboard_arrow_down, color: context.colorScheme.onSurface) : Icon(Icons.keyboard_arrow_up_rounded, color: context.colorScheme.onSurface), ), diff --git a/mobile/lib/providers/album/album_sort_by_options.provider.dart b/mobile/lib/providers/album/album_sort_by_options.provider.dart index 3dd09f1282..c969dbd37d 100644 --- a/mobile/lib/providers/album/album_sort_by_options.provider.dart +++ b/mobile/lib/providers/album/album_sort_by_options.provider.dart @@ -1,4 +1,5 @@ import 'package:collection/collection.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/entities/album.entity.dart'; @@ -73,18 +74,21 @@ class _AlbumSortHandlers { // 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, "sort_recent", _AlbumSortHandlers.mostRecent), - mostOldest(5, "sort_oldest", _AlbumSortHandlers.mostOldest); + title(1, "library_page_sort_title", _AlbumSortHandlers.title, SortOrder.asc), + assetCount(4, "library_page_sort_asset_count", _AlbumSortHandlers.assetCount, SortOrder.desc), + lastModified(3, "library_page_sort_last_modified", _AlbumSortHandlers.lastModified, SortOrder.desc), + created(0, "library_page_sort_created", _AlbumSortHandlers.created, SortOrder.desc), + mostRecent(2, "sort_recent", _AlbumSortHandlers.mostRecent, SortOrder.desc), + mostOldest(5, "sort_oldest", _AlbumSortHandlers.mostOldest, SortOrder.asc); final int storeIndex; final String label; final AlbumSortFn sortFn; + final SortOrder defaultOrder; - const AlbumSortMode(this.storeIndex, this.label, this.sortFn); + const AlbumSortMode(this.storeIndex, this.label, this.sortFn, this.defaultOrder); + + SortOrder effectiveOrder(bool isReverse) => isReverse ? defaultOrder.reverse() : defaultOrder; } @riverpod diff --git a/mobile/test/domain/services/album.service_test.dart b/mobile/test/domain/services/album.service_test.dart index b86819536d..1a36a811c3 100644 --- a/mobile/test/domain/services/album.service_test.dart +++ b/mobile/test/domain/services/album.service_test.dart @@ -85,35 +85,47 @@ void main() { final albums = [albumB, albumA]; final result = await sut.sortAlbums(albums, AlbumSortMode.created); - expect(result, [albumA, albumB]); + expect(result, [albumB, albumA]); }); test('should sort correctly based on updatedAt', () async { final albums = [albumB, albumA]; final result = await sut.sortAlbums(albums, AlbumSortMode.lastModified); - expect(result, [albumA, albumB]); + expect(result, [albumB, albumA]); }); test('should sort correctly based on assetCount', () async { final albums = [albumB, albumA]; final result = await sut.sortAlbums(albums, AlbumSortMode.assetCount); - expect(result, [albumA, albumB]); + expect(result, [albumB, albumA]); }); test('should sort correctly based on newestAssetTimestamp', () async { final albums = [albumB, albumA]; final result = await sut.sortAlbums(albums, AlbumSortMode.mostRecent); - expect(result, [albumA, albumB]); + expect(result, [albumB, albumA]); }); test('should sort correctly based on oldestAssetTimestamp', () async { final albums = [albumB, albumA]; final result = await sut.sortAlbums(albums, AlbumSortMode.mostOldest); - expect(result, [albumB, albumA]); + expect(result, [albumA, albumB]); + }); + + test('should flip order when isReverse is true for all modes', () async { + final albums = [albumB, albumA]; + + for (final mode in AlbumSortMode.values) { + final normal = await sut.sortAlbums(albums, mode, isReverse: false); + final reversed = await sut.sortAlbums(albums, mode, isReverse: true); + + // reversed should be the exact inverse of normal + expect(reversed, normal.reversed.toList(), reason: 'Mode: $mode'); + } }); }); }