mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 11:18:41 +00:00
improve image editor
This commit is contained in:
parent
9a162b5b2f
commit
4667ec09cc
21 changed files with 297 additions and 2022 deletions
|
|
@ -1 +0,0 @@
|
|||
The image editor is based on: https://github.com/hsbijarniya/image_editor_plus/tree/main
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
enum EditorMode {
|
||||
brush,
|
||||
filter,
|
||||
}
|
||||
|
|
@ -34,21 +34,4 @@ class ImageItem {
|
|||
return loader.complete(false);
|
||||
}
|
||||
}
|
||||
|
||||
static ImageItem fromJson(Map json) {
|
||||
var image = ImageItem(json['bytes']);
|
||||
|
||||
image.width = json['width'];
|
||||
image.height = json['height'];
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
Map toJson() {
|
||||
return {
|
||||
'height': height,
|
||||
'width': width,
|
||||
'bytes': bytes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,41 +12,6 @@ class Layer {
|
|||
this.rotation = 0,
|
||||
this.scale = 1,
|
||||
});
|
||||
|
||||
copyFrom(Map json) {
|
||||
offset = Offset(json['offset'][0], json['offset'][1]);
|
||||
opacity = json['opacity'];
|
||||
rotation = json['rotation'];
|
||||
scale = json['scale'];
|
||||
}
|
||||
|
||||
static Layer fromJson(Map json) {
|
||||
switch (json['type']) {
|
||||
case 'BackgroundLayer':
|
||||
return BackgroundLayerData.fromJson(json);
|
||||
case 'EmojiLayer':
|
||||
return EmojiLayerData.fromJson(json);
|
||||
case 'ImageLayer':
|
||||
return ImageLayerData.fromJson(json);
|
||||
case 'LinkLayer':
|
||||
return LinkLayerData.fromJson(json);
|
||||
case 'TextLayer':
|
||||
return TextLayerData.fromJson(json);
|
||||
case 'BackgroundBlurLayer':
|
||||
return BackgroundBlurLayerData.fromJson(json);
|
||||
default:
|
||||
return Layer();
|
||||
}
|
||||
}
|
||||
|
||||
Map toJson() {
|
||||
return {
|
||||
'offset': [offset.dx, offset.dy],
|
||||
'opacity': opacity,
|
||||
'rotation': rotation,
|
||||
'scale': scale,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Attributes used by [BackgroundLayer]
|
||||
|
|
@ -56,20 +21,6 @@ class BackgroundLayerData extends Layer {
|
|||
BackgroundLayerData({
|
||||
required this.image,
|
||||
});
|
||||
|
||||
static BackgroundLayerData fromJson(Map json) {
|
||||
return BackgroundLayerData(
|
||||
image: ImageItem.fromJson(json['image']),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map toJson() {
|
||||
return {
|
||||
'type': 'BackgroundLayer',
|
||||
'image': image.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Attributes used by [EmojiLayer]
|
||||
|
|
@ -85,26 +36,6 @@ class EmojiLayerData extends Layer {
|
|||
super.rotation,
|
||||
super.scale,
|
||||
});
|
||||
|
||||
static EmojiLayerData fromJson(Map json) {
|
||||
var layer = EmojiLayerData(
|
||||
text: json['text'],
|
||||
size: json['size'],
|
||||
);
|
||||
|
||||
layer.copyFrom(json);
|
||||
return layer;
|
||||
}
|
||||
|
||||
@override
|
||||
Map toJson() {
|
||||
return {
|
||||
'type': 'EmojiLayer',
|
||||
'text': text,
|
||||
'size': size,
|
||||
...super.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Attributes used by [ImageLayer]
|
||||
|
|
@ -120,76 +51,19 @@ class ImageLayerData extends Layer {
|
|||
super.rotation,
|
||||
super.scale,
|
||||
});
|
||||
|
||||
static ImageLayerData fromJson(Map json) {
|
||||
var layer = ImageLayerData(
|
||||
image: ImageItem.fromJson(json['image']),
|
||||
size: json['size'],
|
||||
);
|
||||
|
||||
layer.copyFrom(json);
|
||||
return layer;
|
||||
}
|
||||
|
||||
@override
|
||||
Map toJson() {
|
||||
return {
|
||||
'type': 'ImageLayer',
|
||||
'image': image.toJson(),
|
||||
'size': size,
|
||||
...super.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Attributes used by [TextLayer]
|
||||
class TextLayerData extends Layer {
|
||||
String text;
|
||||
double size;
|
||||
Color color, background;
|
||||
double backgroundOpacity;
|
||||
TextAlign align;
|
||||
|
||||
TextLayerData({
|
||||
required this.text,
|
||||
this.size = 64,
|
||||
this.color = Colors.white,
|
||||
this.background = Colors.transparent,
|
||||
this.backgroundOpacity = 0,
|
||||
this.align = TextAlign.left,
|
||||
super.offset,
|
||||
super.opacity,
|
||||
super.rotation,
|
||||
super.scale,
|
||||
});
|
||||
|
||||
static TextLayerData fromJson(Map json) {
|
||||
var layer = TextLayerData(
|
||||
text: json['text'],
|
||||
size: json['size'],
|
||||
color: Color(json['color']),
|
||||
background: Color(json['background']),
|
||||
backgroundOpacity: json['backgroundOpacity'],
|
||||
align: TextAlign.values.firstWhere((e) => e.name == json['align']),
|
||||
);
|
||||
|
||||
layer.copyFrom(json);
|
||||
return layer;
|
||||
}
|
||||
|
||||
@override
|
||||
Map toJson() {
|
||||
return {
|
||||
'type': 'TextLayer',
|
||||
'text': text,
|
||||
'size': size,
|
||||
'color': color.value,
|
||||
'background': background.value,
|
||||
'backgroundOpacity': backgroundOpacity,
|
||||
'align': align.name,
|
||||
...super.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Attributes used by [TextLayer]
|
||||
|
|
@ -212,67 +86,4 @@ class LinkLayerData extends Layer {
|
|||
super.rotation,
|
||||
super.scale,
|
||||
});
|
||||
|
||||
static LinkLayerData fromJson(Map json) {
|
||||
var layer = LinkLayerData(
|
||||
text: json['text'],
|
||||
size: json['size'],
|
||||
color: Color(json['color']),
|
||||
background: Color(json['background']),
|
||||
backgroundOpacity: json['backgroundOpacity'],
|
||||
align: TextAlign.values.firstWhere((e) => e.name == json['align']),
|
||||
);
|
||||
|
||||
layer.copyFrom(json);
|
||||
return layer;
|
||||
}
|
||||
|
||||
@override
|
||||
Map toJson() {
|
||||
return {
|
||||
'type': 'LinkLayer',
|
||||
'text': text,
|
||||
'size': size,
|
||||
'color': color.value,
|
||||
'background': background.value,
|
||||
'backgroundOpacity': backgroundOpacity,
|
||||
'align': align.name,
|
||||
...super.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Attributes used by [BackgroundBlurLayer]
|
||||
class BackgroundBlurLayerData extends Layer {
|
||||
Color color;
|
||||
double radius;
|
||||
|
||||
BackgroundBlurLayerData({
|
||||
required this.color,
|
||||
required this.radius,
|
||||
super.offset,
|
||||
super.opacity,
|
||||
super.rotation,
|
||||
super.scale,
|
||||
});
|
||||
|
||||
static BackgroundBlurLayerData fromJson(Map json) {
|
||||
var layer = BackgroundBlurLayerData(
|
||||
color: Color(json['color']),
|
||||
radius: json['radius'],
|
||||
);
|
||||
|
||||
layer.copyFrom(json);
|
||||
return layer;
|
||||
}
|
||||
|
||||
@override
|
||||
Map toJson() {
|
||||
return {
|
||||
'type': 'BackgroundBlurLayer',
|
||||
'color': color.value,
|
||||
'radius': radius,
|
||||
...super.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flex_color_picker/flex_color_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
|
@ -8,75 +6,29 @@ 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/layer.dart';
|
||||
import 'package:twonly/src/components/image_editor/layers_viewer.dart';
|
||||
import 'package:twonly/src/components/image_editor/loading_screen.dart';
|
||||
import 'package:twonly/src/components/image_editor/modules/all_emojis.dart';
|
||||
import 'package:twonly/src/components/image_editor/modules/text.dart';
|
||||
import 'package:twonly/src/components/image_editor/options.dart' as o;
|
||||
import 'package:screenshot/screenshot.dart';
|
||||
|
||||
late Size viewportSize;
|
||||
double viewportRatio = 1;
|
||||
List<Layer> layers = [];
|
||||
List<Layer> undoLayers = [];
|
||||
List<Layer> removedLayers = [];
|
||||
|
||||
List<Layer> layers = [], undoLayers = [], removedLayers = [];
|
||||
Map<String, String> _translations = {};
|
||||
|
||||
String i18n(String sourceString) =>
|
||||
_translations[sourceString.toLowerCase()] ?? sourceString;
|
||||
|
||||
/// Image editor with all option available
|
||||
class ImageEditor extends StatefulWidget {
|
||||
final dynamic image;
|
||||
final String? savePath;
|
||||
final o.BrushOption? brushOption;
|
||||
final o.EmojiOption? emojiOption;
|
||||
final o.TextOption? textOption;
|
||||
|
||||
const ImageEditor({
|
||||
super.key,
|
||||
this.image,
|
||||
this.savePath,
|
||||
this.brushOption = const o.BrushOption(),
|
||||
this.emojiOption = const o.EmojiOption(),
|
||||
this.textOption = const o.TextOption(),
|
||||
});
|
||||
|
||||
@override
|
||||
createState() => _ImageEditorState();
|
||||
|
||||
static setI18n(Map<String, String> translations) {
|
||||
translations.forEach((key, value) {
|
||||
_translations[key.toLowerCase()] = value;
|
||||
});
|
||||
}
|
||||
|
||||
/// Set custom theme properties default is dark theme with white text
|
||||
static ThemeData theme = ThemeData(
|
||||
scaffoldBackgroundColor: Colors.black,
|
||||
colorScheme: const ColorScheme.dark(
|
||||
surface: Colors.black,
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Colors.black87,
|
||||
iconTheme: IconThemeData(color: Colors.white),
|
||||
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||
toolbarTextStyle: TextStyle(color: Colors.white),
|
||||
titleTextStyle: TextStyle(color: Colors.white),
|
||||
),
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
iconTheme: const IconThemeData(
|
||||
color: Colors.white,
|
||||
),
|
||||
textTheme: const TextTheme(
|
||||
bodyMedium: TextStyle(color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _ImageEditorState extends State<ImageEditor> {
|
||||
ImageItem currentImage = ImageItem();
|
||||
|
||||
ScreenshotController screenshotController = ScreenshotController();
|
||||
|
||||
@override
|
||||
|
|
@ -88,65 +40,52 @@ class _ImageEditorState extends State<ImageEditor> {
|
|||
List<Widget> get filterActions {
|
||||
return [
|
||||
IconButton(
|
||||
icon: Icon(Icons.close, size: 30),
|
||||
icon: FaIcon(
|
||||
FontAwesomeIcons.xmark,
|
||||
size: 30,
|
||||
shadows: <Shadow>[
|
||||
Shadow(
|
||||
color: const Color.fromARGB(122, 0, 0, 0),
|
||||
blurRadius: 1.0,
|
||||
)
|
||||
],
|
||||
),
|
||||
color: Colors.white,
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width - 48,
|
||||
child: Row(children: [
|
||||
IconButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
icon: Icon(Icons.undo,
|
||||
color: layers.length > 1 || removedLayers.isNotEmpty
|
||||
? Colors.white
|
||||
: Colors.grey),
|
||||
onPressed: () {
|
||||
if (removedLayers.isNotEmpty) {
|
||||
layers.add(removedLayers.removeLast());
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
|
||||
if (layers.length <= 1) return; // do not remove image layer
|
||||
|
||||
undoLayers.add(layers.removeLast());
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
icon: Icon(Icons.redo,
|
||||
color: undoLayers.isNotEmpty ? Colors.white : Colors.grey),
|
||||
onPressed: () {
|
||||
if (undoLayers.isEmpty) return;
|
||||
|
||||
layers.add(undoLayers.removeLast());
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
icon: const Icon(Icons.check),
|
||||
onPressed: () async {
|
||||
resetTransformation();
|
||||
setState(() {});
|
||||
|
||||
var loadingScreen = showLoadingScreen(context);
|
||||
|
||||
Uint8List? editedImageBytes = await getMergedImage();
|
||||
|
||||
loadingScreen.hide();
|
||||
|
||||
if (mounted) Navigator.pop(context, editedImageBytes);
|
||||
},
|
||||
),
|
||||
]),
|
||||
Expanded(child: Container()),
|
||||
IconButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
icon: Icon(Icons.undo,
|
||||
color: layers.length > 1 || removedLayers.isNotEmpty
|
||||
? Colors.white
|
||||
: Colors.grey),
|
||||
onPressed: () {
|
||||
if (removedLayers.isNotEmpty) {
|
||||
layers.add(removedLayers.removeLast());
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
if (layers.length <= 1) return; // do not remove image layer
|
||||
undoLayers.add(layers.removeLast());
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
icon: Icon(Icons.redo,
|
||||
color: undoLayers.isNotEmpty ? Colors.white : Colors.grey),
|
||||
onPressed: () {
|
||||
if (undoLayers.isEmpty) return;
|
||||
|
||||
layers.add(undoLayers.removeLast());
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 50)
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -159,14 +98,8 @@ class _ImageEditorState extends State<ImageEditor> {
|
|||
super.initState();
|
||||
}
|
||||
|
||||
double lastScaleFactor = 1, scaleFactor = 1;
|
||||
double widthRatio = 1, heightRatio = 1, pixelRatio = 1;
|
||||
|
||||
resetTransformation() {
|
||||
scaleFactor = 1;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
/// obtain image Uint8List by merging layers
|
||||
Future<Uint8List?> getMergedImage() async {
|
||||
Uint8List? image;
|
||||
|
|
@ -185,68 +118,36 @@ class _ImageEditorState extends State<ImageEditor> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
viewportSize = MediaQuery.of(context).size;
|
||||
pixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
return Stack(children: [
|
||||
GestureDetector(
|
||||
onScaleUpdate: (details) {
|
||||
// print(details);
|
||||
|
||||
// move
|
||||
if (details.pointerCount == 1) {
|
||||
// print(details.focalPointDelta);
|
||||
// x += details.focalPointDelta.dx;
|
||||
// y += details.focalPointDelta.dy;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
// scale
|
||||
if (details.pointerCount == 2) {
|
||||
// print([details.horizontalScale, details.verticalScale]);
|
||||
if (details.horizontalScale != 1) {
|
||||
scaleFactor = lastScaleFactor *
|
||||
math.min(details.horizontalScale, details.verticalScale);
|
||||
SizedBox(
|
||||
height: currentImage.height / pixelRatio,
|
||||
width: currentImage.width / pixelRatio,
|
||||
child: Screenshot(
|
||||
controller: screenshotController,
|
||||
child: LayersViewer(
|
||||
layers: layers,
|
||||
onUpdate: () {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
},
|
||||
onScaleEnd: (details) {
|
||||
lastScaleFactor = scaleFactor;
|
||||
},
|
||||
child: SizedBox(
|
||||
height: currentImage.height / pixelRatio,
|
||||
width: currentImage.width / pixelRatio,
|
||||
child: Screenshot(
|
||||
controller: screenshotController,
|
||||
child: LayersViewer(
|
||||
layers: layers,
|
||||
onUpdate: () {
|
||||
setState(() {});
|
||||
},
|
||||
editable: true,
|
||||
),
|
||||
},
|
||||
editable: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
top: 5,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(75),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Row(
|
||||
children: filterActions,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Row(
|
||||
children: filterActions,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 50,
|
||||
top: 100,
|
||||
child: Container(
|
||||
// color: Colors.black45,
|
||||
alignment: Alignment.bottomCenter,
|
||||
|
|
@ -260,112 +161,77 @@ class _ImageEditorState extends State<ImageEditor> {
|
|||
// ],
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
if (widget.textOption != null)
|
||||
BottomButton(
|
||||
icon: Icons.text_fields,
|
||||
onTap: () async {
|
||||
TextLayerData? layer = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const TextEditorImage(),
|
||||
),
|
||||
);
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
BottomButton(
|
||||
icon: FontAwesomeIcons.font,
|
||||
onTap: () async {
|
||||
undoLayers.clear();
|
||||
removedLayers.clear();
|
||||
|
||||
if (layer == null) return;
|
||||
layers.add(
|
||||
TextLayerData(
|
||||
text: "Test",
|
||||
),
|
||||
);
|
||||
|
||||
undoLayers.clear();
|
||||
removedLayers.clear();
|
||||
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,
|
||||
),
|
||||
);
|
||||
|
||||
layers.add(layer);
|
||||
if (drawing != null) {
|
||||
undoLayers.clear();
|
||||
removedLayers.clear();
|
||||
|
||||
setState(() {});
|
||||
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 (widget.brushOption != null)
|
||||
BottomButton(
|
||||
icon: Icons.edit,
|
||||
onTap: () async {
|
||||
if (widget.brushOption!.translatable) {
|
||||
var drawing = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ImageEditorDrawing(
|
||||
image: currentImage,
|
||||
options: widget.brushOption!,
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
|
||||
if (drawing != null) {
|
||||
undoLayers.clear();
|
||||
removedLayers.clear();
|
||||
if (layer == null) return;
|
||||
|
||||
layers.add(
|
||||
ImageLayerData(
|
||||
image: ImageItem(drawing),
|
||||
offset: Offset(
|
||||
-currentImage.width / 4,
|
||||
-currentImage.height / 4,
|
||||
),
|
||||
),
|
||||
);
|
||||
undoLayers.clear();
|
||||
removedLayers.clear();
|
||||
layers.add(layer);
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
} else {
|
||||
resetTransformation();
|
||||
var loadingScreen = showLoadingScreen(context);
|
||||
var mergedImage = await getMergedImage();
|
||||
loadingScreen.hide();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
var drawing = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ImageEditorDrawing(
|
||||
image: ImageItem(mergedImage!),
|
||||
options: widget.brushOption!,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (drawing != null) {
|
||||
currentImage.load(drawing);
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
if (widget.emojiOption != null)
|
||||
BottomButton(
|
||||
icon: FontAwesomeIcons.faceSmile,
|
||||
onTap: () async {
|
||||
EmojiLayerData? layer = await showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.black,
|
||||
builder: (BuildContext context) {
|
||||
return const Emojies();
|
||||
},
|
||||
);
|
||||
|
||||
if (layer == null) return;
|
||||
|
||||
undoLayers.clear();
|
||||
removedLayers.clear();
|
||||
layers.add(layer);
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -422,15 +288,10 @@ class BottomButton extends StatelessWidget {
|
|||
/// Show image drawing surface over image
|
||||
class ImageEditorDrawing extends StatefulWidget {
|
||||
final ImageItem image;
|
||||
final o.BrushOption options;
|
||||
|
||||
const ImageEditorDrawing({
|
||||
super.key,
|
||||
required this.image,
|
||||
this.options = const o.BrushOption(
|
||||
showBackground: true,
|
||||
translatable: true,
|
||||
),
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -438,9 +299,8 @@ class ImageEditorDrawing extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ImageEditorDrawingState extends State<ImageEditorDrawing> {
|
||||
Color pickerColor = Colors.white,
|
||||
currentColor = Colors.white,
|
||||
currentBackgroundColor = Colors.black;
|
||||
Color pickerColor = Colors.white, currentColor = Colors.white;
|
||||
|
||||
var screenshotController = ScreenshotController();
|
||||
|
||||
final control = HandSignatureControl(
|
||||
|
|
@ -452,12 +312,12 @@ class _ImageEditorDrawingState extends State<ImageEditorDrawing> {
|
|||
List<CubicPath> undoList = [];
|
||||
bool skipNextEvent = false;
|
||||
|
||||
void changeColor(o.BrushColor color) {
|
||||
currentColor = color.color;
|
||||
currentBackgroundColor = color.background;
|
||||
// void changeColor(Colors color) {
|
||||
// currentColor = color.color;
|
||||
// currentBackgroundColor = color.background;
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
// setState(() {});
|
||||
// }
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -478,186 +338,156 @@ class _ImageEditorDrawingState extends State<ImageEditorDrawing> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: ImageEditor.theme,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
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;
|
||||
final colors = [
|
||||
Colors.black,
|
||||
Colors.white,
|
||||
Colors.blue,
|
||||
Colors.green,
|
||||
Colors.pink,
|
||||
Colors.purple,
|
||||
Colors.brown,
|
||||
Colors.indigo,
|
||||
];
|
||||
|
||||
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);
|
||||
|
||||
if (widget.options.translatable) {
|
||||
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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Screenshot(
|
||||
controller: screenshotController,
|
||||
child: Container(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
widget.options.showBackground ? null : currentBackgroundColor,
|
||||
image: widget.options.showBackground
|
||||
? DecorationImage(
|
||||
image: Image.memory(widget.image.bytes).image,
|
||||
fit: BoxFit.contain,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: HandSignature(
|
||||
control: control,
|
||||
color: currentColor,
|
||||
width: 1.0,
|
||||
maxWidth: 7.0,
|
||||
type: SignatureDrawType.shape,
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
child: Container(
|
||||
height: 80,
|
||||
decoration: const BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(blurRadius: 2),
|
||||
],
|
||||
),
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
ColorButton(
|
||||
color: Colors.yellow,
|
||||
onTap: (color) {
|
||||
showModalBottomSheet(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(10),
|
||||
topLeft: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black87,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(
|
||||
MediaQuery.of(context).size.width / 2,
|
||||
),
|
||||
topRight: Radius.circular(
|
||||
MediaQuery.of(context).size.width / 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: ColorPicker(
|
||||
wheelDiameter:
|
||||
MediaQuery.of(context).size.width - 64,
|
||||
color: currentColor,
|
||||
pickersEnabled: const {
|
||||
ColorPickerType.both: false,
|
||||
ColorPickerType.primary: false,
|
||||
ColorPickerType.accent: false,
|
||||
ColorPickerType.bw: false,
|
||||
ColorPickerType.custom: false,
|
||||
ColorPickerType.customSecondary: false,
|
||||
ColorPickerType.wheel: true,
|
||||
},
|
||||
enableShadesSelection: false,
|
||||
onColorChanged: (color) {
|
||||
currentColor = color;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
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),
|
||||
),
|
||||
for (var color in widget.options.colors)
|
||||
ColorButton(
|
||||
color: color.color,
|
||||
onTap: (color) {
|
||||
currentColor = color;
|
||||
// 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(() {});
|
||||
},
|
||||
isSelected: color.color == currentColor,
|
||||
),
|
||||
],
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Button used in bottomNavigationBar in ImageEditorDrawing
|
||||
class ColorButton extends StatelessWidget {
|
||||
final Color color;
|
||||
final Function(Color) onTap;
|
||||
|
|
@ -677,15 +507,15 @@ class ColorButton extends StatelessWidget {
|
|||
onTap(color);
|
||||
},
|
||||
child: Container(
|
||||
height: 34,
|
||||
width: 34,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 23),
|
||||
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 ? 3 : 1,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/components/image_editor/data/layer.dart';
|
||||
|
||||
/// Image layer to blur background using BackdropFilter
|
||||
class BackgroundBlurLayer extends StatefulWidget {
|
||||
final BackgroundBlurLayerData layerData;
|
||||
final VoidCallback? onUpdate;
|
||||
final bool editable;
|
||||
|
||||
const BackgroundBlurLayer({
|
||||
super.key,
|
||||
required this.layerData,
|
||||
this.onUpdate,
|
||||
this.editable = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<BackgroundBlurLayer> createState() => _BackgroundBlurLayerState();
|
||||
}
|
||||
|
||||
class _BackgroundBlurLayerState extends State<BackgroundBlurLayer> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned.fill(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: widget.layerData.radius,
|
||||
sigmaY: widget.layerData.radius,
|
||||
),
|
||||
child: Container(
|
||||
color: widget.layerData.color
|
||||
.withAlpha((widget.layerData.opacity * 100).toInt()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/components/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/components/image_editor/image_editor.dart';
|
||||
import 'package:twonly/src/components/image_editor/modules/emoji_layer_overlay.dart';
|
||||
|
||||
/// Emoji layer
|
||||
class EmojiLayer extends StatefulWidget {
|
||||
|
|
@ -33,30 +31,7 @@ class _EmojiLayerState extends State<EmojiLayer> {
|
|||
left: widget.layerData.offset.dx,
|
||||
top: widget.layerData.offset.dy,
|
||||
child: GestureDetector(
|
||||
onTap: widget.editable
|
||||
? () {
|
||||
showModalBottomSheet(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(10),
|
||||
topLeft: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) {
|
||||
return EmojiLayerOverlay(
|
||||
index: layers.indexOf(widget.layerData),
|
||||
layer: widget.layerData,
|
||||
onUpdate: () {
|
||||
if (widget.onUpdate != null) widget.onUpdate!();
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
: null,
|
||||
onTap: widget.editable ? () {} : null,
|
||||
onScaleUpdate: widget.editable
|
||||
? (detail) {
|
||||
if (detail.pointerCount == 1) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/components/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/components/image_editor/image_editor.dart';
|
||||
import 'package:twonly/src/components/image_editor/modules/image_layer_overlay.dart';
|
||||
|
||||
/// Image layer that can be used to add overlay images and drawings
|
||||
class ImageLayer extends StatefulWidget {
|
||||
|
|
@ -29,73 +27,11 @@ class _ImageLayerState extends State<ImageLayer> {
|
|||
initialSize = widget.layerData.size;
|
||||
initialRotation = widget.layerData.rotation;
|
||||
|
||||
return Positioned(
|
||||
left: widget.layerData.offset.dx,
|
||||
top: widget.layerData.offset.dy,
|
||||
child: GestureDetector(
|
||||
onTap: widget.editable
|
||||
? () {
|
||||
showModalBottomSheet(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(10),
|
||||
topLeft: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) {
|
||||
return ImageLayerOverlay(
|
||||
index: layers.indexOf(widget.layerData),
|
||||
layerData: widget.layerData,
|
||||
onUpdate: () {
|
||||
if (widget.onUpdate != null) widget.onUpdate!();
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
: null,
|
||||
onScaleUpdate: widget.editable
|
||||
? (detail) {
|
||||
if (detail.pointerCount == 1) {
|
||||
widget.layerData.offset = Offset(
|
||||
widget.layerData.offset.dx + detail.focalPointDelta.dx,
|
||||
widget.layerData.offset.dy + detail.focalPointDelta.dy,
|
||||
);
|
||||
} else if (detail.pointerCount == 2) {
|
||||
widget.layerData.scale = detail.scale;
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
: null,
|
||||
child: Transform(
|
||||
transform: Matrix4(
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1 / widget.layerData.scale,
|
||||
),
|
||||
child: SizedBox(
|
||||
width: widget.layerData.image.width.toDouble(),
|
||||
height: widget.layerData.image.height.toDouble(),
|
||||
child: Image.memory(widget.layerData.image.bytes),
|
||||
),
|
||||
),
|
||||
return Positioned.fill(
|
||||
child: SizedBox(
|
||||
width: widget.layerData.image.width.toDouble(),
|
||||
height: widget.layerData.image.height.toDouble(),
|
||||
child: Image.memory(widget.layerData.image.bytes),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/components/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/components/image_editor/image_editor.dart';
|
||||
import 'package:twonly/src/components/image_editor/modules/text_layer_overlay.dart';
|
||||
|
||||
/// Text layer
|
||||
class TextLayer extends StatefulWidget {
|
||||
|
|
@ -20,79 +18,38 @@ class TextLayer extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _TextViewState extends State<TextLayer> {
|
||||
double initialSize = 0;
|
||||
double initialRotation = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
initialSize = widget.layerData.size;
|
||||
initialRotation = widget.layerData.rotation;
|
||||
|
||||
return Positioned(
|
||||
left: widget.layerData.offset.dx,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: widget.layerData.offset.dy,
|
||||
child: GestureDetector(
|
||||
onTap: widget.editable
|
||||
? () {
|
||||
showModalBottomSheet(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(10),
|
||||
topLeft: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) {
|
||||
return TextLayerOverlay(
|
||||
index: layers.indexOf(widget.layerData),
|
||||
layer: widget.layerData,
|
||||
onUpdate: () {
|
||||
if (widget.onUpdate != null) widget.onUpdate!();
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
: null,
|
||||
onTap: widget.editable ? () {} : null,
|
||||
onScaleUpdate: widget.editable
|
||||
? (detail) {
|
||||
if (detail.pointerCount == 1) {
|
||||
widget.layerData.offset = Offset(
|
||||
widget.layerData.offset.dx + detail.focalPointDelta.dx,
|
||||
0,
|
||||
widget.layerData.offset.dy + detail.focalPointDelta.dy,
|
||||
);
|
||||
} else if (detail.pointerCount == 2) {
|
||||
widget.layerData.size =
|
||||
initialSize + detail.scale * (detail.scale > 1 ? 1 : -1);
|
||||
|
||||
// print('angle');
|
||||
// print(detail.rotation);
|
||||
widget.layerData.rotation = detail.rotation;
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
: null,
|
||||
child: Transform.rotate(
|
||||
angle: widget.layerData.rotation,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(64),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.layerData.background
|
||||
.withOpacity(widget.layerData.backgroundOpacity),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
widget.layerData.text.toString(),
|
||||
textAlign: widget.layerData.align,
|
||||
style: TextStyle(
|
||||
color: widget.layerData.color,
|
||||
fontSize: widget.layerData.size,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(100),
|
||||
),
|
||||
child: Text(
|
||||
widget.layerData.text.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/components/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/components/image_editor/layers/background_blur_layer.dart';
|
||||
import 'package:twonly/src/components/image_editor/layers/background_layer.dart';
|
||||
import 'package:twonly/src/components/image_editor/layers/emoji_layer.dart';
|
||||
import 'package:twonly/src/components/image_editor/layers/image_layer.dart';
|
||||
|
|
@ -42,15 +41,6 @@ class LayersViewer extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
// Background blur layer
|
||||
if (layerItem is BackgroundBlurLayerData && layerItem.radius > 0) {
|
||||
return BackgroundBlurLayer(
|
||||
layerData: layerItem,
|
||||
onUpdate: onUpdate,
|
||||
editable: editable,
|
||||
);
|
||||
}
|
||||
|
||||
// Emoji layer
|
||||
if (layerItem is EmojiLayerData) {
|
||||
return EmojiLayer(
|
||||
|
|
|
|||
|
|
@ -1,131 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
LoadingScreenHandler showLoadingScreen(
|
||||
BuildContext context, {
|
||||
String? text,
|
||||
Color? color,
|
||||
}) {
|
||||
var handler = LoadingScreenHandler(
|
||||
color: color,
|
||||
text: text,
|
||||
context: context,
|
||||
);
|
||||
|
||||
showDialog<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => LoadingScreenBody(
|
||||
handler: handler,
|
||||
),
|
||||
);
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
class LoadingScreen {
|
||||
final Color? color;
|
||||
final GlobalKey<NavigatorState> globalKey;
|
||||
|
||||
LoadingScreen({
|
||||
this.color,
|
||||
required this.globalKey,
|
||||
});
|
||||
|
||||
LoadingScreenHandler show({
|
||||
String? text,
|
||||
}) {
|
||||
return showLoadingScreen(
|
||||
globalKey.currentContext!,
|
||||
text: text,
|
||||
color: color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
class LoadingScreenHandler {
|
||||
String? id, text;
|
||||
Color? color;
|
||||
double? _progress;
|
||||
late void Function() refresh;
|
||||
BuildContext context;
|
||||
bool expired = false;
|
||||
|
||||
LoadingScreenHandler({
|
||||
required this.context,
|
||||
this.id,
|
||||
this.color,
|
||||
this.text,
|
||||
double? progress,
|
||||
void Function()? refresh,
|
||||
}) {
|
||||
this.refresh = refresh ?? () {};
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
double? get progress => _progress;
|
||||
set progress(double? value) {
|
||||
_progress = value;
|
||||
refresh();
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (expired) return;
|
||||
|
||||
expired = true;
|
||||
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
class LoadingScreenBody extends StatefulWidget {
|
||||
final LoadingScreenHandler handler;
|
||||
const LoadingScreenBody({super.key, required this.handler});
|
||||
|
||||
@override
|
||||
State<LoadingScreenBody> createState() => _LoadingScreenBodyState();
|
||||
}
|
||||
|
||||
class _LoadingScreenBodyState extends State<LoadingScreenBody> {
|
||||
@override
|
||||
void initState() {
|
||||
widget.handler.refresh = () {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
};
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
widget.handler.context = context;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color.fromRGBO(0, 0, 0, 0),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator(
|
||||
color: widget.handler.color ?? Colors.white,
|
||||
value: widget.handler.progress,
|
||||
semanticsLabel: widget.handler.text,
|
||||
),
|
||||
if (widget.handler.progress != null) const SizedBox(height: 8),
|
||||
if (widget.handler.progress != null)
|
||||
Text(
|
||||
'${(widget.handler.progress! * 100).toStringAsFixed(2)}%',
|
||||
style: TextStyle(
|
||||
color: widget.handler.color ?? Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,15 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/components/image_editor/data/data.dart';
|
||||
import 'package:twonly/src/components/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/components/image_editor/image_editor.dart';
|
||||
|
||||
class Emojies extends StatefulWidget {
|
||||
const Emojies({super.key});
|
||||
class Emojis extends StatefulWidget {
|
||||
const Emojis({super.key});
|
||||
|
||||
@override
|
||||
createState() => _EmojiesState();
|
||||
createState() => _EmojisState();
|
||||
}
|
||||
|
||||
class _EmojiesState extends State<Emojies> {
|
||||
class _EmojisState extends State<Emojis> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
|
|
@ -35,7 +34,7 @@ class _EmojiesState extends State<Emojies> {
|
|||
const SizedBox(height: 16),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Text(
|
||||
i18n('Select Emoji'),
|
||||
('Select Emoji'),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/components/image_editor/image_editor.dart';
|
||||
import 'colors_picker.dart';
|
||||
|
||||
class ColorPickersSlider extends StatefulWidget {
|
||||
const ColorPickersSlider({super.key});
|
||||
|
||||
@override
|
||||
createState() => _ColorPickersSliderState();
|
||||
}
|
||||
|
||||
class _ColorPickersSliderState extends State<ColorPickersSlider> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(10), topLeft: Radius.circular(10)),
|
||||
),
|
||||
padding: const EdgeInsets.all(20),
|
||||
height: 240,
|
||||
child: Column(
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
i18n('Slider Filter Color').toUpperCase(),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(i18n('Slider Color'),
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
// const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: BarColorPicker(
|
||||
width: 300,
|
||||
thumbColor: Colors.white,
|
||||
cornerRadius: 10,
|
||||
pickMode: PickMode.color,
|
||||
colorListener: (int value) {
|
||||
setState(() {
|
||||
// currentColor = Color(value);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: Text(i18n('Reset'),
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(i18n('Slider Opicity'),
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
const SizedBox(height: 10),
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: 0.1,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
onChanged: (v) {},
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: Text(i18n('Reset'),
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
)
|
||||
]),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,353 +0,0 @@
|
|||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum PickMode {
|
||||
color,
|
||||
grey,
|
||||
}
|
||||
|
||||
/// A listener which receives an color in int representation. as used
|
||||
/// by [BarColorPicker.colorListener] and [CircleColorPicker.colorListener].
|
||||
typedef ColorListener = void Function(int value);
|
||||
|
||||
/// Constant color of thumb shadow
|
||||
const _kThumbShadowColor = Color(0x44000000);
|
||||
|
||||
/// A padding used to calculate bar height(thumbRadius * 2 - kBarPadding).
|
||||
const _kBarPadding = 4;
|
||||
|
||||
/// A bar color picker
|
||||
class BarColorPicker extends StatefulWidget {
|
||||
/// mode enum of pick a normal color or pick a grey color
|
||||
final PickMode pickMode;
|
||||
|
||||
/// width of bar, if this widget is horizontal, than
|
||||
/// bar width is this value, if this widget is vertical
|
||||
/// bar height is this value
|
||||
final double width;
|
||||
|
||||
/// A listener receives color pick events.
|
||||
final ColorListener colorListener;
|
||||
|
||||
/// corner radius of the picker bar, for each corners
|
||||
final double cornerRadius;
|
||||
|
||||
/// specifies the bar orientation
|
||||
final bool horizontal;
|
||||
|
||||
/// thumb fill color
|
||||
final Color thumbColor;
|
||||
|
||||
/// radius of thumb
|
||||
final double thumbRadius;
|
||||
|
||||
/// initial color of this color picker.
|
||||
final Color initialColor;
|
||||
|
||||
const BarColorPicker({
|
||||
super.key,
|
||||
this.pickMode = PickMode.color,
|
||||
this.horizontal = true,
|
||||
this.width = 200,
|
||||
this.cornerRadius = 0.0,
|
||||
this.thumbRadius = 8,
|
||||
this.initialColor = const Color(0xffff0000),
|
||||
this.thumbColor = Colors.black,
|
||||
required this.colorListener,
|
||||
});
|
||||
|
||||
@override
|
||||
createState() => _BarColorPickerState();
|
||||
}
|
||||
|
||||
class _BarColorPickerState extends State<BarColorPicker> {
|
||||
double percent = 0.0;
|
||||
late List<Color> colors;
|
||||
late double barWidth, barHeight;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.horizontal) {
|
||||
barWidth = widget.width;
|
||||
barHeight = widget.thumbRadius * 2 - _kBarPadding;
|
||||
} else {
|
||||
barWidth = widget.thumbRadius * 2 - _kBarPadding;
|
||||
barHeight = widget.width;
|
||||
}
|
||||
switch (widget.pickMode) {
|
||||
case PickMode.color:
|
||||
colors = const [
|
||||
Color(0xffff0000),
|
||||
Color(0xffffff00),
|
||||
Color(0xff00ff00),
|
||||
Color(0xff00ffff),
|
||||
Color(0xff0000ff),
|
||||
Color(0xffff00ff),
|
||||
Color(0xffff0000)
|
||||
];
|
||||
break;
|
||||
case PickMode.grey:
|
||||
colors = const [Color(0xff000000), Color(0xffffffff)];
|
||||
break;
|
||||
}
|
||||
percent = HSVColor.fromColor(widget.initialColor).hue / 360;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final thumbRadius = widget.thumbRadius;
|
||||
final horizontal = widget.horizontal;
|
||||
|
||||
double? thumbLeft, thumbTop;
|
||||
if (horizontal) {
|
||||
thumbLeft = barWidth * percent;
|
||||
} else {
|
||||
thumbTop = barHeight * percent;
|
||||
}
|
||||
// build thumb
|
||||
var thumb = Positioned(
|
||||
left: thumbLeft,
|
||||
top: thumbTop,
|
||||
child: Container(
|
||||
padding: EdgeInsets.zero,
|
||||
width: thumbRadius * 2,
|
||||
height: thumbRadius * 2,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(thumbRadius),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: _kThumbShadowColor,
|
||||
spreadRadius: 2,
|
||||
blurRadius: 3,
|
||||
)
|
||||
],
|
||||
color: widget.thumbColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// build frame
|
||||
double frameWidth, frameHeight;
|
||||
if (horizontal) {
|
||||
frameWidth = barWidth + thumbRadius * 2;
|
||||
frameHeight = thumbRadius * 2;
|
||||
} else {
|
||||
frameWidth = thumbRadius * 2;
|
||||
frameHeight = barHeight + thumbRadius * 2;
|
||||
}
|
||||
Widget frame = SizedBox(width: frameWidth, height: frameHeight);
|
||||
|
||||
// build content
|
||||
Gradient gradient;
|
||||
double left, top;
|
||||
if (horizontal) {
|
||||
gradient = LinearGradient(colors: colors);
|
||||
left = thumbRadius;
|
||||
top = (thumbRadius * 2 - barHeight) / 2;
|
||||
} else {
|
||||
gradient = LinearGradient(
|
||||
colors: colors,
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter);
|
||||
left = (thumbRadius * 2 - barWidth) / 2;
|
||||
top = thumbRadius;
|
||||
}
|
||||
var content = Positioned(
|
||||
left: left,
|
||||
top: top,
|
||||
child: Container(
|
||||
padding: EdgeInsets.zero,
|
||||
width: barWidth,
|
||||
height: barHeight,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(widget.cornerRadius),
|
||||
gradient: gradient,
|
||||
),
|
||||
child: const Text(''),
|
||||
),
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
onPanDown: (details) => handleTouch(details.globalPosition, context),
|
||||
onPanStart: (details) => handleTouch(details.globalPosition, context),
|
||||
onPanUpdate: (details) => handleTouch(details.globalPosition, context),
|
||||
child: Stack(children: [frame, content, thumb]),
|
||||
);
|
||||
}
|
||||
|
||||
/// calculate colors picked from palette and update our states.
|
||||
void handleTouch(Offset globalPosition, BuildContext context) {
|
||||
var box = context.findRenderObject() as RenderBox;
|
||||
var localPosition = box.globalToLocal(globalPosition);
|
||||
double percent;
|
||||
if (widget.horizontal) {
|
||||
percent = (localPosition.dx - widget.thumbRadius) / barWidth;
|
||||
} else {
|
||||
percent = (localPosition.dy - widget.thumbRadius) / barHeight;
|
||||
}
|
||||
percent = min(max(0.0, percent), 1.0);
|
||||
setState(() {
|
||||
this.percent = percent;
|
||||
});
|
||||
switch (widget.pickMode) {
|
||||
case PickMode.color:
|
||||
var color = HSVColor.fromAHSV(1.0, percent * 360, 1.0, 1.0).toColor();
|
||||
widget.colorListener(color.value);
|
||||
break;
|
||||
case PickMode.grey:
|
||||
final channel = (0xff * percent).toInt();
|
||||
widget.colorListener(
|
||||
Color.fromARGB(0xff, channel, channel, channel).value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A circle palette color picker.
|
||||
class CircleColorPicker extends StatefulWidget {
|
||||
// radius of the color palette, note that radius * 2 is not the final
|
||||
// width of this widget, instead is (radius + thumbRadius) * 2.
|
||||
final double radius;
|
||||
|
||||
/// thumb fill color.
|
||||
final Color thumbColor;
|
||||
|
||||
/// radius of thumb.
|
||||
final double thumbRadius;
|
||||
|
||||
/// A listener receives color pick events.
|
||||
final ColorListener colorListener;
|
||||
|
||||
/// initial color of this color picker.
|
||||
final Color initialColor;
|
||||
|
||||
const CircleColorPicker({
|
||||
super.key,
|
||||
this.radius = 120,
|
||||
this.initialColor = const Color(0xffff0000),
|
||||
this.thumbColor = Colors.black,
|
||||
this.thumbRadius = 8,
|
||||
required this.colorListener,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CircleColorPicker> createState() {
|
||||
return _CircleColorPickerState();
|
||||
}
|
||||
}
|
||||
|
||||
class _CircleColorPickerState extends State<CircleColorPicker> {
|
||||
static const List<Color> colors = [
|
||||
Color(0xffff0000),
|
||||
Color(0xffffff00),
|
||||
Color(0xff00ff00),
|
||||
Color(0xff00ffff),
|
||||
Color(0xff0000ff),
|
||||
Color(0xffff00ff),
|
||||
Color(0xffff0000)
|
||||
];
|
||||
|
||||
late double thumbDistanceToCenter;
|
||||
late double thumbRadians;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
thumbDistanceToCenter = widget.radius;
|
||||
final hue = HSVColor.fromColor(widget.initialColor).hue;
|
||||
thumbRadians = degreesToRadians(270 - hue);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final radius = widget.radius;
|
||||
final thumbRadius = widget.thumbRadius;
|
||||
|
||||
// compute thumb center coordinate
|
||||
final thumbCenterX = radius + thumbDistanceToCenter * sin(thumbRadians);
|
||||
final thumbCenterY = radius + thumbDistanceToCenter * cos(thumbRadians);
|
||||
|
||||
// build thumb widget
|
||||
Widget thumb = Positioned(
|
||||
child: Positioned(
|
||||
left: thumbCenterX,
|
||||
top: thumbCenterY,
|
||||
child: Container(
|
||||
padding: EdgeInsets.zero,
|
||||
width: thumbRadius * 2,
|
||||
height: thumbRadius * 2,
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: _kThumbShadowColor,
|
||||
spreadRadius: 2,
|
||||
blurRadius: 3,
|
||||
)
|
||||
],
|
||||
borderRadius: BorderRadius.circular(thumbRadius),
|
||||
color: widget.thumbColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onPanDown: (details) => handleTouch(details.globalPosition, context),
|
||||
onPanStart: (details) => handleTouch(details.globalPosition, context),
|
||||
onPanUpdate: (details) => handleTouch(details.globalPosition, context),
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: (radius + thumbRadius) * 2,
|
||||
height: (radius + thumbRadius) * 2),
|
||||
Positioned(
|
||||
left: thumbRadius,
|
||||
top: thumbRadius,
|
||||
child: Container(
|
||||
padding: EdgeInsets.zero,
|
||||
width: radius * 2,
|
||||
height: radius * 2,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
gradient: const SweepGradient(colors: colors),
|
||||
),
|
||||
child: const Text(''),
|
||||
),
|
||||
),
|
||||
thumb
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// calculate colors picked from palette and update our states.
|
||||
void handleTouch(Offset globalPosition, BuildContext context) {
|
||||
var box = context.findRenderObject() as RenderBox;
|
||||
var localPosition = box.globalToLocal(globalPosition);
|
||||
final centerX = box.size.width / 2;
|
||||
final centerY = box.size.height / 2;
|
||||
final deltaX = localPosition.dx - centerX;
|
||||
final deltaY = localPosition.dy - centerY;
|
||||
final distanceToCenter = sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
var theta = atan2(deltaX, deltaY);
|
||||
var degree = 270 - radiansToDegrees(theta);
|
||||
if (degree < 0) degree = 360 + degree;
|
||||
widget.colorListener(HSVColor.fromAHSV(1, degree, 1, 1).toColor().value);
|
||||
setState(() {
|
||||
thumbDistanceToCenter = min(distanceToCenter, widget.radius);
|
||||
thumbRadians = theta;
|
||||
});
|
||||
}
|
||||
|
||||
/// convert an angle value from radian to degree representation.
|
||||
double radiansToDegrees(double radians) {
|
||||
return (radians + pi) / pi * 180;
|
||||
}
|
||||
|
||||
/// convert an angle value from degree to radian representation.
|
||||
double degreesToRadians(double degrees) {
|
||||
return degrees / 180 * pi - pi;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/components/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/components/image_editor/image_editor.dart';
|
||||
|
||||
class EmojiLayerOverlay extends StatefulWidget {
|
||||
final int index;
|
||||
final EmojiLayerData layer;
|
||||
final Function onUpdate;
|
||||
|
||||
const EmojiLayerOverlay({
|
||||
super.key,
|
||||
required this.layer,
|
||||
required this.index,
|
||||
required this.onUpdate,
|
||||
});
|
||||
|
||||
@override
|
||||
createState() => _EmojiLayerOverlayState();
|
||||
}
|
||||
|
||||
class _EmojiLayerOverlayState extends State<EmojiLayerOverlay> {
|
||||
double slider = 0.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// slider = widget.sizevalue;
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 200,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.black87,
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(10), topLeft: Radius.circular(10)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
Center(
|
||||
child: Text(
|
||||
i18n('Size Adjust').toUpperCase(),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
Slider(
|
||||
activeColor: Colors.white,
|
||||
inactiveColor: Colors.grey,
|
||||
value: widget.layer.size,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
onChangeEnd: (v) {
|
||||
setState(() {
|
||||
widget.layer.size = v.toDouble();
|
||||
widget.onUpdate();
|
||||
});
|
||||
},
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
slider = v;
|
||||
// print(v.toDouble());
|
||||
widget.layer.size = v.toDouble();
|
||||
widget.onUpdate();
|
||||
});
|
||||
}),
|
||||
const SizedBox(height: 10),
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
removedLayers.add(layers.removeAt(widget.index));
|
||||
Navigator.pop(context);
|
||||
widget.onUpdate();
|
||||
// back(context);
|
||||
// setState(() {});
|
||||
},
|
||||
child: Text(
|
||||
i18n('Remove'),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/components/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/components/image_editor/image_editor.dart';
|
||||
|
||||
class ImageLayerOverlay extends StatefulWidget {
|
||||
final int index;
|
||||
final ImageLayerData layerData;
|
||||
final Function onUpdate;
|
||||
|
||||
const ImageLayerOverlay({
|
||||
super.key,
|
||||
required this.layerData,
|
||||
required this.index,
|
||||
required this.onUpdate,
|
||||
});
|
||||
|
||||
@override
|
||||
createState() => _ImageLayerOverlayState();
|
||||
}
|
||||
|
||||
class _ImageLayerOverlayState extends State<ImageLayerOverlay> {
|
||||
double slider = 0.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// slider = widget.sizevalue;
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 200,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.black87,
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(10), topLeft: Radius.circular(10)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
Center(
|
||||
child: Text(
|
||||
i18n('Size Adjust').toUpperCase(),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
Slider(
|
||||
activeColor: Colors.white,
|
||||
inactiveColor: Colors.grey,
|
||||
value: widget.layerData.scale,
|
||||
min: 0,
|
||||
max: 2,
|
||||
divisions: 100,
|
||||
onChangeEnd: (v) {
|
||||
setState(() {
|
||||
widget.layerData.scale = v.toDouble();
|
||||
widget.onUpdate();
|
||||
});
|
||||
},
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
slider = v;
|
||||
// print(v.toDouble());
|
||||
widget.layerData.scale = v.toDouble();
|
||||
widget.onUpdate();
|
||||
});
|
||||
}),
|
||||
const SizedBox(height: 10),
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
removedLayers.add(layers.removeAt(widget.index));
|
||||
Navigator.pop(context);
|
||||
widget.onUpdate();
|
||||
// back(context);
|
||||
// setState(() {});
|
||||
},
|
||||
child: Text(
|
||||
i18n('Remove'),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/components/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/components/image_editor/image_editor.dart';
|
||||
|
||||
class TextEditorImage extends StatefulWidget {
|
||||
const TextEditorImage({super.key});
|
||||
|
||||
@override
|
||||
createState() => _TextEditorImageState();
|
||||
}
|
||||
|
||||
class _TextEditorImageState extends State<TextEditorImage> {
|
||||
TextEditingController name = TextEditingController();
|
||||
Color currentColor = Colors.white;
|
||||
double slider = 32.0;
|
||||
TextAlign align = TextAlign.left;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var size = MediaQuery.of(context).size;
|
||||
|
||||
return Theme(
|
||||
data: ThemeData.dark(),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(FontAwesomeIcons.alignLeft,
|
||||
color: align == TextAlign.left
|
||||
? Colors.white
|
||||
: Colors.white.withAlpha(80)),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
align = TextAlign.left;
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(FontAwesomeIcons.alignCenter,
|
||||
color: align == TextAlign.center
|
||||
? Colors.white
|
||||
: Colors.white.withAlpha(80)),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
align = TextAlign.center;
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(FontAwesomeIcons.alignRight,
|
||||
color: align == TextAlign.right
|
||||
? Colors.white
|
||||
: Colors.white.withAlpha(80)),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
align = TextAlign.right;
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.check),
|
||||
onPressed: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
TextLayerData(
|
||||
background: Colors.transparent,
|
||||
text: name.text,
|
||||
color: currentColor,
|
||||
size: slider.toDouble(),
|
||||
align: align,
|
||||
),
|
||||
);
|
||||
},
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.all(15),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(children: [
|
||||
SizedBox(
|
||||
height: size.height / 2.2,
|
||||
child: TextField(
|
||||
controller: name,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.all(10),
|
||||
hintText: i18n('Insert Your Message'),
|
||||
hintStyle: const TextStyle(color: Colors.white),
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
scrollPadding: const EdgeInsets.all(20.0),
|
||||
keyboardType: TextInputType.multiline,
|
||||
minLines: 5,
|
||||
maxLines: 99999,
|
||||
style: TextStyle(
|
||||
color: currentColor,
|
||||
),
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,242 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/components/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/components/image_editor/image_editor.dart';
|
||||
import 'colors_picker.dart';
|
||||
|
||||
class TextLayerOverlay extends StatefulWidget {
|
||||
final int index;
|
||||
final TextLayerData layer;
|
||||
final Function onUpdate;
|
||||
|
||||
const TextLayerOverlay({
|
||||
super.key,
|
||||
required this.layer,
|
||||
required this.index,
|
||||
required this.onUpdate,
|
||||
});
|
||||
|
||||
@override
|
||||
createState() => _TextLayerOverlayState();
|
||||
}
|
||||
|
||||
class _TextLayerOverlayState extends State<TextLayerOverlay> {
|
||||
double slider = 0.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// slider = widget.sizevalue;
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 450,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.black87,
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(10),
|
||||
topLeft: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child:
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Text(
|
||||
i18n('Size'),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
Row(children: [
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
thumbColor: Colors.white,
|
||||
value: widget.layer.size,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
onChangeEnd: (v) {
|
||||
setState(() {
|
||||
widget.layer.size = v.toDouble();
|
||||
widget.onUpdate();
|
||||
});
|
||||
},
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
slider = v;
|
||||
// print(v.toDouble());
|
||||
widget.layer.size = v.toDouble();
|
||||
widget.onUpdate();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
widget.layer.backgroundOpacity = 0.5;
|
||||
widget.onUpdate();
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
i18n('Reset'),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Text(
|
||||
i18n('Color'),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
Row(children: [
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: BarColorPicker(
|
||||
width: 300,
|
||||
thumbColor: Colors.white,
|
||||
initialColor: widget.layer.color,
|
||||
cornerRadius: 10,
|
||||
pickMode: PickMode.color,
|
||||
colorListener: (int value) {
|
||||
setState(() {
|
||||
widget.layer.color = Color(value);
|
||||
widget.onUpdate();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
widget.layer.color = Colors.black;
|
||||
widget.onUpdate();
|
||||
});
|
||||
},
|
||||
child: Text(i18n('Reset'),
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Text(
|
||||
i18n('Background Color'),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
Row(children: [
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: BarColorPicker(
|
||||
width: 300,
|
||||
initialColor: widget.layer.background,
|
||||
thumbColor: Colors.white,
|
||||
cornerRadius: 10,
|
||||
pickMode: PickMode.color,
|
||||
colorListener: (int value) {
|
||||
setState(() {
|
||||
widget.layer.background = Color(value);
|
||||
if (widget.layer.backgroundOpacity == 0) {
|
||||
widget.layer.backgroundOpacity = 0.5;
|
||||
}
|
||||
|
||||
widget.onUpdate();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
widget.layer.background = Colors.transparent;
|
||||
widget.layer.backgroundOpacity = 0;
|
||||
widget.onUpdate();
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
i18n('Reset'),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Text(
|
||||
i18n('Background Opacity'),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
Row(children: [
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
min: 0,
|
||||
max: 1,
|
||||
divisions: 100,
|
||||
value: widget.layer.backgroundOpacity,
|
||||
thumbColor: Colors.white,
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
widget.layer.backgroundOpacity = value;
|
||||
widget.onUpdate();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
widget.layer.backgroundOpacity = 0;
|
||||
widget.onUpdate();
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
i18n('Reset'),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
]),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
removedLayers.add(layers.removeAt(widget.index));
|
||||
|
||||
Navigator.pop(context);
|
||||
widget.onUpdate();
|
||||
// back(context);
|
||||
// setState(() {});
|
||||
},
|
||||
child: Text(
|
||||
i18n('Remove'),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class BrushOption {
|
||||
/// show background image on draw screen
|
||||
final bool showBackground;
|
||||
|
||||
/// User will able to move, zoom drawn image
|
||||
/// Note: Layer may not be placed precisely
|
||||
final bool translatable;
|
||||
final List<BrushColor> colors;
|
||||
|
||||
const BrushOption({
|
||||
this.showBackground = true,
|
||||
this.translatable = false,
|
||||
this.colors = const [
|
||||
BrushColor(color: Colors.black, background: Colors.white),
|
||||
BrushColor(color: Colors.white),
|
||||
BrushColor(color: Colors.blue),
|
||||
BrushColor(color: Colors.green),
|
||||
BrushColor(color: Colors.pink),
|
||||
BrushColor(color: Colors.purple),
|
||||
BrushColor(color: Colors.brown),
|
||||
BrushColor(color: Colors.indigo),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
class BrushColor {
|
||||
/// Color of brush
|
||||
final Color color;
|
||||
|
||||
/// Background color while brush is active only be used when showBackground is false
|
||||
final Color background;
|
||||
|
||||
const BrushColor({
|
||||
required this.color,
|
||||
this.background = Colors.black,
|
||||
});
|
||||
}
|
||||
|
||||
class EmojiOption {
|
||||
const EmojiOption();
|
||||
}
|
||||
|
||||
class TextOption {
|
||||
const TextOption();
|
||||
}
|
||||
18
pubspec.lock
18
pubspec.lock
|
|
@ -310,22 +310,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flex_color_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flex_color_picker
|
||||
sha256: c083b79f1c57eaeed9f464368be376951230b3cb1876323b784626152a86e480
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.7.0"
|
||||
flex_seed_scheme:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flex_seed_scheme
|
||||
sha256: d3ba3c5c92d2d79d45e94b4c6c71d01fac3c15017da1545880c53864da5dfeb0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.5.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
|
@ -1233,4 +1217,4 @@ packages:
|
|||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.6.0 <4.0.0"
|
||||
flutter: ">=3.27.0"
|
||||
flutter: ">=3.24.0"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ dependencies:
|
|||
connectivity_plus: ^6.1.2
|
||||
cv: ^1.1.3
|
||||
fixnum: ^1.1.1
|
||||
flex_color_picker: ^3.7.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_image_compress: ^2.4.0
|
||||
|
|
|
|||
Loading…
Reference in a new issue