mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
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:
parent
ebcf133bea
commit
9ca31abae9
5 changed files with 416 additions and 61 deletions
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue