mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(mobile): Home screen customization options (#1563)
* Try staggered layout for home page * Introduce setting for dynamic layout * Fix some provider related bugs * Make asset grouping configurable * Add translation keys, refactor group title * Rename enum values * Fix enum names * Reformat long if statement * Fix timezone related bug * Minor clean up * Fix unit test * Add second assets check back to home screen
This commit is contained in:
		| @@ -12,6 +12,10 @@ | |||||||
|   "album_viewer_appbar_share_leave": "Leave album", |   "album_viewer_appbar_share_leave": "Leave album", | ||||||
|   "album_viewer_appbar_share_remove": "Remove from album", |   "album_viewer_appbar_share_remove": "Remove from album", | ||||||
|   "album_viewer_page_share_add_users": "Add users", |   "album_viewer_page_share_add_users": "Add users", | ||||||
|  |   "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", | ||||||
|  |   "asset_list_layout_settings_group_by": "Group assets by", | ||||||
|  |   "asset_list_layout_settings_group_by_month_day": "Month + day", | ||||||
|  |   "asset_list_layout_settings_group_by_month": "Month", | ||||||
|   "asset_list_settings_subtitle": "Photo grid layout settings", |   "asset_list_settings_subtitle": "Photo grid layout settings", | ||||||
|   "asset_list_settings_title": "Photo Grid", |   "asset_list_settings_title": "Photo Grid", | ||||||
|   "backup_album_selection_page_albums_device": "Albums on device ({})", |   "backup_album_selection_page_albums_device": "Albums on device ({})", | ||||||
| @@ -199,4 +203,4 @@ | |||||||
|   "version_announcement_overlay_text_2": "please take your time to visit the ", |   "version_announcement_overlay_text_2": "please take your time to visit the ", | ||||||
|   "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", |   "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", | ||||||
|   "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89" |   "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import 'dart:math'; | import 'dart:math'; | ||||||
|  |  | ||||||
|  | import 'package:collection/collection.dart'; | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:immich_mobile/shared/models/asset.dart'; | import 'package:immich_mobile/shared/models/asset.dart'; | ||||||
| @@ -9,14 +10,15 @@ final log = Logger('AssetGridDataStructure'); | |||||||
|  |  | ||||||
| enum RenderAssetGridElementType { | enum RenderAssetGridElementType { | ||||||
|   assetRow, |   assetRow, | ||||||
|   dayTitle, |   groupDividerTitle, | ||||||
|   monthTitle; |   monthTitle; | ||||||
| } | } | ||||||
|  |  | ||||||
| class RenderAssetGridRow { | class RenderAssetGridRow { | ||||||
|   final List<Asset> assets; |   final List<Asset> assets; | ||||||
|  |   final List<double> widthDistribution; | ||||||
|  |  | ||||||
|   RenderAssetGridRow(this.assets); |   RenderAssetGridRow(this.assets, this.widthDistribution); | ||||||
| } | } | ||||||
|  |  | ||||||
| class RenderAssetGridElement { | class RenderAssetGridElement { | ||||||
| @@ -35,19 +37,36 @@ class RenderAssetGridElement { | |||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | enum GroupAssetsBy { | ||||||
|  |   day, | ||||||
|  |   month; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class AssetGridLayoutParameters { | ||||||
|  |   final int perRow; | ||||||
|  |   final bool dynamicLayout; | ||||||
|  |   final GroupAssetsBy groupBy; | ||||||
|  |  | ||||||
|  |   AssetGridLayoutParameters( | ||||||
|  |     this.perRow, | ||||||
|  |     this.dynamicLayout, | ||||||
|  |     this.groupBy, | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
| class _AssetGroupsToRenderListComputeParameters { | class _AssetGroupsToRenderListComputeParameters { | ||||||
|   final String monthFormat; |   final String monthFormat; | ||||||
|   final String dayFormat; |   final String dayFormat; | ||||||
|   final String dayFormatYear; |   final String dayFormatYear; | ||||||
|   final Map<String, List<Asset>> groups; |   final List<Asset> assets; | ||||||
|   final int perRow; |   final AssetGridLayoutParameters layout; | ||||||
|  |  | ||||||
|   _AssetGroupsToRenderListComputeParameters( |   _AssetGroupsToRenderListComputeParameters( | ||||||
|     this.monthFormat, |     this.monthFormat, | ||||||
|     this.dayFormat, |     this.dayFormat, | ||||||
|     this.dayFormatYear, |     this.dayFormatYear, | ||||||
|     this.groups, |     this.assets, | ||||||
|     this.perRow, |     this.layout, | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -56,62 +75,75 @@ class RenderList { | |||||||
|  |  | ||||||
|   RenderList(this.elements); |   RenderList(this.elements); | ||||||
|  |  | ||||||
|  |   static Map<String, List<Asset>> _groupAssets( | ||||||
|  |     List<Asset> assets, | ||||||
|  |     GroupAssetsBy groupBy, | ||||||
|  |   ) { | ||||||
|  |     assets.sortByCompare<DateTime>( | ||||||
|  |       (e) => e.createdAt, | ||||||
|  |       (a, b) => b.compareTo(a), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (groupBy == GroupAssetsBy.day) { | ||||||
|  |       return assets.groupListsBy( | ||||||
|  |         (element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()), | ||||||
|  |       ); | ||||||
|  |     } else if (groupBy == GroupAssetsBy.month) { | ||||||
|  |       return assets.groupListsBy( | ||||||
|  |         (element) => DateFormat('y-MM').format(element.createdAt.toLocal()), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return {}; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   static Future<RenderList> _processAssetGroupData( |   static Future<RenderList> _processAssetGroupData( | ||||||
|     _AssetGroupsToRenderListComputeParameters data, |     _AssetGroupsToRenderListComputeParameters data, | ||||||
|   ) async { |   ) async { | ||||||
|     final monthFormat = DateFormat(data.monthFormat); |     final monthFormat = DateFormat(data.monthFormat); | ||||||
|     final dayFormatSameYear = DateFormat(data.dayFormat); |     final dayFormatSameYear = DateFormat(data.dayFormat); | ||||||
|     final dayFormatOtherYear = DateFormat(data.dayFormatYear); |     final dayFormatOtherYear = DateFormat(data.dayFormatYear); | ||||||
|     final groups = data.groups; |     final allAssets = data.assets; | ||||||
|     final perRow = data.perRow; |     final perRow = data.layout.perRow; | ||||||
|  |     final dynamicLayout = data.layout.dynamicLayout; | ||||||
|  |     final groupBy = data.layout.groupBy; | ||||||
|  |  | ||||||
|     List<RenderAssetGridElement> elements = []; |     List<RenderAssetGridElement> elements = []; | ||||||
|     DateTime? lastDate; |     DateTime? lastDate; | ||||||
|  |  | ||||||
|  |     final groups = _groupAssets(allAssets, groupBy); | ||||||
|  |  | ||||||
|     groups.forEach((groupName, assets) { |     groups.forEach((groupName, assets) { | ||||||
|       try { |       try { | ||||||
|         final date = DateTime.parse(groupName); |         final date = assets.first.createdAt.toLocal(); | ||||||
|  |  | ||||||
|         if (lastDate == null || lastDate!.month != date.month) { |  | ||||||
|           // Month title |  | ||||||
|  |  | ||||||
|           var monthTitleText = groupName; |  | ||||||
|  |  | ||||||
|           var groupDate = DateTime.tryParse(groupName); |  | ||||||
|           if (groupDate != null) { |  | ||||||
|             monthTitleText = monthFormat.format(groupDate); |  | ||||||
|           } else { |  | ||||||
|             log.severe("Failed to format date for day title: $groupName"); |  | ||||||
|           } |  | ||||||
|  |  | ||||||
|  |         // Month title | ||||||
|  |         if (groupBy == GroupAssetsBy.day && | ||||||
|  |             (lastDate == null || lastDate!.month != date.month)) { | ||||||
|           elements.add( |           elements.add( | ||||||
|             RenderAssetGridElement( |             RenderAssetGridElement( | ||||||
|               RenderAssetGridElementType.monthTitle, |               RenderAssetGridElementType.monthTitle, | ||||||
|               title: monthTitleText, |               title: monthFormat.format(date), | ||||||
|               date: date, |               date: date, | ||||||
|             ), |             ), | ||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Add group title |         // Group divider title (day or month) | ||||||
|         var currentYear = DateTime.now().year; |         var formatDate = dayFormatOtherYear; | ||||||
|         var groupYear = DateTime.parse(groupName).year; |  | ||||||
|         var formatDate = |  | ||||||
|             currentYear == groupYear ? dayFormatSameYear : dayFormatOtherYear; |  | ||||||
|  |  | ||||||
|         var dateText = groupName; |         if (DateTime.now().year == date.year) { | ||||||
|  |           formatDate = dayFormatSameYear; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         var groupDate = DateTime.tryParse(groupName); |         if (groupBy == GroupAssetsBy.month) { | ||||||
|         if (groupDate != null) { |           formatDate = monthFormat; | ||||||
|           dateText = formatDate.format(groupDate); |  | ||||||
|         } else { |  | ||||||
|           log.severe("Failed to format date for day title: $groupName"); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         elements.add( |         elements.add( | ||||||
|           RenderAssetGridElement( |           RenderAssetGridElement( | ||||||
|             RenderAssetGridElementType.dayTitle, |             RenderAssetGridElementType.groupDividerTitle, | ||||||
|             title: dateText, |             title: formatDate.format(date), | ||||||
|             date: date, |             date: date, | ||||||
|             relatedAssetList: assets, |             relatedAssetList: assets, | ||||||
|           ), |           ), | ||||||
| @@ -121,12 +153,37 @@ class RenderList { | |||||||
|         int cursor = 0; |         int cursor = 0; | ||||||
|         while (cursor < assets.length) { |         while (cursor < assets.length) { | ||||||
|           int rowElements = min(assets.length - cursor, perRow); |           int rowElements = min(assets.length - cursor, perRow); | ||||||
|  |           final rowAssets = assets.sublist(cursor, cursor + rowElements); | ||||||
|  |  | ||||||
|  |           // Default: All assets have the same width | ||||||
|  |           var widthDistribution = List.filled(rowElements, 1.0); | ||||||
|  |  | ||||||
|  |           if (dynamicLayout) { | ||||||
|  |             final aspectRatios = | ||||||
|  |                 rowAssets.map((e) => (e.width ?? 1) / (e.height ?? 1)).toList(); | ||||||
|  |             final meanAspectRatio = aspectRatios.sum / rowElements; | ||||||
|  |  | ||||||
|  |             // 1: mean width | ||||||
|  |             // 0.5: width < mean - threshold | ||||||
|  |             // 1.5: width > mean + threshold | ||||||
|  |             final arConfiguration = aspectRatios.map((e) { | ||||||
|  |               if (e - meanAspectRatio > 0.3) return 1.5; | ||||||
|  |               if (e - meanAspectRatio < -0.3) return 0.5; | ||||||
|  |               return 1.0; | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             // Normalize: | ||||||
|  |             final sum = arConfiguration.sum; | ||||||
|  |             widthDistribution = | ||||||
|  |                 arConfiguration.map((e) => (e * rowElements) / sum).toList(); | ||||||
|  |           } | ||||||
|  |  | ||||||
|           final rowElement = RenderAssetGridElement( |           final rowElement = RenderAssetGridElement( | ||||||
|             RenderAssetGridElementType.assetRow, |             RenderAssetGridElementType.assetRow, | ||||||
|             date: date, |             date: date, | ||||||
|             assetRow: RenderAssetGridRow( |             assetRow: RenderAssetGridRow( | ||||||
|               assets.sublist(cursor, cursor + rowElements), |               rowAssets, | ||||||
|  |               widthDistribution, | ||||||
|             ), |             ), | ||||||
|           ); |           ); | ||||||
|  |  | ||||||
| @@ -143,9 +200,9 @@ class RenderList { | |||||||
|     return RenderList(elements); |     return RenderList(elements); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static Future<RenderList> fromAssetGroups( |   static Future<RenderList> fromAssets( | ||||||
|     Map<String, List<Asset>> assetGroups, |     List<Asset> assets, | ||||||
|     int assetsPerRow, |     AssetGridLayoutParameters layout, | ||||||
|   ) async { |   ) async { | ||||||
|     // Compute only allows for one parameter. Therefore we pass all parameters in a map |     // Compute only allows for one parameter. Therefore we pass all parameters in a map | ||||||
|     return compute( |     return compute( | ||||||
| @@ -154,8 +211,8 @@ class RenderList { | |||||||
|         "monthly_title_text_date_format".tr(), |         "monthly_title_text_date_format".tr(), | ||||||
|         "daily_title_text_date".tr(), |         "daily_title_text_date".tr(), | ||||||
|         "daily_title_text_date_year".tr(), |         "daily_title_text_date_year".tr(), | ||||||
|         assetGroups, |         assets, | ||||||
|         assetsPerRow, |         layout, | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| 
 | 
 | ||||||
| class DailyTitleText extends ConsumerWidget { | class GroupDividerTitle extends ConsumerWidget { | ||||||
|   const DailyTitleText({ |   const GroupDividerTitle({ | ||||||
|     Key? key, |     Key? key, | ||||||
|     required this.text, |     required this.text, | ||||||
|     required this.multiselectEnabled, |     required this.multiselectEnabled, | ||||||
| @@ -7,7 +7,7 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart'; | |||||||
| import 'package:immich_mobile/shared/models/asset.dart'; | import 'package:immich_mobile/shared/models/asset.dart'; | ||||||
| import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; | import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; | ||||||
| import 'asset_grid_data_structure.dart'; | import 'asset_grid_data_structure.dart'; | ||||||
| import 'daily_title_text.dart'; | import 'group_divider_title.dart'; | ||||||
| import 'disable_multi_select_button.dart'; | import 'disable_multi_select_button.dart'; | ||||||
| import 'draggable_scrollbar_custom.dart'; | import 'draggable_scrollbar_custom.dart'; | ||||||
|  |  | ||||||
| @@ -99,12 +99,12 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> { | |||||||
|           widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow; |           widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow; | ||||||
|         return Row( |         return Row( | ||||||
|           key: Key("asset-row-${row.assets.first.id}"), |           key: Key("asset-row-${row.assets.first.id}"), | ||||||
|           children: row.assets.map((Asset asset) { |           children: row.assets.mapIndexed((int index, Asset asset) { | ||||||
|             bool last = asset.id == row.assets.last.id; |             bool last = asset.id == row.assets.last.id; | ||||||
|  |  | ||||||
|             return Container( |             return Container( | ||||||
|               key: Key("asset-${asset.id}"), |               key: Key("asset-${asset.id}"), | ||||||
|               width: size, |               width: size * row.widthDistribution[index], | ||||||
|               height: size, |               height: size, | ||||||
|               margin: EdgeInsets.only( |               margin: EdgeInsets.only( | ||||||
|                 top: widget.margin, |                 top: widget.margin, | ||||||
| @@ -123,7 +123,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> { | |||||||
|     String title, |     String title, | ||||||
|     List<Asset> assets, |     List<Asset> assets, | ||||||
|   ) { |   ) { | ||||||
|     return DailyTitleText( |     return GroupDividerTitle( | ||||||
|       text: title, |       text: title, | ||||||
|       multiselectEnabled: widget.selectionActive, |       multiselectEnabled: widget.selectionActive, | ||||||
|       onSelect: () => _selectAssets(assets), |       onSelect: () => _selectAssets(assets), | ||||||
| @@ -150,7 +150,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> { | |||||||
|   Widget _itemBuilder(BuildContext c, int position) { |   Widget _itemBuilder(BuildContext c, int position) { | ||||||
|     final item = widget.renderList.elements[position]; |     final item = widget.renderList.elements[position]; | ||||||
|  |  | ||||||
|     if (item.type == RenderAssetGridElementType.dayTitle) { |     if (item.type == RenderAssetGridElementType.groupDividerTitle) { | ||||||
|       return _buildTitle(c, item.title!, item.relatedAssetList!); |       return _buildTitle(c, item.title!, item.relatedAssetList!); | ||||||
|     } else if (item.type == RenderAssetGridElementType.monthTitle) { |     } else if (item.type == RenderAssetGridElementType.monthTitle) { | ||||||
|       return _buildMonthTitle(c, item.title!); |       return _buildMonthTitle(c, item.title!); | ||||||
|   | |||||||
| @@ -220,8 +220,8 @@ class HomePage extends HookConsumerWidget { | |||||||
|         top: true, |         top: true, | ||||||
|         child: Stack( |         child: Stack( | ||||||
|           children: [ |           children: [ | ||||||
|             ref.watch(assetProvider).renderList == null || |             ref.watch(assetProvider).renderList == null | ||||||
|                     ref.watch(assetProvider).allAssets.isEmpty |                 || ref.watch(assetProvider).allAssets.isEmpty | ||||||
|                 ? buildLoadingIndicator() |                 ? buildLoadingIndicator() | ||||||
|                 : ImmichAssetGrid( |                 : ImmichAssetGrid( | ||||||
|                     renderList: ref.watch(assetProvider).renderList!, |                     renderList: ref.watch(assetProvider).renderList!, | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| import 'package:collection/collection.dart'; |  | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; | import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; | ||||||
| import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart'; | import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart'; | ||||||
| @@ -7,7 +6,6 @@ import 'package:immich_mobile/modules/search/services/search.service.dart'; | |||||||
| import 'package:immich_mobile/modules/settings/providers/app_settings.provider.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/modules/settings/services/app_settings.service.dart'; | ||||||
| import 'package:immich_mobile/shared/models/asset.dart'; | import 'package:immich_mobile/shared/models/asset.dart'; | ||||||
| import 'package:intl/intl.dart'; |  | ||||||
|  |  | ||||||
| class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> { | class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> { | ||||||
|   SearchResultPageNotifier(this._searchService) |   SearchResultPageNotifier(this._searchService) | ||||||
| @@ -56,23 +54,16 @@ final searchResultPageProvider = | |||||||
|   return SearchResultPageNotifier(ref.watch(searchServiceProvider)); |   return SearchResultPageNotifier(ref.watch(searchServiceProvider)); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| final searchResultGroupByDateTimeProvider = StateProvider((ref) { |  | ||||||
|   var assets = ref.watch(searchResultPageProvider).searchResult; |  | ||||||
|  |  | ||||||
|   assets.sortByCompare<DateTime>( |  | ||||||
|     (e) => e.createdAt, |  | ||||||
|     (a, b) => b.compareTo(a), |  | ||||||
|   ); |  | ||||||
|   return assets.groupListsBy( |  | ||||||
|     (element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()), |  | ||||||
|   ); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| final searchRenderListProvider = FutureProvider((ref) { | final searchRenderListProvider = FutureProvider((ref) { | ||||||
|   var assetGroups = ref.watch(searchResultGroupByDateTimeProvider); |  | ||||||
|  |  | ||||||
|   var settings = ref.watch(appSettingsServiceProvider); |   var settings = ref.watch(appSettingsServiceProvider); | ||||||
|   final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow); |  | ||||||
|  |  | ||||||
|   return RenderList.fromAssetGroups(assetGroups, assetsPerRow); |   final assets = ref.watch(searchResultPageProvider).searchResult; | ||||||
|  |  | ||||||
|  |   final layout = AssetGridLayoutParameters( | ||||||
|  |     settings.getSetting(AppSettingsEnum.tilesPerRow), | ||||||
|  |     settings.getSetting(AppSettingsEnum.dynamicLayout), | ||||||
|  |     GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)], | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   return RenderList.fromAssets(assets, layout); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -6,6 +6,8 @@ enum AppSettingsEnum<T> { | |||||||
|   loadOriginal<bool>("loadOriginal", false), |   loadOriginal<bool>("loadOriginal", false), | ||||||
|   themeMode<String>("themeMode", "system"), // "light","dark","system" |   themeMode<String>("themeMode", "system"), // "light","dark","system" | ||||||
|   tilesPerRow<int>("tilesPerRow", 4), |   tilesPerRow<int>("tilesPerRow", 4), | ||||||
|  |   dynamicLayout<bool>("dynamicLayout", false), | ||||||
|  |   groupAssetsBy<int>("groupBy", 0), | ||||||
|   uploadErrorNotificationGracePeriod<int>( |   uploadErrorNotificationGracePeriod<int>( | ||||||
|     "uploadErrorNotificationGracePeriod", |     "uploadErrorNotificationGracePeriod", | ||||||
|     2, |     2, | ||||||
|   | |||||||
| @@ -0,0 +1,99 @@ | |||||||
|  | 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/modules/home/ui/asset_grid/asset_grid_data_structure.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/asset.provider.dart'; | ||||||
|  |  | ||||||
|  | class LayoutSettings extends HookConsumerWidget { | ||||||
|  |   const LayoutSettings({ | ||||||
|  |     Key? key, | ||||||
|  |   }) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final appSettingService = ref.watch(appSettingsServiceProvider); | ||||||
|  |  | ||||||
|  |     final useDynamicLayout = useState(true); | ||||||
|  |     final groupBy = useState(GroupAssetsBy.day); | ||||||
|  |  | ||||||
|  |     void switchChanged(bool value) { | ||||||
|  |       appSettingService.setSetting(AppSettingsEnum.dynamicLayout, value); | ||||||
|  |       useDynamicLayout.value = value; | ||||||
|  |       ref.watch(assetProvider.notifier).rebuildAssetGridDataStructure(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     void changeGroupValue(GroupAssetsBy? value) { | ||||||
|  |       if (value != null) { | ||||||
|  |         appSettingService.setSetting(AppSettingsEnum.groupAssetsBy, value.index); | ||||||
|  |         groupBy.value = value; | ||||||
|  |         ref.watch(assetProvider.notifier).rebuildAssetGridDataStructure(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     useEffect( | ||||||
|  |       () { | ||||||
|  |         useDynamicLayout.value = | ||||||
|  |             appSettingService.getSetting<bool>(AppSettingsEnum.dynamicLayout); | ||||||
|  |         groupBy.value = | ||||||
|  |             GroupAssetsBy.values[appSettingService.getSetting<int>(AppSettingsEnum.groupAssetsBy)]; | ||||||
|  |  | ||||||
|  |         return null; | ||||||
|  |       }, | ||||||
|  |       [], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return Column( | ||||||
|  |       children: [ | ||||||
|  |         SwitchListTile.adaptive( | ||||||
|  |           activeColor: Theme.of(context).primaryColor, | ||||||
|  |           title: const Text( | ||||||
|  |             "asset_list_layout_settings_dynamic_layout_title", | ||||||
|  |             style: TextStyle( | ||||||
|  |               fontSize: 12, | ||||||
|  |             ), | ||||||
|  |           ).tr(), | ||||||
|  |           onChanged: switchChanged, | ||||||
|  |           value: useDynamicLayout.value, | ||||||
|  |         ), | ||||||
|  |         ListTile( | ||||||
|  |           title: const Text( | ||||||
|  |             "asset_list_layout_settings_group_by", | ||||||
|  |             style: TextStyle( | ||||||
|  |               fontSize: 12, | ||||||
|  |               fontWeight: FontWeight.bold, | ||||||
|  |             ), | ||||||
|  |           ).tr(), | ||||||
|  |         ), | ||||||
|  |         RadioListTile( | ||||||
|  |           activeColor: Theme.of(context).primaryColor, | ||||||
|  |           title: const Text( | ||||||
|  |             "asset_list_layout_settings_group_by_month_day", | ||||||
|  |             style: TextStyle( | ||||||
|  |               fontSize: 12, | ||||||
|  |             ), | ||||||
|  |           ).tr(), | ||||||
|  |           value: GroupAssetsBy.day, | ||||||
|  |           groupValue: groupBy.value, | ||||||
|  |           onChanged: changeGroupValue, | ||||||
|  |           controlAffinity: ListTileControlAffinity.trailing, | ||||||
|  |         ), | ||||||
|  |         RadioListTile( | ||||||
|  |           activeColor: Theme.of(context).primaryColor, | ||||||
|  |           title: const Text( | ||||||
|  |             "asset_list_layout_settings_group_by_month", | ||||||
|  |             style: TextStyle( | ||||||
|  |               fontSize: 12, | ||||||
|  |             ), | ||||||
|  |           ).tr(), | ||||||
|  |           value: GroupAssetsBy.month, | ||||||
|  |           groupValue: groupBy.value, | ||||||
|  |           onChanged: changeGroupValue, | ||||||
|  |           controlAffinity: ListTileControlAffinity.trailing, | ||||||
|  |         ), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,5 +1,6 @@ | |||||||
| 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:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart'; | ||||||
| import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart'; | import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart'; | ||||||
| import 'asset_list_tiles_per_row.dart'; | import 'asset_list_tiles_per_row.dart'; | ||||||
|  |  | ||||||
| @@ -27,6 +28,7 @@ class AssetListSettings extends StatelessWidget { | |||||||
|       children: const [ |       children: const [ | ||||||
|         TilesPerRow(), |         TilesPerRow(), | ||||||
|         StorageIndicator(), |         StorageIndicator(), | ||||||
|  |         LayoutSettings(), | ||||||
|       ], |       ], | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -20,8 +20,7 @@ class StorageIndicator extends HookConsumerWidget { | |||||||
|     void switchChanged(bool value) { |     void switchChanged(bool value) { | ||||||
|       appSettingService.setSetting(AppSettingsEnum.storageIndicator, value); |       appSettingService.setSetting(AppSettingsEnum.storageIndicator, value); | ||||||
|       showStorageIndicator.value = value; |       showStorageIndicator.value = value; | ||||||
|  |       ref.watch(assetProvider.notifier).rebuildAssetGridDataStructure(); | ||||||
|       ref.invalidate(assetProvider); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     useEffect( |     useEffect( | ||||||
|   | |||||||
| @@ -23,8 +23,7 @@ class TilesPerRow extends HookConsumerWidget { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     void sliderChangedEnd(double _) { |     void sliderChangedEnd(double _) { | ||||||
|       ref.invalidate(assetProvider); |       ref.watch(assetProvider.notifier).rebuildAssetGridDataStructure(); | ||||||
|       ref.watch(assetProvider.notifier).getAllAsset(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     useEffect( |     useEffect( | ||||||
|   | |||||||
| @@ -25,11 +25,15 @@ class AssetsState { | |||||||
|  |  | ||||||
|   AssetsState(this.allAssets, {this.renderList}); |   AssetsState(this.allAssets, {this.renderList}); | ||||||
|  |  | ||||||
|   Future<AssetsState> withRenderDataStructure(int groupSize) async { |   Future<AssetsState> withRenderDataStructure( | ||||||
|  |     AssetGridLayoutParameters layout, | ||||||
|  |   ) async { | ||||||
|     return AssetsState( |     return AssetsState( | ||||||
|       allAssets, |       allAssets, | ||||||
|       renderList: |       renderList: await RenderList.fromAssets( | ||||||
|           await RenderList.fromAssetGroups(await _groupByDate(), groupSize), |         allAssets, | ||||||
|  |         layout, | ||||||
|  |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -37,20 +41,6 @@ class AssetsState { | |||||||
|     return AssetsState([...allAssets, ...toAdd]); |     return AssetsState([...allAssets, ...toAdd]); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<Map<String, List<Asset>>> _groupByDate() async { |  | ||||||
|     sortCompare(List<Asset> assets) { |  | ||||||
|       assets.sortByCompare<DateTime>( |  | ||||||
|         (e) => e.createdAt, |  | ||||||
|         (a, b) => b.compareTo(a), |  | ||||||
|       ); |  | ||||||
|       return assets.groupListsBy( |  | ||||||
|         (element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()), |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return await compute(sortCompare, allAssets.toList()); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static AssetsState fromAssetList(List<Asset> assets) { |   static AssetsState fromAssetList(List<Asset> assets) { | ||||||
|     return AssetsState(assets); |     return AssetsState(assets); | ||||||
|   } |   } | ||||||
| @@ -91,10 +81,19 @@ class AssetNotifier extends StateNotifier<AssetsState> { | |||||||
|       _assetCacheService.put(newAssetList); |       _assetCacheService.put(newAssetList); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     state = |     final layout = AssetGridLayoutParameters( | ||||||
|         await AssetsState.fromAssetList(newAssetList).withRenderDataStructure( |  | ||||||
|       _settingsService.getSetting(AppSettingsEnum.tilesPerRow), |       _settingsService.getSetting(AppSettingsEnum.tilesPerRow), | ||||||
|  |       _settingsService.getSetting(AppSettingsEnum.dynamicLayout), | ||||||
|  |       GroupAssetsBy.values[_settingsService.getSetting(AppSettingsEnum.groupAssetsBy)], | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  |     state = await AssetsState.fromAssetList(newAssetList) | ||||||
|  |         .withRenderDataStructure(layout); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Just a little helper to trigger a rebuild of the state object | ||||||
|  |   Future<void> rebuildAssetGridDataStructure() async { | ||||||
|  |     await _updateAssetsState(state.allAssets, cache: false); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getAllAsset() async { |   getAllAsset() async { | ||||||
|   | |||||||
| @@ -25,46 +25,61 @@ void main() { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   final Map<String, List<Asset>> groups = { |   final List<Asset> assets = []; | ||||||
|     '2022-01-05': testAssets.sublist(0, 5).map((e) { |  | ||||||
|  |   assets.addAll( | ||||||
|  |     testAssets.sublist(0, 5).map((e) { | ||||||
|       e.createdAt = DateTime(2022, 1, 5); |       e.createdAt = DateTime(2022, 1, 5); | ||||||
|       return e; |       return e; | ||||||
|     }).toList(), |     }).toList(), | ||||||
|     '2022-01-10': testAssets.sublist(5, 10).map((e) { |   ); | ||||||
|  |   assets.addAll( | ||||||
|  |     testAssets.sublist(5, 10).map((e) { | ||||||
|       e.createdAt = DateTime(2022, 1, 10); |       e.createdAt = DateTime(2022, 1, 10); | ||||||
|       return e; |       return e; | ||||||
|     }).toList(), |     }).toList(), | ||||||
|     '2022-02-17': testAssets.sublist(10, 15).map((e) { |   ); | ||||||
|  |   assets.addAll( | ||||||
|  |     testAssets.sublist(10, 15).map((e) { | ||||||
|       e.createdAt = DateTime(2022, 2, 17); |       e.createdAt = DateTime(2022, 2, 17); | ||||||
|       return e; |       return e; | ||||||
|     }).toList(), |     }).toList(), | ||||||
|     '2022-10-15': testAssets.sublist(15, 30).map((e) { |   ); | ||||||
|  |   assets.addAll( | ||||||
|  |     testAssets.sublist(15, 30).map((e) { | ||||||
|       e.createdAt = DateTime(2022, 10, 15); |       e.createdAt = DateTime(2022, 10, 15); | ||||||
|       return e; |       return e; | ||||||
|     }).toList() |     }).toList(), | ||||||
|   }; |   ); | ||||||
|  |  | ||||||
|   group('Test grouped', () { |   group('Test grouped', () { | ||||||
|     test('test grouped check months', () async { |     test('test grouped check months', () async { | ||||||
|       final renderList = await RenderList.fromAssetGroups(groups, 3); |       final renderList = await RenderList.fromAssets( | ||||||
|  |         assets, | ||||||
|  |         AssetGridLayoutParameters( | ||||||
|  |           3, | ||||||
|  |           false, | ||||||
|  |           GroupAssetsBy.day, | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|       // Jan |  | ||||||
|       // Day 1 |  | ||||||
|       // 5 Assets => 2 Rows |  | ||||||
|       // Day 2 |  | ||||||
|       // 5 Assets => 2 Rows |  | ||||||
|       // Feb |  | ||||||
|       // Day 1 |  | ||||||
|       // 5 Assets => 2 Rows |  | ||||||
|       // Oct |       // Oct | ||||||
|       // Day 1 |       // Day 1 | ||||||
|       // 15 Assets => 5 Rows |       // 15 Assets => 5 Rows | ||||||
|  |       // Feb | ||||||
|  |       // Day 1 | ||||||
|  |       // 5 Assets => 2 Rows | ||||||
|  |       // Jan | ||||||
|  |       // Day 2 | ||||||
|  |       // 5 Assets => 2 Rows | ||||||
|  |       // Day 1 | ||||||
|  |       // 5 Assets => 2 Rows | ||||||
|       expect(renderList.elements.length, 18); |       expect(renderList.elements.length, 18); | ||||||
|       expect( |       expect( | ||||||
|         renderList.elements[0].type, |         renderList.elements[0].type, | ||||||
|         RenderAssetGridElementType.monthTitle, |         RenderAssetGridElementType.monthTitle, | ||||||
|       ); |       ); | ||||||
|       expect(renderList.elements[0].date.month, 1); |       expect(renderList.elements[0].date.month, 10); | ||||||
|       expect( |       expect( | ||||||
|         renderList.elements[7].type, |         renderList.elements[7].type, | ||||||
|         RenderAssetGridElementType.monthTitle, |         RenderAssetGridElementType.monthTitle, | ||||||
| @@ -74,38 +89,44 @@ void main() { | |||||||
|         renderList.elements[11].type, |         renderList.elements[11].type, | ||||||
|         RenderAssetGridElementType.monthTitle, |         RenderAssetGridElementType.monthTitle, | ||||||
|       ); |       ); | ||||||
|       expect(renderList.elements[11].date.month, 10); |       expect(renderList.elements[11].date.month, 1); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     test('test grouped check types', () async { |     test('test grouped check types', () async { | ||||||
|       final renderList = await RenderList.fromAssetGroups(groups, 5); |       final renderList = await RenderList.fromAssets( | ||||||
|  |         assets, | ||||||
|  |         AssetGridLayoutParameters( | ||||||
|  |           5, | ||||||
|  |           false, | ||||||
|  |           GroupAssetsBy.day, | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|       // Jan |  | ||||||
|       // Day 1 |  | ||||||
|       // 5 Assets |  | ||||||
|       // Day 2 |  | ||||||
|       // 5 Assets |  | ||||||
|       // Feb |  | ||||||
|       // Day 1 |  | ||||||
|       // 5 Assets |  | ||||||
|       // Oct |       // Oct | ||||||
|       // Day 1 |       // Day 1 | ||||||
|       // 15 Assets => 3 Rows |       // 15 Assets => 3 Rows | ||||||
|  |       // Feb | ||||||
|  |       // Day 1 | ||||||
|  |       // 5 Assets => 1 Row | ||||||
|  |       // Jan | ||||||
|  |       // Day 2 | ||||||
|  |       // 5 Assets => 1 Row | ||||||
|  |       // Day 1 | ||||||
|  |       // 5 Assets => 1 Row | ||||||
|       final types = [ |       final types = [ | ||||||
|         RenderAssetGridElementType.monthTitle, |         RenderAssetGridElementType.monthTitle, | ||||||
|         RenderAssetGridElementType.dayTitle, |         RenderAssetGridElementType.groupDividerTitle, | ||||||
|  |         RenderAssetGridElementType.assetRow, | ||||||
|         RenderAssetGridElementType.assetRow, |         RenderAssetGridElementType.assetRow, | ||||||
|         RenderAssetGridElementType.dayTitle, |  | ||||||
|         RenderAssetGridElementType.assetRow, |         RenderAssetGridElementType.assetRow, | ||||||
|         RenderAssetGridElementType.monthTitle, |         RenderAssetGridElementType.monthTitle, | ||||||
|         RenderAssetGridElementType.dayTitle, |         RenderAssetGridElementType.groupDividerTitle, | ||||||
|         RenderAssetGridElementType.assetRow, |         RenderAssetGridElementType.assetRow, | ||||||
|         RenderAssetGridElementType.monthTitle, |         RenderAssetGridElementType.monthTitle, | ||||||
|         RenderAssetGridElementType.dayTitle, |         RenderAssetGridElementType.groupDividerTitle, | ||||||
|         RenderAssetGridElementType.assetRow, |         RenderAssetGridElementType.assetRow, | ||||||
|  |         RenderAssetGridElementType.groupDividerTitle, | ||||||
|         RenderAssetGridElementType.assetRow, |         RenderAssetGridElementType.assetRow, | ||||||
|         RenderAssetGridElementType.assetRow |  | ||||||
|       ]; |       ]; | ||||||
|  |  | ||||||
|       expect(renderList.elements.length, types.length); |       expect(renderList.elements.length, types.length); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user