file_info_dialog.dart 17 KB

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