image_editor_page.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. import 'dart:io';
  2. import 'dart:typed_data';
  3. import 'package:extended_image/extended_image.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:image_editor/image_editor.dart';
  6. import 'package:logging/logging.dart';
  7. import 'package:path/path.dart' as path;
  8. import 'package:photo_manager/photo_manager.dart';
  9. import 'package:photos/core/event_bus.dart';
  10. import 'package:photos/db/files_db.dart';
  11. import 'package:photos/events/local_photos_updated_event.dart';
  12. import 'package:photos/models/file.dart' as ente;
  13. import 'package:photos/models/location.dart';
  14. import 'package:photos/services/local_sync_service.dart';
  15. import 'package:photos/services/sync_service.dart';
  16. import 'package:photos/ui/detail_page.dart';
  17. import 'package:photos/ui/editor/filtered_image.dart';
  18. import 'package:photos/ui/loading_widget.dart';
  19. import 'package:photos/utils/dialog_util.dart';
  20. import 'package:photos/utils/navigation_util.dart';
  21. import 'package:photos/utils/toast_util.dart';
  22. import 'package:syncfusion_flutter_core/theme.dart';
  23. import 'package:syncfusion_flutter_sliders/sliders.dart';
  24. class ImageEditorPage extends StatefulWidget {
  25. final ImageProvider imageProvider;
  26. final DetailPageConfiguration detailPageConfig;
  27. final ente.File originalFile;
  28. const ImageEditorPage(
  29. this.imageProvider,
  30. this.originalFile,
  31. this.detailPageConfig, {
  32. Key key,
  33. }) : super(key: key);
  34. @override
  35. _ImageEditorPageState createState() => _ImageEditorPageState();
  36. }
  37. class _ImageEditorPageState extends State<ImageEditorPage> {
  38. static const double kBrightnessDefault = 1;
  39. static const double kBrightnessMin = 0;
  40. static const double kBrightnessMax = 2;
  41. static const double kSaturationDefault = 1;
  42. static const double kSaturationMin = 0;
  43. static const double kSaturationMax = 2;
  44. final _logger = Logger("ImageEditor");
  45. final GlobalKey<ExtendedImageEditorState> editorKey =
  46. GlobalKey<ExtendedImageEditorState>();
  47. double _brightness = kBrightnessDefault;
  48. double _saturation = kSaturationDefault;
  49. bool _hasEdited = false;
  50. @override
  51. Widget build(BuildContext context) {
  52. return WillPopScope(
  53. onWillPop: () async {
  54. if (_hasBeenEdited()) {
  55. await _showExitConfirmationDialog();
  56. } else {
  57. replacePage(context, DetailPage(widget.detailPageConfig));
  58. }
  59. return false;
  60. },
  61. child: Scaffold(
  62. appBar: AppBar(
  63. backgroundColor: Color(0x00000000),
  64. elevation: 0,
  65. actions: _hasBeenEdited()
  66. ? [
  67. IconButton(
  68. padding: const EdgeInsets.only(right: 16, left: 16),
  69. onPressed: () {
  70. editorKey.currentState.reset();
  71. setState(() {
  72. _brightness = kBrightnessDefault;
  73. _saturation = kSaturationDefault;
  74. });
  75. },
  76. icon: Icon(Icons.history),
  77. )
  78. ]
  79. : [],
  80. ),
  81. body: Column(
  82. children: [
  83. Expanded(child: _buildImage()),
  84. Padding(padding: EdgeInsets.all(4)),
  85. Column(
  86. children: [
  87. _buildBrightness(),
  88. _buildSat(),
  89. ],
  90. ),
  91. Padding(padding: EdgeInsets.all(8)),
  92. _buildBottomBar(),
  93. Padding(padding: EdgeInsets.all(Platform.isIOS ? 16 : 6)),
  94. ],
  95. ),
  96. ),
  97. );
  98. }
  99. bool _hasBeenEdited() {
  100. return _hasEdited ||
  101. _saturation != kSaturationDefault ||
  102. _brightness != kBrightnessDefault;
  103. }
  104. Widget _buildImage() {
  105. return Hero(
  106. tag: widget.detailPageConfig.tagPrefix + widget.originalFile.tag(),
  107. child: ExtendedImage(
  108. image: widget.imageProvider,
  109. extendedImageEditorKey: editorKey,
  110. mode: ExtendedImageMode.editor,
  111. fit: BoxFit.contain,
  112. initEditorConfigHandler: (_) => EditorConfig(
  113. maxScale: 8.0,
  114. cropRectPadding: const EdgeInsets.all(20.0),
  115. hitTestSize: 20.0,
  116. cornerColor: Color.fromRGBO(45, 150, 98, 1),
  117. editActionDetailsIsChanged: (_) {
  118. setState(() {
  119. _hasEdited = true;
  120. });
  121. },
  122. ),
  123. loadStateChanged: (state) {
  124. if (state.extendedImageLoadState == LoadState.completed) {
  125. return FilteredImage(
  126. child: state.completedWidget,
  127. brightness: _brightness,
  128. saturation: _saturation,
  129. );
  130. }
  131. return loadWidget;
  132. },
  133. ),
  134. );
  135. }
  136. Widget _buildBottomBar() {
  137. return Row(
  138. mainAxisAlignment: MainAxisAlignment.spaceAround,
  139. children: [
  140. _buildFlipButton(),
  141. _buildRotateLeftButton(),
  142. _buildRotateRightButton(),
  143. _buildSaveButton(),
  144. ],
  145. );
  146. }
  147. Widget _buildFlipButton() {
  148. return GestureDetector(
  149. behavior: HitTestBehavior.translucent,
  150. onTap: () {
  151. flip();
  152. },
  153. child: SizedBox(
  154. width: 80,
  155. child: Column(
  156. children: [
  157. Padding(
  158. padding: const EdgeInsets.only(bottom: 2),
  159. child: Icon(
  160. Icons.flip,
  161. color: Colors.white.withOpacity(0.8),
  162. size: 20,
  163. ),
  164. ),
  165. Padding(padding: EdgeInsets.all(2)),
  166. Text(
  167. "flip",
  168. style: TextStyle(
  169. color: Colors.white.withOpacity(0.6),
  170. fontSize: 12,
  171. ),
  172. textAlign: TextAlign.center,
  173. ),
  174. ],
  175. ),
  176. ),
  177. );
  178. }
  179. Widget _buildRotateLeftButton() {
  180. return GestureDetector(
  181. behavior: HitTestBehavior.translucent,
  182. onTap: () {
  183. rotate(false);
  184. },
  185. child: SizedBox(
  186. width: 80,
  187. child: Column(
  188. children: [
  189. Icon(Icons.rotate_left, color: Colors.white.withOpacity(0.8)),
  190. Padding(padding: EdgeInsets.all(2)),
  191. Text(
  192. "rotate left",
  193. style: TextStyle(
  194. color: Colors.white.withOpacity(0.6),
  195. fontSize: 12,
  196. ),
  197. textAlign: TextAlign.center,
  198. ),
  199. ],
  200. ),
  201. ),
  202. );
  203. }
  204. Widget _buildRotateRightButton() {
  205. return GestureDetector(
  206. behavior: HitTestBehavior.translucent,
  207. onTap: () {
  208. rotate(true);
  209. },
  210. child: SizedBox(
  211. width: 80,
  212. child: Column(
  213. children: [
  214. Icon(Icons.rotate_right, color: Colors.white.withOpacity(0.8)),
  215. Padding(padding: EdgeInsets.all(2)),
  216. Text(
  217. "rotate right",
  218. style: TextStyle(
  219. color: Colors.white.withOpacity(0.6),
  220. fontSize: 12,
  221. ),
  222. textAlign: TextAlign.center,
  223. ),
  224. ],
  225. ),
  226. ),
  227. );
  228. }
  229. Widget _buildSaveButton() {
  230. return GestureDetector(
  231. behavior: HitTestBehavior.translucent,
  232. onTap: () {
  233. _saveEdits();
  234. },
  235. child: SizedBox(
  236. width: 80,
  237. child: Column(
  238. children: [
  239. Icon(Icons.save_alt_outlined, color: Colors.white.withOpacity(0.8)),
  240. Padding(padding: EdgeInsets.all(2)),
  241. Text(
  242. "save copy",
  243. style: TextStyle(
  244. color: Colors.white.withOpacity(0.6),
  245. fontSize: 12,
  246. ),
  247. textAlign: TextAlign.center,
  248. ),
  249. ],
  250. ),
  251. ),
  252. );
  253. }
  254. Future<void> _saveEdits() async {
  255. final dialog = createProgressDialog(context, "saving...");
  256. await dialog.show();
  257. final ExtendedImageEditorState state = editorKey.currentState;
  258. if (state == null) {
  259. return;
  260. }
  261. final Rect rect = state.getCropRect();
  262. if (rect == null) {
  263. return;
  264. }
  265. final EditActionDetails action = state.editAction;
  266. final double radian = action.rotateAngle;
  267. final bool flipHorizontal = action.flipY;
  268. final bool flipVertical = action.flipX;
  269. final Uint8List img = state.rawImageData;
  270. if (img == null) {
  271. _logger.severe("null rawImageData");
  272. showToast("something went wrong");
  273. return;
  274. }
  275. final ImageEditorOption option = ImageEditorOption();
  276. option.addOption(ClipOption.fromRect(rect));
  277. option.addOption(
  278. FlipOption(horizontal: flipHorizontal, vertical: flipVertical));
  279. if (action.hasRotateAngle) {
  280. option.addOption(RotateOption(radian.toInt()));
  281. }
  282. option.addOption(ColorOption.saturation(_saturation));
  283. option.addOption(ColorOption.brightness(_brightness));
  284. option.outputFormat = const OutputFormat.png(88);
  285. final DateTime start = DateTime.now();
  286. final Uint8List result = await ImageEditor.editImage(
  287. image: img,
  288. imageEditorOption: option,
  289. );
  290. _logger.info('result.length = ${result?.length}');
  291. final Duration diff = DateTime.now().difference(start);
  292. _logger.info('image_editor time : $diff');
  293. if (result == null) {
  294. _logger.severe("null result");
  295. showToast("something went wrong");
  296. return;
  297. }
  298. try {
  299. final fileName =
  300. path.basenameWithoutExtension(widget.originalFile.title) +
  301. "_edited_" +
  302. DateTime.now().microsecondsSinceEpoch.toString() +
  303. path.extension(widget.originalFile.title);
  304. final newAsset = await PhotoManager.editor.saveImage(
  305. result,
  306. title: fileName,
  307. );
  308. final newFile =
  309. await ente.File.fromAsset(widget.originalFile.deviceFolder, newAsset);
  310. newFile.creationTime = widget.originalFile.creationTime;
  311. newFile.collectionID = widget.originalFile.collectionID;
  312. newFile.location = widget.originalFile.location;
  313. if (!newFile.hasLocation() && widget.originalFile.localID != null) {
  314. var assetEntity = await widget.originalFile.getAsset();
  315. if (assetEntity != null) {
  316. final latLong = await assetEntity.latlngAsync();
  317. newFile.location = Location(latLong.latitude, latLong.longitude);
  318. }
  319. }
  320. newFile.generatedID = await FilesDB.instance.insert(newFile);
  321. await LocalSyncService.instance.trackEditedFile(newFile);
  322. Bus.instance.fire(LocalPhotosUpdatedEvent([newFile]));
  323. SyncService.instance.sync();
  324. showToast("edits saved");
  325. _logger.info("Original file " + widget.originalFile.toString());
  326. _logger.info("Saved edits to file " + newFile.toString());
  327. final existingFiles = widget.detailPageConfig.files;
  328. final files = (await widget.detailPageConfig.asyncLoader(
  329. existingFiles[existingFiles.length - 1].creationTime,
  330. existingFiles[0].creationTime))
  331. .files;
  332. replacePage(
  333. context,
  334. DetailPage(
  335. widget.detailPageConfig.copyWith(
  336. files: files,
  337. selectedIndex: files
  338. .indexWhere((file) => file.generatedID == newFile.generatedID),
  339. ),
  340. ),
  341. );
  342. } catch (e, s) {
  343. showToast("oops, could not save edits");
  344. _logger.severe(e, s);
  345. }
  346. await dialog.hide();
  347. }
  348. void flip() {
  349. editorKey.currentState?.flip();
  350. }
  351. void rotate(bool right) {
  352. editorKey.currentState?.rotate(right: right);
  353. }
  354. Widget _buildSat() {
  355. return Container(
  356. padding: EdgeInsets.fromLTRB(20, 0, 20, 0),
  357. child: Row(
  358. children: [
  359. SizedBox(
  360. width: 40,
  361. child: Text(
  362. "color",
  363. style: TextStyle(
  364. color: Colors.white.withOpacity(0.8),
  365. fontSize: 14,
  366. ),
  367. ),
  368. ),
  369. Expanded(
  370. child: SfSliderTheme(
  371. data: SfSliderThemeData(
  372. activeTrackHeight: 4,
  373. inactiveTrackHeight: 2,
  374. inactiveTrackColor: Colors.grey[900],
  375. activeTrackColor: Color.fromRGBO(45, 150, 98, 1),
  376. thumbColor: Color.fromRGBO(45, 150, 98, 1),
  377. thumbRadius: 10,
  378. tooltipBackgroundColor: Colors.grey[900],
  379. ),
  380. child: SfSlider(
  381. onChanged: (value) {
  382. setState(() {
  383. _saturation = value;
  384. });
  385. },
  386. value: _saturation,
  387. enableTooltip: true,
  388. stepSize: 0.01,
  389. min: kSaturationMin,
  390. max: kSaturationMax,
  391. ),
  392. ),
  393. ),
  394. ],
  395. ),
  396. );
  397. }
  398. Widget _buildBrightness() {
  399. return Container(
  400. padding: EdgeInsets.fromLTRB(20, 0, 20, 0),
  401. child: Row(
  402. children: [
  403. SizedBox(
  404. width: 40,
  405. child: Text(
  406. "light",
  407. style: TextStyle(
  408. color: Colors.white.withOpacity(0.8),
  409. fontSize: 14,
  410. ),
  411. ),
  412. ),
  413. Expanded(
  414. child: SfSliderTheme(
  415. data: SfSliderThemeData(
  416. activeTrackHeight: 4,
  417. inactiveTrackHeight: 2,
  418. activeTrackColor: Color.fromRGBO(45, 150, 98, 1),
  419. inactiveTrackColor: Colors.grey[900],
  420. thumbColor: Color.fromRGBO(45, 150, 98, 1),
  421. thumbRadius: 10,
  422. tooltipBackgroundColor: Colors.grey[900],
  423. ),
  424. child: SfSlider(
  425. onChanged: (value) {
  426. setState(() {
  427. _brightness = value;
  428. });
  429. },
  430. value: _brightness,
  431. enableTooltip: true,
  432. stepSize: 0.01,
  433. min: kBrightnessMin,
  434. max: kBrightnessMax,
  435. ),
  436. ),
  437. ),
  438. ],
  439. ),
  440. );
  441. }
  442. Future<void> _showExitConfirmationDialog() async {
  443. AlertDialog alert = AlertDialog(
  444. title: Text("discard edits?"),
  445. actions: [
  446. TextButton(
  447. child: Text("yes", style: TextStyle(color: Colors.red)),
  448. onPressed: () {
  449. Navigator.of(context, rootNavigator: true).pop('dialog');
  450. replacePage(context, DetailPage(widget.detailPageConfig));
  451. },
  452. ),
  453. TextButton(
  454. child: Text("no", style: TextStyle(color: Colors.white)),
  455. onPressed: () {
  456. Navigator.of(context, rootNavigator: true).pop('dialog');
  457. },
  458. ),
  459. ],
  460. );
  461. await showDialog(
  462. context: context,
  463. builder: (BuildContext context) {
  464. return alert;
  465. },
  466. barrierColor: Colors.black87,
  467. );
  468. }
  469. }