file_info_dialog.dart 16 KB

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