From 9a162b5b2fd7c4471c1152547f8994c8ee9eb818 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 1 Feb 2025 17:52:40 +0100 Subject: [PATCH] add image editor --- lib/src/components/image_editor/CREDITS.md | 1 + .../components/image_editor/data/data.dart | 710 ++++++++++++++++++ .../image_editor/data/editor_mode.dart | 4 + .../image_editor/data/image_item.dart | 54 ++ .../components/image_editor/data/layer.dart | 278 +++++++ .../components/image_editor/image_editor.dart | 694 +++++++++++++++++ .../layers/background_blur_layer.dart | 39 + .../image_editor/layers/background_layer.dart | 32 + .../image_editor/layers/emoji_layer.dart | 90 +++ .../image_editor/layers/image_layer.dart | 102 +++ .../image_editor/layers/text_layer.dart | 102 +++ .../image_editor/layers_viewer.dart | 77 ++ .../image_editor/loading_screen.dart | 131 ++++ .../image_editor/modules/all_emojis.dart | 83 ++ .../modules/color_pickers_slider.dart | 79 ++ .../image_editor/modules/colors_picker.dart | 353 +++++++++ .../modules/emoji_layer_overlay.dart | 91 +++ .../modules/image_layer_overlay.dart | 92 +++ .../components/image_editor/modules/text.dart | 110 +++ .../modules/text_layer_overlay.dart | 242 ++++++ lib/src/components/image_editor/options.dart | 47 ++ lib/src/providers/api/api.dart | 10 +- lib/src/utils/misc.dart | 10 +- lib/src/views/camera_preview_view.dart | 8 +- lib/src/views/chat_list_view.dart | 3 - lib/src/views/share_image_editor_view.dart | 161 ++-- lib/src/views/share_image_view.dart | 9 +- pubspec.lock | 154 +--- pubspec.yaml | 5 +- 29 files changed, 3535 insertions(+), 236 deletions(-) create mode 100644 lib/src/components/image_editor/CREDITS.md create mode 100755 lib/src/components/image_editor/data/data.dart create mode 100644 lib/src/components/image_editor/data/editor_mode.dart create mode 100755 lib/src/components/image_editor/data/image_item.dart create mode 100755 lib/src/components/image_editor/data/layer.dart create mode 100644 lib/src/components/image_editor/image_editor.dart create mode 100755 lib/src/components/image_editor/layers/background_blur_layer.dart create mode 100755 lib/src/components/image_editor/layers/background_layer.dart create mode 100755 lib/src/components/image_editor/layers/emoji_layer.dart create mode 100755 lib/src/components/image_editor/layers/image_layer.dart create mode 100755 lib/src/components/image_editor/layers/text_layer.dart create mode 100644 lib/src/components/image_editor/layers_viewer.dart create mode 100644 lib/src/components/image_editor/loading_screen.dart create mode 100755 lib/src/components/image_editor/modules/all_emojis.dart create mode 100755 lib/src/components/image_editor/modules/color_pickers_slider.dart create mode 100755 lib/src/components/image_editor/modules/colors_picker.dart create mode 100755 lib/src/components/image_editor/modules/emoji_layer_overlay.dart create mode 100755 lib/src/components/image_editor/modules/image_layer_overlay.dart create mode 100755 lib/src/components/image_editor/modules/text.dart create mode 100755 lib/src/components/image_editor/modules/text_layer_overlay.dart create mode 100644 lib/src/components/image_editor/options.dart diff --git a/lib/src/components/image_editor/CREDITS.md b/lib/src/components/image_editor/CREDITS.md new file mode 100644 index 0000000..9cee998 --- /dev/null +++ b/lib/src/components/image_editor/CREDITS.md @@ -0,0 +1 @@ +The image editor is based on: https://github.com/hsbijarniya/image_editor_plus/tree/main diff --git a/lib/src/components/image_editor/data/data.dart b/lib/src/components/image_editor/data/data.dart new file mode 100755 index 0000000..a686020 --- /dev/null +++ b/lib/src/components/image_editor/data/data.dart @@ -0,0 +1,710 @@ +Map emojiWeights = {}; + +List emojis = [ + '๐Ÿ˜€', + '๐Ÿ˜', + '๐Ÿ˜‚', + '๐Ÿคฃ', + '๐Ÿ˜ƒ', + '๐Ÿ˜„', + '๐Ÿ˜…', + '๐Ÿ˜†', + '๐Ÿ˜‰', + '๐Ÿ˜Š', + '๐Ÿ˜‹', + '๐Ÿ˜Ž', + '๐Ÿ˜', + '๐Ÿ˜˜', + '๐Ÿฅฐ', + '๐Ÿ˜—', + '๐Ÿ˜™', + '๐Ÿ˜š', + '๐Ÿ™‚๏ธ', + '๐Ÿค—', + '๐Ÿคฉ', + '๐Ÿค”', + '๐Ÿค”', + '๐Ÿคจ', + '๐Ÿ˜', + '๐Ÿ˜‘', + '๐Ÿ˜ถ', + '๐Ÿ™„', + '๐Ÿ˜', + '๐Ÿ˜ฃ', + '๐Ÿ˜ฅ', + '๐Ÿ˜ฎ', + '๐Ÿค', + '๐Ÿ˜ฏ', + '๐Ÿ˜ช', + '๐Ÿ˜ซ', + '๐Ÿ˜ด', + '๐Ÿ˜Œ', + '๐Ÿ˜›', + '๐Ÿ˜œ', + '๐Ÿ˜', + '๐Ÿคค', + '๐Ÿ˜’', + '๐Ÿ˜“', + '๐Ÿ˜”', + '๐Ÿ˜•', + '๐Ÿ™ƒ', + '๐Ÿค‘', + '๐Ÿ˜ฒ', + '๐Ÿ™', + '๐Ÿ˜–', + '๐Ÿ˜ž', + '๐Ÿ˜Ÿ', + '๐Ÿ˜ค', + '๐Ÿ˜ข', + '๐Ÿ˜ญ', + '๐Ÿ˜ฆ', + '๐Ÿ˜ง', + '๐Ÿ˜จ', + '๐Ÿ˜ฉ', + '๐Ÿคฏ', + '๐Ÿ˜ฌ', + '๐Ÿ˜ฐ', + '๐Ÿ˜ฑ', + '๐Ÿฅต', + '๐Ÿฅถ', + '๐Ÿ˜ณ', + '๐Ÿคช', + '๐Ÿ˜ต', + '๐Ÿ˜ก', + '๐Ÿ˜ ', + '๐Ÿคฌ', + '๐Ÿ˜ท', + '๐Ÿค’', + '๐Ÿค•', + '๐Ÿคข', + '๐Ÿคฎ', + '๐Ÿคง', + '๐Ÿ˜‡', + '๐Ÿค ', + '๐Ÿคก', + '๐Ÿฅณ', + '๐Ÿฅด', + '๐Ÿคฅ', + '๐Ÿคซ', + '๐Ÿคญ', + '๐Ÿคญ', + '๐Ÿง', + '๐Ÿค“', + '๐Ÿ˜ˆ', + '๐Ÿ‘ฟ', + '๐Ÿ‘น', + '๐Ÿ‘บ', + '๐Ÿ’€', + '๐Ÿ‘ป', + '๐Ÿ‘ฝ', + '๐Ÿค–', + '๐Ÿ’ฉ', + '๐Ÿ˜บ', + '๐Ÿ˜ธ', + '๐Ÿ˜น', + '๐Ÿ˜ป', + '๐Ÿ˜ผ', + '๐Ÿ˜ฝ', + '๐Ÿ™€', + '๐Ÿ˜ฟ', + '๐Ÿ˜พ', + '๐Ÿ˜พ', + + /// People and Fantasy + '๐Ÿ‘ถ', + '๐Ÿ‘ง', + '๐Ÿง’', + '๐Ÿ‘ฉ', + '๐Ÿง‘', + '๐Ÿ‘จ', + '๐Ÿ‘ต', + '๐Ÿ‘ด', + '๐Ÿ‘ฒ', + '๐Ÿ‘ณโ€โ™€๏ธโ€๏ธ', + '๐Ÿ‘ณโ€โ™‚๏ธ๏ธโ€๏ธ', + '๐Ÿง•๏ธ๏ธโ€๏ธ', + '๐Ÿง”โ€', + '๐Ÿ‘ฑโ€โ™‚๏ธ๏ธโ€', + '๐Ÿ‘ฑโ€โ™€๏ธ๏ธ๏ธโ€', + '๐Ÿ‘จโ€๐Ÿฆฐ๏ธ๏ธ๏ธโ€', + '๐Ÿ‘ฉโ€๐Ÿฆฐโ€', + '๐Ÿ‘จโ€๐Ÿฆฑโ€โ€', + '๐Ÿ‘จโ€๐Ÿฆฒโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿฆฒโ€โ€', + '๐Ÿ‘จโ€๐Ÿฆณโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿฆณโ€โ€', + '๐Ÿฆธโ€โ™€๏ธโ€โ€', + '๐Ÿฆธโ€โ™‚๏ธ๏ธโ€โ€', + '๐Ÿฆนโ€โ™€๏ธ๏ธ๏ธโ€โ€', + '๐Ÿฆนโ€โ™‚๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ‘ฎโ€โ™€๏ธโ€โ€', + '๐Ÿ‘ฎโ€โ™‚๏ธ๏ธโ€โ€', + '๐Ÿ‘ทโ€โ™€๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ‘ทโ€โ™‚๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ’‚โ€โ™€๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ’‚โ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ•ต๏ธโ€โ™€๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ•ต๏ธโ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ‘ฉโ€โš•๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ‘จโ€โš•๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ‘ฉโ€๐ŸŒพ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ‘จโ€๐ŸŒพโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿณโ€โ€', + '๐Ÿ‘จโ€๐Ÿณโ€โ€', + '๐Ÿ‘ฉโ€๐ŸŽ“โ€โ€', + '๐Ÿ‘จโ€๐ŸŽ“โ€โ€', + '๐Ÿ‘ฉโ€๐ŸŽคโ€โ€', + '๐Ÿ‘จโ€๐ŸŽคโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿซโ€โ€', + '๐Ÿ‘จโ€๐Ÿซโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿญโ€โ€', + '๐Ÿ‘จโ€๐Ÿญโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿ’ปโ€โ€', + '๐Ÿ‘จโ€๐Ÿ’ปโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿ’ผโ€โ€', + '๐Ÿ‘จโ€๐Ÿ’ผโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿ”งโ€โ€', + '๐Ÿ‘จโ€๐Ÿ”งโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿ”ฌโ€โ€', + '๐Ÿ‘จโ€๐Ÿ”ฌโ€โ€', + '๐Ÿ‘ฉโ€๐ŸŽจโ€โ€', + '๐Ÿ‘จโ€๐ŸŽจโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿš’โ€โ€', + '๐Ÿ‘จโ€๐Ÿš’โ€โ€', + '๐Ÿ‘ฉโ€โœˆ๏ธโ€โ€', + '๐Ÿ‘จโ€โœˆ๏ธ๏ธโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿš€โ€โ€', + '๐Ÿ‘จโ€๐Ÿš€โ€โ€', + '๐Ÿ‘ฉโ€โš–๏ธโ€โ€', + '๐Ÿ‘จโ€โš–๏ธ๏ธโ€โ€', + '๐Ÿ‘ฐโ€โ€', + '๐Ÿคตโ€โ€', + '๐Ÿ‘ธโ€โ€', + '๐Ÿคดโ€โ€', + '๐Ÿคถโ€โ€', + '๐ŸŽ…โ€โ€', + '๐Ÿง™โ€โ™€๏ธโ€โ€', + '๐Ÿง™โ€โ™‚๏ธ๏ธโ€โ€', + '๐Ÿงโ€โ™€๏ธ๏ธ๏ธโ€โ€', + '๐Ÿงโ€โ™‚๏ธโ€โ€', + '๐Ÿง›โ€โ™€๏ธ๏ธโ€โ€', + '๐Ÿง›โ€โ™‚๏ธ๏ธ๏ธโ€โ€', + '๐ŸงŸโ€โ™€๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐ŸงŸโ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿงžโ€โ™€๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿงžโ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿงœโ€โ™€๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿงœโ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿงšโ€โ™€๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿงšโ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ‘ผ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿคฐโ€โ€', + '๐Ÿคฑโ€โ€', + '๐Ÿ™‡โ€โ™€๏ธโ€โ€', + '๐Ÿ™‡โ€โ™‚๏ธโ€โ€', + '๐Ÿ’โ€โ™€๏ธ๏ธโ€โ€', + '๐Ÿ’โ€โ™‚๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ™…โ€โ™€๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ™…โ€โ™‚๏ธโ€โ€', + '๐Ÿ™†โ€โ™€๏ธ๏ธโ€โ€', + '๐Ÿ™†โ€โ™‚๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ™‹โ€โ™€๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ™‹โ€โ™‚๏ธโ€โ€', + '๐Ÿคฆโ€โ™€๏ธ๏ธโ€โ€', + '๐Ÿคฆโ€โ™‚๏ธ๏ธ๏ธโ€โ€', + '๐Ÿคทโ€โ™€๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿคทโ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ™Žโ€โ™€๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ™Žโ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ™โ€โ™€๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ™โ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ’‡โ€โ™€๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ’‡โ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ’†โ€โ™€๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ’†โ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿง–โ€โ™€๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿง–โ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ’…๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿคณ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ’ƒ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ•บ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿ‘ฏโ€โ™€๏ธโ€โ€', + '๐Ÿ‘ฏโ€โ™‚๏ธ๏ธโ€โ€', + '๐Ÿ•ด๏ธ๏ธโ€โ€', + '๐Ÿšถโ€โ™€๏ธ๏ธโ€โ€', + '๐Ÿšถโ€โ™‚๏ธ๏ธ๏ธโ€โ€', + '๐Ÿƒโ€โ™€๏ธ๏ธ๏ธ๏ธโ€โ€', + '๐Ÿƒโ€โ™‚๏ธโ€โ€', + '๐Ÿ‘ซ๏ธโ€โ€', + '๐Ÿ‘ญโ€โ€', + '๐Ÿ‘ฌโ€โ€', + '๐Ÿ’‘โ€โ€', + '๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ‘ฉโ€โ€', + '๐Ÿ‘จโ€โค๏ธโ€๐Ÿ‘จโ€โ€', + '๐Ÿ’โ€โ€', + '๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉโ€โ€', + '๐Ÿ‘จโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จโ€โ€', + '๐Ÿ‘ชโ€โ€', + '๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€โ€', + '๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆโ€โ€', + '๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆโ€โ€', + '๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘งโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘งโ€โ€', + '๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆโ€โ€', + '๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€โ€', + '๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆโ€โ€', + '๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆโ€โ€', + '๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘งโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿ‘งโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆโ€โ€', + '๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘งโ€โ€', + '๐Ÿ‘จโ€๐Ÿ‘ฆโ€โ€', + '๐Ÿ‘จโ€๐Ÿ‘งโ€โ€', + '๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆโ€โ€', + '๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆโ€โ€', + '๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘งโ€โ€', + '๐Ÿคฒโ€โ€', + '๐Ÿ‘โ€โ€', + '๐Ÿ™Œโ€โ€', + '๐Ÿ‘โ€โ€', + '๐Ÿคโ€โ€', + '๐Ÿ‘โ€โ€', + '๐Ÿ‘Žโ€โ€', + '๐Ÿ‘Šโ€โ€', + 'โœŠโ€โ€', + '๐Ÿค›โ€โ€', + '๐Ÿคœโ€โ€', + '๐Ÿคžโ€โ€', + 'โœŒ๏ธโ€โ€', + '๐ŸคŸ๏ธโ€โ€', + '๐Ÿค˜โ€โ€', + '๐Ÿ‘Œโ€โ€', + '๐Ÿ‘ˆโ€โ€', + '๐Ÿ‘‰โ€โ€', + '๐Ÿ‘†โ€โ€', + '๐Ÿ‘‡โ€โ€', + 'โ˜๏ธโ€โ€', + 'โœ‹๏ธโ€โ€', + '๐Ÿคš๏ธโ€โ€', + '๐Ÿคš๏ธโ€โ€', + '๐Ÿ–โ€โ€', + '๐Ÿ––โ€โ€', + '๐Ÿ‘‹โ€โ€', + '๐Ÿค™โ€โ€', + '๐Ÿ’ชโ€โ€', + '๐Ÿฆตโ€โ€', + '๐Ÿฆถโ€โ€', + '๐Ÿ–•โ€โ€', + 'โœ๏ธโ€โ€', + '๐Ÿ™๏ธโ€โ€', + '๐Ÿ’โ€โ€', + '๐Ÿ’„โ€โ€', + '๐Ÿ’‹โ€โ€', + '๐Ÿ‘„โ€โ€', + '๐Ÿ‘…โ€โ€', + '๐Ÿ‘‚โ€โ€', + '๐Ÿ‘ƒโ€โ€', + '๐Ÿ‘ฃโ€โ€', + '๐Ÿ‘โ€โ€', + '๐Ÿ‘€โ€โ€', + '๐Ÿง โ€โ€', + '๐Ÿฆดโ€โ€', + '๐Ÿฆทโ€โ€', + '๐Ÿ—ฃโ€โ€', + '๐Ÿ‘คโ€โ€', + '๐Ÿ‘ฅโ€โ€', + '๐Ÿงฅโ€โ€', + '๐Ÿ‘šโ€โ€', + '๐Ÿ‘•โ€โ€', + '๐Ÿ‘–โ€โ€', + '๐Ÿ‘”โ€โ€', + '๐Ÿ‘—โ€โ€', + '๐Ÿ‘™โ€โ€', + '๐Ÿ‘˜โ€โ€', + '๐Ÿ‘ โ€โ€', + '๐Ÿ‘กโ€โ€', + '๐Ÿ‘ขโ€โ€', + '๐Ÿ‘žโ€โ€', + '๐Ÿ‘Ÿโ€โ€', + '๐Ÿฅพโ€โ€', + '๐Ÿฅฟโ€โ€', + '๐Ÿงฆโ€โ€', + '๐Ÿงคโ€โ€', + '๐Ÿงฃโ€โ€', + '๐ŸŽฉโ€โ€', + '๐Ÿงขโ€โ€', + '๐Ÿ‘’โ€โ€', + '๐ŸŽ“โ€โ€', + 'โ›‘โ€โ€', + '๐Ÿ‘‘โ€โ€', + '๐Ÿ‘โ€โ€', + '๐Ÿ‘›โ€โ€', + '๐Ÿ‘œโ€โ€', + '๐Ÿ’ผโ€โ€', + '๐ŸŽ’โ€โ€', + '๐Ÿ‘“โ€โ€', + '๐Ÿ•ถโ€โ€', + '๐Ÿฅฝโ€โ€', + '๐Ÿฅผโ€โ€', + '๐ŸŒ‚โ€โ€', + '๐Ÿงตโ€โ€', + '๐Ÿงถโ€โ€', + + /// Animals + '๐Ÿถโ€โ€', + '๐Ÿฑโ€โ€', + '๐Ÿญโ€โ€', + '๐Ÿฐโ€โ€', + '๐ŸฆŠโ€โ€', + '๐Ÿฆโ€โ€', + '๐Ÿปโ€โ€', + '๐Ÿฆ˜โ€โ€', + '๐Ÿฆกโ€โ€', + '๐Ÿจโ€โ€', + '๐Ÿฏโ€โ€', + '๐Ÿฆโ€โ€', + '๐Ÿผโ€โ€', + '๐Ÿผโ€โ€', + '๐Ÿฎโ€โ€', + '๐Ÿทโ€โ€', + '๐Ÿฝโ€โ€', + '๐Ÿธโ€โ€', + '๐Ÿตโ€โ€', + '๐Ÿ™ˆโ€โ€', + '๐Ÿ™‰โ€โ€', + '๐Ÿ™Šโ€โ€', + '๐Ÿ’โ€โ€', + '๐Ÿ”โ€โ€', + '๐Ÿงโ€โ€', + '๐Ÿฆโ€โ€', + '๐Ÿคโ€โ€', + '๐Ÿฃโ€โ€', + '๐Ÿฅโ€โ€', + '๐Ÿฆ†โ€โ€', + '๐Ÿฆขโ€โ€', + '๐Ÿฆ…โ€โ€', + '๐Ÿฆ‰โ€โ€', + '๐Ÿฆšโ€โ€', + '๐Ÿฆœโ€โ€', + '๐Ÿฆ‡โ€โ€', + '๐Ÿบโ€โ€', + '๐Ÿ—โ€โ€', + '๐Ÿดโ€โ€', + '๐Ÿฆ„โ€โ€', + '๐Ÿโ€โ€', + '๐Ÿ›โ€โ€', + '๐Ÿฆ‹โ€โ€', + '๐ŸŒโ€โ€', + '๐Ÿšโ€โ€', + '๐Ÿžโ€โ€', + '๐Ÿœโ€โ€', + '๐Ÿฆ—โ€โ€', + '๐Ÿ•ทโ€โ€', + '๐Ÿ•ธโ€โ€', + '๐Ÿฆ‚โ€โ€', + '๐ŸฆŸโ€โ€', + '๐Ÿฆ โ€โ€', + '๐Ÿขโ€โ€', + '๐Ÿโ€โ€', + '๐ŸฆŽโ€โ€', + '๐Ÿฆ–โ€โ€', + '๐Ÿฆ•โ€โ€', + '๐Ÿ™โ€โ€', + '๐Ÿฆ‘โ€โ€', + '๐Ÿฆโ€โ€', + '๐Ÿฆ€โ€โ€', + '๐Ÿกโ€โ€', + '๐Ÿ โ€โ€', + '๐ŸŸโ€โ€', + '๐Ÿฌโ€โ€', + '๐Ÿณโ€โ€', + '๐Ÿ‹โ€โ€', + '๐Ÿฆˆโ€โ€', + '๐ŸŠโ€โ€', + '๐Ÿ…โ€โ€', + '๐Ÿ†โ€โ€', + '๐Ÿฆ“โ€โ€', + '๐Ÿฆโ€โ€', + '๐Ÿ˜โ€โ€', + '๐Ÿฆโ€โ€', + '๐Ÿฆ›โ€โ€', + '๐Ÿชโ€โ€', + '๐Ÿซโ€โ€', + '๐Ÿฆ™โ€โ€', + '๐Ÿฆ’โ€โ€', + '๐Ÿƒโ€โ€', + '๐Ÿ‚โ€โ€', + '๐Ÿ„โ€โ€', + '๐ŸŽโ€โ€', + '๐Ÿ–โ€โ€', + '๐Ÿโ€โ€', + '๐Ÿโ€โ€', + '๐ŸฆŒโ€โ€', + '๐Ÿ•โ€โ€', + '๐Ÿฉโ€โ€', + '๐Ÿˆโ€โ€', + '๐Ÿ“โ€โ€', + '๐Ÿฆƒโ€โ€', + '๐Ÿ•Šโ€โ€', + '๐Ÿ‡โ€โ€', + '๐Ÿโ€โ€', + '๐Ÿ€โ€โ€', + '๐Ÿฟโ€โ€', + '๐Ÿฆ”โ€โ€', + '๐Ÿพโ€', + '๐Ÿ‰โ€', + '๐Ÿฒโ€', + '๐ŸŒตโ€', + '๐ŸŽ„โ€', + '๐ŸŒฒโ€', + '๐ŸŒณโ€', + '๐ŸŒดโ€', + '๐ŸŒฑโ€', + '๐ŸŒฟโ€', + 'โ˜˜๏ธโ€', + '๐ŸŽ๏ธโ€', + '๐ŸŽ‹๏ธโ€', + '๐Ÿƒโ€', + '๐Ÿ‚โ€', + '๐Ÿโ€', + '๐Ÿ„โ€', + '๐ŸŒพ๏ธโ€', + '๐Ÿ’๏ธโ€', + '๐ŸŒท๏ธโ€', + '๐ŸŒนโ€', + '๐Ÿฅ€โ€', + '๐ŸŒบโ€', + '๐ŸŒธโ€', + '๐ŸŒผโ€', + '๐ŸŒป๏ธโ€', + '๐ŸŒžโ€', + '๐ŸŒโ€', + '๐ŸŒ›โ€', + '๐ŸŒœโ€', + '๐ŸŒšโ€', + '๐ŸŒ•โ€', + '๐ŸŒ–โ€', + '๐ŸŒ—โ€', + '๐ŸŒ˜โ€', + '๐ŸŒ‘โ€', + '๐ŸŒ’โ€', + '๐ŸŒ”โ€', + '๐ŸŒ™โ€', + '๐ŸŒŽโ€', + '๐ŸŒโ€', + '๐ŸŒโ€', + '๐Ÿ’ซโ€', + 'โญ๏ธโ€', + '๐ŸŒŸ๏ธโ€', + 'โœจ๏ธโ€', + 'โšก๏ธ๏ธโ€', + 'โ˜„๏ธ๏ธ๏ธโ€', + '๐Ÿ’ฅ๏ธ๏ธ๏ธโ€', + '๐Ÿ”ฅโ€', + '๐ŸŒชโ€', + '๐ŸŒˆโ€', + 'โ˜€๏ธโ€', + '๐ŸŒค๏ธโ€', + 'โ›…๏ธ๏ธโ€', + '๐ŸŒฅ๏ธ๏ธโ€', + 'โ˜๏ธ๏ธโ€', + '๐ŸŒฆ๏ธ๏ธโ€', + '๐ŸŒง๏ธโ€', + 'โ›ˆโ€', + '๐ŸŒฉโ€', + '๐ŸŒจโ€', + 'โ„๏ธโ€', + 'โ˜ƒ๏ธ๏ธโ€', + 'โ›„๏ธ๏ธ๏ธโ€', + '๐ŸŒฌ๏ธ๏ธ๏ธโ€', + '๐Ÿ’จ๏ธ๏ธ๏ธโ€', + '๐Ÿ’ง๏ธ๏ธ๏ธโ€', + '๐Ÿ’ฆ๏ธ๏ธ๏ธโ€', + 'โ˜”๏ธ๏ธ๏ธ๏ธโ€', + 'โ˜‚๏ธ๏ธ๏ธ๏ธ๏ธโ€', + '๐ŸŒŠ๏ธ๏ธ๏ธ๏ธ๏ธโ€', + '๐ŸŒซ๏ธ๏ธ๏ธ๏ธโ€', + + /// Foods + '๐Ÿโ€', + '๐ŸŽโ€', + '๐Ÿโ€', + '๐ŸŠโ€', + '๐Ÿ‹โ€', + '๐ŸŒโ€', + '๐Ÿ‰โ€', + '๐Ÿ‡โ€', + '๐Ÿ“โ€', + '๐Ÿˆโ€', + '๐Ÿ’โ€', + '๐Ÿ‘โ€', + '๐Ÿโ€', + '๐Ÿฅญโ€', + '๐Ÿฅฅโ€', + '๐Ÿฅโ€', + '๐Ÿ…โ€', + '๐Ÿ†โ€', + '๐Ÿฅ‘โ€', + '๐Ÿฅฆโ€', + '๐Ÿฅ’โ€', + '๐Ÿฅฌโ€', + '๐ŸŒถโ€', + '๐ŸŒฝโ€', + '๐Ÿฅ•โ€', + '๐Ÿฅ”โ€', + '๐Ÿ โ€', + '๐Ÿฅโ€', + '๐Ÿžโ€', + '๐Ÿฅ–โ€', + '๐Ÿฅจโ€', + '๐Ÿฅฏโ€', + '๐Ÿง€โ€', + '๐Ÿฅšโ€', + '๐Ÿณโ€', + '๐Ÿฅžโ€', + '๐Ÿฅ“โ€', + '๐Ÿฅฉโ€', + '๐Ÿ—โ€', + '๐Ÿ–โ€', + '๐ŸŒญโ€', + '๐Ÿ”โ€', + '๐ŸŸโ€', + '๐Ÿ•โ€', + '๐Ÿฅชโ€', + '๐Ÿฅ™โ€', + '๐ŸŒฎโ€', + '๐ŸŒฏโ€', + '๐Ÿฅ—โ€', + '๐Ÿฅ˜โ€', + '๐Ÿฅซโ€', + '๐Ÿโ€', + '๐Ÿœโ€', + '๐Ÿฒโ€', + '๐Ÿ›โ€', + '๐Ÿฃโ€', + '๐Ÿฑโ€', + '๐ŸฅŸโ€', + '๐Ÿคโ€', + '๐Ÿ™โ€', + '๐Ÿšโ€', + '๐Ÿ˜โ€', + '๐Ÿฅโ€', + '๐Ÿฅฎโ€', + '๐Ÿฅ โ€', + '๐Ÿขโ€', + '๐Ÿงโ€', + '๐Ÿจโ€', + '๐Ÿฆโ€', + '๐Ÿฅงโ€', + '๐Ÿฐโ€', + '๐ŸŽ‚โ€', + '๐Ÿฎโ€', + '๐Ÿญโ€', + '๐Ÿฌโ€', + '๐Ÿซโ€', + '๐Ÿฟโ€', + '๐Ÿง‚โ€', + '๐Ÿฉโ€', + '๐Ÿชโ€', + '๐ŸŒฐโ€', + '๐Ÿฅœโ€', + '๐Ÿฏโ€', + '๐Ÿฅ›โ€', + '๐Ÿผโ€', + 'โ˜•๏ธโ€', + '๐Ÿต๏ธโ€', + '๐Ÿฅค๏ธโ€', + '๐Ÿถโ€', + '๐Ÿบโ€', + '๐Ÿปโ€', + '๐Ÿฅ‚โ€', + '๐Ÿทโ€', + '๐Ÿธโ€', + '๐Ÿนโ€', + '๐Ÿพโ€', + '๐Ÿฅ„โ€', + '๐Ÿดโ€', + '๐Ÿฝโ€', + '๐Ÿฅฃโ€', + '๐Ÿฅกโ€', + '๐Ÿฅขโ€', + + /// Activity and Sports + 'โšฝ๏ธโ€', + '๐Ÿ€๏ธโ€', + '๐Ÿˆโ€', + 'โšพ๏ธโ€', + '๐ŸฅŽ๏ธโ€', + '๐Ÿ๏ธโ€', + '๐Ÿ‰โ€', + '๐ŸŽพโ€', + '๐Ÿฅโ€', + '๐ŸŽฑโ€', + '๐Ÿ“โ€', + '๐Ÿธโ€', + '๐Ÿฅ…โ€', + '๐Ÿ’โ€', + '๐Ÿ‘โ€', + '๐Ÿฅโ€', + '๐Ÿโ€', + 'โ›ณ๏ธโ€', + '๐Ÿน๏ธโ€', + '๐ŸŽฃ๏ธโ€', + '๐ŸฅŠโ€', + '๐Ÿฅ‹โ€', + '๐ŸŽฝโ€', + 'โ›ธโ€', + '๐ŸฅŒโ€', + '๐Ÿ›ทโ€', + '๐Ÿ›นโ€', + '๐ŸŽฟโ€', + 'โ›ทโ€', + '๐Ÿ‚โ€', + '๐Ÿ‹๏ธโ€โ™€๏ธโ€', + '๐Ÿ‹๐Ÿผโ€โ™€๏ธโ€', + '๐Ÿ‹๐Ÿฝโ€โ™€๏ธ๏ธโ€', + '๐Ÿ‹๐Ÿพโ€โ™€๏ธ๏ธ๏ธโ€', + '๐Ÿ‹๐Ÿฟโ€โ™€๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿ‹๏ธโ€โ™‚๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿ‹๐Ÿปโ€โ™‚๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿ‹๐Ÿผโ€โ™‚๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿ‹๐Ÿฝโ€โ™‚๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿ‹๐Ÿพโ€โ™‚๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿ‹๐Ÿฟโ€โ™‚๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿคผโ€โ™€๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿคผโ€โ™‚๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿคธโ€โ™€๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿคธ๐Ÿปโ€โ™€๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿคธ๐Ÿผโ€โ™€๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿคธ๐Ÿฝโ€โ™€๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿคธ๐Ÿฟโ€โ™€๏ธ๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿคธโ€โ™‚๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿคธ๐Ÿปโ€โ™‚๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿคธ๐Ÿผโ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿคธ๐Ÿฝโ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿคธ๐Ÿพโ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€', + '๐Ÿคธ๐Ÿฟโ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€', + 'โ›น๏ธโ€โ™€๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€', + 'โ›น๐Ÿปโ€โ™€๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€', + 'โ›น๐Ÿผโ€โ™€๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€', + 'โ›น๐Ÿฝโ€โ™€๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€', + 'โ›น๐Ÿพโ€โ™€๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€', + 'โ›น๐Ÿฟโ€โ™€๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€', + 'โ›น๏ธโ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€', + 'โ›น๐Ÿปโ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€', + 'โ›น๐Ÿผโ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€', + 'โ›น๐Ÿฝโ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€', + 'โ›น๐Ÿพโ€โ™‚๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธ๏ธโ€', + 'โ›น๐Ÿฟโ€โ™‚๏ธโ€', + '๐Ÿคบ๏ธโ€', + '๐Ÿคพโ€โ™€๏ธโ€', + '๐Ÿคพ๐Ÿปโ€โ™€๏ธ๏ธโ€', + '๐Ÿคพ๐Ÿผโ€โ™€๏ธ๏ธ๏ธโ€', + '๐Ÿคพ๐Ÿพโ€โ™€๏ธ๏ธ๏ธ๏ธโ€', +]; diff --git a/lib/src/components/image_editor/data/editor_mode.dart b/lib/src/components/image_editor/data/editor_mode.dart new file mode 100644 index 0000000..2a38cba --- /dev/null +++ b/lib/src/components/image_editor/data/editor_mode.dart @@ -0,0 +1,4 @@ +enum EditorMode { + brush, + filter, +} diff --git a/lib/src/components/image_editor/data/image_item.dart b/lib/src/components/image_editor/data/image_item.dart new file mode 100755 index 0000000..920a8cd --- /dev/null +++ b/lib/src/components/image_editor/data/image_item.dart @@ -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 loader = Completer(); + + ImageItem([dynamic image]) { + if (image != null) load(image); + } + + Future load(dynamic image) async { + loader = Completer(); + + 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, + }; + } +} diff --git a/lib/src/components/image_editor/data/layer.dart b/lib/src/components/image_editor/data/layer.dart new file mode 100755 index 0000000..73abb08 --- /dev/null +++ b/lib/src/components/image_editor/data/layer.dart @@ -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(), + }; + } +} diff --git a/lib/src/components/image_editor/image_editor.dart b/lib/src/components/image_editor/image_editor.dart new file mode 100644 index 0000000..a2dbe42 --- /dev/null +++ b/lib/src/components/image_editor/image_editor.dart @@ -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 layers = [], undoLayers = [], removedLayers = []; +Map _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 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 { + ImageItem currentImage = ImageItem(); + + ScreenshotController screenshotController = ScreenshotController(); + + @override + void dispose() { + layers.clear(); + super.dispose(); + } + + List 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 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: [ + 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 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 createState() => _ImageEditorDrawingState(); +} + +class _ImageEditorDrawingState extends State { + 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 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: [ + 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, + ), + ), + ), + ); + } +} diff --git a/lib/src/components/image_editor/layers/background_blur_layer.dart b/lib/src/components/image_editor/layers/background_blur_layer.dart new file mode 100755 index 0000000..56f975e --- /dev/null +++ b/lib/src/components/image_editor/layers/background_blur_layer.dart @@ -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 createState() => _BackgroundBlurLayerState(); +} + +class _BackgroundBlurLayerState extends State { + @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()), + ), + ), + ); + } +} diff --git a/lib/src/components/image_editor/layers/background_layer.dart b/lib/src/components/image_editor/layers/background_layer.dart new file mode 100755 index 0000000..8ec78e6 --- /dev/null +++ b/lib/src/components/image_editor/layers/background_layer.dart @@ -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 createState() => _BackgroundLayerState(); +} + +class _BackgroundLayerState extends State { + @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), + ); + } +} diff --git a/lib/src/components/image_editor/layers/emoji_layer.dart b/lib/src/components/image_editor/layers/emoji_layer.dart new file mode 100755 index 0000000..77a6b9d --- /dev/null +++ b/lib/src/components/image_editor/layers/emoji_layer.dart @@ -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 { + 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, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/components/image_editor/layers/image_layer.dart b/lib/src/components/image_editor/layers/image_layer.dart new file mode 100755 index 0000000..e7590ee --- /dev/null +++ b/lib/src/components/image_editor/layers/image_layer.dart @@ -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 { + 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), + ), + ), + ), + ); + } +} diff --git a/lib/src/components/image_editor/layers/text_layer.dart b/lib/src/components/image_editor/layers/text_layer.dart new file mode 100755 index 0000000..d5ca3e8 --- /dev/null +++ b/lib/src/components/image_editor/layers/text_layer.dart @@ -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 { + 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, + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/components/image_editor/layers_viewer.dart b/lib/src/components/image_editor/layers_viewer.dart new file mode 100644 index 0000000..e443a47 --- /dev/null +++ b/lib/src/components/image_editor/layers_viewer.dart @@ -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 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(), + ); + } +} diff --git a/lib/src/components/image_editor/loading_screen.dart b/lib/src/components/image_editor/loading_screen.dart new file mode 100644 index 0000000..098a76c --- /dev/null +++ b/lib/src/components/image_editor/loading_screen.dart @@ -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( + context: context, + builder: (BuildContext context) => LoadingScreenBody( + handler: handler, + ), + ); + + return handler; +} + +class LoadingScreen { + final Color? color; + final GlobalKey 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 createState() => _LoadingScreenBodyState(); +} + +class _LoadingScreenBodyState extends State { + @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: [ + 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, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/components/image_editor/modules/all_emojis.dart b/lib/src/components/image_editor/modules/all_emojis.dart new file mode 100755 index 0000000..f519f5a --- /dev/null +++ b/lib/src/components/image_editor/modules/all_emojis.dart @@ -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 { + @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(), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/src/components/image_editor/modules/color_pickers_slider.dart b/lib/src/components/image_editor/modules/color_pickers_slider.dart new file mode 100755 index 0000000..9cd695b --- /dev/null +++ b/lib/src/components/image_editor/modules/color_pickers_slider.dart @@ -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 { + @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)), + ) + ]), + ], + ), + ); + } +} diff --git a/lib/src/components/image_editor/modules/colors_picker.dart b/lib/src/components/image_editor/modules/colors_picker.dart new file mode 100755 index 0000000..fdf59cc --- /dev/null +++ b/lib/src/components/image_editor/modules/colors_picker.dart @@ -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 { + double percent = 0.0; + late List 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 createState() { + return _CircleColorPickerState(); + } +} + +class _CircleColorPickerState extends State { + static const List 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; + } +} diff --git a/lib/src/components/image_editor/modules/emoji_layer_overlay.dart b/lib/src/components/image_editor/modules/emoji_layer_overlay.dart new file mode 100755 index 0000000..1e2a70b --- /dev/null +++ b/lib/src/components/image_editor/modules/emoji_layer_overlay.dart @@ -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 { + 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), + ), + ), + ), + ]), + ], + ), + ); + } +} diff --git a/lib/src/components/image_editor/modules/image_layer_overlay.dart b/lib/src/components/image_editor/modules/image_layer_overlay.dart new file mode 100755 index 0000000..284094d --- /dev/null +++ b/lib/src/components/image_editor/modules/image_layer_overlay.dart @@ -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 { + 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), + ), + ), + ), + ]), + ], + ), + ); + } +} diff --git a/lib/src/components/image_editor/modules/text.dart b/lib/src/components/image_editor/modules/text.dart new file mode 100755 index 0000000..e2ccb1c --- /dev/null +++ b/lib/src/components/image_editor/modules/text.dart @@ -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 { + 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: [ + 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, + ), + ), + ]), + ), + ), + ), + ); + } +} diff --git a/lib/src/components/image_editor/modules/text_layer_overlay.dart b/lib/src/components/image_editor/modules/text_layer_overlay.dart new file mode 100755 index 0000000..6f25093 --- /dev/null +++ b/lib/src/components/image_editor/modules/text_layer_overlay.dart @@ -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 { + 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), + ), + ), + ), + ]), + ], + ), + ); + } +} diff --git a/lib/src/components/image_editor/options.dart b/lib/src/components/image_editor/options.dart new file mode 100644 index 0000000..b6327fe --- /dev/null +++ b/lib/src/components/image_editor/options.dart @@ -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 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(); +} diff --git a/lib/src/providers/api/api.dart b/lib/src/providers/api/api.dart index 23a3369..ac45179 100644 --- a/lib/src/providers/api/api.dart +++ b/lib/src/providers/api/api.dart @@ -155,19 +155,17 @@ Future encryptAndUploadMediaFile(Int64 target, Uint8List imageBytes) async { await uploadMediaFile(messageId, target, encryptBytes); } -Future sendImage(List userIds, String imagePath) async { +Future sendImage(List 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); } } diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 28858d8..937e598 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -30,13 +30,13 @@ FlutterSecureStorage getSecureStorage() { return FlutterSecureStorage(aOptions: _getAndroidOptions()); } -Future saveImageToGallery(path) async { +Future 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 getCompressedImage(File file) async { - var result = await FlutterImageCompress.compressWithFile( - file.absolute.path, +Future getCompressedImage(Uint8List imageBytes) async { + var result = await FlutterImageCompress.compressWithList( + imageBytes, quality: 90, ); return result; diff --git a/lib/src/views/camera_preview_view.dart b/lib/src/views/camera_preview_view.dart index 510c214..bc522bd 100644 --- a/lib/src/views/camera_preview_view.dart +++ b/lib/src/views/camera_preview_view.dart @@ -71,15 +71,15 @@ class _CameraPreviewViewState extends State { 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; diff --git a/lib/src/views/chat_list_view.dart b/lib/src/views/chat_list_view.dart index e76a5d3..af03d35 100644 --- a/lib/src/views/chat_list_view.dart +++ b/lib/src/views/chat_list_view.dart @@ -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'; diff --git a/lib/src/views/share_image_editor_view.dart b/lib/src/views/share_image_editor_view.dart index ece06e0..93dc116 100644 --- a/lib/src/views/share_image_editor_view.dart +++ b/lib/src/views/share_image_editor_view.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 createState() => _ShareImageEditorView(); } class _ShareImageEditorView extends State { - bool _isImageLoaded = false; bool _imageSaved = false; @override void initState() { super.initState(); - imageIsLoaded(); - } - - Future imageIsLoaded() async { - Future.delayed(Duration(milliseconds: 600), () { - setState(() { - _isImageLoaded = true; - }); - }); } @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: _isImageLoaded + backgroundColor: true ? Theme.of(context).colorScheme.surface : Colors.white.withAlpha(0), body: Stack( @@ -42,99 +34,78 @@ class _ShareImageEditorView extends State { children: [ Positioned( top: 0, - // bottom: 0, + bottom: 70, left: 0, right: 0, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 50), + padding: const EdgeInsets.symmetric(vertical: 0), child: ClipRRect( borderRadius: BorderRadius.circular(22), - child: Image.file( - File(widget.image), - fit: BoxFit.contain, + // child: Container(), + child: ImageEditor( + image: widget.imageBytes, ), ), ), ), - _isImageLoaded - ? Positioned( - left: 10, - top: 60, - child: Row( - // mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.close, size: 30), - color: Colors.white, - onPressed: () async { - Navigator.pop(context); - }, - ), - ], + Positioned( + bottom: 70, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + OutlinedButton.icon( + icon: _imageSaved + ? Icon(Icons.check) + : FaIcon(FontAwesomeIcons.floppyDisk), + style: OutlinedButton.styleFrom( + iconColor: _imageSaved + ? Theme.of(context).colorScheme.outline + : Theme.of(context).colorScheme.primary, + foregroundColor: _imageSaved + ? Theme.of(context).colorScheme.outline + : Theme.of(context).colorScheme.primary, ), - ) - : Container(), - _isImageLoaded - ? Positioned( - bottom: 70, - left: 0, - right: 0, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - OutlinedButton.icon( - icon: _imageSaved - ? Icon(Icons.check) - : FaIcon(FontAwesomeIcons.floppyDisk), - style: OutlinedButton.styleFrom( - iconColor: _imageSaved - ? Theme.of(context).colorScheme.outline - : Theme.of(context).colorScheme.primary, - foregroundColor: _imageSaved - ? Theme.of(context).colorScheme.outline - : Theme.of(context).colorScheme.primary, - ), - onPressed: () async { - if (_imageSaved) return; - final res = await saveImageToGallery(widget.image); - if (res == null) { - setState(() { - _imageSaved = true; - }); - } - }, - label: Text(_imageSaved - ? AppLocalizations.of(context)! - .shareImagedEditorSavedImage - : AppLocalizations.of(context)! - .shareImagedEditorSaveImage), - ), - const SizedBox(width: 20), - FilledButton.icon( - icon: FaIcon(FontAwesomeIcons.solidPaperPlane), - onPressed: () async { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - ShareImageView(image: widget.image)), - ); - }, - style: ButtonStyle( - padding: WidgetStateProperty.all( - EdgeInsets.symmetric(vertical: 10, horizontal: 30), - ), - ), - label: Text( - AppLocalizations.of(context)! - .shareImagedEditorShareWith, - style: TextStyle(fontSize: 17), - ), - ), - ], + onPressed: () async { + if (_imageSaved) return; + final res = await saveImageToGallery(widget.imageBytes); + if (res == null) { + setState(() { + _imageSaved = true; + }); + } + }, + label: Text(_imageSaved + ? AppLocalizations.of(context)! + .shareImagedEditorSavedImage + : AppLocalizations.of(context)! + .shareImagedEditorSaveImage), + ), + const SizedBox(width: 20), + FilledButton.icon( + icon: FaIcon(FontAwesomeIcons.solidPaperPlane), + onPressed: () async { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ShareImageView(imageBytes: widget.imageBytes)), + ); + }, + style: ButtonStyle( + padding: WidgetStateProperty.all( + EdgeInsets.symmetric(vertical: 10, horizontal: 30), + ), ), - ) - : Container(), + label: Text( + AppLocalizations.of(context)!.shareImagedEditorShareWith, + style: TextStyle(fontSize: 17), + ), + ), + ], + ), + ), ], ), ); diff --git a/lib/src/views/share_image_view.dart b/lib/src/views/share_image_view.dart index ee16c74..fc639d6 100644 --- a/lib/src/views/share_image_view.dart +++ b/lib/src/views/share_image_view.dart @@ -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 createState() => _ShareImageView(); @@ -26,7 +27,6 @@ class _ShareImageView extends State { List _bestFriends = []; int maxTotalMediaCounter = 0; final HashSet _selectedUserIds = HashSet(); - String _lastSearchQuery = ''; final TextEditingController searchUserName = TextEditingController(); @override @@ -90,7 +90,6 @@ class _ShareImageView extends State { user.displayName.toLowerCase().contains(query.toLowerCase())) .toList(); _updateUsers(usersFiltered); - _lastSearchQuery = query; } @override @@ -146,7 +145,7 @@ class _ShareImageView extends State { 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); diff --git a/pubspec.lock b/pubspec.lock index 321186d..55d1a86 100644 --- a/pubspec.lock +++ b/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: diff --git a/pubspec.yaml b/pubspec.yaml index 286e7b1..3c74961 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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