mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	Merge branch 'main' of github.com:immich-app/immich into dev/mobile-cosmetic-improvement
This commit is contained in:
		
						commit
						e38166837d
					
				@ -171,8 +171,5 @@
 | 
			
		||||
  "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
 | 
			
		||||
  "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
 | 
			
		||||
  "experimental_settings_title": "Experimental",
 | 
			
		||||
  "experimental_settings_subtitle": "Use at your own risk!",
 | 
			
		||||
  "experimental_settings_new_asset_list_title": "Enable experimental photo grid",
 | 
			
		||||
  "experimental_settings_new_asset_list_subtitle": "Work in progress",
 | 
			
		||||
  "settings_require_restart": "Please restart Immich to apply this setting"
 | 
			
		||||
  "experimental_settings_subtitle": "Use at your own risk!"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
enum RenderAssetGridElementType {
 | 
			
		||||
  assetRow,
 | 
			
		||||
  dayTitle,
 | 
			
		||||
  monthTitle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class RenderAssetGridRow {
 | 
			
		||||
  final List<AssetResponseDto> assets;
 | 
			
		||||
 | 
			
		||||
  RenderAssetGridRow(this.assets);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class RenderAssetGridElement {
 | 
			
		||||
  final RenderAssetGridElementType type;
 | 
			
		||||
  final RenderAssetGridRow? assetRow;
 | 
			
		||||
  final String? title;
 | 
			
		||||
  final DateTime date;
 | 
			
		||||
  final List<AssetResponseDto>? relatedAssetList;
 | 
			
		||||
 | 
			
		||||
  RenderAssetGridElement(
 | 
			
		||||
    this.type, {
 | 
			
		||||
    this.assetRow,
 | 
			
		||||
    this.title,
 | 
			
		||||
    required this.date,
 | 
			
		||||
    this.relatedAssetList,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final renderListProvider = StateProvider((ref) {
 | 
			
		||||
  var assetGroups = ref.watch(assetGroupByDateTimeProvider);
 | 
			
		||||
  var settings = ref.watch(appSettingsServiceProvider);
 | 
			
		||||
 | 
			
		||||
  var settings = ref.watch(appSettingsServiceProvider);
 | 
			
		||||
  final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
 | 
			
		||||
 | 
			
		||||
  List<RenderAssetGridElement> elements = [];
 | 
			
		||||
  DateTime? lastDate;
 | 
			
		||||
 | 
			
		||||
  assetGroups.forEach((groupName, assets) {
 | 
			
		||||
    try {
 | 
			
		||||
      final date = DateTime.parse(groupName);
 | 
			
		||||
 | 
			
		||||
      if (lastDate == null || lastDate!.month != date.month) {
 | 
			
		||||
        elements.add(
 | 
			
		||||
          RenderAssetGridElement(
 | 
			
		||||
            RenderAssetGridElementType.monthTitle,
 | 
			
		||||
            title: groupName,
 | 
			
		||||
            date: date,
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Add group title
 | 
			
		||||
      elements.add(
 | 
			
		||||
        RenderAssetGridElement(
 | 
			
		||||
          RenderAssetGridElementType.dayTitle,
 | 
			
		||||
          title: groupName,
 | 
			
		||||
          date: date,
 | 
			
		||||
          relatedAssetList: assets,
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // Add rows
 | 
			
		||||
      int cursor = 0;
 | 
			
		||||
      while (cursor < assets.length) {
 | 
			
		||||
        int rowElements = min(assets.length - cursor, assetsPerRow);
 | 
			
		||||
 | 
			
		||||
        final rowElement = RenderAssetGridElement(
 | 
			
		||||
          RenderAssetGridElementType.assetRow,
 | 
			
		||||
          date: date,
 | 
			
		||||
          assetRow: RenderAssetGridRow(
 | 
			
		||||
            assets.sublist(cursor, cursor + rowElements),
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        elements.add(rowElement);
 | 
			
		||||
        cursor += rowElements;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      lastDate = date;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint(e.toString());
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return elements;
 | 
			
		||||
  return assetGroupsToRenderList(assetGroups, assetsPerRow);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
                  ),
 | 
			
		||||
          )
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -13,11 +13,8 @@ class DisableMultiSelectButton extends ConsumerWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    return Positioned(
 | 
			
		||||
      top: 10,
 | 
			
		||||
      left: 0,
 | 
			
		||||
      child: Padding(
 | 
			
		||||
        padding: const EdgeInsets.only(left: 16.0, top: 46),
 | 
			
		||||
    return Padding(
 | 
			
		||||
        padding: const EdgeInsets.only(left: 16.0, top: 15),
 | 
			
		||||
        child: Padding(
 | 
			
		||||
          padding: const EdgeInsets.symmetric(horizontal: 4.0),
 | 
			
		||||
          child: ElevatedButton.icon(
 | 
			
		||||
@ -34,7 +31,6 @@ class DisableMultiSelectButton extends ConsumerWidget {
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -5,7 +5,6 @@ import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:hive_flutter/hive_flutter.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/constants/hive_box.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/routing/router.dart';
 | 
			
		||||
import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
			
		||||
@ -16,6 +15,10 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
			
		||||
  final List<AssetResponseDto> assetList;
 | 
			
		||||
  final bool showStorageIndicator;
 | 
			
		||||
  final bool useGrayBoxPlaceholder;
 | 
			
		||||
  final bool isSelected;
 | 
			
		||||
  final bool multiselectEnabled;
 | 
			
		||||
  final Function? onSelect;
 | 
			
		||||
  final Function? onDeselect;
 | 
			
		||||
 | 
			
		||||
  const ThumbnailImage({
 | 
			
		||||
    Key? key,
 | 
			
		||||
@ -23,19 +26,21 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
			
		||||
    required this.assetList,
 | 
			
		||||
    this.showStorageIndicator = true,
 | 
			
		||||
    this.useGrayBoxPlaceholder = false,
 | 
			
		||||
    this.isSelected = false,
 | 
			
		||||
    this.multiselectEnabled = false,
 | 
			
		||||
    this.onDeselect,
 | 
			
		||||
    this.onSelect,
 | 
			
		||||
  }) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    var box = Hive.box(userInfoBox);
 | 
			
		||||
    var thumbnailRequestUrl = getThumbnailUrl(asset);
 | 
			
		||||
    var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
 | 
			
		||||
    var isMultiSelectEnable =
 | 
			
		||||
        ref.watch(homePageStateProvider).isMultiSelectEnable;
 | 
			
		||||
    var deviceId = ref.watch(authenticationProvider).deviceId;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    Widget buildSelectionIcon(AssetResponseDto asset) {
 | 
			
		||||
      if (selectedAsset.contains(asset)) {
 | 
			
		||||
      if (isSelected) {
 | 
			
		||||
        return Icon(
 | 
			
		||||
          Icons.check_circle,
 | 
			
		||||
          color: Theme.of(context).primaryColor,
 | 
			
		||||
@ -50,20 +55,12 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
    return GestureDetector(
 | 
			
		||||
      onTap: () {
 | 
			
		||||
        if (isMultiSelectEnable &&
 | 
			
		||||
            selectedAsset.contains(asset) &&
 | 
			
		||||
            selectedAsset.length == 1) {
 | 
			
		||||
          ref.watch(homePageStateProvider.notifier).disableMultiSelect();
 | 
			
		||||
        } else if (isMultiSelectEnable &&
 | 
			
		||||
            selectedAsset.contains(asset) &&
 | 
			
		||||
            selectedAsset.length > 1) {
 | 
			
		||||
          ref
 | 
			
		||||
              .watch(homePageStateProvider.notifier)
 | 
			
		||||
              .removeSingleSelectedItem(asset);
 | 
			
		||||
        } else if (isMultiSelectEnable && !selectedAsset.contains(asset)) {
 | 
			
		||||
          ref
 | 
			
		||||
              .watch(homePageStateProvider.notifier)
 | 
			
		||||
              .addSingleSelectedItem(asset);
 | 
			
		||||
        if (multiselectEnabled) {
 | 
			
		||||
          if (isSelected) {
 | 
			
		||||
            onDeselect?.call();
 | 
			
		||||
          } else {
 | 
			
		||||
            onSelect?.call();
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          AutoRouter.of(context).push(
 | 
			
		||||
            GalleryViewerRoute(
 | 
			
		||||
@ -74,8 +71,7 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      onLongPress: () {
 | 
			
		||||
        // Enable multi select function
 | 
			
		||||
        ref.watch(homePageStateProvider.notifier).enableMultiSelect({asset});
 | 
			
		||||
        onSelect?.call();
 | 
			
		||||
        HapticFeedback.heavyImpact();
 | 
			
		||||
      },
 | 
			
		||||
      child: Hero(
 | 
			
		||||
@ -84,7 +80,7 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
			
		||||
          children: [
 | 
			
		||||
            Container(
 | 
			
		||||
              decoration: BoxDecoration(
 | 
			
		||||
                border: isMultiSelectEnable && selectedAsset.contains(asset)
 | 
			
		||||
                border: multiselectEnabled && isSelected
 | 
			
		||||
                    ? Border.all(
 | 
			
		||||
                        color: Theme.of(context).primaryColorLight,
 | 
			
		||||
                        width: 10,
 | 
			
		||||
@ -128,7 +124,7 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            if (isMultiSelectEnable)
 | 
			
		||||
            if (multiselectEnabled)
 | 
			
		||||
              Padding(
 | 
			
		||||
                padding: const EdgeInsets.all(3.0),
 | 
			
		||||
                child: Align(
 | 
			
		||||
@ -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:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
 | 
			
		||||
 | 
			
		||||
class ControlBottomAppBar extends ConsumerWidget {
 | 
			
		||||
  const ControlBottomAppBar({Key? key}) : super(key: key);
 | 
			
		||||
  final Function onShare;
 | 
			
		||||
  final Function onDelete;
 | 
			
		||||
 | 
			
		||||
  const ControlBottomAppBar(
 | 
			
		||||
      {Key? key, required this.onShare, required this.onDelete})
 | 
			
		||||
      : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
@ -36,7 +40,9 @@ class ControlBottomAppBar extends ConsumerWidget {
 | 
			
		||||
                      showDialog(
 | 
			
		||||
                        context: context,
 | 
			
		||||
                        builder: (BuildContext context) {
 | 
			
		||||
                          return const DeleteDialog();
 | 
			
		||||
                          return DeleteDialog(
 | 
			
		||||
                            onDelete: onDelete,
 | 
			
		||||
                          );
 | 
			
		||||
                        },
 | 
			
		||||
                      );
 | 
			
		||||
                    },
 | 
			
		||||
@ -45,14 +51,7 @@ class ControlBottomAppBar extends ConsumerWidget {
 | 
			
		||||
                    iconData: Icons.share,
 | 
			
		||||
                    label: "control_bottom_app_bar_share".tr(),
 | 
			
		||||
                    onPressed: () {
 | 
			
		||||
                      final homePageState = ref.watch(homePageStateProvider);
 | 
			
		||||
                      ref.watch(homePageStateProvider.notifier).shareAssets(
 | 
			
		||||
                            homePageState.selectedItems.toList(),
 | 
			
		||||
                            context,
 | 
			
		||||
                          );
 | 
			
		||||
                      ref
 | 
			
		||||
                          .watch(homePageStateProvider.notifier)
 | 
			
		||||
                          .disableMultiSelect();
 | 
			
		||||
                      onShare();
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
 | 
			
		||||
@ -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:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
 | 
			
		||||
 | 
			
		||||
class DeleteDialog extends ConsumerWidget {
 | 
			
		||||
  const DeleteDialog({Key? key}) : super(key: key);
 | 
			
		||||
  final Function onDelete;
 | 
			
		||||
 | 
			
		||||
  const DeleteDialog({Key? key, required this.onDelete}) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final homePageState = ref.watch(homePageStateProvider);
 | 
			
		||||
 | 
			
		||||
    return AlertDialog(
 | 
			
		||||
      // backgroundColor: Colors.grey[200],
 | 
			
		||||
@ -31,11 +30,7 @@ class DeleteDialog extends ConsumerWidget {
 | 
			
		||||
        ),
 | 
			
		||||
        TextButton(
 | 
			
		||||
          onPressed: () {
 | 
			
		||||
            ref
 | 
			
		||||
                .watch(assetProvider.notifier)
 | 
			
		||||
                .deleteAssets(homePageState.selectedItems);
 | 
			
		||||
            ref.watch(homePageStateProvider.notifier).disableMultiSelect();
 | 
			
		||||
 | 
			
		||||
            onDelete();
 | 
			
		||||
            Navigator.of(context).pop();
 | 
			
		||||
          },
 | 
			
		||||
          child: Text(
 | 
			
		||||
 | 
			
		||||
@ -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:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/providers/home_page_render_list_provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/daily_title_text.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/disable_multi_select_button.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/image_grid.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/asset_list_v2/immich_asset_grid.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/services/share.service.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
class HomePage extends HookConsumerWidget {
 | 
			
		||||
@ -26,22 +21,9 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final appSettingService = ref.watch(appSettingsServiceProvider);
 | 
			
		||||
 | 
			
		||||
    var renderList = ref.watch(renderListProvider);
 | 
			
		||||
 | 
			
		||||
    ScrollController scrollController = useScrollController();
 | 
			
		||||
    var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
 | 
			
		||||
    List<Widget> imageGridGroup = [];
 | 
			
		||||
    var isMultiSelectEnable =
 | 
			
		||||
        ref.watch(homePageStateProvider).isMultiSelectEnable;
 | 
			
		||||
    var homePageState = ref.watch(homePageStateProvider);
 | 
			
		||||
    List<AssetResponseDto> sortedAssetList = [];
 | 
			
		||||
    // set sorted List
 | 
			
		||||
    for (var group in assetGroupByDateTime.values) {
 | 
			
		||||
      for (var value in group) {
 | 
			
		||||
        sortedAssetList.add(value);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    final multiselectEnabled = ref.watch(multiselectProvider.notifier);
 | 
			
		||||
    final selection = useState(<AssetResponseDto>{});
 | 
			
		||||
 | 
			
		||||
    useEffect(
 | 
			
		||||
      () {
 | 
			
		||||
@ -57,62 +39,33 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
      ref.read(assetProvider.notifier).getAllAsset();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _buildSelectedItemCountIndicator() {
 | 
			
		||||
      return DisableMultiSelectButton(
 | 
			
		||||
        onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect,
 | 
			
		||||
        selectedItemCount: homePageState.selectedItems.length,
 | 
			
		||||
      );
 | 
			
		||||
    Widget buildBody() {
 | 
			
		||||
      void selectionListener(
 | 
			
		||||
        bool multiselect,
 | 
			
		||||
        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,
 | 
			
		||||
                  ),
 | 
			
		||||
                );
 | 
			
		||||
              }
 | 
			
		||||
      void onShareAssets() {
 | 
			
		||||
        ref.watch(shareServiceProvider).shareAssets(selection.value.toList());
 | 
			
		||||
        multiselectEnabled.state = false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
            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()}",
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      void onDelete() {
 | 
			
		||||
        ref.watch(assetProvider.notifier).deleteAssets(selection.value);
 | 
			
		||||
        multiselectEnabled.state = false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      _buildSliverAppBar() {
 | 
			
		||||
        return isMultiSelectEnable
 | 
			
		||||
      return SafeArea(
 | 
			
		||||
        bottom: !multiselectEnabled.state,
 | 
			
		||||
        top: !multiselectEnabled.state,
 | 
			
		||||
        child: Stack(
 | 
			
		||||
          children: [
 | 
			
		||||
            CustomScrollView(
 | 
			
		||||
              slivers: [
 | 
			
		||||
                multiselectEnabled.state
 | 
			
		||||
                    ? const SliverToBoxAdapter(
 | 
			
		||||
                        child: SizedBox(
 | 
			
		||||
                          height: 70,
 | 
			
		||||
@ -121,51 +74,26 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
                      )
 | 
			
		||||
                    : ImmichSliverAppBar(
 | 
			
		||||
                        onPopBack: reloadAllAsset,
 | 
			
		||||
              );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      _buildAssetGrid() {
 | 
			
		||||
        if (appSettingService
 | 
			
		||||
            .getSetting(AppSettingsEnum.useExperimentalAssetGrid)) {
 | 
			
		||||
          return ImmichAssetGrid(
 | 
			
		||||
                      ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
            Padding(
 | 
			
		||||
              padding: const EdgeInsets.only(top: 60.0, bottom: 0.0),
 | 
			
		||||
              child: 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,
 | 
			
		||||
              ],
 | 
			
		||||
                listener: selectionListener,
 | 
			
		||||
                selectionActive: multiselectEnabled.state,
 | 
			
		||||
              ),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return SafeArea(
 | 
			
		||||
        bottom: !isMultiSelectEnable,
 | 
			
		||||
        top: !isMultiSelectEnable,
 | 
			
		||||
        child: Stack(
 | 
			
		||||
          children: [
 | 
			
		||||
            CustomScrollView(
 | 
			
		||||
              slivers: [
 | 
			
		||||
                _buildSliverAppBar(),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
            Padding(
 | 
			
		||||
              padding: const EdgeInsets.only(top: 60.0, bottom: 0.0),
 | 
			
		||||
              child: _buildAssetGrid(),
 | 
			
		||||
            if (multiselectEnabled.state) ...[
 | 
			
		||||
              ControlBottomAppBar(
 | 
			
		||||
                onShare: onShareAssets,
 | 
			
		||||
                onDelete: onDelete,
 | 
			
		||||
              ),
 | 
			
		||||
            if (isMultiSelectEnable) ...[
 | 
			
		||||
              _buildSelectedItemCountIndicator(),
 | 
			
		||||
              const ControlBottomAppBar(),
 | 
			
		||||
            ],
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
@ -174,7 +102,7 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      drawer: const ProfileDrawer(),
 | 
			
		||||
      body: _buildBody(),
 | 
			
		||||
      body: buildBody(),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,11 @@
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 | 
			
		||||
import 'package:intl/intl.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
@ -66,3 +69,12 @@ final searchResultGroupByDateTimeProvider = StateProvider((ref) {
 | 
			
		||||
        .format(DateTime.parse(element.createdAt).toLocal()),
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
final searchRenderListProvider = StateProvider((ref) {
 | 
			
		||||
  var assetGroups = ref.watch(searchResultGroupByDateTimeProvider);
 | 
			
		||||
 | 
			
		||||
  var settings = ref.watch(appSettingsServiceProvider);
 | 
			
		||||
  final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
 | 
			
		||||
 | 
			
		||||
  return assetGroupsToRenderList(assetGroups, assetsPerRow);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -4,14 +4,12 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:flutter_spinkit/flutter_spinkit.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/daily_title_text.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/image_grid.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 | 
			
		||||
 | 
			
		||||
class SearchResultPage extends HookConsumerWidget {
 | 
			
		||||
  const SearchResultPage({Key? key, required this.searchTerm})
 | 
			
		||||
@ -21,17 +19,12 @@ class SearchResultPage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    ScrollController scrollController = useScrollController();
 | 
			
		||||
    final searchTermController = useTextEditingController(text: "");
 | 
			
		||||
    final isNewSearch = useState(false);
 | 
			
		||||
    final currentSearchTerm = useState(searchTerm);
 | 
			
		||||
 | 
			
		||||
    final List<Widget> imageGridGroup = [];
 | 
			
		||||
 | 
			
		||||
    FocusNode? searchFocusNode;
 | 
			
		||||
 | 
			
		||||
    List<AssetResponseDto> sortedAssetList = [];
 | 
			
		||||
 | 
			
		||||
    useEffect(
 | 
			
		||||
      () {
 | 
			
		||||
        searchFocusNode = FocusNode();
 | 
			
		||||
@ -117,7 +110,12 @@ class SearchResultPage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
    _buildSearchResult() {
 | 
			
		||||
      var searchResultPageState = ref.watch(searchResultPageProvider);
 | 
			
		||||
      var assetGroupByDateTime = ref.watch(searchResultGroupByDateTimeProvider);
 | 
			
		||||
      var searchResultRenderList = ref.watch(searchRenderListProvider);
 | 
			
		||||
 | 
			
		||||
      var settings = ref.watch(appSettingsServiceProvider);
 | 
			
		||||
      final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
 | 
			
		||||
      final showStorageIndicator =
 | 
			
		||||
          settings.getSetting(AppSettingsEnum.storageIndicator);
 | 
			
		||||
 | 
			
		||||
      if (searchResultPageState.isError) {
 | 
			
		||||
        return const Text("Error");
 | 
			
		||||
@ -132,58 +130,12 @@ class SearchResultPage extends HookConsumerWidget {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (searchResultPageState.isSuccess) {
 | 
			
		||||
        if (searchResultPageState.searchResult.isNotEmpty) {
 | 
			
		||||
          int? lastMonth;
 | 
			
		||||
          // set sorted List
 | 
			
		||||
          for (var group in assetGroupByDateTime.values) {
 | 
			
		||||
            for (var value in group) {
 | 
			
		||||
              sortedAssetList.add(value);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          assetGroupByDateTime.forEach((dateGroup, immichAssetList) {
 | 
			
		||||
            DateTime parseDateGroup = DateTime.parse(dateGroup);
 | 
			
		||||
            int currentMonth = parseDateGroup.month;
 | 
			
		||||
 | 
			
		||||
            if (lastMonth != null) {
 | 
			
		||||
              if (currentMonth - lastMonth! != 0) {
 | 
			
		||||
                imageGridGroup.add(
 | 
			
		||||
                  MonthlyTitleText(
 | 
			
		||||
                    isoDate: dateGroup,
 | 
			
		||||
                  ),
 | 
			
		||||
        return ImmichAssetGrid(
 | 
			
		||||
          renderList: searchResultRenderList,
 | 
			
		||||
          assetsPerRow: assetsPerRow,
 | 
			
		||||
          showStorageIndicator: showStorageIndicator,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,6 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:fluttertoast/fluttertoast.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
 | 
			
		||||
 | 
			
		||||
class ExperimentalSettings extends HookConsumerWidget {
 | 
			
		||||
  const ExperimentalSettings({
 | 
			
		||||
@ -14,33 +9,6 @@ class ExperimentalSettings extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final appSettingService = ref.watch(appSettingsServiceProvider);
 | 
			
		||||
 | 
			
		||||
    final useExperimentalAssetGrid = useState(false);
 | 
			
		||||
 | 
			
		||||
    useEffect(
 | 
			
		||||
      () {
 | 
			
		||||
        useExperimentalAssetGrid.value = appSettingService
 | 
			
		||||
            .getSetting(AppSettingsEnum.useExperimentalAssetGrid);
 | 
			
		||||
        return null;
 | 
			
		||||
      },
 | 
			
		||||
      [],
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    void changeUseExperimentalAssetGrid(bool status) {
 | 
			
		||||
      useExperimentalAssetGrid.value = status;
 | 
			
		||||
      appSettingService.setSetting(
 | 
			
		||||
        AppSettingsEnum.useExperimentalAssetGrid,
 | 
			
		||||
        status,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      ImmichToast.show(
 | 
			
		||||
        context: context,
 | 
			
		||||
        msg: "settings_require_restart".tr(),
 | 
			
		||||
        gravity: ToastGravity.BOTTOM,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return ExpansionTile(
 | 
			
		||||
      textColor: Theme.of(context).primaryColor,
 | 
			
		||||
      title: const Text(
 | 
			
		||||
@ -55,25 +23,25 @@ class ExperimentalSettings extends HookConsumerWidget {
 | 
			
		||||
          fontSize: 13,
 | 
			
		||||
        ),
 | 
			
		||||
      ).tr(),
 | 
			
		||||
      children: [
 | 
			
		||||
        SwitchListTile.adaptive(
 | 
			
		||||
          activeColor: Theme.of(context).primaryColor,
 | 
			
		||||
          title: const Text(
 | 
			
		||||
            "experimental_settings_new_asset_list_title",
 | 
			
		||||
            style: TextStyle(
 | 
			
		||||
              fontSize: 12,
 | 
			
		||||
              fontWeight: FontWeight.bold,
 | 
			
		||||
            ),
 | 
			
		||||
          ).tr(),
 | 
			
		||||
          subtitle: const Text(
 | 
			
		||||
            "experimental_settings_new_asset_list_subtitle",
 | 
			
		||||
            style: TextStyle(
 | 
			
		||||
              fontSize: 12,
 | 
			
		||||
            ),
 | 
			
		||||
          ).tr(),
 | 
			
		||||
          value: useExperimentalAssetGrid.value,
 | 
			
		||||
          onChanged: changeUseExperimentalAssetGrid,
 | 
			
		||||
        ),
 | 
			
		||||
      children: const [
 | 
			
		||||
        // SwitchListTile.adaptive(
 | 
			
		||||
        //   activeColor: Theme.of(context).primaryColor,
 | 
			
		||||
        //   title: const Text(
 | 
			
		||||
        //     "experimental_settings_new_asset_list_title",
 | 
			
		||||
        //     style: TextStyle(
 | 
			
		||||
        //       fontSize: 12,
 | 
			
		||||
        //       fontWeight: FontWeight.bold,
 | 
			
		||||
        //     ),
 | 
			
		||||
        //   ).tr(),
 | 
			
		||||
        //   subtitle: const Text(
 | 
			
		||||
        //     "experimental_settings_new_asset_list_subtitle",
 | 
			
		||||
        //     style: TextStyle(
 | 
			
		||||
        //       fontSize: 12,
 | 
			
		||||
        //     ),
 | 
			
		||||
        //   ).tr(),
 | 
			
		||||
        //   value: useExperimentalAssetGrid.value,
 | 
			
		||||
        //   onChanged: changeUseExperimentalAssetGrid,
 | 
			
		||||
        // ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -43,7 +43,7 @@ class SettingsPage extends HookConsumerWidget {
 | 
			
		||||
              const ThemeSetting(),
 | 
			
		||||
              const AssetListSettings(),
 | 
			
		||||
              if (Platform.isAndroid) const NotificationSetting(),
 | 
			
		||||
              const ExperimentalSettings(),
 | 
			
		||||
              //const ExperimentalSettings(),
 | 
			
		||||
            ],
 | 
			
		||||
          ).toList(),
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/routing/router.dart';
 | 
			
		||||
 | 
			
		||||
class TabControllerPage extends ConsumerWidget {
 | 
			
		||||
@ -10,8 +10,7 @@ class TabControllerPage extends ConsumerWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    var isMultiSelectEnable =
 | 
			
		||||
        ref.watch(homePageStateProvider).isMultiSelectEnable;
 | 
			
		||||
    final multiselectEnabled = ref.watch(multiselectProvider);
 | 
			
		||||
 | 
			
		||||
    return AutoTabsRouter(
 | 
			
		||||
      routes: [
 | 
			
		||||
@ -32,7 +31,7 @@ class TabControllerPage extends ConsumerWidget {
 | 
			
		||||
              opacity: animation,
 | 
			
		||||
              child: child,
 | 
			
		||||
            ),
 | 
			
		||||
            bottomNavigationBar: isMultiSelectEnable
 | 
			
		||||
            bottomNavigationBar: multiselectEnabled
 | 
			
		||||
                ? null
 | 
			
		||||
                : BottomNavigationBar(
 | 
			
		||||
                    selectedLabelStyle: const TextStyle(
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										159
									
								
								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]);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user