add image editor

This commit is contained in:
otsmr 2025-02-01 17:52:40 +01:00
parent b466b6a8cb
commit 9a162b5b2f
29 changed files with 3535 additions and 236 deletions

View file

@ -0,0 +1 @@
The image editor is based on: https://github.com/hsbijarniya/image_editor_plus/tree/main

View file

@ -0,0 +1,710 @@
Map<String, String> emojiWeights = {};
List<String> emojis = [
'😀',
'😁',
'😂',
'🤣',
'😃',
'😄',
'😅',
'😆',
'😉',
'😊',
'😋',
'😎',
'😍',
'😘',
'🥰',
'😗',
'😙',
'😚',
'🙂️',
'🤗',
'🤩',
'🤔',
'🤔',
'🤨',
'😐',
'😑',
'😶',
'🙄',
'😏',
'😣',
'😥',
'😮',
'🤐',
'😯',
'😪',
'😫',
'😴',
'😌',
'😛',
'😜',
'😝',
'🤤',
'😒',
'😓',
'😔',
'😕',
'🙃',
'🤑',
'😲',
'🙁',
'😖',
'😞',
'😟',
'😤',
'😢',
'😭',
'😦',
'😧',
'😨',
'😩',
'🤯',
'😬',
'😰',
'😱',
'🥵',
'🥶',
'😳',
'🤪',
'😵',
'😡',
'😠',
'🤬',
'😷',
'🤒',
'🤕',
'🤢',
'🤮',
'🤧',
'😇',
'🤠',
'🤡',
'🥳',
'🥴',
'🤥',
'🤫',
'🤭',
'🤭',
'🧐',
'🤓',
'😈',
'👿',
'👹',
'👺',
'💀',
'👻',
'👽',
'🤖',
'💩',
'😺',
'😸',
'😹',
'😻',
'😼',
'😽',
'🙀',
'😿',
'😾',
'😾',
/// People and Fantasy
'👶',
'👧',
'🧒',
'👩',
'🧑',
'👨',
'👵',
'👴',
'👲',
'👳‍♀️‍️',
'👳‍♂️️‍️',
'🧕️️‍️',
'🧔‍',
'👱‍♂️️‍',
'👱‍♀️️️‍',
'👨‍🦰️️️‍',
'👩‍🦰‍',
'👨‍🦱‍‍',
'👨‍🦲‍‍',
'👩‍🦲‍‍',
'👨‍🦳‍‍',
'👩‍🦳‍‍',
'🦸‍♀️‍‍',
'🦸‍♂️️‍‍',
'🦹‍♀️️️‍‍',
'🦹‍♂️️️️‍‍',
'👮‍♀️‍‍',
'👮‍♂️️‍‍',
'👷‍♀️️️‍‍',
'👷‍♂️️️️‍‍',
'💂‍♀️️️️️‍‍',
'💂‍♂️️️️️️‍‍',
'🕵️‍♀️️️️️️️‍‍',
'🕵️‍♂️️️️️️️️‍‍',
'👩‍⚕️️️️️️️️️‍‍',
'👨‍⚕️️️️️️️️️️‍‍',
'👩‍🌾️️️️️️️️️️‍‍',
'👨‍🌾‍‍',
'👩‍🍳‍‍',
'👨‍🍳‍‍',
'👩‍🎓‍‍',
'👨‍🎓‍‍',
'👩‍🎤‍‍',
'👨‍🎤‍‍',
'👩‍🏫‍‍',
'👨‍🏫‍‍',
'👩‍🏭‍‍',
'👨‍🏭‍‍',
'👩‍💻‍‍',
'👨‍💻‍‍',
'👩‍💼‍‍',
'👨‍💼‍‍',
'👩‍🔧‍‍',
'👨‍🔧‍‍',
'👩‍🔬‍‍',
'👨‍🔬‍‍',
'👩‍🎨‍‍',
'👨‍🎨‍‍',
'👩‍🚒‍‍',
'👨‍🚒‍‍',
'👩‍✈️‍‍',
'👨‍✈️️‍‍',
'👩‍🚀‍‍',
'👨‍🚀‍‍',
'👩‍⚖️‍‍',
'👨‍⚖️️‍‍',
'👰‍‍',
'🤵‍‍',
'👸‍‍',
'🤴‍‍',
'🤶‍‍',
'🎅‍‍',
'🧙‍♀️‍‍',
'🧙‍♂️️‍‍',
'🧝‍♀️️️‍‍',
'🧝‍♂️‍‍',
'🧛‍♀️️‍‍',
'🧛‍♂️️️‍‍',
'🧟‍♀️️️️‍‍',
'🧟‍♂️️️️️‍‍',
'🧞‍♀️️️️️️‍‍',
'🧞‍♂️️️️️️️‍‍',
'🧜‍♀️️️️️️️️‍‍',
'🧜‍♂️️️️️️️️️‍‍',
'🧚‍♀️️️️️️️️️️‍‍',
'🧚‍♂️️️️️️️️️️️‍‍',
'👼️️️️️️️️️️️‍‍',
'🤰‍‍',
'🤱‍‍',
'🙇‍♀️‍‍',
'🙇‍♂️‍‍',
'💁‍♀️️‍‍',
'💁‍♂️️️‍‍',
'🙅‍♀️️️️‍‍',
'🙅‍♂️‍‍',
'🙆‍♀️️‍‍',
'🙆‍♂️️️‍‍',
'🙋‍♀️️️️‍‍',
'🙋‍♂️‍‍',
'🤦‍♀️️‍‍',
'🤦‍♂️️️‍‍',
'🤷‍♀️️️️‍‍',
'🤷‍♂️️️️️‍‍',
'🙎‍♀️️️️️️‍‍',
'🙎‍♂️️️️️️️‍‍',
'🙍‍♀️️️️️️️️‍‍',
'🙍‍♂️️️️️️️️️‍‍',
'💇‍♀️️️️️️️️️️‍‍',
'💇‍♂️️️️️️️️️️️‍‍',
'💆‍♀️️️️️️️️️️️️‍‍',
'💆‍♂️️️️️️️️️️️️️‍‍',
'🧖‍♀️️️️️️️️️️️️️️‍‍',
'🧖‍♂️️️️️️️️️️️️️️️‍‍',
'💅️️️️️️️️️️️️️️️‍‍',
'🤳️️️️️️️️️️️️️️‍‍',
'💃️️️️️️️️️️️️️‍‍',
'🕺️️️️️️️️️️️️‍‍',
'👯‍♀️‍‍',
'👯‍♂️️‍‍',
'🕴️️‍‍',
'🚶‍♀️️‍‍',
'🚶‍♂️️️‍‍',
'🏃‍♀️️️️‍‍',
'🏃‍♂️‍‍',
'👫️‍‍',
'👭‍‍',
'👬‍‍',
'💑‍‍',
'👩‍❤️‍👩‍‍',
'👨‍❤️‍👨‍‍',
'💏‍‍',
'👩‍❤️‍💋‍👩‍‍',
'👨‍❤️‍💋‍👨‍‍',
'👪‍‍',
'👨‍👩‍👧‍‍',
'👨‍👩‍👧‍👦‍‍',
'👨‍👩‍👦‍👦‍‍',
'👨‍👩‍👧‍👧‍‍',
'👩‍👩‍👦‍‍',
'👩‍👩‍👧‍‍',
'👩‍👩‍👧‍👦‍‍',
'👩‍👩‍👦‍👦‍‍',
'👩‍👩‍👧‍👧‍‍',
'👨‍👨‍👦‍‍',
'👨‍👨‍👧‍‍',
'👨‍👨‍👧‍👦‍‍',
'👨‍👨‍👦‍👦‍‍',
'👨‍👨‍👧‍👧‍‍',
'👩‍👦‍‍',
'👩‍👧‍‍',
'👩‍👧‍👦‍‍',
'👩‍👦‍👦‍‍',
'👩‍👧‍👧‍‍',
'👨‍👦‍‍',
'👨‍👧‍‍',
'👨‍👧‍👦‍‍',
'👨‍👦‍👦‍‍',
'👨‍👧‍👧‍‍',
'🤲‍‍',
'👐‍‍',
'🙌‍‍',
'👏‍‍',
'🤝‍‍',
'👍‍‍',
'👎‍‍',
'👊‍‍',
'✊‍‍',
'🤛‍‍',
'🤜‍‍',
'🤞‍‍',
'✌️‍‍',
'🤟️‍‍',
'🤘‍‍',
'👌‍‍',
'👈‍‍',
'👉‍‍',
'👆‍‍',
'👇‍‍',
'☝️‍‍',
'✋️‍‍',
'🤚️‍‍',
'🤚️‍‍',
'🖐‍‍',
'🖖‍‍',
'👋‍‍',
'🤙‍‍',
'💪‍‍',
'🦵‍‍',
'🦶‍‍',
'🖕‍‍',
'✍️‍‍',
'🙏️‍‍',
'💍‍‍',
'💄‍‍',
'💋‍‍',
'👄‍‍',
'👅‍‍',
'👂‍‍',
'👃‍‍',
'👣‍‍',
'👁‍‍',
'👀‍‍',
'🧠‍‍',
'🦴‍‍',
'🦷‍‍',
'🗣‍‍',
'👤‍‍',
'👥‍‍',
'🧥‍‍',
'👚‍‍',
'👕‍‍',
'👖‍‍',
'👔‍‍',
'👗‍‍',
'👙‍‍',
'👘‍‍',
'👠‍‍',
'👡‍‍',
'👢‍‍',
'👞‍‍',
'👟‍‍',
'🥾‍‍',
'🥿‍‍',
'🧦‍‍',
'🧤‍‍',
'🧣‍‍',
'🎩‍‍',
'🧢‍‍',
'👒‍‍',
'🎓‍‍',
'⛑‍‍',
'👑‍‍',
'👝‍‍',
'👛‍‍',
'👜‍‍',
'💼‍‍',
'🎒‍‍',
'👓‍‍',
'🕶‍‍',
'🥽‍‍',
'🥼‍‍',
'🌂‍‍',
'🧵‍‍',
'🧶‍‍',
/// Animals
'🐶‍‍',
'🐱‍‍',
'🐭‍‍',
'🐰‍‍',
'🦊‍‍',
'🦝‍‍',
'🐻‍‍',
'🦘‍‍',
'🦡‍‍',
'🐨‍‍',
'🐯‍‍',
'🦁‍‍',
'🐼‍‍',
'🐼‍‍',
'🐮‍‍',
'🐷‍‍',
'🐽‍‍',
'🐸‍‍',
'🐵‍‍',
'🙈‍‍',
'🙉‍‍',
'🙊‍‍',
'🐒‍‍',
'🐔‍‍',
'🐧‍‍',
'🐦‍‍',
'🐤‍‍',
'🐣‍‍',
'🐥‍‍',
'🦆‍‍',
'🦢‍‍',
'🦅‍‍',
'🦉‍‍',
'🦚‍‍',
'🦜‍‍',
'🦇‍‍',
'🐺‍‍',
'🐗‍‍',
'🐴‍‍',
'🦄‍‍',
'🐝‍‍',
'🐛‍‍',
'🦋‍‍',
'🐌‍‍',
'🐚‍‍',
'🐞‍‍',
'🐜‍‍',
'🦗‍‍',
'🕷‍‍',
'🕸‍‍',
'🦂‍‍',
'🦟‍‍',
'🦠‍‍',
'🐢‍‍',
'🐍‍‍',
'🦎‍‍',
'🦖‍‍',
'🦕‍‍',
'🐙‍‍',
'🦑‍‍',
'🦐‍‍',
'🦀‍‍',
'🐡‍‍',
'🐠‍‍',
'🐟‍‍',
'🐬‍‍',
'🐳‍‍',
'🐋‍‍',
'🦈‍‍',
'🐊‍‍',
'🐅‍‍',
'🐆‍‍',
'🦓‍‍',
'🦍‍‍',
'🐘‍‍',
'🦏‍‍',
'🦛‍‍',
'🐪‍‍',
'🐫‍‍',
'🦙‍‍',
'🦒‍‍',
'🐃‍‍',
'🐂‍‍',
'🐄‍‍',
'🐎‍‍',
'🐖‍‍',
'🐏‍‍',
'🐐‍‍',
'🦌‍‍',
'🐕‍‍',
'🐩‍‍',
'🐈‍‍',
'🐓‍‍',
'🦃‍‍',
'🕊‍‍',
'🐇‍‍',
'🐁‍‍',
'🐀‍‍',
'🐿‍‍',
'🦔‍‍',
'🐾‍',
'🐉‍',
'🐲‍',
'🌵‍',
'🎄‍',
'🌲‍',
'🌳‍',
'🌴‍',
'🌱‍',
'🌿‍',
'☘️‍',
'🎍️‍',
'🎋️‍',
'🍃‍',
'🍂‍',
'🍁‍',
'🍄‍',
'🌾️‍',
'💐️‍',
'🌷️‍',
'🌹‍',
'🥀‍',
'🌺‍',
'🌸‍',
'🌼‍',
'🌻️‍',
'🌞‍',
'🌝‍',
'🌛‍',
'🌜‍',
'🌚‍',
'🌕‍',
'🌖‍',
'🌗‍',
'🌘‍',
'🌑‍',
'🌒‍',
'🌔‍',
'🌙‍',
'🌎‍',
'🌍‍',
'🌏‍',
'💫‍',
'⭐️‍',
'🌟️‍',
'✨️‍',
'⚡️️‍',
'☄️️️‍',
'💥️️️‍',
'🔥‍',
'🌪‍',
'🌈‍',
'☀️‍',
'🌤️‍',
'⛅️️‍',
'🌥️️‍',
'☁️️‍',
'🌦️️‍',
'🌧️‍',
'⛈‍',
'🌩‍',
'🌨‍',
'❄️‍',
'☃️️‍',
'⛄️️️‍',
'🌬️️️‍',
'💨️️️‍',
'💧️️️‍',
'💦️️️‍',
'☔️️️️‍',
'☂️️️️️‍',
'🌊️️️️️‍',
'🌫️️️️‍',
/// Foods
'🍏‍',
'🍎‍',
'🍐‍',
'🍊‍',
'🍋‍',
'🍌‍',
'🍉‍',
'🍇‍',
'🍓‍',
'🍈‍',
'🍒‍',
'🍑‍',
'🍍‍',
'🥭‍',
'🥥‍',
'🥝‍',
'🍅‍',
'🍆‍',
'🥑‍',
'🥦‍',
'🥒‍',
'🥬‍',
'🌶‍',
'🌽‍',
'🥕‍',
'🥔‍',
'🍠‍',
'🥐‍',
'🍞‍',
'🥖‍',
'🥨‍',
'🥯‍',
'🧀‍',
'🥚‍',
'🍳‍',
'🥞‍',
'🥓‍',
'🥩‍',
'🍗‍',
'🍖‍',
'🌭‍',
'🍔‍',
'🍟‍',
'🍕‍',
'🥪‍',
'🥙‍',
'🌮‍',
'🌯‍',
'🥗‍',
'🥘‍',
'🥫‍',
'🍝‍',
'🍜‍',
'🍲‍',
'🍛‍',
'🍣‍',
'🍱‍',
'🥟‍',
'🍤‍',
'🍙‍',
'🍚‍',
'🍘‍',
'🍥‍',
'🥮‍',
'🥠‍',
'🍢‍',
'🍧‍',
'🍨‍',
'🍦‍',
'🥧‍',
'🍰‍',
'🎂‍',
'🍮‍',
'🍭‍',
'🍬‍',
'🍫‍',
'🍿‍',
'🧂‍',
'🍩‍',
'🍪‍',
'🌰‍',
'🥜‍',
'🍯‍',
'🥛‍',
'🍼‍',
'☕️‍',
'🍵️‍',
'🥤️‍',
'🍶‍',
'🍺‍',
'🍻‍',
'🥂‍',
'🍷‍',
'🍸‍',
'🍹‍',
'🍾‍',
'🥄‍',
'🍴‍',
'🍽‍',
'🥣‍',
'🥡‍',
'🥢‍',
/// Activity and Sports
'⚽️‍',
'🏀️‍',
'🏈‍',
'⚾️‍',
'🥎️‍',
'🏐️‍',
'🏉‍',
'🎾‍',
'🥏‍',
'🎱‍',
'🏓‍',
'🏸‍',
'🥅‍',
'🏒‍',
'🏑‍',
'🥍‍',
'🏏‍',
'⛳️‍',
'🏹️‍',
'🎣️‍',
'🥊‍',
'🥋‍',
'🎽‍',
'⛸‍',
'🥌‍',
'🛷‍',
'🛹‍',
'🎿‍',
'⛷‍',
'🏂‍',
'🏋️‍♀️‍',
'🏋🏼‍♀️‍',
'🏋🏽‍♀️️‍',
'🏋🏾‍♀️️️‍',
'🏋🏿‍♀️️️️‍',
'🏋️‍♂️️️️‍',
'🏋🏻‍♂️️️️‍',
'🏋🏼‍♂️️️️‍',
'🏋🏽‍♂️️️️‍',
'🏋🏾‍♂️️️️‍',
'🏋🏿‍♂️️️️‍',
'🤼‍♀️️️️‍',
'🤼‍♂️️️️‍',
'🤸‍♀️️️️‍',
'🤸🏻‍♀️️️️‍',
'🤸🏼‍♀️️️️‍',
'🤸🏽‍♀️️️️‍',
'🤸🏿‍♀️️️️️‍',
'🤸‍♂️️️️‍',
'🤸🏻‍♂️️️️‍',
'🤸🏼‍♂️️️️️‍',
'🤸🏽‍♂️️️️️️‍',
'🤸🏾‍♂️️️️️️‍',
'🤸🏿‍♂️️️️️️‍',
'⛹️‍♀️️️️️️‍',
'⛹🏻‍♀️️️️️️️‍',
'⛹🏼‍♀️️️️️️️️‍',
'⛹🏽‍♀️️️️️️️️️‍',
'⛹🏾‍♀️️️️️️️️️️‍',
'⛹🏿‍♀️️️️️️️️️️️‍',
'⛹️‍♂️️️️️️️️️️️️‍',
'⛹🏻‍♂️️️️️️️️️️️️️‍',
'⛹🏼‍♂️️️️️️️️️️️️️️‍',
'⛹🏽‍♂️️️️️️️️️️️️️️️‍',
'⛹🏾‍♂️️️️️️️️️️️️️️️️‍',
'⛹🏿‍♂️‍',
'🤺️‍',
'🤾‍♀️‍',
'🤾🏻‍♀️️‍',
'🤾🏼‍♀️️️‍',
'🤾🏾‍♀️️️️‍',
];

