123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516 |
- import 'dart:io';
- import 'dart:typed_data';
- import 'package:extended_image/extended_image.dart';
- import 'package:flutter/material.dart';
- import 'package:image_editor/image_editor.dart';
- import 'package:logging/logging.dart';
- import 'package:path/path.dart' as path;
- import 'package:photo_manager/photo_manager.dart';
- 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/models/file.dart' as ente;
- import 'package:photos/models/location.dart';
- import 'package:photos/services/local_sync_service.dart';
- import 'package:photos/services/sync_service.dart';
- import 'package:photos/ui/common/loading_widget.dart';
- import 'package:photos/ui/editor/filtered_image.dart';
- import 'package:photos/ui/viewer/file/detail_page.dart';
- import 'package:photos/utils/dialog_util.dart';
- import 'package:photos/utils/navigation_util.dart';
- import 'package:photos/utils/toast_util.dart';
- import 'package:syncfusion_flutter_core/theme.dart';
- import 'package:syncfusion_flutter_sliders/sliders.dart';
- class ImageEditorPage extends StatefulWidget {
- final ImageProvider imageProvider;
- final DetailPageConfiguration detailPageConfig;
- final ente.File originalFile;
- const ImageEditorPage(
- this.imageProvider,
- this.originalFile,
- this.detailPageConfig, {
- Key key,
- }) : super(key: key);
- @override
- _ImageEditorPageState createState() => _ImageEditorPageState();
- }
- class _ImageEditorPageState extends State<ImageEditorPage> {
- static const double kBrightnessDefault = 1;
- static const double kBrightnessMin = 0;
- static const double kBrightnessMax = 2;
- static const double kSaturationDefault = 1;
- static const double kSaturationMin = 0;
- static const double kSaturationMax = 2;
- final _logger = Logger("ImageEditor");
- final GlobalKey<ExtendedImageEditorState> editorKey =
- GlobalKey<ExtendedImageEditorState>();
- double _brightness = kBrightnessDefault;
- double _saturation = kSaturationDefault;
- bool _hasEdited = false;
- @override
- Widget build(BuildContext context) {
- return WillPopScope(
- onWillPop: () async {
- if (_hasBeenEdited()) {
- await _showExitConfirmationDialog();
- } else {
- replacePage(context, DetailPage(widget.detailPageConfig));
- }
- return false;
- },
- child: Scaffold(
- appBar: AppBar(
- backgroundColor: Color(0x00000000),
- elevation: 0,
- actions: _hasBeenEdited()
- ? [
- IconButton(
- padding: const EdgeInsets.only(right: 16, left: 16),
- onPressed: () {
- editorKey.currentState.reset();
- setState(() {
- _brightness = kBrightnessDefault;
- _saturation = kSaturationDefault;
- });
- },
- icon: Icon(Icons.history),
- )
- ]
- : [],
- ),
- body: Column(
- children: [
- Expanded(child: _buildImage()),
- Padding(padding: EdgeInsets.all(4)),
- Column(
- children: [
- _buildBrightness(),
- _buildSat(),
- ],
- ),
- Padding(padding: EdgeInsets.all(8)),
- _buildBottomBar(),
- Padding(padding: EdgeInsets.all(Platform.isIOS ? 16 : 6)),
- ],
- ),
- ),
- );
- }
- bool _hasBeenEdited() {
- return _hasEdited ||
- _saturation != kSaturationDefault ||
- _brightness != kBrightnessDefault;
- }
- Widget _buildImage() {
- return Hero(
- 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,
- cornerColor: Color.fromRGBO(45, 150, 98, 1),
- editActionDetailsIsChanged: (_) {
- setState(() {
- _hasEdited = true;
- });
- },
- ),
- loadStateChanged: (state) {
- if (state.extendedImageLoadState == LoadState.completed) {
- return FilteredImage(
- child: state.completedWidget,
- brightness: _brightness,
- saturation: _saturation,
- );
- }
- return loadWidget;
- },
- ),
- );
- }
- Widget _buildBottomBar() {
- return Row(
- mainAxisAlignment: MainAxisAlignment.spaceAround,
- children: [
- _buildFlipButton(),
- _buildRotateLeftButton(),
- _buildRotateRightButton(),
- _buildSaveButton(),
- ],
- );
- }
- Widget _buildFlipButton() {
- TextStyle subtitle2 = Theme.of(context).textTheme.subtitle2;
- return GestureDetector(
- behavior: HitTestBehavior.translucent,
- onTap: () {
- flip();
- },
- child: SizedBox(
- width: 80,
- child: Column(
- children: [
- Padding(
- padding: const EdgeInsets.only(bottom: 2),
- child: Icon(
- Icons.flip,
- color: Theme.of(context).iconTheme.color.withOpacity(0.8),
- size: 20,
- ),
- ),
- Padding(padding: EdgeInsets.all(2)),
- Text(
- "Flip",
- style: subtitle2.copyWith(
- color: subtitle2.color.withOpacity(0.8),
- ),
- textAlign: TextAlign.center,
- ),
- ],
- ),
- ),
- );
- }
- Widget _buildRotateLeftButton() {
- TextStyle subtitle2 = Theme.of(context).textTheme.subtitle2;
- return GestureDetector(
- behavior: HitTestBehavior.translucent,
- onTap: () {
- rotate(false);
- },
- child: SizedBox(
- width: 80,
- child: Column(
- children: [
- Icon(
- Icons.rotate_left,
- color: Theme.of(context).iconTheme.color.withOpacity(0.8),
- ),
- Padding(padding: EdgeInsets.all(2)),
- Text(
- "Rotate left",
- style: subtitle2.copyWith(
- color: subtitle2.color.withOpacity(0.8),
- ),
- textAlign: TextAlign.center,
- ),
- ],
- ),
- ),
- );
- }
- Widget _buildRotateRightButton() {
- TextStyle subtitle2 = Theme.of(context).textTheme.subtitle2;
- return GestureDetector(
- behavior: HitTestBehavior.translucent,
- onTap: () {
- rotate(true);
- },
- child: SizedBox(
- width: 80,
- child: Column(
- children: [
- Icon(
- Icons.rotate_right,
- color: Theme.of(context).iconTheme.color.withOpacity(0.8),
- ),
- Padding(padding: EdgeInsets.all(2)),
- Text(
- "Rotate right",
- style: subtitle2.copyWith(
- color: subtitle2.color.withOpacity(0.8),
- ),
- textAlign: TextAlign.center,
- ),
- ],
- ),
- ),
- );
- }
- Widget _buildSaveButton() {
- TextStyle subtitle2 = Theme.of(context).textTheme.subtitle2;
- return GestureDetector(
- behavior: HitTestBehavior.translucent,
- onTap: () {
- _saveEdits();
- },
- child: SizedBox(
- width: 80,
- child: Column(
- children: [
- Icon(
- Icons.save_alt_outlined,
- color: Theme.of(context).iconTheme.color.withOpacity(0.8),
- ),
- Padding(padding: EdgeInsets.all(2)),
- Text(
- "Save copy",
- style: subtitle2.copyWith(
- color: subtitle2.color.withOpacity(0.8),
- ),
- textAlign: TextAlign.center,
- ),
- ],
- ),
- ),
- );
- }
- Future<void> _saveEdits() async {
- final dialog = createProgressDialog(context, "Saving...");
- await dialog.show();
- 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) {
- _logger.severe("null rawImageData");
- showToast(context, "Something went wrong");
- 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()));
- }
- option.addOption(ColorOption.saturation(_saturation));
- option.addOption(ColorOption.brightness(_brightness));
- option.outputFormat = const OutputFormat.png(88);
- final DateTime start = DateTime.now();
- final Uint8List result = await ImageEditor.editImage(
- image: img,
- imageEditorOption: option,
- );
- _logger.info('result.length = ${result?.length}');
- final Duration diff = DateTime.now().difference(start);
- _logger.info('image_editor time : $diff');
- if (result == null) {
- _logger.severe("null result");
- showToast(context, "Something went wrong");
- return;
- }
- try {
- final fileName =
- path.basenameWithoutExtension(widget.originalFile.title) +
- "_edited_" +
- DateTime.now().microsecondsSinceEpoch.toString() +
- path.extension(widget.originalFile.title);
- final newAsset = await PhotoManager.editor.saveImage(
- result,
- title: fileName,
- );
- final newFile =
- await ente.File.fromAsset(widget.originalFile.deviceFolder, newAsset);
- newFile.creationTime = widget.originalFile.creationTime;
- newFile.collectionID = widget.originalFile.collectionID;
- newFile.location = widget.originalFile.location;
- if (!newFile.hasLocation() && widget.originalFile.localID != null) {
- var assetEntity = await widget.originalFile.getAsset();
- if (assetEntity != null) {
- final latLong = await assetEntity.latlngAsync();
- newFile.location = Location(latLong.latitude, latLong.longitude);
- }
- }
- newFile.generatedID = await FilesDB.instance.insert(newFile);
- await LocalSyncService.instance.trackEditedFile(newFile);
- Bus.instance.fire(LocalPhotosUpdatedEvent([newFile]));
- SyncService.instance.sync();
- showToast(context, "Edits saved");
- _logger.info("Original file " + widget.originalFile.toString());
- _logger.info("Saved edits to file " + newFile.toString());
- final existingFiles = widget.detailPageConfig.files;
- final files = (await widget.detailPageConfig.asyncLoader(
- existingFiles[existingFiles.length - 1].creationTime,
- existingFiles[0].creationTime,
- ))
- .files;
- replacePage(
- context,
- DetailPage(
- widget.detailPageConfig.copyWith(
- files: files,
- selectedIndex: files
- .indexWhere((file) => file.generatedID == newFile.generatedID),
- ),
- ),
- );
- } catch (e, s) {
- showToast(context, "Oops, could not save edits");
- _logger.severe(e, s);
- }
- await dialog.hide();
- }
- void flip() {
- editorKey.currentState?.flip();
- }
- void rotate(bool right) {
- editorKey.currentState?.rotate(right: right);
- }
- Widget _buildSat() {
- TextStyle subtitle2 = Theme.of(context).textTheme.subtitle2;
- return Container(
- padding: EdgeInsets.fromLTRB(20, 0, 20, 0),
- child: Row(
- children: [
- SizedBox(
- width: 40,
- child: Text(
- "Color",
- style: subtitle2.copyWith(
- color: subtitle2.color.withOpacity(0.8),
- ),
- ),
- ),
- 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(() {
- _saturation = value;
- });
- },
- value: _saturation,
- enableTooltip: true,
- stepSize: 0.01,
- min: kSaturationMin,
- max: kSaturationMax,
- ),
- ),
- ),
- ],
- ),
- );
- }
- Widget _buildBrightness() {
- TextStyle subtitle2 = Theme.of(context).textTheme.subtitle2;
- return Container(
- padding: EdgeInsets.fromLTRB(20, 0, 20, 0),
- child: Row(
- children: [
- SizedBox(
- width: 40,
- child: Text(
- "Light",
- style: subtitle2.copyWith(
- color: subtitle2.color.withOpacity(0.8),
- ),
- ),
- ),
- 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(() {
- _brightness = value;
- });
- },
- value: _brightness,
- enableTooltip: true,
- stepSize: 0.01,
- min: kBrightnessMin,
- max: kBrightnessMax,
- ),
- ),
- ),
- ],
- ),
- );
- }
- 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');
- replacePage(context, DetailPage(widget.detailPageConfig));
- },
- ),
- 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,
- );
- }
- }
|