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
5 changed files with 416 additions and 61 deletions

View File

@@ -1,18 +1,25 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
class TimelineHeader extends StatelessWidget {
class TimelineHeader extends ConsumerWidget {
final Bucket bucket;
final HeaderType header;
final double height;
final int assetOffset;
const TimelineHeader({
super.key,
required this.bucket,
required this.header,
required this.height,
required this.assetOffset,
});
String _formatMonth(BuildContext context, DateTime date) {
@@ -28,33 +35,109 @@ class TimelineHeader extends StatelessWidget {
}
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
if (bucket is! TimeBucket || header == HeaderType.none) {
return const SizedBox.shrink();
}
final date = (bucket as TimeBucket).date;
return Container(
padding: const EdgeInsets.only(left: 10, top: 30, bottom: 10),
height: height,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
if (header == HeaderType.month || header == HeaderType.monthAndDay)
Text(
_formatMonth(context, date),
style: context.textTheme.labelLarge
?.copyWith(fontSize: 24, fontWeight: FontWeight.w500),
),
if (header == HeaderType.day || header == HeaderType.monthAndDay)
Text(
_formatDay(context, date),
style: context.textTheme.labelLarge
?.copyWith(fontWeight: FontWeight.w500),
),
],
List<BaseAsset> bucketAssets;
try {
bucketAssets = ref
.watch(timelineServiceProvider)
.getAssets(assetOffset, bucket.assetCount);
} catch (e) {
bucketAssets = <BaseAsset>[];
}
final isAllSelected = ref.watch(bucketSelectionProvider(bucketAssets));
final isMonthHeader =
header == HeaderType.month || header == HeaderType.monthAndDay;
final isDayHeader =
header == HeaderType.day || header == HeaderType.monthAndDay;
return Padding(
padding: EdgeInsets.only(
top: isMonthHeader ? 8.0 : 0.0,
left: 12.0,
right: 12.0,
),
child: SizedBox(
height: height,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (isMonthHeader)
Row(
children: [
Text(
_formatMonth(context, date),
style: context.textTheme.labelLarge?.copyWith(fontSize: 24),
),
const Spacer(),
if (header != HeaderType.monthAndDay)
_BulkSelectIconButton(
isAllSelected: isAllSelected,
onPressed: () => ref
.read(multiSelectProvider.notifier)
.toggleBucketSelection(
assetOffset,
bucket.assetCount,
),
),
],
),
if (isDayHeader)
Row(
children: [
Text(
_formatDay(context, date),
style: context.textTheme.labelLarge?.copyWith(
fontSize: 15,
),
),
const Spacer(),
_BulkSelectIconButton(
isAllSelected: isAllSelected,
onPressed: () => ref
.read(multiSelectProvider.notifier)
.toggleBucketSelection(assetOffset, bucket.assetCount),
),
],
),
],
),
),
);
}
}
class _BulkSelectIconButton extends ConsumerWidget {
final bool isAllSelected;
final VoidCallback onPressed;
const _BulkSelectIconButton({
required this.isAllSelected,
required this.onPressed,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return IconButton(
onPressed: onPressed,
icon: isAllSelected
? Icon(
Icons.check_circle_rounded,
size: 26,
color: context.primaryColor,
)
: Icon(
Icons.check_circle_outline_rounded,
size: 26,
color: context.colorScheme.onSurfaceSecondary,
),
);
}
}