Compare commits

...

6 commits

Author SHA1 Message Date
shalong-tanwen
42731000cf refactor: rename to AlbumSortByOptions 2023-12-06 22:56:18 +05:30
shalong-tanwen
1ce65b39fb refactor: use listview to render shared albums 2023-12-06 15:18:58 +05:30
shalong-tanwen
4a87ec1b04 refactor: sort shared albums with user selected sort 2023-12-06 15:05:43 +05:30
shalong-tanwen
e797998a85 test(mobile): album_sort_options_provider unit tests 2023-12-06 14:44:53 +05:30
shalong-tanwen
1867020746 refactor: use sort order in add to album sheet list 2023-12-06 03:15:58 +05:30
shalong-tanwen
a753832c04 refactor: migrate album sort option to provider 2023-12-06 03:15:45 +05:30
13 changed files with 548 additions and 92 deletions

View file

@ -196,9 +196,11 @@
"library_page_favorites": "Favorites", "library_page_favorites": "Favorites",
"library_page_new_album": "New album", "library_page_new_album": "New album",
"library_page_sharing": "Sharing", "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_last_modified": "Last modified",
"library_page_sort_most_recent_photo": "Most recent photo", "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", "library_page_sort_title": "Album title",
"login_disabled": "Login has been disabled", "login_disabled": "Login has been disabled",
"login_form_api_exception": "API exception. Please check the server URL and try again.", "login_form_api_exception": "API exception. Please check the server URL and try again.",

View file

@ -0,0 +1,135 @@
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<Album> Function(List<Album> albums, bool isReverse);
class _AlbumSortHandlers {
const _AlbumSortHandlers._();
static const AlbumSortFn created = _sortByCreated;
static List<Album> _sortByCreated(List<Album> albums, bool isReverse) {
final sorted =
albums.where((a) => a.isRemote).sortedBy((album) => album.createdAt);
return (isReverse ? sorted.reversed : sorted).toList();
}
static const AlbumSortFn title = _sortByTitle;
static List<Album> _sortByTitle(List<Album> albums, bool isReverse) {
final sorted =
albums.where((a) => a.isRemote).sortedBy((album) => album.name);
return (isReverse ? sorted.reversed : sorted).toList();
}
static const AlbumSortFn lastModified = _sortByLastModified;
static List<Album> _sortByLastModified(List<Album> albums, bool isReverse) {
final sorted =
albums.where((a) => a.isRemote).sortedBy((album) => album.modifiedAt);
return (isReverse ? sorted.reversed : sorted).toList();
}
static const AlbumSortFn assetCount = _sortByAssetCount;
static List<Album> _sortByAssetCount(List<Album> albums, bool isReverse) {
final sorted = albums
.where((a) => a.isRemote)
.sorted((a, b) => a.assetCount.compareTo(b.assetCount));
return (isReverse ? sorted.reversed : sorted).toList();
}
static const AlbumSortFn mostRecent = _sortByMostRecent;
static List<Album> _sortByMostRecent(List<Album> albums, bool isReverse) {
final sorted = albums.where((a) => a.isRemote).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<Album> _sortByMostOldest(List<Album> albums, bool isReverse) {
final sorted = albums.where((a) => a.isRemote).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);
}
}

View file

@ -0,0 +1,43 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'album_sort_by_options.provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$albumSortByOptionsHash() =>
r'8d22fa8b7cbca2d3d7ed20a83bf00211dc948004';
/// See also [AlbumSortByOptions].
@ProviderFor(AlbumSortByOptions)
final albumSortByOptionsProvider =
AutoDisposeNotifierProvider<AlbumSortByOptions, AlbumSortMode>.internal(
AlbumSortByOptions.new,
name: r'albumSortByOptionsProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$albumSortByOptionsHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AlbumSortByOptions = AutoDisposeNotifier<AlbumSortMode>;
String _$albumSortOrderHash() => r'573dea45b4519e69386fc7104c72522e35713440';
/// See also [AlbumSortOrder].
@ProviderFor(AlbumSortOrder)
final albumSortOrderProvider =
AutoDisposeNotifierProvider<AlbumSortOrder, bool>.internal(
AlbumSortOrder.new,
name: r'albumSortOrderProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$albumSortOrderHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AlbumSortOrder = AutoDisposeNotifier<bool>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -1,6 +1,7 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/modules/album/ui/album_thumbnail_listtile.dart';
import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/album.dart';
@ -21,33 +22,44 @@ class AddToAlbumSliverList extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { 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( return SliverList(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
childCount: albums.length + (sharedAlbums.isEmpty ? 0 : 1), childCount: albums.length + (sharedAlbums.isEmpty ? 0 : 1),
(context, index) { (context, index) {
// Build shared expander // Build shared expander
if (index == 0 && sharedAlbums.isNotEmpty) { if (index == 0 && sortedSharedAlbums.isNotEmpty) {
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 8),
child: ExpansionTile( child: ExpansionTile(
title: Text('common_shared'.tr()), title: Text('common_shared'.tr()),
tilePadding: const EdgeInsets.symmetric(horizontal: 10.0), tilePadding: const EdgeInsets.symmetric(horizontal: 10.0),
leading: const Icon(Icons.group), leading: const Icon(Icons.group),
children: sharedAlbums children: [
.map( ListView.builder(
(album) => AlbumThumbnailListTile( shrinkWrap: true,
album: album, physics: const ClampingScrollPhysics(),
onTap: enabled ? () => onAddToAlbum(album) : () {}, itemCount: sortedSharedAlbums.length,
), itemBuilder: (context, index) => AlbumThumbnailListTile(
) album: sortedSharedAlbums[index],
.toList(), onTap: enabled
? () => onAddToAlbum(sortedSharedAlbums[index])
: () {},
),
),
],
), ),
); );
} }
// Build albums list // Build albums list
final offset = index - (sharedAlbums.isNotEmpty ? 1 : 0); final offset = index - (sharedAlbums.isNotEmpty ? 1 : 0);
final album = albums[offset]; final album = sortedAlbums[offset];
return AlbumThumbnailListTile( return AlbumThumbnailListTile(
album: album, album: album,
onTap: enabled ? () => onAddToAlbum(album) : () {}, onTap: enabled ? () => onAddToAlbum(album) : () {},

View file

@ -110,6 +110,7 @@ class AlbumThumbnailCard extends StatelessWidget {
width: cardSize, width: cardSize,
child: Text( child: Text(
album.name, album.name,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyMedium?.copyWith( style: context.textTheme.bodyMedium?.copyWith(
color: context.primaryColor, color: context.primaryColor,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,

View file

@ -1,15 +1,12 @@
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.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.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/modules/album/ui/album_thumbnail_card.dart';
import 'package:immich_mobile/routing/router.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/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/ui/immich_app_bar.dart'; import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
@ -21,8 +18,9 @@ class LibraryPage extends HookConsumerWidget {
final trashEnabled = final trashEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
final albums = ref.watch(albumProvider); final albums = ref.watch(albumProvider);
var isDarkTheme = context.isDarkTheme; final isDarkTheme = context.isDarkTheme;
var settings = ref.watch(appSettingsServiceProvider); final albumSortOption = ref.watch(albumSortByOptionsProvider);
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
useEffect( useEffect(
() { () {
@ -32,64 +30,15 @@ class LibraryPage extends HookConsumerWidget {
[], [],
); );
final selectedAlbumSortOrder =
useState(settings.getSetting(AppSettingsEnum.selectedAlbumSortOrder));
List<Album> 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() { 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( return PopupMenuButton(
position: PopupMenuPosition.over, position: PopupMenuPosition.over,
itemBuilder: (BuildContext context) { itemBuilder: (BuildContext context) {
return options.mapIndexed<PopupMenuEntry<int>>((index, option) { return AlbumSortMode.values
final selected = selectedAlbumSortOrder.value == index; .map<PopupMenuEntry<AlbumSortMode>>((option) {
final selected = albumSortOption == option;
return PopupMenuItem( return PopupMenuItem(
value: index, value: option,
child: Row( child: Row(
children: [ children: [
Padding( Padding(
@ -101,7 +50,7 @@ class LibraryPage extends HookConsumerWidget {
), ),
), ),
Text( Text(
option, option.label.tr(),
style: TextStyle( style: TextStyle(
color: selected ? context.primaryColor : null, color: selected ? context.primaryColor : null,
fontSize: 12.0, fontSize: 12.0,
@ -112,19 +61,31 @@ class LibraryPage extends HookConsumerWidget {
); );
}).toList(); }).toList();
}, },
onSelected: (int value) { onSelected: (AlbumSortMode value) {
selectedAlbumSortOrder.value = value; final selected = albumSortOption == value;
settings.setSetting(AppSettingsEnum.selectedAlbumSortOrder, value); // Switch direction
if (selected) {
ref
.read(albumSortOrderProvider.notifier)
.changeSortDirection(!albumSortIsReverse);
} else {
ref.read(albumSortByOptionsProvider.notifier).changeSortMode(value);
}
}, },
child: Row( child: Row(
children: [ children: [
Icon( Padding(
Icons.swap_vert_rounded, padding: const EdgeInsets.only(right: 5),
size: 18, child: Icon(
color: context.primaryColor, albumSortIsReverse
? Icons.arrow_downward_rounded
: Icons.arrow_upward_rounded,
size: 14,
color: context.primaryColor,
),
), ),
Text( Text(
options[selectedAlbumSortOrder.value], albumSortOption.label.tr(),
style: context.textTheme.labelLarge?.copyWith( style: context.textTheme.labelLarge?.copyWith(
color: context.primaryColor, color: context.primaryColor,
), ),
@ -140,9 +101,8 @@ class LibraryPage extends HookConsumerWidget {
var cardSize = constraints.maxWidth; var cardSize = constraints.maxWidth;
return GestureDetector( return GestureDetector(
onTap: () { onTap: () =>
context.autoPush(CreateAlbumRoute(isSharedAlbum: false)); context.autoPush(CreateAlbumRoute(isSharedAlbum: false)),
},
child: Padding( child: Padding(
padding: padding:
const EdgeInsets.only(bottom: 32), // Adjust padding to suit const EdgeInsets.only(bottom: 32), // Adjust padding to suit
@ -160,7 +120,7 @@ class LibraryPage extends HookConsumerWidget {
: const Color.fromARGB(255, 203, 203, 203), : const Color.fromARGB(255, 203, 203, 203),
), ),
color: isDarkTheme ? Colors.grey[900] : Colors.grey[50], color: isDarkTheme ? Colors.grey[900] : Colors.grey[50],
borderRadius: BorderRadius.circular(20), borderRadius: const BorderRadius.all(Radius.circular(20)),
), ),
child: Center( child: Center(
child: Icon( child: Icon(
@ -223,7 +183,7 @@ class LibraryPage extends HookConsumerWidget {
); );
} }
final sorted = sortedAlbums(); final sorted = albumSortOption.sortFn(albums, albumSortIsReverse);
final local = albums.where((a) => a.isLocal).toList(); final local = albums.where((a) => a.isLocal).toList();
@ -231,7 +191,7 @@ class LibraryPage extends HookConsumerWidget {
return trashEnabled return trashEnabled
? InkWell( ? InkWell(
onTap: () => context.autoPush(const TrashRoute()), onTap: () => context.autoPush(const TrashRoute()),
borderRadius: BorderRadius.circular(12), borderRadius: const BorderRadius.all(Radius.circular(12)),
child: const Icon( child: const Icon(
Icons.delete_rounded, Icons.delete_rounded,
size: 25, size: 25,

View file

@ -3,12 +3,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.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/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.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/providers/partner.provider.dart';
import 'package:immich_mobile/modules/partner/ui/partner_list.dart'; import 'package:immich_mobile/modules/partner/ui/partner_list.dart';
import 'package:immich_mobile/routing/router.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/providers/user.provider.dart';
import 'package:immich_mobile/shared/ui/immich_app_bar.dart'; import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart';
@ -18,7 +18,10 @@ class SharingPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final List<Album> 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 userId = ref.watch(currentUserProvider)?.id;
final partner = ref.watch(partnerSharedWithProvider); final partner = ref.watch(partnerSharedWithProvider);
@ -68,7 +71,7 @@ class SharingPage extends HookConsumerWidget {
return ListTile( return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 12), contentPadding: const EdgeInsets.symmetric(horizontal: 12),
leading: ClipRRect( leading: ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: ImmichImage( child: ImmichImage(
album.thumbnail.value, album.thumbnail.value,
width: 60, width: 60,
@ -167,9 +170,9 @@ class SharingPage extends HookConsumerWidget {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Card( child: Card(
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.all(Radius.circular(20)),
side: const BorderSide( side: BorderSide(
color: Colors.grey, color: Colors.grey,
width: 0.5, width: 0.5,
), ),
@ -212,7 +215,7 @@ class SharingPage extends HookConsumerWidget {
Widget sharePartnerButton() { Widget sharePartnerButton() {
return InkWell( return InkWell(
onTap: () => context.autoPush(const PartnerRoute()), onTap: () => context.autoPush(const PartnerRoute()),
borderRadius: BorderRadius.circular(12), borderRadius: const BorderRadius.all(Radius.circular(12)),
child: const Icon( child: const Icon(
Icons.swap_horizontal_circle_rounded, Icons.swap_horizontal_circle_rounded,
size: 25, size: 25,

View file

@ -51,6 +51,11 @@ enum AppSettingsEnum<T> {
mapIncludeArchived<bool>(StoreKey.mapIncludeArchived, null, false), mapIncludeArchived<bool>(StoreKey.mapIncludeArchived, null, false),
mapRelativeDate<int>(StoreKey.mapRelativeDate, null, 0), mapRelativeDate<int>(StoreKey.mapRelativeDate, null, 0),
allowSelfSignedSSLCert<bool>(StoreKey.selfSignedCert, null, false), allowSelfSignedSSLCert<bool>(StoreKey.selfSignedCert, null, false),
selectedAlbumSortReverse<bool>(
StoreKey.selectedAlbumSortReverse,
null,
false,
),
; ;
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);

View file

@ -182,6 +182,7 @@ enum StoreKey<T> {
mapRelativeDate<int>(119, type: int), mapRelativeDate<int>(119, type: int),
selfSignedCert<bool>(120, type: bool), selfSignedCert<bool>(120, type: bool),
mapIncludeArchived<bool>(121, type: bool), mapIncludeArchived<bool>(121, type: bool),
selectedAlbumSortReverse<bool>(122, type: bool),
; ;
const StoreKey( const StoreKey(

View file

@ -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]);
}

View file

@ -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,
],
);
});
});
}

View file

@ -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,
);
}

View file

@ -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,
);
}