ソースを参照

feat(mobile) integrate new grid system to upstream

Alex 2 年 前
コミット
d43a08eb71
25 ファイル変更933 行追加1128 行削除
  1. 1 4
      mobile/assets/i18n/en-US.json
  2. 0 47
      mobile/lib/modules/home/models/home_page_state.model.dart
  3. 3 84
      mobile/lib/modules/home/providers/home_page_render_list_provider.dart
  4. 0 92
      mobile/lib/modules/home/providers/home_page_state.provider.dart
  5. 5 0
      mobile/lib/modules/home/providers/multiselect.provider.dart
  6. 103 0
      mobile/lib/modules/home/ui/asset_grid/asset_grid_data_structure.dart
  7. 72 0
      mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart
  8. 36 40
      mobile/lib/modules/home/ui/asset_grid/disable_multi_select_button.dart
  9. 0 0
      mobile/lib/modules/home/ui/asset_grid/draggable_scrollbar_custom.dart
  10. 274 0
      mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
  11. 0 0
      mobile/lib/modules/home/ui/asset_grid/monthly_title_text.dart
  12. 172 176
      mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart
  13. 0 107
      mobile/lib/modules/home/ui/asset_list_v2/daily_title_text.dart
  14. 0 167
      mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart
  15. 10 11
      mobile/lib/modules/home/ui/control_bottom_app_bar.dart
  16. 0 109
      mobile/lib/modules/home/ui/daily_title_text.dart
  17. 4 9
      mobile/lib/modules/home/ui/delete_diaglog.dart
  18. 0 47
      mobile/lib/modules/home/ui/image_grid.dart
  19. 45 117
      mobile/lib/modules/home/views/home_page.dart
  20. 12 0
      mobile/lib/modules/search/providers/search_result_page.provider.dart
  21. 14 62
      mobile/lib/modules/search/views/search_result_page.dart
  22. 19 51
      mobile/lib/modules/settings/ui/experimental_settings/experimental_settings.dart
  23. 1 1
      mobile/lib/modules/settings/views/settings_page.dart
  24. 3 4
      mobile/lib/shared/views/tab_controller_page.dart
  25. 159 0
      mobile/test/asset_grid_data_structure_test.dart

+ 1 - 4
mobile/assets/i18n/en-US.json

@@ -171,8 +171,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!"
 }

+ 0 - 47
mobile/lib/modules/home/models/home_page_state.model.dart

@@ -1,47 +0,0 @@
-import 'package:collection/collection.dart';
-
-import 'package:openapi/api.dart';
-
-class HomePageState {
-  final bool isMultiSelectEnable;
-  final Set<AssetResponseDto> selectedItems;
-  final Set<String> selectedDateGroup;
-  HomePageState({
-    required this.isMultiSelectEnable,
-    required this.selectedItems,
-    required this.selectedDateGroup,
-  });
-
-  HomePageState copyWith({
-    bool? isMultiSelectEnable,
-    Set<AssetResponseDto>? selectedItems,
-    Set<String>? 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;
-}

+ 3 - 84
mobile/lib/modules/home/providers/home_page_render_list_provider.dart

@@ -1,95 +1,14 @@
-import 'dart:math';
-
-import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
 import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
-import 'package:openapi/api.dart';
-
-enum RenderAssetGridElementType {
-  assetRow,
-  dayTitle,
-  monthTitle;
-}
-
-class RenderAssetGridRow {
-  final List<AssetResponseDto> assets;
-
-  RenderAssetGridRow(this.assets);
-}
-
-class RenderAssetGridElement {
-  final RenderAssetGridElementType type;
-  final RenderAssetGridRow? assetRow;
-  final String? title;
-  final DateTime date;
-  final List<AssetResponseDto>? relatedAssetList;
-
-  RenderAssetGridElement(
-    this.type, {
-    this.assetRow,
-    this.title,
-    required this.date,
-    this.relatedAssetList,
-  });
-}
 
 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<RenderAssetGridElement> elements = [];
-  DateTime? lastDate;
-
-  assetGroups.forEach((groupName, assets) {
-    try {
-      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;
-    } catch (e) {
-      debugPrint(e.toString());
-    }
-  });
-
-  return elements;
+  return assetGroupsToRenderList(assetGroups, assetsPerRow);
 });

