mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	Merge branch 'main' of github.com:immich-app/immich into dev/mobile-cosmetic-improvement
This commit is contained in:
		@@ -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_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
 | 
				
			||||||
  "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
 | 
					  "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
 | 
				
			||||||
  "experimental_settings_title": "Experimental",
 | 
					  "experimental_settings_title": "Experimental",
 | 
				
			||||||
  "experimental_settings_subtitle": "Use at your own risk!",
 | 
					  "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"
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,95 +1,14 @@
 | 
				
			|||||||
import 'dart:math';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 | 
					import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 | 
					import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/asset.provider.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) {
 | 
					final renderListProvider = StateProvider((ref) {
 | 
				
			||||||
  var assetGroups = ref.watch(assetGroupByDateTimeProvider);
 | 
					  var assetGroups = ref.watch(assetGroupByDateTimeProvider);
 | 
				
			||||||
  var settings = ref.watch(appSettingsServiceProvider);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  var settings = ref.watch(appSettingsServiceProvider);
 | 
				
			||||||
  final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
 | 
					  final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  List<RenderAssetGridElement> elements = [];
 | 
					  return assetGroupsToRenderList(assetGroups, assetsPerRow);
 | 
				
			||||||
  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;
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,90 +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<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))),
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final multiselectProvider = StateProvider((ref) {
 | 
				
			||||||
 | 
					  return false;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@@ -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
									
								
								mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,40 +1,36 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DisableMultiSelectButton extends ConsumerWidget {
 | 
					class DisableMultiSelectButton extends ConsumerWidget {
 | 
				
			||||||
  const DisableMultiSelectButton({
 | 
					  const DisableMultiSelectButton({
 | 
				
			||||||
    Key? key,
 | 
					    Key? key,
 | 
				
			||||||
    required this.onPressed,
 | 
					    required this.onPressed,
 | 
				
			||||||
    required this.selectedItemCount,
 | 
					    required this.selectedItemCount,
 | 
				
			||||||
  }) : super(key: key);
 | 
					  }) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final Function onPressed;
 | 
					  final Function onPressed;
 | 
				
			||||||
  final int selectedItemCount;
 | 
					  final int selectedItemCount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    return Positioned(
 | 
					    return Padding(
 | 
				
			||||||
      top: 10,
 | 
					        padding: const EdgeInsets.only(left: 16.0, top: 15),
 | 
				
			||||||
      left: 0,
 | 
					        child: Padding(
 | 
				
			||||||
      child: Padding(
 | 
					          padding: const EdgeInsets.symmetric(horizontal: 4.0),
 | 
				
			||||||
        padding: const EdgeInsets.only(left: 16.0, top: 46),
 | 
					          child: ElevatedButton.icon(
 | 
				
			||||||
        child: Padding(
 | 
					            onPressed: () {
 | 
				
			||||||
          padding: const EdgeInsets.symmetric(horizontal: 4.0),
 | 
					              onPressed();
 | 
				
			||||||
          child: ElevatedButton.icon(
 | 
					            },
 | 
				
			||||||
            onPressed: () {
 | 
					            icon: const Icon(Icons.close_rounded),
 | 
				
			||||||
              onPressed();
 | 
					            label: Text(
 | 
				
			||||||
            },
 | 
					              '$selectedItemCount',
 | 
				
			||||||
            icon: const Icon(Icons.close_rounded),
 | 
					              style: const TextStyle(
 | 
				
			||||||
            label: Text(
 | 
					                fontWeight: FontWeight.w600,
 | 
				
			||||||
              '$selectedItemCount',
 | 
					                fontSize: 18,
 | 
				
			||||||
              style: const TextStyle(
 | 
					              ),
 | 
				
			||||||
                fontWeight: FontWeight.w600,
 | 
					            ),
 | 
				
			||||||
                fontSize: 18,
 | 
					          ),
 | 
				
			||||||
              ),
 | 
					        ),
 | 
				
			||||||
            ),
 | 
					    );
 | 
				
			||||||
          ),
 | 
					  }
 | 
				
			||||||
        ),
 | 
					}
 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										274
									
								
								mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										274
									
								
								mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,176 +1,172 @@
 | 
				
			|||||||
import 'package:auto_route/auto_route.dart';
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
					import 'package:cached_network_image/cached_network_image.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
import 'package:hive_flutter/hive_flutter.dart';
 | 
					import 'package:hive_flutter/hive_flutter.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:immich_mobile/constants/hive_box.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/modules/login/providers/authentication.provider.dart';
 | 
					import 'package:immich_mobile/routing/router.dart';
 | 
				
			||||||
import 'package:immich_mobile/routing/router.dart';
 | 
					import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
				
			||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					
 | 
				
			||||||
 | 
					class ThumbnailImage extends HookConsumerWidget {
 | 
				
			||||||
class ThumbnailImage extends HookConsumerWidget {
 | 
					  final AssetResponseDto asset;
 | 
				
			||||||
  final AssetResponseDto asset;
 | 
					  final List<AssetResponseDto> assetList;
 | 
				
			||||||
  final List<AssetResponseDto> assetList;
 | 
					  final bool showStorageIndicator;
 | 
				
			||||||
  final bool showStorageIndicator;
 | 
					  final bool useGrayBoxPlaceholder;
 | 
				
			||||||
  final bool useGrayBoxPlaceholder;
 | 
					  final bool isSelected;
 | 
				
			||||||
 | 
					  final bool multiselectEnabled;
 | 
				
			||||||
  const ThumbnailImage({
 | 
					  final Function? onSelect;
 | 
				
			||||||
    Key? key,
 | 
					  final Function? onDeselect;
 | 
				
			||||||
    required this.asset,
 | 
					
 | 
				
			||||||
    required this.assetList,
 | 
					  const ThumbnailImage({
 | 
				
			||||||
    this.showStorageIndicator = true,
 | 
					    Key? key,
 | 
				
			||||||
    this.useGrayBoxPlaceholder = false,
 | 
					    required this.asset,
 | 
				
			||||||
  }) : super(key: key);
 | 
					    required this.assetList,
 | 
				
			||||||
 | 
					    this.showStorageIndicator = true,
 | 
				
			||||||
  @override
 | 
					    this.useGrayBoxPlaceholder = false,
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					    this.isSelected = false,
 | 
				
			||||||
    var box = Hive.box(userInfoBox);
 | 
					    this.multiselectEnabled = false,
 | 
				
			||||||
    var thumbnailRequestUrl = getThumbnailUrl(asset);
 | 
					    this.onDeselect,
 | 
				
			||||||
    var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
 | 
					    this.onSelect,
 | 
				
			||||||
    var isMultiSelectEnable =
 | 
					  }) : super(key: key);
 | 
				
			||||||
        ref.watch(homePageStateProvider).isMultiSelectEnable;
 | 
					
 | 
				
			||||||
    var deviceId = ref.watch(authenticationProvider).deviceId;
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    Widget buildSelectionIcon(AssetResponseDto asset) {
 | 
					    var box = Hive.box(userInfoBox);
 | 
				
			||||||
      if (selectedAsset.contains(asset)) {
 | 
					    var thumbnailRequestUrl = getThumbnailUrl(asset);
 | 
				
			||||||
        return Icon(
 | 
					    var deviceId = ref.watch(authenticationProvider).deviceId;
 | 
				
			||||||
          Icons.check_circle,
 | 
					
 | 
				
			||||||
          color: Theme.of(context).primaryColor,
 | 
					
 | 
				
			||||||
        );
 | 
					    Widget buildSelectionIcon(AssetResponseDto asset) {
 | 
				
			||||||
      } else {
 | 
					      if (isSelected) {
 | 
				
			||||||
        return const Icon(
 | 
					        return Icon(
 | 
				
			||||||
          Icons.circle_outlined,
 | 
					          Icons.check_circle,
 | 
				
			||||||
          color: Colors.white,
 | 
					          color: Theme.of(context).primaryColor,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      }
 | 
					      } else {
 | 
				
			||||||
    }
 | 
					        return const Icon(
 | 
				
			||||||
 | 
					          Icons.circle_outlined,
 | 
				
			||||||
    return GestureDetector(
 | 
					          color: Colors.white,
 | 
				
			||||||
      onTap: () {
 | 
					        );
 | 
				
			||||||
        if (isMultiSelectEnable &&
 | 
					      }
 | 
				
			||||||
            selectedAsset.contains(asset) &&
 | 
					    }
 | 
				
			||||||
            selectedAsset.length == 1) {
 | 
					
 | 
				
			||||||
          ref.watch(homePageStateProvider.notifier).disableMultiSelect();
 | 
					    return GestureDetector(
 | 
				
			||||||
        } else if (isMultiSelectEnable &&
 | 
					      onTap: () {
 | 
				
			||||||
            selectedAsset.contains(asset) &&
 | 
					        if (multiselectEnabled) {
 | 
				
			||||||
            selectedAsset.length > 1) {
 | 
					          if (isSelected) {
 | 
				
			||||||
          ref
 | 
					            onDeselect?.call();
 | 
				
			||||||
              .watch(homePageStateProvider.notifier)
 | 
					          } else {
 | 
				
			||||||
              .removeSingleSelectedItem(asset);
 | 
					            onSelect?.call();
 | 
				
			||||||
        } else if (isMultiSelectEnable && !selectedAsset.contains(asset)) {
 | 
					          }
 | 
				
			||||||
          ref
 | 
					        } else {
 | 
				
			||||||
              .watch(homePageStateProvider.notifier)
 | 
					          AutoRouter.of(context).push(
 | 
				
			||||||
              .addSingleSelectedItem(asset);
 | 
					            GalleryViewerRoute(
 | 
				
			||||||
        } else {
 | 
					              assetList: assetList,
 | 
				
			||||||
          AutoRouter.of(context).push(
 | 
					              asset: asset,
 | 
				
			||||||
            GalleryViewerRoute(
 | 
					            ),
 | 
				
			||||||
              assetList: assetList,
 | 
					          );
 | 
				
			||||||
              asset: asset,
 | 
					        }
 | 
				
			||||||
            ),
 | 
					      },
 | 
				
			||||||
          );
 | 
					      onLongPress: () {
 | 
				
			||||||
        }
 | 
					        onSelect?.call();
 | 
				
			||||||
      },
 | 
					        HapticFeedback.heavyImpact();
 | 
				
			||||||
      onLongPress: () {
 | 
					      },
 | 
				
			||||||
        // Enable multi select function
 | 
					      child: Hero(
 | 
				
			||||||
        ref.watch(homePageStateProvider.notifier).enableMultiSelect({asset});
 | 
					        tag: asset.id,
 | 
				
			||||||
        HapticFeedback.heavyImpact();
 | 
					        child: Stack(
 | 
				
			||||||
      },
 | 
					          children: [
 | 
				
			||||||
      child: Hero(
 | 
					            Container(
 | 
				
			||||||
        tag: asset.id,
 | 
					              decoration: BoxDecoration(
 | 
				
			||||||
        child: Stack(
 | 
					                border: multiselectEnabled && isSelected
 | 
				
			||||||
          children: [
 | 
					                    ? Border.all(
 | 
				
			||||||
            Container(
 | 
					                        color: Theme.of(context).primaryColorLight,
 | 
				
			||||||
              decoration: BoxDecoration(
 | 
					                        width: 10,
 | 
				
			||||||
                border: isMultiSelectEnable && selectedAsset.contains(asset)
 | 
					                      )
 | 
				
			||||||
                    ? Border.all(
 | 
					                    : const Border(),
 | 
				
			||||||
                        color: Theme.of(context).primaryColorLight,
 | 
					              ),
 | 
				
			||||||
                        width: 10,
 | 
					              child: CachedNetworkImage(
 | 
				
			||||||
                      )
 | 
					                cacheKey: 'thumbnail-image-${asset.id}',
 | 
				
			||||||
                    : const Border(),
 | 
					                width: 300,
 | 
				
			||||||
              ),
 | 
					                height: 300,
 | 
				
			||||||
              child: CachedNetworkImage(
 | 
					                memCacheHeight: 200,
 | 
				
			||||||
                cacheKey: 'thumbnail-image-${asset.id}',
 | 
					                maxWidthDiskCache: 200,
 | 
				
			||||||
                width: 300,
 | 
					                maxHeightDiskCache: 200,
 | 
				
			||||||
                height: 300,
 | 
					                fit: BoxFit.cover,
 | 
				
			||||||
                memCacheHeight: 200,
 | 
					                imageUrl: thumbnailRequestUrl,
 | 
				
			||||||
                maxWidthDiskCache: 200,
 | 
					                httpHeaders: {
 | 
				
			||||||
                maxHeightDiskCache: 200,
 | 
					                  "Authorization": "Bearer ${box.get(accessTokenKey)}"
 | 
				
			||||||
                fit: BoxFit.cover,
 | 
					                },
 | 
				
			||||||
                imageUrl: thumbnailRequestUrl,
 | 
					                fadeInDuration: const Duration(milliseconds: 250),
 | 
				
			||||||
                httpHeaders: {
 | 
					                progressIndicatorBuilder: (context, url, downloadProgress) {
 | 
				
			||||||
                  "Authorization": "Bearer ${box.get(accessTokenKey)}"
 | 
					                  if (useGrayBoxPlaceholder) {
 | 
				
			||||||
                },
 | 
					                    return const DecoratedBox(
 | 
				
			||||||
                fadeInDuration: const Duration(milliseconds: 250),
 | 
					                      decoration: BoxDecoration(color: Colors.grey),
 | 
				
			||||||
                progressIndicatorBuilder: (context, url, downloadProgress) {
 | 
					                    );
 | 
				
			||||||
                  if (useGrayBoxPlaceholder) {
 | 
					                  }
 | 
				
			||||||
                    return const DecoratedBox(
 | 
					                  return Transform.scale(
 | 
				
			||||||
                      decoration: BoxDecoration(color: Colors.grey),
 | 
					                    scale: 0.2,
 | 
				
			||||||
                    );
 | 
					                    child: CircularProgressIndicator(
 | 
				
			||||||
                  }
 | 
					                      value: downloadProgress.progress,
 | 
				
			||||||
                  return Transform.scale(
 | 
					                    ),
 | 
				
			||||||
                    scale: 0.2,
 | 
					                  );
 | 
				
			||||||
                    child: CircularProgressIndicator(
 | 
					                },
 | 
				
			||||||
                      value: downloadProgress.progress,
 | 
					                errorWidget: (context, url, error) {
 | 
				
			||||||
                    ),
 | 
					                  debugPrint("Error getting thumbnail $url = $error");
 | 
				
			||||||
                  );
 | 
					                  CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
 | 
				
			||||||
                },
 | 
					
 | 
				
			||||||
                errorWidget: (context, url, error) {
 | 
					                  return Icon(
 | 
				
			||||||
                  debugPrint("Error getting thumbnail $url = $error");
 | 
					                    Icons.image_not_supported_outlined,
 | 
				
			||||||
                  CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
 | 
					                    color: Theme.of(context).primaryColor,
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
                  return Icon(
 | 
					                },
 | 
				
			||||||
                    Icons.image_not_supported_outlined,
 | 
					              ),
 | 
				
			||||||
                    color: Theme.of(context).primaryColor,
 | 
					            ),
 | 
				
			||||||
                  );
 | 
					            if (multiselectEnabled)
 | 
				
			||||||
                },
 | 
					              Padding(
 | 
				
			||||||
              ),
 | 
					                padding: const EdgeInsets.all(3.0),
 | 
				
			||||||
            ),
 | 
					                child: Align(
 | 
				
			||||||
            if (isMultiSelectEnable)
 | 
					                  alignment: Alignment.topLeft,
 | 
				
			||||||
              Padding(
 | 
					                  child: buildSelectionIcon(asset),
 | 
				
			||||||
                padding: const EdgeInsets.all(3.0),
 | 
					                ),
 | 
				
			||||||
                child: Align(
 | 
					              ),
 | 
				
			||||||
                  alignment: Alignment.topLeft,
 | 
					            if (showStorageIndicator)
 | 
				
			||||||
                  child: buildSelectionIcon(asset),
 | 
					              Positioned(
 | 
				
			||||||
                ),
 | 
					                right: 10,
 | 
				
			||||||
              ),
 | 
					                bottom: 5,
 | 
				
			||||||
            if (showStorageIndicator)
 | 
					                child: Icon(
 | 
				
			||||||
              Positioned(
 | 
					                  (deviceId != asset.deviceId)
 | 
				
			||||||
                right: 10,
 | 
					                      ? Icons.cloud_done_outlined
 | 
				
			||||||
                bottom: 5,
 | 
					                      : Icons.photo_library_rounded,
 | 
				
			||||||
                child: Icon(
 | 
					                  color: Colors.white,
 | 
				
			||||||
                  (deviceId != asset.deviceId)
 | 
					                  size: 18,
 | 
				
			||||||
                      ? Icons.cloud_done_outlined
 | 
					                ),
 | 
				
			||||||
                      : Icons.photo_library_rounded,
 | 
					              ),
 | 
				
			||||||
                  color: Colors.white,
 | 
					            if (asset.type != AssetTypeEnum.IMAGE)
 | 
				
			||||||
                  size: 18,
 | 
					              Positioned(
 | 
				
			||||||
                ),
 | 
					                top: 5,
 | 
				
			||||||
              ),
 | 
					                right: 5,
 | 
				
			||||||
            if (asset.type != AssetTypeEnum.IMAGE)
 | 
					                child: Row(
 | 
				
			||||||
              Positioned(
 | 
					                  children: [
 | 
				
			||||||
                top: 5,
 | 
					                    Text(
 | 
				
			||||||
                right: 5,
 | 
					                      asset.duration.toString().substring(0, 7),
 | 
				
			||||||
                child: Row(
 | 
					                      style: const TextStyle(
 | 
				
			||||||
                  children: [
 | 
					                        color: Colors.white,
 | 
				
			||||||
                    Text(
 | 
					                        fontSize: 10,
 | 
				
			||||||
                      asset.duration.toString().substring(0, 7),
 | 
					                      ),
 | 
				
			||||||
                      style: const TextStyle(
 | 
					                    ),
 | 
				
			||||||
                        color: Colors.white,
 | 
					                    const Icon(
 | 
				
			||||||
                        fontSize: 10,
 | 
					                      Icons.play_circle_outline_rounded,
 | 
				
			||||||
                      ),
 | 
					                      color: Colors.white,
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    const Icon(
 | 
					                  ],
 | 
				
			||||||
                      Icons.play_circle_outline_rounded,
 | 
					                ),
 | 
				
			||||||
                      color: Colors.white,
 | 
					              ),
 | 
				
			||||||
                    ),
 | 
					          ],
 | 
				
			||||||
                  ],
 | 
					        ),
 | 
				
			||||||
                ),
 | 
					      ),
 | 
				
			||||||
              ),
 | 
					    );
 | 
				
			||||||
          ],
 | 
					  }
 | 
				
			||||||
        ),
 | 
					}
 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -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,
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
          )
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,173 +0,0 @@
 | 
				
			|||||||
import 'package:collection/collection.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_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,
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,11 +1,15 @@
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
 | 
					import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ControlBottomAppBar extends ConsumerWidget {
 | 
					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
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
@@ -36,7 +40,9 @@ class ControlBottomAppBar extends ConsumerWidget {
 | 
				
			|||||||
                      showDialog(
 | 
					                      showDialog(
 | 
				
			||||||
                        context: context,
 | 
					                        context: context,
 | 
				
			||||||
                        builder: (BuildContext context) {
 | 
					                        builder: (BuildContext context) {
 | 
				
			||||||
                          return const DeleteDialog();
 | 
					                          return DeleteDialog(
 | 
				
			||||||
 | 
					                            onDelete: onDelete,
 | 
				
			||||||
 | 
					                          );
 | 
				
			||||||
                        },
 | 
					                        },
 | 
				
			||||||
                      );
 | 
					                      );
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
@@ -45,14 +51,7 @@ class ControlBottomAppBar extends ConsumerWidget {
 | 
				
			|||||||
                    iconData: Icons.share,
 | 
					                    iconData: Icons.share,
 | 
				
			||||||
                    label: "control_bottom_app_bar_share".tr(),
 | 
					                    label: "control_bottom_app_bar_share".tr(),
 | 
				
			||||||
                    onPressed: () {
 | 
					                    onPressed: () {
 | 
				
			||||||
                      final homePageState = ref.watch(homePageStateProvider);
 | 
					                      onShare();
 | 
				
			||||||
                      ref.watch(homePageStateProvider.notifier).shareAssets(
 | 
					 | 
				
			||||||
                            homePageState.selectedItems.toList(),
 | 
					 | 
				
			||||||
                            context,
 | 
					 | 
				
			||||||
                          );
 | 
					 | 
				
			||||||
                      ref
 | 
					 | 
				
			||||||
                          .watch(homePageStateProvider.notifier)
 | 
					 | 
				
			||||||
                          .disableMultiSelect();
 | 
					 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
          ],
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,15 +1,14 @@
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DeleteDialog extends ConsumerWidget {
 | 
					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
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    final homePageState = ref.watch(homePageStateProvider);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AlertDialog(
 | 
					    return AlertDialog(
 | 
				
			||||||
      // backgroundColor: Colors.grey[200],
 | 
					      // backgroundColor: Colors.grey[200],
 | 
				
			||||||
@@ -31,11 +30,7 @@ class DeleteDialog extends ConsumerWidget {
 | 
				
			|||||||
        ),
 | 
					        ),
 | 
				
			||||||
        TextButton(
 | 
					        TextButton(
 | 
				
			||||||
          onPressed: () {
 | 
					          onPressed: () {
 | 
				
			||||||
            ref
 | 
					            onDelete();
 | 
				
			||||||
                .watch(assetProvider.notifier)
 | 
					 | 
				
			||||||
                .deleteAssets(homePageState.selectedItems);
 | 
					 | 
				
			||||||
            ref.watch(homePageStateProvider.notifier).disableMultiSelect();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Navigator.of(context).pop();
 | 
					            Navigator.of(context).pop();
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          child: Text(
 | 
					          child: Text(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,46 +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) {
 | 
					 | 
				
			||||||
          return GestureDetector(
 | 
					 | 
				
			||||||
            onTap: () {},
 | 
					 | 
				
			||||||
            child: ThumbnailImage(
 | 
					 | 
				
			||||||
              asset: assetGroup[index],
 | 
					 | 
				
			||||||
              assetList: sortedAssetGroup,
 | 
					 | 
				
			||||||
              showStorageIndicator: showStorageIndicator,
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        childCount: assetGroup.length,
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -2,22 +2,17 @@ import 'package:flutter/material.dart';
 | 
				
			|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/providers/home_page_render_list_provider.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/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/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/home/ui/profile_drawer/profile_drawer.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 | 
					import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 | 
					import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:immich_mobile/shared/providers/asset.provider.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/server_info.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/websocket.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';
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class HomePage extends HookConsumerWidget {
 | 
					class HomePage extends HookConsumerWidget {
 | 
				
			||||||
@@ -26,22 +21,9 @@ class HomePage extends HookConsumerWidget {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    final appSettingService = ref.watch(appSettingsServiceProvider);
 | 
					    final appSettingService = ref.watch(appSettingsServiceProvider);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    var renderList = ref.watch(renderListProvider);
 | 
					    var renderList = ref.watch(renderListProvider);
 | 
				
			||||||
 | 
					    final multiselectEnabled = ref.watch(multiselectProvider.notifier);
 | 
				
			||||||
    ScrollController scrollController = useScrollController();
 | 
					    final selection = useState(<AssetResponseDto>{});
 | 
				
			||||||
    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);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(
 | 
					    useEffect(
 | 
				
			||||||
      () {
 | 
					      () {
 | 
				
			||||||
@@ -57,115 +39,61 @@ class HomePage extends HookConsumerWidget {
 | 
				
			|||||||
      ref.read(assetProvider.notifier).getAllAsset();
 | 
					      ref.read(assetProvider.notifier).getAllAsset();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _buildSelectedItemCountIndicator() {
 | 
					    Widget buildBody() {
 | 
				
			||||||
      return DisableMultiSelectButton(
 | 
					      void selectionListener(
 | 
				
			||||||
        onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect,
 | 
					        bool multiselect,
 | 
				
			||||||
        selectedItemCount: homePageState.selectedItems.length,
 | 
					        Set<AssetResponseDto> selectedAssets,
 | 
				
			||||||
      );
 | 
					      ) {
 | 
				
			||||||
    }
 | 
					        multiselectEnabled.state = multiselect;
 | 
				
			||||||
 | 
					        selection.value = selectedAssets;
 | 
				
			||||||
    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() {
 | 
					      void onShareAssets() {
 | 
				
			||||||
        return isMultiSelectEnable
 | 
					        ref.watch(shareServiceProvider).shareAssets(selection.value.toList());
 | 
				
			||||||
            ? const SliverToBoxAdapter(
 | 
					        multiselectEnabled.state = false;
 | 
				
			||||||
                child: SizedBox(
 | 
					 | 
				
			||||||
                  height: 70,
 | 
					 | 
				
			||||||
                  child: null,
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              )
 | 
					 | 
				
			||||||
            : ImmichSliverAppBar(
 | 
					 | 
				
			||||||
                onPopBack: reloadAllAsset,
 | 
					 | 
				
			||||||
              );
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      _buildAssetGrid() {
 | 
					      void onDelete() {
 | 
				
			||||||
        if (appSettingService
 | 
					        ref.watch(assetProvider.notifier).deleteAssets(selection.value);
 | 
				
			||||||
            .getSetting(AppSettingsEnum.useExperimentalAssetGrid)) {
 | 
					        multiselectEnabled.state = false;
 | 
				
			||||||
          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(
 | 
					      return SafeArea(
 | 
				
			||||||
        bottom: !isMultiSelectEnable,
 | 
					        bottom: !multiselectEnabled.state,
 | 
				
			||||||
        top: !isMultiSelectEnable,
 | 
					        top: !multiselectEnabled.state,
 | 
				
			||||||
        child: Stack(
 | 
					        child: Stack(
 | 
				
			||||||
          children: [
 | 
					          children: [
 | 
				
			||||||
            CustomScrollView(
 | 
					            CustomScrollView(
 | 
				
			||||||
              slivers: [
 | 
					              slivers: [
 | 
				
			||||||
                _buildSliverAppBar(),
 | 
					                multiselectEnabled.state
 | 
				
			||||||
 | 
					                    ? const SliverToBoxAdapter(
 | 
				
			||||||
 | 
					                        child: SizedBox(
 | 
				
			||||||
 | 
					                          height: 70,
 | 
				
			||||||
 | 
					                          child: null,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      )
 | 
				
			||||||
 | 
					                    : ImmichSliverAppBar(
 | 
				
			||||||
 | 
					                        onPopBack: reloadAllAsset,
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            Padding(
 | 
					            Padding(
 | 
				
			||||||
              padding: const EdgeInsets.only(top: 60.0, bottom: 0.0),
 | 
					              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) ...[
 | 
					            if (multiselectEnabled.state) ...[
 | 
				
			||||||
              _buildSelectedItemCountIndicator(),
 | 
					              ControlBottomAppBar(
 | 
				
			||||||
              const ControlBottomAppBar(),
 | 
					                onShare: onShareAssets,
 | 
				
			||||||
 | 
					                onDelete: onDelete,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
@@ -174,7 +102,7 @@ class HomePage extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
      drawer: const ProfileDrawer(),
 | 
					      drawer: const ProfileDrawer(),
 | 
				
			||||||
      body: _buildBody(),
 | 
					      body: buildBody(),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,11 @@
 | 
				
			|||||||
import 'package:collection/collection.dart';
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/search/models/search_result_page_state.model.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/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:intl/intl.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -66,3 +69,12 @@ final searchResultGroupByDateTimeProvider = StateProvider((ref) {
 | 
				
			|||||||
        .format(DateTime.parse(element.createdAt).toLocal()),
 | 
					        .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);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,14 +4,12 @@ import 'package:flutter/material.dart';
 | 
				
			|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
import 'package:flutter_spinkit/flutter_spinkit.dart';
 | 
					import 'package:flutter_spinkit/flutter_spinkit.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.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/asset_grid/immich_asset_grid.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/search/providers/search_page_state.provider.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/providers/search_result_page.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.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 {
 | 
					class SearchResultPage extends HookConsumerWidget {
 | 
				
			||||||
  const SearchResultPage({Key? key, required this.searchTerm})
 | 
					  const SearchResultPage({Key? key, required this.searchTerm})
 | 
				
			||||||
@@ -21,17 +19,12 @@ class SearchResultPage extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    ScrollController scrollController = useScrollController();
 | 
					 | 
				
			||||||
    final searchTermController = useTextEditingController(text: "");
 | 
					    final searchTermController = useTextEditingController(text: "");
 | 
				
			||||||
    final isNewSearch = useState(false);
 | 
					    final isNewSearch = useState(false);
 | 
				
			||||||
    final currentSearchTerm = useState(searchTerm);
 | 
					    final currentSearchTerm = useState(searchTerm);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final List<Widget> imageGridGroup = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    FocusNode? searchFocusNode;
 | 
					    FocusNode? searchFocusNode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    List<AssetResponseDto> sortedAssetList = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    useEffect(
 | 
					    useEffect(
 | 
				
			||||||
      () {
 | 
					      () {
 | 
				
			||||||
        searchFocusNode = FocusNode();
 | 
					        searchFocusNode = FocusNode();
 | 
				
			||||||
@@ -117,7 +110,12 @@ class SearchResultPage extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    _buildSearchResult() {
 | 
					    _buildSearchResult() {
 | 
				
			||||||
      var searchResultPageState = ref.watch(searchResultPageProvider);
 | 
					      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) {
 | 
					      if (searchResultPageState.isError) {
 | 
				
			||||||
        return const Text("Error");
 | 
					        return const Text("Error");
 | 
				
			||||||
@@ -132,57 +130,11 @@ class SearchResultPage extends HookConsumerWidget {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (searchResultPageState.isSuccess) {
 | 
					      if (searchResultPageState.isSuccess) {
 | 
				
			||||||
        if (searchResultPageState.searchResult.isNotEmpty) {
 | 
					        return ImmichAssetGrid(
 | 
				
			||||||
          int? lastMonth;
 | 
					          renderList: searchResultRenderList,
 | 
				
			||||||
          // set sorted List
 | 
					          assetsPerRow: assetsPerRow,
 | 
				
			||||||
          for (var group in assetGroupByDateTime.values) {
 | 
					          showStorageIndicator: showStorageIndicator,
 | 
				
			||||||
            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 const SizedBox();
 | 
					      return const SizedBox();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,6 @@
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					 | 
				
			||||||
import 'package:fluttertoast/fluttertoast.dart';
 | 
					 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.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 {
 | 
					class ExperimentalSettings extends HookConsumerWidget {
 | 
				
			||||||
  const ExperimentalSettings({
 | 
					  const ExperimentalSettings({
 | 
				
			||||||
@@ -14,33 +9,6 @@ class ExperimentalSettings extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  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(
 | 
					    return ExpansionTile(
 | 
				
			||||||
      textColor: Theme.of(context).primaryColor,
 | 
					      textColor: Theme.of(context).primaryColor,
 | 
				
			||||||
      title: const Text(
 | 
					      title: const Text(
 | 
				
			||||||
@@ -55,25 +23,25 @@ class ExperimentalSettings extends HookConsumerWidget {
 | 
				
			|||||||
          fontSize: 13,
 | 
					          fontSize: 13,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ).tr(),
 | 
					      ).tr(),
 | 
				
			||||||
      children: [
 | 
					      children: const [
 | 
				
			||||||
        SwitchListTile.adaptive(
 | 
					        // SwitchListTile.adaptive(
 | 
				
			||||||
          activeColor: Theme.of(context).primaryColor,
 | 
					        //   activeColor: Theme.of(context).primaryColor,
 | 
				
			||||||
          title: const Text(
 | 
					        //   title: const Text(
 | 
				
			||||||
            "experimental_settings_new_asset_list_title",
 | 
					        //     "experimental_settings_new_asset_list_title",
 | 
				
			||||||
            style: TextStyle(
 | 
					        //     style: TextStyle(
 | 
				
			||||||
              fontSize: 12,
 | 
					        //       fontSize: 12,
 | 
				
			||||||
              fontWeight: FontWeight.bold,
 | 
					        //       fontWeight: FontWeight.bold,
 | 
				
			||||||
            ),
 | 
					        //     ),
 | 
				
			||||||
          ).tr(),
 | 
					        //   ).tr(),
 | 
				
			||||||
          subtitle: const Text(
 | 
					        //   subtitle: const Text(
 | 
				
			||||||
            "experimental_settings_new_asset_list_subtitle",
 | 
					        //     "experimental_settings_new_asset_list_subtitle",
 | 
				
			||||||
            style: TextStyle(
 | 
					        //     style: TextStyle(
 | 
				
			||||||
              fontSize: 12,
 | 
					        //       fontSize: 12,
 | 
				
			||||||
            ),
 | 
					        //     ),
 | 
				
			||||||
          ).tr(),
 | 
					        //   ).tr(),
 | 
				
			||||||
          value: useExperimentalAssetGrid.value,
 | 
					        //   value: useExperimentalAssetGrid.value,
 | 
				
			||||||
          onChanged: changeUseExperimentalAssetGrid,
 | 
					        //   onChanged: changeUseExperimentalAssetGrid,
 | 
				
			||||||
        ),
 | 
					        // ),
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -43,7 +43,7 @@ class SettingsPage extends HookConsumerWidget {
 | 
				
			|||||||
              const ThemeSetting(),
 | 
					              const ThemeSetting(),
 | 
				
			||||||
              const AssetListSettings(),
 | 
					              const AssetListSettings(),
 | 
				
			||||||
              if (Platform.isAndroid) const NotificationSetting(),
 | 
					              if (Platform.isAndroid) const NotificationSetting(),
 | 
				
			||||||
              const ExperimentalSettings(),
 | 
					              //const ExperimentalSettings(),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ).toList(),
 | 
					          ).toList(),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.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';
 | 
					import 'package:immich_mobile/routing/router.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TabControllerPage extends ConsumerWidget {
 | 
					class TabControllerPage extends ConsumerWidget {
 | 
				
			||||||
@@ -10,8 +10,7 @@ class TabControllerPage extends ConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    var isMultiSelectEnable =
 | 
					    final multiselectEnabled = ref.watch(multiselectProvider);
 | 
				
			||||||
        ref.watch(homePageStateProvider).isMultiSelectEnable;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AutoTabsRouter(
 | 
					    return AutoTabsRouter(
 | 
				
			||||||
      routes: [
 | 
					      routes: [
 | 
				
			||||||
@@ -32,7 +31,7 @@ class TabControllerPage extends ConsumerWidget {
 | 
				
			|||||||
              opacity: animation,
 | 
					              opacity: animation,
 | 
				
			||||||
              child: child,
 | 
					              child: child,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            bottomNavigationBar: isMultiSelectEnable
 | 
					            bottomNavigationBar: multiselectEnabled
 | 
				
			||||||
                ? null
 | 
					                ? null
 | 
				
			||||||
                : BottomNavigationBar(
 | 
					                : BottomNavigationBar(
 | 
				
			||||||
                    selectedLabelStyle: const TextStyle(
 | 
					                    selectedLabelStyle: const TextStyle(
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										159
									
								
								mobile/test/asset_grid_data_structure_test.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								mobile/test/asset_grid_data_structure_test.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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]);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user