file_details_widget.dart 12 KB

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