ente/lib/ui/image_editor_page.dart

471 lines
13 KiB
Dart
Raw Normal View History

2021-06-02 13:54:31 +00:00
import 'dart:convert';
import 'dart:typed_data';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:image_editor/image_editor.dart';
2021-06-02 16:45:35 +00:00
import 'package:logging/logging.dart';
import 'package:photo_manager/photo_manager.dart';
2021-06-02 21:46:38 +00:00
import 'package:photos/core/event_bus.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/services/sync_service.dart';
2021-06-09 13:38:27 +00:00
import 'package:photos/ui/detail_page.dart';
2021-06-02 16:45:35 +00:00
import 'package:photos/utils/dialog_util.dart';
2021-06-09 13:38:27 +00:00
import 'package:photos/utils/navigation_util.dart';
2021-06-02 16:45:35 +00:00
import 'package:photos/utils/toast_util.dart';
import 'package:photos/models/file.dart' as ente;
import 'package:path/path.dart' as path;
import 'package:syncfusion_flutter_core/theme.dart';
import 'package:syncfusion_flutter_sliders/sliders.dart';
2021-06-02 13:54:31 +00:00
class ImageEditorPage extends StatefulWidget {
final ImageProvider imageProvider;
2021-06-09 13:38:27 +00:00
final DetailPageConfiguration detailPageConfig;
2021-06-02 16:45:35 +00:00
final ente.File originalFile;
2021-06-02 13:54:31 +00:00
2021-06-09 13:38:27 +00:00
const ImageEditorPage(
this.imageProvider,
this.originalFile,
this.detailPageConfig, {
Key key,
}) : super(key: key);
2021-06-02 13:54:31 +00:00
@override
_ImageEditorPageState createState() => _ImageEditorPageState();
}
class _ImageEditorPageState extends State<ImageEditorPage> {
2021-06-02 16:45:35 +00:00
final _logger = Logger("ImageEditor");
2021-06-02 13:54:31 +00:00
final GlobalKey<ExtendedImageEditorState> editorKey =
GlobalKey<ExtendedImageEditorState>();
2021-06-10 18:28:09 +00:00
double _brightness = 0;
double _saturation = 0;
2021-06-10 18:52:45 +00:00
bool _hasEdited = false;
2021-06-10 18:28:09 +00:00
2021-06-02 13:54:31 +00:00
@override
Widget build(BuildContext context) {
2021-06-04 18:57:07 +00:00
return WillPopScope(
onWillPop: () async {
2021-06-10 18:52:45 +00:00
if (_hasBeenEdited()) {
await _showExitConfirmationDialog();
} else {
replacePage(context, DetailPage(widget.detailPageConfig));
}
2021-06-04 18:57:07 +00:00
return false;
},
child: Scaffold(
appBar: AppBar(
backgroundColor: Color(0x00000000),
elevation: 0,
2021-06-10 18:52:45 +00:00
actions: _hasBeenEdited()
? [
IconButton(
padding: const EdgeInsets.only(right: 16, left: 16),
onPressed: () {
editorKey.currentState.reset();
setState(() {
_brightness = 0;
_saturation = 0;
});
},
icon: Icon(Icons.history),
)
]
: [],
2021-06-04 18:57:07 +00:00
),
body: Container(
child: Column(
children: [
Expanded(child: _buildImage()),
Padding(padding: EdgeInsets.all(4)),
Column(
children: [
_buildBrightness(),
_buildSat(),
],
),
Padding(padding: EdgeInsets.all(8)),
2021-06-04 18:57:07 +00:00
_buildBottomBar(),
Padding(padding: EdgeInsets.all(6)),
2021-06-04 18:57:07 +00:00
],
),
2021-06-02 13:54:31 +00:00
),
),
);
}
2021-06-10 18:52:45 +00:00
bool _hasBeenEdited() {
return _hasEdited || _saturation != 0 || _brightness != 0;
}
2021-06-02 13:54:31 +00:00
Widget _buildImage() {
return Hero(
2021-06-09 13:38:27 +00:00
tag: widget.detailPageConfig.tagPrefix + widget.originalFile.tag(),
child: ExtendedImage(
image: widget.imageProvider,
extendedImageEditorKey: editorKey,
mode: ExtendedImageMode.editor,
fit: BoxFit.contain,
initEditorConfigHandler: (_) => EditorConfig(
maxScale: 8.0,
cropRectPadding: const EdgeInsets.all(20.0),
hitTestSize: 20.0,
2021-06-10 18:25:48 +00:00
cornerColor: Color.fromRGBO(45, 150, 98, 1),
2021-06-10 18:52:45 +00:00
editActionDetailsIsChanged: (_) {
setState(() {
_hasEdited = true;
});
},
2021-06-02 13:54:31 +00:00
),
2021-06-10 18:28:09 +00:00
brightness: _brightness,
saturation: _saturation,
2021-06-02 13:54:31 +00:00
),
);
}
Widget _buildBottomBar() {
2021-06-02 16:45:35 +00:00
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildFlipButton(),
_buildRotateLeftButton(),
_buildRotateRightButton(),
_buildSaveButton(),
],
);
2021-06-02 13:54:31 +00:00
}
Widget _buildFlipButton() {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
flip();
},
child: Container(
width: 80,
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Icon(
Icons.flip,
color: Colors.white.withOpacity(0.8),
size: 20,
),
),
Padding(padding: EdgeInsets.all(2)),
Text(
"flip",
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 12,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _buildRotateLeftButton() {
return GestureDetector(
2021-06-02 16:46:45 +00:00
behavior: HitTestBehavior.opaque,
2021-06-02 13:54:31 +00:00
onTap: () {
rotate(false);
},
child: Container(
width: 80,
child: Column(
children: [
Icon(Icons.rotate_left, color: Colors.white.withOpacity(0.8)),
Padding(padding: EdgeInsets.all(2)),
Text(
"rotate left",
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 12,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _buildRotateRightButton() {
return GestureDetector(
2021-06-02 16:46:45 +00:00
behavior: HitTestBehavior.opaque,
2021-06-02 13:54:31 +00:00
onTap: () {
rotate(true);
},
child: Container(
width: 80,
child: Column(
children: [
Icon(Icons.rotate_right, color: Colors.white.withOpacity(0.8)),
Padding(padding: EdgeInsets.all(2)),
Text(
"rotate right",
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 12,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _buildSaveButton() {
return GestureDetector(
2021-06-02 16:46:45 +00:00
behavior: HitTestBehavior.opaque,
2021-06-02 13:54:31 +00:00
onTap: () {
2021-06-02 16:45:35 +00:00
_saveEdits();
2021-06-02 13:54:31 +00:00
},
child: Container(
width: 80,
child: Column(
children: [
Icon(Icons.save_alt_outlined, color: Colors.white.withOpacity(0.8)),
Padding(padding: EdgeInsets.all(2)),
Text(
"save copy",
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 12,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
2021-06-02 16:45:35 +00:00
Future<void> _saveEdits() async {
final dialog = createProgressDialog(context, "saving...");
await dialog.show();
2021-06-02 13:54:31 +00:00
final ExtendedImageEditorState state = editorKey.currentState;
if (state == null) {
return;
}
final Rect rect = state.getCropRect();
if (rect == null) {
return;
}
final EditActionDetails action = state.editAction;
final double radian = action.rotateAngle;
final bool flipHorizontal = action.flipY;
final bool flipVertical = action.flipX;
final Uint8List img = state.rawImageData;
if (img == null) {
2021-06-02 16:45:35 +00:00
_logger.severe("null rawImageData");
showToast("something went wrong");
2021-06-02 13:54:31 +00:00
return;
}
final ImageEditorOption option = ImageEditorOption();
option.addOption(ClipOption.fromRect(rect));
option.addOption(
FlipOption(horizontal: flipHorizontal, vertical: flipVertical));
if (action.hasRotateAngle) {
option.addOption(RotateOption(radian.toInt()));
}
2021-06-10 18:28:09 +00:00
option.addOption(ColorOption.saturation(_saturation + 1));
option.addOption(ColorOption.brightness(_brightness + 1));
2021-06-02 13:54:31 +00:00
option.outputFormat = const OutputFormat.png(88);
print(const JsonEncoder.withIndent(' ').convert(option.toJson()));
final DateTime start = DateTime.now();
final Uint8List result = await ImageEditor.editImage(
image: img,
imageEditorOption: option,
);
2021-06-02 16:45:35 +00:00
_logger.info('result.length = ${result?.length}');
2021-06-02 13:54:31 +00:00
final Duration diff = DateTime.now().difference(start);
2021-06-02 16:45:35 +00:00
_logger.info('image_editor time : $diff');
2021-06-02 13:54:31 +00:00
2021-06-02 16:45:35 +00:00
if (result == null) {
_logger.severe("null result");
showToast("something went wrong");
return;
}
try {
2021-06-02 21:46:38 +00:00
final fileName =
path.basenameWithoutExtension(widget.originalFile.title) +
"_edited_" +
DateTime.now().microsecondsSinceEpoch.toString() +
path.extension(widget.originalFile.title);
final newAsset = await PhotoManager.editor.saveImage(
result,
2021-06-02 21:46:38 +00:00
title: fileName,
);
final newFile =
await ente.File.fromAsset(widget.originalFile.deviceFolder, newAsset);
2021-06-02 21:46:38 +00:00
newFile.creationTime = widget.originalFile.creationTime;
2021-06-09 15:32:30 +00:00
newFile.collectionID = widget.originalFile.collectionID;
newFile.generatedID = await FilesDB.instance.insert(newFile);
2021-06-02 21:46:38 +00:00
Bus.instance.fire(LocalPhotosUpdatedEvent([newFile]));
SyncService.instance.sync();
2021-06-02 16:45:35 +00:00
showToast("edits saved");
2021-06-09 13:38:27 +00:00
final existingFiles = widget.detailPageConfig.files;
final files = await widget.detailPageConfig.asyncLoader(
existingFiles[existingFiles.length - 1].creationTime,
existingFiles[0].creationTime,
);
replacePage(
context,
DetailPage(
widget.detailPageConfig.copyWith(
files: files,
2021-06-09 15:32:30 +00:00
selectedIndex: files
.indexWhere((file) => file.generatedID == newFile.generatedID),
2021-06-09 13:38:27 +00:00
),
),
);
2021-06-02 16:45:35 +00:00
} catch (e, s) {
showToast("oops, could not save edits");
_logger.severe(e, s);
}
await dialog.hide();
2021-06-02 13:54:31 +00:00
}
void flip() {
editorKey.currentState?.flip();
}
void rotate(bool right) {
editorKey.currentState?.rotate(right: right);
}
Widget _buildSat() {
return Container(
padding: EdgeInsets.fromLTRB(20, 0, 20, 0),
child: Row(
children: [
Container(
width: 40,
child: Text(
"color",
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
),
),
),
Expanded(
child: SfSliderTheme(
data: SfSliderThemeData(
activeTrackHeight: 4,
inactiveTrackHeight: 2,
inactiveTrackColor: Colors.grey[900],
activeTrackColor: Color.fromRGBO(45, 150, 98, 1),
thumbColor: Color.fromRGBO(45, 150, 98, 1),
thumbRadius: 10,
tooltipBackgroundColor: Colors.grey[900],
),
child: SfSlider(
onChanged: (value) {
setState(() {
2021-06-10 18:28:09 +00:00
_saturation = value;
});
},
2021-06-10 18:28:09 +00:00
value: _saturation,
enableTooltip: true,
stepSize: 0.01,
min: -1.0,
max: 1.0,
),
),
),
],
),
2021-06-02 13:54:31 +00:00
);
}
Widget _buildBrightness() {
return Container(
padding: EdgeInsets.fromLTRB(20, 0, 20, 0),
child: Row(
children: [
Container(
width: 40,
child: Text(
"light",
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
),
),
),
Expanded(
child: SfSliderTheme(
data: SfSliderThemeData(
activeTrackHeight: 4,
inactiveTrackHeight: 2,
activeTrackColor: Color.fromRGBO(45, 150, 98, 1),
inactiveTrackColor: Colors.grey[900],
thumbColor: Color.fromRGBO(45, 150, 98, 1),
thumbRadius: 10,
tooltipBackgroundColor: Colors.grey[900],
),
child: SfSlider(
onChanged: (value) {
setState(() {
2021-06-10 18:28:09 +00:00
_brightness = value;
});
},
2021-06-10 18:28:09 +00:00
value: _brightness,
enableTooltip: true,
stepSize: 0.01,
min: -1.0,
max: 1.0,
),
),
),
],
),
2021-06-02 13:54:31 +00:00
);
}
2021-06-04 18:57:07 +00:00
Future<void> _showExitConfirmationDialog() async {
AlertDialog alert = AlertDialog(
title: Text("discard edits?"),
actions: [
TextButton(
child: Text("yes", style: TextStyle(color: Colors.red)),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
2021-06-09 13:38:27 +00:00
replacePage(context, DetailPage(widget.detailPageConfig));
2021-06-04 18:57:07 +00:00
},
),
TextButton(
child: Text("no", style: TextStyle(color: Colors.white)),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
},
),
],
);
await showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
barrierColor: Colors.black87,
);
}
2021-06-02 13:54:31 +00:00
}