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({
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,

View File

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

View File

@ -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;
@ -45,9 +38,48 @@ class ImmichAssetGrid extends HookConsumerWidget {
.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();
}
}

View File

@ -1,5 +1,6 @@
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';
@ -16,6 +17,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 +28,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 +57,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 +73,7 @@ class ThumbnailImage extends HookConsumerWidget {
}
},
onLongPress: () {
// Enable multi select function
ref.watch(homePageStateProvider.notifier).enableMultiSelect({asset});
onSelect?.call();
HapticFeedback.heavyImpact();
},
child: Hero(
@ -84,7 +82,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 +126,7 @@ class ThumbnailImage extends HookConsumerWidget {
},
),
),
if (isMultiSelectEnable)
if (multiselectEnabled)
Padding(
padding: const EdgeInsets.all(3.0),
child: Align(

View File

@ -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

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