file_info_dialog.dart 15 KB

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