file_details_widget.dart 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import "package:exif/exif.dart";
  2. import "package:flutter/cupertino.dart";
  3. import "package:flutter/material.dart";
  4. import "package:photos/core/configuration.dart";
  5. import "package:photos/models/file.dart";
  6. import "package:photos/models/file_type.dart";
  7. import "package:photos/services/feature_flag_service.dart";
  8. import 'package:photos/theme/ente_theme.dart';
  9. import 'package:photos/ui/components/buttons/icon_button_widget.dart';
  10. import "package:photos/ui/components/divider_widget.dart";
  11. import 'package:photos/ui/components/title_bar_widget.dart';
  12. import 'package:photos/ui/viewer/file/file_caption_widget.dart';
  13. import "package:photos/ui/viewer/file_details/added_by_widget.dart";
  14. import "package:photos/ui/viewer/file_details/albums_item_widget.dart";
  15. import 'package:photos/ui/viewer/file_details/backed_up_time_item_widget.dart';
  16. import "package:photos/ui/viewer/file_details/creation_time_item_widget.dart";
  17. import 'package:photos/ui/viewer/file_details/exif_item_widgets.dart';
  18. import "package:photos/ui/viewer/file_details/file_properties_item_widget.dart";
  19. import "package:photos/ui/viewer/file_details/objects_item_widget.dart";
  20. import "package:photos/utils/exif_util.dart";
  21. class FileDetailsWidget extends StatefulWidget {
  22. final File file;
  23. const FileDetailsWidget(
  24. this.file, {
  25. Key? key,
  26. }) : super(key: key);
  27. @override
  28. State<FileDetailsWidget> createState() => _FileDetailsWidgetState();
  29. }
  30. class _FileDetailsWidgetState extends State<FileDetailsWidget> {
  31. final ValueNotifier<Map<String, IfdTag>?> _exifNotifier = ValueNotifier(null);
  32. final Map<String, dynamic> _exifData = {
  33. "focalLength": null,
  34. "fNumber": null,
  35. "resolution": null,
  36. "takenOnDevice": null,
  37. "exposureTime": null,
  38. "ISO": null,
  39. "megaPixels": null
  40. };
  41. bool _isImage = false;
  42. late int _currentUserID;
  43. bool showExifListTile = false;
  44. @override
  45. void initState() {
  46. debugPrint('file_details_sheet initState');
  47. _currentUserID = Configuration.instance.getUserID()!;
  48. _isImage = widget.file.fileType == FileType.image ||
  49. widget.file.fileType == FileType.livePhoto;
  50. if (_isImage) {
  51. _exifNotifier.addListener(() {
  52. if (_exifNotifier.value != null) {
  53. _generateExifForDetails(_exifNotifier.value!);
  54. }
  55. showExifListTile = _exifData["focalLength"] != null ||
  56. _exifData["fNumber"] != null ||
  57. _exifData["takenOnDevice"] != null ||
  58. _exifData["exposureTime"] != null ||
  59. _exifData["ISO"] != null;
  60. });
  61. getExif(widget.file).then((exif) {
  62. _exifNotifier.value = exif;
  63. });
  64. }
  65. super.initState();
  66. }
  67. @override
  68. void dispose() {
  69. _exifNotifier.dispose();
  70. super.dispose();
  71. }
  72. @override
  73. Widget build(BuildContext context) {
  74. final file = widget.file;
  75. final bool isFileOwner =
  76. file.ownerID == null || file.ownerID == _currentUserID;
  77. //Make sure the bottom most tile is always the same one, that is it should
  78. //not be rendered only if a condition is met.
  79. final fileDetailsTiles = <Widget>[];
  80. fileDetailsTiles.add(
  81. !widget.file.isUploaded ||
  82. (!isFileOwner && (widget.file.caption?.isEmpty ?? true))
  83. ? const SizedBox(height: 16)
  84. : Padding(
  85. padding: const EdgeInsets.only(top: 8, bottom: 24),
  86. child: isFileOwner
  87. ? FileCaptionWidget(file: widget.file)
  88. : FileCaptionReadyOnly(caption: widget.file.caption!),
  89. ),
  90. );
  91. fileDetailsTiles.addAll([
  92. CreationTimeItem(file, _currentUserID),
  93. const FileDetailsDivider(),
  94. ValueListenableBuilder(
  95. valueListenable: _exifNotifier,
  96. builder: (context, _, __) => FilePropertiesItemWidget(
  97. file,
  98. _isImage,
  99. _exifData,
  100. _currentUserID,
  101. ),
  102. ),
  103. const FileDetailsDivider(),
  104. ]);
  105. fileDetailsTiles.add(
  106. ValueListenableBuilder(
  107. valueListenable: _exifNotifier,
  108. builder: (context, value, _) {
  109. return showExifListTile
  110. ? Column(
  111. children: [
  112. BasicExifItemWidget(_exifData),
  113. const FileDetailsDivider(),
  114. ],
  115. )
  116. : const SizedBox.shrink();
  117. },
  118. ),
  119. );
  120. if (_isImage) {
  121. fileDetailsTiles.addAll([
  122. ValueListenableBuilder(
  123. valueListenable: _exifNotifier,
  124. builder: (context, value, _) {
  125. return Column(
  126. children: [
  127. AllExifItemWidget(file, _exifNotifier.value),
  128. const FileDetailsDivider(),
  129. ],
  130. );
  131. },
  132. )
  133. ]);
  134. }
  135. if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
  136. fileDetailsTiles.addAll([
  137. ObjectsItemWidget(file),
  138. const FileDetailsDivider(),
  139. ]);
  140. }
  141. if (file.uploadedFileID != null && file.updationTime != null) {
  142. fileDetailsTiles.addAll(
  143. [
  144. BackedUpTimeItemWidget(file),
  145. const FileDetailsDivider(),
  146. ],
  147. );
  148. }
  149. fileDetailsTiles.add(AlbumsItemWidget(file, _currentUserID));
  150. return SafeArea(
  151. top: false,
  152. child: Scrollbar(
  153. thickness: 4,
  154. radius: const Radius.circular(2),
  155. thumbVisibility: true,
  156. child: Padding(
  157. padding: const EdgeInsets.all(8.0),
  158. child: CustomScrollView(
  159. physics: const ClampingScrollPhysics(),
  160. shrinkWrap: true,
  161. slivers: <Widget>[
  162. TitleBarWidget(
  163. isFlexibleSpaceDisabled: true,
  164. title: "Details",
  165. isOnTopOfScreen: false,
  166. backgroundColor: getEnteColorScheme(context).backgroundElevated,
  167. leading: IconButtonWidget(
  168. icon: Icons.expand_more_outlined,
  169. iconButtonType: IconButtonType.primary,
  170. onTap: () => Navigator.pop(context),
  171. ),
  172. ),
  173. SliverToBoxAdapter(
  174. child: AddedByWidget(
  175. widget.file,
  176. _currentUserID,
  177. ),
  178. ),
  179. SliverList(
  180. delegate: SliverChildBuilderDelegate(
  181. (context, index) {
  182. return fileDetailsTiles[index];
  183. },
  184. childCount: fileDetailsTiles.length,
  185. ),
  186. )
  187. ],
  188. ),
  189. ),
  190. ),
  191. );
  192. }
  193. _generateExifForDetails(Map<String, IfdTag> exif) {
  194. if (exif["EXIF FocalLength"] != null) {
  195. _exifData["focalLength"] =
  196. (exif["EXIF FocalLength"]!.values.toList()[0] as Ratio).numerator /
  197. (exif["EXIF FocalLength"]!.values.toList()[0] as Ratio)
  198. .denominator;
  199. }
  200. if (exif["EXIF FNumber"] != null) {
  201. _exifData["fNumber"] =
  202. (exif["EXIF FNumber"]!.values.toList()[0] as Ratio).numerator /
  203. (exif["EXIF FNumber"]!.values.toList()[0] as Ratio).denominator;
  204. }
  205. final imageWidth = exif["EXIF ExifImageWidth"] ?? exif["Image ImageWidth"];
  206. final imageLength = exif["EXIF ExifImageLength"] ??
  207. exif["Image "
  208. "ImageLength"];
  209. if (imageWidth != null && imageLength != null) {
  210. _exifData["resolution"] = '$imageWidth x $imageLength';
  211. _exifData['megaPixels'] =
  212. ((imageWidth.values.firstAsInt() * imageLength.values.firstAsInt()) /
  213. 1000000)
  214. .toStringAsFixed(1);
  215. } else {
  216. debugPrint("No image width/height");
  217. }
  218. if (exif["Image Make"] != null && exif["Image Model"] != null) {
  219. _exifData["takenOnDevice"] =
  220. exif["Image Make"].toString() + " " + exif["Image Model"].toString();
  221. }
  222. if (exif["EXIF ExposureTime"] != null) {
  223. _exifData["exposureTime"] = exif["EXIF ExposureTime"].toString();
  224. }
  225. if (exif["EXIF ISOSpeedRatings"] != null) {
  226. _exifData['ISO'] = exif["EXIF ISOSpeedRatings"].toString();
  227. }
  228. }
  229. }
  230. class FileDetailsDivider extends StatelessWidget {
  231. const FileDetailsDivider({super.key});
  232. @override
  233. Widget build(BuildContext context) {
  234. const dividerPadding = EdgeInsets.symmetric(vertical: 15.5);
  235. return const DividerWidget(
  236. dividerType: DividerType.menu,
  237. divColorHasBlur: false,
  238. padding: dividerPadding,
  239. );
  240. }
  241. }