+ 0 - 92
mobile/lib/modules/home/providers/home_page_state.provider.dart

@@ -1,92 +0,0 @@
-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';
-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<HomePageState> {
-
-  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<AssetResponseDto> 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<AssetResponseDto> assets) {
-    state = state.copyWith(selectedItems: {...state.selectedItems, ...assets});
-  }
-
-  void removeSingleSelectedItem(AssetResponseDto asset) {
-    Set<AssetResponseDto> currentList = state.selectedItems;
-
-    currentList.removeWhere((e) => e.id == asset.id);
-
-    state = state.copyWith(selectedItems: currentList);
-  }
-
-  void removeMultipleSelectedItem(List<AssetResponseDto> assets) {
-    Set<AssetResponseDto> currentList = state.selectedItems;
-
-    for (AssetResponseDto asset in assets) {
-      currentList.removeWhere((e) => e.id == asset.id);
-    }
-
-    state = state.copyWith(selectedItems: currentList);
-  }
-
-  void shareAssets(List<AssetResponseDto> 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<HomePageStateNotifier, HomePageState>(
-  ((ref) => HomePageStateNotifier(ref.watch(shareServiceProvider))),
-);

+ 5 - 0
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;
+});

+ 103 - 0
mobile/lib/modules/home/ui/asset_grid/asset_grid_data_structure.dart

@@ -0,0 +1,103 @@
+import 'dart:math';
+
+import 'package:openapi/api.dart';
+
+enum RenderAssetGridElementType {
+  assetRow,
+  dayTitle,
+  monthTitle;
+}
+
+class RenderAssetGridRow {
+  final List<AssetResponseDto> assets;
+
+  RenderAssetGridRow(this.assets);
+}
+
+class RenderAssetGridElement {
+  final RenderAssetGridElementType type;
+  final RenderAssetGridRow? assetRow;
+  final String? title;
+  final DateTime date;
+  final List<AssetResponseDto>? relatedAssetList;
+
+  RenderAssetGridElement(
+    this.type, {
+    this.assetRow,
+    this.title,
+    required this.date,
+    this.relatedAssetList,
+  });
+}
+
+List<RenderAssetGridElement> assetsToRenderList(
+    List<AssetResponseDto> assets, int assetsPerRow) {
+  List<RenderAssetGridElement> 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;
+}
+
+List<RenderAssetGridElement> assetGroupsToRenderList(
+    Map<String, List<AssetResponseDto>> assetGroups, int assetsPerRow) {
+  List<RenderAssetGridElement> 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;
+}

+ 72 - 0
mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart

@@ -0,0 +1,72 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+
+class DailyTitleText extends ConsumerWidget {
+  const DailyTitleText({
+    Key? key,
+    required this.isoDate,
+    required this.multiselectEnabled,
+    required this.onSelect,
+    required this.onDeselect,
+    required this.selected,
+  }) : super(key: key);
+
+  final String isoDate;
+  final bool multiselectEnabled;
+  final Function onSelect;
+  final Function onDeselect;
+  final bool selected;
+
+  @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));
+
+    void handleTitleIconClick() {
+      if (selected) {
+        onDeselect();
+      } else {
+        onSelect();
+      }
+    }
+
+    return 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: multiselectEnabled && selected
+                ? Icon(
+                    Icons.check_circle_rounded,
+                    color: Theme.of(context).primaryColor,
+                  )
+                : const Icon(
+                    Icons.check_circle_outline_rounded,
+                    color: Colors.grey,
+                  ),
+          )
+        ],
+      ),
+    );
+  }
+}

+ 36 - 40
mobile/lib/modules/home/ui/disable_multi_select_button.dart → 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,
+              ),
+            ),
+          ),
+        ),
+    );
+  }
+}

+ 0 - 0
mobile/lib/modules/home/ui/asset_list_v2/draggable_scrollbar_custom.dart → mobile/lib/modules/home/ui/asset_grid/draggable_scrollbar_custom.dart


+ 274 - 0
mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart

