Преглед на файлове

Add interface for image editor

Vishnu Mohandas преди 4 години
родител
ревизия
409543081d
променени са 5 файла, в които са добавени 367 реда и са изтрити 10 реда
  1. 28 3
      lib/ui/detail_page.dart
  2. 306 0
      lib/ui/image_editor_page.dart
  3. 11 7
      lib/utils/file_util.dart
  4. 21 0
      pubspec.lock
  5. 1 0
      pubspec.yaml

+ 28 - 3
lib/ui/detail_page.dart

@@ -1,5 +1,6 @@
 import 'dart:io';
 
+import 'package:extended_image/extended_image.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:like_button/like_button.dart';
@@ -9,12 +10,14 @@ import 'package:photos/services/favorites_service.dart';
 import 'package:photos/models/file_type.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/ui/gallery.dart';
+import 'package:photos/ui/image_editor_page.dart';
 import 'package:photos/ui/video_widget.dart';
 import 'package:photos/ui/zoomable_image.dart';
 import 'package:photos/utils/date_time_util.dart';
 import 'package:photos/utils/delete_file_util.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/file_util.dart';
+import 'package:photos/utils/navigation_util.dart';
 import 'package:photos/utils/share_util.dart';
 import 'package:logging/logging.dart';
 import 'package:photos/utils/toast_util.dart';
