improve draw editor

This commit is contained in:
otsmr 2025-02-03 19:16:32 +01:00
parent fdaa1ff7dd
commit 5f45de620a
9 changed files with 674 additions and 580 deletions

25
.vscode/launch.json vendored Normal file
View file

@ -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"
}
]
}

View file

@ -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,
);
}
}

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hand_signature/signature.dart';
import 'package:twonly/src/components/image_editor/data/image_item.dart'; import 'package:twonly/src/components/image_editor/data/image_item.dart';
/// Layer class with some common properties /// Layer class with some common properties
@ -7,12 +8,14 @@ class Layer {
double rotation, scale, opacity; double rotation, scale, opacity;
bool isEditing; bool isEditing;
bool isDeleted; bool isDeleted;
bool hasCustomActionButtons;
Layer({ Layer({
this.offset = const Offset(0, 0), this.offset = const Offset(0, 0),
this.opacity = 1, this.opacity = 1,
this.isEditing = false, this.isEditing = false,
this.isDeleted = false, this.isDeleted = false,
this.hasCustomActionButtons = false,
this.rotation = 0, this.rotation = 0,
this.scale = 1, this.scale = 1,
}); });
@ -71,3 +74,22 @@ class TextLayerData extends Layer {
super.isEditing = true, 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,
});
}

View file

@ -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<DrawLayer> {
Color pickerColor = Colors.white, currentColor = Colors.white;
var screenshotController = ScreenshotController();
List<CubicPath> 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: <Widget>[
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,
))
],
);
}
}

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/components/image_editor/data/layer.dart';
import 'package:twonly/src/views/camera_to_share/share_image_editor_view.dart';
/// Text layer /// Text layer
class TextLayer extends StatefulWidget { class TextLayer extends StatefulWidget {
@ -148,9 +148,8 @@ class _TextViewState extends State<TextLayer> {
onTapUp: (d) { onTapUp: (d) {
textController.text = ""; textController.text = "";
}, },
child: FaIcon( child: ActionButton(
FontAwesomeIcons.trashCan, FontAwesomeIcons.trashCan,
shadows: ShareImageEditorView.iconShadow,
color: deleteLayer ? Colors.red : Colors.white, color: deleteLayer ? Colors.red : Colors.white,
), ),
), ),

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/components/image_editor/data/layer.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/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/emoji_layer.dart';
import 'package:twonly/src/components/image_editor/layers/image_layer.dart'; import 'package:twonly/src/components/image_editor/layers/image_layer.dart';
import 'package:twonly/src/components/image_editor/layers/text_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) { Widget build(BuildContext context) {
return Stack( return Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: layers.map((layerItem) { children: [
// Background layer // Background and Image layers at the bottom
if (layerItem is BackgroundLayerData) { ...layers
return BackgroundLayer( .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<DrawLayerData>().map((layerItem) {
return DrawLayer(
layerData: layerItem, layerData: layerItem,
onUpdate: onUpdate, onUpdate: onUpdate,
); );
} }),
// Image layer // Emoji and Text layers at the top
if (layerItem is ImageLayerData) { ...layers
return ImageLayer( .where((layerItem) =>
layerData: layerItem, layerItem is EmojiLayerData || layerItem is TextLayerData)
onUpdate: onUpdate, .map((layerItem) {
); if (layerItem is EmojiLayerData) {
} return EmojiLayer(
layerData: layerItem,
// Emoji layer onUpdate: onUpdate,
if (layerItem is EmojiLayerData) { );
return EmojiLayer( } else if (layerItem is TextLayerData) {
layerData: layerItem, return TextLayer(
onUpdate: onUpdate, layerData: layerItem,
); onUpdate: onUpdate,
} );
}
// Text layer return Container(); // Fallback, should not reach here
if (layerItem is TextLayerData) { }),
return TextLayer( ],
layerData: layerItem,
onUpdate: onUpdate,
);
}
// Blank layer
return Container();
}).toList(),
); );
} }
} }

View file

