@ -1,13 +1,12 @@
import ' package:flutter/material.dart ' ;
import ' package:hooks_riverpod/hooks_riverpod.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/duration_extensions.dart ' ;
import ' package:immich_mobile/extensions/theme_extensions.dart ' ;
import ' package:immich_mobile/widgets/common/immich_thumbnail.dart ' ;
import ' package:immich_mobile/utils/storage_indicator.dart ' ;
class ThumbnailImage extends Consumer Widget {
class ThumbnailImage extends Stateless Widget {
/ / / The asset to show the thumbnail image for
final Asset asset ;
@ -41,144 +40,10 @@ class ThumbnailImage extends ConsumerWidget {
} ) ;
@ override
Widget build ( BuildContext context , WidgetRef ref ) {
Widget build ( BuildContext context ) {
final assetContainerColor = context . isDarkTheme
? context . primaryColor . darken ( amount: 0.6 )
: context . primaryColor . lighten ( amount: 0.8 ) ;
/ / Assets from response DTOs do not have an isar id , querying which would give us the default autoIncrement id
final isFromDto = asset . id = = noDbId ;
Widget buildSelectionIcon ( ) {
if ( isSelected ) {
return Container (
decoration: BoxDecoration (
shape: BoxShape . circle ,
color: assetContainerColor ,
) ,
child: Icon (
Icons . check_circle_rounded ,
color: context . primaryColor ,
) ,
) ;
} else {
return const Icon (
Icons . circle_outlined ,
color: Colors . white ,
) ;
}
}
Widget buildVideoIcon ( ) {
final minutes = asset . duration . inMinutes ;
final durationString = asset . duration . toString ( ) ;
return Positioned (
top: 5 ,
right: 8 ,
child: Row (
children: [
Text (
minutes > 59
? durationString . substring ( 0 , 7 ) / / h: mm: ss
: minutes > 0
? durationString . substring ( 2 , 7 ) / / mm: ss
: durationString . substring ( 3 , 7 ) , / / m: ss
style: const TextStyle (
color: Colors . white ,
fontSize: 10 ,
fontWeight: FontWeight . bold ,
) ,
) ,
const SizedBox (
width: 3 ,
) ,
const Icon (
Icons . play_circle_fill_rounded ,
color: Colors . white ,
size: 18 ,
) ,
] ,
) ,
) ;
}
Widget buildStackIcon ( ) {
return Positioned (
top: ! asset . isImage ? 28 : 5 ,
right: 8 ,
child: Row (
children: [
if ( asset . stackCount > 1 )
Text (
" ${ asset . stackCount } " ,
style: const TextStyle (
color: Colors . white ,
fontSize: 10 ,
fontWeight: FontWeight . bold ,
) ,
) ,
if ( asset . stackCount > 1 )
const SizedBox (
width: 3 ,
) ,
const Icon (
Icons . burst_mode_rounded ,
color: Colors . white ,
size: 18 ,
) ,
] ,
) ,
) ;
}
Widget buildImage ( ) {
final image = SizedBox . expand (
child: Hero (
tag: isFromDto
? ' ${ asset . remoteId } - $ heroOffset '
: asset . id + heroOffset ,
child: Stack (
children: [
SizedBox . expand (
child: ImmichThumbnail (
asset: asset ,
height: 250 ,
width: 250 ,
) ,
) ,
Container (
decoration: const BoxDecoration (
gradient: LinearGradient (
colors: [
Color . fromRGBO ( 0 , 0 , 0 , 0.1 ) ,
Colors . transparent ,
Colors . transparent ,
Color . fromRGBO ( 0 , 0 , 0 , 0.1 ) ,
] ,
begin: Alignment . topCenter ,
end: Alignment . bottomCenter ,
stops: [ 0 , 0.3 , 0.6 , 1 ] ,
) ,
) ,
) ,
] ,
) ,
) ,
) ;
if ( ! multiselectEnabled | | ! isSelected ) {
return image ;
}
return Container (
decoration: BoxDecoration (
color: canDeselect ? assetContainerColor : Colors . grey ,
) ,
child: ClipRRect (
borderRadius: const BorderRadius . all (
Radius . circular ( 15.0 ) ,
) ,
child: image ,
) ,
) ;
}
return Stack (
children: [
@ -187,32 +52,30 @@ class ThumbnailImage extends ConsumerWidget {
curve: Curves . decelerate ,
decoration: BoxDecoration (
border: multiselectEnabled & & isSelected
? Border . all (
color: canDeselect ? assetContainerColor : Colors . grey ,
width: 8 ,
)
? 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: [
buildImage ( ) ,
if ( showStorageIndicator )
Positioned (
right: 8 ,
bottom: 5 ,
child: Icon (
storageIcon ( asset ) ,
color: Colors . white . withValues ( alpha: . 8 ) ,
size: 16 ,
shadows: [
Shadow (
blurRadius: 5.0 ,
color: Colors . black . withValues ( alpha: 0.6 ) ,
offset: const Offset ( 0.0 , 0.0 ) ,
) ,
] ,
) ,
) ,
_ImageIcon (
heroOffset: heroOffset ,
asset: asset ,
assetContainerColor: assetContainerColor ,
multiselectEnabled: multiselectEnabled ,
canDeselect: canDeselect ,
isSelected: isSelected ,
) ,
if ( showStorageIndicator ) _StorageIcon ( storage: asset . storage ) ,
if ( asset . isFavorite )
const Positioned (
left: 8 ,
@ -223,20 +86,249 @@ class ThumbnailImage extends ConsumerWidget {
size: 16 ,
) ,
) ,
if ( ! asset . isImage ) buildVideoIcon ( ) ,
if ( asset . stackCount > 0 ) buildStackIcon ( ) ,
if ( asset . isVideo ) _VideoIcon ( duration: asset . duration ) ,
if ( asset . stackCount > 0 )
_StackIcon (
isVideo: asset . isVideo ,
stackCount: asset . stackCount ,
) ,
] ,
) ,
) ,
if ( multiselectEnabled )
Padding (
padding: const EdgeInsets . all ( 3.0 ) ,
child: Align (
alignment: Alignment . topLeft ,
child: buildSelectionIcon ( ) ,
isSelected
? const Padding (
padding: EdgeInsets . all ( 3.0 ) ,
child: Align (
alignment: Alignment . topLeft ,
child: _SelectedIcon ( ) ,
) ,
)
: const Icon (
Icons . circle_outlined ,
color: Colors . white ,
) ,
] ,
) ;
}
}
class _SelectedIcon extends StatelessWidget {
const _SelectedIcon ( ) ;
@ override
Widget build ( BuildContext context ) {
final assetContainerColor = context . isDarkTheme
? context . primaryColor . darken ( amount: 0.6 )
: context . primaryColor . lighten ( amount: 0.8 ) ;
return DecoratedBox (
decoration: BoxDecoration (
shape: BoxShape . circle ,
color: assetContainerColor ,
) ,
child: Icon (
Icons . check_circle_rounded ,
color: context . primaryColor ,
) ,
) ;
}
}
class _VideoIcon extends StatelessWidget {
final Duration duration ;
const _VideoIcon ( { required this . duration } ) ;
@ override
Widget build ( BuildContext context ) {
return Positioned (
top: 5 ,
right: 8 ,
child: Row (
children: [
Text (
duration . format ( ) ,
style: const TextStyle (
color: Colors . white ,
fontSize: 10 ,
fontWeight: FontWeight . bold ,
) ,
) ,
] ,
const SizedBox ( width: 3 ) ,
const Icon (
Icons . play_circle_fill_rounded ,
color: Colors . white ,
size: 18 ,
) ,
] ,
) ,
) ;
}
}
class _StackIcon extends StatelessWidget {
final bool isVideo ;
final int stackCount ;
const _StackIcon ( { required this . isVideo , required this . stackCount } ) ;
@ override
Widget build ( BuildContext context ) {
return Positioned (
top: isVideo ? 28 : 5 ,
right: 8 ,
child: Row (
children: [
if ( stackCount > 1 )
Text (
" $ stackCount " ,
style: const TextStyle (
color: Colors . white ,
fontSize: 10 ,
fontWeight: FontWeight . bold ,
) ,
) ,
if ( stackCount > 1 )
const SizedBox (
width: 3 ,
) ,
const Icon (
Icons . burst_mode_rounded ,
color: Colors . white ,
size: 18 ,
) ,
] ,
) ,
) ;
}
}
class _StorageIcon extends StatelessWidget {
final AssetState storage ;
const _StorageIcon ( { required this . storage } ) ;
@ override
Widget build ( BuildContext context ) {
return switch ( storage ) {
AssetState . local = > const Positioned (
right: 8 ,
bottom: 5 ,
child: Icon (
Icons . cloud_off_outlined ,
color: Color . fromRGBO ( 255 , 255 , 255 , 0.8 ) ,
size: 16 ,
shadows: [
Shadow (
blurRadius: 5.0 ,
color: Color . fromRGBO ( 0 , 0 , 0 , 0.6 ) ,
offset: Offset ( 0.0 , 0.0 ) ,
) ,
] ,
) ,
) ,
AssetState . remote = > const Positioned (
right: 8 ,
bottom: 5 ,
child: Icon (
Icons . cloud_outlined ,
color: Color . fromRGBO ( 255 , 255 , 255 , 0.8 ) ,
size: 16 ,
shadows: [
Shadow (
blurRadius: 5.0 ,
color: Color . fromRGBO ( 0 , 0 , 0 , 0.6 ) ,
offset: Offset ( 0.0 , 0.0 ) ,
) ,
] ,
) ,
) ,
AssetState . merged = > const Positioned (
right: 8 ,
bottom: 5 ,
child: Icon (
Icons . cloud_done_outlined ,
color: Color . fromRGBO ( 255 , 255 , 255 , 0.8 ) ,
size: 16 ,
shadows: [
Shadow (
blurRadius: 5.0 ,
color: Color . fromRGBO ( 0 , 0 , 0 , 0.6 ) ,
offset: Offset ( 0.0 , 0.0 ) ,
) ,
] ,
) ,
) ,
} ;
}
}
class _ImageIcon extends StatelessWidget {
final int heroOffset ;
final Asset asset ;
final Color assetContainerColor ;
final bool multiselectEnabled ;
final bool canDeselect ;
final bool isSelected ;
const _ImageIcon ( {
required this . heroOffset ,
required this . asset ,
required this . assetContainerColor ,
required this . multiselectEnabled ,
required this . canDeselect ,
required this . isSelected ,
} ) ;
@ override
Widget build ( BuildContext context ) {
/ / Assets from response DTOs do not have an isar id , querying which would give us the default autoIncrement id
final isDto = asset . id = = noDbId ;
final image = SizedBox . expand (
child: Hero (
tag: isDto ? ' ${ asset . remoteId } - $ heroOffset ' : asset . id + heroOffset ,
child: Stack (
children: [
SizedBox . expand (
child: ImmichThumbnail (
asset: asset ,
height: 250 ,
width: 250 ,
) ,
) ,
const DecoratedBox (
decoration: BoxDecoration (
gradient: LinearGradient (
colors: [
Color . fromRGBO ( 0 , 0 , 0 , 0.1 ) ,
Colors . transparent ,
Colors . transparent ,
Color . fromRGBO ( 0 , 0 , 0 , 0.1 ) ,
] ,
begin: Alignment . topCenter ,
end: Alignment . bottomCenter ,
stops: [ 0 , 0.3 , 0.6 , 1 ] ,
) ,
) ,
) ,
] ,
) ,
) ,
) ;
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 ,
) ,
) ;
}
}