1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-26 10:50:29 +02:00

Move selection logic to asset grid class

This commit is contained in:
Matthias Rupp 2022-10-01 19:19:40 +02:00
parent 347ac70063
commit a117e897ca
6 changed files with 354 additions and 328 deletions

View File

@ -8,11 +8,17 @@ class DailyTitleText extends ConsumerWidget {
const DailyTitleText({ const DailyTitleText({
Key? key, Key? key,
required this.isoDate, required this.isoDate,
required this.assetGroup, required this.multiselectEnabled,
required this.onSelect,
required this.onDeselect,
required this.selected,
}) : super(key: key); }) : super(key: key);
final String isoDate; final String isoDate;
final List<AssetResponseDto> assetGroup; final bool multiselectEnabled;
final Function onSelect;
final Function onDeselect;
final bool selected;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -23,51 +29,12 @@ class DailyTitleText extends ConsumerWidget {
: "daily_title_text_date_year".tr(); : "daily_title_text_date_year".tr();
var dateText = var dateText =
DateFormat(formatDateTemplate).format(DateTime.parse(isoDate)); 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() { void handleTitleIconClick() {
if (isMultiSelectEnable && if (selected) {
selectedDateGroup.contains(dateText) && onDeselect();
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 { } else {
ref onSelect();
.watch(homePageStateProvider.notifier)
.enableMultiSelect(assetGroup.toSet());
ref
.watch(homePageStateProvider.notifier)
.addSelectedDateGroup(dateText);
} }
} }
@ -89,8 +56,8 @@ class DailyTitleText extends ConsumerWidget {
), ),
const Spacer(), const Spacer(),
GestureDetector( GestureDetector(
onTap: _handleTitleIconClick, onTap: handleTitleIconClick,
child: isMultiSelectEnable && selectedDateGroup.contains(dateText) child: multiselectEnabled && selected
? Icon( ? Icon(
Icons.check_circle_rounded, Icons.check_circle_rounded,
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,

View File

@ -13,11 +13,8 @@ class DisableMultiSelectButton extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return Positioned( return Padding(
top: 10, padding: const EdgeInsets.only(left: 16.0, top: 15),
left: 0,
child: Padding(
padding: const EdgeInsets.only(left: 16.0, top: 46),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0), padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: ElevatedButton.icon( child: ElevatedButton.icon(
@ -34,7 +31,6 @@ class DisableMultiSelectButton extends ConsumerWidget {
), ),
), ),
), ),
),
); );
} }
} }

View File

