diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..58a9b76 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Verwendet IntelliSense zum Ermitteln möglicher Attribute. + // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen. + // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Connect-App", + "request": "launch", + "type": "dart", + }, + { + "name": "Connect-App (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "Connect-App (release mode)", + "request": "launch", + "type": "dart", + "flutterMode": "release" + } + ] +} \ No newline at end of file diff --git a/lib/src/components/image_editor/action_button.dart b/lib/src/components/image_editor/action_button.dart new file mode 100644 index 0000000..283fae6 --- /dev/null +++ b/lib/src/components/image_editor/action_button.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +class ActionButton extends StatelessWidget { + final VoidCallback? onPressed; + final IconData? icon; + final Color? color; + + const ActionButton(this.icon, {super.key, this.onPressed, this.color}); + + @override + Widget build(BuildContext context) { + return IconButton( + icon: FaIcon( + icon, + size: 30, + color: color, + shadows: [ + Shadow( + color: const Color.fromARGB(122, 0, 0, 0), + blurRadius: 5.0, + ) + ], + ), + onPressed: onPressed, + ); + } +} diff --git a/lib/src/components/image_editor/data/layer.dart b/lib/src/components/image_editor/data/layer.dart index 0aef43b..6e68173 100755 --- a/lib/src/components/image_editor/data/layer.dart +++ b/lib/src/components/image_editor/data/layer.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:hand_signature/signature.dart'; import 'package:twonly/src/components/image_editor/data/image_item.dart'; /// Layer class with some common properties @@ -7,12 +8,14 @@ class Layer { double rotation, scale, opacity; bool isEditing; bool isDeleted; + bool hasCustomActionButtons; Layer({ this.offset = const Offset(0, 0), this.opacity = 1, this.isEditing = false, this.isDeleted = false, + this.hasCustomActionButtons = false, this.rotation = 0, this.scale = 1, }); @@ -71,3 +74,22 @@ class TextLayerData extends Layer { super.isEditing = true, }); } + +/// Attributes used by [DrawLayer] +class DrawLayerData extends Layer { + final control = HandSignatureControl( + threshold: 3.0, + smoothRatio: 0.65, + velocityRange: 2.0, + ); + + // String text; + DrawLayerData({ + super.offset, + super.opacity, + super.rotation, + super.scale, + super.hasCustomActionButtons = true, + super.isEditing = true, + }); +} diff --git a/lib/src/components/image_editor/layers/draw_layer.dart b/lib/src/components/image_editor/layers/draw_layer.dart new file mode 100644 index 0000000..7466d5f --- /dev/null +++ b/lib/src/components/image_editor/layers/draw_layer.dart @@ -0,0 +1,200 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:hand_signature/signature.dart'; +import 'package:screenshot/screenshot.dart'; +import 'package:twonly/src/components/image_editor/action_button.dart'; +import 'package:twonly/src/components/image_editor/data/layer.dart'; + +class DrawLayer extends StatefulWidget { + final DrawLayerData layerData; + final VoidCallback? onUpdate; + + const DrawLayer({ + super.key, + required this.layerData, + this.onUpdate, + }); + @override + createState() => _DrawLayerState(); +} + +class _DrawLayerState extends State { + Color pickerColor = Colors.white, currentColor = Colors.white; + + var screenshotController = ScreenshotController(); + + List undoList = []; + bool skipNextEvent = false; + + @override + void initState() { + widget.layerData.control.addListener(() { + if (widget.layerData.control.hasActivePath) return; + + if (skipNextEvent) { + skipNextEvent = false; + return; + } + + undoList = []; + setState(() {}); + }); + + super.initState(); + } + + double _sliderValue = 0.0; + + final colors = [ + Colors.white, + Colors.red, + Colors.orange, + Colors.yellow, + Colors.green, + Colors.indigo, + Colors.blue, + Colors.black, + ]; + + Color _getColorFromSliderValue(double value) { + // Calculate the index based on the slider value + int index = (value * (colors.length - 1)).floor(); + int nextIndex = (index + 1).clamp(0, colors.length - 1); + + // Calculate the interpolation factor + double factor = value * (colors.length - 1) - index; + + // Interpolate between the two colors + return Color.lerp(colors[index], colors[nextIndex], factor)!; + } + + void _onSliderChanged(double value) { + setState(() { + _sliderValue = value; + currentColor = _getColorFromSliderValue(value); + }); + } + + @override + Widget build(BuildContext context) { + return Stack( + fit: StackFit.expand, + children: [ + Positioned.fill( + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + ), + child: Screenshot( + controller: screenshotController, + child: HandSignature( + control: widget.layerData.control, + color: currentColor, + width: 1.0, + maxWidth: 7.0, + type: SignatureDrawType.shape, + ), + ), + ), + ), + if (widget.layerData.isEditing) + Positioned( + top: 5, + left: 5, + right: 50, + child: Row( + children: [ + ActionButton( + FontAwesomeIcons.check, + onPressed: () async { + widget.layerData.isEditing = false; + }, + ), + Expanded(child: Container()), + ActionButton( + FontAwesomeIcons.arrowRotateLeft, + color: widget.layerData.control.paths.isNotEmpty + ? Colors.white + : Colors.white.withAlpha(80), + onPressed: () { + if (widget.layerData.control.paths.isEmpty) return; + skipNextEvent = true; + undoList.add(widget.layerData.control.paths.last); + widget.layerData.control.stepBack(); + setState(() {}); + }, + ), + ActionButton( + FontAwesomeIcons.arrowRotateRight, + color: undoList.isNotEmpty + ? Colors.white + : Colors.white.withAlpha(80), + onPressed: () { + if (undoList.isEmpty) return; + + widget.layerData.control.paths.add(undoList.removeLast()); + setState(() {}); + }, + ), + ], + ), + ), + if (widget.layerData.isEditing) + Positioned( + right: 20, + top: 50, + child: Stack( + children: [ + Container( + height: 240, + width: 40, + color: Colors.transparent, + ), + SizedBox( + height: 240, + width: 40, + child: Center( + child: Container( + alignment: Alignment.center, + width: 10, + height: 195, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: colors, + stops: List.generate(colors.length, + (index) => index / (colors.length - 1)), + ), + borderRadius: BorderRadius.circular(10), + ), + ), + ), + ), + Positioned.fill( + child: RotatedBox( + quarterTurns: 1, + child: Slider( + value: _sliderValue, + thumbColor: currentColor, + activeColor: Colors.transparent, + inactiveColor: Colors.transparent, + onChanged: _onSliderChanged, + min: 0.0, + max: 1.0, + divisions: 100, + ), + ), + ), + ], + ), + ), + if (!widget.layerData.isEditing) + Positioned.fill( + child: Container( + color: Colors.transparent, + )) + ], + ); + } +} diff --git a/lib/src/components/image_editor/layers/text_layer.dart b/lib/src/components/image_editor/layers/text_layer.dart index 944b9b4..857a4d2 100755 --- a/lib/src/components/image_editor/layers/text_layer.dart +++ b/lib/src/components/image_editor/layers/text_layer.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/components/image_editor/action_button.dart'; import 'package:twonly/src/components/image_editor/data/layer.dart'; -import 'package:twonly/src/views/camera_to_share/share_image_editor_view.dart'; /// Text layer class TextLayer extends StatefulWidget { @@ -148,9 +148,8 @@ class _TextViewState extends State { onTapUp: (d) { textController.text = ""; }, - child: FaIcon( + child: ActionButton( FontAwesomeIcons.trashCan, - shadows: ShareImageEditorView.iconShadow, color: deleteLayer ? Colors.red : Colors.white, ), ), diff --git a/lib/src/components/image_editor/layers_viewer.dart b/lib/src/components/image_editor/layers_viewer.dart index ca4e430..28b1970 100644 --- a/lib/src/components/image_editor/layers_viewer.dart +++ b/lib/src/components/image_editor/layers_viewer.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:twonly/src/components/image_editor/data/layer.dart'; import 'package:twonly/src/components/image_editor/layers/background_layer.dart'; +import 'package:twonly/src/components/image_editor/layers/draw_layer.dart'; import 'package:twonly/src/components/image_editor/layers/emoji_layer.dart'; import 'package:twonly/src/components/image_editor/layers/image_layer.dart'; import 'package:twonly/src/components/image_editor/layers/text_layer.dart'; @@ -20,42 +21,53 @@ class LayersViewer extends StatelessWidget { Widget build(BuildContext context) { return Stack( alignment: Alignment.center, - children: layers.map((layerItem) { - // Background layer - if (layerItem is BackgroundLayerData) { - return BackgroundLayer( + children: [ + // Background and Image layers at the bottom + ...layers + .where((layerItem) => + layerItem is BackgroundLayerData || layerItem is ImageLayerData) + .map((layerItem) { + if (layerItem is BackgroundLayerData) { + return BackgroundLayer( + layerData: layerItem, + onUpdate: onUpdate, + ); + } else if (layerItem is ImageLayerData) { + return ImageLayer( + layerData: layerItem, + onUpdate: onUpdate, + ); + } + return Container(); // Fallback, should not reach here + }), + + // Draw layer (if needed, can be placed anywhere) + ...layers.whereType().map((layerItem) { + return DrawLayer( layerData: layerItem, onUpdate: onUpdate, ); - } + }), - // Image layer - if (layerItem is ImageLayerData) { - return ImageLayer( - layerData: layerItem, - onUpdate: onUpdate, - ); - } - - // Emoji layer - if (layerItem is EmojiLayerData) { - return EmojiLayer( - layerData: layerItem, - onUpdate: onUpdate, - ); - } - - // Text layer - if (layerItem is TextLayerData) { - return TextLayer( - layerData: layerItem, - onUpdate: onUpdate, - ); - } - - // Blank layer - return Container(); - }).toList(), + // Emoji and Text layers at the top + ...layers + .where((layerItem) => + layerItem is EmojiLayerData || layerItem is TextLayerData) + .map((layerItem) { + if (layerItem is EmojiLayerData) { + return EmojiLayer( + layerData: layerItem, + onUpdate: onUpdate, + ); + } else if (layerItem is TextLayerData) { + return TextLayer( + layerData: layerItem, + onUpdate: onUpdate, + ); + } + return Container(); // Fallback, should not reach here + }), + ], ); } } diff --git a/lib/src/views/camera_to_share/camera_preview_view.dart b/lib/src/views/camera_to_share/camera_preview_view.dart index a1247cc..d17caa5 100644 --- a/lib/src/views/camera_to_share/camera_preview_view.dart +++ b/lib/src/views/camera_to_share/camera_preview_view.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:camerawesome/camerawesome_plugin.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -58,234 +59,254 @@ class _CameraPreviewViewState extends State { @override Widget build(BuildContext context) { return MediaViewSizing( - ClipRRect( - borderRadius: BorderRadius.circular(22), - child: CameraAwesomeBuilder.custom( - previewAlignment: Alignment.topLeft, - sensorConfig: SensorConfig.single( - aspectRatio: CameraAspectRatios.ratio_16_9, - zoom: 0.07, - ), - previewFit: CameraPreviewFit.contain, - progressIndicator: Container(), - onMediaCaptureEvent: (event) { - switch ((event.status, event.isPicture, event.isVideo)) { - case (MediaCaptureStatus.capturing, true, false): - debugPrint('Capturing picture...'); - case (MediaCaptureStatus.success, true, false): - event.captureRequest.when( - single: (single) async { - final imageBytes = await single.file?.readAsBytes(); - if (imageBytes == null || !context.mounted) return; - setState(() { - sharePreviewIsShown = true; - }); - await Navigator.push( - context, - PageRouteBuilder( - opaque: false, - pageBuilder: (context, a1, a2) => - ShareImageEditorView(imageBytes: imageBytes), - transitionsBuilder: - (context, animation, secondaryAnimation, child) { - return child; - }, - transitionDuration: Duration.zero, - reverseTransitionDuration: Duration.zero, - ), - ); - if (context.mounted) { - setState(() { - sharePreviewIsShown = false; - }); - } - }, - multiple: (multiple) { - multiple.fileBySensor.forEach((key, value) { - debugPrint('multiple image taken: $key ${value?.path}'); - }); - }, - ); - case (MediaCaptureStatus.failure, true, false): - debugPrint('Failed to capture picture: ${event.exception}'); - case (MediaCaptureStatus.capturing, false, true): - debugPrint('Capturing video...'); - case (MediaCaptureStatus.success, false, true): - event.captureRequest.when( - single: (single) { - debugPrint('Video saved: ${single.file?.path}'); - }, - multiple: (multiple) { - multiple.fileBySensor.forEach((key, value) { - debugPrint('multiple video taken: $key ${value?.path}'); - }); - }, - ); - case (MediaCaptureStatus.failure, false, true): - debugPrint('Failed to capture video: ${event.exception}'); - default: - debugPrint('Unknown event: $event'); - } - }, - builder: (cameraState, preview) { - return Stack( - //alignment: Alignment.bottomCenter, - children: [ - Positioned.fill( - child: GestureDetector( - onPanStart: (details) async { - setState(() { - _basePanY = details.localPosition.dy; - }); - }, - onPanUpdate: (details) async { - var diff = _basePanY - details.localPosition.dy; - if (diff > 200) diff = 200; - if (diff < 0) diff = 0; - var tmp = (diff / 200 * 50).toInt() / 50; - if (tmp != _lastZoom) { - cameraState.sensorConfig.setZoom(tmp); + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(22), + child: CameraAwesomeBuilder.custom( + previewAlignment: Alignment.topLeft, + sensorConfig: SensorConfig.single( + aspectRatio: CameraAspectRatios.ratio_16_9, + zoom: 0.07, + ), + previewFit: CameraPreviewFit.contain, + progressIndicator: Container(), + onMediaCaptureEvent: (event) { + switch ((event.status, event.isPicture, event.isVideo)) { + case (MediaCaptureStatus.capturing, true, false): + debugPrint('Capturing picture...'); + case (MediaCaptureStatus.success, true, false): + event.captureRequest.when( + single: (single) async { + final imageBytes = await single.file?.readAsBytes(); + if (imageBytes == null || !context.mounted) return; setState(() { - (tmp); - _lastZoom = tmp; + sharePreviewIsShown = true; }); - } - }, - onDoubleTap: () async { - cameraState.switchCameraSensor( - aspectRatio: CameraAspectRatios.ratio_16_9); - }, - ), - ), - if (!sharePreviewIsShown) - Positioned( - right: 0, - top: 100, - child: Container( - alignment: Alignment.bottomCenter, - padding: const EdgeInsets.symmetric(vertical: 16), - child: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - BottomButton( - icon: FontAwesomeIcons.repeat, - onTap: () async { - cameraState.switchCameraSensor( - aspectRatio: CameraAspectRatios.ratio_16_9); - }, - ), - SizedBox(height: 20), - BottomButton( - icon: FontAwesomeIcons.bolt, - color: isFlashOn - ? const Color.fromARGB(255, 255, 230, 0) - : const Color.fromARGB(158, 255, 255, 255), - onTap: () async { - if (isFlashOn) { - cameraState.sensorConfig - .setFlashMode(FlashMode.none); - isFlashOn = false; - } else { - cameraState.sensorConfig - .setFlashMode(FlashMode.always); - isFlashOn = true; - } - setState(() {}); - //cameraState.sensorConfig.switchCameraFlash(); - }, - ), - ], - ), + await Navigator.push( + context, + PageRouteBuilder( + opaque: false, + pageBuilder: (context, a1, a2) => + ShareImageEditorView(imageBytes: imageBytes), + transitionsBuilder: (context, animation, + secondaryAnimation, child) { + return child; + }, + transitionDuration: Duration.zero, + reverseTransitionDuration: Duration.zero, + ), + ); + if (context.mounted) { + setState(() { + sharePreviewIsShown = false; + }); + } + }, + multiple: (multiple) { + multiple.fileBySensor.forEach((key, value) { + debugPrint( + 'multiple image taken: $key ${value?.path}'); + }); + }, + ); + case (MediaCaptureStatus.failure, true, false): + debugPrint('Failed to capture picture: ${event.exception}'); + case (MediaCaptureStatus.capturing, false, true): + debugPrint('Capturing video...'); + case (MediaCaptureStatus.success, false, true): + event.captureRequest.when( + single: (single) { + debugPrint('Video saved: ${single.file?.path}'); + }, + multiple: (multiple) { + multiple.fileBySensor.forEach((key, value) { + debugPrint( + 'multiple video taken: $key ${value?.path}'); + }); + }, + ); + case (MediaCaptureStatus.failure, false, true): + debugPrint('Failed to capture video: ${event.exception}'); + default: + debugPrint('Unknown event: $event'); + } + }, + builder: (cameraState, preview) { + return Stack( + //alignment: Alignment.bottomCenter, + children: [ + Positioned.fill( + child: GestureDetector( + onPanStart: (details) async { + setState(() { + _basePanY = details.localPosition.dy; + }); + }, + onPanUpdate: (details) async { + var diff = _basePanY - details.localPosition.dy; + if (diff > 200) diff = 200; + if (diff < 0) diff = 0; + var tmp = (diff / 200 * 50).toInt() / 50; + if (tmp != _lastZoom) { + cameraState.sensorConfig.setZoom(tmp); + setState(() { + (tmp); + _lastZoom = tmp; + }); + } + }, + onDoubleTap: () async { + cameraState.switchCameraSensor( + aspectRatio: CameraAspectRatios.ratio_16_9); + }, ), ), - ), - if (!sharePreviewIsShown) - Positioned( - bottom: 30, - left: 0, - right: 0, - child: Align( - alignment: Alignment.bottomCenter, - child: Column( - children: [ - AwesomeZoomSelector(state: cameraState), - const SizedBox(height: 30), - GestureDetector( - onTap: () async { - cameraState.when( - onPhotoMode: (picState) => - picState.takePhoto()); - }, - onLongPress: () async {}, - child: Align( - alignment: Alignment.center, - child: Container( - height: 100, - width: 100, - clipBehavior: Clip.antiAliasWithSaveLayer, - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - width: 7, - color: Colors.white, + if (!sharePreviewIsShown) + Positioned( + right: 0, + top: 100, + child: Container( + alignment: Alignment.bottomCenter, + padding: const EdgeInsets.symmetric(vertical: 16), + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + BottomButton( + icon: FontAwesomeIcons.repeat, + onTap: () async { + cameraState.switchCameraSensor( + aspectRatio: + CameraAspectRatios.ratio_16_9); + }, + ), + SizedBox(height: 20), + BottomButton( + icon: FontAwesomeIcons.bolt, + color: isFlashOn + ? const Color.fromARGB(255, 255, 230, 0) + : const Color.fromARGB( + 158, 255, 255, 255), + onTap: () async { + if (isFlashOn) { + cameraState.sensorConfig + .setFlashMode(FlashMode.none); + isFlashOn = false; + } else { + cameraState.sensorConfig + .setFlashMode(FlashMode.always); + isFlashOn = true; + } + setState(() {}); + //cameraState.sensorConfig.switchCameraFlash(); + }, + ), + ], + ), + ), + ), + ), + if (!sharePreviewIsShown) + Positioned( + bottom: 30, + left: 0, + right: 0, + child: Align( + alignment: Alignment.bottomCenter, + child: Column( + children: [ + AwesomeZoomSelector(state: cameraState), + const SizedBox(height: 30), + GestureDetector( + onTap: () async { + cameraState.when( + onPhotoMode: (picState) => + picState.takePhoto()); + }, + onLongPress: () async {}, + child: Align( + alignment: Alignment.center, + child: Container( + height: 100, + width: 100, + clipBehavior: Clip.antiAliasWithSaveLayer, + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 7, + color: Colors.white, + ), + ), ), ), ), - ), + ], ), - ], + ), ), - ), - ), - ], - ); - }, - saveConfig: SaveConfig.photoAndVideo( - photoPathBuilder: (sensors) async { - final Directory extDir = await getTemporaryDirectory(); - final testDir = await Directory( - '${extDir.path}/images', - ).create(recursive: true); - final String filePath = - '${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.jpg'; - return SingleCaptureRequest(filePath, sensors.first); - // // Separate pictures taken with front and back camera - // return MultipleCaptureRequest( - // { - // for (final sensor in sensors) - // sensor: - // '${testDir.path}/${sensor.position == SensorPosition.front ? 'front_' : "back_"}${DateTime.now().millisecondsSinceEpoch}.jpg', + ], + ); + }, + saveConfig: SaveConfig.photoAndVideo( + photoPathBuilder: (sensors) async { + final Directory extDir = await getTemporaryDirectory(); + final testDir = await Directory( + '${extDir.path}/images', + ).create(recursive: true); + final String filePath = + '${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.jpg'; + return SingleCaptureRequest(filePath, sensors.first); + // // Separate pictures taken with front and back camera + // return MultipleCaptureRequest( + // { + // for (final sensor in sensors) + // sensor: + // '${testDir.path}/${sensor.position == SensorPosition.front ? 'front_' : "back_"}${DateTime.now().millisecondsSinceEpoch}.jpg', + // }, + // ); + }, + ), + // onPreviewTapBuilder: (state) => OnPreviewTap( + // onTap: (Offset position, PreviewSize flutterPreviewSize, + // PreviewSize pixelPreviewSize) { + // state.when(onPhotoMode: (picState) => picState.takePhoto()); // }, - // ); - }, + // onTapPainter: (tapPosition) => TweenAnimationBuilder( + // key: ValueKey(tapPosition), + // tween: Tween(begin: 1.0, end: 0.0), + // duration: const Duration(milliseconds: 500), + // builder: (context, anim, child) { + // return Transform.rotate( + // angle: anim * 2 * pi, + // child: Transform.scale( + // scale: 4 * anim, + // child: child, + // ), + // ); + // }, + // child: const Icon( + // Icons.camera, + // color: Colors.white, + // ), + // ), + // ), + ), ), - // onPreviewTapBuilder: (state) => OnPreviewTap( - // onTap: (Offset position, PreviewSize flutterPreviewSize, - // PreviewSize pixelPreviewSize) { - // state.when(onPhotoMode: (picState) => picState.takePhoto()); - // }, - // onTapPainter: (tapPosition) => TweenAnimationBuilder( - // key: ValueKey(tapPosition), - // tween: Tween(begin: 1.0, end: 0.0), - // duration: const Duration(milliseconds: 500), - // builder: (context, anim, child) { - // return Transform.rotate( - // angle: anim * 2 * pi, - // child: Transform.scale( - // scale: 4 * anim, - // child: child, - // ), - // ); - // }, - // child: const Icon( - // Icons.camera, - // color: Colors.white, - // ), - // ), - // ), - ), + if (sharePreviewIsShown) + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 100.0, + sigmaY: 100.0, + ), + child: Center( + child: CircularProgressIndicator(), + ), + ), + ) + ], ), ); } diff --git a/lib/src/views/camera_to_share/share_image_editor_view.dart b/lib/src/views/camera_to_share/share_image_editor_view.dart index 4fa1e7c..f553072 100644 --- a/lib/src/views/camera_to_share/share_image_editor_view.dart +++ b/lib/src/views/camera_to_share/share_image_editor_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/components/image_editor/action_button.dart'; import 'package:twonly/src/components/media_view_sizing.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera_to_share/share_image_view.dart'; @@ -11,7 +12,6 @@ import 'package:twonly/src/components/image_editor/data/layer.dart'; import 'package:twonly/src/components/image_editor/layers_viewer.dart'; import 'package:twonly/src/components/image_editor/modules/all_emojis.dart'; import 'package:screenshot/screenshot.dart'; -import 'package:hand_signature/signature.dart'; List layers = []; List undoLayers = []; @@ -20,18 +20,13 @@ List removedLayers = []; class ShareImageEditorView extends StatefulWidget { const ShareImageEditorView({super.key, required this.imageBytes}); final Uint8List imageBytes; - static List get iconShadow => [ - Shadow( - color: const Color.fromARGB(122, 0, 0, 0), - blurRadius: 5.0, - ) - ]; @override State createState() => _ShareImageEditorView(); } class _ShareImageEditorView extends State { bool _imageSaved = false; + bool _imageSaving = false; ImageItem currentImage = ImageItem(); ScreenshotController screenshotController = ScreenshotController(); @@ -49,24 +44,90 @@ class _ShareImageEditorView extends State { super.dispose(); } - List get filterActions { + List get actionsAtTheRight { + if (layers.isNotEmpty && + layers.last.isEditing && + layers.last.hasCustomActionButtons) { + return []; + } + return [ + BottomButton( + icon: FontAwesomeIcons.font, + onTap: () async { + undoLayers.clear(); + removedLayers.clear(); + layers.add(TextLayerData()); + setState(() {}); + }, + ), + BottomButton( + icon: FontAwesomeIcons.pencil, + onTap: () async { + // var drawing = await Navigator.push( + // context, + // PageRouteBuilder( + // opaque: false, + // pageBuilder: (context, a, b) => ImageEditorDrawing( + // image: currentImage, + // ), + // transitionDuration: Duration.zero, + // reverseTransitionDuration: Duration.zero, + // ), + // ); + + // if (drawing != null) { + undoLayers.clear(); + removedLayers.clear(); + + layers.add(DrawLayerData()); + + // setState(() {}); + // } + }, + ), + BottomButton( + icon: FontAwesomeIcons.faceGrinWide, + onTap: () async { + EmojiLayerData? layer = await showModalBottomSheet( + context: context, + backgroundColor: Colors.black, + builder: (BuildContext context) { + return const Emojis(); + }, + ); + + if (layer == null) return; + + undoLayers.clear(); + removedLayers.clear(); + layers.add(layer); + + setState(() {}); + }, + ), + ]; + } + + List get actionsAtTheTop { + if (layers.isNotEmpty && + layers.last.isEditing && + layers.last.hasCustomActionButtons) { + return []; + } return [ - IconButton( - icon: FaIcon(FontAwesomeIcons.xmark, - size: 30, shadows: ShareImageEditorView.iconShadow), - color: Colors.white, + ActionButton( + FontAwesomeIcons.xmark, onPressed: () async { Navigator.pop(context); }, ), Expanded(child: Container()), - IconButton( - padding: const EdgeInsets.symmetric(horizontal: 8), - icon: FaIcon(FontAwesomeIcons.rotateLeft, - color: layers.length > 1 || removedLayers.isNotEmpty - ? Colors.white - : Colors.grey, - shadows: ShareImageEditorView.iconShadow), + const SizedBox(width: 8), + ActionButton( + FontAwesomeIcons.rotateLeft, + color: layers.length > 1 || removedLayers.isNotEmpty + ? Colors.white + : Colors.grey, onPressed: () { if (removedLayers.isNotEmpty) { layers.add(removedLayers.removeLast()); @@ -79,16 +140,13 @@ class _ShareImageEditorView extends State { setState(() {}); }, ), - IconButton( - padding: const EdgeInsets.symmetric(horizontal: 8), - icon: FaIcon(FontAwesomeIcons.rotateRight, - color: undoLayers.isNotEmpty ? Colors.white : Colors.grey, - shadows: ShareImageEditorView.iconShadow), + const SizedBox(width: 8), + ActionButton( + FontAwesomeIcons.rotateRight, + color: undoLayers.isNotEmpty ? Colors.white : Colors.grey, onPressed: () { if (undoLayers.isEmpty) return; - layers.add(undoLayers.removeLast()); - setState(() {}); }, ), @@ -168,7 +226,7 @@ class _ShareImageEditorView extends State { right: 0, child: SafeArea( child: Row( - children: filterActions, + children: actionsAtTheTop, ), ), ), @@ -181,69 +239,7 @@ class _ShareImageEditorView extends State { child: SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.center, - children: [ - BottomButton( - icon: FontAwesomeIcons.font, - onTap: () async { - undoLayers.clear(); - removedLayers.clear(); - layers.add(TextLayerData()); - setState(() {}); - }, - ), - SizedBox(height: 20), - BottomButton( - icon: FontAwesomeIcons.pencil, - onTap: () async { - var drawing = await Navigator.push( - context, - PageRouteBuilder( - opaque: false, - pageBuilder: (context, a, b) => ImageEditorDrawing( - image: currentImage, - ), - transitionDuration: Duration.zero, - reverseTransitionDuration: Duration.zero, - ), - ); - - if (drawing != null) { - undoLayers.clear(); - removedLayers.clear(); - - layers.add( - ImageLayerData( - image: ImageItem(drawing), - offset: Offset(0, 0), - ), - ); - - setState(() {}); - } - }, - ), - SizedBox(height: 20), - BottomButton( - icon: FontAwesomeIcons.faceGrinWide, - onTap: () async { - EmojiLayerData? layer = await showModalBottomSheet( - context: context, - backgroundColor: Colors.black, - builder: (BuildContext context) { - return const Emojis(); - }, - ); - - if (layer == null) return; - - undoLayers.clear(); - removedLayers.clear(); - layers.add(layer); - - setState(() {}); - }, - ), - ], + children: actionsAtTheRight, ), ), ), @@ -259,9 +255,14 @@ class _ShareImageEditorView extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ OutlinedButton.icon( - icon: _imageSaved - ? Icon(Icons.check) - : FaIcon(FontAwesomeIcons.floppyDisk), + icon: _imageSaving + ? SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator(strokeWidth: 1)) + : _imageSaved + ? Icon(Icons.check) + : FaIcon(FontAwesomeIcons.floppyDisk), style: OutlinedButton.styleFrom( iconColor: _imageSaved ? Theme.of(context).colorScheme.outline @@ -271,11 +272,15 @@ class _ShareImageEditorView extends State { : Theme.of(context).colorScheme.primary, ), onPressed: () async { + setState(() { + _imageSaving = true; + }); Uint8List? imageBytes = await getMergedImage(); if (imageBytes == null || !context.mounted) return; final res = await saveImageToGallery(imageBytes); if (res == null) { setState(() { + _imageSaving = false; _imageSaved = true; }); } @@ -341,10 +346,10 @@ class BottomButton extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 12), child: Column( children: [ - FaIcon( + ActionButton( icon, color: color, - shadows: ShareImageEditorView.iconShadow, + onPressed: onTap ?? () {}, ), const SizedBox(height: 8), ], @@ -354,240 +359,20 @@ class BottomButton extends StatelessWidget { } } -/// Show image drawing surface over image -class ImageEditorDrawing extends StatefulWidget { - final ImageItem image; +// /// Show image drawing surface over image +// class ImageEditorDrawing extends StatefulWidget { +// final ImageItem image; - const ImageEditorDrawing({ - super.key, - required this.image, - }); +// const ImageEditorDrawing({ +// super.key, +// required this.image, +// }); - @override - State createState() => _ImageEditorDrawingState(); -} +// @override +// State createState() => _ImageEditorDrawingState(); +// } -class _ImageEditorDrawingState extends State { - Color pickerColor = Colors.white, currentColor = Colors.white; +// class _ImageEditorDrawingState extends State { - var screenshotController = ScreenshotController(); - - final control = HandSignatureControl( - threshold: 3.0, - smoothRatio: 0.65, - velocityRange: 2.0, - ); - - List undoList = []; - bool skipNextEvent = false; - - // void changeColor(Colors color) { - // currentColor = color.color; - // currentBackgroundColor = color.background; - - // setState(() {}); - // } - - @override - void initState() { - control.addListener(() { - if (control.hasActivePath) return; - - if (skipNextEvent) { - skipNextEvent = false; - return; - } - - undoList = []; - setState(() {}); - }); - - super.initState(); - } - - @override - Widget build(BuildContext context) { - final colors = [ - Colors.black, - Colors.white, - Colors.blue, - Colors.green, - Colors.pink, - Colors.purple, - Colors.brown, - Colors.indigo, - ]; - - return Scaffold( - backgroundColor: Colors.red.withAlpha(0), - body: SafeArea( - child: Stack( - fit: StackFit.expand, - children: [ - Positioned.fill( - top: 0, - child: Container( - height: 600, - width: 300, - decoration: BoxDecoration( - color: const Color.fromARGB(0, 210, 7, 7), - ), - // child: Container(), - child: Screenshot( - controller: screenshotController, - // image: widget.options.showBackground - // ? DecorationImage( - // image: Image.memory(widget.image.bytes).image, - // fit: BoxFit.contain, - // ) - // : null, - // child: Container(), - child: HandSignature( - control: control, - color: currentColor, - width: 1.0, - maxWidth: 7.0, - type: SignatureDrawType.shape, - ), - ), - ), - ), - Positioned( - top: 100, - right: 50, - child: Column( - children: [ - IconButton( - padding: const EdgeInsets.symmetric(horizontal: 8), - icon: const Icon(Icons.clear), - onPressed: () { - Navigator.pop(context); - }, - ), - IconButton( - padding: const EdgeInsets.symmetric(horizontal: 8), - icon: Icon( - Icons.undo, - color: control.paths.isNotEmpty - ? Colors.white - : Colors.white.withAlpha(80), - ), - onPressed: () { - if (control.paths.isEmpty) return; - skipNextEvent = true; - undoList.add(control.paths.last); - control.stepBack(); - setState(() {}); - }, - ), - IconButton( - padding: const EdgeInsets.symmetric(horizontal: 8), - icon: Icon( - Icons.redo, - color: undoList.isNotEmpty - ? Colors.white - : Colors.white.withAlpha(80), - ), - onPressed: () { - if (undoList.isEmpty) return; - - control.paths.add(undoList.removeLast()); - setState(() {}); - }, - ), - IconButton( - padding: const EdgeInsets.symmetric(horizontal: 8), - icon: const Icon(Icons.check), - onPressed: () async { - if (control.paths.isEmpty) return Navigator.pop(context); - - var data = await control.toImage( - color: currentColor, - height: widget.image.height, - width: widget.image.width, - ); - - if (!mounted) return; - - return Navigator.pop(context, data!.buffer.asUint8List()); - - // var loadingScreen = showLoadingScreen(context); - // var image = await screenshotController.capture(); - // loadingScreen.hide(); - - // if (!mounted) return; - - // return Navigator.pop(context, image); - }, - ), - ], - ), - ), - Positioned( - right: 0, - top: 50, - child: Container( - child: Container( - // height: 80, - padding: EdgeInsets.symmetric(vertical: 10), - decoration: BoxDecoration( - color: const Color.fromARGB(130, 0, 0, 0), - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - child: Column( - children: [ - for (var color in colors) - ColorButton( - color: color, - onTap: (color) { - currentColor = color; - setState(() {}); - }, - isSelected: color == currentColor, - ), - ], - ), - ), - ), - ), - ], - ), - ), - ); - } -} - -class ColorButton extends StatelessWidget { - final Color color; - final Function(Color) onTap; - final bool isSelected; - - const ColorButton({ - super.key, - required this.color, - required this.onTap, - this.isSelected = false, - }); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - onTap(color); - }, - child: Container( - height: 17, - width: 17, - margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isSelected ? Colors.white : Colors.white54, - width: isSelected ? 2 : 1, - ), - ), - ), - ); - } -} +// } +// } diff --git a/pubspec.yaml b/pubspec.yaml index b82a8c5..fc0b3e1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,8 @@ environment: dependencies: camerawesome: ^2.1.0 + # camerawesome: + # path: ../CamerAwesome collection: ^1.18.0 connectivity_plus: ^6.1.2 cv: ^1.1.3