@@ -0,0 +1,274 @@
+import 'dart:collection';
+
+import 'package:collection/collection.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.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';
+
+typedef ImmichAssetGridSelectionListener = void Function(
+  bool,
+  Set<AssetResponseDto>,
+);
+
+class ImmichAssetGridState extends State<ImmichAssetGrid> {
+  final ItemScrollController _itemScrollController = ItemScrollController();
+  final ItemPositionsListener _itemPositionsListener =
+      ItemPositionsListener.create();
+
+  bool _scrolling = false;
+  final Set<String> _selectedAssets = HashSet();
+
+  List<AssetResponseDto> get _assets {
+    return widget.renderList
+        .map((e) {
+          if (e.type == RenderAssetGridElementType.assetRow) {
+            return e.assetRow!.assets;
+          } else {
+            return List<AssetResponseDto>.empty();
+          }
+        })
+        .flattened
+        .toList();
+  }
+
+  Set<AssetResponseDto> _getSelectedAssets() {
+    return _selectedAssets
+        .map((e) => _assets.firstWhereOrNull((a) => a.id == e))
+        .whereNotNull()
+        .toSet();
+  }
+
+  void _callSelectionListener(bool selectionActive) {
+    widget.listener?.call(selectionActive, _getSelectedAssets());
+  }
+
+  void _selectAssets(List<AssetResponseDto> assets) {
+    setState(() {
+      for (var e in assets) {
+        _selectedAssets.add(e.id);
+      }
+      _callSelectionListener(true);
+    });
+  }
+
+  void _deselectAssets(List<AssetResponseDto> assets) {
+    setState(() {
+      for (var e in assets) {
+        _selectedAssets.remove(e.id);
+      }
+      _callSelectionListener(_selectedAssets.isNotEmpty);
+    });
+  }
+
+  void _deselectAll() {
+    setState(() {
+      _selectedAssets.clear();
+    });
+
+    _callSelectionListener(false);
+  }
+
+  bool _allAssetsSelected(List<AssetResponseDto> assets) {
+    return widget.selectionActive &&
+        assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
+  }
+
+  double _getItemSize(BuildContext context) {
+    return MediaQuery.of(context).size.width / widget.assetsPerRow -
+        widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
+  }
+
+  Widget _buildThumbnailOrPlaceholder(
+    AssetResponseDto asset,
+    bool placeholder,
+  ) {
+    if (placeholder) {
+      return const DecoratedBox(
+        decoration: BoxDecoration(color: Colors.grey),
+      );
+    }
+    return ThumbnailImage(
+      asset: asset,
+      assetList: _assets,
+      multiselectEnabled: widget.selectionActive,
+      isSelected: _selectedAssets.contains(asset.id),
+      onSelect: () => _selectAssets([asset]),
+      onDeselect: () => _deselectAssets([asset]),
+      useGrayBoxPlaceholder: true,
+      showStorageIndicator: widget.showStorageIndicator,
+    );
+  }
+
+  Widget _buildAssetRow(
+    BuildContext context,
+    RenderAssetGridRow row,
+    bool scrolling,
+  ) {
+    double size = _getItemSize(context);
+
+    return Row(
+      key: Key("asset-row-${row.assets.first.id}"),
+      children: row.assets.map((AssetResponseDto asset) {
+        bool last = asset == row.assets.last;
+
+        return Container(
+          key: Key("asset-${asset.id}"),
+          width: size,
+          height: size,
+          margin: EdgeInsets.only(
+            top: widget.margin,
+            right: last ? 0.0 : widget.margin,
+          ),
+          child: _buildThumbnailOrPlaceholder(asset, scrolling),
+        );
+      }).toList(),
+    );
+  }
+
+  Widget _buildTitle(
+    BuildContext context,
+    String title,
+    List<AssetResponseDto> assets,
+  ) {
+    return DailyTitleText(
+      isoDate: title,
+      multiselectEnabled: widget.selectionActive,
+      onSelect: () => _selectAssets(assets),
+      onDeselect: () => _deselectAssets(assets),
+      selected: _allAssetsSelected(assets),
+    );
+  }
+
+  Widget _buildMonthTitle(BuildContext context, String title) {
+    var monthTitleText = DateFormat("monthly_title_text_date_format".tr())
+        .format(DateTime.parse(title));
+
+    return Padding(
+      key: Key("month-$title"),
+      padding: const EdgeInsets.only(left: 12.0, top: 32),
+      child: Text(
+        monthTitleText,
+        style: TextStyle(
+          fontSize: 26,
+          fontWeight: FontWeight.bold,
+          color: Theme.of(context).textTheme.headline1?.color,
+        ),
+      ),
+    );
+  }
+
+  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 const Text("Invalid widget type!");
+  }
+
+  Text _labelBuilder(int pos) {
+    final date = widget.renderList[pos].date;
+    return Text(
+      DateFormat.yMMMd().format(date),
+      style: const TextStyle(
+        color: Colors.white,
+        fontWeight: FontWeight.bold,
+      ),
+    );
+  }
+
+  Widget _buildMultiSelectIndicator() {
+    return DisableMultiSelectButton(
+      onPressed: () => _deselectAll(),
+      selectedItemCount: _selectedAssets.length,
+    );
+  }
+
+  Widget _buildAssetGrid() {
+    final useDragScrolling = _assets.length >= 20;
+
+    void dragScrolling(bool active) {
+      setState(() {
+        _scrolling = active;
+      });
+    }
+
+    final listWidget = ScrollablePositionedList.builder(
+      itemBuilder: _itemBuilder,
+      itemPositionsListener: _itemPositionsListener,
+      itemScrollController: _itemScrollController,
+      itemCount: widget.renderList.length,
+    );
+
+    if (!useDragScrolling) {
+      return listWidget;
+    }
+
+    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,
+    );
+  }
+
+  @override
+  void didUpdateWidget(ImmichAssetGrid oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (!widget.selectionActive) {
+      setState(() {
+        _selectedAssets.clear();
+      });
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+      children: [
+        _buildAssetGrid(),
+        if (widget.selectionActive) _buildMultiSelectIndicator(),
+      ],
+    );
+  }
+}
+
+class ImmichAssetGrid extends StatefulWidget {
+  final List<RenderAssetGridElement> renderList;
+  final int assetsPerRow;
+  final double margin;
+  final bool showStorageIndicator;
+  final ImmichAssetGridSelectionListener? listener;
+  final bool selectionActive;
+
+  const ImmichAssetGrid({
+    super.key,
+    required this.renderList,
+    required this.assetsPerRow,
+    required this.showStorageIndicator,
+    this.listener,
+    this.margin = 5.0,
+    this.selectionActive = false,
+  });
+
+  @override
+  State<StatefulWidget> createState() {
+    return ImmichAssetGridState();
+  }
+}

