mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 09:28: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);
|
||||
}
|
||||
|
||||
Future sendImage(List<Int64> userIds, String imagePath) async {
|
||||
Future sendImage(List<Int64> userIds, Uint8List imageBytes) async {
|
||||
// 1. set notifier provider
|
||||
|
||||
File imageFile = File(imagePath);
|
||||
|
||||
Uint8List? imageBytes = await getCompressedImage(imageFile);
|
||||
if (imageBytes == null) {
|
||||
Uint8List? imageBytesCompressed = await getCompressedImage(imageBytes);
|
||||
if (imageBytesCompressed == null) {
|
||||
Logger("api.dart").shout("Error compressing image!");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < userIds.length; i++) {
|
||||
encryptAndUploadMediaFile(userIds[i], imageBytes);
|
||||
encryptAndUploadMediaFile(userIds[i], imageBytesCompressed);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,13 +30,13 @@ FlutterSecureStorage getSecureStorage() {
|
|||
return FlutterSecureStorage(aOptions: _getAndroidOptions());
|
||||
}
|
||||
|
||||
Future<String?> saveImageToGallery(path) async {
|
||||
Future<String?> saveImageToGallery(Uint8List imageBytes) async {
|
||||
final hasAccess = await Gal.hasAccess();
|
||||
if (!hasAccess) {
|
||||
await Gal.requestAccess();
|
||||
}
|
||||
try {
|
||||
await Gal.putImage(path);
|
||||
await Gal.putImageBytes(imageBytes);
|
||||
return null;
|
||||
} on GalException catch (e) {
|
||||
return e.type.message;
|
||||
|
|
@ -120,9 +120,9 @@ InputDecoration getInputDecoration(context, hintText) {
|
|||
);
|
||||
}
|
||||
|
||||
Future<Uint8List?> getCompressedImage(File file) async {
|
||||
var result = await FlutterImageCompress.compressWithFile(
|
||||
file.absolute.path,
|
||||
Future<Uint8List?> getCompressedImage(Uint8List imageBytes) async {
|
||||
var result = await FlutterImageCompress.compressWithList(
|
||||
imageBytes,
|
||||
quality: 90,
|
||||
);
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -71,15 +71,15 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
debugPrint('Capturing picture...');
|
||||
case (MediaCaptureStatus.success, true, false):
|
||||
event.captureRequest.when(
|
||||
single: (single) {
|
||||
final path = single.file?.path;
|
||||
if (path == null) return;
|
||||
single: (single) async {
|
||||
final imageBytes = await single.file?.readAsBytes();
|
||||
if (imageBytes == null) return;
|
||||
Navigator.push(
|
||||
context,
|
||||
PageRouteBuilder(
|
||||
opaque: false,
|
||||
pageBuilder: (context, a1, a2) =>
|
||||
ShareImageEditorView(image: path),
|
||||
ShareImageEditorView(imageBytes: imageBytes),
|
||||
transitionsBuilder:
|
||||
(context, animation, secondaryAnimation, child) {
|
||||
return child;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/src/components/flame.dart';
|
||||
import 'package:twonly/src/components/initialsavatar.dart';
|
||||
|
|
|
|||
|
|
@ -1,40 +1,32 @@
|
|||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/components/image_editor/image_editor.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/share_image_view.dart';
|
||||
|
||||
class ShareImageEditorView extends StatefulWidget {
|
||||
const ShareImageEditorView({super.key, required this.image});
|
||||
final String image;
|
||||
const ShareImageEditorView({super.key, required this.imageBytes});
|
||||
final Uint8List imageBytes;
|
||||
|
||||
@override
|
||||
State<ShareImageEditorView> createState() => _ShareImageEditorView();
|
||||
}
|
||||
|
||||
class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||
bool _isImageLoaded = false;
|
||||
bool _imageSaved = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
imageIsLoaded();
|
||||
}
|
||||
|
||||
Future imageIsLoaded() async {
|
||||
Future.delayed(Duration(milliseconds: 600), () {
|
||||
setState(() {
|
||||
_isImageLoaded = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: _isImageLoaded
|
||||
backgroundColor: true
|
||||
? Theme.of(context).colorScheme.surface
|
||||
: Colors.white.withAlpha(0),
|
||||
body: Stack(
|
||||
|
|
@ -42,40 +34,21 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
// bottom: 0,
|
||||
bottom: 70,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 50),
|
||||
padding: const EdgeInsets.symmetric(vertical: 0),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
child: Image.file(
|
||||
File(widget.image),
|
||||
fit: BoxFit.contain,
|
||||
// child: Container(),
|
||||
child: ImageEditor(
|
||||
image: widget.imageBytes,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_isImageLoaded
|
||||
? Positioned(
|
||||
left: 10,
|
||||
top: 60,
|
||||
child: Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.close, size: 30),
|
||||
color: Colors.white,
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
_isImageLoaded
|
||||
? Positioned(
|
||||
Positioned(
|
||||
bottom: 70,
|
||||
left: 0,
|
||||
right: 0,
|
||||
|
|
@ -96,7 +69,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
),
|
||||
onPressed: () async {
|
||||
if (_imageSaved) return;
|
||||
final res = await saveImageToGallery(widget.image);
|
||||
final res = await saveImageToGallery(widget.imageBytes);
|
||||
if (res == null) {
|
||||
setState(() {
|
||||
_imageSaved = true;
|
||||
|
|
@ -117,7 +90,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
ShareImageView(image: widget.image)),
|
||||
ShareImageView(imageBytes: widget.imageBytes)),
|
||||
);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
|
|
@ -126,15 +99,13 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
),
|
||||
),
|
||||
label: Text(
|
||||
AppLocalizations.of(context)!
|
||||
.shareImagedEditorShareWith,
|
||||
AppLocalizations.of(context)!.shareImagedEditorShareWith,
|
||||
style: TextStyle(fontSize: 17),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:collection';
|
||||
import 'dart:typed_data';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
|
@ -13,8 +14,8 @@ import 'package:twonly/src/utils/misc.dart';
|
|||
import 'package:twonly/src/views/home_view.dart';
|
||||
|
||||
class ShareImageView extends StatefulWidget {
|
||||
const ShareImageView({super.key, required this.image});
|
||||
final String image;
|
||||
const ShareImageView({super.key, required this.imageBytes});
|
||||
final Uint8List imageBytes;
|
||||
|
||||
@override
|
||||
State<ShareImageView> createState() => _ShareImageView();
|
||||
|
|
@ -26,7 +27,6 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
List<Contact> _bestFriends = [];
|
||||
int maxTotalMediaCounter = 0;
|
||||
final HashSet<Int64> _selectedUserIds = HashSet<Int64>();
|
||||
String _lastSearchQuery = '';
|
||||
final TextEditingController searchUserName = TextEditingController();
|
||||
|
||||
@override
|
||||
|
|
@ -90,7 +90,6 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
user.displayName.toLowerCase().contains(query.toLowerCase()))
|
||||
.toList();
|
||||
_updateUsers(usersFiltered);
|
||||
_lastSearchQuery = query;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -146,7 +145,7 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
FilledButton.icon(
|
||||
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () async {
|
||||
sendImage(_selectedUserIds.toList(), widget.image);
|
||||
sendImage(_selectedUserIds.toList(), widget.imageBytes);
|
||||
|
||||
// TODO: pop back to the HomeView page popUntil did not work. check later how to improve in case of pushing more then 2
|
||||
Navigator.pop(context);
|
||||
|
|
|
|||
154
pubspec.lock
154
pubspec.lock
|
|
@ -262,22 +262,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
device_info_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: b37d37c2f912ad4e8ec694187de87d05de2a3cb82b465ff1f65f65a2d05de544
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.2.1"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_platform_interface
|
||||
sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
dots_indicator:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -294,14 +278,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
emoji_picker_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: emoji_picker_flutter
|
||||
sha256: "63dee6be976c51c8b971eccbc73fc637f021b6b679eed1b2ec3b503947304734"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -334,6 +310,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flex_color_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flex_color_picker
|
||||
sha256: c083b79f1c57eaeed9f464368be376951230b3cb1876323b784626152a86e480
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.7.0"
|
||||
flex_seed_scheme:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flex_seed_scheme
|
||||
sha256: d3ba3c5c92d2d79d45e94b4c6c71d01fac3c15017da1545880c53864da5dfeb0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.5.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
|
@ -562,6 +554,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
hand_signature:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hand_signature
|
||||
sha256: e007153776b9558234761150b6b3ae98a6b3008e9b824da9911475794a982994
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
hive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -938,14 +938,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.1"
|
||||
pro_image_editor:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pro_image_editor
|
||||
sha256: "918f156f28a72b9185d950f865032f6aa83da485d6d45b22fb63d78b63bf7e21"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.6.4"
|
||||
protobuf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -978,6 +970,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
reorderables:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: reorderables
|
||||
sha256: "004a886e4878df1ee27321831c838bc1c976311f4ca6a74ce7d561e506540a77"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
restart_app:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -994,62 +994,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.28.0"
|
||||
shared_preferences:
|
||||
dependency: transitive
|
||||
screenshot:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a
|
||||
name: screenshot
|
||||
sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.5"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "138b7bbbc7f59c56236e426c37afb8f78cbc57b094ac64c440e0bb90e380a4f5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "3.0.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1183,14 +1135,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
universal_io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: universal_io
|
||||
sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1207,22 +1151,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
vibration:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vibration
|
||||
sha256: "3b08a0579c2f9c18d5d78cb5c74f1005f731e02eeca6d72561a2e8059bf98ec3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
vibration_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vibration_platform_interface
|
||||
sha256: "6ffeee63547562a6fef53c05a41d4fdcae2c0595b83ef59a4813b0612cd2bc36"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.3"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1271,14 +1199,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.10.0"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32_registry
|
||||
sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.5"
|
||||
x25519:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ dependencies:
|
|||
connectivity_plus: ^6.1.2
|
||||
cv: ^1.1.3
|
||||
fixnum: ^1.1.1
|
||||
flex_color_picker: ^3.7.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_image_compress: ^2.4.0
|
||||
|
|
@ -24,6 +25,7 @@ dependencies:
|
|||
font_awesome_flutter: ^10.8.0
|
||||
gal: ^2.3.1
|
||||
google_fonts: ^6.2.1
|
||||
hand_signature: ^3.0.3
|
||||
hive: ^2.2.3
|
||||
image: ^4.3.0
|
||||
intl: any
|
||||
|
|
@ -35,10 +37,11 @@ dependencies:
|
|||
path_provider: ^2.1.5
|
||||
permission_handler: ^11.3.1
|
||||
pie_menu: ^3.2.7
|
||||
pro_image_editor: ^7.6.4
|
||||
protobuf: ^2.1.0
|
||||
provider: ^6.1.2
|
||||
reorderables: ^0.6.0
|
||||
restart_app: ^1.3.2
|
||||
screenshot: ^3.0.0
|
||||
sqflite_sqlcipher: ^3.1.0+1
|
||||
web_socket_channel: ^3.0.1
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue