Compare commits
6 commits
main
...
refactor/m
Author | SHA1 | Date | |
---|---|---|---|
|
42731000cf | ||
|
1ce65b39fb | ||
|
4a87ec1b04 | ||
|
e797998a85 | ||
|
1867020746 | ||
|
a753832c04 |
13 changed files with 548 additions and 92 deletions
|
@ -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.",
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
43
mobile/lib/modules/album/providers/album_sort_by_options.provider.g.dart
generated
Normal file
43
mobile/lib/modules/album/providers/album_sort_by_options.provider.g.dart
generated
Normal 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
|
|
@ -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) : () {},
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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(
|
||||||
|
|
54
mobile/test/album.stub.dart
Normal file
54
mobile/test/album.stub.dart
Normal 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]);
|
||||||
|
}
|
182
mobile/test/album_sort_by_options_provider_test.dart
Normal file
182
mobile/test/album_sort_by_options_provider_test.dart
Normal 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,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
37
mobile/test/asset.stub.dart
Normal file
37
mobile/test/asset.stub.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
21
mobile/test/user.stub.dart
Normal file
21
mobile/test/user.stub.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in a new issue