@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:camerawesome/camerawesome_plugin.dart'; import 'package:camerawesome/camerawesome_plugin.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
@ -58,234 +59,254 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MediaViewSizing( return MediaViewSizing(
ClipRRect( Stack(
borderRadius: BorderRadius.circular(22), children: [
child: CameraAwesomeBuilder.custom( ClipRRect(
previewAlignment: Alignment.topLeft, borderRadius: BorderRadius.circular(22),
sensorConfig: SensorConfig.single( child: CameraAwesomeBuilder.custom(
aspectRatio: CameraAspectRatios.ratio_16_9, previewAlignment: Alignment.topLeft,
zoom: 0.07, sensorConfig: SensorConfig.single(
), aspectRatio: CameraAspectRatios.ratio_16_9,
previewFit: CameraPreviewFit.contain, zoom: 0.07,
progressIndicator: Container(), ),
onMediaCaptureEvent: (event) { previewFit: CameraPreviewFit.contain,
switch ((event.status, event.isPicture, event.isVideo)) { progressIndicator: Container(),
case (MediaCaptureStatus.capturing, true, false): onMediaCaptureEvent: (event) {
debugPrint('Capturing picture...'); switch ((event.status, event.isPicture, event.isVideo)) {
case (MediaCaptureStatus.success, true, false): case (MediaCaptureStatus.capturing, true, false):
event.captureRequest.when( debugPrint('Capturing picture...');
single: (single) async { case (MediaCaptureStatus.success, true, false):
final imageBytes = await single.file?.readAsBytes(); event.captureRequest.when(
if (imageBytes == null || !context.mounted) return; single: (single) async {
setState(() { final imageBytes = await single.file?.readAsBytes();
sharePreviewIsShown = true; if (imageBytes == null || !context.mounted) return;
});
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(() { setState(() {
(tmp); sharePreviewIsShown = true;
_lastZoom = tmp;
}); });
} await Navigator.push(
}, context,
onDoubleTap: () async { PageRouteBuilder(
cameraState.switchCameraSensor( opaque: false,
aspectRatio: CameraAspectRatios.ratio_16_9); pageBuilder: (context, a1, a2) =>
}, ShareImageEditorView(imageBytes: imageBytes),
), transitionsBuilder: (context, animation,
), secondaryAnimation, child) {
if (!sharePreviewIsShown) return child;
Positioned( },
right: 0, transitionDuration: Duration.zero,
top: 100, reverseTransitionDuration: Duration.zero,
child: Container( ),
alignment: Alignment.bottomCenter, );
padding: const EdgeInsets.symmetric(vertical: 16), if (context.mounted) {
child: SafeArea( setState(() {
child: Column( sharePreviewIsShown = false;
mainAxisAlignment: MainAxisAlignment.center, });
children: <Widget>[ }
BottomButton( },
icon: FontAwesomeIcons.repeat, multiple: (multiple) {
onTap: () async { multiple.fileBySensor.forEach((key, value) {
cameraState.switchCameraSensor( debugPrint(
aspectRatio: CameraAspectRatios.ratio_16_9); 'multiple image taken: $key ${value?.path}');
}, });
), },
SizedBox(height: 20), );
BottomButton( case (MediaCaptureStatus.failure, true, false):
icon: FontAwesomeIcons.bolt, debugPrint('Failed to capture picture: ${event.exception}');
color: isFlashOn case (MediaCaptureStatus.capturing, false, true):
? const Color.fromARGB(255, 255, 230, 0) debugPrint('Capturing video...');
: const Color.fromARGB(158, 255, 255, 255), case (MediaCaptureStatus.success, false, true):
onTap: () async { event.captureRequest.when(
if (isFlashOn) { single: (single) {
cameraState.sensorConfig debugPrint('Video saved: ${single.file?.path}');
.setFlashMode(FlashMode.none); },
isFlashOn = false; multiple: (multiple) {
} else { multiple.fileBySensor.forEach((key, value) {
cameraState.sensorConfig debugPrint(
.setFlashMode(FlashMode.always); 'multiple video taken: $key ${value?.path}');
isFlashOn = true; });
} },
setState(() {}); );
//cameraState.sensorConfig.switchCameraFlash(); 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)
if (!sharePreviewIsShown) Positioned(
Positioned( right: 0,
bottom: 30, top: 100,
left: 0, child: Container(
right: 0, alignment: Alignment.bottomCenter,
child: Align( padding: const EdgeInsets.symmetric(vertical: 16),
alignment: Alignment.bottomCenter, child: SafeArea(
child: Column( child: Column(
children: [ mainAxisAlignment: MainAxisAlignment.center,
AwesomeZoomSelector(state: cameraState), children: <Widget>[
const SizedBox(height: 30), BottomButton(
GestureDetector( icon: FontAwesomeIcons.repeat,
onTap: () async { onTap: () async {
cameraState.when( cameraState.switchCameraSensor(
onPhotoMode: (picState) => aspectRatio:
picState.takePhoto()); CameraAspectRatios.ratio_16_9);
}, },
onLongPress: () async {}, ),
child: Align( SizedBox(height: 20),
alignment: Alignment.center, BottomButton(
child: Container( icon: FontAwesomeIcons.bolt,
height: 100, color: isFlashOn
width: 100, ? const Color.fromARGB(255, 255, 230, 0)
clipBehavior: Clip.antiAliasWithSaveLayer, : const Color.fromARGB(
padding: const EdgeInsets.all(2), 158, 255, 255, 255),
decoration: BoxDecoration( onTap: () async {
shape: BoxShape.circle, if (isFlashOn) {
border: Border.all( cameraState.sensorConfig
width: 7, .setFlashMode(FlashMode.none);
color: Colors.white, 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 {
saveConfig: SaveConfig.photoAndVideo( final Directory extDir = await getTemporaryDirectory();
photoPathBuilder: (sensors) async { final testDir = await Directory(
final Directory extDir = await getTemporaryDirectory(); '${extDir.path}/images',
final testDir = await Directory( ).create(recursive: true);
'${extDir.path}/images', final String filePath =
).create(recursive: true); '${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.jpg';
final String filePath = return SingleCaptureRequest(filePath, sensors.first);
'${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.jpg'; // // Separate pictures taken with front and back camera
return SingleCaptureRequest(filePath, sensors.first); // return MultipleCaptureRequest(
// // Separate pictures taken with front and back camera // {
// return MultipleCaptureRequest( // for (final sensor in sensors)
// { // sensor:
// for (final sensor in sensors) // '${testDir.path}/${sensor.position == SensorPosition.front ? 'front_' : "back_"}${DateTime.now().millisecondsSinceEpoch}.jpg',
// 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<double>(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( if (sharePreviewIsShown)
// onTap: (Offset position, PreviewSize flutterPreviewSize, Positioned.fill(
// PreviewSize pixelPreviewSize) { child: BackdropFilter(
// state.when(onPhotoMode: (picState) => picState.takePhoto()); filter: ImageFilter.blur(
// }, sigmaX: 100.0,
// onTapPainter: (tapPosition) => TweenAnimationBuilder( sigmaY: 100.0,
// key: ValueKey(tapPosition), ),
// tween: Tween<double>(begin: 1.0, end: 0.0), child: Center(
// duration: const Duration(milliseconds: 500), child: CircularProgressIndicator(),
// 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,
// ),
// ),
// ),
),
), ),
); );
} }

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/components/media_view_sizing.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera_to_share/share_image_view.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/layers_viewer.dart';
import 'package:twonly/src/components/image_editor/modules/all_emojis.dart'; import 'package:twonly/src/components/image_editor/modules/all_emojis.dart';
import 'package:screenshot/screenshot.dart'; import 'package:screenshot/screenshot.dart';
import 'package:hand_signature/signature.dart';
List<Layer> layers = []; List<Layer> layers = [];
List<Layer> undoLayers = []; List<Layer> undoLayers = [];
@ -20,18 +20,13 @@ List<Layer> removedLayers = [];
class ShareImageEditorView extends StatefulWidget { class ShareImageEditorView extends StatefulWidget {
const ShareImageEditorView({super.key, required this.imageBytes}); const ShareImageEditorView({super.key, required this.imageBytes});
final Uint8List imageBytes; final Uint8List imageBytes;
static List<Shadow> get iconShadow => [
Shadow(
color: const Color.fromARGB(122, 0, 0, 0),
blurRadius: 5.0,
)
];
@override @override
State<ShareImageEditorView> createState() => _ShareImageEditorView(); State<ShareImageEditorView> createState() => _ShareImageEditorView();
} }
class _ShareImageEditorView extends State<ShareImageEditorView> { class _ShareImageEditorView extends State<ShareImageEditorView> {
bool _imageSaved = false; bool _imageSaved = false;
bool _imageSaving = false;
ImageItem currentImage = ImageItem(); ImageItem currentImage = ImageItem();
ScreenshotController screenshotController = ScreenshotController(); ScreenshotController screenshotController = ScreenshotController();
@ -49,24 +44,90 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
super.dispose(); super.dispose();
} }
List<Widget> get filterActions { List<Widget> get actionsAtTheRight {
if (layers.isNotEmpty &&
layers.last.isEditing &&
layers.last.hasCustomActionButtons) {
return [];
}
return <Widget>[
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<Widget> get actionsAtTheTop {
if (layers.isNotEmpty &&
layers.last.isEditing &&
layers.last.hasCustomActionButtons) {
return [];
}
return [ return [
IconButton( ActionButton(
icon: FaIcon(FontAwesomeIcons.xmark, FontAwesomeIcons.xmark,
size: 30, shadows: ShareImageEditorView.iconShadow),
color: Colors.white,
onPressed: () async { onPressed: () async {
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
Expanded(child: Container()), Expanded(child: Container()),
IconButton( const SizedBox(width: 8),
padding: const EdgeInsets.symmetric(horizontal: 8), ActionButton(
icon: FaIcon(FontAwesomeIcons.rotateLeft, FontAwesomeIcons.rotateLeft,
color: layers.length > 1 || removedLayers.isNotEmpty color: layers.length > 1 || removedLayers.isNotEmpty
? Colors.white ? Colors.white
: Colors.grey, : Colors.grey,
shadows: ShareImageEditorView.iconShadow),
onPressed: () { onPressed: () {
if (removedLayers.isNotEmpty) { if (removedLayers.isNotEmpty) {
layers.add(removedLayers.removeLast()); layers.add(removedLayers.removeLast());
@ -79,16 +140,13 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
setState(() {}); setState(() {});
}, },
), ),
IconButton( const SizedBox(width: 8),
padding: const EdgeInsets.symmetric(horizontal: 8), ActionButton(
icon: FaIcon(FontAwesomeIcons.rotateRight, FontAwesomeIcons.rotateRight,
color: undoLayers.isNotEmpty ? Colors.white : Colors.grey, color: undoLayers.isNotEmpty ? Colors.white : Colors.grey,
shadows: ShareImageEditorView.iconShadow),
onPressed: () { onPressed: () {
if (undoLayers.isEmpty) return; if (undoLayers.isEmpty) return;
layers.add(undoLayers.removeLast()); layers.add(undoLayers.removeLast());
setState(() {}); setState(() {});
}, },
), ),
@ -168,7 +226,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
right: 0, right: 0,
child: SafeArea( child: SafeArea(
child: Row( child: Row(
children: filterActions, children: actionsAtTheTop,
), ),
), ),
), ),
@ -181,69 +239,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
child: SafeArea( child: SafeArea(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: actionsAtTheRight,
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(() {});
},
),
],
), ),
), ),
), ),
@ -259,9 +255,14 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
OutlinedButton.icon( OutlinedButton.icon(
icon: _imageSaved icon: _imageSaving
? Icon(Icons.check) ? SizedBox(
: FaIcon(FontAwesomeIcons.floppyDisk), width: 12,
height: 12,
child: CircularProgressIndicator(strokeWidth: 1))
: _imageSaved
? Icon(Icons.check)
: FaIcon(FontAwesomeIcons.floppyDisk),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
iconColor: _imageSaved iconColor: _imageSaved
? Theme.of(context).colorScheme.outline ? Theme.of(context).colorScheme.outline
@ -271,11 +272,15 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
: Theme.of(context).colorScheme.primary, : Theme.of(context).colorScheme.primary,
), ),
onPressed: () async { onPressed: () async {
setState(() {
_imageSaving = true;
});
Uint8List? imageBytes = await getMergedImage(); Uint8List? imageBytes = await getMergedImage();
if (imageBytes == null || !context.mounted) return; if (imageBytes == null || !context.mounted) return;
final res = await saveImageToGallery(imageBytes); final res = await saveImageToGallery(imageBytes);
if (res == null) { if (res == null) {
setState(() { setState(() {
_imageSaving = false;
_imageSaved = true; _imageSaved = true;
}); });
} }
@ -341,10 +346,10 @@ class BottomButton extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column( child: Column(
children: [ children: [
FaIcon( ActionButton(
icon, icon,
color: color, color: color,
shadows: ShareImageEditorView.iconShadow, onPressed: onTap ?? () {},
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
@ -354,240 +359,20 @@ class BottomButton extends StatelessWidget {
} }
} }
/// Show image drawing surface over image // /// Show image drawing surface over image
class ImageEditorDrawing extends StatefulWidget { // class ImageEditorDrawing extends StatefulWidget {
final ImageItem image; // final ImageItem image;
const ImageEditorDrawing({ // const ImageEditorDrawing({
super.key, // super.key,
required this.image, // required this.image,
}); // });
@override // @override
State<ImageEditorDrawing> createState() => _ImageEditorDrawingState(); // State<ImageEditorDrawing> createState() => _ImageEditorDrawingState();
} // }
class _ImageEditorDrawingState extends State<ImageEditorDrawing> { // class _ImageEditorDrawingState extends State<ImageEditorDrawing> {
Color pickerColor = Colors.white, currentColor = Colors.white;
var screenshotController = ScreenshotController(); // }
// }
final control = HandSignatureControl(
threshold: 3.0,
smoothRatio: 0.65,
velocityRange: 2.0,
);
List<CubicPath> 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: <Widget>[
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,
),
),
),
);
}
}

View file

@ -11,6 +11,8 @@ environment:
dependencies: dependencies:
camerawesome: ^2.1.0 camerawesome: ^2.1.0
# camerawesome:
# path: ../CamerAwesome
collection: ^1.18.0 collection: ^1.18.0
connectivity_plus: ^6.1.2 connectivity_plus: ^6.1.2
cv: ^1.1.3 cv: ^1.1.3