@ -1,3 +1,4 @@
import 'dart:collection';
import 'dart:math'; import 'dart:math';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -5,35 +6,27 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'asset_grid_data_structure.dart'; import 'asset_grid_data_structure.dart';
import 'daily_title_text.dart'; import 'daily_title_text.dart';
import 'disable_multi_select_button.dart';
import 'draggable_scrollbar_custom.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 ItemScrollController _itemScrollController = ItemScrollController();
final ItemPositionsListener _itemPositionsListener = final ItemPositionsListener _itemPositionsListener =
ItemPositionsListener.create(); ItemPositionsListener.create();
final List<RenderAssetGridElement> renderList; bool _scrolling = false;
final int assetsPerRow; bool _multiselect = false;
final double margin; Set<String> _selectedAssets = HashSet();
final bool showStorageIndicator;
ImmichAssetGrid({
super.key,
required this.renderList,
required this.assetsPerRow,
required this.showStorageIndicator,
this.margin = 5.0,
});
List<AssetResponseDto> get _assets { List<AssetResponseDto> get _assets {
return renderList return widget.renderList
.map((e) { .map((e) {
if (e.type == RenderAssetGridElementType.assetRow) { if (e.type == RenderAssetGridElementType.assetRow) {
return e.assetRow!.assets; return e.assetRow!.assets;
@ -45,9 +38,48 @@ class ImmichAssetGrid extends HookConsumerWidget {
.toList(); .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) { double _getItemSize(BuildContext context) {
return MediaQuery.of(context).size.width / assetsPerRow - return MediaQuery.of(context).size.width / widget.assetsPerRow -
margin * (assetsPerRow - 1) / assetsPerRow; widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
} }
Widget _buildThumbnailOrPlaceholder( Widget _buildThumbnailOrPlaceholder(
@ -60,7 +92,10 @@ class ImmichAssetGrid extends HookConsumerWidget {
return ThumbnailImage( return ThumbnailImage(
asset: asset, asset: asset,
assetList: _assets, assetList: _assets,
showStorageIndicator: showStorageIndicator, multiselectEnabled: _multiselect,
isSelected: _selectedAssets.contains(asset.id),
onSelect: () => _selectAssets([asset]),
onDeselect: () => _deselectAssets([asset]),
useGrayBoxPlaceholder: true, useGrayBoxPlaceholder: true,
); );
} }
@ -78,7 +113,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
key: Key("asset-${asset.id}"), key: Key("asset-${asset.id}"),
width: size, width: size,
height: 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), child: _buildThumbnailOrPlaceholder(asset, scrolling),
); );
}).toList(), }).toList(),
@ -89,7 +124,10 @@ class ImmichAssetGrid extends HookConsumerWidget {
BuildContext context, String title, List<AssetResponseDto> assets) { BuildContext context, String title, List<AssetResponseDto> assets) {
return DailyTitleText( return DailyTitleText(
isoDate: title, 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) { Widget _itemBuilder(BuildContext c, int position) {
final item = renderList[position]; final item = widget.renderList[position];
if (item.type == RenderAssetGridElementType.dayTitle) { if (item.type == RenderAssetGridElementType.dayTitle) {
return _buildTitle(c, item.title!, item.relatedAssetList!); return _buildTitle(c, item.title!, item.relatedAssetList!);
} else if (item.type == RenderAssetGridElementType.monthTitle) { } else if (item.type == RenderAssetGridElementType.monthTitle) {
return _buildMonthTitle(c, item.title!); return _buildMonthTitle(c, item.title!);
} else if (item.type == RenderAssetGridElementType.assetRow) { } else if (item.type == RenderAssetGridElementType.assetRow) {
return _buildAssetRow(c, item.assetRow!, scrolling); return _buildAssetRow(c, item.assetRow!, _scrolling);
} }
return const Text("Invalid widget type!"); return const Text("Invalid widget type!");
} }
Text _labelBuilder(int pos) { Text _labelBuilder(int pos) {
final date = renderList[pos].date; final date = widget.renderList[pos].date;
return Text(DateFormat.yMMMd().format(date), return Text(DateFormat.yMMMd().format(date),
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
@ -135,26 +173,27 @@ class ImmichAssetGrid extends HookConsumerWidget {
); );
} }
Widget _buildMultiSelectIndicator() {
return DisableMultiSelectButton(
onPressed: () => _deselectAll(),
selectedItemCount: _selectedAssets.length,
);
}
@override Widget _buildAssetGrid() {
Widget build(BuildContext context, WidgetRef ref) {
final scrolling = useState(false);
final useDragScrolling = _assets.length > 100; final useDragScrolling = _assets.length > 100;
void dragScrolling(bool active) { void dragScrolling(bool active) {
scrolling.value = active; setState(() {
} _scrolling = active;
});
Widget itemBuilder(BuildContext c, int position) {
return _itemBuilder(c, position, scrolling.value);
} }
final listWidget = ScrollablePositionedList.builder( final listWidget = ScrollablePositionedList.builder(
itemBuilder: itemBuilder, itemBuilder: _itemBuilder,
itemPositionsListener: _itemPositionsListener, itemPositionsListener: _itemPositionsListener,
itemScrollController: _itemScrollController, itemScrollController: _itemScrollController,
itemCount: renderList.length, itemCount: widget.renderList.length,
); );
if (!useDragScrolling) { if (!useDragScrolling) {
@ -173,4 +212,37 @@ class ImmichAssetGrid extends HookConsumerWidget {
child: listWidget, 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();
}
} }

View File

@ -1,5 +1,6 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
@ -16,6 +17,10 @@ class ThumbnailImage extends HookConsumerWidget {
final List<AssetResponseDto> assetList; final List<AssetResponseDto> assetList;
final bool showStorageIndicator; final bool showStorageIndicator;
final bool useGrayBoxPlaceholder; final bool useGrayBoxPlaceholder;
final bool isSelected;
final bool multiselectEnabled;
final Function? onSelect;
final Function? onDeselect;
const ThumbnailImage({ const ThumbnailImage({
Key? key, Key? key,
@ -23,19 +28,21 @@ class ThumbnailImage extends HookConsumerWidget {
required this.assetList, required this.assetList,
this.showStorageIndicator = true, this.showStorageIndicator = true,
this.useGrayBoxPlaceholder = false, this.useGrayBoxPlaceholder = false,
this.isSelected = false,
this.multiselectEnabled = false,
this.onDeselect,
this.onSelect,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
var box = Hive.box(userInfoBox); var box = Hive.box(userInfoBox);
var thumbnailRequestUrl = getThumbnailUrl(asset); var thumbnailRequestUrl = getThumbnailUrl(asset);
var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
var isMultiSelectEnable =
ref.watch(homePageStateProvider).isMultiSelectEnable;
var deviceId = ref.watch(authenticationProvider).deviceId; var deviceId = ref.watch(authenticationProvider).deviceId;
Widget buildSelectionIcon(AssetResponseDto asset) { Widget buildSelectionIcon(AssetResponseDto asset) {
if (selectedAsset.contains(asset)) { if (isSelected) {
return Icon( return Icon(
Icons.check_circle, Icons.check_circle,
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
@ -50,20 +57,12 @@ class ThumbnailImage extends HookConsumerWidget {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
if (isMultiSelectEnable && if (multiselectEnabled) {
selectedAsset.contains(asset) && if (isSelected) {
selectedAsset.length == 1) { onDeselect?.call();
ref.watch(homePageStateProvider.notifier).disableMultiSelect(); } else {
} else if (isMultiSelectEnable && onSelect?.call();
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 { } else {
AutoRouter.of(context).push( AutoRouter.of(context).push(
GalleryViewerRoute( GalleryViewerRoute(
@ -74,8 +73,7 @@ class ThumbnailImage extends HookConsumerWidget {
} }
}, },
onLongPress: () { onLongPress: () {
// Enable multi select function onSelect?.call();
ref.watch(homePageStateProvider.notifier).enableMultiSelect({asset});
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
}, },
child: Hero( child: Hero(
@ -84,7 +82,7 @@ class ThumbnailImage extends HookConsumerWidget {
children: [ children: [
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: isMultiSelectEnable && selectedAsset.contains(asset) border: multiselectEnabled && isSelected
? Border.all( ? Border.all(
color: Theme.of(context).primaryColorLight, color: Theme.of(context).primaryColorLight,
width: 10, width: 10,
@ -128,7 +126,7 @@ class ThumbnailImage extends HookConsumerWidget {
}, },
), ),
), ),
if (isMultiSelectEnable) if (multiselectEnabled)
Padding( Padding(
padding: const EdgeInsets.all(3.0), padding: const EdgeInsets.all(3.0),
child: Align( child: Align(

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
// ignore: must_be_immutable // ignore: must_be_immutable

View File

@ -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/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/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart'; import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
import 'package:immich_mobile/modules/home/ui/disable_multi_select_button.dart';
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart'; import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart'; import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
@ -20,12 +19,9 @@ class HomePage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final appSettingService = ref.watch(appSettingsServiceProvider); final appSettingService = ref.watch(appSettingsServiceProvider);
var renderList = ref.watch(renderListProvider); var renderList = ref.watch(renderListProvider);
var isMultiSelectEnable = final multiselectEnabled = useState(false);
ref.watch(homePageStateProvider).isMultiSelectEnable;
var homePageState = ref.watch(homePageStateProvider);
useEffect( useEffect(
() { () {
@ -41,16 +37,9 @@ class HomePage extends HookConsumerWidget {
ref.read(assetProvider.notifier).getAllAsset(); ref.read(assetProvider.notifier).getAllAsset();
} }
buildSelectedItemCountIndicator() {
return DisableMultiSelectButton(
onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect,
selectedItemCount: homePageState.selectedItems.length,
);
}
Widget buildBody() { Widget buildBody() {
buildSliverAppBar() { buildSliverAppBar() {
return isMultiSelectEnable return multiselectEnabled.value
? const SliverToBoxAdapter( ? const SliverToBoxAdapter(
child: SizedBox( child: SizedBox(
height: 70, height: 70,
@ -62,9 +51,13 @@ class HomePage extends HookConsumerWidget {
); );
} }
void selectionListener(bool multiselect) {
multiselectEnabled.value = multiselect;
}
return SafeArea( return SafeArea(
bottom: !isMultiSelectEnable, bottom: !multiselectEnabled.value,
top: !isMultiSelectEnable, top: !multiselectEnabled.value,
child: Stack( child: Stack(
children: [ children: [
CustomScrollView( CustomScrollView(
@ -80,10 +73,10 @@ class HomePage extends HookConsumerWidget {
appSettingService.getSetting(AppSettingsEnum.tilesPerRow), appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
showStorageIndicator: appSettingService showStorageIndicator: appSettingService
.getSetting(AppSettingsEnum.storageIndicator), .getSetting(AppSettingsEnum.storageIndicator),
listener: selectionListener,
), ),
), ),
if (isMultiSelectEnable) ...[ if (multiselectEnabled.value) ...[
buildSelectedItemCountIndicator(),
const ControlBottomAppBar(), const ControlBottomAppBar(),
], ],
], ],