thumbnail_image.dart 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import 'package:auto_route/auto_route.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. import 'package:immich_mobile/routing/router.dart';
  5. import 'package:immich_mobile/shared/models/asset.dart';
  6. import 'package:immich_mobile/shared/ui/immich_image.dart';
  7. import 'package:immich_mobile/utils/storage_indicator.dart';
  8. import 'package:isar/isar.dart';
  9. class ThumbnailImage extends StatelessWidget {
  10. final Asset asset;
  11. final int index;
  12. final Asset Function(int index) loadAsset;
  13. final int totalAssets;
  14. final bool showStorageIndicator;
  15. final bool showStack;
  16. final bool isOwner;
  17. final bool useGrayBoxPlaceholder;
  18. final bool isSelected;
  19. final bool multiselectEnabled;
  20. final Function? onSelect;
  21. final Function? onDeselect;
  22. final int heroOffset;
  23. final String? sharedAlbumId;
  24. const ThumbnailImage({
  25. Key? key,
  26. required this.asset,
  27. required this.index,
  28. required this.loadAsset,
  29. required this.totalAssets,
  30. this.showStorageIndicator = true,
  31. this.showStack = false,
  32. this.isOwner = true,
  33. this.sharedAlbumId,
  34. this.useGrayBoxPlaceholder = false,
  35. this.isSelected = false,
  36. this.multiselectEnabled = false,
  37. this.onDeselect,
  38. this.onSelect,
  39. this.heroOffset = 0,
  40. }) : super(key: key);
  41. @override
  42. Widget build(BuildContext context) {
  43. final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
  44. final assetContainerColor =
  45. isDarkTheme ? Colors.blueGrey : Theme.of(context).primaryColorLight;
  46. // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
  47. final isFromDto = asset.id == Isar.autoIncrement;
  48. Widget buildSelectionIcon(Asset asset) {
  49. if (isSelected) {
  50. return Container(
  51. decoration: BoxDecoration(
  52. shape: BoxShape.circle,
  53. color: assetContainerColor,
  54. ),
  55. child: Icon(
  56. Icons.check_circle_rounded,
  57. color: Theme.of(context).primaryColor,
  58. ),
  59. );
  60. } else {
  61. return const Icon(
  62. Icons.circle_outlined,
  63. color: Colors.white,
  64. );
  65. }
  66. }
  67. Widget buildVideoIcon() {
  68. final minutes = asset.duration.inMinutes;
  69. final durationString = asset.duration.toString();
  70. return Positioned(
  71. top: 5,
  72. right: 8,
  73. child: Row(
  74. children: [
  75. Text(
  76. minutes > 59
  77. ? durationString.substring(0, 7) // h:mm:ss
  78. : minutes > 0
  79. ? durationString.substring(2, 7) // mm:ss
  80. : durationString.substring(3, 7), // m:ss
  81. style: const TextStyle(
  82. color: Colors.white,
  83. fontSize: 10,
  84. fontWeight: FontWeight.bold,
  85. ),
  86. ),
  87. const SizedBox(
  88. width: 3,
  89. ),
  90. const Icon(
  91. Icons.play_circle_fill_rounded,
  92. color: Colors.white,
  93. size: 18,
  94. ),
  95. ],
  96. ),
  97. );
  98. }
  99. Widget buildStackIcon() {
  100. return Positioned(
  101. top: !asset.isImage ? 28 : 5,
  102. right: 8,
  103. child: Row(
  104. children: [
  105. if (asset.stackChildrenCount > 1)
  106. Text(
  107. "${asset.stackChildrenCount}",
  108. style: const TextStyle(
  109. color: Colors.white,
  110. fontSize: 10,
  111. fontWeight: FontWeight.bold,
  112. ),
  113. ),
  114. if (asset.stackChildrenCount > 1)
  115. const SizedBox(
  116. width: 3,
  117. ),
  118. const Icon(
  119. Icons.burst_mode_rounded,
  120. color: Colors.white,
  121. size: 18,
  122. ),
  123. ],
  124. ),
  125. );
  126. }
  127. Widget buildImage() {
  128. final image = SizedBox(
  129. width: 300,
  130. height: 300,
  131. child: Hero(
  132. tag: isFromDto
  133. ? '${asset.remoteId}-$heroOffset'
  134. : asset.id + heroOffset,
  135. child: ImmichImage(
  136. asset,
  137. useGrayBoxPlaceholder: useGrayBoxPlaceholder,
  138. fit: BoxFit.cover,
  139. ),
  140. ),
  141. );
  142. if (!multiselectEnabled || !isSelected) {
  143. return image;
  144. }
  145. return Container(
  146. decoration: BoxDecoration(
  147. border: Border.all(
  148. width: 0,
  149. color: onDeselect == null ? Colors.grey : assetContainerColor,
  150. ),
  151. color: onDeselect == null ? Colors.grey : assetContainerColor,
  152. ),
  153. child: ClipRRect(
  154. borderRadius: const BorderRadius.only(
  155. topRight: Radius.circular(15.0),
  156. bottomRight: Radius.circular(15.0),
  157. bottomLeft: Radius.circular(15.0),
  158. topLeft: Radius.zero,
  159. ),
  160. child: image,
  161. ),
  162. );
  163. }
  164. return GestureDetector(
  165. onTap: () {
  166. if (multiselectEnabled) {
  167. if (isSelected) {
  168. onDeselect?.call();
  169. } else {
  170. onSelect?.call();
  171. }
  172. } else {
  173. AutoRouter.of(context).push(
  174. GalleryViewerRoute(
  175. initialIndex: index,
  176. loadAsset: loadAsset,
  177. totalAssets: totalAssets,
  178. heroOffset: heroOffset,
  179. showStack: showStack,
  180. isOwner: isOwner,
  181. sharedAlbumId: sharedAlbumId,
  182. ),
  183. );
  184. }
  185. },
  186. onLongPress: () {
  187. onSelect?.call();
  188. HapticFeedback.heavyImpact();
  189. },
  190. child: Stack(
  191. children: [
  192. Container(
  193. decoration: BoxDecoration(
  194. border: multiselectEnabled && isSelected
  195. ? Border.all(
  196. color: onDeselect == null
  197. ? Colors.grey
  198. : assetContainerColor,
  199. width: 8,
  200. )
  201. : const Border(),
  202. ),
  203. child: buildImage(),
  204. ),
  205. if (multiselectEnabled)
  206. Padding(
  207. padding: const EdgeInsets.all(3.0),
  208. child: Align(
  209. alignment: Alignment.topLeft,
  210. child: buildSelectionIcon(asset),
  211. ),
  212. ),
  213. if (showStorageIndicator)
  214. Positioned(
  215. right: 8,
  216. bottom: 5,
  217. child: Icon(
  218. storageIcon(asset),
  219. color: Colors.white,
  220. size: 18,
  221. ),
  222. ),
  223. if (asset.isFavorite)
  224. const Positioned(
  225. left: 8,
  226. bottom: 5,
  227. child: Icon(
  228. Icons.favorite,
  229. color: Colors.white,
  230. size: 18,
  231. ),
  232. ),
  233. if (!asset.isImage) buildVideoIcon(),
  234. if (asset.stackChildrenCount > 0) buildStackIcon(),
  235. ],
  236. ),
  237. );
  238. }
  239. }