file_info_dialog.dart 13 KB

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