From c5504aae6eb6a004241a652698b524cf1650bce5 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Thu, 7 Dec 2023 18:14:09 +0000 Subject: [PATCH] refactor(mobile): album sort (#5510) * refactor: migrate album sort option to provider * refactor: use sort order in add to album sheet list * test(mobile): album_sort_options_provider unit tests * refactor: sort shared albums with user selected sort * refactor: use listview to render shared albums * refactor: rename to AlbumSortByOptions * refactor: remove filtering inside sort functions * font size --------- Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex Tran --- mobile/assets/i18n/en-US.json | 4 +- .../album_sort_by_options.provider.dart | 131 +++++++++++++ .../album_sort_by_options.provider.g.dart | Bin 0 -> 1522 bytes .../album/ui/add_to_album_sliverlist.dart | 32 ++- .../album/ui/album_thumbnail_card.dart | 1 + .../lib/modules/album/views/library_page.dart | 112 ++++------- .../lib/modules/album/views/sharing_page.dart | 17 +- .../services/app_settings.service.dart | 5 + mobile/lib/shared/models/store.dart | 1 + mobile/test/album.stub.dart | 54 ++++++ .../album_sort_by_options_provider_test.dart | 182 ++++++++++++++++++ mobile/test/asset.stub.dart | 37 ++++ mobile/test/user.stub.dart | 21 ++ 13 files changed, 503 insertions(+), 94 deletions(-) create mode 100644 mobile/lib/modules/album/providers/album_sort_by_options.provider.dart create mode 100644 mobile/lib/modules/album/providers/album_sort_by_options.provider.g.dart create mode 100644 mobile/test/album.stub.dart create mode 100644 mobile/test/album_sort_by_options_provider_test.dart create mode 100644 mobile/test/asset.stub.dart create mode 100644 mobile/test/user.stub.dart diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 099cfd5752..c691f13ac3 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -195,9 +195,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.", diff --git a/mobile/lib/modules/album/providers/album_sort_by_options.provider.dart b/mobile/lib/modules/album/providers/album_sort_by_options.provider.dart new file mode 100644 index 0000000000..2309e19aa7 --- /dev/null +++ b/mobile/lib/modules/album/providers/album_sort_by_options.provider.dart @@ -0,0 +1,131 @@ +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_by_options.provider.g.dart'; + +typedef AlbumSortFn = List Function(List albums, bool isReverse); + +class _AlbumSortHandlers { + const _AlbumSortHandlers._(); + + static const AlbumSortFn created = _sortByCreated; + static List _sortByCreated(List albums, bool isReverse) { + final sorted = albums.sortedBy((album) => album.createdAt); + return (isReverse ? sorted.reversed : sorted).toList(); + } + + static const AlbumSortFn title = _sortByTitle; + static List _sortByTitle(List albums, bool isReverse) { + final sorted = albums.sortedBy((album) => album.name); + return (isReverse ? sorted.reversed : sorted).toList(); + } + + static const AlbumSortFn lastModified = _sortByLastModified; + static List _sortByLastModified(List albums, bool isReverse) { + final sorted = albums.sortedBy((album) => album.modifiedAt); + return (isReverse ? sorted.reversed : sorted).toList(); + } + + static const AlbumSortFn assetCount = _sortByAssetCount; + static List _sortByAssetCount(List albums, bool isReverse) { + final sorted = + albums.sorted((a, b) => a.assetCount.compareTo(b.assetCount)); + return (isReverse ? sorted.reversed : sorted).toList(); + } + + static const AlbumSortFn mostRecent = _sortByMostRecent; + static List _sortByMostRecent(List albums, bool isReverse) { + final sorted = albums.sorted((a, b) { + if (a.endDate != null && b.endDate != null) { + return a.endDate!.compareTo(b.endDate!); + } + 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 _sortByMostOldest(List albums, bool isReverse) { + final sorted = albums.sorted((a, b) { + if (a.startDate != null && b.startDate != null) { + return a.startDate!.compareTo(b.startDate!); + } + 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 AlbumSortByOptions extends _$AlbumSortByOptions { + @override + AlbumSortMode build() { + final sortOpt = ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.selectedAlbumSortOrder); + return AlbumSortMode.values.firstWhere( + (e) => e.index == sortOpt, + orElse: () => 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); + } +} diff --git a/mobile/lib/modules/album/providers/album_sort_by_options.provider.g.dart b/mobile/lib/modules/album/providers/album_sort_by_options.provider.g.dart new file mode 100644 index 0000000000000000000000000000000000000000..d97057ebddc16c80f9ac62123870261d108dfbc1 GIT binary patch literal 1522 zcmchX-*4J55Xay1SKPxSU}7PJP=0i<(T37J6sE#UHBFHdUzjDwj%}`{y5vrW4K+#bNB%93YM!4Jgnj`cTaHh1oyLL?0F?o24oHm z!qciqv{WWZ_lYbGlS2EYk~>CG`PAwf*Phqu!235_b+$*gLsd$;K!J)FsouDRw>FB2 zEhO(;hilv5&3<)g@1E!%%@$0i9@wQCLmGxT8K#3Q%}7XlbbvGr$gr2@K@fJkG#f?3 zAczh|bYP7Lglh@kX0>}Sa)0-|&tIS2yOXMUu4s8V6NK9gf1W*_gNeJ?tTHlYx|AB1 z(y*MNIx0S%r)|S&??F8#I`~zOmeA`oN+rgHDMEO2kq6U zuF_hN$UMU9Mq)1B1Lr;-!aWneyD3u8~a;(mU_Pct`zUi literal 0 HcmV?d00001 diff --git a/mobile/lib/modules/album/ui/add_to_album_sliverlist.dart b/mobile/lib/modules/album/ui/add_to_album_sliverlist.dart index d18740a9ad..d948afcd8e 100644 --- a/mobile/lib/modules/album/ui/add_to_album_sliverlist.dart +++ b/mobile/lib/modules/album/ui/add_to_album_sliverlist.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/album/providers/album_sort_by_options.provider.dart'; import 'package:immich_mobile/modules/album/ui/album_thumbnail_listtile.dart'; import 'package:immich_mobile/shared/models/album.dart'; @@ -21,33 +22,44 @@ class AddToAlbumSliverList extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final albumSortMode = ref.watch(albumSortByOptionsProvider); + final albumSortIsReverse = ref.watch(albumSortOrderProvider); + final sortedAlbums = albumSortMode.sortFn(albums, albumSortIsReverse); + final sortedSharedAlbums = + albumSortMode.sortFn(sharedAlbums, albumSortIsReverse); + return SliverList( delegate: SliverChildBuilderDelegate( childCount: albums.length + (sharedAlbums.isEmpty ? 0 : 1), (context, index) { // Build shared expander - if (index == 0 && sharedAlbums.isNotEmpty) { + if (index == 0 && sortedSharedAlbums.isNotEmpty) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: ExpansionTile( title: Text('common_shared'.tr()), tilePadding: const EdgeInsets.symmetric(horizontal: 10.0), leading: const Icon(Icons.group), - children: sharedAlbums - .map( - (album) => AlbumThumbnailListTile( - album: album, - onTap: enabled ? () => onAddToAlbum(album) : () {}, - ), - ) - .toList(), + children: [ + ListView.builder( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + itemCount: sortedSharedAlbums.length, + itemBuilder: (context, index) => AlbumThumbnailListTile( + album: sortedSharedAlbums[index], + onTap: enabled + ? () => onAddToAlbum(sortedSharedAlbums[index]) + : () {}, + ), + ), + ], ), ); } // Build albums list final offset = index - (sharedAlbums.isNotEmpty ? 1 : 0); - final album = albums[offset]; + final album = sortedAlbums[offset]; return AlbumThumbnailListTile( album: album, onTap: enabled ? () => onAddToAlbum(album) : () {}, diff --git a/mobile/lib/modules/album/ui/album_thumbnail_card.dart b/mobile/lib/modules/album/ui/album_thumbnail_card.dart index b295deec54..1ef032d55c 100644 --- a/mobile/lib/modules/album/ui/album_thumbnail_card.dart +++ b/mobile/lib/modules/album/ui/album_thumbnail_card.dart @@ -110,6 +110,7 @@ class AlbumThumbnailCard extends StatelessWidget { width: cardSize, child: Text( album.name, + overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium?.copyWith( color: context.primaryColor, fontWeight: FontWeight.w500, diff --git a/mobile/lib/modules/album/views/library_page.dart b/mobile/lib/modules/album/views/library_page.dart index a902dfdb21..605ffdc8ef 100644 --- a/mobile/lib/modules/album/views/library_page.dart +++ b/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_by_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(albumSortByOptionsProvider); + final albumSortIsReverse = ref.watch(albumSortOrderProvider); useEffect( () { @@ -32,64 +30,15 @@ class LibraryPage extends HookConsumerWidget { [], ); - final selectedAlbumSortOrder = - useState(settings.getSetting(AppSettingsEnum.selectedAlbumSortOrder)); - - List 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>((index, option) { - final selected = selectedAlbumSortOrder.value == index; + return AlbumSortMode.values + .map>((option) { + final selected = albumSortOption == option; return PopupMenuItem( - value: index, + value: option, child: Row( children: [ Padding( @@ -101,10 +50,10 @@ class LibraryPage extends HookConsumerWidget { ), ), Text( - option, + option.label.tr(), style: TextStyle( color: selected ? context.primaryColor : null, - fontSize: 12.0, + fontSize: 14.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(albumSortByOptionsProvider.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,15 +183,15 @@ class LibraryPage extends HookConsumerWidget { ); } - final sorted = sortedAlbums(); - + final remote = albums.where((a) => a.isRemote).toList(); + final sorted = albumSortOption.sortFn(remote, albumSortIsReverse); final local = albums.where((a) => a.isLocal).toList(); Widget? shareTrashButton() { 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, diff --git a/mobile/lib/modules/album/views/sharing_page.dart b/mobile/lib/modules/album/views/sharing_page.dart index 2e2e44aca9..9defb19bd5 100644 --- a/mobile/lib/modules/album/views/sharing_page.dart +++ b/mobile/lib/modules/album/views/sharing_page.dart @@ -3,12 +3,12 @@ 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_sort_by_options.provider.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart'; import 'package:immich_mobile/modules/partner/providers/partner.provider.dart'; import 'package:immich_mobile/modules/partner/ui/partner_list.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/providers/user.provider.dart'; import 'package:immich_mobile/shared/ui/immich_app_bar.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; @@ -18,7 +18,10 @@ class SharingPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final List sharedAlbums = ref.watch(sharedAlbumProvider); + final albumSortOption = ref.watch(albumSortByOptionsProvider); + final albumSortIsReverse = ref.watch(albumSortOrderProvider); + final albums = ref.watch(sharedAlbumProvider); + final sharedAlbums = albumSortOption.sortFn(albums, albumSortIsReverse); final userId = ref.watch(currentUserProvider)?.id; final partner = ref.watch(partnerSharedWithProvider); @@ -68,7 +71,7 @@ class SharingPage extends HookConsumerWidget { return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 12), leading: ClipRRect( - borderRadius: BorderRadius.circular(8), + borderRadius: const BorderRadius.all(Radius.circular(8)), child: ImmichImage( album.thumbnail.value, width: 60, @@ -167,9 +170,9 @@ class SharingPage extends HookConsumerWidget { padding: const EdgeInsets.all(8.0), child: Card( elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - side: const BorderSide( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(20)), + side: BorderSide( color: Colors.grey, width: 0.5, ), @@ -212,7 +215,7 @@ class SharingPage extends HookConsumerWidget { Widget sharePartnerButton() { return InkWell( onTap: () => context.autoPush(const PartnerRoute()), - borderRadius: BorderRadius.circular(12), + borderRadius: const BorderRadius.all(Radius.circular(12)), child: const Icon( Icons.swap_horizontal_circle_rounded, size: 25, diff --git a/mobile/lib/modules/settings/services/app_settings.service.dart b/mobile/lib/modules/settings/services/app_settings.service.dart index 509dd5a93c..7e43b2103d 100644 --- a/mobile/lib/modules/settings/services/app_settings.service.dart +++ b/mobile/lib/modules/settings/services/app_settings.service.dart @@ -52,6 +52,11 @@ enum AppSettingsEnum { mapRelativeDate(StoreKey.mapRelativeDate, null, 0), allowSelfSignedSSLCert(StoreKey.selfSignedCert, null, false), ignoreIcloudAssets(StoreKey.ignoreIcloudAssets, null, false), + selectedAlbumSortReverse( + StoreKey.selectedAlbumSortReverse, + null, + false, + ), ; const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); diff --git a/mobile/lib/shared/models/store.dart b/mobile/lib/shared/models/store.dart index 7887f7255c..b8b3ba8a5c 100644 --- a/mobile/lib/shared/models/store.dart +++ b/mobile/lib/shared/models/store.dart @@ -183,6 +183,7 @@ enum StoreKey { selfSignedCert(120, type: bool), mapIncludeArchived(121, type: bool), ignoreIcloudAssets(122, type: bool), + selectedAlbumSortReverse(123, type: bool), ; const StoreKey( diff --git a/mobile/test/album.stub.dart b/mobile/test/album.stub.dart new file mode 100644 index 0000000000..a7c6302630 --- /dev/null +++ b/mobile/test/album.stub.dart @@ -0,0 +1,54 @@ +import 'package:immich_mobile/shared/models/album.dart'; + +import 'asset.stub.dart'; +import 'user.stub.dart'; + +final class AlbumStub { + const AlbumStub._(); + + static final emptyAlbum = Album( + name: "empty-album", + localId: "empty-album-local", + remoteId: "empty-album-remote", + createdAt: DateTime(2000), + modifiedAt: DateTime(2023), + shared: false, + activityEnabled: false, + startDate: DateTime(2020), + ); + + static final sharedWithUser = Album( + name: "empty-album-shared-with-user", + localId: "empty-album-shared-with-user-local", + remoteId: "empty-album-shared-with-user-remote", + createdAt: DateTime(2023), + modifiedAt: DateTime(2023), + shared: true, + activityEnabled: false, + endDate: DateTime(2020), + )..sharedUsers.addAll([UserStub.admin]); + + static final oneAsset = Album( + name: "album-with-single-asset", + localId: "album-with-single-asset-local", + remoteId: "album-with-single-asset-remote", + createdAt: DateTime(2022), + modifiedAt: DateTime(2023), + shared: false, + activityEnabled: false, + startDate: DateTime(2020), + endDate: DateTime(2023), + )..assets.addAll([AssetStub.image1]); + + static final twoAsset = Album( + name: "album-with-two-assets", + localId: "album-with-two-assets-local", + remoteId: "album-with-two-assets-remote", + createdAt: DateTime(2001), + modifiedAt: DateTime(2010), + shared: false, + activityEnabled: false, + startDate: DateTime(2019), + endDate: DateTime(2020), + )..assets.addAll([AssetStub.image1, AssetStub.image2]); +} diff --git a/mobile/test/album_sort_by_options_provider_test.dart b/mobile/test/album_sort_by_options_provider_test.dart new file mode 100644 index 0000000000..18efc5b423 --- /dev/null +++ b/mobile/test/album_sort_by_options_provider_test.dart @@ -0,0 +1,182 @@ +import 'package:collection/collection.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/modules/album/providers/album_sort_by_options.provider.dart'; +import 'package:immich_mobile/shared/models/album.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/models/user.dart'; +import 'package:isar/isar.dart'; + +import 'album.stub.dart'; +import 'asset.stub.dart'; + +void main() { + late final Isar db; + + setUpAll(() async { + await Isar.initializeIsarCore(download: true); + db = await Isar.open( + [ + AssetSchema, + AlbumSchema, + UserSchema, + ], + maxSizeMiB: 256, + directory: ".", + ); + }); + + final albums = [ + AlbumStub.emptyAlbum, + AlbumStub.sharedWithUser, + AlbumStub.oneAsset, + AlbumStub.twoAsset, + ]; + + setUp(() { + db.writeTxnSync(() { + db.clearSync(); + // Save all assets + db.assets.putAllSync([AssetStub.image1, AssetStub.image2]); + db.albums.putAllSync(albums); + for (final album in albums) { + album.sharedUsers.saveSync(); + album.assets.saveSync(); + } + }); + expect(db.albums.countSync(), 4); + expect(db.assets.countSync(), 2); + }); + + group("Album sort - Created Time", () { + const created = AlbumSortMode.created; + test("Created time - ASC", () { + final sorted = created.sortFn(albums, false); + expect(sorted.isSortedBy((a) => a.createdAt), true); + }); + + test("Created time - DESC", () { + final sorted = created.sortFn(albums, true); + expect( + sorted.isSorted((b, a) => a.createdAt.compareTo(b.createdAt)), + true, + ); + }); + }); + + group("Album sort - Asset count", () { + const assetCount = AlbumSortMode.assetCount; + test("Asset Count - ASC", () { + final sorted = assetCount.sortFn(albums, false); + expect( + sorted.isSorted((a, b) => a.assetCount.compareTo(b.assetCount)), + true, + ); + }); + + test("Asset Count - DESC", () { + final sorted = assetCount.sortFn(albums, true); + expect( + sorted.isSorted((b, a) => a.assetCount.compareTo(b.assetCount)), + true, + ); + }); + }); + + group("Album sort - Last modified", () { + const lastModified = AlbumSortMode.lastModified; + test("Last modified - ASC", () { + final sorted = lastModified.sortFn(albums, false); + expect( + sorted.isSorted((a, b) => a.modifiedAt.compareTo(b.modifiedAt)), + true, + ); + }); + + test("Last modified - DESC", () { + final sorted = lastModified.sortFn(albums, true); + expect( + sorted.isSorted((b, a) => a.modifiedAt.compareTo(b.modifiedAt)), + true, + ); + }); + }); + + group("Album sort - Created", () { + const created = AlbumSortMode.created; + test("Created - ASC", () { + final sorted = created.sortFn(albums, false); + expect( + sorted.isSorted((a, b) => a.createdAt.compareTo(b.createdAt)), + true, + ); + }); + + test("Created - DESC", () { + final sorted = created.sortFn(albums, true); + expect( + sorted.isSorted((b, a) => a.createdAt.compareTo(b.createdAt)), + true, + ); + }); + }); + + group("Album sort - Most Recent", () { + const mostRecent = AlbumSortMode.mostRecent; + + test("Most Recent - ASC", () { + final sorted = mostRecent.sortFn(albums, false); + expect( + sorted, + [ + AlbumStub.sharedWithUser, + AlbumStub.twoAsset, + AlbumStub.oneAsset, + AlbumStub.emptyAlbum, + ], + ); + }); + + test("Most Recent - DESC", () { + final sorted = mostRecent.sortFn(albums, true); + expect( + sorted, + [ + AlbumStub.emptyAlbum, + AlbumStub.oneAsset, + AlbumStub.twoAsset, + AlbumStub.sharedWithUser, + ], + ); + }); + }); + + group("Album sort - Most Oldest", () { + const mostOldest = AlbumSortMode.mostOldest; + + test("Most Oldest - ASC", () { + final sorted = mostOldest.sortFn(albums, false); + expect( + sorted, + [ + AlbumStub.twoAsset, + AlbumStub.emptyAlbum, + AlbumStub.oneAsset, + AlbumStub.sharedWithUser, + ], + ); + }); + + test("Most Oldest - DESC", () { + final sorted = mostOldest.sortFn(albums, true); + expect( + sorted, + [ + AlbumStub.sharedWithUser, + AlbumStub.oneAsset, + AlbumStub.emptyAlbum, + AlbumStub.twoAsset, + ], + ); + }); + }); +} diff --git a/mobile/test/asset.stub.dart b/mobile/test/asset.stub.dart new file mode 100644 index 0000000000..3dba434b2f --- /dev/null +++ b/mobile/test/asset.stub.dart @@ -0,0 +1,37 @@ +import 'package:immich_mobile/shared/models/asset.dart'; + +final class AssetStub { + const AssetStub._(); + + static final image1 = Asset( + checksum: "image1-checksum", + localId: "image1", + ownerId: 1, + fileCreatedAt: DateTime.now(), + fileModifiedAt: DateTime.now(), + updatedAt: DateTime.now(), + durationInSeconds: 0, + type: AssetType.image, + fileName: "image1.jpg", + isFavorite: true, + isArchived: false, + isTrashed: false, + stackCount: 0, + ); + + static final image2 = Asset( + checksum: "image2-checksum", + localId: "image2", + ownerId: 1, + fileCreatedAt: DateTime(2000), + fileModifiedAt: DateTime(2010), + updatedAt: DateTime.now(), + durationInSeconds: 60, + type: AssetType.video, + fileName: "image2.jpg", + isFavorite: false, + isArchived: false, + isTrashed: false, + stackCount: 0, + ); +} diff --git a/mobile/test/user.stub.dart b/mobile/test/user.stub.dart new file mode 100644 index 0000000000..b0dcab094d --- /dev/null +++ b/mobile/test/user.stub.dart @@ -0,0 +1,21 @@ +import 'package:immich_mobile/shared/models/user.dart'; + +final class UserStub { + const UserStub._(); + + static final admin = User( + id: "admin", + updatedAt: DateTime(2021), + email: "admin@test.com", + name: "admin", + isAdmin: true, + ); + + static final user1 = User( + id: "user1", + updatedAt: DateTime(2022), + email: "user1@test.com", + name: "user1", + isAdmin: false, + ); +}