+ 0 - 0
mobile/lib/modules/home/ui/monthly_title_text.dart → mobile/lib/modules/home/ui/asset_grid/monthly_title_text.dart


+ 172 - 176
mobile/lib/modules/home/ui/thumbnail_image.dart → mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart

@@ -1,176 +1,172 @@
-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<AssetResponseDto> 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: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/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<AssetResponseDto> 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,
+                    ),
+                  ],
+                ),
+              ),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 0 - 107
mobile/lib/modules/home/ui/asset_list_v2/daily_title_text.dart

@@ -1,107 +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<AssetResponseDto> 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));
-    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 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,
-                  ),
-          )
-        ],
-      ),
-    );
-  }
-}

+ 0 - 167
mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart

@@ -1,167 +0,0 @@
-import 'dart:math';
-
-import 'package:collection/collection.dart';
-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/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';
-
-class ImmichAssetGrid extends HookConsumerWidget {
-  final ItemScrollController _itemScrollController = ItemScrollController();
-  final ItemPositionsListener _itemPositionsListener =
-      ItemPositionsListener.create();
-
-  final List<RenderAssetGridElement> 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,
-  });
-
-  List<AssetResponseDto> get _assets {
-    return renderList
-        .map((e) {
-          if (e.type == RenderAssetGridElementType.assetRow) {
-            return e.assetRow!.assets;
-          } else {
-            return List<AssetResponseDto>.empty();
-          }
-        })
-        .flattened
-        .toList();
-  }
-
-  double _getItemSize(BuildContext context) {
-    return MediaQuery.of(context).size.width / assetsPerRow -
-        margin * (assetsPerRow - 1) / assetsPerRow;
-  }
-
-  Widget _buildThumbnailOrPlaceholder(
-      AssetResponseDto asset, bool placeholder) {
-    if (placeholder) {
-      return const DecoratedBox(
-        decoration: BoxDecoration(color: Colors.grey),
-      );
-    }
-    return ThumbnailImage(
-      asset: asset,
-      assetList: _assets,
-      showStorageIndicator: showStorageIndicator,
-      useGrayBoxPlaceholder: true,
-    );
-  }
-
-  Widget _buildAssetRow(
-      BuildContext context, RenderAssetGridRow row, bool scrolling) {
-    double size = _getItemSize(context);
-
-    return Row(
-      key: Key("asset-row-${row.assets.first.id}"),
-      children: row.assets.map((AssetResponseDto asset) {
-        bool last = asset == row.assets.last;
-
-        return Container(
-          key: Key("asset-${asset.id}"),
-          width: size,
-          height: size,
-          margin: EdgeInsets.only(top: margin, right: last ? 0.0 : margin),
-          child: _buildThumbnailOrPlaceholder(asset, scrolling),
-        );
-      }).toList(),
-    );
-  }
-
-  Widget _buildTitle(
-      BuildContext context, String title, List<AssetResponseDto> assets) {
-    return DailyTitleText(
-      isoDate: title,
-      assetGroup: assets,
-    );
-  }
-
-  Widget _buildMonthTitle(BuildContext context, String title) {
-    var monthTitleText = DateFormat("monthly_title_text_date_format".tr())
-        .format(DateTime.parse(title));
-
-    return Padding(
-      key: Key("month-$title"),
-      padding: const EdgeInsets.only(left: 12.0, top: 32),
-      child: Text(
-        monthTitleText,
-        style: TextStyle(
-          fontSize: 26,
-          fontWeight: FontWeight.bold,
-          color: Theme.of(context).textTheme.headline1?.color,
-        ),
-      ),
-    );
-  }
-
-  Widget _itemBuilder(BuildContext c, int position, bool scrolling) {
-    final item = 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 const Text("Invalid widget type!");
-  }
-
-  Text _labelBuilder(int pos) {
-    final date = renderList[pos].date;
-    return Text(DateFormat.yMMMd().format(date),
-      style: const TextStyle(
-        color: Colors.white,
-        fontWeight: FontWeight.bold,
-      ),
-    );
-  }
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    final scrolling = useState(false);
-
-    void dragScrolling(bool active) {
-      scrolling.value = active;
-    }
-
-    Widget itemBuilder(BuildContext c, int position) {
-      return _itemBuilder(c, position, scrolling.value);
-    }
-
-    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: ScrollablePositionedList.builder(
-          itemBuilder: itemBuilder,
-          itemPositionsListener: _itemPositionsListener,
-          itemScrollController: _itemScrollController,
-          itemCount: renderList.length,
-        ));
-  }
-}

+ 10 - 11
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();
                     },
                   ),
                 ],

+ 0 - 109
mobile/lib/modules/home/ui/daily_title_text.dart

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

+ 4 - 9
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(

+ 0 - 47
mobile/lib/modules/home/ui/image_grid.dart

@@ -1,47 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
-import 'package:openapi/api.dart';
-
-// ignore: must_be_immutable
-class ImageGrid extends ConsumerWidget {
-  final List<AssetResponseDto> assetGroup;
-  final List<AssetResponseDto> 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<AssetResponseDto> 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,
-      ),
-    );
-  }
-}

+ 45 - 117
mobile/lib/modules/home/views/home_page.dart

@@ -2,22 +2,17 @@ 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/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/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:immich_mobile/shared/services/share.service.dart';
 import 'package:openapi/api.dart';
 
 class HomePage extends HookConsumerWidget {
@@ -26,22 +21,9 @@ class HomePage extends HookConsumerWidget {
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     final appSettingService = ref.watch(appSettingsServiceProvider);
-
     var renderList = ref.watch(renderListProvider);
-
-    ScrollController scrollController = useScrollController();
-    var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
-    List<Widget> imageGridGroup = [];
-    var isMultiSelectEnable =
-        ref.watch(homePageStateProvider).isMultiSelectEnable;
-    var homePageState = ref.watch(homePageStateProvider);
-    List<AssetResponseDto> sortedAssetList = [];
-    // set sorted List
-    for (var group in assetGroupByDateTime.values) {
-      for (var value in group) {
-        sortedAssetList.add(value);
-      }
-    }
+    final multiselectEnabled = ref.watch(multiselectProvider.notifier);
+    final selection = useState(<AssetResponseDto>{});
 
     useEffect(
       () {
@@ -57,115 +39,61 @@ class HomePage extends HookConsumerWidget {
       ref.read(assetProvider.notifier).getAllAsset();
     }
 
-    _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()}",
-            );
-          }
-        });
+    Widget buildBody() {
+      void selectionListener(
+        bool multiselect,
+        Set<AssetResponseDto> selectedAssets,
+      ) {
+        multiselectEnabled.state = multiselect;
+        selection.value = selectedAssets;
       }
 
-      _buildSliverAppBar() {
-        return isMultiSelectEnable
-            ? const SliverToBoxAdapter(
-                child: SizedBox(
-                  height: 70,
-                  child: null,
-                ),
-              )
-            : ImmichSliverAppBar(
-                onPopBack: reloadAllAsset,
-              );
+      void onShareAssets() {
+        ref.watch(shareServiceProvider).shareAssets(selection.value.toList());
+        multiselectEnabled.state = false;
       }
 
-      _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,
-              ],
-            ),
-          );
-        }
+      void onDelete() {
+        ref.watch(assetProvider.notifier).deleteAssets(selection.value);
+        multiselectEnabled.state = false;
       }
 
       return SafeArea(
-        bottom: !isMultiSelectEnable,
-        top: !isMultiSelectEnable,
+        bottom: !multiselectEnabled.state,
+        top: !multiselectEnabled.state,
         child: Stack(
           children: [
             CustomScrollView(
               slivers: [
-                _buildSliverAppBar(),
+                multiselectEnabled.state
+                    ? const SliverToBoxAdapter(
+                        child: SizedBox(
+                          height: 70,
+                          child: null,
+                        ),
+                      )
+                    : ImmichSliverAppBar(
+                        onPopBack: reloadAllAsset,
+                      ),
               ],
             ),
             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),
+                listener: selectionListener,
+                selectionActive: multiselectEnabled.state,
+              ),
             ),
