mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 12:48:41 +00:00
add image editor
This commit is contained in:
parent
b466b6a8cb
commit
9a162b5b2f
29 changed files with 3535 additions and 236 deletions
1
lib/src/components/image_editor/CREDITS.md
Normal file
1
lib/src/components/image_editor/CREDITS.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
The image editor is based on: https://github.com/hsbijarniya/image_editor_plus/tree/main
|
||||||
710
lib/src/components/image_editor/data/data.dart
Executable file
710
lib/src/components/image_editor/data/data.dart
Executable file
|
|
@ -0,0 +1,710 @@
|
||||||
|
Map<String, String> emojiWeights = {};
|
||||||
|
|
||||||
|
List<String> emojis = [
|
||||||
|
'😀',
|
||||||
|
'😁',
|
||||||
|
'😂',
|
||||||
|
'🤣',
|
||||||
|
'😃',
|
||||||
|
'😄',
|
||||||
|
'😅',
|
||||||
|
'😆',
|
||||||
|
'😉',
|
||||||
|
'😊',
|
||||||
|
'😋',
|
||||||
|
'😎',
|
||||||
|
'😍',
|
||||||
|
'😘',
|
||||||
|
'🥰',
|
||||||
|
'😗',
|
||||||
|
'😙',
|
||||||
|
'😚',
|
||||||
|
'🙂️',
|
||||||
|
'🤗',
|
||||||
|
'🤩',
|
||||||
|
'🤔',
|
||||||
|
'🤔',
|
||||||
|
'🤨',
|
||||||
|
'😐',
|
||||||
|
'😑',
|
||||||
|
'😶',
|
||||||
|
'🙄',
|
||||||
|
'😏',
|
||||||
|
'😣',
|
||||||
|
'😥',
|
||||||
|
'😮',
|
||||||
|
'🤐',
|
||||||
|
'😯',
|
||||||
|
'😪',
|
||||||
|
'😫',
|
||||||
|
'😴',
|
||||||
|
'😌',
|
||||||
|
'😛',
|
||||||
|
'😜',
|
||||||
|
'😝',
|
||||||
|
'🤤',
|
||||||
|
'😒',
|
||||||
|
'😓',
|
||||||
|
'😔',
|
||||||
|
'😕',
|
||||||
|
'🙃',
|
||||||
|
'🤑',
|
||||||
|
'😲',
|
||||||
|
'🙁',
|
||||||
|
'😖',
|
||||||
|
'😞',
|
||||||
|
'😟',
|
||||||
|
'😤',
|
||||||
|
'😢',
|
||||||
|
'😭',
|
||||||
|
'😦',
|
||||||
|
'😧',
|
||||||
|
'😨',
|
||||||
|
'😩',
|
||||||
|
'🤯',
|
||||||
|
'😬',
|
||||||
|
'😰',
|
||||||
|
'😱',
|
||||||
|
'🥵',
|
||||||
|
'🥶',
|
||||||
|
'😳',
|
||||||
|
'🤪',
|
||||||
|
'😵',
|
||||||
|
'😡',
|
||||||
|
'😠',
|
||||||
|
'🤬',
|
||||||
|
'😷',
|
||||||
|
'🤒',
|
||||||
|
'🤕',
|
||||||
|
'🤢',
|
||||||
|
'🤮',
|
||||||
|
'🤧',
|
||||||
|
'😇',
|
||||||
|
'🤠',
|
||||||
|
'🤡',
|
||||||
|
'🥳',
|
||||||
|
'🥴',
|
||||||
|
'🤥',
|
||||||
|
'🤫',
|
||||||
|
'🤭',
|
||||||
|
'🤭',
|
||||||
|
'🧐',
|
||||||
|
'🤓',
|
||||||
|
'😈',
|
||||||
|
'👿',
|
||||||
|
'👹',
|
||||||
|
'👺',
|
||||||
|
'💀',
|
||||||
|
'👻',
|
||||||
|
'👽',
|
||||||
|
'🤖',
|
||||||
|
'💩',
|
||||||
|
'😺',
|
||||||
|
'😸',
|
||||||
|
'😹',
|
||||||
|
'😻',
|
||||||
|
'😼',
|
||||||
|
'😽',
|
||||||
|
'🙀',
|
||||||
|
'😿',
|
||||||
|
'😾',
|
||||||
|
'😾',
|
||||||
|
|
||||||
|
/// People and Fantasy
|
||||||
|
'👶',
|
||||||
|
'👧',
|
||||||
|
'🧒',
|
||||||
|
'👩',
|
||||||
|
'🧑',
|
||||||
|
'👨',
|
||||||
|
'👵',
|
||||||
|
'👴',
|
||||||
|
'👲',
|
||||||
|
'👳♀️️',
|
||||||
|
'👳♂️️️',
|
||||||
|
'🧕️️️',
|
||||||
|
'🧔',
|
||||||
|
'👱♂️️',
|
||||||
|
'👱♀️️️',
|
||||||
|
'👨🦰️️️',
|
||||||
|
'👩🦰',
|
||||||
|
'👨🦱',
|
||||||
|
'👨🦲',
|
||||||
|
'👩🦲',
|
||||||
|
'👨🦳',
|
||||||
|
'👩🦳',
|
||||||
|
'🦸♀️',
|
||||||
|
'🦸♂️️',
|
||||||
|
'🦹♀️️️',
|
||||||
|
'🦹♂️️️️',
|
||||||
|
'👮♀️',
|
||||||
|
'👮♂️️',
|
||||||
|
'👷♀️️️',
|
||||||
|
'👷♂️️️️',
|
||||||
|
'💂♀️️️️️',
|
||||||
|
'💂♂️️️️️️',
|
||||||
|
'🕵️♀️️️️️️️',
|
||||||
|
'🕵️♂️️️️️️️️',
|
||||||
|
'👩⚕️️️️️️️️️',
|
||||||
|
'👨⚕️️️️️️️️️️',
|
||||||
|
'👩🌾️️️️️️️️️️',
|
||||||
|
'👨🌾',
|
||||||
|
'👩🍳',
|
||||||
|
'👨🍳',
|
||||||
|
'👩🎓',
|
||||||
|
'👨🎓',
|
||||||
|
'👩🎤',
|
||||||
|
'👨🎤',
|
||||||
|
'👩🏫',
|
||||||
|
'👨🏫',
|
||||||
|
'👩🏭',
|
||||||
|
'👨🏭',
|
||||||
|
'👩💻',
|
||||||
|
'👨💻',
|
||||||
|
'👩💼',
|
||||||
|
'👨💼',
|
||||||
|
'👩🔧',
|
||||||
|
'👨🔧',
|
||||||
|
'👩🔬',
|
||||||
|
'👨🔬',
|
||||||
|
'👩🎨',
|
||||||
|
'👨🎨',
|
||||||
|
'👩🚒',
|
||||||
|
'👨🚒',
|
||||||
|
'👩✈️',
|
||||||
|
'👨✈️️',
|
||||||
|
'👩🚀',
|
||||||
|
'👨🚀',
|
||||||
|
'👩⚖️',
|
||||||
|
'👨⚖️️',
|
||||||
|
'👰',
|
||||||
|
'🤵',
|
||||||
|
'👸',
|
||||||
|
'🤴',
|
||||||
|
'🤶',
|
||||||
|
'🎅',
|
||||||
|
'🧙♀️',
|
||||||
|
'🧙♂️️',
|
||||||
|
'🧝♀️️️',
|
||||||
|
'🧝♂️',
|
||||||
|
'🧛♀️️',
|
||||||
|
'🧛♂️️️',
|
||||||
|
'🧟♀️️️️',
|
||||||
|
'🧟♂️️️️️',
|
||||||
|
'🧞♀️️️️️️',
|
||||||
|
'🧞♂️️️️️️️',
|
||||||
|
'🧜♀️️️️️️️️',
|
||||||
|
'🧜♂️️️️️️️️️',
|
||||||
|
'🧚♀️️️️️️️️️️',
|
||||||
|
'🧚♂️️️️️️️️️️️',
|
||||||
|
'👼️️️️️️️️️️️',
|
||||||
|
'🤰',
|
||||||
|
'🤱',
|
||||||
|
'🙇♀️',
|
||||||
|
'🙇♂️',
|
||||||
|
'💁♀️️',
|
||||||
|
'💁♂️️️',
|
||||||
|
'🙅♀️️️️',
|
||||||
|
'🙅♂️',
|
||||||
|
'🙆♀️️',
|
||||||
|
'🙆♂️️️',
|
||||||
|
'🙋♀️️️️',
|
||||||
|
'🙋♂️',
|
||||||
|
'🤦♀️️',
|
||||||
|
'🤦♂️️️',
|
||||||
|
'🤷♀️️️️',
|
||||||
|
'🤷♂️️️️️',
|
||||||
|
'🙎♀️️️️️️',
|
||||||
|
'🙎♂️️️️️️️',
|
||||||
|
'🙍♀️️️️️️️️',
|
||||||
|
'🙍♂️️️️️️️️️',
|
||||||
|
'💇♀️️️️️️️️️️',
|
||||||
|
'💇♂️️️️️️️️️️️',
|
||||||
|
'💆♀️️️️️️️️️️️️',
|
||||||
|
'💆♂️️️️️️️️️️️️️',
|
||||||
|
'🧖♀️️️️️️️️️️️️️️',
|
||||||
|
'🧖♂️️️️️️️️️️️️️️️',
|
||||||
|
'💅️️️️️️️️️️️️️️️',
|
||||||
|
'🤳️️️️️️️️️️️️️️',
|
||||||
|
'💃️️️️️️️️️️️️️',
|
||||||
|
'🕺️️️️️️️️️️️️',
|
||||||
|
'👯♀️',
|
||||||
|
'👯♂️️',
|
||||||
|
'🕴️️',
|
||||||
|
'🚶♀️️',
|
||||||
|
'🚶♂️️️',
|
||||||
|
'🏃♀️️️️',
|
||||||
|
'🏃♂️',
|
||||||
|
'👫️',
|
||||||
|
'👭',
|
||||||
|
'👬',
|
||||||
|
'💑',
|
||||||
|
'👩❤️👩',
|
||||||
|
'👨❤️👨',
|
||||||
|
'💏',
|
||||||
|
'👩❤️💋👩',
|
||||||
|
'👨❤️💋👨',
|
||||||
|
'👪',
|
||||||
|
'👨👩👧',
|
||||||
|
'👨👩👧👦',
|
||||||
|
'👨👩👦👦',
|
||||||
|
'👨👩👧👧',
|
||||||
|
'👩👩👦',
|
||||||
|
'👩👩👧',
|
||||||
|
'👩👩👧👦',
|
||||||
|
'👩👩👦👦',
|
||||||
|
'👩👩👧👧',
|
||||||
|
'👨👨👦',
|
||||||
|
'👨👨👧',
|
||||||
|
'👨👨👧👦',
|
||||||
|
'👨👨👦👦',
|
||||||
|
'👨👨👧👧',
|
||||||
|
'👩👦',
|
||||||
|
'👩👧',
|
||||||
|
'👩👧👦',
|
||||||
|
'👩👦👦',
|
||||||
|
'👩👧👧',
|
||||||
|
'👨👦',
|
||||||
|
'👨👧',
|
||||||
|
'👨👧👦',
|
||||||
|
'👨👦👦',
|
||||||
|
'👨👧👧',
|
||||||
|
'🤲',
|
||||||
|
'👐',
|
||||||
|
'🙌',
|
||||||
|
'👏',
|
||||||
|
'🤝',
|
||||||
|
'👍',
|
||||||
|
'👎',
|
||||||
|
'👊',
|
||||||
|
'✊',
|
||||||
|
'🤛',
|
||||||
|
'🤜',
|
||||||
|
'🤞',
|
||||||
|
'✌️',
|
||||||
|
'🤟️',
|
||||||
|
'🤘',
|
||||||
|
'👌',
|
||||||
|
'👈',
|
||||||
|
'👉',
|
||||||
|
'👆',
|
||||||
|
'👇',
|
||||||
|
'☝️',
|
||||||
|
'✋️',
|
||||||
|
'🤚️',
|
||||||
|
'🤚️',
|
||||||
|
'🖐',
|
||||||
|
'🖖',
|
||||||
|
'👋',
|
||||||
|
'🤙',
|
||||||
|
'💪',
|
||||||
|
'🦵',
|
||||||
|
'🦶',
|
||||||
|
'🖕',
|
||||||
|
'✍️',
|
||||||
|
'🙏️',
|
||||||
|
'💍',
|
||||||
|
'💄',
|
||||||
|
'💋',
|
||||||
|
'👄',
|
||||||
|
'👅',
|
||||||
|
'👂',
|
||||||
|
'👃',
|
||||||
|
'👣',
|
||||||
|
'👁',
|
||||||
|
'👀',
|
||||||
|
'🧠',
|
||||||
|
'🦴',
|
||||||
|
'🦷',
|
||||||
|
'🗣',
|
||||||
|
'👤',
|
||||||
|
'👥',
|
||||||
|
'🧥',
|
||||||
|
'👚',
|
||||||
|
'👕',
|
||||||
|
'👖',
|
||||||
|
'👔',
|
||||||
|
'👗',
|
||||||
|
'👙',
|
||||||
|
'👘',
|
||||||
|
'👠',
|
||||||
|
'👡',
|
||||||
|
'👢',
|
||||||
|
'👞',
|
||||||
|
'👟',
|
||||||
|
'🥾',
|
||||||
|
'🥿',
|
||||||
|
'🧦',
|
||||||
|
'🧤',
|
||||||
|
'🧣',
|
||||||
|
'🎩',
|
||||||
|
'🧢',
|
||||||
|
'👒',
|
||||||
|
'🎓',
|
||||||
|
'⛑',
|
||||||
|
'👑',
|
||||||
|
'👝',
|
||||||
|
'👛',
|
||||||
|
'👜',
|
||||||
|
'💼',
|
||||||
|
'🎒',
|
||||||
|
'👓',
|
||||||
|
'🕶',
|
||||||
|
'🥽',
|
||||||
|
'🥼',
|
||||||
|
'🌂',
|
||||||
|
'🧵',
|
||||||
|
'🧶',
|
||||||
|
|
||||||
|
/// Animals
|
||||||
|
'🐶',
|
||||||
|
'🐱',
|
||||||
|
'🐭',
|
||||||
|
'🐰',
|
||||||
|
'🦊',
|
||||||
|
'🦝',
|
||||||
|
'🐻',
|
||||||
|
'🦘',
|
||||||
|
'🦡',
|
||||||
|
'🐨',
|
||||||
|
'🐯',
|
||||||
|
'🦁',
|
||||||
|
'🐼',
|
||||||
|
'🐼',
|
||||||
|
'🐮',
|
||||||
|
'🐷',
|
||||||
|
'🐽',
|
||||||
|
'🐸',
|
||||||
|
'🐵',
|
||||||
|
'🙈',
|
||||||
|
'🙉',
|
||||||
|
'🙊',
|
||||||
|
'🐒',
|
||||||
|
'🐔',
|
||||||
|
'🐧',
|
||||||
|
'🐦',
|
||||||
|
'🐤',
|
||||||
|
'🐣',
|
||||||
|
'🐥',
|
||||||
|
'🦆',
|
||||||
|
'🦢',
|
||||||
|
'🦅',
|
||||||
|
'🦉',
|
||||||
|
'🦚',
|
||||||
|
'🦜',
|
||||||
|
'🦇',
|
||||||
|
'🐺',
|
||||||
|
'🐗',
|
||||||
|
'🐴',
|
||||||
|
'🦄',
|
||||||
|
'🐝',
|
||||||
|
'🐛',
|
||||||
|
'🦋',
|
||||||
|
'🐌',
|
||||||
|
'🐚',
|
||||||
|
'🐞',
|
||||||
|
'🐜',
|
||||||
|
'🦗',
|
||||||
|
'🕷',
|
||||||
|
'🕸',
|
||||||
|
'🦂',
|
||||||
|
'🦟',
|
||||||
|
'🦠',
|
||||||
|
'🐢',
|
||||||
|
'🐍',
|
||||||
|
'🦎',
|
||||||
|
'🦖',
|
||||||
|
'🦕',
|
||||||
|
'🐙',
|
||||||
|
'🦑',
|
||||||
|
'🦐',
|
||||||
|
'🦀',
|
||||||
|
'🐡',
|
||||||
|
'🐠',
|
||||||
|
'🐟',
|
||||||
|
'🐬',
|
||||||
|
'🐳',
|
||||||
|
'🐋',
|
||||||
|
'🦈',
|
||||||
|
'🐊',
|
||||||
|
'🐅',
|
||||||
|
'🐆',
|
||||||
|
'🦓',
|
||||||
|
'🦍',
|
||||||
|
'🐘',
|
||||||
|
'🦏',
|
||||||
|
'🦛',
|
||||||
|
'🐪',
|
||||||
|
'🐫',
|
||||||
|
'🦙',
|
||||||
|
'🦒',
|
||||||
|
'🐃',
|
||||||
|
'🐂',
|
||||||
|
'🐄',
|
||||||
|
'🐎',
|
||||||
|
'🐖',
|
||||||
|
'🐏',
|
||||||
|
'🐐',
|
||||||
|
'🦌',
|
||||||
|
'🐕',
|
||||||
|
'🐩',
|
||||||
|
'🐈',
|
||||||
|
'🐓',
|
||||||
|
'🦃',
|
||||||
|
'🕊',
|
||||||
|
'🐇',
|
||||||
|
'🐁',
|
||||||
|
'🐀',
|
||||||
|
'🐿',
|
||||||
|
'🦔',
|
||||||
|
'🐾',
|
||||||
|
'🐉',
|
||||||
|
'🐲',
|
||||||
|
'🌵',
|
||||||
|
'🎄',
|
||||||
|
'🌲',
|
||||||
|
'🌳',
|
||||||
|
'🌴',
|
||||||
|
'🌱',
|
||||||
|
'🌿',
|
||||||
|
'☘️',
|
||||||
|
'🎍️',
|
||||||
|
'🎋️',
|
||||||
|
'🍃',
|
||||||
|
'🍂',
|
||||||
|
'🍁',
|
||||||
|
'🍄',
|
||||||
|
'🌾️',
|
||||||
|
'💐️',
|
||||||
|
'🌷️',
|
||||||
|
'🌹',
|
||||||
|
'🥀',
|
||||||
|
'🌺',
|
||||||
|
'🌸',
|
||||||
|
'🌼',
|
||||||
|
'🌻️',
|
||||||
|
'🌞',
|
||||||
|
'🌝',
|
||||||
|
'🌛',
|
||||||
|
'🌜',
|
||||||
|
'🌚',
|
||||||
|
'🌕',
|
||||||
|
'🌖',
|
||||||
|
'🌗',
|
||||||
|
'🌘',
|
||||||
|
'🌑',
|
||||||
|
'🌒',
|
||||||
|
'🌔',
|
||||||
|
'🌙',
|
||||||
|
'🌎',
|
||||||
|
'🌍',
|
||||||
|
'🌏',
|
||||||
|
'💫',
|
||||||
|
'⭐️',
|
||||||
|
'🌟️',
|
||||||
|
'✨️',
|
||||||
|
'⚡️️',
|
||||||
|
'☄️️️',
|
||||||
|
'💥️️️',
|
||||||
|
'🔥',
|
||||||
|
'🌪',
|
||||||
|
'🌈',
|
||||||
|
'☀️',
|
||||||
|
'🌤️',
|
||||||
|
'⛅️️',
|
||||||
|
'🌥️️',
|
||||||
|
'☁️️',
|
||||||
|
'🌦️️',
|
||||||
|
'🌧️',
|
||||||
|
'⛈',
|
||||||
|
'🌩',
|
||||||
|
'🌨',
|
||||||
|
'❄️',
|
||||||
|
'☃️️',
|
||||||
|
'⛄️️️',
|
||||||
|
'🌬️️️',
|
||||||
|
'💨️️️',
|
||||||
|
'💧️️️',
|
||||||
|
'💦️️️',
|
||||||
|
'☔️️️️',
|
||||||
|
'☂️️️️️',
|
||||||
|
'🌊️️️️️',
|
||||||
|
'🌫️️️️',
|
||||||
|
|
||||||
|
/// Foods
|
||||||
|
'🍏',
|
||||||
|
'🍎',
|
||||||
|
'🍐',
|
||||||
|
'🍊',
|
||||||
|
'🍋',
|
||||||
|
'🍌',
|
||||||
|
'🍉',
|
||||||
|
'🍇',
|
||||||
|
'🍓',
|
||||||
|
'🍈',
|
||||||
|
'🍒',
|
||||||
|
'🍑',
|
||||||
|
'🍍',
|
||||||
|
'🥭',
|
||||||
|
'🥥',
|
||||||
|
'🥝',
|
||||||
|
'🍅',
|
||||||
|
'🍆',
|
||||||
|
'🥑',
|
||||||
|
'🥦',
|
||||||
|
'🥒',
|
||||||
|
'🥬',
|
||||||
|
'🌶',
|
||||||
|
'🌽',
|
||||||
|
'🥕',
|
||||||
|
'🥔',
|
||||||
|
'🍠',
|
||||||
|
'🥐',
|
||||||
|
'🍞',
|
||||||
|
'🥖',
|
||||||
|
'🥨',
|
||||||
|
'🥯',
|
||||||
|
'🧀',
|
||||||
|
'🥚',
|
||||||
|
'🍳',
|
||||||
|
'🥞',
|
||||||
|
'🥓',
|
||||||
|
'🥩',
|
||||||
|
'🍗',
|
||||||
|
'🍖',
|
||||||
|
'🌭',
|
||||||
|
'🍔',
|
||||||
|
'🍟',
|
||||||
|
'🍕',
|
||||||
|
'🥪',
|
||||||
|
'🥙',
|
||||||
|
'🌮',
|
||||||
|
'🌯',
|
||||||
|
'🥗',
|
||||||
|
'🥘',
|
||||||
|
'🥫',
|
||||||
|
'🍝',
|
||||||
|
'🍜',
|
||||||
|
'🍲',
|
||||||
|
'🍛',
|
||||||
|
'🍣',
|
||||||
|
'🍱',
|
||||||
|
'🥟',
|
||||||
|
'🍤',
|
||||||
|
'🍙',
|
||||||
|
'🍚',
|
||||||
|
'🍘',
|
||||||
|
'🍥',
|
||||||
|
'🥮',
|
||||||
|
'🥠',
|
||||||
|
'🍢',
|
||||||
|
'🍧',
|
||||||
|
'🍨',
|
||||||
|
'🍦',
|
||||||
|
'🥧',
|
||||||
|
'🍰',
|
||||||
|
'🎂',
|
||||||
|
'🍮',
|
||||||
|
'🍭',
|
||||||
|
'🍬',
|
||||||
|
'🍫',
|
||||||
|
'🍿',
|
||||||
|
'🧂',
|
||||||
|
'🍩',
|
||||||
|
'🍪',
|
||||||
|
'🌰',
|
||||||
|
'🥜',
|
||||||
|
'🍯',
|
||||||
|
'🥛',
|
||||||
|
'🍼',
|
||||||
|
'☕️',
|
||||||
|
'🍵️',
|
||||||
|
'🥤️',
|
||||||
|
'🍶',
|
||||||
|
'🍺',
|
||||||
|
'🍻',
|
||||||
|
'🥂',
|
||||||
|
'🍷',
|
||||||
|
'🍸',
|
||||||
|
'🍹',
|
||||||
|
'🍾',
|
||||||
|
'🥄',
|
||||||
|
'🍴',
|
||||||
|
'🍽',
|
||||||
|
'🥣',
|
||||||
|
'🥡',
|
||||||
|
'🥢',
|
||||||
|
|
||||||
|
/// Activity and Sports
|
||||||
|
'⚽️',
|
||||||
|
'🏀️',
|
||||||
|
'🏈',
|
||||||
|
'⚾️',
|
||||||
|
'🥎️',
|
||||||
|
'🏐️',
|
||||||
|
'🏉',
|
||||||
|
'🎾',
|
||||||
|
'🥏',
|
||||||
|
'🎱',
|
||||||
|
'🏓',
|
||||||
|
'🏸',
|
||||||
|
'🥅',
|
||||||
|
'🏒',
|
||||||
|
'🏑',
|
||||||
|
'🥍',
|
||||||
|
'🏏',
|
||||||
|
'⛳️',
|
||||||
|
'🏹️',
|
||||||
|
'🎣️',
|
||||||
|
'🥊',
|
||||||
|
'🥋',
|
||||||
|
'🎽',
|
||||||
|
'⛸',
|
||||||
|
'🥌',
|
||||||
|
'🛷',
|
||||||
|
'🛹',
|
||||||
|
'🎿',
|
||||||
|
'⛷',
|
||||||
|
'🏂',
|
||||||
|
'🏋️♀️',
|
||||||
|
'🏋🏼♀️',
|
||||||
|
'🏋🏽♀️️',
|
||||||
|
'🏋🏾♀️️️',
|
||||||
|
'🏋🏿♀️️️️',
|
||||||
|
'🏋️♂️️️️',
|
||||||
|
'🏋🏻♂️️️️',
|
||||||
|
'🏋🏼♂️️️️',
|
||||||
|
'🏋🏽♂️️️️',
|
||||||
|
'🏋🏾♂️️️️',
|
||||||
|
'🏋🏿♂️️️️',
|
||||||
|
'🤼♀️️️️',
|
||||||
|
'🤼♂️️️️',
|
||||||
|
'🤸♀️️️️',
|
||||||
|
'🤸🏻♀️️️️',
|
||||||
|
'🤸🏼♀️️️️',
|
||||||
|
'🤸🏽♀️️️️',
|
||||||
|
'🤸🏿♀️️️️️',
|
||||||
|
'🤸♂️️️️',
|
||||||
|
'🤸🏻♂️️️️',
|
||||||
|
'🤸🏼♂️️️️️',
|
||||||
|
'🤸🏽♂️️️️️️',
|
||||||
|
'🤸🏾♂️️️️️️',
|
||||||
|
'🤸🏿♂️️️️️️',
|
||||||
|
'⛹️♀️️️️️️',
|
||||||
|
'⛹🏻♀️️️️️️️',
|
||||||
|
'⛹🏼♀️️️️️️️️',
|
||||||
|
'⛹🏽♀️️️️️️️️️',
|
||||||
|
'⛹🏾♀️️️️️️️️️️',
|
||||||
|
'⛹🏿♀️️️️️️️️️️️',
|
||||||
|
'⛹️♂️️️️️️️️️️️️',
|
||||||
|
'⛹🏻♂️️️️️️️️️️️️️',
|
||||||
|
'⛹🏼♂️️️️️️️️️️️️️️',
|
||||||
|
'⛹🏽♂️️️️️️️️️️️️️️️',
|
||||||
|
'⛹🏾♂️️️️️️️️️️️️️️️️',
|
||||||
|
'⛹🏿♂️',
|
||||||
|
'🤺️',
|
||||||
|
'🤾♀️',
|
||||||
|
'🤾🏻♀️️',
|
||||||
|
'🤾🏼♀️️️',
|
||||||
|
'🤾🏾♀️️️️',
|
||||||
|
];
|
||||||
4
lib/src/components/image_editor/data/editor_mode.dart
Normal file
4
lib/src/components/image_editor/data/editor_mode.dart
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
enum EditorMode {
|
||||||
|
brush,
|
||||||
|
filter,
|
||||||
|
}
|
||||||
54
lib/src/components/image_editor/data/image_item.dart
Executable file
54
lib/src/components/image_editor/data/image_item.dart
Executable 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
278
lib/src/components/image_editor/data/layer.dart
Executable file
278
lib/src/components/image_editor/data/layer.dart
Executable 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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
694
lib/src/components/image_editor/image_editor.dart
Normal file
694
lib/src/components/image_editor/image_editor.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
lib/src/components/image_editor/layers/background_blur_layer.dart
Executable file
39
lib/src/components/image_editor/layers/background_blur_layer.dart
Executable 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()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
lib/src/components/image_editor/layers/background_layer.dart
Executable file
32
lib/src/components/image_editor/layers/background_layer.dart
Executable 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
90
lib/src/components/image_editor/layers/emoji_layer.dart
Executable file
90
lib/src/components/image_editor/layers/emoji_layer.dart
Executable 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
102
lib/src/components/image_editor/layers/image_layer.dart
Executable file
102
lib/src/components/image_editor/layers/image_layer.dart
Executable 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
102
lib/src/components/image_editor/layers/text_layer.dart
Executable file
102
lib/src/components/image_editor/layers/text_layer.dart
Executable 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
77
lib/src/components/image_editor/layers_viewer.dart
Normal file
77
lib/src/components/image_editor/layers_viewer.dart
Normal 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
131
lib/src/components/image_editor/loading_screen.dart
Normal file
131
lib/src/components/image_editor/loading_screen.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
83
lib/src/components/image_editor/modules/all_emojis.dart
Executable file
83
lib/src/components/image_editor/modules/all_emojis.dart
Executable 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(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
79
lib/src/components/image_editor/modules/color_pickers_slider.dart
Executable file
79
lib/src/components/image_editor/modules/color_pickers_slider.dart
Executable 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)),
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
353
lib/src/components/image_editor/modules/colors_picker.dart
Executable file
353
lib/src/components/image_editor/modules/colors_picker.dart
Executable 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
91
lib/src/components/image_editor/modules/emoji_layer_overlay.dart
Executable file
91
lib/src/components/image_editor/modules/emoji_layer_overlay.dart
Executable 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
92
lib/src/components/image_editor/modules/image_layer_overlay.dart
Executable file
92
lib/src/components/image_editor/modules/image_layer_overlay.dart
Executable 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
110
lib/src/components/image_editor/modules/text.dart
Executable file
110
lib/src/components/image_editor/modules/text.dart
Executable 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
242
lib/src/components/image_editor/modules/text_layer_overlay.dart
Executable file
242
lib/src/components/image_editor/modules/text_layer_overlay.dart
Executable 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
47
lib/src/components/image_editor/options.dart
Normal file
47
lib/src/components/image_editor/options.dart
Normal 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();
|
||||||
|
}
|
||||||
|
|
@ -155,19 +155,17 @@ Future encryptAndUploadMediaFile(Int64 target, Uint8List imageBytes) async {
|
||||||
await uploadMediaFile(messageId, target, encryptBytes);
|
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
|
// 1. set notifier provider
|
||||||
|
|
||||||
File imageFile = File(imagePath);
|
Uint8List? imageBytesCompressed = await getCompressedImage(imageBytes);
|
||||||
|
if (imageBytesCompressed == null) {
|
||||||
Uint8List? imageBytes = await getCompressedImage(imageFile);
|
|
||||||
if (imageBytes == null) {
|
|
||||||
Logger("api.dart").shout("Error compressing image!");
|
Logger("api.dart").shout("Error compressing image!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < userIds.length; i++) {
|
for (int i = 0; i < userIds.length; i++) {
|
||||||
encryptAndUploadMediaFile(userIds[i], imageBytes);
|
encryptAndUploadMediaFile(userIds[i], imageBytesCompressed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,13 @@ FlutterSecureStorage getSecureStorage() {
|
||||||
return FlutterSecureStorage(aOptions: _getAndroidOptions());
|
return FlutterSecureStorage(aOptions: _getAndroidOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> saveImageToGallery(path) async {
|
Future<String?> saveImageToGallery(Uint8List imageBytes) async {
|
||||||
final hasAccess = await Gal.hasAccess();
|
final hasAccess = await Gal.hasAccess();
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
await Gal.requestAccess();
|
await Gal.requestAccess();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await Gal.putImage(path);
|
await Gal.putImageBytes(imageBytes);
|
||||||
return null;
|
return null;
|
||||||
} on GalException catch (e) {
|
} on GalException catch (e) {
|
||||||
return e.type.message;
|
return e.type.message;
|
||||||
|
|
@ -120,9 +120,9 @@ InputDecoration getInputDecoration(context, hintText) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List?> getCompressedImage(File file) async {
|
Future<Uint8List?> getCompressedImage(Uint8List imageBytes) async {
|
||||||
var result = await FlutterImageCompress.compressWithFile(
|
var result = await FlutterImageCompress.compressWithList(
|
||||||
file.absolute.path,
|
imageBytes,
|
||||||
quality: 90,
|
quality: 90,
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -71,15 +71,15 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
debugPrint('Capturing picture...');
|
debugPrint('Capturing picture...');
|
||||||
case (MediaCaptureStatus.success, true, false):
|
case (MediaCaptureStatus.success, true, false):
|
||||||
event.captureRequest.when(
|
event.captureRequest.when(
|
||||||
single: (single) {
|
single: (single) async {
|
||||||
final path = single.file?.path;
|
final imageBytes = await single.file?.readAsBytes();
|
||||||
if (path == null) return;
|
if (imageBytes == null) return;
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
PageRouteBuilder(
|
PageRouteBuilder(
|
||||||
opaque: false,
|
opaque: false,
|
||||||
pageBuilder: (context, a1, a2) =>
|
pageBuilder: (context, a1, a2) =>
|
||||||
ShareImageEditorView(image: path),
|
ShareImageEditorView(imageBytes: imageBytes),
|
||||||
transitionsBuilder:
|
transitionsBuilder:
|
||||||
(context, animation, secondaryAnimation, child) {
|
(context, animation, secondaryAnimation, child) {
|
||||||
return child;
|
return child;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:twonly/src/components/flame.dart';
|
import 'package:twonly/src/components/flame.dart';
|
||||||
import 'package:twonly/src/components/initialsavatar.dart';
|
import 'package:twonly/src/components/initialsavatar.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,32 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:twonly/src/components/image_editor/image_editor.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/views/share_image_view.dart';
|
import 'package:twonly/src/views/share_image_view.dart';
|
||||||
|
|
||||||
class ShareImageEditorView extends StatefulWidget {
|
class ShareImageEditorView extends StatefulWidget {
|
||||||
const ShareImageEditorView({super.key, required this.image});
|
const ShareImageEditorView({super.key, required this.imageBytes});
|
||||||
final String image;
|
final Uint8List imageBytes;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ShareImageEditorView> createState() => _ShareImageEditorView();
|
State<ShareImageEditorView> createState() => _ShareImageEditorView();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ShareImageEditorView extends State<ShareImageEditorView> {
|
class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
bool _isImageLoaded = false;
|
|
||||||
bool _imageSaved = false;
|
bool _imageSaved = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
imageIsLoaded();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future imageIsLoaded() async {
|
|
||||||
Future.delayed(Duration(milliseconds: 600), () {
|
|
||||||
setState(() {
|
|
||||||
_isImageLoaded = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: _isImageLoaded
|
backgroundColor: true
|
||||||
? Theme.of(context).colorScheme.surface
|
? Theme.of(context).colorScheme.surface
|
||||||
: Colors.white.withAlpha(0),
|
: Colors.white.withAlpha(0),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
|
|
@ -42,99 +34,78 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
children: [
|
children: [
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 0,
|
top: 0,
|
||||||
// bottom: 0,
|
bottom: 70,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 50),
|
padding: const EdgeInsets.symmetric(vertical: 0),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(22),
|
borderRadius: BorderRadius.circular(22),
|
||||||
child: Image.file(
|
// child: Container(),
|
||||||
File(widget.image),
|
child: ImageEditor(
|
||||||
fit: BoxFit.contain,
|
image: widget.imageBytes,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_isImageLoaded
|
Positioned(
|
||||||
? Positioned(
|
bottom: 70,
|
||||||
left: 10,
|
left: 0,
|
||||||
top: 60,
|
right: 0,
|
||||||
child: Row(
|
child: Row(
|
||||||
// mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
OutlinedButton.icon(
|
||||||
icon: Icon(Icons.close, size: 30),
|
icon: _imageSaved
|
||||||
color: Colors.white,
|
? Icon(Icons.check)
|
||||||
onPressed: () async {
|
: FaIcon(FontAwesomeIcons.floppyDisk),
|
||||||
Navigator.pop(context);
|
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 {
|
||||||
: Container(),
|
if (_imageSaved) return;
|
||||||
_isImageLoaded
|
final res = await saveImageToGallery(widget.imageBytes);
|
||||||
? Positioned(
|
if (res == null) {
|
||||||
bottom: 70,
|
setState(() {
|
||||||
left: 0,
|
_imageSaved = true;
|
||||||
right: 0,
|
});
|
||||||
child: Row(
|
}
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
},
|
||||||
children: [
|
label: Text(_imageSaved
|
||||||
OutlinedButton.icon(
|
? AppLocalizations.of(context)!
|
||||||
icon: _imageSaved
|
.shareImagedEditorSavedImage
|
||||||
? Icon(Icons.check)
|
: AppLocalizations.of(context)!
|
||||||
: FaIcon(FontAwesomeIcons.floppyDisk),
|
.shareImagedEditorSaveImage),
|
||||||
style: OutlinedButton.styleFrom(
|
),
|
||||||
iconColor: _imageSaved
|
const SizedBox(width: 20),
|
||||||
? Theme.of(context).colorScheme.outline
|
FilledButton.icon(
|
||||||
: Theme.of(context).colorScheme.primary,
|
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||||
foregroundColor: _imageSaved
|
onPressed: () async {
|
||||||
? Theme.of(context).colorScheme.outline
|
Navigator.push(
|
||||||
: Theme.of(context).colorScheme.primary,
|
context,
|
||||||
),
|
MaterialPageRoute(
|
||||||
onPressed: () async {
|
builder: (context) =>
|
||||||
if (_imageSaved) return;
|
ShareImageView(imageBytes: widget.imageBytes)),
|
||||||
final res = await saveImageToGallery(widget.image);
|
);
|
||||||
if (res == null) {
|
},
|
||||||
setState(() {
|
style: ButtonStyle(
|
||||||
_imageSaved = true;
|
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||||
});
|
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
||||||
}
|
),
|
||||||
},
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
)
|
label: Text(
|
||||||
: Container(),
|
AppLocalizations.of(context)!.shareImagedEditorShareWith,
|
||||||
|
style: TextStyle(fontSize: 17),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'dart:typed_data';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
@ -13,8 +14,8 @@ import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/views/home_view.dart';
|
import 'package:twonly/src/views/home_view.dart';
|
||||||
|
|
||||||
class ShareImageView extends StatefulWidget {
|
class ShareImageView extends StatefulWidget {
|
||||||
const ShareImageView({super.key, required this.image});
|
const ShareImageView({super.key, required this.imageBytes});
|
||||||
final String image;
|
final Uint8List imageBytes;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ShareImageView> createState() => _ShareImageView();
|
State<ShareImageView> createState() => _ShareImageView();
|
||||||
|
|
@ -26,7 +27,6 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
List<Contact> _bestFriends = [];
|
List<Contact> _bestFriends = [];
|
||||||
int maxTotalMediaCounter = 0;
|
int maxTotalMediaCounter = 0;
|
||||||
final HashSet<Int64> _selectedUserIds = HashSet<Int64>();
|
final HashSet<Int64> _selectedUserIds = HashSet<Int64>();
|
||||||
String _lastSearchQuery = '';
|
|
||||||
final TextEditingController searchUserName = TextEditingController();
|
final TextEditingController searchUserName = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -90,7 +90,6 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
user.displayName.toLowerCase().contains(query.toLowerCase()))
|
user.displayName.toLowerCase().contains(query.toLowerCase()))
|
||||||
.toList();
|
.toList();
|
||||||
_updateUsers(usersFiltered);
|
_updateUsers(usersFiltered);
|
||||||
_lastSearchQuery = query;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -146,7 +145,7 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
|
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||||
onPressed: () async {
|
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
|
// 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);
|
Navigator.pop(context);
|
||||||
|
|
|
||||||
154
pubspec.lock
154
pubspec.lock
|
|
@ -262,22 +262,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.11"
|
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:
|
dots_indicator:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -294,14 +278,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.1"
|
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:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -334,6 +310,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
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:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
@ -562,6 +554,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
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:
|
hive:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -938,14 +938,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.1"
|
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:
|
protobuf:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -978,6 +970,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
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:
|
restart_app:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -994,62 +994,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.28.0"
|
version: "0.28.0"
|
||||||
shared_preferences:
|
screenshot:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: screenshot
|
||||||
sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a
|
sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.5"
|
version: "3.0.0"
|
||||||
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"
|
|
||||||
shelf:
|
shelf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1183,14 +1135,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
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:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1207,22 +1151,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.0"
|
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:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1271,14 +1199,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.10.0"
|
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:
|
x25519:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ dependencies:
|
||||||
connectivity_plus: ^6.1.2
|
connectivity_plus: ^6.1.2
|
||||||
cv: ^1.1.3
|
cv: ^1.1.3
|
||||||
fixnum: ^1.1.1
|
fixnum: ^1.1.1
|
||||||
|
flex_color_picker: ^3.7.0
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_image_compress: ^2.4.0
|
flutter_image_compress: ^2.4.0
|
||||||
|
|
@ -24,6 +25,7 @@ dependencies:
|
||||||
font_awesome_flutter: ^10.8.0
|
font_awesome_flutter: ^10.8.0
|
||||||
gal: ^2.3.1
|
gal: ^2.3.1
|
||||||
google_fonts: ^6.2.1
|
google_fonts: ^6.2.1
|
||||||
|
hand_signature: ^3.0.3
|
||||||
hive: ^2.2.3
|
hive: ^2.2.3
|
||||||
image: ^4.3.0
|
image: ^4.3.0
|
||||||
intl: any
|
intl: any
|
||||||
|
|
@ -35,10 +37,11 @@ dependencies:
|
||||||
path_provider: ^2.1.5
|
path_provider: ^2.1.5
|
||||||
permission_handler: ^11.3.1
|
permission_handler: ^11.3.1
|
||||||
pie_menu: ^3.2.7
|
pie_menu: ^3.2.7
|
||||||
pro_image_editor: ^7.6.4
|
|
||||||
protobuf: ^2.1.0
|
protobuf: ^2.1.0
|
||||||
provider: ^6.1.2
|
provider: ^6.1.2
|
||||||
|
reorderables: ^0.6.0
|
||||||
restart_app: ^1.3.2
|
restart_app: ^1.3.2
|
||||||
|
screenshot: ^3.0.0
|
||||||
sqflite_sqlcipher: ^3.1.0+1
|
sqflite_sqlcipher: ^3.1.0+1
|
||||||
web_socket_channel: ^3.0.1
|
web_socket_channel: ^3.0.1
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue