file_info_dialog.dart 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. import "dart:io";
  2. import "package:exif/exif.dart";
  3. import "package:flutter/cupertino.dart";
  4. import "package:flutter/material.dart";
  5. import "package:photos/core/configuration.dart";
  6. import "package:photos/ente_theme_data.dart";
  7. import "package:photos/models/file.dart";
  8. import "package:photos/models/file_type.dart";
  9. import "package:photos/services/collections_service.dart";
  10. import "package:photos/ui/viewer/file/exif_info_dialog.dart";
  11. import "package:photos/utils/date_time_util.dart";
  12. import "package:photos/utils/exif_util.dart";
  13. import "package:photos/utils/file_util.dart";
  14. import "package:photos/utils/magic_util.dart";
  15. import "package:photos/utils/toast_util.dart";
  16. class FileInfoWidget extends StatefulWidget {
  17. final File file;
  18. const FileInfoWidget(
  19. this.file, {
  20. Key key,
  21. }) : super(key: key);
  22. @override
  23. State<FileInfoWidget> createState() => _FileInfoWidgetState();
  24. }
  25. class _FileInfoWidgetState extends State<FileInfoWidget> {
  26. Map<String, IfdTag> _exif;
  27. final Map<String, dynamic> _exifData = {
  28. "focalLength": null,
  29. "fNumber": null,
  30. "resolution": null,
  31. "takenOnDevice": null,
  32. "exposureTime": null,
  33. "ISO": null,
  34. "megaPixels": null
  35. };
  36. bool _isImage = false;
  37. Color infoColor;
  38. @override
  39. void initState() {
  40. _isImage = widget.file.fileType == FileType.image ||
  41. widget.file.fileType == FileType.livePhoto;
  42. if (_isImage) {
  43. getExif(widget.file).then((exif) {
  44. setState(() {
  45. _exif = exif;
  46. });
  47. });
  48. }
  49. super.initState();
  50. }
  51. @override
  52. Widget build(BuildContext context) {
  53. final file = widget.file;
  54. final dateTime = DateTime.fromMicrosecondsSinceEpoch(file.creationTime);
  55. infoColor = Theme.of(context).colorScheme.onSurface.withOpacity(0.85);
  56. var listTiles = <Widget>[
  57. ListTile(
  58. leading: const Padding(
  59. padding: EdgeInsets.only(top: 8, left: 6),
  60. child: Icon(Icons.calendar_today_rounded),
  61. ),
  62. title: Text(
  63. getFullDate(
  64. DateTime.fromMicrosecondsSinceEpoch(file.creationTime),
  65. ),
  66. ),
  67. subtitle: Text(
  68. getTimeIn12hrFormat(dateTime) + " " + dateTime.timeZoneName,
  69. style: Theme.of(context)
  70. .textTheme
  71. .bodyText2
  72. .copyWith(color: Colors.black.withOpacity(0.5)),
  73. ),
  74. trailing: (widget.file.ownerID == null ||
  75. widget.file.ownerID ==
  76. Configuration.instance.getUserID()) &&
  77. widget.file.uploadedFileID != null
  78. ? IconButton(
  79. onPressed: () {
  80. PopupMenuItem(
  81. value: 2,
  82. child: Row(
  83. children: [
  84. Icon(
  85. Platform.isAndroid
  86. ? Icons.access_time_rounded
  87. : CupertinoIcons.time,
  88. color: Theme.of(context).iconTheme.color,
  89. ),
  90. const Padding(
  91. padding: EdgeInsets.all(8),
  92. ),
  93. const Text("Edit time"),
  94. ],
  95. ),
  96. );
  97. },
  98. icon: const Icon(Icons.edit),
  99. )
  100. : const SizedBox.shrink(),
  101. ),
  102. const DividerWithPadding(),
  103. ListTile(
  104. leading: const Padding(
  105. padding: EdgeInsets.only(top: 8, left: 6),
  106. child: Icon(
  107. Icons.image,
  108. ),
  109. ),
  110. title: Text(
  111. file.getDisplayName(),
  112. ),
  113. subtitle: Row(
  114. children: [
  115. _getFileSize(),
  116. ],
  117. ),
  118. trailing: file.uploadedFileID == null ||
  119. file.ownerID != Configuration.instance.getUserID()
  120. ? const SizedBox.shrink()
  121. : IconButton(
  122. onPressed: () async {
  123. await editFilename(context, file);
  124. setState(() {});
  125. },
  126. icon: const Icon(Icons.edit),
  127. ),
  128. ),
  129. const DividerWithPadding(),
  130. ListTile(
  131. leading: const Padding(
  132. padding: EdgeInsets.only(left: 6),
  133. child: Icon(Icons.folder_outlined),
  134. ),
  135. title: Text(
  136. file.deviceFolder ??
  137. CollectionsService.instance
  138. .getCollectionByID(file.collectionID)
  139. .name,
  140. ),
  141. )
  142. ];
  143. var items = <Widget>[
  144. Row(
  145. children: [
  146. Icon(Icons.calendar_today_outlined, color: infoColor),
  147. const SizedBox(height: 8),
  148. Text(
  149. getFormattedTime(
  150. DateTime.fromMicrosecondsSinceEpoch(file.creationTime),
  151. ),
  152. style: TextStyle(color: infoColor),
  153. ),
  154. ],
  155. ),
  156. const SizedBox(height: 12),
  157. Row(
  158. children: [
  159. Icon(Icons.folder_outlined, color: infoColor),
  160. const Padding(padding: EdgeInsets.all(4)),
  161. Text(
  162. file.deviceFolder ??
  163. CollectionsService.instance
  164. .getCollectionByID(file.collectionID)
  165. .name,
  166. style: TextStyle(color: infoColor),
  167. ),
  168. ],
  169. ),
  170. const SizedBox(height: 12),
  171. ];
  172. items.addAll(
  173. [
  174. Row(
  175. children: [
  176. Icon(Icons.sd_storage_outlined, color: infoColor),
  177. const Padding(padding: EdgeInsets.all(4)),
  178. _getFileSize(),
  179. ],
  180. ),
  181. const SizedBox(height: 12),
  182. ],
  183. );
  184. if (file.localID != null && !_isImage) {
  185. items.addAll(
  186. [
  187. Row(
  188. children: [
  189. Icon(Icons.timer_outlined, color: infoColor),
  190. const Padding(padding: EdgeInsets.all(4)),
  191. FutureBuilder(
  192. future: file.getAsset(),
  193. builder: (context, snapshot) {
  194. if (snapshot.hasData) {
  195. return Text(
  196. snapshot.data.videoDuration.toString().split(".")[0],
  197. style: TextStyle(color: infoColor),
  198. );
  199. } else {
  200. return Center(
  201. child: SizedBox.fromSize(
  202. size: const Size.square(24),
  203. child: const CupertinoActivityIndicator(
  204. radius: 8,
  205. ),
  206. ),
  207. );
  208. }
  209. },
  210. ),
  211. ],
  212. ),
  213. const SizedBox(height: 12),
  214. ],
  215. );
  216. }
  217. if (_isImage && _exif != null) {
  218. // items.add(_getExifWidgets(_exif));
  219. _generateExifDataforUI(_exif);
  220. }
  221. if (file.uploadedFileID != null && file.updationTime != null) {
  222. items.addAll(
  223. [
  224. Row(
  225. children: [
  226. Icon(Icons.cloud_upload_outlined, color: infoColor),
  227. const Padding(padding: EdgeInsets.all(4)),
  228. Text(
  229. getFormattedTime(
  230. DateTime.fromMicrosecondsSinceEpoch(file.updationTime),
  231. ),
  232. style: TextStyle(color: infoColor),
  233. ),
  234. ],
  235. ),
  236. ],
  237. );
  238. }
  239. items.add(
  240. const SizedBox(height: 12),
  241. );
  242. items.add(
  243. Row(
  244. mainAxisAlignment:
  245. _isImage ? MainAxisAlignment.spaceBetween : MainAxisAlignment.end,
  246. children: _getActions(),
  247. ),
  248. );
  249. Widget titleContent;
  250. if (file.uploadedFileID == null ||
  251. file.ownerID != Configuration.instance.getUserID()) {
  252. titleContent = Text(file.getDisplayName());
  253. } else {
  254. titleContent = InkWell(
  255. child: Row(
  256. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  257. children: [
  258. Flexible(
  259. child: Text(
  260. file.getDisplayName(),
  261. style: Theme.of(context).textTheme.headline5,
  262. ),
  263. ),
  264. const SizedBox(width: 16),
  265. Icon(Icons.edit, color: infoColor),
  266. ],
  267. ),
  268. onTap: () async {
  269. await editFilename(context, file);
  270. setState(() {});
  271. },
  272. );
  273. }
  274. // return AlertDialog(
  275. // title: titleContent,
  276. // content: SingleChildScrollView(
  277. // child: ListBody(
  278. // children: items,
  279. // ),
  280. // ),
  281. // );
  282. return Column(
  283. children: [
  284. Padding(
  285. padding: const EdgeInsets.all(10),
  286. child: Row(
  287. crossAxisAlignment: CrossAxisAlignment.center,
  288. children: [
  289. IconButton(
  290. onPressed: () {
  291. Navigator.pop(context);
  292. },
  293. icon: const Icon(
  294. Icons.close,
  295. ),
  296. ),
  297. const SizedBox(width: 6),
  298. Padding(
  299. padding: const EdgeInsets.only(bottom: 2),
  300. child: Text(
  301. "Details",
  302. style: Theme.of(context).textTheme.bodyText1,
  303. ),
  304. ),
  305. ],
  306. ),
  307. ),
  308. ...listTiles
  309. ],
  310. );
  311. }
  312. List<Widget> _getActions() {
  313. final List<Widget> actions = [];
  314. if (_isImage) {
  315. if (_exif == null) {
  316. actions.add(
  317. TextButton(
  318. child: Row(
  319. mainAxisAlignment: MainAxisAlignment.spaceAround,
  320. children: [
  321. Center(
  322. child: SizedBox.fromSize(
  323. size: const Size.square(24),
  324. child: const CupertinoActivityIndicator(
  325. radius: 8,
  326. ),
  327. ),
  328. ),
  329. const Padding(padding: EdgeInsets.all(4)),
  330. Text(
  331. "EXIF",
  332. style: TextStyle(color: infoColor),
  333. ),
  334. ],
  335. ),
  336. onPressed: () {
  337. showDialog(
  338. context: context,
  339. builder: (BuildContext context) {
  340. return ExifInfoDialog(widget.file);
  341. },
  342. barrierColor: Colors.black87,
  343. );
  344. },
  345. ),
  346. );
  347. } else if (_exif.isNotEmpty) {
  348. actions.add(
  349. TextButton(
  350. child: Row(
  351. mainAxisAlignment: MainAxisAlignment.spaceAround,
  352. children: [
  353. Icon(Icons.feed_outlined, color: infoColor),
  354. const Padding(padding: EdgeInsets.all(4)),
  355. Text(
  356. "View raw EXIF",
  357. style: TextStyle(color: infoColor),
  358. ),
  359. ],
  360. ),
  361. onPressed: () {
  362. showDialog(
  363. context: context,
  364. builder: (BuildContext context) {
  365. return ExifInfoDialog(widget.file);
  366. },
  367. barrierColor: Colors.black87,
  368. );
  369. },
  370. ),
  371. );
  372. } else {
  373. actions.add(
  374. TextButton(
  375. child: Row(
  376. mainAxisAlignment: MainAxisAlignment.spaceAround,
  377. children: [
  378. Icon(
  379. Icons.feed_outlined,
  380. color: Theme.of(context)
  381. .colorScheme
  382. .defaultTextColor
  383. .withOpacity(0.5),
  384. ),
  385. const Padding(padding: EdgeInsets.all(4)),
  386. Text(
  387. "No exif",
  388. style: TextStyle(
  389. color: Theme.of(context)
  390. .colorScheme
  391. .defaultTextColor
  392. .withOpacity(0.5),
  393. ),
  394. ),
  395. ],
  396. ),
  397. onPressed: () {
  398. showShortToast(context, "This image has no exif data");
  399. },
  400. ),
  401. );
  402. }
  403. }
  404. actions.add(
  405. TextButton(
  406. child: Text(
  407. "Close",
  408. style: TextStyle(
  409. color: infoColor,
  410. ),
  411. ),
  412. onPressed: () {
  413. Navigator.of(context, rootNavigator: true).pop("dialog");
  414. },
  415. ),
  416. );
  417. return actions;
  418. }
  419. _generateExifDataforUI(Map<String, IfdTag> exif) {
  420. print(_exifData);
  421. // _exifData["focalLength"] = (exif["EXIF FocalLength"] != null
  422. // ? (exif["EXIF FocalLength"].values.toList()[0] as Ratio).numerator /
  423. // (exif["EXIF FocalLength"].values.toList()[0] as Ratio).denominator
  424. // : null);
  425. // _exifData["fNumber"] = (exif["EXIF FNumber"] != null
  426. // ? (exif["EXIF FNumber"].values.toList()[0] as Ratio).numerator /
  427. // (exif["EXIF FNumber"].values.toList()[0] as Ratio).denominator
  428. // : null);
  429. if (exif["EXIF FocalLength"] != null) {
  430. _exifData["focalLength"] =
  431. (exif["EXIF FocalLength"].values.toList()[0] as Ratio).numerator /
  432. (exif["EXIF FocalLength"].values.toList()[0] as Ratio)
  433. .denominator;
  434. }
  435. if (exif["EXIF FNumber"] != null) {
  436. _exifData["fNumber"] =
  437. (exif["EXIF FNumber"].values.toList()[0] as Ratio).numerator /
  438. (exif["EXIF FNumber"].values.toList()[0] as Ratio).denominator;
  439. }
  440. if (exif["EXIF ExifImageWidth"] != null &&
  441. exif["EXIF ExifImageLength"] != null) {
  442. _exifData["resolution"] = exif["EXIF ExifImageWidth"].toString() +
  443. " x " +
  444. exif["EXIF ExifImageLength"].toString();
  445. _exifData['megaPixels'] = ((exif["Image ImageWidth"].values.firstAsInt() *
  446. exif["Image ImageLength"].values.firstAsInt()) /
  447. 1000000)
  448. .toStringAsFixed(1);
  449. } else if (exif["Image ImageWidth"] != null &&
  450. exif["Image ImageLength"] != null) {
  451. _exifData["resolution"] = exif["Image ImageWidth"].toString() +
  452. " x " +
  453. exif["Image ImageLength"].toString();
  454. }
  455. if (exif["Image Make"] != null && exif["Image Model"] != null) {
  456. _exifData["takenOnDevice"] =
  457. exif["Image Make"].toString() + " " + exif["Image Model"].toString();
  458. }
  459. if (exif["EXIF ExposureTime"] != null) {
  460. _exifData["exposureTime"] = exif["EXIF ExposureTime"].toString();
  461. }
  462. if (exif["EXIF ISOSpeedRatings"] != null) {
  463. _exifData['ISO'] = exif["EXIF ISOSpeedRatings"].toString();
  464. }
  465. print(_exifData);
  466. }
  467. Widget _getExifWidgets(Map<String, IfdTag> exif) {
  468. final focalLength = exif["EXIF FocalLength"] != null
  469. ? (exif["EXIF FocalLength"].values.toList()[0] as Ratio).numerator /
  470. (exif["EXIF FocalLength"].values.toList()[0] as Ratio)
  471. .denominator //to remove
  472. : null;
  473. final fNumber = exif["EXIF FNumber"] != null
  474. ? (exif["EXIF FNumber"].values.toList()[0] as Ratio).numerator /
  475. (exif["EXIF FNumber"].values.toList()[0] as Ratio)
  476. .denominator //to remove
  477. : null;
  478. final List<Widget> children = [];
  479. if (exif["EXIF ExifImageWidth"] != null &&
  480. exif["EXIF ExifImageLength"] != null) {
  481. children.addAll([
  482. Row(
  483. children: [
  484. Icon(Icons.photo_size_select_actual_outlined, color: infoColor),
  485. const Padding(padding: EdgeInsets.all(4)),
  486. Text(
  487. exif["EXIF ExifImageWidth"].toString() +
  488. " x " +
  489. exif["EXIF ExifImageLength"].toString(),
  490. style: TextStyle(color: infoColor),
  491. ),
  492. ],
  493. ),
  494. const Padding(padding: EdgeInsets.all(6)),
  495. ]);
  496. } else if (exif["Image ImageWidth"] != null &&
  497. exif["Image ImageLength"] != null) {
  498. children.addAll([
  499. Row(
  500. children: [
  501. Icon(Icons.photo_size_select_actual_outlined, color: infoColor),
  502. const Padding(padding: EdgeInsets.all(4)),
  503. Text(
  504. exif["Image ImageWidth"].toString() +
  505. " x " +
  506. exif["Image ImageLength"].toString(),
  507. style: TextStyle(color: infoColor),
  508. ),
  509. ],
  510. ),
  511. const Padding(padding: EdgeInsets.all(6)),
  512. ]);
  513. }
  514. if (exif["Image Make"] != null && exif["Image Model"] != null) {
  515. children.addAll(
  516. [
  517. Row(
  518. children: [
  519. Icon(Icons.camera_outlined, color: infoColor),
  520. const Padding(padding: EdgeInsets.all(4)),
  521. Flexible(
  522. child: Text(
  523. exif["Image Make"].toString() +
  524. " " +
  525. exif["Image Model"].toString(),
  526. style: TextStyle(color: infoColor),
  527. overflow: TextOverflow.clip,
  528. ),
  529. ),
  530. ],
  531. ),
  532. const Padding(padding: EdgeInsets.all(6)),
  533. ],
  534. );
  535. }
  536. if (fNumber != null) {
  537. children.addAll([
  538. Row(
  539. children: [
  540. Icon(CupertinoIcons.f_cursive, color: infoColor),
  541. const Padding(padding: EdgeInsets.all(4)),
  542. Text(
  543. fNumber.toString(),
  544. style: TextStyle(color: infoColor),
  545. ),
  546. ],
  547. ),
  548. const Padding(padding: EdgeInsets.all(6)),
  549. ]);
  550. }
  551. if (focalLength != null) {
  552. children.addAll([
  553. Row(
  554. children: [
  555. Icon(Icons.center_focus_strong_outlined, color: infoColor),
  556. const Padding(padding: EdgeInsets.all(4)),
  557. Text(
  558. focalLength.toString() + " mm",
  559. style: TextStyle(color: infoColor),
  560. ),
  561. ],
  562. ),
  563. const Padding(padding: EdgeInsets.all(6)),
  564. ]);
  565. }
  566. if (exif["EXIF ExposureTime"] != null) {
  567. children.addAll([
  568. Row(
  569. children: [
  570. Icon(Icons.shutter_speed, color: infoColor),
  571. const Padding(padding: EdgeInsets.all(4)),
  572. Text(
  573. exif["EXIF ExposureTime"].toString(),
  574. style: TextStyle(color: infoColor),
  575. ),
  576. ],
  577. ),
  578. const Padding(padding: EdgeInsets.all(6)),
  579. ]);
  580. }
  581. return Column(
  582. children: children,
  583. );
  584. }
  585. Widget _getFileSize() {
  586. return FutureBuilder(
  587. future: getFile(widget.file).then((f) => f.length()),
  588. builder: (context, snapshot) {
  589. if (snapshot.hasData) {
  590. return Text(
  591. (snapshot.data / (1024 * 1024)).toStringAsFixed(2) + " MB",
  592. );
  593. } else {
  594. return Center(
  595. child: SizedBox.fromSize(
  596. size: const Size.square(24),
  597. child: const CupertinoActivityIndicator(
  598. radius: 8,
  599. ),
  600. ),
  601. );
  602. }
  603. },
  604. );
  605. }
  606. }
  607. class DividerWithPadding extends StatelessWidget {
  608. const DividerWithPadding({Key key}) : super(key: key);
  609. @override
  610. Widget build(BuildContext context) {
  611. return Padding(
  612. padding: const EdgeInsets.fromLTRB(70, 0, 20, 0),
  613. child: Divider(
  614. thickness: 0.5,
  615. ),
  616. );
  617. }
  618. }