image_editor_page.dart 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. import 'dart:convert';
  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:photo_manager/photo_manager.dart';
  8. import 'package:photos/core/configuration.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/services/sync_service.dart';
  13. import 'package:photos/utils/dialog_util.dart';
  14. import 'package:photos/utils/toast_util.dart';
  15. import 'package:photos/models/file.dart' as ente;
  16. import 'package:path/path.dart' as path;
  17. import 'dart:io' as io;
  18. class ImageEditorPage extends StatefulWidget {
  19. final ImageProvider imageProvider;
  20. final ente.File originalFile;
  21. final String heroTag;
  22. const ImageEditorPage(this.imageProvider, this.originalFile, this.heroTag,
  23. {Key key})
  24. : super(key: key);
  25. @override
  26. _ImageEditorPageState createState() => _ImageEditorPageState();
  27. }
  28. class _ImageEditorPageState extends State<ImageEditorPage> {
  29. final _logger = Logger("ImageEditor");
  30. final GlobalKey<ExtendedImageEditorState> editorKey =
  31. GlobalKey<ExtendedImageEditorState>();
  32. @override
  33. Widget build(BuildContext context) {
  34. return WillPopScope(
  35. onWillPop: () async {
  36. await _showExitConfirmationDialog();
  37. return false;
  38. },
  39. child: Scaffold(
  40. appBar: AppBar(
  41. backgroundColor: Color(0x00000000),
  42. elevation: 0,
  43. ),
  44. body: Container(
  45. child: Column(
  46. children: [
  47. Expanded(child: _buildImage()),
  48. _buildBottomBar(),
  49. ],
  50. ),
  51. ),
  52. ),
  53. );
  54. }
  55. Widget _buildImage() {
  56. return Hero(
  57. tag: widget.heroTag,
  58. child: ExtendedImage(
  59. image: widget.imageProvider,
  60. extendedImageEditorKey: editorKey,
  61. mode: ExtendedImageMode.editor,
  62. fit: BoxFit.contain,
  63. initEditorConfigHandler: (_) => EditorConfig(
  64. maxScale: 8.0,
  65. cropRectPadding: const EdgeInsets.all(20.0),
  66. hitTestSize: 20.0,
  67. cornerColor: Color.fromRGBO(45, 194, 98, 1.0),
  68. ),
  69. ),
  70. );
  71. }
  72. Widget _buildBottomBar() {
  73. return Row(
  74. mainAxisAlignment: MainAxisAlignment.spaceAround,
  75. children: [
  76. _buildFlipButton(),
  77. _buildRotateLeftButton(),
  78. _buildRotateRightButton(),
  79. _buildSaveButton(),
  80. ],
  81. );
  82. }
  83. Widget _buildFlipButton() {
  84. return GestureDetector(
  85. behavior: HitTestBehavior.opaque,
  86. onTap: () {
  87. flip();
  88. },
  89. child: Container(
  90. width: 80,
  91. child: Column(
  92. children: [
  93. Padding(
  94. padding: const EdgeInsets.only(bottom: 2),
  95. child: Icon(
  96. Icons.flip,
  97. color: Colors.white.withOpacity(0.8),
  98. size: 20,
  99. ),
  100. ),
  101. Padding(padding: EdgeInsets.all(2)),
  102. Text(
  103. "flip",
  104. style: TextStyle(
  105. color: Colors.white.withOpacity(0.6),
  106. fontSize: 12,
  107. ),
  108. textAlign: TextAlign.center,
  109. ),
  110. ],
  111. ),
  112. ),
  113. );
  114. }
  115. Widget _buildRotateLeftButton() {
  116. return GestureDetector(
  117. behavior: HitTestBehavior.opaque,
  118. onTap: () {
  119. rotate(false);
  120. },
  121. child: Container(
  122. width: 80,
  123. child: Column(
  124. children: [
  125. Icon(Icons.rotate_left, color: Colors.white.withOpacity(0.8)),
  126. Padding(padding: EdgeInsets.all(2)),
  127. Text(
  128. "rotate left",
  129. style: TextStyle(
  130. color: Colors.white.withOpacity(0.6),
  131. fontSize: 12,
  132. ),
  133. textAlign: TextAlign.center,
  134. ),
  135. ],
  136. ),
  137. ),
  138. );
  139. }
  140. Widget _buildRotateRightButton() {
  141. return GestureDetector(
  142. behavior: HitTestBehavior.opaque,
  143. onTap: () {
  144. rotate(true);
  145. },
  146. child: Container(
  147. width: 80,
  148. child: Column(
  149. children: [
  150. Icon(Icons.rotate_right, color: Colors.white.withOpacity(0.8)),
  151. Padding(padding: EdgeInsets.all(2)),
  152. Text(
  153. "rotate right",
  154. style: TextStyle(
  155. color: Colors.white.withOpacity(0.6),
  156. fontSize: 12,
  157. ),
  158. textAlign: TextAlign.center,
  159. ),
  160. ],
  161. ),
  162. ),
  163. );
  164. }
  165. Widget _buildSaveButton() {
  166. return GestureDetector(
  167. behavior: HitTestBehavior.opaque,
  168. onTap: () {
  169. _saveEdits();
  170. },
  171. child: Container(
  172. width: 80,
  173. child: Column(
  174. children: [
  175. Icon(Icons.save_alt_outlined, color: Colors.white.withOpacity(0.8)),
  176. Padding(padding: EdgeInsets.all(2)),
  177. Text(
  178. "save copy",
  179. style: TextStyle(
  180. color: Colors.white.withOpacity(0.6),
  181. fontSize: 12,
  182. ),
  183. textAlign: TextAlign.center,
  184. ),
  185. ],
  186. ),
  187. ),
  188. );
  189. }
  190. Future<void> _saveEdits() async {
  191. final dialog = createProgressDialog(context, "saving...");
  192. await dialog.show();
  193. final ExtendedImageEditorState state = editorKey.currentState;
  194. if (state == null) {
  195. return;
  196. }
  197. final Rect rect = state.getCropRect();
  198. if (rect == null) {
  199. return;
  200. }
  201. final EditActionDetails action = state.editAction;
  202. final double radian = action.rotateAngle;
  203. final bool flipHorizontal = action.flipY;
  204. final bool flipVertical = action.flipX;
  205. final Uint8List img = state.rawImageData;
  206. if (img == null) {
  207. _logger.severe("null rawImageData");
  208. showToast("something went wrong");
  209. return;
  210. }
  211. final ImageEditorOption option = ImageEditorOption();
  212. option.addOption(ClipOption.fromRect(rect));
  213. option.addOption(
  214. FlipOption(horizontal: flipHorizontal, vertical: flipVertical));
  215. if (action.hasRotateAngle) {
  216. option.addOption(RotateOption(radian.toInt()));
  217. }
  218. option.addOption(ColorOption.saturation(sat));
  219. option.addOption(ColorOption.brightness(bright));
  220. option.addOption(ColorOption.contrast(con));
  221. option.outputFormat = const OutputFormat.png(88);
  222. print(const JsonEncoder.withIndent(' ').convert(option.toJson()));
  223. final DateTime start = DateTime.now();
  224. final Uint8List result = await ImageEditor.editImage(
  225. image: img,
  226. imageEditorOption: option,
  227. );
  228. _logger.info('result.length = ${result?.length}');
  229. final Duration diff = DateTime.now().difference(start);
  230. _logger.info('image_editor time : $diff');
  231. if (result == null) {
  232. _logger.severe("null result");
  233. showToast("something went wrong");
  234. return;
  235. }
  236. try {
  237. final fileName =
  238. path.basenameWithoutExtension(widget.originalFile.title) +
  239. "_edited_" +
  240. DateTime.now().microsecondsSinceEpoch.toString() +
  241. path.extension(widget.originalFile.title);
  242. final newAsset = await PhotoManager.editor.saveImage(
  243. result,
  244. title: fileName,
  245. );
  246. final newFile =
  247. await ente.File.fromAsset(widget.originalFile.deviceFolder, newAsset);
  248. newFile.creationTime = widget.originalFile.creationTime;
  249. await FilesDB.instance.insertMultiple([newFile]);
  250. Bus.instance.fire(LocalPhotosUpdatedEvent([newFile]));
  251. SyncService.instance.sync();
  252. showToast("edits saved");
  253. } catch (e, s) {
  254. showToast("oops, could not save edits");
  255. _logger.severe(e, s);
  256. }
  257. await dialog.hide();
  258. }
  259. void flip() {
  260. editorKey.currentState?.flip();
  261. }
  262. void rotate(bool right) {
  263. editorKey.currentState?.rotate(right: right);
  264. }
  265. double sat = 1;
  266. double bright = 1;
  267. double con = 1;
  268. Widget _buildSat() {
  269. return Slider(
  270. label: 'sat : ${sat.toStringAsFixed(2)}',
  271. onChanged: (double value) {
  272. setState(() {
  273. sat = value;
  274. });
  275. },
  276. value: sat,
  277. min: 0,
  278. max: 2,
  279. );
  280. }
  281. Widget _buildBrightness() {
  282. return Slider(
  283. label: 'brightness : ${bright.toStringAsFixed(2)}',
  284. onChanged: (double value) {
  285. setState(() {
  286. bright = value;
  287. });
  288. },
  289. value: bright,
  290. min: 0,
  291. max: 2,
  292. );
  293. }
  294. Widget _buildCon() {
  295. return Slider(
  296. label: 'con : ${con.toStringAsFixed(2)}',
  297. onChanged: (double value) {
  298. setState(() {
  299. con = value;
  300. });
  301. },
  302. value: con,
  303. min: 0,
  304. max: 4,
  305. );
  306. }
  307. Future<void> _showExitConfirmationDialog() async {
  308. AlertDialog alert = AlertDialog(
  309. title: Text("discard edits?"),
  310. actions: [
  311. TextButton(
  312. child: Text("yes", style: TextStyle(color: Colors.red)),
  313. onPressed: () {
  314. Navigator.of(context, rootNavigator: true).pop('dialog');
  315. Navigator.of(context).pop();
  316. },
  317. ),
  318. TextButton(
  319. child: Text("no", style: TextStyle(color: Colors.white)),
  320. onPressed: () {
  321. Navigator.of(context, rootNavigator: true).pop('dialog');
  322. },
  323. ),
  324. ],
  325. );
  326. await showDialog(
  327. context: context,
  328. builder: (BuildContext context) {
  329. return alert;
  330. },
  331. barrierColor: Colors.black87,
  332. );
  333. }
  334. }