file_details_widget.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. import "package:exif/exif.dart";
  2. import "package:flutter/cupertino.dart";
  3. import "package:flutter/material.dart";
  4. import "package:logging/logging.dart";
  5. import "package:photos/core/configuration.dart";
  6. import 'package:photos/db/files_db.dart';
  7. import "package:photos/ente_theme_data.dart";
  8. import "package:photos/models/collection.dart";
  9. import "package:photos/models/collection_items.dart";
  10. import "package:photos/models/file.dart";
  11. import "package:photos/models/file_type.dart";
  12. import "package:photos/models/gallery_type.dart";
  13. import 'package:photos/services/collections_service.dart';
  14. import "package:photos/services/feature_flag_service.dart";
  15. import "package:photos/services/object_detection/object_detection_service.dart";
  16. import "package:photos/theme/colors.dart";
  17. import 'package:photos/theme/ente_theme.dart';
  18. import "package:photos/ui/components/buttons/chip_button_widget.dart";
  19. import 'package:photos/ui/components/buttons/icon_button_widget.dart';
  20. import "package:photos/ui/components/buttons/inline_button_widget.dart";
  21. import 'package:photos/ui/components/divider_widget.dart';
  22. import "package:photos/ui/components/info_item_widget.dart";
  23. import 'package:photos/ui/components/title_bar_widget.dart';
  24. import "package:photos/ui/viewer/file/exif_info_dialog.dart";
  25. import 'package:photos/ui/viewer/file/file_caption_widget.dart';
  26. import "package:photos/ui/viewer/file_details/creation_time_item_widget.dart";
  27. import "package:photos/ui/viewer/file_details/file_properties_item_widget.dart";
  28. import "package:photos/ui/viewer/gallery/collection_page.dart";
  29. import "package:photos/utils/date_time_util.dart";
  30. import "package:photos/utils/exif_util.dart";
  31. import "package:photos/utils/navigation_util.dart";
  32. import "package:photos/utils/thumbnail_util.dart";
  33. import "package:photos/utils/toast_util.dart";
  34. class FileDetailsWidget extends StatefulWidget {
  35. final File file;
  36. const FileDetailsWidget(
  37. this.file, {
  38. Key? key,
  39. }) : super(key: key);
  40. @override
  41. State<FileDetailsWidget> createState() => _FileDetailsWidgetState();
  42. }
  43. class _FileDetailsWidgetState extends State<FileDetailsWidget> {
  44. Map<String, IfdTag>? _exif;
  45. final Map<String, dynamic> _exifData = {
  46. "focalLength": null,
  47. "fNumber": null,
  48. "resolution": null,
  49. "takenOnDevice": null,
  50. "exposureTime": null,
  51. "ISO": null,
  52. "megaPixels": null
  53. };
  54. bool _isImage = false;
  55. late int _currentUserID;
  56. @override
  57. void initState() {
  58. debugPrint('file_details_sheet initState');
  59. _currentUserID = Configuration.instance.getUserID()!;
  60. _isImage = widget.file.fileType == FileType.image ||
  61. widget.file.fileType == FileType.livePhoto;
  62. if (_isImage) {
  63. getExif(widget.file).then((exif) {
  64. if (mounted) {
  65. setState(() {
  66. _exif = exif;
  67. });
  68. }
  69. });
  70. }
  71. super.initState();
  72. }
  73. @override
  74. Widget build(BuildContext context) {
  75. final subtitleTextTheme = getEnteTextTheme(context).smallMuted;
  76. final file = widget.file;
  77. final fileIsBackedup = file.uploadedFileID == null ? false : true;
  78. final bool isFileOwner =
  79. file.ownerID == null || file.ownerID == _currentUserID;
  80. late Future<Set<int>> allCollectionIDsOfFile;
  81. //Typing this as Future<Set<T>> as it would be easier to implement showing multiple device folders for a file in the future
  82. final Future<Set<String>> allDeviceFoldersOfFile =
  83. Future.sync(() => {file.deviceFolder ?? ''});
  84. if (fileIsBackedup) {
  85. allCollectionIDsOfFile = FilesDB.instance.getAllCollectionIDsOfFile(
  86. file.uploadedFileID!,
  87. );
  88. }
  89. final dateTimeForUpdationTime =
  90. DateTime.fromMicrosecondsSinceEpoch(file.updationTime!);
  91. if (_isImage && _exif != null) {
  92. _generateExifForDetails(_exif!);
  93. }
  94. final bool showExifListTile = _exifData["focalLength"] != null ||
  95. _exifData["fNumber"] != null ||
  96. _exifData["takenOnDevice"] != null ||
  97. _exifData["exposureTime"] != null ||
  98. _exifData["ISO"] != null;
  99. final fileDetailsTiles = <Widget?>[
  100. !widget.file.isUploaded ||
  101. (!isFileOwner && (widget.file.caption?.isEmpty ?? true))
  102. ? const SizedBox(height: 16)
  103. : Padding(
  104. padding: const EdgeInsets.only(top: 8, bottom: 24),
  105. child: isFileOwner
  106. ? FileCaptionWidget(file: widget.file)
  107. : FileCaptionReadyOnly(caption: widget.file.caption!),
  108. ),
  109. CreationTimeItem(file, _currentUserID),
  110. FilePropertiesWidget(file, _isImage, _exifData, _currentUserID),
  111. showExifListTile
  112. ? InfoItemWidget(
  113. key: const ValueKey("Basic EXIF"),
  114. leadingIcon: Icons.camera_outlined,
  115. title: _exifData["takenOnDevice"] ?? "--",
  116. subtitleSection: Future.value([
  117. if (_exifData["fNumber"] != null)
  118. Text(
  119. 'ƒ/' + _exifData["fNumber"].toString(),
  120. style: subtitleTextTheme,
  121. ),
  122. if (_exifData["exposureTime"] != null)
  123. Text(
  124. _exifData["exposureTime"],
  125. style: subtitleTextTheme,
  126. ),
  127. if (_exifData["focalLength"] != null)
  128. Text(
  129. _exifData["focalLength"].toString() + "mm",
  130. style: subtitleTextTheme,
  131. ),
  132. if (_exifData["ISO"] != null)
  133. Text(
  134. "ISO" + _exifData["ISO"].toString(),
  135. style: subtitleTextTheme,
  136. ),
  137. ]),
  138. )
  139. : null,
  140. _isImage
  141. ? InfoItemWidget(
  142. leadingIcon: Icons.text_snippet_outlined,
  143. title: "EXIF",
  144. subtitleSection: _exifButton(file, _exif),
  145. )
  146. : null,
  147. FeatureFlagService.instance.isInternalUserOrDebugBuild()
  148. ? InfoItemWidget(
  149. key: const ValueKey("Objects"),
  150. leadingIcon: Icons.image_search_outlined,
  151. title: "Objects",
  152. subtitleSection: _objectTags(file),
  153. hasChipButtons: true,
  154. )
  155. : null,
  156. (file.uploadedFileID != null && file.updationTime != null)
  157. ? InfoItemWidget(
  158. key: const ValueKey("Backedup date"),
  159. leadingIcon: Icons.backup_outlined,
  160. title: getFullDate(
  161. DateTime.fromMicrosecondsSinceEpoch(file.updationTime!),
  162. ),
  163. subtitleSection: Future.value([
  164. Text(
  165. getTimeIn12hrFormat(dateTimeForUpdationTime) +
  166. " " +
  167. dateTimeForUpdationTime.timeZoneName,
  168. style: subtitleTextTheme,
  169. ),
  170. ]),
  171. )
  172. : null,
  173. InfoItemWidget(
  174. key: const ValueKey("Albums"),
  175. leadingIcon: Icons.folder_outlined,
  176. title: "Albums",
  177. subtitleSection: fileIsBackedup
  178. ? _collectionsListOfFile(allCollectionIDsOfFile, _currentUserID!)
  179. : _deviceFoldersListOfFile(allDeviceFoldersOfFile),
  180. hasChipButtons: true,
  181. ),
  182. ];
  183. fileDetailsTiles.removeWhere(
  184. (element) => element == null,
  185. );
  186. return SafeArea(
  187. top: false,
  188. child: Scrollbar(
  189. thickness: 4,
  190. radius: const Radius.circular(2),
  191. thumbVisibility: true,
  192. child: Padding(
  193. padding: const EdgeInsets.all(8.0),
  194. child: CustomScrollView(
  195. physics: const ClampingScrollPhysics(),
  196. shrinkWrap: true,
  197. slivers: <Widget>[
  198. TitleBarWidget(
  199. isFlexibleSpaceDisabled: true,
  200. title: "Details",
  201. isOnTopOfScreen: false,
  202. backgroundColor: getEnteColorScheme(context).backgroundElevated,
  203. leading: IconButtonWidget(
  204. icon: Icons.expand_more_outlined,
  205. iconButtonType: IconButtonType.primary,
  206. onTap: () => Navigator.pop(context),
  207. ),
  208. ),
  209. SliverToBoxAdapter(child: addedBy(widget.file)),
  210. SliverList(
  211. delegate: SliverChildBuilderDelegate(
  212. (context, index) {
  213. //Dividers occupy odd indexes
  214. if (index.isOdd) {
  215. return index == 1
  216. ? const SizedBox.shrink()
  217. : const Padding(
  218. padding: EdgeInsets.symmetric(vertical: 15.5),
  219. child: DividerWidget(
  220. dividerType: DividerType.menu,
  221. divColorHasBlur: false,
  222. ),
  223. );
  224. } else {
  225. return fileDetailsTiles[index ~/ 2];
  226. }
  227. },
  228. childCount: (fileDetailsTiles.length * 2) - 1,
  229. ),
  230. )
  231. ],
  232. ),
  233. ),
  234. ),
  235. );
  236. }
  237. Future<List<InlineButtonWidget>> _exifButton(
  238. File file,
  239. Map<String, IfdTag>? exif,
  240. ) {
  241. late final String label;
  242. late final VoidCallback? onTap;
  243. if (exif == null) {
  244. label = "Loading EXIF data...";
  245. onTap = null;
  246. } else if (exif.isNotEmpty) {
  247. label = "View all EXIF data";
  248. onTap = () => showDialog(
  249. context: context,
  250. builder: (BuildContext context) {
  251. return ExifInfoDialog(file);
  252. },
  253. barrierColor: backdropFaintDark,
  254. );
  255. } else {
  256. label = "No EXIF data";
  257. onTap = () => showShortToast(context, "This image has no exif data");
  258. }
  259. return Future.value([
  260. InlineButtonWidget(
  261. label,
  262. onTap,
  263. )
  264. ]);
  265. }
  266. Future<List<ChipButtonWidget>> _objectTags(File file) async {
  267. try {
  268. final chipButtons = <ChipButtonWidget>[];
  269. final objectTags = await getThumbnail(file).then((data) {
  270. return ObjectDetectionService.instance.predict(data!);
  271. });
  272. for (String objectTag in objectTags) {
  273. chipButtons.add(ChipButtonWidget(objectTag));
  274. }
  275. if (chipButtons.isEmpty) {
  276. return const [
  277. ChipButtonWidget(
  278. "No results",
  279. noChips: true,
  280. )
  281. ];
  282. }
  283. return chipButtons;
  284. } catch (e, s) {
  285. Logger("FileInfoWidget").info(e, s);
  286. return [];
  287. }
  288. }
  289. Future<List<ChipButtonWidget>> _deviceFoldersListOfFile(
  290. Future<Set<String>> allDeviceFoldersOfFile,
  291. ) async {
  292. try {
  293. final chipButtons = <ChipButtonWidget>[];
  294. final List<String> deviceFolders =
  295. (await allDeviceFoldersOfFile).toList();
  296. for (var deviceFolder in deviceFolders) {
  297. chipButtons.add(
  298. ChipButtonWidget(
  299. deviceFolder,
  300. ),
  301. );
  302. }
  303. return chipButtons;
  304. } catch (e, s) {
  305. Logger("FileInfoWidget").info(e, s);
  306. return [];
  307. }
  308. }
  309. Future<List<ChipButtonWidget>> _collectionsListOfFile(
  310. Future<Set<int>> allCollectionIDsOfFile,
  311. int currentUserID,
  312. ) async {
  313. try {
  314. final chipButtons = <ChipButtonWidget>[];
  315. final Set<int> collectionIDs = await allCollectionIDsOfFile;
  316. final collections = <Collection>[];
  317. for (var collectionID in collectionIDs) {
  318. final c = CollectionsService.instance.getCollectionByID(collectionID);
  319. collections.add(c!);
  320. chipButtons.add(
  321. ChipButtonWidget(
  322. c.isHidden() ? "Hidden" : c.name,
  323. onTap: () {
  324. if (c.isHidden()) {
  325. return;
  326. }
  327. routeToPage(
  328. context,
  329. CollectionPage(
  330. CollectionWithThumbnail(c, null),
  331. appBarType: c.isOwner(currentUserID)
  332. ? GalleryType.ownedCollection
  333. : GalleryType.sharedCollection,
  334. ),
  335. );
  336. },
  337. ),
  338. );
  339. }
  340. return chipButtons;
  341. } catch (e, s) {
  342. Logger("FileInfoWidget").info(e, s);
  343. return [];
  344. }
  345. }
  346. Widget addedBy(File file) {
  347. if (file.uploadedFileID == null) {
  348. return const SizedBox.shrink();
  349. }
  350. String? addedBy;
  351. if (file.ownerID == _currentUserID) {
  352. if (file.pubMagicMetadata!.uploaderName != null) {
  353. addedBy = file.pubMagicMetadata!.uploaderName;
  354. }
  355. } else {
  356. final fileOwner = CollectionsService.instance
  357. .getFileOwner(file.ownerID!, file.collectionID);
  358. addedBy = fileOwner.email;
  359. }
  360. if (addedBy == null || addedBy.isEmpty) {
  361. return const SizedBox.shrink();
  362. }
  363. final enteTheme = Theme.of(context).colorScheme.enteTheme;
  364. return Padding(
  365. padding: const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 16),
  366. child: Text(
  367. "Added by $addedBy",
  368. style: enteTheme.textTheme.mini
  369. .copyWith(color: enteTheme.colorScheme.textMuted),
  370. ),
  371. );
  372. }
  373. _generateExifForDetails(Map<String, IfdTag> exif) {
  374. if (exif["EXIF FocalLength"] != null) {
  375. _exifData["focalLength"] =
  376. (exif["EXIF FocalLength"]!.values.toList()[0] as Ratio).numerator /
  377. (exif["EXIF FocalLength"]!.values.toList()[0] as Ratio)
  378. .denominator;
  379. }
  380. if (exif["EXIF FNumber"] != null) {
  381. _exifData["fNumber"] =
  382. (exif["EXIF FNumber"]!.values.toList()[0] as Ratio).numerator /
  383. (exif["EXIF FNumber"]!.values.toList()[0] as Ratio).denominator;
  384. }
  385. final imageWidth = exif["EXIF ExifImageWidth"] ?? exif["Image ImageWidth"];
  386. final imageLength = exif["EXIF ExifImageLength"] ??
  387. exif["Image "
  388. "ImageLength"];
  389. if (imageWidth != null && imageLength != null) {
  390. _exifData["resolution"] = '$imageWidth x $imageLength';
  391. _exifData['megaPixels'] =
  392. ((imageWidth.values.firstAsInt() * imageLength.values.firstAsInt()) /
  393. 1000000)
  394. .toStringAsFixed(1);
  395. } else {
  396. debugPrint("No image width/height");
  397. }
  398. if (exif["Image Make"] != null && exif["Image Model"] != null) {
  399. _exifData["takenOnDevice"] =
  400. exif["Image Make"].toString() + " " + exif["Image Model"].toString();
  401. }
  402. if (exif["EXIF ExposureTime"] != null) {
  403. _exifData["exposureTime"] = exif["EXIF ExposureTime"].toString();
  404. }
  405. if (exif["EXIF ISOSpeedRatings"] != null) {
  406. _exifData['ISO'] = exif["EXIF ISOSpeedRatings"].toString();
  407. }
  408. }
  409. }