From 11cfa86ef3e417167a8284659c38a037eeb60d64 Mon Sep 17 00:00:00 2001 From: Lauritz Tieste Date: Sat, 26 Jul 2025 18:50:47 +0200 Subject: [PATCH 1/3] feat: improve thumbnail border radius animation feat: remove thin border between image and image selection container feat: enhance selection icon in thumbnail image feat: add animated selection indicator for multiselect in thumbnail image feat: remove unnecessary widgets and variables style: format code fix: errors with formatting checks --- .../widgets/asset_grid/thumbnail_image.dart | 62 +++++++++++-------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/mobile/lib/widgets/asset_grid/thumbnail_image.dart b/mobile/lib/widgets/asset_grid/thumbnail_image.dart index 93385b88b3..7e49f450c2 100644 --- a/mobile/lib/widgets/asset_grid/thumbnail_image.dart +++ b/mobile/lib/widgets/asset_grid/thumbnail_image.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/duration_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; @@ -47,21 +47,11 @@ class ThumbnailImage extends StatelessWidget { return Stack( children: [ + Container(color: canDeselect ? assetContainerColor : Colors.grey), AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.decelerate, - decoration: BoxDecoration( - border: multiselectEnabled && isSelected - ? canDeselect - ? Border.all(color: assetContainerColor, width: 8) - : const Border( - top: BorderSide(color: Colors.grey, width: 8), - right: BorderSide(color: Colors.grey, width: 8), - bottom: BorderSide(color: Colors.grey, width: 8), - left: BorderSide(color: Colors.grey, width: 8), - ) - : const Border(), - ), + padding: EdgeInsets.all(multiselectEnabled && isSelected ? 8 : 0), child: Stack( children: [ _ImageIcon( @@ -80,13 +70,30 @@ class ThumbnailImage extends StatelessWidget { ], ), ), - if (multiselectEnabled) - isSelected - ? const Padding( - padding: EdgeInsets.all(3.0), - child: Align(alignment: Alignment.topLeft, child: _SelectedIcon()), - ) - : const Icon(Icons.circle_outlined, color: Colors.white), + TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: multiselectEnabled ? 1.0 : 0.0), + duration: const Duration(milliseconds: 300), + curve: Curves.decelerate, + builder: (context, value, child) { + final double outlineCircleScale = 0.6 + (0.4 * value); + return Padding( + padding: EdgeInsets.all(isSelected ? value * 3.0 : 3.0), + child: Align( + alignment: Alignment.topLeft, + child: Opacity( + opacity: isSelected ? 1 : value, + child: Transform.scale( + scale: isSelected ? value : outlineCircleScale, + alignment: isSelected ? Alignment.topLeft : Alignment.center, + child: isSelected + ? const _SelectedIcon() + : Icon(Icons.circle_outlined, color: Colors.white.withValues(alpha: 0.8)), + ), + ), + ), + ); + }, + ), ], ); } @@ -247,13 +254,14 @@ class _ImageIcon extends StatelessWidget { ), ); - if (!multiselectEnabled || !isSelected) { - return image; - } - - return DecoratedBox( - decoration: canDeselect ? BoxDecoration(color: assetContainerColor) : const BoxDecoration(color: Colors.grey), - child: ClipRRect(borderRadius: const BorderRadius.all(Radius.circular(15.0)), child: image), + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: (multiselectEnabled && isSelected) ? 15.0 : 0.0), + duration: const Duration(milliseconds: 300), + curve: Curves.decelerate, + builder: (context, value, child) { + return ClipRRect(borderRadius: BorderRadius.all(Radius.circular(value)), child: child); + }, + child: image, ); } } From 13127cf830dfc4dfc4b526985ffa30a41b7ed222 Mon Sep 17 00:00:00 2001 From: bwees Date: Sat, 11 Oct 2025 22:15:21 -0500 Subject: [PATCH 2/3] chore: port to new timeline --- .../widgets/images/thumbnail_tile.widget.dart | 63 +++++++++---------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart index 5359391261..c7628cb472 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart @@ -42,31 +42,23 @@ class ThumbnailTile extends ConsumerWidget { multiSelectProvider.select((multiselect) => multiselect.selectedAssets.contains(asset)), ); - final borderStyle = lockSelection - ? BoxDecoration( - color: context.colorScheme.surfaceContainerHighest, - border: Border.all(color: context.colorScheme.surfaceContainerHighest, width: 6), - ) - : isSelected - ? BoxDecoration( - color: assetContainerColor, - border: Border.all(color: assetContainerColor, width: 6), - ) - : const BoxDecoration(); - final bool storageIndicator = ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator))) && showStorageIndicator; return Stack( children: [ + Container(color: lockSelection ? context.colorScheme.surfaceContainerHighest : assetContainerColor), AnimatedContainer( duration: Durations.short4, curve: Curves.decelerate, - decoration: borderStyle, - child: ClipRRect( - borderRadius: isSelected || lockSelection - ? const BorderRadius.all(Radius.circular(15.0)) - : BorderRadius.zero, + padding: EdgeInsets.all(isSelected || lockSelection ? 6 : 0), + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: (isSelected || lockSelection) ? 15.0 : 0.0), + duration: Durations.short4, + curve: Curves.decelerate, + builder: (context, value, child) { + return ClipRRect(borderRadius: BorderRadius.all(Radius.circular(value)), child: child); + }, child: Stack( children: [ Positioned.fill( @@ -116,29 +108,36 @@ class ThumbnailTile extends ConsumerWidget { ), ), ), - if (isSelected || lockSelection) - Padding( - padding: const EdgeInsets.all(3.0), - child: Align( - alignment: Alignment.topLeft, - child: _SelectionIndicator( - isSelected: isSelected, - isLocked: lockSelection, - color: lockSelection ? context.colorScheme.surfaceContainerHighest : assetContainerColor, + TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: (isSelected || lockSelection) ? 1.0 : 0.0), + duration: Durations.short4, + curve: Curves.decelerate, + builder: (context, value, child) { + return Padding( + padding: EdgeInsets.all((isSelected || lockSelection) ? value * 3.0 : 3.0), + child: Align( + alignment: Alignment.topLeft, + child: Opacity( + opacity: (isSelected || lockSelection) ? 1 : value, + child: _SelectionIndicator( + isLocked: lockSelection, + color: lockSelection ? context.colorScheme.surfaceContainerHighest : assetContainerColor, + ), + ), ), - ), - ), + ); + }, + ), ], ); } } class _SelectionIndicator extends StatelessWidget { - final bool isSelected; final bool isLocked; final Color? color; - const _SelectionIndicator({required this.isSelected, required this.isLocked, this.color}); + const _SelectionIndicator({required this.isLocked, this.color}); @override Widget build(BuildContext context) { @@ -147,13 +146,11 @@ class _SelectionIndicator extends StatelessWidget { decoration: BoxDecoration(shape: BoxShape.circle, color: color), child: const Icon(Icons.check_circle_rounded, color: Colors.grey), ); - } else if (isSelected) { + } else { return DecoratedBox( 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); } } } From 75c6c22025b7e9b6e6e6e22a9adcc01bae3b769b Mon Sep 17 00:00:00 2001 From: bwees Date: Sat, 11 Oct 2025 22:16:00 -0500 Subject: [PATCH 3/3] chore: revert mobile/lib/widgets/asset_grid/thumbnail_image.dart --- .../widgets/asset_grid/thumbnail_image.dart | 62 ++++++++----------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/mobile/lib/widgets/asset_grid/thumbnail_image.dart b/mobile/lib/widgets/asset_grid/thumbnail_image.dart index 7e49f450c2..93385b88b3 100644 --- a/mobile/lib/widgets/asset_grid/thumbnail_image.dart +++ b/mobile/lib/widgets/asset_grid/thumbnail_image.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/duration_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; @@ -47,11 +47,21 @@ class ThumbnailImage extends StatelessWidget { return Stack( children: [ - Container(color: canDeselect ? assetContainerColor : Colors.grey), AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.decelerate, - padding: EdgeInsets.all(multiselectEnabled && isSelected ? 8 : 0), + decoration: BoxDecoration( + border: multiselectEnabled && isSelected + ? canDeselect + ? Border.all(color: assetContainerColor, width: 8) + : const Border( + top: BorderSide(color: Colors.grey, width: 8), + right: BorderSide(color: Colors.grey, width: 8), + bottom: BorderSide(color: Colors.grey, width: 8), + left: BorderSide(color: Colors.grey, width: 8), + ) + : const Border(), + ), child: Stack( children: [ _ImageIcon( @@ -70,30 +80,13 @@ class ThumbnailImage extends StatelessWidget { ], ), ), - TweenAnimationBuilder( - tween: Tween(begin: 0.0, end: multiselectEnabled ? 1.0 : 0.0), - duration: const Duration(milliseconds: 300), - curve: Curves.decelerate, - builder: (context, value, child) { - final double outlineCircleScale = 0.6 + (0.4 * value); - return Padding( - padding: EdgeInsets.all(isSelected ? value * 3.0 : 3.0), - child: Align( - alignment: Alignment.topLeft, - child: Opacity( - opacity: isSelected ? 1 : value, - child: Transform.scale( - scale: isSelected ? value : outlineCircleScale, - alignment: isSelected ? Alignment.topLeft : Alignment.center, - child: isSelected - ? const _SelectedIcon() - : Icon(Icons.circle_outlined, color: Colors.white.withValues(alpha: 0.8)), - ), - ), - ), - ); - }, - ), + if (multiselectEnabled) + isSelected + ? const Padding( + padding: EdgeInsets.all(3.0), + child: Align(alignment: Alignment.topLeft, child: _SelectedIcon()), + ) + : const Icon(Icons.circle_outlined, color: Colors.white), ], ); } @@ -254,14 +247,13 @@ class _ImageIcon extends StatelessWidget { ), ); - return TweenAnimationBuilder( - tween: Tween(begin: 0.0, end: (multiselectEnabled && isSelected) ? 15.0 : 0.0), - duration: const Duration(milliseconds: 300), - curve: Curves.decelerate, - builder: (context, value, child) { - return ClipRRect(borderRadius: BorderRadius.all(Radius.circular(value)), child: child); - }, - child: image, + if (!multiselectEnabled || !isSelected) { + return image; + } + + return DecoratedBox( + decoration: canDeselect ? BoxDecoration(color: assetContainerColor) : const BoxDecoration(color: Colors.grey), + child: ClipRRect(borderRadius: const BorderRadius.all(Radius.circular(15.0)), child: image), ); } }