file_info_dialog.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. import "package:exif/exif.dart";
  2. import "package:flutter/cupertino.dart";
  3. import "package:flutter/material.dart";
  4. import 'package:flutter_datetime_picker/flutter_datetime_picker.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/file.dart";
  9. import "package:photos/models/file_type.dart";
  10. import 'package:photos/ui/common/DividerWithPadding.dart';
  11. import 'package:photos/ui/viewer/file/collections_list_of_file_widget.dart';
  12. import 'package:photos/ui/viewer/file/device_folders_list_of_file_widget.dart';
  13. import 'package:photos/ui/viewer/file/raw_exif_button.dart';
  14. import "package:photos/utils/date_time_util.dart";
  15. import "package:photos/utils/exif_util.dart";
  16. import "package:photos/utils/file_util.dart";
  17. import "package:photos/utils/magic_util.dart";
  18. class FileInfoWidget extends StatefulWidget {
  19. final File file;
  20. const FileInfoWidget(
  21. this.file, {
  22. Key key,
  23. }) : super(key: key);
  24. @override
  25. State<FileInfoWidget> createState() => _FileInfoWidgetState();
  26. }
  27. class _FileInfoWidgetState extends State<FileInfoWidget> {
  28. Map<String, IfdTag> _exif;
  29. final Map<String, dynamic> _exifData = {
  30. "focalLength": null,
  31. "fNumber": null,
  32. "resolution": null,
  33. "takenOnDevice": null,
  34. "exposureTime": null,
  35. "ISO": null,
  36. "megaPixels": null
  37. };
  38. bool _isImage = false;
  39. @override
  40. void initState() {
  41. debugPrint('file_info_dialog initState');
  42. _isImage = widget.file.fileType == FileType.image ||
  43. widget.file.fileType == FileType.livePhoto;
  44. if (_isImage) {
  45. getExif(widget.file).then((exif) {
  46. setState(() {
  47. _exif = exif;
  48. });
  49. });
  50. }
  51. super.initState();
  52. }
  53. @override
  54. Widget build(BuildContext context) {
  55. final file = widget.file;
  56. final fileIsBackedup = file.uploadedFileID == null ? false : true;
  57. Future<Set<int>> allCollectionIDsOfFile;
  58. Future<Set<String>>
  59. allDeviceFoldersOfFile; //Typing this as Future<Set<T>> as it would be easier to implement showing multiple device folders for a file in the future
  60. if (fileIsBackedup) {
  61. allCollectionIDsOfFile = FilesDB.instance.getAllCollectionIDsOfFile(
  62. file.uploadedFileID,
  63. );
  64. } else {
  65. allDeviceFoldersOfFile = Future.sync(() => {file.deviceFolder});
  66. }
  67. final dateTime = DateTime.fromMicrosecondsSinceEpoch(file.creationTime);
  68. final dateTimeForUpdationTime =
  69. DateTime.fromMicrosecondsSinceEpoch(file.updationTime);
  70. if (_isImage && _exif != null) {
  71. _generateExifForDetails(_exif);
  72. }
  73. final bool showExifListTile = _exifData["focalLength"] != null ||
  74. _exifData["fNumber"] != null ||
  75. _exifData["takenOnDevice"] != null ||
  76. _exifData["exposureTime"] != null ||
  77. _exifData["ISO"] != null;
  78. final bool showDimension =
  79. _exifData["resolution"] != null && _exifData["megaPixels"] != null;
  80. final listTiles = <Widget>[
  81. ListTile(
  82. leading: const Padding(
  83. padding: EdgeInsets.only(top: 8, left: 6),
  84. child: Icon(Icons.calendar_today_rounded),
  85. ),
  86. title: Text(
  87. getFullDate(
  88. DateTime.fromMicrosecondsSinceEpoch(file.creationTime),
  89. ),
  90. ),
  91. subtitle: Text(
  92. getTimeIn12hrFormat(dateTime) + " " + dateTime.timeZoneName,
  93. style: Theme.of(context).textTheme.bodyText2.copyWith(
  94. color: Theme.of(context)
  95. .colorScheme
  96. .defaultTextColor
  97. .withOpacity(0.5),
  98. ),
  99. ),
  100. trailing: (widget.file.ownerID == null ||
  101. widget.file.ownerID ==
  102. Configuration.instance.getUserID()) &&
  103. widget.file.uploadedFileID != null
  104. ? IconButton(
  105. onPressed: () {
  106. _showDateTimePicker(widget.file);
  107. },
  108. icon: const Icon(Icons.edit),
  109. )
  110. : const SizedBox.shrink(),
  111. ),
  112. const DividerWithPadding(left: 70, right: 20),
  113. ListTile(
  114. leading: _isImage
  115. ? const Padding(
  116. padding: EdgeInsets.only(top: 8, left: 6),
  117. child: Icon(
  118. Icons.image,
  119. ),
  120. )
  121. : const Padding(
  122. padding: EdgeInsets.only(top: 8, left: 6),
  123. child: Icon(
  124. Icons.video_camera_back,
  125. size: 27,
  126. ),
  127. ),
  128. title: Text(
  129. file.getDisplayName(),
  130. ),
  131. subtitle: Row(
  132. children: [
  133. showDimension
  134. ? Text(
  135. "${_exifData["megaPixels"]}MP "
  136. "${_exifData["resolution"]} ",
  137. )
  138. : const SizedBox.shrink(),
  139. _getFileSize(),
  140. (file.fileType == FileType.video) &&
  141. (file.localID != null || file.duration != 0)
  142. ? Padding(
  143. padding: const EdgeInsets.only(left: 8.0),
  144. child: _getVideoDuration(),
  145. )
  146. : const SizedBox.shrink(),
  147. ],
  148. ),
  149. trailing: file.uploadedFileID == null ||
  150. file.ownerID != Configuration.instance.getUserID()
  151. ? const SizedBox.shrink()
  152. : IconButton(
  153. onPressed: () async {
  154. await editFilename(context, file);
  155. setState(() {});
  156. },
  157. icon: const Icon(Icons.edit),
  158. ),
  159. ),
  160. const DividerWithPadding(left: 70, right: 20),
  161. showExifListTile
  162. ? ListTile(
  163. leading: const Padding(
  164. padding: EdgeInsets.only(left: 6),
  165. child: Icon(Icons.camera_rounded),
  166. ),
  167. title: Text(_exifData["takenOnDevice"] ?? "--"),
  168. subtitle: Row(
  169. children: [
  170. _exifData["fNumber"] != null
  171. ? Padding(
  172. padding: const EdgeInsets.only(right: 10),
  173. child: Text('ƒ/' + _exifData["fNumber"].toString()),
  174. )
  175. : const SizedBox.shrink(),
  176. _exifData["exposureTime"] != null
  177. ? Padding(
  178. padding: const EdgeInsets.only(right: 10),
  179. child: Text(_exifData["exposureTime"]),
  180. )
  181. : const SizedBox.shrink(),
  182. _exifData["focalLength"] != null
  183. ? Padding(
  184. padding: const EdgeInsets.only(right: 10),
  185. child:
  186. Text(_exifData["focalLength"].toString() + "mm"),
  187. )
  188. : const SizedBox.shrink(),
  189. _exifData["ISO"] != null
  190. ? Padding(
  191. padding: const EdgeInsets.only(right: 10),
  192. child: Text("ISO" + _exifData["ISO"].toString()),
  193. )
  194. : const SizedBox.shrink(),
  195. ],
  196. ),
  197. )
  198. : const SizedBox.shrink(),
  199. showExifListTile
  200. ? const DividerWithPadding(left: 70, right: 20)
  201. : const SizedBox.shrink(),
  202. SizedBox(
  203. height: 62,
  204. child: ListTile(
  205. leading: const Padding(
  206. padding: EdgeInsets.only(left: 6),
  207. child: Icon(Icons.folder_outlined),
  208. ),
  209. title: fileIsBackedup
  210. ? CollectionsListOfFile(allCollectionIDsOfFile)
  211. : DeviceFoldersListOfFile(allDeviceFoldersOfFile),
  212. ),
  213. ),
  214. const DividerWithPadding(left: 70, right: 20),
  215. (file.uploadedFileID != null && file.updationTime != null)
  216. ? ListTile(
  217. leading: const Padding(
  218. padding: EdgeInsets.only(top: 8, left: 6),
  219. child: Icon(Icons.cloud_upload_outlined),
  220. ),
  221. title: Text(
  222. getFullDate(
  223. DateTime.fromMicrosecondsSinceEpoch(file.updationTime),
  224. ),
  225. ),
  226. subtitle: Text(
  227. getTimeIn12hrFormat(dateTimeForUpdationTime) +
  228. " " +
  229. dateTimeForUpdationTime.timeZoneName,
  230. style: Theme.of(context).textTheme.bodyText2.copyWith(
  231. color: Theme.of(context)
  232. .colorScheme
  233. .defaultTextColor
  234. .withOpacity(0.5),
  235. ),
  236. ),
  237. )
  238. : const SizedBox.shrink(),
  239. _isImage
  240. ? Padding(
  241. padding: const EdgeInsets.fromLTRB(0, 24, 0, 16),
  242. child: SafeArea(
  243. child: RawExifButton(_exif, widget.file),
  244. ),
  245. )
  246. : const SizedBox(
  247. height: 12,
  248. )
  249. ];
  250. return Column(
  251. mainAxisSize: MainAxisSize.min,
  252. children: [
  253. Padding(
  254. padding: const EdgeInsets.all(10),
  255. child: Row(
  256. crossAxisAlignment: CrossAxisAlignment.center,
  257. children: [
  258. IconButton(
  259. onPressed: () {
  260. Navigator.pop(context);
  261. },
  262. icon: const Icon(
  263. Icons.close,
  264. ),
  265. ),
  266. const SizedBox(width: 6),
  267. Padding(
  268. padding: const EdgeInsets.only(bottom: 2),
  269. child: Text(
  270. "Details",
  271. style: Theme.of(context).textTheme.bodyText1,
  272. ),
  273. ),
  274. ],
  275. ),
  276. ),
  277. ...listTiles
  278. ],
  279. );
  280. }
  281. _generateExifForDetails(Map<String, IfdTag> exif) {
  282. if (exif["EXIF FocalLength"] != null) {
  283. _exifData["focalLength"] =
  284. (exif["EXIF FocalLength"].values.toList()[0] as Ratio).numerator /
  285. (exif["EXIF FocalLength"].values.toList()[0] as Ratio)
  286. .denominator;
  287. }
  288. if (exif["EXIF FNumber"] != null) {
  289. _exifData["fNumber"] =
  290. (exif["EXIF FNumber"].values.toList()[0] as Ratio).numerator /
  291. (exif["EXIF FNumber"].values.toList()[0] as Ratio).denominator;
  292. }
  293. final imageWidth = exif["EXIF ExifImageWidth"] ?? exif["Image ImageWidth"];
  294. final imageLength = exif["EXIF ExifImageLength"] ??
  295. exif["Image "
  296. "ImageLength"];
  297. if (imageWidth != null && imageLength != null) {
  298. _exifData["resolution"] = '$imageWidth x $imageLength';
  299. _exifData['megaPixels'] =
  300. ((imageWidth.values.firstAsInt() * imageLength.values.firstAsInt()) /
  301. 1000000)
  302. .toStringAsFixed(1);
  303. } else {
  304. debugPrint("No image width/height");
  305. }
  306. if (exif["Image Make"] != null && exif["Image Model"] != null) {
  307. _exifData["takenOnDevice"] =
  308. exif["Image Make"].toString() + " " + exif["Image Model"].toString();
  309. }
  310. if (exif["EXIF ExposureTime"] != null) {
  311. _exifData["exposureTime"] = exif["EXIF ExposureTime"].toString();
  312. }
  313. if (exif["EXIF ISOSpeedRatings"] != null) {
  314. _exifData['ISO'] = exif["EXIF ISOSpeedRatings"].toString();
  315. }
  316. }
  317. Widget _getFileSize() {
  318. return FutureBuilder(
  319. future: getFile(widget.file).then((f) => f.length()),
  320. builder: (context, snapshot) {
  321. if (snapshot.hasData) {
  322. return Text(
  323. (snapshot.data / (1024 * 1024)).toStringAsFixed(2) + " MB",
  324. );
  325. } else {
  326. return Center(
  327. child: SizedBox.fromSize(
  328. size: const Size.square(24),
  329. child: const CupertinoActivityIndicator(
  330. radius: 8,
  331. ),
  332. ),
  333. );
  334. }
  335. },
  336. );
  337. }
  338. Widget _getVideoDuration() {
  339. if (widget.file.duration != 0) {
  340. return Text(
  341. secondsToHHMMSS(widget.file.duration),
  342. );
  343. }
  344. return FutureBuilder(
  345. future: widget.file.getAsset(),
  346. builder: (context, snapshot) {
  347. if (snapshot.hasData) {
  348. return Text(
  349. snapshot.data.videoDuration.toString().split(".")[0],
  350. );
  351. } else {
  352. return Center(
  353. child: SizedBox.fromSize(
  354. size: const Size.square(24),
  355. child: const CupertinoActivityIndicator(
  356. radius: 8,
  357. ),
  358. ),
  359. );
  360. }
  361. },
  362. );
  363. }
  364. void _showDateTimePicker(File file) async {
  365. final dateResult = await DatePicker.showDatePicker(
  366. context,
  367. minTime: DateTime(1800, 1, 1),
  368. maxTime: DateTime.now(),
  369. currentTime: DateTime.fromMicrosecondsSinceEpoch(file.creationTime),
  370. locale: LocaleType.en,
  371. theme: Theme.of(context).colorScheme.dateTimePickertheme,
  372. );
  373. if (dateResult == null) {
  374. return;
  375. }
  376. final dateWithTimeResult = await DatePicker.showTime12hPicker(
  377. context,
  378. showTitleActions: true,
  379. currentTime: dateResult,
  380. locale: LocaleType.en,
  381. theme: Theme.of(context).colorScheme.dateTimePickertheme,
  382. );
  383. if (dateWithTimeResult != null) {
  384. if (await editTime(
  385. context,
  386. List.of([widget.file]),
  387. dateWithTimeResult.microsecondsSinceEpoch,
  388. )) {
  389. widget.file.creationTime = dateWithTimeResult.microsecondsSinceEpoch;
  390. setState(() {});
  391. }
  392. }
  393. }
  394. }