View file

@ -0,0 +1,4 @@
enum EditorMode {
brush,
filter,
}

View file

@ -0,0 +1,54 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
class ImageItem {
int width = 1;
int height = 1;
Uint8List bytes = Uint8List.fromList([]);
Completer<bool> loader = Completer<bool>();
ImageItem([dynamic image]) {
if (image != null) load(image);
}
Future load(dynamic image) async {
loader = Completer<bool>();
if (image is ImageItem) {
bytes = image.bytes;
height = image.height;
width = image.width;
return loader.complete(true);
} else if (image is Uint8List) {
bytes = image;
var decodedImage = await decodeImageFromList(bytes);
height = decodedImage.height;
width = decodedImage.width;
return loader.complete(true);
} else {
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,
};
}
}

View file

@ -0,0 +1,278 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/components/image_editor/data/image_item.dart';
/// Layer class with some common properties
class Layer {
Offset offset;
double rotation, scale, opacity;
Layer({
this.offset = const Offset(64, 64),
this.opacity = 1,
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]
class BackgroundLayerData extends Layer {
ImageItem image;
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]
class EmojiLayerData extends Layer {
String text;
double size;
EmojiLayerData({
this.text = '',
this.size = 64,
super.offset,
super.opacity,
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]
class ImageLayerData extends Layer {
ImageItem image;
double size;
ImageLayerData({
required this.image,
this.size = 64,
super.offset,
super.opacity,
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]
class LinkLayerData extends Layer {
String text;
double size;
Color color, background;
double backgroundOpacity;
TextAlign align;
LinkLayerData({
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 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(),
};
}
}

View file

@ -0,0 +1,694 @@
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';
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 = [], 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
void dispose() {
layers.clear();
super.dispose();
}
List<Widget> get filterActions {
return [
IconButton(
icon: Icon(Icons.close, size: 30),
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);
},
),
]),
),
];
}
@override
void initState() {
if (widget.image != null) {
loadImage(widget.image!);
}
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;
if (layers.length > 1) {
image = await screenshotController.capture(pixelRatio: pixelRatio);
} else if (layers.length == 1) {
if (layers.first is BackgroundLayerData) {
image = (layers.first as BackgroundLayerData).image.bytes;
} else if (layers.first is ImageLayerData) {
image = (layers.first as ImageLayerData).image.bytes;
}
}
return image;
}
@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);
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,
),
),
),
),
Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
color: Colors.black.withAlpha(75),
),
child: SafeArea(
child: Row(
children: filterActions,
),
),
),
),
Positioned(
right: 0,
top: 50,
child: Container(
// color: Colors.black45,
alignment: Alignment.bottomCenter,
// height: 86 + MediaQuery.of(context).padding.bottom,
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: const BoxDecoration(
// color: Colors.black87,
// shape: BoxShape.rectangle,
// boxShadow: [
// BoxShadow(blurRadius: 1),
// ],
),
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(),
),
);
if (layer == null) return;
undoLayers.clear();
removedLayers.clear();
layers.add(layer);
setState(() {});
},
),
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();
layers.add(
ImageLayerData(
image: ImageItem(drawing),
offset: Offset(
-currentImage.width / 4,
-currentImage.height / 4,
),
),
);
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(() {});
},
),
],
),
),
),
),
),
]);
}
Future<void> loadImage(dynamic imageFile) async {
await currentImage.load(imageFile);
layers.clear();
layers.add(BackgroundLayerData(
image: currentImage,
));
setState(() {});
}
}
/// Button used in bottomNavigationBar in ImageEditor
class BottomButton extends StatelessWidget {
final VoidCallback? onTap, onLongPress;
final IconData icon;
const BottomButton({
super.key,
this.onTap,
this.onLongPress,
required this.icon,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
onLongPress: onLongPress,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
children: [
Icon(
icon,
color: Colors.white,
),
const SizedBox(height: 8),
],
),
),
);
}
}
/// 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
State<ImageEditorDrawing> createState() => _ImageEditorDrawingState();
}
class _ImageEditorDrawingState extends State<ImageEditorDrawing> {
Color pickerColor = Colors.white,
currentColor = Colors.white,
currentBackgroundColor = Colors.black;
var screenshotController = ScreenshotController();
final control = HandSignatureControl(
threshold: 3.0,
smoothRatio: 0.65,
velocityRange: 2.0,
);
List<CubicPath> undoList = [];
bool skipNextEvent = false;
void changeColor(o.BrushColor 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) {
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;
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(() {});
},
),
),
);
},
);
},
),
for (var color in widget.options.colors)
ColorButton(
color: color.color,
onTap: (color) {
currentColor = color;
setState(() {});
},
isSelected: color.color == currentColor,
),
],
),
),
),
),
);
}
}
/// Button used in bottomNavigationBar in ImageEditorDrawing
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: 34,
width: 34,
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 23),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? Colors.white : Colors.white54,
width: isSelected ? 3 : 1,
),
),
),
);
}
}

