feat: new timeline multi-selection (#19443)

* feat: new timeline multiselection

* select all from bucket

* wip

* group multi-select

* group multi-select

* pr feedback

* pr feedback

* lint
This commit is contained in:
Alex 2025-06-24 02:05:25 -05:00 committed by GitHub
parent ebcf133bea
commit 9ca31abae9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 416 additions and 61 deletions

View file

@ -9,7 +9,9 @@ import 'package:immich_mobile/presentation/widgets/timeline/header.widget.dart';
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/segment_builder.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
class FixedSegment extends Segment {
final double tileHeight;
@ -60,21 +62,39 @@ class FixedSegment extends Segment {
return gridIndex + firstRowBelow - 1;
}
void _handleOnTap(WidgetRef ref, BaseAsset asset) {
if (!ref.read(multiSelectProvider.select((s) => s.isEnabled))) {
return;
}
ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset);
}
void _handleOnLongPress(WidgetRef ref, BaseAsset asset) {
if (ref.read(multiSelectProvider.select((s) => s.isEnabled))) {
return;
}
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset);
}
@override
Widget builder(BuildContext context, int index) {
final rowIndexInSegment = index - (firstIndex + 1);
final assetIndex = rowIndexInSegment * columnCount;
final assetCount = bucket.assetCount;
final numberOfAssets = math.min(columnCount, assetCount - assetIndex);
if (index == firstIndex) {
return TimelineHeader(
bucket: bucket,
header: header,
height: headerExtent,
assetOffset: firstAssetIndex,
);
}
final rowIndexInSegment = index - (firstIndex + 1);
final assetIndex = rowIndexInSegment * columnCount;
final assetCount = bucket.assetCount;
final numberOfAssets = math.min(columnCount, assetCount - assetIndex);
return _buildRow(firstAssetIndex + assetIndex, numberOfAssets);
}
@ -97,7 +117,12 @@ class FixedSegment extends Segment {
// Bucket is already loaded, show the assets
if (timelineService.hasRange(assetIndex, count)) {
final assets = timelineService.getAssets(assetIndex, count);
return _buildAssetRow(ctx, assets);
return _buildAssetRow(
ctx,
assets,
onTap: (asset) => _handleOnTap(ref, asset),
onLongPress: (asset) => _handleOnLongPress(ref, asset),
);
}
// Bucket is not loaded, show placeholders and load the bucket
@ -113,20 +138,36 @@ class FixedSegment extends Segment {
);
}
return _buildAssetRow(ctxx, snap.requireData);
return _buildAssetRow(
ctxx,
snap.requireData,
onTap: (asset) => _handleOnTap(ref, asset),
onLongPress: (asset) => _handleOnLongPress(ref, asset),
);
},
);
},
);
Widget _buildAssetRow(BuildContext context, List<BaseAsset> assets) =>
Widget _buildAssetRow(
BuildContext context,
List<BaseAsset> assets, {
required void Function(BaseAsset) onTap,
required void Function(BaseAsset) onLongPress,
}) =>
FixedTimelineRow(
dimension: tileHeight,
spacing: spacing,
textDirection: Directionality.of(context),
children: List.generate(
assets.length,
(i) => RepaintBoundary(child: ThumbnailTile(assets[i])),
(i) => RepaintBoundary(
child: GestureDetector(
onTap: () => onTap(assets[i]),
onLongPress: () => onLongPress(assets[i]),
child: ThumbnailTile(assets[i]),
),
),
),
);
}