From 8440d9890c28fa022db124847069f45b66d777a0 Mon Sep 17 00:00:00 2001 From: Matthias Rupp Date: Thu, 29 Sep 2022 21:53:35 +0200 Subject: [PATCH 01/11] Improve scrolling performance in albums and search --- .../album/views/album_viewer_page.dart | 65 ++++--------------- .../home_page_render_list_provider.dart | 30 +-------- .../asset_grid_data_structure.dart | 55 ++++++++++++++++ .../ui/asset_list_v2/immich_asset_grid.dart | 2 +- 4 files changed, 71 insertions(+), 81 deletions(-) create mode 100644 mobile/lib/modules/home/ui/asset_list_v2/asset_grid_data_structure.dart diff --git a/mobile/lib/modules/album/views/album_viewer_page.dart b/mobile/lib/modules/album/views/album_viewer_page.dart index c525c9922..5ebd2a11d 100644 --- a/mobile/lib/modules/album/views/album_viewer_page.dart +++ b/mobile/lib/modules/album/views/album_viewer_page.dart @@ -1,9 +1,12 @@ +import 'dart:math'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; +import 'package:immich_mobile/modules/home/ui/asset_list_v2/asset_grid_data_structure.dart'; +import 'package:immich_mobile/modules/home/ui/asset_list_v2/immich_asset_grid.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart'; import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart'; @@ -12,12 +15,10 @@ import 'package:immich_mobile/modules/album/services/album.service.dart'; import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart'; import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart'; import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart'; -import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.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/routing/router.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; -import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart'; import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; import 'package:openapi/api.dart'; @@ -28,6 +29,7 @@ class AlbumViewerPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final appSettingService = ref.watch(appSettingsServiceProvider); FocusNode titleFocusNode = useFocusNode(); ScrollController scrollController = useScrollController(); var albumInfo = ref.watch(sharedAlbumDetailProvider(albumId)); @@ -188,34 +190,17 @@ class AlbumViewerPage extends HookConsumerWidget { } Widget _buildImageGrid(AlbumResponseDto albumInfo) { - final appSettingService = ref.watch(appSettingsServiceProvider); + final assetsPerRow = + appSettingService.getSetting(AppSettingsEnum.tilesPerRow); final bool showStorageIndicator = appSettingService.getSetting(AppSettingsEnum.storageIndicator); + final renderList = assetsToRenderList(albumInfo.assets, assetsPerRow); - if (albumInfo.assets.isNotEmpty) { - return SliverPadding( - padding: const EdgeInsets.only(top: 10.0), - sliver: SliverGrid( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: - appSettingService.getSetting(AppSettingsEnum.tilesPerRow), - crossAxisSpacing: 5.0, - mainAxisSpacing: 5, - ), - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - return AlbumViewerThumbnail( - asset: albumInfo.assets[index], - assetList: albumInfo.assets, - showStorageIndicator: showStorageIndicator, - ); - }, - childCount: albumInfo.assetCount, - ), - ), - ); - } - return const SliverToBoxAdapter(); + return ImmichAssetGrid( + assetsPerRow: assetsPerRow, + renderList: renderList, + showStorageIndicator: showStorageIndicator, + ); } Widget _buildControlButton(AlbumResponseDto albumInfo) { @@ -248,29 +233,7 @@ class AlbumViewerPage extends HookConsumerWidget { onTap: () { titleFocusNode.unfocus(); }, - child: DraggableScrollbar.semicircle( - backgroundColor: Theme.of(context).hintColor, - controller: scrollController, - heightScrollThumb: 48.0, - child: CustomScrollView( - controller: scrollController, - slivers: [ - _buildHeader(albumInfo), - SliverPersistentHeader( - pinned: true, - delegate: ImmichSliverPersistentAppBarDelegate( - minHeight: 50, - maxHeight: 50, - child: Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: _buildControlButton(albumInfo), - ), - ), - ), - _buildImageGrid(albumInfo) - ], - ), - ), + child: _buildImageGrid(albumInfo), ); } diff --git a/mobile/lib/modules/home/providers/home_page_render_list_provider.dart b/mobile/lib/modules/home/providers/home_page_render_list_provider.dart index edb673362..a0e4c1eef 100644 --- a/mobile/lib/modules/home/providers/home_page_render_list_provider.dart +++ b/mobile/lib/modules/home/providers/home_page_render_list_provider.dart @@ -1,38 +1,10 @@ import 'dart:math'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/home/ui/asset_list_v2/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'; -import 'package:openapi/api.dart'; - -enum RenderAssetGridElementType { - assetRow, - dayTitle, - monthTitle; -} - -class RenderAssetGridRow { - final List assets; - - RenderAssetGridRow(this.assets); -} - -class RenderAssetGridElement { - final RenderAssetGridElementType type; - final RenderAssetGridRow? assetRow; - final String? title; - final DateTime date; - final List? relatedAssetList; - - RenderAssetGridElement( - this.type, { - this.assetRow, - this.title, - required this.date, - this.relatedAssetList, - }); -} final renderListProvider = StateProvider((ref) { var assetGroups = ref.watch(assetGroupByDateTimeProvider); diff --git a/mobile/lib/modules/home/ui/asset_list_v2/asset_grid_data_structure.dart b/mobile/lib/modules/home/ui/asset_list_v2/asset_grid_data_structure.dart new file mode 100644 index 000000000..c008680eb --- /dev/null +++ b/mobile/lib/modules/home/ui/asset_list_v2/asset_grid_data_structure.dart @@ -0,0 +1,55 @@ +import 'dart:math'; + +import 'package:openapi/api.dart'; + +enum RenderAssetGridElementType { + assetRow, + dayTitle, + monthTitle; +} + +class RenderAssetGridRow { + final List assets; + + RenderAssetGridRow(this.assets); +} + +class RenderAssetGridElement { + final RenderAssetGridElementType type; + final RenderAssetGridRow? assetRow; + final String? title; + final DateTime date; + final List? relatedAssetList; + + RenderAssetGridElement( + this.type, { + this.assetRow, + this.title, + required this.date, + this.relatedAssetList, + }); +} + +List assetsToRenderList( + List assets, int assetsPerRow) { + List elements = []; + + int cursor = 0; + while (cursor < assets.length) { + int rowElements = min(assets.length - cursor, assetsPerRow); + final date = DateTime.parse(assets[cursor].createdAt); + + final rowElement = RenderAssetGridElement( + RenderAssetGridElementType.assetRow, + date: date, + assetRow: RenderAssetGridRow( + assets.sublist(cursor, cursor + rowElements), + ), + ); + + elements.add(rowElement); + cursor += rowElements; + } + + return elements; +} diff --git a/mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart index 4a085f3bf..2916e5f42 100644 --- a/mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart +++ b/mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart @@ -7,13 +7,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/providers/home_page_render_list_provider.dart'; import 'package:immich_mobile/modules/home/ui/asset_list_v2/daily_title_text.dart'; import 'package:immich_mobile/modules/home/ui/asset_list_v2/draggable_scrollbar_custom.dart'; import 'package:openapi/api.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import '../thumbnail_image.dart'; +import 'asset_grid_data_structure.dart'; class ImmichAssetGrid extends HookConsumerWidget { final ItemScrollController _itemScrollController = ItemScrollController(); From dd71a53f5ed9b079ea94eb31156f484bd94aa0e4 Mon Sep 17 00:00:00 2001 From: Matthias Rupp Date: Fri, 30 Sep 2022 10:47:31 +0200 Subject: [PATCH 02/11] Hide scroll handle for lists < 100 assets --- .../ui/asset_list_v2/immich_asset_grid.dart | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart index 2916e5f42..d45c7d73c 100644 --- a/mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart +++ b/mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart @@ -136,10 +136,13 @@ class ImmichAssetGrid extends HookConsumerWidget { ); } + @override Widget build(BuildContext context, WidgetRef ref) { final scrolling = useState(false); + final useDragScrolling = _assets.length > 100; + void dragScrolling(bool active) { scrolling.value = active; } @@ -148,6 +151,17 @@ class ImmichAssetGrid extends HookConsumerWidget { return _itemBuilder(c, position, scrolling.value); } + final listWidget = ScrollablePositionedList.builder( + itemBuilder: itemBuilder, + itemPositionsListener: _itemPositionsListener, + itemScrollController: _itemScrollController, + itemCount: renderList.length, + ); + + if (!useDragScrolling) { + return listWidget; + } + return DraggableScrollbar.semicircle( scrollStateListener: dragScrolling, itemPositionsListener: _itemPositionsListener, @@ -157,11 +171,7 @@ class ImmichAssetGrid extends HookConsumerWidget { labelConstraints: const BoxConstraints(maxHeight: 28), scrollbarAnimationDuration: const Duration(seconds: 1), scrollbarTimeToFade: const Duration(seconds: 4), - child: ScrollablePositionedList.builder( - itemBuilder: itemBuilder, - itemPositionsListener: _itemPositionsListener, - itemScrollController: _itemScrollController, - itemCount: renderList.length, - )); + child: listWidget, + ); } } From 1970a64f6fd7ed46d5f12155e40890614e958356 Mon Sep 17 00:00:00 2001 From: Matthias Rupp Date: Fri, 30 Sep 2022 11:05:54 +0200 Subject: [PATCH 03/11] Use new asset grid for search result page --- .../home_page_render_list_provider.dart | 47 +----------- .../asset_grid_data_structure.dart | 48 ++++++++++++ .../search_result_page.provider.dart | 12 +++ .../search/views/search_result_page.dart | 75 ++++--------------- 4 files changed, 76 insertions(+), 106 deletions(-) diff --git a/mobile/lib/modules/home/providers/home_page_render_list_provider.dart b/mobile/lib/modules/home/providers/home_page_render_list_provider.dart index a0e4c1eef..4ad6be68f 100644 --- a/mobile/lib/modules/home/providers/home_page_render_list_provider.dart +++ b/mobile/lib/modules/home/providers/home_page_render_list_provider.dart @@ -8,52 +8,9 @@ import 'package:immich_mobile/shared/providers/asset.provider.dart'; final renderListProvider = StateProvider((ref) { var assetGroups = ref.watch(assetGroupByDateTimeProvider); - var settings = ref.watch(appSettingsServiceProvider); + var settings = ref.watch(appSettingsServiceProvider); final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow); - List elements = []; - DateTime? lastDate; - - assetGroups.forEach((groupName, assets) { - final date = DateTime.parse(groupName); - - if (lastDate == null || lastDate!.month != date.month) { - elements.add( - RenderAssetGridElement(RenderAssetGridElementType.monthTitle, - title: groupName, date: date), - ); - } - - // Add group title - elements.add( - RenderAssetGridElement( - RenderAssetGridElementType.dayTitle, - title: groupName, - date: date, - relatedAssetList: assets, - ), - ); - - // Add rows - int cursor = 0; - while (cursor < assets.length) { - int rowElements = min(assets.length - cursor, assetsPerRow); - - final rowElement = RenderAssetGridElement( - RenderAssetGridElementType.assetRow, - date: date, - assetRow: RenderAssetGridRow( - assets.sublist(cursor, cursor + rowElements), - ), - ); - - elements.add(rowElement); - cursor += rowElements; - } - - lastDate = date; - }); - - return elements; + return assetGroupsToRenderList(assetGroups, assetsPerRow); }); diff --git a/mobile/lib/modules/home/ui/asset_list_v2/asset_grid_data_structure.dart b/mobile/lib/modules/home/ui/asset_list_v2/asset_grid_data_structure.dart index c008680eb..934385959 100644 --- a/mobile/lib/modules/home/ui/asset_list_v2/asset_grid_data_structure.dart +++ b/mobile/lib/modules/home/ui/asset_list_v2/asset_grid_data_structure.dart @@ -53,3 +53,51 @@ List assetsToRenderList( return elements; } + +List assetGroupsToRenderList( + Map> assetGroups, int assetsPerRow) { + List elements = []; + DateTime? lastDate; + + assetGroups.forEach((groupName, assets) { + final date = DateTime.parse(groupName); + + if (lastDate == null || lastDate!.month != date.month) { + elements.add( + RenderAssetGridElement(RenderAssetGridElementType.monthTitle, + title: groupName, date: date), + ); + } + + // Add group title + elements.add( + RenderAssetGridElement( + RenderAssetGridElementType.dayTitle, + title: groupName, + date: date, + relatedAssetList: assets, + ), + ); + + // Add rows + int cursor = 0; + while (cursor < assets.length) { + int rowElements = min(assets.length - cursor, assetsPerRow); + + final rowElement = RenderAssetGridElement( + RenderAssetGridElementType.assetRow, + date: date, + assetRow: RenderAssetGridRow( + assets.sublist(cursor, cursor + rowElements), + ), + ); + + elements.add(rowElement); + cursor += rowElements; + } + + lastDate = date; + }); + + return elements; +} diff --git a/mobile/lib/modules/search/providers/search_result_page.provider.dart b/mobile/lib/modules/search/providers/search_result_page.provider.dart index 66c7a4419..b52c172f6 100644 --- a/mobile/lib/modules/search/providers/search_result_page.provider.dart +++ b/mobile/lib/modules/search/providers/search_result_page.provider.dart @@ -1,8 +1,11 @@ import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/home/ui/asset_list_v2/asset_grid_data_structure.dart'; import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart'; 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/services/app_settings.service.dart'; import 'package:intl/intl.dart'; import 'package:openapi/api.dart'; @@ -66,3 +69,12 @@ final searchResultGroupByDateTimeProvider = StateProvider((ref) { .format(DateTime.parse(element.createdAt).toLocal()), ); }); + +final searchRenderListProvider = StateProvider((ref) { + var assetGroups = ref.watch(searchResultGroupByDateTimeProvider); + + var settings = ref.watch(appSettingsServiceProvider); + final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow); + + return assetGroupsToRenderList(assetGroups, assetsPerRow); +}); diff --git a/mobile/lib/modules/search/views/search_result_page.dart b/mobile/lib/modules/search/views/search_result_page.dart index 09f38ffb0..850de5cd9 100644 --- a/mobile/lib/modules/search/views/search_result_page.dart +++ b/mobile/lib/modules/search/views/search_result_page.dart @@ -4,13 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/ui/daily_title_text.dart'; -import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; -import 'package:immich_mobile/modules/home/ui/image_grid.dart'; -import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart'; +import 'package:immich_mobile/modules/home/ui/asset_list_v2/immich_asset_grid.dart'; import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart'; import 'package:immich_mobile/modules/search/ui/search_suggestion_list.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:openapi/api.dart'; class SearchResultPage extends HookConsumerWidget { @@ -21,17 +20,12 @@ class SearchResultPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - ScrollController scrollController = useScrollController(); final searchTermController = useTextEditingController(text: ""); final isNewSearch = useState(false); final currentSearchTerm = useState(searchTerm); - final List imageGridGroup = []; - FocusNode? searchFocusNode; - List sortedAssetList = []; - useEffect( () { searchFocusNode = FocusNode(); @@ -117,7 +111,12 @@ class SearchResultPage extends HookConsumerWidget { _buildSearchResult() { var searchResultPageState = ref.watch(searchResultPageProvider); - var assetGroupByDateTime = ref.watch(searchResultGroupByDateTimeProvider); + var searchResultRenderList = ref.watch(searchRenderListProvider); + + var settings = ref.watch(appSettingsServiceProvider); + final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow); + final showStorageIndicator = + settings.getSetting(AppSettingsEnum.storageIndicator); if (searchResultPageState.isError) { return const Text("Error"); @@ -132,57 +131,11 @@ class SearchResultPage extends HookConsumerWidget { } if (searchResultPageState.isSuccess) { - if (searchResultPageState.searchResult.isNotEmpty) { - int? lastMonth; - // set sorted List - for (var group in assetGroupByDateTime.values) { - for (var value in group) { - sortedAssetList.add(value); - } - } - assetGroupByDateTime.forEach((dateGroup, immichAssetList) { - DateTime parseDateGroup = DateTime.parse(dateGroup); - int currentMonth = parseDateGroup.month; - - if (lastMonth != null) { - if (currentMonth - lastMonth! != 0) { - imageGridGroup.add( - MonthlyTitleText( - isoDate: dateGroup, - ), - ); - } - } - - imageGridGroup.add( - DailyTitleText( - isoDate: dateGroup, - assetGroup: immichAssetList, - ), - ); - - imageGridGroup.add( - ImageGrid( - assetGroup: immichAssetList, - sortedAssetGroup: sortedAssetList, - ), - ); - - lastMonth = currentMonth; - }); - - return DraggableScrollbar.semicircle( - backgroundColor: Theme.of(context).hintColor, - controller: scrollController, - heightScrollThumb: 48.0, - child: CustomScrollView( - controller: scrollController, - slivers: [...imageGridGroup], - ), - ); - } else { - return const Text("No assets found"); - } + return ImmichAssetGrid( + renderList: searchResultRenderList, + assetsPerRow: assetsPerRow, + showStorageIndicator: showStorageIndicator, + ); } return const SizedBox(); From 50842ef815c2e6961b54b86ad06f4dd48a80e11f Mon Sep 17 00:00:00 2001 From: Matthias Rupp Date: Fri, 30 Sep 2022 11:38:00 +0200 Subject: [PATCH 04/11] Add tests --- .../test/asset_grid_data_structure_test.dart | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 mobile/test/asset_grid_data_structure_test.dart diff --git a/mobile/test/asset_grid_data_structure_test.dart b/mobile/test/asset_grid_data_structure_test.dart new file mode 100644 index 000000000..87ea20323 --- /dev/null +++ b/mobile/test/asset_grid_data_structure_test.dart @@ -0,0 +1,159 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/modules/home/ui/asset_list_v2/asset_grid_data_structure.dart'; +import 'package:openapi/api.dart'; + +void main() { + final List testAssets = []; + + for (int i = 0; i < 150; i++) { + int month = i ~/ 31; + int day = (i % 31).toInt(); + + DateTime date = DateTime(2022, month, day); + + testAssets.add(AssetResponseDto( + type: AssetTypeEnum.IMAGE, + id: '$i', + deviceAssetId: '', + ownerId: '', + deviceId: '', + originalPath: '', + resizePath: '', + createdAt: date.toIso8601String(), + modifiedAt: date.toIso8601String(), + isFavorite: false, + mimeType: 'image/jpeg', + duration: '', + webpPath: '', + encodedVideoPath: '', + )); + } + + final Map> groups = { + '2022-01-05': testAssets.sublist(0, 5).map((e) { + e.createdAt = DateTime(2022, 1, 5).toIso8601String(); + return e; + }).toList(), + '2022-01-10': testAssets.sublist(5, 10).map((e) { + e.createdAt = DateTime(2022, 1, 10).toIso8601String(); + return e; + }).toList(), + '2022-02-17': testAssets.sublist(10, 15).map((e) { + e.createdAt = DateTime(2022, 2, 17).toIso8601String(); + return e; + }).toList(), + '2022-10-15': testAssets.sublist(15, 30).map((e) { + e.createdAt = DateTime(2022, 10, 15).toIso8601String(); + return e; + }).toList() + }; + + group('Asset only list', () { + test('items < itemsPerRow', () { + final assets = testAssets.sublist(0, 2); + final renderList = assetsToRenderList(assets, 3); + + expect(renderList.length, 1); + expect(renderList[0].assetRow!.assets.length, 2); + }); + + test('items = itemsPerRow', () { + final assets = testAssets.sublist(0, 3); + final renderList = assetsToRenderList(assets, 3); + + expect(renderList.length, 1); + expect(renderList[0].assetRow!.assets.length, 3); + }); + + test('items > itemsPerRow', () { + final assets = testAssets.sublist(0, 20); + final renderList = assetsToRenderList(assets, 3); + + expect(renderList.length, 7); + expect(renderList[6].assetRow!.assets.length, 2); + }); + + test('items > itemsPerRow partition 4', () { + final assets = testAssets.sublist(0, 21); + final renderList = assetsToRenderList(assets, 4); + + expect(renderList.length, 6); + expect(renderList[5].assetRow!.assets.length, 1); + }); + + test('items > itemsPerRow check ids', () { + final assets = testAssets.sublist(0, 21); + final renderList = assetsToRenderList(assets, 3); + + expect(renderList.length, 7); + expect(renderList[6].assetRow!.assets.length, 3); + expect(renderList[0].assetRow!.assets[0].id, '0'); + expect(renderList[1].assetRow!.assets[1].id, '4'); + expect(renderList[3].assetRow!.assets[2].id, '11'); + expect(renderList[6].assetRow!.assets[2].id, '20'); + }); + }); + + group('Test grouped', () { + test('test grouped check months', () { + final renderList = assetGroupsToRenderList(groups, 3); + + // Jan + // Day 1 + // 5 Assets => 2 Rows + // Day 2 + // 5 Assets => 2 Rows + // Feb + // Day 1 + // 5 Assets => 2 Rows + // Oct + // Day 1 + // 15 Assets => 5 Rows + expect(renderList.length, 18); + expect(renderList[0].type, RenderAssetGridElementType.monthTitle); + expect(renderList[0].date.month, 1); + expect(renderList[7].type, RenderAssetGridElementType.monthTitle); + expect(renderList[7].date.month, 2); + expect(renderList[11].type, RenderAssetGridElementType.monthTitle); + expect(renderList[11].date.month, 10); + }); + + test('test grouped check types', () { + final renderList = assetGroupsToRenderList(groups, 5); + + // Jan + // Day 1 + // 5 Assets + // Day 2 + // 5 Assets + // Feb + // Day 1 + // 5 Assets + // Oct + // Day 1 + // 15 Assets => 3 Rows + + final types = [ + RenderAssetGridElementType.monthTitle, + RenderAssetGridElementType.dayTitle, + RenderAssetGridElementType.assetRow, + RenderAssetGridElementType.dayTitle, + RenderAssetGridElementType.assetRow, + RenderAssetGridElementType.monthTitle, + RenderAssetGridElementType.dayTitle, + RenderAssetGridElementType.assetRow, + RenderAssetGridElementType.monthTitle, + RenderAssetGridElementType.dayTitle, + RenderAssetGridElementType.assetRow, + RenderAssetGridElementType.assetRow, + RenderAssetGridElementType.assetRow + ]; + + expect(renderList.length, types.length); + + for (int i = 0; i < renderList.length; i++) { + expect(renderList[i].type, types[i]); + } + }); + }); +} From 347ac70063bb7bc8205296178cd7b9ab8e20c33e Mon Sep 17 00:00:00 2001 From: Matthias Rupp Date: Sat, 1 Oct 2022 10:33:06 +0200 Subject: [PATCH 05/11] Make new asset grid the default --- mobile/assets/i18n/en-US.json | 5 +- .../album/views/album_viewer_page.dart | 4 +- .../home_page_render_list_provider.dart | 2 +- .../providers/home_page_state.provider.dart | 1 - .../asset_grid_data_structure.dart | 0 .../daily_title_text.dart | 0 .../draggable_scrollbar_custom.dart | 0 .../immich_asset_grid.dart | 7 +- mobile/lib/modules/home/views/home_page.dart | 109 +++--------------- .../search_result_page.provider.dart | 2 +- .../search/views/search_result_page.dart | 3 +- .../experimental_settings.dart | 70 +++-------- .../modules/settings/views/settings_page.dart | 2 +- .../test/asset_grid_data_structure_test.dart | 2 +- 14 files changed, 44 insertions(+), 163 deletions(-) rename mobile/lib/modules/home/ui/{asset_list_v2 => asset_grid}/asset_grid_data_structure.dart (100%) rename mobile/lib/modules/home/ui/{asset_list_v2 => asset_grid}/daily_title_text.dart (100%) rename mobile/lib/modules/home/ui/{asset_list_v2 => asset_grid}/draggable_scrollbar_custom.dart (100%) rename mobile/lib/modules/home/ui/{asset_list_v2 => asset_grid}/immich_asset_grid.dart (96%) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index c29e842fa..c81ebdfe6 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -167,8 +167,5 @@ "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", "experimental_settings_title": "Experimental", - "experimental_settings_subtitle": "Use at your own risk!", - "experimental_settings_new_asset_list_title": "Enable experimental photo grid", - "experimental_settings_new_asset_list_subtitle": "Work in progress", - "settings_require_restart": "Please restart Immich to apply this setting" + "experimental_settings_subtitle": "Use at your own risk!" } \ No newline at end of file diff --git a/mobile/lib/modules/album/views/album_viewer_page.dart b/mobile/lib/modules/album/views/album_viewer_page.dart index 5ebd2a11d..59f66d997 100644 --- a/mobile/lib/modules/album/views/album_viewer_page.dart +++ b/mobile/lib/modules/album/views/album_viewer_page.dart @@ -5,8 +5,8 @@ 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_list_v2/asset_grid_data_structure.dart'; -import 'package:immich_mobile/modules/home/ui/asset_list_v2/immich_asset_grid.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart'; import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart'; diff --git a/mobile/lib/modules/home/providers/home_page_render_list_provider.dart b/mobile/lib/modules/home/providers/home_page_render_list_provider.dart index 4ad6be68f..707f98fb7 100644 --- a/mobile/lib/modules/home/providers/home_page_render_list_provider.dart +++ b/mobile/lib/modules/home/providers/home_page_render_list_provider.dart @@ -1,7 +1,7 @@ import 'dart:math'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/ui/asset_list_v2/asset_grid_data_structure.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'; diff --git a/mobile/lib/modules/home/providers/home_page_state.provider.dart b/mobile/lib/modules/home/providers/home_page_state.provider.dart index 974706d36..ff4f8cfd9 100644 --- a/mobile/lib/modules/home/providers/home_page_state.provider.dart +++ b/mobile/lib/modules/home/providers/home_page_state.provider.dart @@ -1,4 +1,3 @@ -import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/home/models/home_page_state.model.dart'; diff --git a/mobile/lib/modules/home/ui/asset_list_v2/asset_grid_data_structure.dart b/mobile/lib/modules/home/ui/asset_grid/asset_grid_data_structure.dart similarity index 100% rename from mobile/lib/modules/home/ui/asset_list_v2/asset_grid_data_structure.dart rename to mobile/lib/modules/home/ui/asset_grid/asset_grid_data_structure.dart diff --git a/mobile/lib/modules/home/ui/asset_list_v2/daily_title_text.dart b/mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart similarity index 100% rename from mobile/lib/modules/home/ui/asset_list_v2/daily_title_text.dart rename to mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart diff --git a/mobile/lib/modules/home/ui/asset_list_v2/draggable_scrollbar_custom.dart b/mobile/lib/modules/home/ui/asset_grid/draggable_scrollbar_custom.dart similarity index 100% rename from mobile/lib/modules/home/ui/asset_list_v2/draggable_scrollbar_custom.dart rename to mobile/lib/modules/home/ui/asset_grid/draggable_scrollbar_custom.dart diff --git a/mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart similarity index 96% rename from mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart rename to mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart index d45c7d73c..7ff4a7890 100644 --- a/mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart @@ -7,13 +7,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/ui/asset_list_v2/daily_title_text.dart'; -import 'package:immich_mobile/modules/home/ui/asset_list_v2/draggable_scrollbar_custom.dart'; +import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart'; import 'package:openapi/api.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -import '../thumbnail_image.dart'; import 'asset_grid_data_structure.dart'; +import 'daily_title_text.dart'; +import 'draggable_scrollbar_custom.dart'; class ImmichAssetGrid extends HookConsumerWidget { final ItemScrollController _itemScrollController = ItemScrollController(); diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index a1d8f46b2..ebfc55680 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -3,22 +3,16 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/home/providers/home_page_render_list_provider.dart'; import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart'; -import 'package:immich_mobile/modules/home/ui/daily_title_text.dart'; import 'package:immich_mobile/modules/home/ui/disable_multi_select_button.dart'; -import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; -import 'package:immich_mobile/modules/home/ui/image_grid.dart'; -import 'package:immich_mobile/modules/home/ui/asset_list_v2/immich_asset_grid.dart'; import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart'; -import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart'; import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.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'; import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/providers/websocket.provider.dart'; -import 'package:openapi/api.dart'; class HomePage extends HookConsumerWidget { const HomePage({Key? key}) : super(key: key); @@ -29,19 +23,9 @@ class HomePage extends HookConsumerWidget { var renderList = ref.watch(renderListProvider); - ScrollController scrollController = useScrollController(); - var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider); - List imageGridGroup = []; var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable; var homePageState = ref.watch(homePageStateProvider); - List sortedAssetList = []; - // set sorted List - for (var group in assetGroupByDateTime.values) { - for (var value in group) { - sortedAssetList.add(value); - } - } useEffect( () { @@ -57,61 +41,15 @@ class HomePage extends HookConsumerWidget { ref.read(assetProvider.notifier).getAllAsset(); } - _buildSelectedItemCountIndicator() { + buildSelectedItemCountIndicator() { return DisableMultiSelectButton( onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect, selectedItemCount: homePageState.selectedItems.length, ); } - Widget _buildBody() { - if (assetGroupByDateTime.isNotEmpty) { - int? lastMonth; - - assetGroupByDateTime.forEach((dateGroup, immichAssetList) { - try { - DateTime parseDateGroup = DateTime.parse(dateGroup); - int currentMonth = parseDateGroup.month; - - if (lastMonth != null) { - if (currentMonth - lastMonth! != 0) { - imageGridGroup.add( - MonthlyTitleText( - isoDate: dateGroup, - ), - ); - } - } - - imageGridGroup.add( - DailyTitleText( - key: Key('${dateGroup.toString()}title'), - isoDate: dateGroup, - assetGroup: immichAssetList, - ), - ); - - imageGridGroup.add( - ImageGrid( - assetGroup: immichAssetList, - sortedAssetGroup: sortedAssetList, - tilesPerRow: - appSettingService.getSetting(AppSettingsEnum.tilesPerRow), - showStorageIndicator: appSettingService - .getSetting(AppSettingsEnum.storageIndicator), - ), - ); - - lastMonth = currentMonth; - } catch (e) { - debugPrint( - "[ERROR] Cannot parse $dateGroup - Wrong create date format : ${immichAssetList.map((asset) => asset.createdAt).toList()}", - ); - } - }); - } - - _buildSliverAppBar() { + Widget buildBody() { + buildSliverAppBar() { return isMultiSelectEnable ? const SliverToBoxAdapter( child: SizedBox( @@ -124,31 +62,6 @@ class HomePage extends HookConsumerWidget { ); } - _buildAssetGrid() { - if (appSettingService - .getSetting(AppSettingsEnum.useExperimentalAssetGrid)) { - return ImmichAssetGrid( - renderList: renderList, - assetsPerRow: - appSettingService.getSetting(AppSettingsEnum.tilesPerRow), - showStorageIndicator: appSettingService - .getSetting(AppSettingsEnum.storageIndicator), - ); - } else { - return DraggableScrollbar.semicircle( - backgroundColor: Theme.of(context).hintColor, - controller: scrollController, - heightScrollThumb: 48.0, - child: CustomScrollView( - controller: scrollController, - slivers: [ - ...imageGridGroup, - ], - ), - ); - } - } - return SafeArea( bottom: !isMultiSelectEnable, top: !isMultiSelectEnable, @@ -156,15 +69,21 @@ class HomePage extends HookConsumerWidget { children: [ CustomScrollView( slivers: [ - _buildSliverAppBar(), + buildSliverAppBar(), ], ), Padding( padding: const EdgeInsets.only(top: 60.0, bottom: 0.0), - child: _buildAssetGrid(), + child: ImmichAssetGrid( + renderList: renderList, + assetsPerRow: + appSettingService.getSetting(AppSettingsEnum.tilesPerRow), + showStorageIndicator: appSettingService + .getSetting(AppSettingsEnum.storageIndicator), + ), ), if (isMultiSelectEnable) ...[ - _buildSelectedItemCountIndicator(), + buildSelectedItemCountIndicator(), const ControlBottomAppBar(), ], ], @@ -174,7 +93,7 @@ class HomePage extends HookConsumerWidget { return Scaffold( drawer: const ProfileDrawer(), - body: _buildBody(), + body: buildBody(), ); } } diff --git a/mobile/lib/modules/search/providers/search_result_page.provider.dart b/mobile/lib/modules/search/providers/search_result_page.provider.dart index b52c172f6..c152be25e 100644 --- a/mobile/lib/modules/search/providers/search_result_page.provider.dart +++ b/mobile/lib/modules/search/providers/search_result_page.provider.dart @@ -1,6 +1,6 @@ import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/ui/asset_list_v2/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/services/search.service.dart'; diff --git a/mobile/lib/modules/search/views/search_result_page.dart b/mobile/lib/modules/search/views/search_result_page.dart index 850de5cd9..5f213cdce 100644 --- a/mobile/lib/modules/search/views/search_result_page.dart +++ b/mobile/lib/modules/search/views/search_result_page.dart @@ -4,13 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/ui/asset_list_v2/immich_asset_grid.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart'; import 'package:immich_mobile/modules/search/ui/search_suggestion_list.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:openapi/api.dart'; class SearchResultPage extends HookConsumerWidget { const SearchResultPage({Key? key, required this.searchTerm}) diff --git a/mobile/lib/modules/settings/ui/experimental_settings/experimental_settings.dart b/mobile/lib/modules/settings/ui/experimental_settings/experimental_settings.dart index 2043d0d24..f6db82494 100644 --- a/mobile/lib/modules/settings/ui/experimental_settings/experimental_settings.dart +++ b/mobile/lib/modules/settings/ui/experimental_settings/experimental_settings.dart @@ -1,11 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.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/ui/immich_toast.dart'; class ExperimentalSettings extends HookConsumerWidget { const ExperimentalSettings({ @@ -14,33 +9,6 @@ class ExperimentalSettings extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final appSettingService = ref.watch(appSettingsServiceProvider); - - final useExperimentalAssetGrid = useState(false); - - useEffect( - () { - useExperimentalAssetGrid.value = appSettingService - .getSetting(AppSettingsEnum.useExperimentalAssetGrid); - return null; - }, - [], - ); - - void changeUseExperimentalAssetGrid(bool status) { - useExperimentalAssetGrid.value = status; - appSettingService.setSetting( - AppSettingsEnum.useExperimentalAssetGrid, - status, - ); - - ImmichToast.show( - context: context, - msg: "settings_require_restart".tr(), - gravity: ToastGravity.BOTTOM, - ); - } - return ExpansionTile( textColor: Theme.of(context).primaryColor, title: const Text( @@ -55,25 +23,25 @@ class ExperimentalSettings extends HookConsumerWidget { fontSize: 13, ), ).tr(), - children: [ - SwitchListTile.adaptive( - activeColor: Theme.of(context).primaryColor, - title: const Text( - "experimental_settings_new_asset_list_title", - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ).tr(), - subtitle: const Text( - "experimental_settings_new_asset_list_subtitle", - style: TextStyle( - fontSize: 12, - ), - ).tr(), - value: useExperimentalAssetGrid.value, - onChanged: changeUseExperimentalAssetGrid, - ), + children: const [ + // SwitchListTile.adaptive( + // activeColor: Theme.of(context).primaryColor, + // title: const Text( + // "experimental_settings_new_asset_list_title", + // style: TextStyle( + // fontSize: 12, + // fontWeight: FontWeight.bold, + // ), + // ).tr(), + // subtitle: const Text( + // "experimental_settings_new_asset_list_subtitle", + // style: TextStyle( + // fontSize: 12, + // ), + // ).tr(), + // value: useExperimentalAssetGrid.value, + // onChanged: changeUseExperimentalAssetGrid, + // ), ], ); } diff --git a/mobile/lib/modules/settings/views/settings_page.dart b/mobile/lib/modules/settings/views/settings_page.dart index 4176629dd..9a1679342 100644 --- a/mobile/lib/modules/settings/views/settings_page.dart +++ b/mobile/lib/modules/settings/views/settings_page.dart @@ -43,7 +43,7 @@ class SettingsPage extends HookConsumerWidget { const ThemeSetting(), const AssetListSettings(), if (Platform.isAndroid) const NotificationSetting(), - const ExperimentalSettings(), + //const ExperimentalSettings(), ], ).toList(), ], diff --git a/mobile/test/asset_grid_data_structure_test.dart b/mobile/test/asset_grid_data_structure_test.dart index 87ea20323..94d646ea0 100644 --- a/mobile/test/asset_grid_data_structure_test.dart +++ b/mobile/test/asset_grid_data_structure_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/modules/home/ui/asset_list_v2/asset_grid_data_structure.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; import 'package:openapi/api.dart'; void main() { From a117e897caca2ce19c1912d4f51b105ada2a8e2a Mon Sep 17 00:00:00 2001 From: Matthias Rupp Date: Sat, 1 Oct 2022 19:19:40 +0200 Subject: [PATCH 06/11] Move selection logic to asset grid class --- .../home/ui/asset_grid/daily_title_text.dart | 61 +-- .../disable_multi_select_button.dart | 76 ++-- .../home/ui/asset_grid/immich_asset_grid.dart | 166 ++++++--- .../ui/{ => asset_grid}/thumbnail_image.dart | 350 +++++++++--------- mobile/lib/modules/home/ui/image_grid.dart | 2 +- mobile/lib/modules/home/views/home_page.dart | 27 +- 6 files changed, 354 insertions(+), 328 deletions(-) rename mobile/lib/modules/home/ui/{ => asset_grid}/disable_multi_select_button.dart (83%) rename mobile/lib/modules/home/ui/{ => asset_grid}/thumbnail_image.dart (80%) diff --git a/mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart b/mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart index e16bea2b3..6814cccb3 100644 --- a/mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart +++ b/mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart @@ -8,11 +8,17 @@ class DailyTitleText extends ConsumerWidget { const DailyTitleText({ Key? key, required this.isoDate, - required this.assetGroup, + required this.multiselectEnabled, + required this.onSelect, + required this.onDeselect, + required this.selected, }) : super(key: key); final String isoDate; - final List assetGroup; + final bool multiselectEnabled; + final Function onSelect; + final Function onDeselect; + final bool selected; @override Widget build(BuildContext context, WidgetRef ref) { @@ -23,51 +29,12 @@ class DailyTitleText extends ConsumerWidget { : "daily_title_text_date_year".tr(); var dateText = DateFormat(formatDateTemplate).format(DateTime.parse(isoDate)); - var isMultiSelectEnable = - ref.watch(homePageStateProvider).isMultiSelectEnable; - var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup; - var selectedItems = ref.watch(homePageStateProvider).selectedItems; - void _handleTitleIconClick() { - if (isMultiSelectEnable && - selectedDateGroup.contains(dateText) && - selectedDateGroup.length == 1 && - selectedItems.length <= assetGroup.length) { - // Multi select is active - click again on the icon while it is the only active group -> disable multi select - ref.watch(homePageStateProvider.notifier).disableMultiSelect(); - } else if (isMultiSelectEnable && - selectedDateGroup.contains(dateText) && - selectedItems.length != assetGroup.length) { - // Multi select is active - click again on the icon while it is not the only active group -> remove that group from selected group/items - ref - .watch(homePageStateProvider.notifier) - .removeSelectedDateGroup(dateText); - ref - .watch(homePageStateProvider.notifier) - .removeMultipleSelectedItem(assetGroup); - } else if (isMultiSelectEnable && - selectedDateGroup.contains(dateText) && - selectedDateGroup.length > 1) { - ref - .watch(homePageStateProvider.notifier) - .removeSelectedDateGroup(dateText); - ref - .watch(homePageStateProvider.notifier) - .removeMultipleSelectedItem(assetGroup); - } else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) { - ref - .watch(homePageStateProvider.notifier) - .addSelectedDateGroup(dateText); - ref - .watch(homePageStateProvider.notifier) - .addMultipleSelectedItems(assetGroup); + void handleTitleIconClick() { + if (selected) { + onDeselect(); } else { - ref - .watch(homePageStateProvider.notifier) - .enableMultiSelect(assetGroup.toSet()); - ref - .watch(homePageStateProvider.notifier) - .addSelectedDateGroup(dateText); + onSelect(); } } @@ -89,8 +56,8 @@ class DailyTitleText extends ConsumerWidget { ), const Spacer(), GestureDetector( - onTap: _handleTitleIconClick, - child: isMultiSelectEnable && selectedDateGroup.contains(dateText) + onTap: handleTitleIconClick, + child: multiselectEnabled && selected ? Icon( Icons.check_circle_rounded, color: Theme.of(context).primaryColor, diff --git a/mobile/lib/modules/home/ui/disable_multi_select_button.dart b/mobile/lib/modules/home/ui/asset_grid/disable_multi_select_button.dart similarity index 83% rename from mobile/lib/modules/home/ui/disable_multi_select_button.dart rename to mobile/lib/modules/home/ui/asset_grid/disable_multi_select_button.dart index 2d450832e..cca2e0b20 100644 --- a/mobile/lib/modules/home/ui/disable_multi_select_button.dart +++ b/mobile/lib/modules/home/ui/asset_grid/disable_multi_select_button.dart @@ -1,40 +1,36 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -class DisableMultiSelectButton extends ConsumerWidget { - const DisableMultiSelectButton({ - Key? key, - required this.onPressed, - required this.selectedItemCount, - }) : super(key: key); - - final Function onPressed; - final int selectedItemCount; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Positioned( - top: 10, - left: 0, - child: Padding( - padding: const EdgeInsets.only(left: 16.0, top: 46), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: ElevatedButton.icon( - onPressed: () { - onPressed(); - }, - icon: const Icon(Icons.close_rounded), - label: Text( - '$selectedItemCount', - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 18, - ), - ), - ), - ), - ), - ); - } -} +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class DisableMultiSelectButton extends ConsumerWidget { + const DisableMultiSelectButton({ + Key? key, + required this.onPressed, + required this.selectedItemCount, + }) : super(key: key); + + final Function onPressed; + final int selectedItemCount; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: const EdgeInsets.only(left: 16.0, top: 15), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: ElevatedButton.icon( + onPressed: () { + onPressed(); + }, + icon: const Icon(Icons.close_rounded), + label: Text( + '$selectedItemCount', + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 18, + ), + ), + ), + ), + ); + } +} diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart index 7ff4a7890..568f7ee57 100644 --- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart @@ -1,3 +1,4 @@ +import 'dart:collection'; import 'dart:math'; import 'package:collection/collection.dart'; @@ -5,35 +6,27 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/framework.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart'; import 'package:openapi/api.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'asset_grid_data_structure.dart'; import 'daily_title_text.dart'; +import 'disable_multi_select_button.dart'; import 'draggable_scrollbar_custom.dart'; -class ImmichAssetGrid extends HookConsumerWidget { +typedef ImmichAssetGridSelectionListener = void Function(bool); + +class ImmichAssetGridState extends State { final ItemScrollController _itemScrollController = ItemScrollController(); final ItemPositionsListener _itemPositionsListener = - ItemPositionsListener.create(); + ItemPositionsListener.create(); - final List renderList; - final int assetsPerRow; - final double margin; - final bool showStorageIndicator; - - ImmichAssetGrid({ - super.key, - required this.renderList, - required this.assetsPerRow, - required this.showStorageIndicator, - this.margin = 5.0, - }); + bool _scrolling = false; + bool _multiselect = false; + Set _selectedAssets = HashSet(); List get _assets { - return renderList + return widget.renderList .map((e) { if (e.type == RenderAssetGridElementType.assetRow) { return e.assetRow!.assets; @@ -44,10 +37,49 @@ class ImmichAssetGrid extends HookConsumerWidget { .flattened .toList(); } + + void _selectAssets(List assets) { + setState(() { + + if (!_multiselect) { + _multiselect = true; + widget.listener?.call(true); + } + + for (var e in assets) { + _selectedAssets.add(e.id); + } + }); + } + + void _deselectAssets(List assets) { + setState(() { + for (var e in assets) { + _selectedAssets.remove(e.id); + } + + if (_selectedAssets.isEmpty) { + _multiselect = false; + widget.listener?.call(false); + } + }); + } + + void _deselectAll() { + setState(() { + _multiselect = false; + _selectedAssets.clear(); + }); + widget.listener?.call(false); + } + + bool _allAssetsSelected(List assets) { + return _multiselect && assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null; + } double _getItemSize(BuildContext context) { - return MediaQuery.of(context).size.width / assetsPerRow - - margin * (assetsPerRow - 1) / assetsPerRow; + return MediaQuery.of(context).size.width / widget.assetsPerRow - + widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow; } Widget _buildThumbnailOrPlaceholder( @@ -60,7 +92,10 @@ class ImmichAssetGrid extends HookConsumerWidget { return ThumbnailImage( asset: asset, assetList: _assets, - showStorageIndicator: showStorageIndicator, + multiselectEnabled: _multiselect, + isSelected: _selectedAssets.contains(asset.id), + onSelect: () => _selectAssets([asset]), + onDeselect: () => _deselectAssets([asset]), useGrayBoxPlaceholder: true, ); } @@ -78,7 +113,7 @@ class ImmichAssetGrid extends HookConsumerWidget { key: Key("asset-${asset.id}"), width: size, height: size, - margin: EdgeInsets.only(top: margin, right: last ? 0.0 : margin), + margin: EdgeInsets.only(top: widget.margin, right: last ? 0.0 : widget.margin), child: _buildThumbnailOrPlaceholder(asset, scrolling), ); }).toList(), @@ -89,7 +124,10 @@ class ImmichAssetGrid extends HookConsumerWidget { BuildContext context, String title, List assets) { return DailyTitleText( isoDate: title, - assetGroup: assets, + multiselectEnabled: _multiselect, + onSelect: () => _selectAssets(assets), + onDeselect: () => _deselectAssets(assets), + selected: _allAssetsSelected(assets), ); } @@ -111,22 +149,22 @@ class ImmichAssetGrid extends HookConsumerWidget { ); } - Widget _itemBuilder(BuildContext c, int position, bool scrolling) { - final item = renderList[position]; + Widget _itemBuilder(BuildContext c, int position) { + final item = widget.renderList[position]; if (item.type == RenderAssetGridElementType.dayTitle) { return _buildTitle(c, item.title!, item.relatedAssetList!); } else if (item.type == RenderAssetGridElementType.monthTitle) { return _buildMonthTitle(c, item.title!); } else if (item.type == RenderAssetGridElementType.assetRow) { - return _buildAssetRow(c, item.assetRow!, scrolling); + return _buildAssetRow(c, item.assetRow!, _scrolling); } return const Text("Invalid widget type!"); } Text _labelBuilder(int pos) { - final date = renderList[pos].date; + final date = widget.renderList[pos].date; return Text(DateFormat.yMMMd().format(date), style: const TextStyle( color: Colors.white, @@ -135,26 +173,27 @@ class ImmichAssetGrid extends HookConsumerWidget { ); } + Widget _buildMultiSelectIndicator() { + return DisableMultiSelectButton( + onPressed: () => _deselectAll(), + selectedItemCount: _selectedAssets.length, + ); + } - @override - Widget build(BuildContext context, WidgetRef ref) { - final scrolling = useState(false); - + Widget _buildAssetGrid() { final useDragScrolling = _assets.length > 100; void dragScrolling(bool active) { - scrolling.value = active; - } - - Widget itemBuilder(BuildContext c, int position) { - return _itemBuilder(c, position, scrolling.value); + setState(() { + _scrolling = active; + }); } final listWidget = ScrollablePositionedList.builder( - itemBuilder: itemBuilder, + itemBuilder: _itemBuilder, itemPositionsListener: _itemPositionsListener, itemScrollController: _itemScrollController, - itemCount: renderList.length, + itemCount: widget.renderList.length, ); if (!useDragScrolling) { @@ -162,15 +201,48 @@ class ImmichAssetGrid extends HookConsumerWidget { } return DraggableScrollbar.semicircle( - scrollStateListener: dragScrolling, - itemPositionsListener: _itemPositionsListener, - controller: _itemScrollController, - backgroundColor: Theme.of(context).hintColor, - labelTextBuilder: _labelBuilder, - labelConstraints: const BoxConstraints(maxHeight: 28), - scrollbarAnimationDuration: const Duration(seconds: 1), - scrollbarTimeToFade: const Duration(seconds: 4), - child: listWidget, + scrollStateListener: dragScrolling, + itemPositionsListener: _itemPositionsListener, + controller: _itemScrollController, + backgroundColor: Theme.of(context).hintColor, + labelTextBuilder: _labelBuilder, + labelConstraints: const BoxConstraints(maxHeight: 28), + scrollbarAnimationDuration: const Duration(seconds: 1), + scrollbarTimeToFade: const Duration(seconds: 4), + child: listWidget, + ); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + _buildAssetGrid(), + if (_multiselect) _buildMultiSelectIndicator(), + ], ); } } + +class ImmichAssetGrid extends StatefulWidget { + final List renderList; + final int assetsPerRow; + final double margin; + final bool showStorageIndicator; + final ImmichAssetGridSelectionListener? listener; + + + ImmichAssetGrid({ + super.key, + required this.renderList, + required this.assetsPerRow, + required this.showStorageIndicator, + this.listener, + this.margin = 5.0, + }); + + @override + State createState() { + return ImmichAssetGridState(); + } +} \ No newline at end of file diff --git a/mobile/lib/modules/home/ui/thumbnail_image.dart b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart similarity index 80% rename from mobile/lib/modules/home/ui/thumbnail_image.dart rename to mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart index 1eb165f62..13533942d 100644 --- a/mobile/lib/modules/home/ui/thumbnail_image.dart +++ b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart @@ -1,176 +1,174 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/hive_box.dart'; -import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; -import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:openapi/api.dart'; - -class ThumbnailImage extends HookConsumerWidget { - final AssetResponseDto asset; - final List assetList; - final bool showStorageIndicator; - final bool useGrayBoxPlaceholder; - - const ThumbnailImage({ - Key? key, - required this.asset, - required this.assetList, - this.showStorageIndicator = true, - this.useGrayBoxPlaceholder = false, - }) : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - var box = Hive.box(userInfoBox); - var thumbnailRequestUrl = getThumbnailUrl(asset); - var selectedAsset = ref.watch(homePageStateProvider).selectedItems; - var isMultiSelectEnable = - ref.watch(homePageStateProvider).isMultiSelectEnable; - var deviceId = ref.watch(authenticationProvider).deviceId; - - Widget buildSelectionIcon(AssetResponseDto asset) { - if (selectedAsset.contains(asset)) { - return Icon( - Icons.check_circle, - color: Theme.of(context).primaryColor, - ); - } else { - return const Icon( - Icons.circle_outlined, - color: Colors.white, - ); - } - } - - return GestureDetector( - onTap: () { - if (isMultiSelectEnable && - selectedAsset.contains(asset) && - selectedAsset.length == 1) { - ref.watch(homePageStateProvider.notifier).disableMultiSelect(); - } else if (isMultiSelectEnable && - selectedAsset.contains(asset) && - selectedAsset.length > 1) { - ref - .watch(homePageStateProvider.notifier) - .removeSingleSelectedItem(asset); - } else if (isMultiSelectEnable && !selectedAsset.contains(asset)) { - ref - .watch(homePageStateProvider.notifier) - .addSingleSelectedItem(asset); - } else { - AutoRouter.of(context).push( - GalleryViewerRoute( - assetList: assetList, - asset: asset, - ), - ); - } - }, - onLongPress: () { - // Enable multi select function - ref.watch(homePageStateProvider.notifier).enableMultiSelect({asset}); - HapticFeedback.heavyImpact(); - }, - child: Hero( - tag: asset.id, - child: Stack( - children: [ - Container( - decoration: BoxDecoration( - border: isMultiSelectEnable && selectedAsset.contains(asset) - ? Border.all( - color: Theme.of(context).primaryColorLight, - width: 10, - ) - : const Border(), - ), - child: CachedNetworkImage( - cacheKey: 'thumbnail-image-${asset.id}', - width: 300, - height: 300, - memCacheHeight: 200, - maxWidthDiskCache: 200, - maxHeightDiskCache: 200, - fit: BoxFit.cover, - imageUrl: thumbnailRequestUrl, - httpHeaders: { - "Authorization": "Bearer ${box.get(accessTokenKey)}" - }, - fadeInDuration: const Duration(milliseconds: 250), - progressIndicatorBuilder: (context, url, downloadProgress) { - if (useGrayBoxPlaceholder) { - return const DecoratedBox( - decoration: BoxDecoration(color: Colors.grey), - ); - } - return Transform.scale( - scale: 0.2, - child: CircularProgressIndicator( - value: downloadProgress.progress, - ), - ); - }, - errorWidget: (context, url, error) { - debugPrint("Error getting thumbnail $url = $error"); - CachedNetworkImage.evictFromCache(thumbnailRequestUrl); - - return Icon( - Icons.image_not_supported_outlined, - color: Theme.of(context).primaryColor, - ); - }, - ), - ), - if (isMultiSelectEnable) - Padding( - padding: const EdgeInsets.all(3.0), - child: Align( - alignment: Alignment.topLeft, - child: buildSelectionIcon(asset), - ), - ), - if (showStorageIndicator) - Positioned( - right: 10, - bottom: 5, - child: Icon( - (deviceId != asset.deviceId) - ? Icons.cloud_done_outlined - : Icons.photo_library_rounded, - color: Colors.white, - size: 18, - ), - ), - if (asset.type != AssetTypeEnum.IMAGE) - Positioned( - top: 5, - right: 5, - child: Row( - children: [ - Text( - asset.duration.toString().substring(0, 7), - style: const TextStyle( - color: Colors.white, - fontSize: 10, - ), - ), - const Icon( - Icons.play_circle_outline_rounded, - color: Colors.white, - ), - ], - ), - ), - ], - ), - ), - ); - } -} +import 'package:auto_route/auto_route.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/hive_box.dart'; +import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; +import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:openapi/api.dart'; + +class ThumbnailImage extends HookConsumerWidget { + final AssetResponseDto asset; + final List assetList; + final bool showStorageIndicator; + final bool useGrayBoxPlaceholder; + final bool isSelected; + final bool multiselectEnabled; + final Function? onSelect; + final Function? onDeselect; + + const ThumbnailImage({ + Key? key, + required this.asset, + required this.assetList, + this.showStorageIndicator = true, + this.useGrayBoxPlaceholder = false, + this.isSelected = false, + this.multiselectEnabled = false, + this.onDeselect, + this.onSelect, + }) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + var box = Hive.box(userInfoBox); + var thumbnailRequestUrl = getThumbnailUrl(asset); + var deviceId = ref.watch(authenticationProvider).deviceId; + + + Widget buildSelectionIcon(AssetResponseDto asset) { + if (isSelected) { + return Icon( + Icons.check_circle, + color: Theme.of(context).primaryColor, + ); + } else { + return const Icon( + Icons.circle_outlined, + color: Colors.white, + ); + } + } + + return GestureDetector( + onTap: () { + if (multiselectEnabled) { + if (isSelected) { + onDeselect?.call(); + } else { + onSelect?.call(); + } + } else { + AutoRouter.of(context).push( + GalleryViewerRoute( + assetList: assetList, + asset: asset, + ), + ); + } + }, + onLongPress: () { + onSelect?.call(); + HapticFeedback.heavyImpact(); + }, + child: Hero( + tag: asset.id, + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + border: multiselectEnabled && isSelected + ? Border.all( + color: Theme.of(context).primaryColorLight, + width: 10, + ) + : const Border(), + ), + child: CachedNetworkImage( + cacheKey: 'thumbnail-image-${asset.id}', + width: 300, + height: 300, + memCacheHeight: 200, + maxWidthDiskCache: 200, + maxHeightDiskCache: 200, + fit: BoxFit.cover, + imageUrl: thumbnailRequestUrl, + httpHeaders: { + "Authorization": "Bearer ${box.get(accessTokenKey)}" + }, + fadeInDuration: const Duration(milliseconds: 250), + progressIndicatorBuilder: (context, url, downloadProgress) { + if (useGrayBoxPlaceholder) { + return const DecoratedBox( + decoration: BoxDecoration(color: Colors.grey), + ); + } + return Transform.scale( + scale: 0.2, + child: CircularProgressIndicator( + value: downloadProgress.progress, + ), + ); + }, + errorWidget: (context, url, error) { + debugPrint("Error getting thumbnail $url = $error"); + CachedNetworkImage.evictFromCache(thumbnailRequestUrl); + + return Icon( + Icons.image_not_supported_outlined, + color: Theme.of(context).primaryColor, + ); + }, + ), + ), + if (multiselectEnabled) + Padding( + padding: const EdgeInsets.all(3.0), + child: Align( + alignment: Alignment.topLeft, + child: buildSelectionIcon(asset), + ), + ), + if (showStorageIndicator) + Positioned( + right: 10, + bottom: 5, + child: Icon( + (deviceId != asset.deviceId) + ? Icons.cloud_done_outlined + : Icons.photo_library_rounded, + color: Colors.white, + size: 18, + ), + ), + if (asset.type != AssetTypeEnum.IMAGE) + Positioned( + top: 5, + right: 5, + child: Row( + children: [ + Text( + asset.duration.toString().substring(0, 7), + style: const TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), + const Icon( + Icons.play_circle_outline_rounded, + color: Colors.white, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/modules/home/ui/image_grid.dart b/mobile/lib/modules/home/ui/image_grid.dart index f7efe613d..7f0c6304e 100644 --- a/mobile/lib/modules/home/ui/image_grid.dart +++ b/mobile/lib/modules/home/ui/image_grid.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart'; import 'package:openapi/api.dart'; // ignore: must_be_immutable diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index ebfc55680..500e30b08 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -5,7 +5,6 @@ import 'package:immich_mobile/modules/home/providers/home_page_render_list_provi import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart'; -import 'package:immich_mobile/modules/home/ui/disable_multi_select_button.dart'; import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart'; import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; @@ -20,12 +19,9 @@ class HomePage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final appSettingService = ref.watch(appSettingsServiceProvider); - var renderList = ref.watch(renderListProvider); - var isMultiSelectEnable = - ref.watch(homePageStateProvider).isMultiSelectEnable; - var homePageState = ref.watch(homePageStateProvider); + final multiselectEnabled = useState(false); useEffect( () { @@ -41,16 +37,9 @@ class HomePage extends HookConsumerWidget { ref.read(assetProvider.notifier).getAllAsset(); } - buildSelectedItemCountIndicator() { - return DisableMultiSelectButton( - onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect, - selectedItemCount: homePageState.selectedItems.length, - ); - } - Widget buildBody() { buildSliverAppBar() { - return isMultiSelectEnable + return multiselectEnabled.value ? const SliverToBoxAdapter( child: SizedBox( height: 70, @@ -62,9 +51,13 @@ class HomePage extends HookConsumerWidget { ); } + void selectionListener(bool multiselect) { + multiselectEnabled.value = multiselect; + } + return SafeArea( - bottom: !isMultiSelectEnable, - top: !isMultiSelectEnable, + bottom: !multiselectEnabled.value, + top: !multiselectEnabled.value, child: Stack( children: [ CustomScrollView( @@ -80,10 +73,10 @@ class HomePage extends HookConsumerWidget { appSettingService.getSetting(AppSettingsEnum.tilesPerRow), showStorageIndicator: appSettingService .getSetting(AppSettingsEnum.storageIndicator), + listener: selectionListener, ), ), - if (isMultiSelectEnable) ...[ - buildSelectedItemCountIndicator(), + if (multiselectEnabled.value) ...[ const ControlBottomAppBar(), ], ], From 6b8453463243be8163f30fbbbe767d54c2fd0200 Mon Sep 17 00:00:00 2001 From: Matthias Rupp Date: Thu, 6 Oct 2022 22:41:56 +0200 Subject: [PATCH 07/11] Get rid of home page state provider --- .../home/models/home_page_state.model.dart | 47 -------- .../providers/home_page_state.provider.dart | 91 --------------- .../home/ui/asset_grid/daily_title_text.dart | 2 - .../home/ui/asset_grid/immich_asset_grid.dart | 45 +++++--- .../{ => asset_grid}/monthly_title_text.dart | 0 .../home/ui/asset_grid/thumbnail_image.dart | 2 - .../home/ui/control_bottom_app_bar.dart | 21 ++-- .../lib/modules/home/ui/daily_title_text.dart | 109 ------------------ .../lib/modules/home/ui/delete_diaglog.dart | 13 +-- mobile/lib/modules/home/ui/image_grid.dart | 47 -------- mobile/lib/modules/home/views/home_page.dart | 43 ++++--- .../lib/shared/views/tab_controller_page.dart | 6 +- 12 files changed, 72 insertions(+), 354 deletions(-) delete mode 100644 mobile/lib/modules/home/models/home_page_state.model.dart delete mode 100644 mobile/lib/modules/home/providers/home_page_state.provider.dart rename mobile/lib/modules/home/ui/{ => asset_grid}/monthly_title_text.dart (100%) delete mode 100644 mobile/lib/modules/home/ui/daily_title_text.dart delete mode 100644 mobile/lib/modules/home/ui/image_grid.dart diff --git a/mobile/lib/modules/home/models/home_page_state.model.dart b/mobile/lib/modules/home/models/home_page_state.model.dart deleted file mode 100644 index 1701ac3ff..000000000 --- a/mobile/lib/modules/home/models/home_page_state.model.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:collection/collection.dart'; - -import 'package:openapi/api.dart'; - -class HomePageState { - final bool isMultiSelectEnable; - final Set selectedItems; - final Set selectedDateGroup; - HomePageState({ - required this.isMultiSelectEnable, - required this.selectedItems, - required this.selectedDateGroup, - }); - - HomePageState copyWith({ - bool? isMultiSelectEnable, - Set? selectedItems, - Set? selectedDateGroup, - }) { - return HomePageState( - isMultiSelectEnable: isMultiSelectEnable ?? this.isMultiSelectEnable, - selectedItems: selectedItems ?? this.selectedItems, - selectedDateGroup: selectedDateGroup ?? this.selectedDateGroup, - ); - } - - @override - String toString() => - 'HomePageState(isMultiSelectEnable: $isMultiSelectEnable, selectedItems: $selectedItems, selectedDateGroup: $selectedDateGroup)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final setEquals = const DeepCollectionEquality().equals; - - return other is HomePageState && - other.isMultiSelectEnable == isMultiSelectEnable && - setEquals(other.selectedItems, selectedItems) && - setEquals(other.selectedDateGroup, selectedDateGroup); - } - - @override - int get hashCode => - isMultiSelectEnable.hashCode ^ - selectedItems.hashCode ^ - selectedDateGroup.hashCode; -} diff --git a/mobile/lib/modules/home/providers/home_page_state.provider.dart b/mobile/lib/modules/home/providers/home_page_state.provider.dart deleted file mode 100644 index ff4f8cfd9..000000000 --- a/mobile/lib/modules/home/providers/home_page_state.provider.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/models/home_page_state.model.dart'; -import 'package:immich_mobile/shared/services/share.service.dart'; -import 'package:immich_mobile/shared/ui/share_dialog.dart'; -import 'package:openapi/api.dart'; - -class HomePageStateNotifier extends StateNotifier { - - final ShareService _shareService; - - HomePageStateNotifier(this._shareService) - : super( - HomePageState( - isMultiSelectEnable: false, - selectedItems: {}, - selectedDateGroup: {}, - ), - ); - - void addSelectedDateGroup(String dateGroupTitle) { - state = state.copyWith( - selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle}, - ); - } - - void removeSelectedDateGroup(String dateGroupTitle) { - var currentDateGroup = state.selectedDateGroup; - - currentDateGroup.removeWhere((e) => e == dateGroupTitle); - - state = state.copyWith(selectedDateGroup: currentDateGroup); - } - - void enableMultiSelect(Set selectedItems) { - state = - state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems); - } - - void disableMultiSelect() { - state = state.copyWith( - isMultiSelectEnable: false, - selectedItems: {}, - selectedDateGroup: {}, - ); - } - - void addSingleSelectedItem(AssetResponseDto asset) { - state = state.copyWith(selectedItems: {...state.selectedItems, asset}); - } - - void addMultipleSelectedItems(List assets) { - state = state.copyWith(selectedItems: {...state.selectedItems, ...assets}); - } - - void removeSingleSelectedItem(AssetResponseDto asset) { - Set currentList = state.selectedItems; - - currentList.removeWhere((e) => e.id == asset.id); - - state = state.copyWith(selectedItems: currentList); - } - - void removeMultipleSelectedItem(List assets) { - Set currentList = state.selectedItems; - - for (AssetResponseDto asset in assets) { - currentList.removeWhere((e) => e.id == asset.id); - } - - state = state.copyWith(selectedItems: currentList); - } - - void shareAssets(List assets, BuildContext context) { - showDialog( - context: context, - builder: (BuildContext buildContext) { - _shareService - .shareAssets(assets) - .then((_) => Navigator.of(buildContext).pop()); - return const ShareDialog(); - }, - barrierDismissible: false, - ); - } -} - -final homePageStateProvider = - StateNotifierProvider( - ((ref) => HomePageStateNotifier(ref.watch(shareServiceProvider))), -); diff --git a/mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart b/mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart index 6814cccb3..78e033ce5 100644 --- a/mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart +++ b/mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart @@ -1,8 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; -import 'package:openapi/api.dart'; class DailyTitleText extends ConsumerWidget { const DailyTitleText({ diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart index 568f7ee57..aa50b6c24 100644 --- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart @@ -14,12 +14,13 @@ import 'daily_title_text.dart'; import 'disable_multi_select_button.dart'; import 'draggable_scrollbar_custom.dart'; -typedef ImmichAssetGridSelectionListener = void Function(bool); +typedef ImmichAssetGridSelectionListener = void Function( + bool, Set); class ImmichAssetGridState extends State { final ItemScrollController _itemScrollController = ItemScrollController(); final ItemPositionsListener _itemPositionsListener = - ItemPositionsListener.create(); + ItemPositionsListener.create(); bool _scrolling = false; bool _multiselect = false; @@ -37,18 +38,26 @@ class ImmichAssetGridState extends State { .flattened .toList(); } - + + Set _getSelectedAssets() { + return _selectedAssets + .map((e) => _assets.firstWhereOrNull((a) => a.id == e)) + .whereNotNull() + .toSet(); + } + + void _callSelectionListener() { + widget.listener?.call(_multiselect, _getSelectedAssets()); + } + void _selectAssets(List assets) { setState(() { - - if (!_multiselect) { - _multiselect = true; - widget.listener?.call(true); - } - for (var e in assets) { _selectedAssets.add(e.id); } + + _multiselect = true; + _callSelectionListener(); }); } @@ -60,8 +69,9 @@ class ImmichAssetGridState extends State { if (_selectedAssets.isEmpty) { _multiselect = false; - widget.listener?.call(false); } + + _callSelectionListener(); }); } @@ -70,11 +80,13 @@ class ImmichAssetGridState extends State { _multiselect = false; _selectedAssets.clear(); }); - widget.listener?.call(false); + + _callSelectionListener(); } bool _allAssetsSelected(List assets) { - return _multiselect && assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null; + return _multiselect && + assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null; } double _getItemSize(BuildContext context) { @@ -113,7 +125,8 @@ class ImmichAssetGridState extends State { key: Key("asset-${asset.id}"), width: size, height: size, - margin: EdgeInsets.only(top: widget.margin, right: last ? 0.0 : widget.margin), + margin: EdgeInsets.only( + top: widget.margin, right: last ? 0.0 : widget.margin), child: _buildThumbnailOrPlaceholder(asset, scrolling), ); }).toList(), @@ -165,7 +178,8 @@ class ImmichAssetGridState extends State { Text _labelBuilder(int pos) { final date = widget.renderList[pos].date; - return Text(DateFormat.yMMMd().format(date), + return Text( + DateFormat.yMMMd().format(date), style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, @@ -231,7 +245,6 @@ class ImmichAssetGrid extends StatefulWidget { final bool showStorageIndicator; final ImmichAssetGridSelectionListener? listener; - ImmichAssetGrid({ super.key, required this.renderList, @@ -245,4 +258,4 @@ class ImmichAssetGrid extends StatefulWidget { State createState() { return ImmichAssetGridState(); } -} \ No newline at end of file +} diff --git a/mobile/lib/modules/home/ui/monthly_title_text.dart b/mobile/lib/modules/home/ui/asset_grid/monthly_title_text.dart similarity index 100% rename from mobile/lib/modules/home/ui/monthly_title_text.dart rename to mobile/lib/modules/home/ui/asset_grid/monthly_title_text.dart diff --git a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart index 13533942d..fecdf66eb 100644 --- a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart +++ b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart @@ -1,12 +1,10 @@ import 'package:auto_route/auto_route.dart'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/hive_box.dart'; -import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; diff --git a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart index 875647e73..dceaea4ac 100644 --- a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart +++ b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart @@ -1,11 +1,15 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart'; class ControlBottomAppBar extends ConsumerWidget { - const ControlBottomAppBar({Key? key}) : super(key: key); + final Function onShare; + final Function onDelete; + + const ControlBottomAppBar( + {Key? key, required this.onShare, required this.onDelete}) + : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { @@ -36,7 +40,9 @@ class ControlBottomAppBar extends ConsumerWidget { showDialog( context: context, builder: (BuildContext context) { - return const DeleteDialog(); + return DeleteDialog( + onDelete: onDelete, + ); }, ); }, @@ -45,14 +51,7 @@ class ControlBottomAppBar extends ConsumerWidget { iconData: Icons.share, label: "control_bottom_app_bar_share".tr(), onPressed: () { - final homePageState = ref.watch(homePageStateProvider); - ref.watch(homePageStateProvider.notifier).shareAssets( - homePageState.selectedItems.toList(), - context, - ); - ref - .watch(homePageStateProvider.notifier) - .disableMultiSelect(); + onShare(); }, ), ], diff --git a/mobile/lib/modules/home/ui/daily_title_text.dart b/mobile/lib/modules/home/ui/daily_title_text.dart deleted file mode 100644 index f083cc6e6..000000000 --- a/mobile/lib/modules/home/ui/daily_title_text.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; -import 'package:openapi/api.dart'; - -class DailyTitleText extends ConsumerWidget { - const DailyTitleText({ - Key? key, - required this.isoDate, - required this.assetGroup, - }) : super(key: key); - - final String isoDate; - final List assetGroup; - - @override - Widget build(BuildContext context, WidgetRef ref) { - var currentYear = DateTime.now().year; - var groupYear = DateTime.parse(isoDate).year; - var formatDateTemplate = currentYear == groupYear - ? "daily_title_text_date".tr() - : "daily_title_text_date_year".tr(); - var dateText = DateFormat(formatDateTemplate) - .format(DateTime.parse(isoDate).toLocal()); - var isMultiSelectEnable = - ref.watch(homePageStateProvider).isMultiSelectEnable; - var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup; - var selectedItems = ref.watch(homePageStateProvider).selectedItems; - - void _handleTitleIconClick() { - if (isMultiSelectEnable && - selectedDateGroup.contains(dateText) && - selectedDateGroup.length == 1 && - selectedItems.length <= assetGroup.length) { - // Multi select is active - click again on the icon while it is the only active group -> disable multi select - ref.watch(homePageStateProvider.notifier).disableMultiSelect(); - } else if (isMultiSelectEnable && - selectedDateGroup.contains(dateText) && - selectedItems.length != assetGroup.length) { - // Multi select is active - click again on the icon while it is not the only active group -> remove that group from selected group/items - ref - .watch(homePageStateProvider.notifier) - .removeSelectedDateGroup(dateText); - ref - .watch(homePageStateProvider.notifier) - .removeMultipleSelectedItem(assetGroup); - } else if (isMultiSelectEnable && - selectedDateGroup.contains(dateText) && - selectedDateGroup.length > 1) { - ref - .watch(homePageStateProvider.notifier) - .removeSelectedDateGroup(dateText); - ref - .watch(homePageStateProvider.notifier) - .removeMultipleSelectedItem(assetGroup); - } else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) { - ref - .watch(homePageStateProvider.notifier) - .addSelectedDateGroup(dateText); - ref - .watch(homePageStateProvider.notifier) - .addMultipleSelectedItems(assetGroup); - } else { - ref - .watch(homePageStateProvider.notifier) - .enableMultiSelect(assetGroup.toSet()); - ref - .watch(homePageStateProvider.notifier) - .addSelectedDateGroup(dateText); - } - } - - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only( - top: 29.0, - bottom: 29.0, - left: 12.0, - right: 12.0, - ), - child: Row( - children: [ - Text( - dateText, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - const Spacer(), - GestureDetector( - onTap: _handleTitleIconClick, - child: isMultiSelectEnable && selectedDateGroup.contains(dateText) - ? Icon( - Icons.check_circle_rounded, - color: Theme.of(context).primaryColor, - ) - : const Icon( - Icons.check_circle_outline_rounded, - color: Colors.grey, - ), - ) - ], - ), - ), - ); - } -} diff --git a/mobile/lib/modules/home/ui/delete_diaglog.dart b/mobile/lib/modules/home/ui/delete_diaglog.dart index 3adef135a..cca569ee0 100644 --- a/mobile/lib/modules/home/ui/delete_diaglog.dart +++ b/mobile/lib/modules/home/ui/delete_diaglog.dart @@ -1,15 +1,14 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/shared/providers/asset.provider.dart'; -import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; class DeleteDialog extends ConsumerWidget { - const DeleteDialog({Key? key}) : super(key: key); + final Function onDelete; + + const DeleteDialog({Key? key, required this.onDelete}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { - final homePageState = ref.watch(homePageStateProvider); return AlertDialog( backgroundColor: Colors.grey[200], @@ -28,11 +27,7 @@ class DeleteDialog extends ConsumerWidget { ), TextButton( onPressed: () { - ref - .watch(assetProvider.notifier) - .deleteAssets(homePageState.selectedItems); - ref.watch(homePageStateProvider.notifier).disableMultiSelect(); - + onDelete(); Navigator.of(context).pop(); }, child: Text( diff --git a/mobile/lib/modules/home/ui/image_grid.dart b/mobile/lib/modules/home/ui/image_grid.dart deleted file mode 100644 index 7f0c6304e..000000000 --- a/mobile/lib/modules/home/ui/image_grid.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart'; -import 'package:openapi/api.dart'; - -// ignore: must_be_immutable -class ImageGrid extends ConsumerWidget { - final List assetGroup; - final List sortedAssetGroup; - final int tilesPerRow; - final bool showStorageIndicator; - - ImageGrid({ - Key? key, - required this.assetGroup, - required this.sortedAssetGroup, - this.tilesPerRow = 4, - this.showStorageIndicator = true, - }) : super(key: key); - - List imageSortedList = []; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SliverGrid( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: tilesPerRow, - crossAxisSpacing: 5.0, - mainAxisSpacing: 5, - ), - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - var assetType = assetGroup[index].type; - return GestureDetector( - onTap: () {}, - child: ThumbnailImage( - asset: assetGroup[index], - assetList: sortedAssetGroup, - showStorageIndicator: showStorageIndicator, - ), - ); - }, - childCount: assetGroup.length, - ), - ); - } -} diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index 500e30b08..2e38ee08f 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -2,7 +2,6 @@ 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/providers/home_page_render_list_provider.dart'; -import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart'; import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart'; @@ -12,6 +11,8 @@ import 'package:immich_mobile/modules/settings/services/app_settings.service.dar import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/providers/websocket.provider.dart'; +import 'package:immich_mobile/shared/services/share.service.dart'; +import 'package:openapi/api.dart'; class HomePage extends HookConsumerWidget { const HomePage({Key? key}) : super(key: key); @@ -22,6 +23,7 @@ class HomePage extends HookConsumerWidget { var renderList = ref.watch(renderListProvider); final multiselectEnabled = useState(false); + final selection = useState({}); useEffect( () { @@ -38,21 +40,18 @@ class HomePage extends HookConsumerWidget { } Widget buildBody() { - buildSliverAppBar() { - return multiselectEnabled.value - ? const SliverToBoxAdapter( - child: SizedBox( - height: 70, - child: null, - ), - ) - : ImmichSliverAppBar( - onPopBack: reloadAllAsset, - ); + void selectionListener( + bool multiselect, Set selectedAssets) { + multiselectEnabled.value = multiselect; + selection.value = selectedAssets; } - void selectionListener(bool multiselect) { - multiselectEnabled.value = multiselect; + void onShareAssets() { + ref.watch(shareServiceProvider).shareAssets(selection.value.toList()); + } + + void onDelete() { + ref.watch(assetProvider.notifier).deleteAssets(selection.value); } return SafeArea( @@ -62,7 +61,16 @@ class HomePage extends HookConsumerWidget { children: [ CustomScrollView( slivers: [ - buildSliverAppBar(), + multiselectEnabled.value + ? const SliverToBoxAdapter( + child: SizedBox( + height: 70, + child: null, + ), + ) + : ImmichSliverAppBar( + onPopBack: reloadAllAsset, + ), ], ), Padding( @@ -77,7 +85,10 @@ class HomePage extends HookConsumerWidget { ), ), if (multiselectEnabled.value) ...[ - const ControlBottomAppBar(), + ControlBottomAppBar( + onShare: onShareAssets, + onDelete: onDelete, + ), ], ], ), diff --git a/mobile/lib/shared/views/tab_controller_page.dart b/mobile/lib/shared/views/tab_controller_page.dart index 51a13fea5..7fe3c2f2f 100644 --- a/mobile/lib/shared/views/tab_controller_page.dart +++ b/mobile/lib/shared/views/tab_controller_page.dart @@ -1,8 +1,8 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; import 'package:immich_mobile/routing/router.dart'; class TabControllerPage extends ConsumerWidget { @@ -10,8 +10,6 @@ class TabControllerPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - var isMultiSelectEnable = - ref.watch(homePageStateProvider).isMultiSelectEnable; return AutoTabsRouter( routes: [ @@ -32,7 +30,7 @@ class TabControllerPage extends ConsumerWidget { opacity: animation, child: child, ), - bottomNavigationBar: isMultiSelectEnable + bottomNavigationBar: false ? null : BottomNavigationBar( selectedLabelStyle: const TextStyle( From 3c807ae86e9eb854fce115709c178ef267e306e2 Mon Sep 17 00:00:00 2001 From: Matthias Rupp Date: Sat, 8 Oct 2022 13:08:56 +0200 Subject: [PATCH 08/11] Exernalize multiselect state --- .../home/ui/asset_grid/immich_asset_grid.dart | 40 ++++++++++--------- mobile/lib/modules/home/views/home_page.dart | 3 ++ 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart index aa50b6c24..74124f76e 100644 --- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart @@ -23,7 +23,6 @@ class ImmichAssetGridState extends State { ItemPositionsListener.create(); bool _scrolling = false; - bool _multiselect = false; Set _selectedAssets = HashSet(); List get _assets { @@ -46,8 +45,8 @@ class ImmichAssetGridState extends State { .toSet(); } - void _callSelectionListener() { - widget.listener?.call(_multiselect, _getSelectedAssets()); + void _callSelectionListener(bool selectionActive) { + widget.listener?.call(selectionActive, _getSelectedAssets()); } void _selectAssets(List assets) { @@ -55,9 +54,7 @@ class ImmichAssetGridState extends State { for (var e in assets) { _selectedAssets.add(e.id); } - - _multiselect = true; - _callSelectionListener(); + _callSelectionListener(true); }); } @@ -66,26 +63,20 @@ class ImmichAssetGridState extends State { for (var e in assets) { _selectedAssets.remove(e.id); } - - if (_selectedAssets.isEmpty) { - _multiselect = false; - } - - _callSelectionListener(); + _callSelectionListener(_selectedAssets.isNotEmpty); }); } void _deselectAll() { setState(() { - _multiselect = false; _selectedAssets.clear(); }); - _callSelectionListener(); + _callSelectionListener(false); } bool _allAssetsSelected(List assets) { - return _multiselect && + return widget.selectionActive && assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null; } @@ -104,7 +95,7 @@ class ImmichAssetGridState extends State { return ThumbnailImage( asset: asset, assetList: _assets, - multiselectEnabled: _multiselect, + multiselectEnabled: widget.selectionActive, isSelected: _selectedAssets.contains(asset.id), onSelect: () => _selectAssets([asset]), onDeselect: () => _deselectAssets([asset]), @@ -137,7 +128,7 @@ class ImmichAssetGridState extends State { BuildContext context, String title, List assets) { return DailyTitleText( isoDate: title, - multiselectEnabled: _multiselect, + multiselectEnabled: widget.selectionActive, onSelect: () => _selectAssets(assets), onDeselect: () => _deselectAssets(assets), selected: _allAssetsSelected(assets), @@ -227,12 +218,23 @@ class ImmichAssetGridState extends State { ); } + + @override + void didUpdateWidget(ImmichAssetGrid oldWidget) { + super.didUpdateWidget(oldWidget); + if (!widget.selectionActive) { + setState(() { + _selectedAssets.clear(); + }); + } + } + @override Widget build(BuildContext context) { return Stack( children: [ _buildAssetGrid(), - if (_multiselect) _buildMultiSelectIndicator(), + if (widget.selectionActive) _buildMultiSelectIndicator(), ], ); } @@ -244,6 +246,7 @@ class ImmichAssetGrid extends StatefulWidget { final double margin; final bool showStorageIndicator; final ImmichAssetGridSelectionListener? listener; + final bool selectionActive; ImmichAssetGrid({ super.key, @@ -252,6 +255,7 @@ class ImmichAssetGrid extends StatefulWidget { required this.showStorageIndicator, this.listener, this.margin = 5.0, + this.selectionActive = false }); @override diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index 2e38ee08f..d3926f93a 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -48,10 +48,12 @@ class HomePage extends HookConsumerWidget { void onShareAssets() { ref.watch(shareServiceProvider).shareAssets(selection.value.toList()); + multiselectEnabled.value = false; } void onDelete() { ref.watch(assetProvider.notifier).deleteAssets(selection.value); + multiselectEnabled.value = false; } return SafeArea( @@ -82,6 +84,7 @@ class HomePage extends HookConsumerWidget { showStorageIndicator: appSettingService .getSetting(AppSettingsEnum.storageIndicator), listener: selectionListener, + selectionActive: multiselectEnabled.value, ), ), if (multiselectEnabled.value) ...[ From 6ab6507db9c21ed6d36551aae22736f65d8a1f3f Mon Sep 17 00:00:00 2001 From: Matthias Rupp Date: Sat, 8 Oct 2022 13:18:45 +0200 Subject: [PATCH 09/11] Revert changes to albums --- .../album/views/album_viewer_page.dart | 65 +++++++++++++++---- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/mobile/lib/modules/album/views/album_viewer_page.dart b/mobile/lib/modules/album/views/album_viewer_page.dart index 59f66d997..c525c9922 100644 --- a/mobile/lib/modules/album/views/album_viewer_page.dart +++ b/mobile/lib/modules/album/views/album_viewer_page.dart @@ -1,12 +1,9 @@ -import 'dart:math'; - import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; +import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart'; import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart'; @@ -15,10 +12,12 @@ import 'package:immich_mobile/modules/album/services/album.service.dart'; import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart'; import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart'; import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart'; +import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.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/routing/router.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; +import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart'; import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; import 'package:openapi/api.dart'; @@ -29,7 +28,6 @@ class AlbumViewerPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final appSettingService = ref.watch(appSettingsServiceProvider); FocusNode titleFocusNode = useFocusNode(); ScrollController scrollController = useScrollController(); var albumInfo = ref.watch(sharedAlbumDetailProvider(albumId)); @@ -190,17 +188,34 @@ class AlbumViewerPage extends HookConsumerWidget { } Widget _buildImageGrid(AlbumResponseDto albumInfo) { - final assetsPerRow = - appSettingService.getSetting(AppSettingsEnum.tilesPerRow); + final appSettingService = ref.watch(appSettingsServiceProvider); final bool showStorageIndicator = appSettingService.getSetting(AppSettingsEnum.storageIndicator); - final renderList = assetsToRenderList(albumInfo.assets, assetsPerRow); - return ImmichAssetGrid( - assetsPerRow: assetsPerRow, - renderList: renderList, - showStorageIndicator: showStorageIndicator, - ); + if (albumInfo.assets.isNotEmpty) { + return SliverPadding( + padding: const EdgeInsets.only(top: 10.0), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: + appSettingService.getSetting(AppSettingsEnum.tilesPerRow), + crossAxisSpacing: 5.0, + mainAxisSpacing: 5, + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return AlbumViewerThumbnail( + asset: albumInfo.assets[index], + assetList: albumInfo.assets, + showStorageIndicator: showStorageIndicator, + ); + }, + childCount: albumInfo.assetCount, + ), + ), + ); + } + return const SliverToBoxAdapter(); } Widget _buildControlButton(AlbumResponseDto albumInfo) { @@ -233,7 +248,29 @@ class AlbumViewerPage extends HookConsumerWidget { onTap: () { titleFocusNode.unfocus(); }, - child: _buildImageGrid(albumInfo), + child: DraggableScrollbar.semicircle( + backgroundColor: Theme.of(context).hintColor, + controller: scrollController, + heightScrollThumb: 48.0, + child: CustomScrollView( + controller: scrollController, + slivers: [ + _buildHeader(albumInfo), + SliverPersistentHeader( + pinned: true, + delegate: ImmichSliverPersistentAppBarDelegate( + minHeight: 50, + maxHeight: 50, + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: _buildControlButton(albumInfo), + ), + ), + ), + _buildImageGrid(albumInfo) + ], + ), + ), ); } From 2c12f539379bd53a0dc6392575de7bf4ae86ee4f Mon Sep 17 00:00:00 2001 From: Matthias Rupp Date: Fri, 14 Oct 2022 21:17:23 +0200 Subject: [PATCH 10/11] Fix storage indicator settings --- mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart index 74124f76e..3c2bdf3f7 100644 --- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart @@ -100,6 +100,7 @@ class ImmichAssetGridState extends State { onSelect: () => _selectAssets([asset]), onDeselect: () => _deselectAssets([asset]), useGrayBoxPlaceholder: true, + showStorageIndicator: widget.showStorageIndicator, ); } From 293e713af66aa0db52abe9daa82edff2ee23adc6 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Fri, 14 Oct 2022 15:37:15 -0500 Subject: [PATCH 11/11] Hide bottom app bar when multiselect enabled --- .../home/providers/multiselect.provider.dart | 5 ++++ mobile/lib/modules/home/views/home_page.dart | 24 ++++++++++--------- .../lib/shared/views/tab_controller_page.dart | 5 ++-- 3 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 mobile/lib/modules/home/providers/multiselect.provider.dart diff --git a/mobile/lib/modules/home/providers/multiselect.provider.dart b/mobile/lib/modules/home/providers/multiselect.provider.dart new file mode 100644 index 000000000..22210e88c --- /dev/null +++ b/mobile/lib/modules/home/providers/multiselect.provider.dart @@ -0,0 +1,5 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +final multiselectProvider = StateProvider((ref) { + return false; +}); diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index d3926f93a..82045c687 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -2,6 +2,7 @@ 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/providers/home_page_render_list_provider.dart'; +import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart'; import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart'; @@ -21,8 +22,7 @@ class HomePage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final appSettingService = ref.watch(appSettingsServiceProvider); var renderList = ref.watch(renderListProvider); - - final multiselectEnabled = useState(false); + final multiselectEnabled = ref.watch(multiselectProvider.notifier); final selection = useState({}); useEffect( @@ -41,29 +41,31 @@ class HomePage extends HookConsumerWidget { Widget buildBody() { void selectionListener( - bool multiselect, Set selectedAssets) { - multiselectEnabled.value = multiselect; + bool multiselect, + Set selectedAssets, + ) { + multiselectEnabled.state = multiselect; selection.value = selectedAssets; } void onShareAssets() { ref.watch(shareServiceProvider).shareAssets(selection.value.toList()); - multiselectEnabled.value = false; + multiselectEnabled.state = false; } void onDelete() { ref.watch(assetProvider.notifier).deleteAssets(selection.value); - multiselectEnabled.value = false; + multiselectEnabled.state = false; } return SafeArea( - bottom: !multiselectEnabled.value, - top: !multiselectEnabled.value, + bottom: !multiselectEnabled.state, + top: !multiselectEnabled.state, child: Stack( children: [ CustomScrollView( slivers: [ - multiselectEnabled.value + multiselectEnabled.state ? const SliverToBoxAdapter( child: SizedBox( height: 70, @@ -84,10 +86,10 @@ class HomePage extends HookConsumerWidget { showStorageIndicator: appSettingService .getSetting(AppSettingsEnum.storageIndicator), listener: selectionListener, - selectionActive: multiselectEnabled.value, + selectionActive: multiselectEnabled.state, ), ), - if (multiselectEnabled.value) ...[ + if (multiselectEnabled.state) ...[ ControlBottomAppBar( onShare: onShareAssets, onDelete: onDelete, diff --git a/mobile/lib/shared/views/tab_controller_page.dart b/mobile/lib/shared/views/tab_controller_page.dart index 7fe3c2f2f..0b0634ae0 100644 --- a/mobile/lib/shared/views/tab_controller_page.dart +++ b/mobile/lib/shared/views/tab_controller_page.dart @@ -1,8 +1,8 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart'; import 'package:immich_mobile/routing/router.dart'; class TabControllerPage extends ConsumerWidget { @@ -10,6 +10,7 @@ class TabControllerPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final multiselectEnabled = ref.watch(multiselectProvider); return AutoTabsRouter( routes: [ @@ -30,7 +31,7 @@ class TabControllerPage extends ConsumerWidget { opacity: animation, child: child, ), - bottomNavigationBar: false + bottomNavigationBar: multiselectEnabled ? null : BottomNavigationBar( selectedLabelStyle: const TextStyle(