View file

@ -0,0 +1,39 @@
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()),
),
),
);
}
}

View file

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/components/image_editor/data/layer.dart';
/// Main layer
class BackgroundLayer extends StatefulWidget {
final BackgroundLayerData layerData;
final VoidCallback? onUpdate;
final bool editable;
const BackgroundLayer({
super.key,
required this.layerData,
this.onUpdate,
this.editable = false,
});
@override
State<BackgroundLayer> createState() => _BackgroundLayerState();
}
class _BackgroundLayerState extends State<BackgroundLayer> {
@override
Widget build(BuildContext context) {
return Container(
width: widget.layerData.image.width.toDouble(),
height: widget.layerData.image.height.toDouble(),
// color: black,
padding: EdgeInsets.zero,
child: Image.memory(widget.layerData.image.bytes),
);
}
}

View file

@ -0,0 +1,90 @@
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 {
final EmojiLayerData layerData;
final VoidCallback? onUpdate;
final bool editable;
const EmojiLayer({
super.key,
required this.layerData,
this.onUpdate,
this.editable = false,
});
@override
createState() => _EmojiLayerState();
}
class _EmojiLayerState extends State<EmojiLayer> {
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,
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,
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.size = initialSize +
detail.scale * 5 * (detail.scale > 1 ? 1 : -1);
}
setState(() {});
}
: null,
child: Transform.rotate(
angle: widget.layerData.rotation,
child: Container(
padding: const EdgeInsets.all(64),
child: Text(
widget.layerData.text.toString(),
style: TextStyle(
fontSize: widget.layerData.size,
),
),
),
),
),
);
}
}