@@ -161,8 +164,30 @@ class _DetailPageState extends State<DetailPage> {
   }
 
   AppBar _buildAppBar() {
-    final actions = List<Widget>();
+    final file = _files[_selectedIndex];
+    final List<Widget> actions = [];
     actions.add(_getFavoriteButton());
+    if (file.fileType == FileType.image) {
+      actions.add(
+        Padding(
+          padding: const EdgeInsets.only(left: 8),
+          child: IconButton(
+            onPressed: () async {
+              final imageProvider =
+                  ExtendedFileImageProvider(await getImage(file));
+              await precacheImage(imageProvider, context);
+              routeToPage(
+                  context,
+                  ImageEditorPage(
+                    imageProvider,
+                    widget.tagPrefix + file.tag(),
+                  ));
+            },
+            icon: Icon(Icons.edit),
+          ),
+        ),
+      );
+    }
     actions.add(PopupMenuButton(
       itemBuilder: (context) {
         return [
@@ -212,9 +237,9 @@ class _DetailPageState extends State<DetailPage> {
       },
       onSelected: (value) {
         if (value == 1) {
-          share(context, [_files[_selectedIndex]]);
+          share(context, [file]);
         } else if (value == 2) {
-          _displayInfo(_files[_selectedIndex]);
+          _displayInfo(file);
         } else if (value == 3) {
           _showDeleteSheet();
         }

+ 306 - 0
lib/ui/image_editor_page.dart

@@ -0,0 +1,306 @@
+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';
+
+class ImageEditorPage extends StatefulWidget {
+  final ImageProvider imageProvider;
+  final String heroTag;
+
+  const ImageEditorPage(this.imageProvider, this.heroTag, {Key key}) : super(key: key);
+
+  @override
+  _ImageEditorPageState createState() => _ImageEditorPageState();
+}
+
+class _ImageEditorPageState extends State<ImageEditorPage> {
+  final GlobalKey<ExtendedImageEditorState> editorKey =
+      GlobalKey<ExtendedImageEditorState>();
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        backgroundColor: Color(0x00000000),
+        elevation: 0,
+      ),
+      body: Container(
+        child: Column(
+          children: [
+            Expanded(child: _buildImage()),
+            _buildBottomBar(),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildImage() {
+    return Hero(
+      tag: widget.heroTag,
+      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, 194, 98, 1.0),
+        ),
+      ),
+    );
+  }
+
+  Widget _buildBottomBar() {
+    return Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [
+      _buildFlipButton(),
+      _buildRotateLeftButton(),
+      _buildRotateRightButton(),
+      _buildSaveButton(),
+    ]);
+  }
+
+  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(
+      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(
+      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(
+      onTap: () {
+        // todo
+      },
+      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,
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Future<void> crop([bool test = false]) async {
+    final ExtendedImageEditorState state = editorKey.currentState;
+    if (state == null) {
+      return;
+    }
+    final Rect rect = state.getCropRect();
+    if (rect == null) {
+      // showToast('The crop rect is null.');
+      return;
+    }
+    final EditActionDetails action = state.editAction;
+    final double radian = action.rotateAngle;
+
+    final bool flipHorizontal = action.flipY;
+    final bool flipVertical = action.flipX;
+    // final img = await getImageFromEditorKey(editorKey);
+    final Uint8List img = state.rawImageData;
+
+    if (img == null) {
+      // showToast('The img is null.');
+      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(sat));
+    option.addOption(ColorOption.brightness(bright));
+    option.addOption(ColorOption.contrast(con));
+
+    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,
+    );
+
+    print('result.length = ${result?.length}');
+
+    final Duration diff = DateTime.now().difference(start);
+
+    print('image_editor time : $diff');
+    // showToast('handle duration: $diff',
+    // duration: const Duration(seconds: 5), dismissOtherToast: true);
+
+    if (result == null) return;
+
+    showPreviewDialog(result);
+  }
+
+  void flip() {
+    editorKey.currentState?.flip();
+  }
+
+  void rotate(bool right) {
+    editorKey.currentState?.rotate(right: right);
+  }
+
+  void showPreviewDialog(Uint8List image) {
+    showDialog<void>(
+      context: context,
+      builder: (BuildContext ctx) => GestureDetector(
+        onTap: () => Navigator.pop(context),
+        child: Container(
+          color: Colors.grey.withOpacity(0.5),
+          child: Center(
+            child: SizedBox.fromSize(
+              size: const Size.square(200),
+              child: Container(
+                child: Image.memory(image),
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  double sat = 1;
+  double bright = 1;
+  double con = 1;
+
+  Widget _buildSat() {
+    return Slider(
+      label: 'sat : ${sat.toStringAsFixed(2)}',
+      onChanged: (double value) {
+        setState(() {
+          sat = value;
+        });
+      },
+      value: sat,
+      min: 0,
+      max: 2,
+    );
+  }
+
+  Widget _buildBrightness() {
+    return Slider(
+      label: 'brightness : ${bright.toStringAsFixed(2)}',
+      onChanged: (double value) {
+        setState(() {
+          bright = value;
+        });
+      },
+      value: bright,
+      min: 0,
+      max: 2,
+    );
+  }
+
+  Widget _buildCon() {
+    return Slider(
+      label: 'con : ${con.toStringAsFixed(2)}',
+      onChanged: (double value) {
+        setState(() {
+          con = value;
+        });
+      },
+      value: con,
+      min: 0,
+      max: 4,
+    );
+  }
+}

+ 11 - 7
lib/utils/file_util.dart

@@ -27,16 +27,20 @@ void preloadFile(ente.File file) {
   if (file.fileType == FileType.video) {
     return;
   }
+  getImage(file);
+}
+
+Future<io.File> getImage(ente.File file) async {
   if (file.localID == null) {
-    getFileFromServer(file);
+    return getFileFromServer(file);
   } else {
-    if (FileLruCache.get(file) == null) {
-      file.getAsset().then((asset) {
-        asset.file.then((assetFile) {
-          FileLruCache.put(file, assetFile);
-        });
-      });
+    final cachedFile = FileLruCache.get(file);
+    if (cachedFile == null) {
+      final diskFile = await (await file.getAsset()).file;
+      FileLruCache.put(file, diskFile);
+      return diskFile;
     }
+    return cachedFile;
   }
 }
 

+ 21 - 0
pubspec.lock

@@ -197,6 +197,20 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "0.1.0"
+  extended_image:
+    dependency: "direct main"
+    description:
+      name: extended_image
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.1.0"
+  extended_image_library:
+    dependency: transitive
+    description:
+      name: extended_image_library
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.1.0"
   fake_async:
     dependency: transitive
     description:
@@ -380,6 +394,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "0.13.3"
+  http_client_helper:
+    dependency: transitive
+    description:
+      name: http_client_helper
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.2"
   http_parser:
     dependency: transitive
     description:

+ 1 - 0
pubspec.yaml

@@ -81,6 +81,7 @@ dependencies:
   flutter_local_notifications: ^5.0.0+4
   open_file: ^3.2.1
   email_validator: '^1.0.6'
+  extended_image: ^4.1.0
 
 dev_dependencies:
   flutter_test: