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

@ -1,13 +1,18 @@
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/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
class ThumbnailTile extends StatelessWidget {
class ThumbnailTile extends ConsumerWidget {
const ThumbnailTile(
this.asset, {
this.size = const Size.square(256),
this.fit = BoxFit.cover,
this.showStorageIndicator = true,
this.canDeselect = true,
super.key,
});
@ -16,46 +21,128 @@ class ThumbnailTile extends StatelessWidget {
final BoxFit fit;
final bool showStorageIndicator;
/// If we are allowed to deselect this image
final bool canDeselect;
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final assetContainerColor = context.isDarkTheme
? context.primaryColor.darken(amount: 0.6)
: context.primaryColor.lighten(amount: 0.8);
final isSelected = ref
.watch(multiSelectProvider.select((state) => state.selectedAssets))
.contains(asset);
return Stack(
children: [
Positioned.fill(child: Thumbnail(asset: asset, fit: fit, size: size)),
if (asset.isVideo)
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(right: 10.0, top: 6.0),
child: _VideoIndicator(asset.durationInSeconds ?? 0),
AnimatedContainer(
duration: Durations.short4,
curve: Curves.decelerate,
decoration: BoxDecoration(
color: isSelected
? (canDeselect ? assetContainerColor : Colors.grey)
: null,
border: isSelected
? Border.all(
color: canDeselect ? assetContainerColor : Colors.grey,
width: 8,
)
: const Border(),
),
child: ClipRRect(
borderRadius: isSelected
? const BorderRadius.all(Radius.circular(15.0))
: BorderRadius.zero,
child: Stack(
children: [
Positioned.fill(
child: Thumbnail(
asset: asset,
fit: fit,
size: size,
),
),
if (asset.isVideo)
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(right: 10.0, top: 6.0),
child: _VideoIndicator(asset.durationInSeconds ?? 0),
),
),
if (showStorageIndicator)
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(right: 10.0, bottom: 6.0),
child: _TileOverlayIcon(
switch (asset.storage) {
AssetState.local => Icons.cloud_off_outlined,
AssetState.remote => Icons.cloud_outlined,
AssetState.merged => Icons.cloud_done_outlined,
},
),
),
),
if (asset.isFavorite)
const Align(
alignment: Alignment.bottomLeft,
child: Padding(
padding: EdgeInsets.only(left: 10.0, bottom: 6.0),
child: _TileOverlayIcon(Icons.favorite_rounded),
),
),
],
),
),
if (showStorageIndicator)
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(right: 10.0, bottom: 6.0),
child: _TileOverlayIcon(
switch (asset.storage) {
AssetState.local => Icons.cloud_off_outlined,
AssetState.remote => Icons.cloud_outlined,
AssetState.merged => Icons.cloud_done_outlined,
},
),
if (isSelected)
Padding(
padding: const EdgeInsets.all(3.0),
child: Align(
alignment: Alignment.topLeft,
child: _SelectionIndicator(
isSelected: isSelected,
color: assetContainerColor,
),
),
),
if (asset.isFavorite)
const Align(
alignment: Alignment.bottomLeft,
child: Padding(
padding: EdgeInsets.only(left: 10.0, bottom: 6.0),
child: _TileOverlayIcon(Icons.favorite_rounded),
),
),
],
);
}
}
class _SelectionIndicator extends StatelessWidget {
final bool isSelected;
final Color? color;
const _SelectionIndicator({
required this.isSelected,
this.color,
});
@override
Widget build(BuildContext context) {
if (isSelected) {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
),
child: Icon(
Icons.check_circle_rounded,
color: context.primaryColor,
),
);
} else {
return const Icon(
Icons.circle_outlined,
color: Colors.white,
);
}
}
}
class _VideoIndicator extends StatelessWidget {
final int durationInSeconds;
const _VideoIndicator(this.durationInSeconds);