View file

@ -0,0 +1,102 @@
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 {
final ImageLayerData layerData;
final VoidCallback? onUpdate;
final bool editable;
const ImageLayer({
super.key,
required this.layerData,
this.onUpdate,
this.editable = false,
});
@override
createState() => _ImageLayerState();
}
class _ImageLayerState extends State<ImageLayer> {
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,
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),
),
),
),
);
}
}

View file

@ -0,0 +1,102 @@
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 {
final TextLayerData layerData;
final VoidCallback? onUpdate;
final bool editable;
const TextLayer({
super.key,
required this.layerData,
this.onUpdate,
this.editable = false,
});
@override
createState() => _TextViewState();
}
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,
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,
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.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,
),
),
),
),
),
),
);
}
}

View file

@ -0,0 +1,77 @@
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';
import 'package:twonly/src/components/image_editor/layers/text_layer.dart';
/// View stacked layers (unbounded height, width)
class LayersViewer extends StatelessWidget {
final List<Layer> layers;
final Function()? onUpdate;
final bool editable;
const LayersViewer({
super.key,
required this.layers,
required this.editable,
this.onUpdate,
});
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
children: layers.map((layerItem) {
// Background layer
if (layerItem is BackgroundLayerData) {
return BackgroundLayer(
layerData: layerItem,
onUpdate: onUpdate,
editable: editable,
);
}
// Image layer
if (layerItem is ImageLayerData) {
return ImageLayer(
layerData: layerItem,
onUpdate: onUpdate,
editable: editable,
);
}
// 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(
layerData: layerItem,
onUpdate: onUpdate,
editable: editable,
);
}
// Text layer
if (layerItem is TextLayerData) {
return TextLayer(
layerData: layerItem,
onUpdate: onUpdate,
editable: editable,
);
}
// Blank layer
return Container();
}).toList(),
);
}
}