-            if (isMultiSelectEnable) ...[
-              _buildSelectedItemCountIndicator(),
-              const ControlBottomAppBar(),
+            if (multiselectEnabled.state) ...[
+              ControlBottomAppBar(
+                onShare: onShareAssets,
+                onDelete: onDelete,
+              ),
             ],
           ],
         ),
@@ -174,7 +102,7 @@ class HomePage extends HookConsumerWidget {
 
     return Scaffold(
       drawer: const ProfileDrawer(),
-      body: _buildBody(),
+      body: buildBody(),
     );
   }
 }

+ 12 - 0
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_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';
+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);
+});

+ 14 - 62
mobile/lib/modules/search/views/search_result_page.dart

@@ -4,14 +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_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:openapi/api.dart';
+import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
+import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 
 class SearchResultPage extends HookConsumerWidget {
   const SearchResultPage({Key? key, required this.searchTerm})
@@ -21,17 +19,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<Widget> imageGridGroup = [];
-
     FocusNode? searchFocusNode;
 
-    List<AssetResponseDto> sortedAssetList = [];
-
     useEffect(
       () {
         searchFocusNode = FocusNode();
@@ -117,7 +110,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 +130,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();

+ 19 - 51
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,
+        // ),
       ],
     );
   }

+ 1 - 1
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(),
         ],

+ 3 - 4
mobile/lib/shared/views/tab_controller_page.dart

@@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
 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/providers/multiselect.provider.dart';
 import 'package:immich_mobile/routing/router.dart';
 
 class TabControllerPage extends ConsumerWidget {
@@ -10,8 +10,7 @@ class TabControllerPage extends ConsumerWidget {
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    var isMultiSelectEnable =
-        ref.watch(homePageStateProvider).isMultiSelectEnable;
+    final multiselectEnabled = ref.watch(multiselectProvider);
 
     return AutoTabsRouter(
       routes: [
@@ -32,7 +31,7 @@ class TabControllerPage extends ConsumerWidget {
               opacity: animation,
               child: child,
             ),
-            bottomNavigationBar: isMultiSelectEnable
+            bottomNavigationBar: multiselectEnabled
                 ? null
                 : BottomNavigationBar(
                     selectedLabelStyle: const TextStyle(

+ 159 - 0
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_grid/asset_grid_data_structure.dart';
+import 'package:openapi/api.dart';
+
+void main() {
+  final List<AssetResponseDto> 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<String, List<AssetResponseDto>> 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]);
+      }
+    });
+  });
+}