mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:39:37 -05:00 
			
		
		
		
	Move selection logic to asset grid class
This commit is contained in:
		
							parent
							
								
									347ac70063
								
							
						
					
					
						commit
						a117e897ca
					
				@ -8,11 +8,17 @@ class DailyTitleText extends ConsumerWidget {
 | 
			
		||||
  const DailyTitleText({
 | 
			
		||||
    Key? key,
 | 
			
		||||
    required this.isoDate,
 | 
			
		||||
    required this.assetGroup,
 | 
			
		||||
    required this.multiselectEnabled,
 | 
			
		||||
    required this.onSelect,
 | 
			
		||||
    required this.onDeselect,
 | 
			
		||||
    required this.selected,
 | 
			
		||||
  }) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  final String isoDate;
 | 
			
		||||
  final List<AssetResponseDto> assetGroup;
 | 
			
		||||
  final bool multiselectEnabled;
 | 
			
		||||
  final Function onSelect;
 | 
			
		||||
  final Function onDeselect;
 | 
			
		||||
  final bool selected;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
@ -23,51 +29,12 @@ class DailyTitleText extends ConsumerWidget {
 | 
			
		||||
        : "daily_title_text_date_year".tr();
 | 
			
		||||
    var dateText =
 | 
			
		||||
        DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
 | 
			
		||||
    var isMultiSelectEnable =
 | 
			
		||||
        ref.watch(homePageStateProvider).isMultiSelectEnable;
 | 
			
		||||
    var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup;
 | 
			
		||||
    var selectedItems = ref.watch(homePageStateProvider).selectedItems;
 | 
			
		||||
 | 
			
		||||
    void _handleTitleIconClick() {
 | 
			
		||||
      if (isMultiSelectEnable &&
 | 
			
		||||
          selectedDateGroup.contains(dateText) &&
 | 
			
		||||
          selectedDateGroup.length == 1 &&
 | 
			
		||||
          selectedItems.length <= assetGroup.length) {
 | 
			
		||||
        // Multi select is active - click again on the icon while it is the only active group -> disable multi select
 | 
			
		||||
        ref.watch(homePageStateProvider.notifier).disableMultiSelect();
 | 
			
		||||
      } else if (isMultiSelectEnable &&
 | 
			
		||||
          selectedDateGroup.contains(dateText) &&
 | 
			
		||||
          selectedItems.length != assetGroup.length) {
 | 
			
		||||
        // Multi select is active - click again on the icon while it is not the only active group -> remove that group from selected group/items
 | 
			
		||||
        ref
 | 
			
		||||
            .watch(homePageStateProvider.notifier)
 | 
			
		||||
            .removeSelectedDateGroup(dateText);
 | 
			
		||||
        ref
 | 
			
		||||
            .watch(homePageStateProvider.notifier)
 | 
			
		||||
            .removeMultipleSelectedItem(assetGroup);
 | 
			
		||||
      } else if (isMultiSelectEnable &&
 | 
			
		||||
          selectedDateGroup.contains(dateText) &&
 | 
			
		||||
          selectedDateGroup.length > 1) {
 | 
			
		||||
        ref
 | 
			
		||||
            .watch(homePageStateProvider.notifier)
 | 
			
		||||
            .removeSelectedDateGroup(dateText);
 | 
			
		||||
        ref
 | 
			
		||||
            .watch(homePageStateProvider.notifier)
 | 
			
		||||
            .removeMultipleSelectedItem(assetGroup);
 | 
			
		||||
      } else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) {
 | 
			
		||||
        ref
 | 
			
		||||
            .watch(homePageStateProvider.notifier)
 | 
			
		||||
            .addSelectedDateGroup(dateText);
 | 
			
		||||
        ref
 | 
			
		||||
            .watch(homePageStateProvider.notifier)
 | 
			
		||||
            .addMultipleSelectedItems(assetGroup);
 | 
			
		||||
    void handleTitleIconClick() {
 | 
			
		||||
      if (selected) {
 | 
			
		||||
        onDeselect();
 | 
			
		||||
      } else {
 | 
			
		||||
        ref
 | 
			
		||||
            .watch(homePageStateProvider.notifier)
 | 
			
		||||
            .enableMultiSelect(assetGroup.toSet());
 | 
			
		||||
        ref
 | 
			
		||||
            .watch(homePageStateProvider.notifier)
 | 
			
		||||
            .addSelectedDateGroup(dateText);
 | 
			
		||||
        onSelect();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -89,8 +56,8 @@ class DailyTitleText extends ConsumerWidget {
 | 
			
		||||
          ),
 | 
			
		||||
          const Spacer(),
 | 
			
		||||
          GestureDetector(
 | 
			
		||||
            onTap: _handleTitleIconClick,
 | 
			
		||||
            child: isMultiSelectEnable && selectedDateGroup.contains(dateText)
 | 
			
		||||
            onTap: handleTitleIconClick,
 | 
			
		||||
            child: multiselectEnabled && selected
 | 
			
		||||
                ? Icon(
 | 
			
		||||
                    Icons.check_circle_rounded,
 | 
			
		||||
                    color: Theme.of(context).primaryColor,
 | 
			
		||||
 | 
			
		||||
@ -1,40 +1,36 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
 | 
			
		||||
class DisableMultiSelectButton extends ConsumerWidget {
 | 
			
		||||
  const DisableMultiSelectButton({
 | 
			
		||||
    Key? key,
 | 
			
		||||
    required this.onPressed,
 | 
			
		||||
    required this.selectedItemCount,
 | 
			
		||||
  }) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  final Function onPressed;
 | 
			
		||||
  final int selectedItemCount;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    return Positioned(
 | 
			
		||||
      top: 10,
 | 
			
		||||
      left: 0,
 | 
			
		||||
      child: Padding(
 | 
			
		||||
        padding: const EdgeInsets.only(left: 16.0, top: 46),
 | 
			
		||||
        child: Padding(
 | 
			
		||||
          padding: const EdgeInsets.symmetric(horizontal: 4.0),
 | 
			
		||||
          child: ElevatedButton.icon(
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              onPressed();
 | 
			
		||||
            },
 | 
			
		||||
            icon: const Icon(Icons.close_rounded),
 | 
			
		||||
            label: Text(
 | 
			
		||||
              '$selectedItemCount',
 | 
			
		||||
              style: const TextStyle(
 | 
			
		||||
                fontWeight: FontWeight.w600,
 | 
			
		||||
                fontSize: 18,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
 | 
			
		||||
class DisableMultiSelectButton extends ConsumerWidget {
 | 
			
		||||
  const DisableMultiSelectButton({
 | 
			
		||||
    Key? key,
 | 
			
		||||
    required this.onPressed,
 | 
			
		||||
    required this.selectedItemCount,
 | 
			
		||||
  }) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  final Function onPressed;
 | 
			
		||||
  final int selectedItemCount;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    return Padding(
 | 
			
		||||
        padding: const EdgeInsets.only(left: 16.0, top: 15),
 | 
			
		||||
        child: Padding(
 | 
			
		||||
          padding: const EdgeInsets.symmetric(horizontal: 4.0),
 | 
			
		||||
          child: ElevatedButton.icon(
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              onPressed();
 | 
			
		||||
            },
 | 
			
		||||
            icon: const Icon(Icons.close_rounded),
 | 
			
		||||
            label: Text(
 | 
			
		||||
              '$selectedItemCount',
 | 
			
		||||
              style: const TextStyle(
 | 
			
		||||
                fontWeight: FontWeight.w600,
 | 
			
		||||
                fontSize: 18,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import 'dart:collection';
 | 
			
		||||
import 'dart:math';
 | 
			
		||||
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
@ -5,35 +6,27 @@ import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/cupertino.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/src/widgets/framework.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
 | 
			
		||||
import 'asset_grid_data_structure.dart';
 | 
			
		||||
import 'daily_title_text.dart';
 | 
			
		||||
import 'disable_multi_select_button.dart';
 | 
			
		||||
import 'draggable_scrollbar_custom.dart';
 | 
			
		||||
 | 
			
		||||
class ImmichAssetGrid extends HookConsumerWidget {
 | 
			
		||||
typedef ImmichAssetGridSelectionListener = void Function(bool);
 | 
			
		||||
 | 
			
		||||
class ImmichAssetGridState extends State<ImmichAssetGrid> {
 | 
			
		||||
  final ItemScrollController _itemScrollController = ItemScrollController();
 | 
			
		||||
  final ItemPositionsListener _itemPositionsListener =
 | 
			
		||||
      ItemPositionsListener.create();
 | 
			
		||||
  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,
 | 
			
		||||
  });
 | 
			
		||||
  bool _scrolling = false;
 | 
			
		||||
  bool _multiselect = false;
 | 
			
		||||
  Set<String> _selectedAssets = HashSet();
 | 
			
		||||
 | 
			
		||||
  List<AssetResponseDto> get _assets {
 | 
			
		||||
    return renderList
 | 
			
		||||
    return widget.renderList
 | 
			
		||||
        .map((e) {
 | 
			
		||||
          if (e.type == RenderAssetGridElementType.assetRow) {
 | 
			
		||||
            return e.assetRow!.assets;
 | 
			
		||||
@ -44,10 +37,49 @@ class ImmichAssetGrid extends HookConsumerWidget {
 | 
			
		||||
        .flattened
 | 
			
		||||
        .toList();
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  void _selectAssets(List<AssetResponseDto> assets) {
 | 
			
		||||
    setState(() {
 | 
			
		||||
 | 
			
		||||
      if (!_multiselect) {
 | 
			
		||||
        _multiselect = true;
 | 
			
		||||
        widget.listener?.call(true);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      for (var e in assets) {
 | 
			
		||||
        _selectedAssets.add(e.id);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _deselectAssets(List<AssetResponseDto> assets) {
 | 
			
		||||
    setState(() {
 | 
			
		||||
      for (var e in assets) {
 | 
			
		||||
        _selectedAssets.remove(e.id);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (_selectedAssets.isEmpty) {
 | 
			
		||||
        _multiselect = false;
 | 
			
		||||
        widget.listener?.call(false);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _deselectAll() {
 | 
			
		||||
    setState(() {
 | 
			
		||||
      _multiselect = false;
 | 
			
		||||
      _selectedAssets.clear();
 | 
			
		||||
    });
 | 
			
		||||
    widget.listener?.call(false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool _allAssetsSelected(List<AssetResponseDto> assets) {
 | 
			
		||||
    return _multiselect && assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  double _getItemSize(BuildContext context) {
 | 
			
		||||
    return MediaQuery.of(context).size.width / assetsPerRow -
 | 
			
		||||
        margin * (assetsPerRow - 1) / assetsPerRow;
 | 
			
		||||
    return MediaQuery.of(context).size.width / widget.assetsPerRow -
 | 
			
		||||
        widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Widget _buildThumbnailOrPlaceholder(
 | 
			
		||||
@ -60,7 +92,10 @@ class ImmichAssetGrid extends HookConsumerWidget {
 | 
			
		||||
    return ThumbnailImage(
 | 
			
		||||
      asset: asset,
 | 
			
		||||
      assetList: _assets,
 | 
			
		||||
      showStorageIndicator: showStorageIndicator,
 | 
			
		||||
      multiselectEnabled: _multiselect,
 | 
			
		||||
      isSelected: _selectedAssets.contains(asset.id),
 | 
			
		||||
      onSelect: () => _selectAssets([asset]),
 | 
			
		||||
      onDeselect: () => _deselectAssets([asset]),
 | 
			
		||||
      useGrayBoxPlaceholder: true,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
@ -78,7 +113,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
 | 
			
		||||
          key: Key("asset-${asset.id}"),
 | 
			
		||||
          width: size,
 | 
			
		||||
          height: size,
 | 
			
		||||
          margin: EdgeInsets.only(top: margin, right: last ? 0.0 : margin),
 | 
			
		||||
          margin: EdgeInsets.only(top: widget.margin, right: last ? 0.0 : widget.margin),
 | 
			
		||||
          child: _buildThumbnailOrPlaceholder(asset, scrolling),
 | 
			
		||||
        );
 | 
			
		||||
      }).toList(),
 | 
			
		||||
@ -89,7 +124,10 @@ class ImmichAssetGrid extends HookConsumerWidget {
 | 
			
		||||
      BuildContext context, String title, List<AssetResponseDto> assets) {
 | 
			
		||||
    return DailyTitleText(
 | 
			
		||||
      isoDate: title,
 | 
			
		||||
      assetGroup: assets,
 | 
			
		||||
      multiselectEnabled: _multiselect,
 | 
			
		||||
      onSelect: () => _selectAssets(assets),
 | 
			
		||||
      onDeselect: () => _deselectAssets(assets),
 | 
			
		||||
      selected: _allAssetsSelected(assets),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -111,22 +149,22 @@ class ImmichAssetGrid extends HookConsumerWidget {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Widget _itemBuilder(BuildContext c, int position, bool scrolling) {
 | 
			
		||||
    final item = renderList[position];
 | 
			
		||||
  Widget _itemBuilder(BuildContext c, int position) {
 | 
			
		||||
    final item = widget.renderList[position];
 | 
			
		||||
 | 
			
		||||
    if (item.type == RenderAssetGridElementType.dayTitle) {
 | 
			
		||||
      return _buildTitle(c, item.title!, item.relatedAssetList!);
 | 
			
		||||
    } else if (item.type == RenderAssetGridElementType.monthTitle) {
 | 
			
		||||
      return _buildMonthTitle(c, item.title!);
 | 
			
		||||
    } else if (item.type == RenderAssetGridElementType.assetRow) {
 | 
			
		||||
      return _buildAssetRow(c, item.assetRow!, scrolling);
 | 
			
		||||
      return _buildAssetRow(c, item.assetRow!, _scrolling);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return const Text("Invalid widget type!");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Text _labelBuilder(int pos) {
 | 
			
		||||
    final date = renderList[pos].date;
 | 
			
		||||
    final date = widget.renderList[pos].date;
 | 
			
		||||
    return Text(DateFormat.yMMMd().format(date),
 | 
			
		||||
      style: const TextStyle(
 | 
			
		||||
        color: Colors.white,
 | 
			
		||||
@ -135,26 +173,27 @@ class ImmichAssetGrid extends HookConsumerWidget {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Widget _buildMultiSelectIndicator() {
 | 
			
		||||
    return DisableMultiSelectButton(
 | 
			
		||||
      onPressed: () => _deselectAll(),
 | 
			
		||||
      selectedItemCount: _selectedAssets.length,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final scrolling = useState(false);
 | 
			
		||||
 | 
			
		||||
  Widget _buildAssetGrid() {
 | 
			
		||||
    final useDragScrolling = _assets.length > 100;
 | 
			
		||||
 | 
			
		||||
    void dragScrolling(bool active) {
 | 
			
		||||
      scrolling.value = active;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Widget itemBuilder(BuildContext c, int position) {
 | 
			
		||||
      return _itemBuilder(c, position, scrolling.value);
 | 
			
		||||
      setState(() {
 | 
			
		||||
        _scrolling = active;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final listWidget = ScrollablePositionedList.builder(
 | 
			
		||||
      itemBuilder: itemBuilder,
 | 
			
		||||
      itemBuilder: _itemBuilder,
 | 
			
		||||
      itemPositionsListener: _itemPositionsListener,
 | 
			
		||||
      itemScrollController: _itemScrollController,
 | 
			
		||||
      itemCount: renderList.length,
 | 
			
		||||
      itemCount: widget.renderList.length,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!useDragScrolling) {
 | 
			
		||||
@ -162,15 +201,48 @@ class ImmichAssetGrid extends HookConsumerWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return DraggableScrollbar.semicircle(
 | 
			
		||||
        scrollStateListener: dragScrolling,
 | 
			
		||||
        itemPositionsListener: _itemPositionsListener,
 | 
			
		||||
        controller: _itemScrollController,
 | 
			
		||||
        backgroundColor: Theme.of(context).hintColor,
 | 
			
		||||
        labelTextBuilder: _labelBuilder,
 | 
			
		||||
        labelConstraints: const BoxConstraints(maxHeight: 28),
 | 
			
		||||
        scrollbarAnimationDuration: const Duration(seconds: 1),
 | 
			
		||||
        scrollbarTimeToFade: const Duration(seconds: 4),
 | 
			
		||||
        child: listWidget,
 | 
			
		||||
      scrollStateListener: dragScrolling,
 | 
			
		||||
      itemPositionsListener: _itemPositionsListener,
 | 
			
		||||
      controller: _itemScrollController,
 | 
			
		||||
      backgroundColor: Theme.of(context).hintColor,
 | 
			
		||||
      labelTextBuilder: _labelBuilder,
 | 
			
		||||
      labelConstraints: const BoxConstraints(maxHeight: 28),
 | 
			
		||||
      scrollbarAnimationDuration: const Duration(seconds: 1),
 | 
			
		||||
      scrollbarTimeToFade: const Duration(seconds: 4),
 | 
			
		||||
      child: listWidget,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Stack(
 | 
			
		||||
      children: [
 | 
			
		||||
        _buildAssetGrid(),
 | 
			
		||||
        if (_multiselect) _buildMultiSelectIndicator(),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ImmichAssetGrid extends StatefulWidget {
 | 
			
		||||
  final List<RenderAssetGridElement> renderList;
 | 
			
		||||
  final int assetsPerRow;
 | 
			
		||||
  final double margin;
 | 
			
		||||
  final bool showStorageIndicator;
 | 
			
		||||
  final ImmichAssetGridSelectionListener? listener;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  ImmichAssetGrid({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.renderList,
 | 
			
		||||
    required this.assetsPerRow,
 | 
			
		||||
    required this.showStorageIndicator,
 | 
			
		||||
    this.listener,
 | 
			
		||||
    this.margin = 5.0,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<StatefulWidget> createState() {
 | 
			
		||||
    return ImmichAssetGridState();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,176 +1,174 @@
 | 
			
		||||
import 'package:auto_route/auto_route.dart';
 | 
			
		||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:hive_flutter/hive_flutter.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/constants/hive_box.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/routing/router.dart';
 | 
			
		||||
import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
class ThumbnailImage extends HookConsumerWidget {
 | 
			
		||||
  final AssetResponseDto asset;
 | 
			
		||||
  final List<AssetResponseDto> assetList;
 | 
			
		||||
  final bool showStorageIndicator;
 | 
			
		||||
  final bool useGrayBoxPlaceholder;
 | 
			
		||||
 | 
			
		||||
  const ThumbnailImage({
 | 
			
		||||
    Key? key,
 | 
			
		||||
    required this.asset,
 | 
			
		||||
    required this.assetList,
 | 
			
		||||
    this.showStorageIndicator = true,
 | 
			
		||||
    this.useGrayBoxPlaceholder = false,
 | 
			
		||||
  }) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    var box = Hive.box(userInfoBox);
 | 
			
		||||
    var thumbnailRequestUrl = getThumbnailUrl(asset);
 | 
			
		||||
    var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
 | 
			
		||||
    var isMultiSelectEnable =
 | 
			
		||||
        ref.watch(homePageStateProvider).isMultiSelectEnable;
 | 
			
		||||
    var deviceId = ref.watch(authenticationProvider).deviceId;
 | 
			
		||||
 | 
			
		||||
    Widget buildSelectionIcon(AssetResponseDto asset) {
 | 
			
		||||
      if (selectedAsset.contains(asset)) {
 | 
			
		||||
        return Icon(
 | 
			
		||||
          Icons.check_circle,
 | 
			
		||||
          color: Theme.of(context).primaryColor,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        return const Icon(
 | 
			
		||||
          Icons.circle_outlined,
 | 
			
		||||
          color: Colors.white,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return GestureDetector(
 | 
			
		||||
      onTap: () {
 | 
			
		||||
        if (isMultiSelectEnable &&
 | 
			
		||||
            selectedAsset.contains(asset) &&
 | 
			
		||||
            selectedAsset.length == 1) {
 | 
			
		||||
          ref.watch(homePageStateProvider.notifier).disableMultiSelect();
 | 
			
		||||
        } else if (isMultiSelectEnable &&
 | 
			
		||||
            selectedAsset.contains(asset) &&
 | 
			
		||||
            selectedAsset.length > 1) {
 | 
			
		||||
          ref
 | 
			
		||||
              .watch(homePageStateProvider.notifier)
 | 
			
		||||
              .removeSingleSelectedItem(asset);
 | 
			
		||||
        } else if (isMultiSelectEnable && !selectedAsset.contains(asset)) {
 | 
			
		||||
          ref
 | 
			
		||||
              .watch(homePageStateProvider.notifier)
 | 
			
		||||
              .addSingleSelectedItem(asset);
 | 
			
		||||
        } else {
 | 
			
		||||
          AutoRouter.of(context).push(
 | 
			
		||||
            GalleryViewerRoute(
 | 
			
		||||
              assetList: assetList,
 | 
			
		||||
              asset: asset,
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      onLongPress: () {
 | 
			
		||||
        // Enable multi select function
 | 
			
		||||
        ref.watch(homePageStateProvider.notifier).enableMultiSelect({asset});
 | 
			
		||||
        HapticFeedback.heavyImpact();
 | 
			
		||||
      },
 | 
			
		||||
      child: Hero(
 | 
			
		||||
        tag: asset.id,
 | 
			
		||||
        child: Stack(
 | 
			
		||||
          children: [
 | 
			
		||||
            Container(
 | 
			
		||||
              decoration: BoxDecoration(
 | 
			
		||||
                border: isMultiSelectEnable && selectedAsset.contains(asset)
 | 
			
		||||
                    ? Border.all(
 | 
			
		||||
                        color: Theme.of(context).primaryColorLight,
 | 
			
		||||
                        width: 10,
 | 
			
		||||
                      )
 | 
			
		||||
                    : const Border(),
 | 
			
		||||
              ),
 | 
			
		||||
              child: CachedNetworkImage(
 | 
			
		||||
                cacheKey: 'thumbnail-image-${asset.id}',
 | 
			
		||||
                width: 300,
 | 
			
		||||
                height: 300,
 | 
			
		||||
                memCacheHeight: 200,
 | 
			
		||||
                maxWidthDiskCache: 200,
 | 
			
		||||
                maxHeightDiskCache: 200,
 | 
			
		||||
                fit: BoxFit.cover,
 | 
			
		||||
                imageUrl: thumbnailRequestUrl,
 | 
			
		||||
                httpHeaders: {
 | 
			
		||||
                  "Authorization": "Bearer ${box.get(accessTokenKey)}"
 | 
			
		||||
                },
 | 
			
		||||
                fadeInDuration: const Duration(milliseconds: 250),
 | 
			
		||||
                progressIndicatorBuilder: (context, url, downloadProgress) {
 | 
			
		||||
                  if (useGrayBoxPlaceholder) {
 | 
			
		||||
                    return const DecoratedBox(
 | 
			
		||||
                      decoration: BoxDecoration(color: Colors.grey),
 | 
			
		||||
                    );
 | 
			
		||||
                  }
 | 
			
		||||
                  return Transform.scale(
 | 
			
		||||
                    scale: 0.2,
 | 
			
		||||
                    child: CircularProgressIndicator(
 | 
			
		||||
                      value: downloadProgress.progress,
 | 
			
		||||
                    ),
 | 
			
		||||
                  );
 | 
			
		||||
                },
 | 
			
		||||
                errorWidget: (context, url, error) {
 | 
			
		||||
                  debugPrint("Error getting thumbnail $url = $error");
 | 
			
		||||
                  CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
 | 
			
		||||
 | 
			
		||||
                  return Icon(
 | 
			
		||||
                    Icons.image_not_supported_outlined,
 | 
			
		||||
                    color: Theme.of(context).primaryColor,
 | 
			
		||||
                  );
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            if (isMultiSelectEnable)
 | 
			
		||||
              Padding(
 | 
			
		||||
                padding: const EdgeInsets.all(3.0),
 | 
			
		||||
                child: Align(
 | 
			
		||||
                  alignment: Alignment.topLeft,
 | 
			
		||||
                  child: buildSelectionIcon(asset),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            if (showStorageIndicator)
 | 
			
		||||
              Positioned(
 | 
			
		||||
                right: 10,
 | 
			
		||||
                bottom: 5,
 | 
			
		||||
                child: Icon(
 | 
			
		||||
                  (deviceId != asset.deviceId)
 | 
			
		||||
                      ? Icons.cloud_done_outlined
 | 
			
		||||
                      : Icons.photo_library_rounded,
 | 
			
		||||
                  color: Colors.white,
 | 
			
		||||
                  size: 18,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            if (asset.type != AssetTypeEnum.IMAGE)
 | 
			
		||||
              Positioned(
 | 
			
		||||
                top: 5,
 | 
			
		||||
                right: 5,
 | 
			
		||||
                child: Row(
 | 
			
		||||
                  children: [
 | 
			
		||||
                    Text(
 | 
			
		||||
                      asset.duration.toString().substring(0, 7),
 | 
			
		||||
                      style: const TextStyle(
 | 
			
		||||
                        color: Colors.white,
 | 
			
		||||
                        fontSize: 10,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    const Icon(
 | 
			
		||||
                      Icons.play_circle_outline_rounded,
 | 
			
		||||
                      color: Colors.white,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
import 'package:auto_route/auto_route.dart';
 | 
			
		||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:hive_flutter/hive_flutter.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/constants/hive_box.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/routing/router.dart';
 | 
			
		||||
import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
class ThumbnailImage extends HookConsumerWidget {
 | 
			
		||||
  final AssetResponseDto asset;
 | 
			
		||||
  final List<AssetResponseDto> assetList;
 | 
			
		||||
  final bool showStorageIndicator;
 | 
			
		||||
  final bool useGrayBoxPlaceholder;
 | 
			
		||||
  final bool isSelected;
 | 
			
		||||
  final bool multiselectEnabled;
 | 
			
		||||
  final Function? onSelect;
 | 
			
		||||
  final Function? onDeselect;
 | 
			
		||||
 | 
			
		||||
  const ThumbnailImage({
 | 
			
		||||
    Key? key,
 | 
			
		||||
    required this.asset,
 | 
			
		||||
    required this.assetList,
 | 
			
		||||
    this.showStorageIndicator = true,
 | 
			
		||||
    this.useGrayBoxPlaceholder = false,
 | 
			
		||||
    this.isSelected = false,
 | 
			
		||||
    this.multiselectEnabled = false,
 | 
			
		||||
    this.onDeselect,
 | 
			
		||||
    this.onSelect,
 | 
			
		||||
  }) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    var box = Hive.box(userInfoBox);
 | 
			
		||||
    var thumbnailRequestUrl = getThumbnailUrl(asset);
 | 
			
		||||
    var deviceId = ref.watch(authenticationProvider).deviceId;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    Widget buildSelectionIcon(AssetResponseDto asset) {
 | 
			
		||||
      if (isSelected) {
 | 
			
		||||
        return Icon(
 | 
			
		||||
          Icons.check_circle,
 | 
			
		||||
          color: Theme.of(context).primaryColor,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        return const Icon(
 | 
			
		||||
          Icons.circle_outlined,
 | 
			
		||||
          color: Colors.white,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return GestureDetector(
 | 
			
		||||
      onTap: () {
 | 
			
		||||
        if (multiselectEnabled) {
 | 
			
		||||
          if (isSelected) {
 | 
			
		||||
            onDeselect?.call();
 | 
			
		||||
          } else {
 | 
			
		||||
            onSelect?.call();
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          AutoRouter.of(context).push(
 | 
			
		||||
            GalleryViewerRoute(
 | 
			
		||||
              assetList: assetList,
 | 
			
		||||
              asset: asset,
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      onLongPress: () {
 | 
			
		||||
        onSelect?.call();
 | 
			
		||||
        HapticFeedback.heavyImpact();
 | 
			
		||||
      },
 | 
			
		||||
      child: Hero(
 | 
			
		||||
        tag: asset.id,
 | 
			
		||||
        child: Stack(
 | 
			
		||||
          children: [
 | 
			
		||||
            Container(
 | 
			
		||||
              decoration: BoxDecoration(
 | 
			
		||||
                border: multiselectEnabled && isSelected
 | 
			
		||||
                    ? Border.all(
 | 
			
		||||
                        color: Theme.of(context).primaryColorLight,
 | 
			
		||||
                        width: 10,
 | 
			
		||||
                      )
 | 
			
		||||
                    : const Border(),
 | 
			
		||||
              ),
 | 
			
		||||
              child: CachedNetworkImage(
 | 
			
		||||
                cacheKey: 'thumbnail-image-${asset.id}',
 | 
			
		||||
                width: 300,
 | 
			
		||||
                height: 300,
 | 
			
		||||
                memCacheHeight: 200,
 | 
			
		||||
                maxWidthDiskCache: 200,
 | 
			
		||||
                maxHeightDiskCache: 200,
 | 
			
		||||
                fit: BoxFit.cover,
 | 
			
		||||
                imageUrl: thumbnailRequestUrl,
 | 
			
		||||
                httpHeaders: {
 | 
			
		||||
                  "Authorization": "Bearer ${box.get(accessTokenKey)}"
 | 
			
		||||
                },
 | 
			
		||||
                fadeInDuration: const Duration(milliseconds: 250),
 | 
			
		||||
                progressIndicatorBuilder: (context, url, downloadProgress) {
 | 
			
		||||
                  if (useGrayBoxPlaceholder) {
 | 
			
		||||
                    return const DecoratedBox(
 | 
			
		||||
                      decoration: BoxDecoration(color: Colors.grey),
 | 
			
		||||
                    );
 | 
			
		||||
                  }
 | 
			
		||||
                  return Transform.scale(
 | 
			
		||||
                    scale: 0.2,
 | 
			
		||||
                    child: CircularProgressIndicator(
 | 
			
		||||
                      value: downloadProgress.progress,
 | 
			
		||||
                    ),
 | 
			
		||||
                  );
 | 
			
		||||
                },
 | 
			
		||||
                errorWidget: (context, url, error) {
 | 
			
		||||
                  debugPrint("Error getting thumbnail $url = $error");
 | 
			
		||||
                  CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
 | 
			
		||||
 | 
			
		||||
                  return Icon(
 | 
			
		||||
                    Icons.image_not_supported_outlined,
 | 
			
		||||
                    color: Theme.of(context).primaryColor,
 | 
			
		||||
                  );
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            if (multiselectEnabled)
 | 
			
		||||
              Padding(
 | 
			
		||||
                padding: const EdgeInsets.all(3.0),
 | 
			
		||||
                child: Align(
 | 
			
		||||
                  alignment: Alignment.topLeft,
 | 
			
		||||
                  child: buildSelectionIcon(asset),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            if (showStorageIndicator)
 | 
			
		||||
              Positioned(
 | 
			
		||||
                right: 10,
 | 
			
		||||
                bottom: 5,
 | 
			
		||||
                child: Icon(
 | 
			
		||||
                  (deviceId != asset.deviceId)
 | 
			
		||||
                      ? Icons.cloud_done_outlined
 | 
			
		||||
                      : Icons.photo_library_rounded,
 | 
			
		||||
                  color: Colors.white,
 | 
			
		||||
                  size: 18,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            if (asset.type != AssetTypeEnum.IMAGE)
 | 
			
		||||
              Positioned(
 | 
			
		||||
                top: 5,
 | 
			
		||||
                right: 5,
 | 
			
		||||
                child: Row(
 | 
			
		||||
                  children: [
 | 
			
		||||
                    Text(
 | 
			
		||||
                      asset.duration.toString().substring(0, 7),
 | 
			
		||||
                      style: const TextStyle(
 | 
			
		||||
                        color: Colors.white,
 | 
			
		||||
                        fontSize: 10,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    const Icon(
 | 
			
		||||
                      Icons.play_circle_outline_rounded,
 | 
			
		||||
                      color: Colors.white,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
// ignore: must_be_immutable
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ import 'package:immich_mobile/modules/home/providers/home_page_render_list_provi
 | 
			
		||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/disable_multi_select_button.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 | 
			
		||||
@ -20,12 +19,9 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final appSettingService = ref.watch(appSettingsServiceProvider);
 | 
			
		||||
 | 
			
		||||
    var renderList = ref.watch(renderListProvider);
 | 
			
		||||
 | 
			
		||||
    var isMultiSelectEnable =
 | 
			
		||||
        ref.watch(homePageStateProvider).isMultiSelectEnable;
 | 
			
		||||
    var homePageState = ref.watch(homePageStateProvider);
 | 
			
		||||
    final multiselectEnabled = useState(false);
 | 
			
		||||
 | 
			
		||||
    useEffect(
 | 
			
		||||
      () {
 | 
			
		||||
@ -41,16 +37,9 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
      ref.read(assetProvider.notifier).getAllAsset();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    buildSelectedItemCountIndicator() {
 | 
			
		||||
      return DisableMultiSelectButton(
 | 
			
		||||
        onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect,
 | 
			
		||||
        selectedItemCount: homePageState.selectedItems.length,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Widget buildBody() {
 | 
			
		||||
      buildSliverAppBar() {
 | 
			
		||||
        return isMultiSelectEnable
 | 
			
		||||
        return multiselectEnabled.value
 | 
			
		||||
            ? const SliverToBoxAdapter(
 | 
			
		||||
                child: SizedBox(
 | 
			
		||||
                  height: 70,
 | 
			
		||||
@ -62,9 +51,13 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
              );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      void selectionListener(bool multiselect) {
 | 
			
		||||
        multiselectEnabled.value = multiselect;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return SafeArea(
 | 
			
		||||
        bottom: !isMultiSelectEnable,
 | 
			
		||||
        top: !isMultiSelectEnable,
 | 
			
		||||
        bottom: !multiselectEnabled.value,
 | 
			
		||||
        top: !multiselectEnabled.value,
 | 
			
		||||
        child: Stack(
 | 
			
		||||
          children: [
 | 
			
		||||
            CustomScrollView(
 | 
			
		||||
@ -80,10 +73,10 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
                    appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
 | 
			
		||||
                showStorageIndicator: appSettingService
 | 
			
		||||
                    .getSetting(AppSettingsEnum.storageIndicator),
 | 
			
		||||
                listener: selectionListener,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            if (isMultiSelectEnable) ...[
 | 
			
		||||
              buildSelectedItemCountIndicator(),
 | 
			
		||||
            if (multiselectEnabled.value) ...[
 | 
			
		||||
              const ControlBottomAppBar(),
 | 
			
		||||
            ],
 | 
			
		||||
          ],
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user