View file

@ -0,0 +1,131 @@
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,
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,83 @@
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});
@override
createState() => _EmojiesState();
}
class _EmojiesState extends State<Emojies> {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Container(
padding: const EdgeInsets.all(0.0),
height: 400,
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(32),
topRight: Radius.circular(32),
),
color: Colors.black,
boxShadow: [
BoxShadow(
blurRadius: 10.9,
color: Color.fromRGBO(0, 0, 0, 0.1),
),
],
),
child: Column(
children: [
const SizedBox(height: 16),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Text(
i18n('Select Emoji'),
style: const TextStyle(color: Colors.white),
),
]),
const SizedBox(height: 16),
Container(
height: 315,
padding: const EdgeInsets.all(0.0),
child: GridView(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
scrollDirection: Axis.vertical,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
mainAxisSpacing: 0.0,
maxCrossAxisExtent: 60.0,
),
children: emojis.map((String emoji) {
return GridTile(
child: GestureDetector(
onTap: () {
Navigator.pop(
context,
EmojiLayerData(
text: emoji,
size: 32.0,
),
);
},
child: Container(
padding: EdgeInsets.zero,
alignment: Alignment.center,
child: Text(
emoji,
style: const TextStyle(fontSize: 35),
),
),
));
}).toList(),
),
)
],
),
),
);
}
}

View file

@ -0,0 +1,79 @@
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)),
)
]),
],
),
);
}
}

View file

@ -0,0 +1,353 @@
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;
}
}

View file

@ -0,0 +1,91 @@
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),
),
),
),
]),
],
),
);
}
}

View file

@ -0,0 +1,92 @@
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),
),
),
),
]),
],
),
);
}
}

View file

@ -0,0 +1,110 @@
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,
),
),
]),
),
),
),
);
}
}

View file

@ -0,0 +1,242 @@
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),
),
),
),
]),
],
),
);
}
}

View file

@ -0,0 +1,47 @@
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();
}

View file

@ -155,19 +155,17 @@ Future encryptAndUploadMediaFile(Int64 target, Uint8List imageBytes) async {
await uploadMediaFile(messageId, target, encryptBytes);
}
Future sendImage(List<Int64> userIds, String imagePath) async {
Future sendImage(List<Int64> userIds, Uint8List imageBytes) async {
// 1. set notifier provider
File imageFile = File(imagePath);
Uint8List? imageBytes = await getCompressedImage(imageFile);
if (imageBytes == null) {
Uint8List? imageBytesCompressed = await getCompressedImage(imageBytes);
if (imageBytesCompressed == null) {
Logger("api.dart").shout("Error compressing image!");
return;
}
for (int i = 0; i < userIds.length; i++) {
encryptAndUploadMediaFile(userIds[i], imageBytes);
encryptAndUploadMediaFile(userIds[i], imageBytesCompressed);
}
}

View file

@ -30,13 +30,13 @@ FlutterSecureStorage getSecureStorage() {
return FlutterSecureStorage(aOptions: _getAndroidOptions());
}
Future<String?> saveImageToGallery(path) async {
Future<String?> saveImageToGallery(Uint8List imageBytes) async {
final hasAccess = await Gal.hasAccess();
if (!hasAccess) {
await Gal.requestAccess();
}
try {
await Gal.putImage(path);
await Gal.putImageBytes(imageBytes);
return null;
} on GalException catch (e) {
return e.type.message;
@ -120,9 +120,9 @@ InputDecoration getInputDecoration(context, hintText) {
);
}
Future<Uint8List?> getCompressedImage(File file) async {
var result = await FlutterImageCompress.compressWithFile(
file.absolute.path,
Future<Uint8List?> getCompressedImage(Uint8List imageBytes) async {
var result = await FlutterImageCompress.compressWithList(
imageBytes,
quality: 90,
);
return result;

View file

@ -71,15 +71,15 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
debugPrint('Capturing picture...');
case (MediaCaptureStatus.success, true, false):
event.captureRequest.when(
single: (single) {
final path = single.file?.path;
if (path == null) return;
single: (single) async {
final imageBytes = await single.file?.readAsBytes();
if (imageBytes == null) return;
Navigator.push(
context,
PageRouteBuilder(
opaque: false,
pageBuilder: (context, a1, a2) =>
ShareImageEditorView(image: path),
ShareImageEditorView(imageBytes: imageBytes),
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return child;

View file

@ -1,6 +1,3 @@
import 'dart:math';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'package:twonly/src/components/flame.dart';
import 'package:twonly/src/components/initialsavatar.dart';

View file

@ -1,40 +1,32 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/components/image_editor/image_editor.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/share_image_view.dart';
class ShareImageEditorView extends StatefulWidget {
const ShareImageEditorView({super.key, required this.image});
final String image;
const ShareImageEditorView({super.key, required this.imageBytes});
final Uint8List imageBytes;
@override
State<ShareImageEditorView> createState() => _ShareImageEditorView();
}
class _ShareImageEditorView extends State<ShareImageEditorView> {
bool _isImageLoaded = false;
bool _imageSaved = false;
@override
void initState() {
super.initState();
imageIsLoaded();
}
Future imageIsLoaded() async {
Future.delayed(Duration(milliseconds: 600), () {
setState(() {
_isImageLoaded = true;
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _isImageLoaded
backgroundColor: true
? Theme.of(context).colorScheme.surface
: Colors.white.withAlpha(0),
body: Stack(
@ -42,99 +34,78 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
children: [
Positioned(
top: 0,
// bottom: 0,
bottom: 70,
left: 0,
right: 0,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 50),
padding: const EdgeInsets.symmetric(vertical: 0),
child: ClipRRect(
borderRadius: BorderRadius.circular(22),
child: Image.file(
File(widget.image),
fit: BoxFit.contain,
// child: Container(),
child: ImageEditor(
image: widget.imageBytes,
),
),
),
),
_isImageLoaded
? Positioned(
left: 10,
top: 60,
child: Row(
// mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.close, size: 30),
color: Colors.white,
onPressed: () async {
Navigator.pop(context);
},
),
],
Positioned(
bottom: 70,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
OutlinedButton.icon(
icon: _imageSaved
? Icon(Icons.check)
: FaIcon(FontAwesomeIcons.floppyDisk),
style: OutlinedButton.styleFrom(
iconColor: _imageSaved
? Theme.of(context).colorScheme.outline
: Theme.of(context).colorScheme.primary,
foregroundColor: _imageSaved
? Theme.of(context).colorScheme.outline
: Theme.of(context).colorScheme.primary,
),
)
: Container(),
_isImageLoaded
? Positioned(
bottom: 70,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
OutlinedButton.icon(
icon: _imageSaved
? Icon(Icons.check)
: FaIcon(FontAwesomeIcons.floppyDisk),
style: OutlinedButton.styleFrom(
iconColor: _imageSaved
? Theme.of(context).colorScheme.outline
: Theme.of(context).colorScheme.primary,
foregroundColor: _imageSaved
? Theme.of(context).colorScheme.outline
: Theme.of(context).colorScheme.primary,
),
onPressed: () async {
if (_imageSaved) return;
final res = await saveImageToGallery(widget.image);
if (res == null) {
setState(() {
_imageSaved = true;
});
}
},
label: Text(_imageSaved
? AppLocalizations.of(context)!
.shareImagedEditorSavedImage
: AppLocalizations.of(context)!
.shareImagedEditorSaveImage),
),
const SizedBox(width: 20),
FilledButton.icon(
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ShareImageView(image: widget.image)),
);
},
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
),
),
label: Text(
AppLocalizations.of(context)!
.shareImagedEditorShareWith,
style: TextStyle(fontSize: 17),
),
),
],
onPressed: () async {
if (_imageSaved) return;
final res = await saveImageToGallery(widget.imageBytes);
if (res == null) {
setState(() {
_imageSaved = true;
});
}
},
label: Text(_imageSaved
? AppLocalizations.of(context)!
.shareImagedEditorSavedImage
: AppLocalizations.of(context)!
.shareImagedEditorSaveImage),
),
const SizedBox(width: 20),
FilledButton.icon(
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ShareImageView(imageBytes: widget.imageBytes)),
);
},
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
),
),
)
: Container(),
label: Text(
AppLocalizations.of(context)!.shareImagedEditorShareWith,
style: TextStyle(fontSize: 17),
),
),
],
),
),
],
),
);

View file

@ -1,4 +1,5 @@
import 'dart:collection';
import 'dart:typed_data';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -13,8 +14,8 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/home_view.dart';
class ShareImageView extends StatefulWidget {
const ShareImageView({super.key, required this.image});
final String image;
const ShareImageView({super.key, required this.imageBytes});
final Uint8List imageBytes;
@override
State<ShareImageView> createState() => _ShareImageView();
@ -26,7 +27,6 @@ class _ShareImageView extends State<ShareImageView> {
List<Contact> _bestFriends = [];
int maxTotalMediaCounter = 0;
final HashSet<Int64> _selectedUserIds = HashSet<Int64>();
String _lastSearchQuery = '';
final TextEditingController searchUserName = TextEditingController();
@override
@ -90,7 +90,6 @@ class _ShareImageView extends State<ShareImageView> {
user.displayName.toLowerCase().contains(query.toLowerCase()))
.toList();
_updateUsers(usersFiltered);
_lastSearchQuery = query;
}
@override
@ -146,7 +145,7 @@ class _ShareImageView extends State<ShareImageView> {
FilledButton.icon(
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async {
sendImage(_selectedUserIds.toList(), widget.image);
sendImage(_selectedUserIds.toList(), widget.imageBytes);
// TODO: pop back to the HomeView page popUntil did not work. check later how to improve in case of pushing more then 2
Navigator.pop(context);

View file

@ -262,22 +262,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.11"
device_info_plus:
dependency: transitive
description:
name: device_info_plus
sha256: b37d37c2f912ad4e8ec694187de87d05de2a3cb82b465ff1f65f65a2d05de544
url: "https://pub.dev"
source: hosted
version: "11.2.1"
device_info_plus_platform_interface:
dependency: transitive
description:
name: device_info_plus_platform_interface
sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2"
url: "https://pub.dev"
source: hosted
version: "7.0.2"
dots_indicator:
dependency: transitive
description:
@ -294,14 +278,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.1"
emoji_picker_flutter:
dependency: transitive
description:
name: emoji_picker_flutter
sha256: "63dee6be976c51c8b971eccbc73fc637f021b6b679eed1b2ec3b503947304734"
url: "https://pub.dev"
source: hosted
version: "4.2.0"
fake_async:
dependency: transitive
description:
@ -334,6 +310,22 @@ 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
@ -562,6 +554,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
hand_signature:
dependency: "direct main"
description:
name: hand_signature
sha256: e007153776b9558234761150b6b3ae98a6b3008e9b824da9911475794a982994
url: "https://pub.dev"
source: hosted
version: "3.0.3"
hive:
dependency: "direct main"
description:
@ -938,14 +938,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.1"
pro_image_editor:
dependency: "direct main"
description:
name: pro_image_editor
sha256: "918f156f28a72b9185d950f865032f6aa83da485d6d45b22fb63d78b63bf7e21"
url: "https://pub.dev"
source: hosted
version: "7.6.4"
protobuf:
dependency: "direct main"
description:
@ -978,6 +970,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.0"
reorderables:
dependency: "direct main"
description:
name: reorderables
sha256: "004a886e4878df1ee27321831c838bc1c976311f4ca6a74ce7d561e506540a77"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
restart_app:
dependency: "direct main"
description:
@ -994,62 +994,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.28.0"
shared_preferences:
dependency: transitive
screenshot:
dependency: "direct main"
description:
name: shared_preferences
sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a
name: screenshot
sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b"
url: "https://pub.dev"
source: hosted
version: "2.3.5"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "138b7bbbc7f59c56236e426c37afb8f78cbc57b094ac64c440e0bb90e380a4f5"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "3.0.0"
shelf:
dependency: transitive
description:
@ -1183,14 +1135,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
universal_io:
dependency: transitive
description:
name: universal_io
sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
vector_math:
dependency: transitive
description:
@ -1207,22 +1151,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.1.0"
vibration:
dependency: transitive
description:
name: vibration
sha256: "3b08a0579c2f9c18d5d78cb5c74f1005f731e02eeca6d72561a2e8059bf98ec3"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
vibration_platform_interface:
dependency: transitive
description:
name: vibration_platform_interface
sha256: "6ffeee63547562a6fef53c05a41d4fdcae2c0595b83ef59a4813b0612cd2bc36"
url: "https://pub.dev"
source: hosted
version: "0.0.3"
vm_service:
dependency: transitive
description:
@ -1271,14 +1199,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.10.0"
win32_registry:
dependency: transitive
description:
name: win32_registry
sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
url: "https://pub.dev"
source: hosted
version: "1.1.5"
x25519:
dependency: transitive
description:

View file

@ -15,6 +15,7 @@ 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
@ -24,6 +25,7 @@ dependencies:
font_awesome_flutter: ^10.8.0
gal: ^2.3.1
google_fonts: ^6.2.1
hand_signature: ^3.0.3
hive: ^2.2.3
image: ^4.3.0
intl: any
@ -35,10 +37,11 @@ dependencies:
path_provider: ^2.1.5
permission_handler: ^11.3.1
pie_menu: ^3.2.7
pro_image_editor: ^7.6.4
protobuf: ^2.1.0
provider: ^6.1.2
reorderables: ^0.6.0
restart_app: ^1.3.2
screenshot: ^3.0.0
sqflite_sqlcipher: ^3.1.0+1
web_socket_channel: ^3.0.1