diff --git a/.flutter-plugins b/.flutter-plugins
new file mode 100644
index 0000000..2432fc2
--- /dev/null
+++ b/.flutter-plugins
@@ -0,0 +1,21 @@
+# This is a generated file; do not edit or check into version control.
+ffmpeg_kit_flutter=/Users/amirkhan/.pub-cache/hosted/pub.dev/ffmpeg_kit_flutter-6.0.3/
+file_picker=/Users/amirkhan/.pub-cache/hosted/pub.dev/file_picker-8.0.1/
+flutter_keyboard_visibility=/Users/amirkhan/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility-6.0.0/
+flutter_keyboard_visibility_linux=/Users/amirkhan/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility_linux-1.0.0/
+flutter_keyboard_visibility_macos=/Users/amirkhan/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility_macos-1.0.0/
+flutter_keyboard_visibility_web=/Users/amirkhan/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility_web-2.0.0/
+flutter_keyboard_visibility_windows=/Users/amirkhan/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility_windows-1.0.0/
+flutter_plugin_android_lifecycle=/Users/amirkhan/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.19/
+image_cropper=/Users/amirkhan/.pub-cache/hosted/pub.dev/image_cropper-5.0.1/
+image_cropper_for_web=/Users/amirkhan/.pub-cache/hosted/pub.dev/image_cropper_for_web-3.0.0/
+path_provider=/Users/amirkhan/.pub-cache/hosted/pub.dev/path_provider-2.1.3/
+path_provider_android=/Users/amirkhan/.pub-cache/hosted/pub.dev/path_provider_android-2.2.4/
+path_provider_foundation=/Users/amirkhan/.pub-cache/hosted/pub.dev/path_provider_foundation-2.3.2/
+path_provider_linux=/Users/amirkhan/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/
+path_provider_windows=/Users/amirkhan/.pub-cache/hosted/pub.dev/path_provider_windows-2.2.1/
+video_player=/Users/amirkhan/.pub-cache/hosted/pub.dev/video_player-2.8.6/
+video_player_android=/Users/amirkhan/.pub-cache/hosted/pub.dev/video_player_android-2.4.14/
+video_player_avfoundation=/Users/amirkhan/.pub-cache/hosted/pub.dev/video_player_avfoundation-2.5.7/
+video_player_web=/Users/amirkhan/.pub-cache/hosted/pub.dev/video_player_web-2.3.0/
+video_thumbnail=/Users/amirkhan/.pub-cache/hosted/pub.dev/video_thumbnail-0.5.3/
diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies
new file mode 100644
index 0000000..c69d866
--- /dev/null
+++ b/.flutter-plugins-dependencies
@@ -0,0 +1 @@
+{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"ffmpeg_kit_flutter","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/ffmpeg_kit_flutter-6.0.3/","native_build":true,"dependencies":[]},{"name":"file_picker","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/file_picker-8.0.1/","native_build":true,"dependencies":[]},{"name":"flutter_keyboard_visibility","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility-6.0.0/","native_build":true,"dependencies":[]},{"name":"image_cropper","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/image_cropper-5.0.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/path_provider_foundation-2.3.2/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"video_player_avfoundation","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/video_player_avfoundation-2.5.7/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"video_thumbnail","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/video_thumbnail-0.5.3/","native_build":true,"dependencies":[]}],"android":[{"name":"ffmpeg_kit_flutter","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/ffmpeg_kit_flutter-6.0.3/","native_build":true,"dependencies":[]},{"name":"file_picker","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/file_picker-8.0.1/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"]},{"name":"flutter_keyboard_visibility","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility-6.0.0/","native_build":true,"dependencies":[]},{"name":"flutter_plugin_android_lifecycle","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.19/","native_build":true,"dependencies":[]},{"name":"image_cropper","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/image_cropper-5.0.1/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/path_provider_android-2.2.4/","native_build":true,"dependencies":[]},{"name":"video_player_android","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/video_player_android-2.4.14/","native_build":true,"dependencies":[]},{"name":"video_thumbnail","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/video_thumbnail-0.5.3/","native_build":true,"dependencies":[]}],"macos":[{"name":"ffmpeg_kit_flutter","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/ffmpeg_kit_flutter-6.0.3/","native_build":true,"dependencies":[]},{"name":"flutter_keyboard_visibility_macos","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility_macos-1.0.0/","native_build":false,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/path_provider_foundation-2.3.2/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"video_player_avfoundation","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/video_player_avfoundation-2.5.7/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"flutter_keyboard_visibility_linux","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility_linux-1.0.0/","native_build":false,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"flutter_keyboard_visibility_windows","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility_windows-1.0.0/","native_build":false,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/path_provider_windows-2.2.1/","native_build":false,"dependencies":[]}],"web":[{"name":"file_picker","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/file_picker-8.0.1/","dependencies":[]},{"name":"flutter_keyboard_visibility_web","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility_web-2.0.0/","dependencies":[]},{"name":"image_cropper_for_web","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/image_cropper_for_web-3.0.0/","dependencies":[]},{"name":"video_player_web","path":"/Users/amirkhan/.pub-cache/hosted/pub.dev/video_player_web-2.3.0/","dependencies":[]}]},"dependencyGraph":[{"name":"ffmpeg_kit_flutter","dependencies":[]},{"name":"file_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"flutter_keyboard_visibility","dependencies":["flutter_keyboard_visibility_linux","flutter_keyboard_visibility_macos","flutter_keyboard_visibility_web","flutter_keyboard_visibility_windows"]},{"name":"flutter_keyboard_visibility_linux","dependencies":[]},{"name":"flutter_keyboard_visibility_macos","dependencies":[]},{"name":"flutter_keyboard_visibility_web","dependencies":[]},{"name":"flutter_keyboard_visibility_windows","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"image_cropper","dependencies":["image_cropper_for_web"]},{"name":"image_cropper_for_web","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"video_player","dependencies":["video_player_android","video_player_avfoundation","video_player_web"]},{"name":"video_player_android","dependencies":[]},{"name":"video_player_avfoundation","dependencies":[]},{"name":"video_player_web","dependencies":[]},{"name":"video_thumbnail","dependencies":[]}],"date_created":"2024-04-18 14:45:16.049397","version":"3.19.0"}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ac5aa98
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,29 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
+/pubspec.lock
+**/doc/api/
+.dart_tool/
+build/
diff --git a/.metadata b/.metadata
new file mode 100644
index 0000000..48326e7
--- /dev/null
+++ b/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "bae5e49bc2a867403c43b2aae2de8f8c33b037e4"
+ channel: "stable"
+
+project_type: package
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..de10f4b
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.0.1
+
+* Initial release of *Flutter Story Editor*
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7a3cc5e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Muhammad Adnan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6f48c02
--- /dev/null
+++ b/README.md
@@ -0,0 +1,164 @@
+# flutter_story_editor [![Pub](https://img.shields.io/pub/v/flutter_story_editor.svg)](https://pub.dev/packages/flutter_story_editor)
+
+This package is created using style of the WhatsApp story image/video editor, with which you can edit images and videos both together. You can add texts, stickers, freehand finger drawing, apply filter, and undo. The edited images will be returned in a **onSave** call back as **List of Files**. You can then upload it to some storage or save it locally to your gallery.
+
+>> Video editing for now only support trimming. In future more video editing features will be added.
+
+## Features
+
+✅ You can edit Images, and videos both together.
+
+✅ Draggable fancy text with (custom colors, font families, and resize)
+
+✅ Draggable stickers & emojis
+
+✅ Apply filters to images
+
+✅ Freehand drawing over images
+
+✅ Trimming video frames
+
+## Future features
+
+🚀 Drawing painting over video frames (requires platform specific work)
+
+🚀 More image and video editing functionality like (WhatsApp & Instagram) stories
+
+🚀 The UI is currently like WhatsApp, but I think we should go with something unique for flutter (your contribution & ideas will be very invaluable)
+
+🚀 improve and enhance performance and existing features.
+
+## Package Demo
+
+
+
+
+
+
+
+
+
+## Installation
+
+Add flutter_story_editor: latest_version to your **pubspec.yaml** and then import it.
+
+```dartimport 'package:stories_editor/stories_editor.dart';```
+
+### Android
+add the following code to your `AndroidMAnifest.xml` file
+ ```xml
+
+ ```
+### iOS
+add the following code to your `info.plist` file
+
+```xml
+NSCameraUsageDescription
+Used to demonstrate image picker plugin
+NSMicrophoneUsageDescription
+Used to capture audio for image picker plugin
+NSPhotoLibraryUsageDescription
+Used to demonstrate image picker plugin
+```
+## How to use
+```dart
+ // Inialize controllers within the state
+ FlutterStoryEditorController controller = FlutterStoryEditorController();
+ final TextEditingController _captionController = TextEditingController();
+
+ // TODO: create a method to pick files (videos and images) either separate or together.
+
+
+ // Select files
+ selectMedia().then((value) {
+ if (_selectedMedia != null && _selectedMedia!.isNotEmpty) {
+ showModalBottomSheet(
+ isScrollControlled: true,
+ isDismissible: false,
+ enableDrag: false,
+ context: context,
+ builder: (context) {
+
+ return FlutterStoryEditor(
+ controller: controller,
+ captionController: _captionController,
+ selectedFiles: _selectedMedia,
+ onSaveClickListener: (files) {
+ // Here you go with your edited files.
+ }
+ );
+ },
+ );
+ }
+ },
+ );
+ }, icon: const Icon(Icons.upload, size: 50,)),
+ ),
+```
+
+For more information : visit example project inside `example/example.dart`.
+
+## Screenshots
+
+Initial view & Multiple images selected
+
+
+
+
+
+
+
+
+Images & videos together & Apply filters
+
+
+
+
+
+
+
+
+Crop, scale and rotate & Add draggable stickers
+
+
+
+
+
+
+
+
+Add emojis & Add draggable fancy text
+
+
+
+
+
+
+
+
+
+Draw freehand painting over images
+
+
+
+
+
+
+
+
+
+## Must read
+
+The initial release of `flutter_story_editor` may have small bugs, and issues. If you found some, and you're willing to contribute feel free to create issue and rasie a PR. Make sure you inform me through my [LinkedIn DM](https://www.linkedin.com/in/muhammad-adnan-23bb8821b/) for the issues you create in both cases either or not if you want to contribute.
+
+This package will be improved more along the time, your contribution will be very invaluable.
+
+
+## Created & Maintained By
+
+[@MuhammadAdnan](https://github.com/AdnanKhan45), LinkedIn : [@MuhammadAdnan](https://www.linkedin.com/in/muhammad-adnan-23bb8821b/) , Instagram : [@MuhammadAdnan](https://www.instagram.com/dev.adnankhan/).
+
+YouTube : [@eTechViral](https://www.youtube.com/c/eTechViral)
\ No newline at end of file
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000..a5744c1
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1,4 @@
+include: package:flutter_lints/flutter.yaml
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/assets/emojies/e1511.png b/assets/emojies/e1511.png
new file mode 100644
index 0000000..9889fab
Binary files /dev/null and b/assets/emojies/e1511.png differ
diff --git a/assets/emojies/e1512.png b/assets/emojies/e1512.png
new file mode 100644
index 0000000..fa20e78
Binary files /dev/null and b/assets/emojies/e1512.png differ
diff --git a/assets/emojies/e1513.png b/assets/emojies/e1513.png
new file mode 100644
index 0000000..206c15d
Binary files /dev/null and b/assets/emojies/e1513.png differ
diff --git a/assets/emojies/e1514.png b/assets/emojies/e1514.png
new file mode 100644
index 0000000..025e972
Binary files /dev/null and b/assets/emojies/e1514.png differ
diff --git a/assets/emojies/e1515.png b/assets/emojies/e1515.png
new file mode 100644
index 0000000..9416af9
Binary files /dev/null and b/assets/emojies/e1515.png differ
diff --git a/assets/emojies/e1516.png b/assets/emojies/e1516.png
new file mode 100644
index 0000000..73fd322
Binary files /dev/null and b/assets/emojies/e1516.png differ
diff --git a/assets/emojies/e1517.png b/assets/emojies/e1517.png
new file mode 100644
index 0000000..54de1ff
Binary files /dev/null and b/assets/emojies/e1517.png differ
diff --git a/assets/emojies/e1518.png b/assets/emojies/e1518.png
new file mode 100644
index 0000000..1fa49a3
Binary files /dev/null and b/assets/emojies/e1518.png differ
diff --git a/assets/emojies/e1519.png b/assets/emojies/e1519.png
new file mode 100644
index 0000000..ea4b401
Binary files /dev/null and b/assets/emojies/e1519.png differ
diff --git a/assets/emojies/e1520.png b/assets/emojies/e1520.png
new file mode 100644
index 0000000..f4a60b3
Binary files /dev/null and b/assets/emojies/e1520.png differ
diff --git a/assets/emojies/e1521.png b/assets/emojies/e1521.png
new file mode 100644
index 0000000..cd78e00
Binary files /dev/null and b/assets/emojies/e1521.png differ
diff --git a/assets/emojies/e1522.png b/assets/emojies/e1522.png
new file mode 100644
index 0000000..1c78d78
Binary files /dev/null and b/assets/emojies/e1522.png differ
diff --git a/assets/emojies/e1523.png b/assets/emojies/e1523.png
new file mode 100644
index 0000000..8759d5f
Binary files /dev/null and b/assets/emojies/e1523.png differ
diff --git a/assets/emojies/e1524.png b/assets/emojies/e1524.png
new file mode 100644
index 0000000..451a166
Binary files /dev/null and b/assets/emojies/e1524.png differ
diff --git a/assets/emojies/e1525.png b/assets/emojies/e1525.png
new file mode 100644
index 0000000..60cac0a
Binary files /dev/null and b/assets/emojies/e1525.png differ
diff --git a/assets/emojies/e1526.png b/assets/emojies/e1526.png
new file mode 100644
index 0000000..c340f61
Binary files /dev/null and b/assets/emojies/e1526.png differ
diff --git a/assets/emojies/e1527.png b/assets/emojies/e1527.png
new file mode 100644
index 0000000..e29e8e8
Binary files /dev/null and b/assets/emojies/e1527.png differ
diff --git a/assets/emojies/e1528.png b/assets/emojies/e1528.png
new file mode 100644
index 0000000..9d216fb
Binary files /dev/null and b/assets/emojies/e1528.png differ
diff --git a/assets/emojies/e1529.png b/assets/emojies/e1529.png
new file mode 100644
index 0000000..c2974c9
Binary files /dev/null and b/assets/emojies/e1529.png differ
diff --git a/assets/emojies/e1530.png b/assets/emojies/e1530.png
new file mode 100644
index 0000000..e628118
Binary files /dev/null and b/assets/emojies/e1530.png differ
diff --git a/assets/emojies/e1531.png b/assets/emojies/e1531.png
new file mode 100644
index 0000000..3c4234b
Binary files /dev/null and b/assets/emojies/e1531.png differ
diff --git a/assets/emojies/e1532.png b/assets/emojies/e1532.png
new file mode 100644
index 0000000..d33d44a
Binary files /dev/null and b/assets/emojies/e1532.png differ
diff --git a/assets/emojies/e1533.png b/assets/emojies/e1533.png
new file mode 100644
index 0000000..1aa25cf
Binary files /dev/null and b/assets/emojies/e1533.png differ
diff --git a/assets/emojies/e1534.png b/assets/emojies/e1534.png
new file mode 100644
index 0000000..7db7c7e
Binary files /dev/null and b/assets/emojies/e1534.png differ
diff --git a/assets/emojies/e1535.png b/assets/emojies/e1535.png
new file mode 100644
index 0000000..3156d7c
Binary files /dev/null and b/assets/emojies/e1535.png differ
diff --git a/assets/emojies/e1536.png b/assets/emojies/e1536.png
new file mode 100644
index 0000000..3f841c8
Binary files /dev/null and b/assets/emojies/e1536.png differ
diff --git a/assets/emojies/e1537.png b/assets/emojies/e1537.png
new file mode 100644
index 0000000..9a2c6c0
Binary files /dev/null and b/assets/emojies/e1537.png differ
diff --git a/assets/emojies/e1538.png b/assets/emojies/e1538.png
new file mode 100644
index 0000000..cb5b381
Binary files /dev/null and b/assets/emojies/e1538.png differ
diff --git a/assets/emojies/e1539.png b/assets/emojies/e1539.png
new file mode 100644
index 0000000..8f89dc1
Binary files /dev/null and b/assets/emojies/e1539.png differ
diff --git a/assets/emojies/e1540.png b/assets/emojies/e1540.png
new file mode 100644
index 0000000..06e7202
Binary files /dev/null and b/assets/emojies/e1540.png differ
diff --git a/assets/emojies/e1541.png b/assets/emojies/e1541.png
new file mode 100644
index 0000000..a96153f
Binary files /dev/null and b/assets/emojies/e1541.png differ
diff --git a/assets/emojies/e1542.png b/assets/emojies/e1542.png
new file mode 100644
index 0000000..62ba755
Binary files /dev/null and b/assets/emojies/e1542.png differ
diff --git a/assets/emojies/e1543.png b/assets/emojies/e1543.png
new file mode 100644
index 0000000..1739b5c
Binary files /dev/null and b/assets/emojies/e1543.png differ
diff --git a/assets/emojies/e1544.png b/assets/emojies/e1544.png
new file mode 100644
index 0000000..1cdf0b1
Binary files /dev/null and b/assets/emojies/e1544.png differ
diff --git a/assets/emojies/e1545.png b/assets/emojies/e1545.png
new file mode 100644
index 0000000..493f4cc
Binary files /dev/null and b/assets/emojies/e1545.png differ
diff --git a/assets/emojies/e1546.png b/assets/emojies/e1546.png
new file mode 100644
index 0000000..a9b701e
Binary files /dev/null and b/assets/emojies/e1546.png differ
diff --git a/assets/emojies/e1547.png b/assets/emojies/e1547.png
new file mode 100644
index 0000000..8de2702
Binary files /dev/null and b/assets/emojies/e1547.png differ
diff --git a/assets/emojies/e1548.png b/assets/emojies/e1548.png
new file mode 100644
index 0000000..eb442e4
Binary files /dev/null and b/assets/emojies/e1548.png differ
diff --git a/assets/emojies/e1549.png b/assets/emojies/e1549.png
new file mode 100644
index 0000000..f06edc7
Binary files /dev/null and b/assets/emojies/e1549.png differ
diff --git a/assets/emojies/e1550.png b/assets/emojies/e1550.png
new file mode 100644
index 0000000..549a994
Binary files /dev/null and b/assets/emojies/e1550.png differ
diff --git a/assets/emojies/e1551.png b/assets/emojies/e1551.png
new file mode 100644
index 0000000..8c74ed1
Binary files /dev/null and b/assets/emojies/e1551.png differ
diff --git a/assets/emojies/e1552.png b/assets/emojies/e1552.png
new file mode 100644
index 0000000..7613c78
Binary files /dev/null and b/assets/emojies/e1552.png differ
diff --git a/assets/emojies/e1553.png b/assets/emojies/e1553.png
new file mode 100644
index 0000000..590f411
Binary files /dev/null and b/assets/emojies/e1553.png differ
diff --git a/assets/emojies/e1554.png b/assets/emojies/e1554.png
new file mode 100644
index 0000000..e519e27
Binary files /dev/null and b/assets/emojies/e1554.png differ
diff --git a/assets/emojies/e1555.png b/assets/emojies/e1555.png
new file mode 100644
index 0000000..4985b8e
Binary files /dev/null and b/assets/emojies/e1555.png differ
diff --git a/assets/emojies/e1556.png b/assets/emojies/e1556.png
new file mode 100644
index 0000000..02e551b
Binary files /dev/null and b/assets/emojies/e1556.png differ
diff --git a/assets/emojies/e1557.png b/assets/emojies/e1557.png
new file mode 100644
index 0000000..7024868
Binary files /dev/null and b/assets/emojies/e1557.png differ
diff --git a/assets/emojies/e1558.png b/assets/emojies/e1558.png
new file mode 100644
index 0000000..58f1155
Binary files /dev/null and b/assets/emojies/e1558.png differ
diff --git a/assets/emojies/e1559.png b/assets/emojies/e1559.png
new file mode 100644
index 0000000..4594632
Binary files /dev/null and b/assets/emojies/e1559.png differ
diff --git a/assets/emojies/e1560.png b/assets/emojies/e1560.png
new file mode 100644
index 0000000..f007a7f
Binary files /dev/null and b/assets/emojies/e1560.png differ
diff --git a/assets/emojies/e1561.png b/assets/emojies/e1561.png
new file mode 100644
index 0000000..747835e
Binary files /dev/null and b/assets/emojies/e1561.png differ
diff --git a/assets/emojies/e1562.png b/assets/emojies/e1562.png
new file mode 100644
index 0000000..281ca4a
Binary files /dev/null and b/assets/emojies/e1562.png differ
diff --git a/assets/emojies/e1563.png b/assets/emojies/e1563.png
new file mode 100644
index 0000000..9b43da0
Binary files /dev/null and b/assets/emojies/e1563.png differ
diff --git a/assets/emojies/e1564.png b/assets/emojies/e1564.png
new file mode 100644
index 0000000..b84091c
Binary files /dev/null and b/assets/emojies/e1564.png differ
diff --git a/assets/emojies/e1565.png b/assets/emojies/e1565.png
new file mode 100644
index 0000000..1cf3cae
Binary files /dev/null and b/assets/emojies/e1565.png differ
diff --git a/assets/emojies/e1566.png b/assets/emojies/e1566.png
new file mode 100644
index 0000000..d4400c6
Binary files /dev/null and b/assets/emojies/e1566.png differ
diff --git a/assets/emojies/e1567.png b/assets/emojies/e1567.png
new file mode 100644
index 0000000..50b890a
Binary files /dev/null and b/assets/emojies/e1567.png differ
diff --git a/assets/emojies/e1568.png b/assets/emojies/e1568.png
new file mode 100644
index 0000000..94ff4ff
Binary files /dev/null and b/assets/emojies/e1568.png differ
diff --git a/assets/emojies/e1569.png b/assets/emojies/e1569.png
new file mode 100644
index 0000000..249eb36
Binary files /dev/null and b/assets/emojies/e1569.png differ
diff --git a/assets/emojies/e1570.png b/assets/emojies/e1570.png
new file mode 100644
index 0000000..489b31b
Binary files /dev/null and b/assets/emojies/e1570.png differ
diff --git a/assets/emojies/e1571.png b/assets/emojies/e1571.png
new file mode 100644
index 0000000..52dee71
Binary files /dev/null and b/assets/emojies/e1571.png differ
diff --git a/assets/emojies/e1572.png b/assets/emojies/e1572.png
new file mode 100644
index 0000000..324b135
Binary files /dev/null and b/assets/emojies/e1572.png differ
diff --git a/assets/emojies/e1573.png b/assets/emojies/e1573.png
new file mode 100644
index 0000000..9b309d5
Binary files /dev/null and b/assets/emojies/e1573.png differ
diff --git a/assets/emojies/e1574.png b/assets/emojies/e1574.png
new file mode 100644
index 0000000..2b513a9
Binary files /dev/null and b/assets/emojies/e1574.png differ
diff --git a/assets/emojies/e1575.png b/assets/emojies/e1575.png
new file mode 100644
index 0000000..9ebcd26
Binary files /dev/null and b/assets/emojies/e1575.png differ
diff --git a/assets/emojies/e1576.png b/assets/emojies/e1576.png
new file mode 100644
index 0000000..da3dd03
Binary files /dev/null and b/assets/emojies/e1576.png differ
diff --git a/assets/emojies/e1577.png b/assets/emojies/e1577.png
new file mode 100644
index 0000000..7f91ad0
Binary files /dev/null and b/assets/emojies/e1577.png differ
diff --git a/assets/emojies/e1578.png b/assets/emojies/e1578.png
new file mode 100644
index 0000000..b6678b0
Binary files /dev/null and b/assets/emojies/e1578.png differ
diff --git a/assets/emojies/e1579.png b/assets/emojies/e1579.png
new file mode 100644
index 0000000..9f19c91
Binary files /dev/null and b/assets/emojies/e1579.png differ
diff --git a/assets/emojies/e1580.png b/assets/emojies/e1580.png
new file mode 100644
index 0000000..2e5a255
Binary files /dev/null and b/assets/emojies/e1580.png differ
diff --git a/assets/emojies/e1581.png b/assets/emojies/e1581.png
new file mode 100644
index 0000000..c935748
Binary files /dev/null and b/assets/emojies/e1581.png differ
diff --git a/assets/emojies/e1582.png b/assets/emojies/e1582.png
new file mode 100644
index 0000000..e98548c
Binary files /dev/null and b/assets/emojies/e1582.png differ
diff --git a/assets/emojies/e1583.png b/assets/emojies/e1583.png
new file mode 100644
index 0000000..60e37ad
Binary files /dev/null and b/assets/emojies/e1583.png differ
diff --git a/assets/emojies/e1584.png b/assets/emojies/e1584.png
new file mode 100644
index 0000000..e23e372
Binary files /dev/null and b/assets/emojies/e1584.png differ
diff --git a/assets/emojies/e1585.png b/assets/emojies/e1585.png
new file mode 100644
index 0000000..4d31006
Binary files /dev/null and b/assets/emojies/e1585.png differ
diff --git a/assets/emojies/e1586.png b/assets/emojies/e1586.png
new file mode 100644
index 0000000..85fe6bf
Binary files /dev/null and b/assets/emojies/e1586.png differ
diff --git a/assets/emojies/e1587.png b/assets/emojies/e1587.png
new file mode 100644
index 0000000..b57be97
Binary files /dev/null and b/assets/emojies/e1587.png differ
diff --git a/assets/emojies/e1588.png b/assets/emojies/e1588.png
new file mode 100644
index 0000000..0d523c0
Binary files /dev/null and b/assets/emojies/e1588.png differ
diff --git a/assets/emojies/e1589.png b/assets/emojies/e1589.png
new file mode 100644
index 0000000..9d43234
Binary files /dev/null and b/assets/emojies/e1589.png differ
diff --git a/assets/emojies/e1590.png b/assets/emojies/e1590.png
new file mode 100644
index 0000000..06cf344
Binary files /dev/null and b/assets/emojies/e1590.png differ
diff --git a/assets/emojies/e1591.png b/assets/emojies/e1591.png
new file mode 100644
index 0000000..ee52a0c
Binary files /dev/null and b/assets/emojies/e1591.png differ
diff --git a/assets/emojies/e1592.png b/assets/emojies/e1592.png
new file mode 100644
index 0000000..b2f978b
Binary files /dev/null and b/assets/emojies/e1592.png differ
diff --git a/assets/emojies/e1593.png b/assets/emojies/e1593.png
new file mode 100644
index 0000000..9bd2646
Binary files /dev/null and b/assets/emojies/e1593.png differ
diff --git a/assets/emojies/e1594.png b/assets/emojies/e1594.png
new file mode 100644
index 0000000..5264ecf
Binary files /dev/null and b/assets/emojies/e1594.png differ
diff --git a/assets/emojies/e1595.png b/assets/emojies/e1595.png
new file mode 100644
index 0000000..ee4f96e
Binary files /dev/null and b/assets/emojies/e1595.png differ
diff --git a/assets/emojies/e1596.png b/assets/emojies/e1596.png
new file mode 100644
index 0000000..74711a5
Binary files /dev/null and b/assets/emojies/e1596.png differ
diff --git a/assets/emojies/e1597.png b/assets/emojies/e1597.png
new file mode 100644
index 0000000..460afc9
Binary files /dev/null and b/assets/emojies/e1597.png differ
diff --git a/assets/emojies/e1598.png b/assets/emojies/e1598.png
new file mode 100644
index 0000000..330dc14
Binary files /dev/null and b/assets/emojies/e1598.png differ
diff --git a/assets/emojies/e1599.png b/assets/emojies/e1599.png
new file mode 100644
index 0000000..d0c80bc
Binary files /dev/null and b/assets/emojies/e1599.png differ
diff --git a/assets/emojies/e1600.png b/assets/emojies/e1600.png
new file mode 100644
index 0000000..a0a4c91
Binary files /dev/null and b/assets/emojies/e1600.png differ
diff --git a/assets/emojies/e1601.png b/assets/emojies/e1601.png
new file mode 100644
index 0000000..20df83d
Binary files /dev/null and b/assets/emojies/e1601.png differ
diff --git a/assets/emojies/e1602.png b/assets/emojies/e1602.png
new file mode 100644
index 0000000..937549f
Binary files /dev/null and b/assets/emojies/e1602.png differ
diff --git a/assets/emojies/e1603.png b/assets/emojies/e1603.png
new file mode 100644
index 0000000..0c78695
Binary files /dev/null and b/assets/emojies/e1603.png differ
diff --git a/assets/emojies/e1604.png b/assets/emojies/e1604.png
new file mode 100644
index 0000000..1d9646f
Binary files /dev/null and b/assets/emojies/e1604.png differ
diff --git a/assets/emojies/e1605.png b/assets/emojies/e1605.png
new file mode 100644
index 0000000..e25a33f
Binary files /dev/null and b/assets/emojies/e1605.png differ
diff --git a/assets/emojies/e1606.png b/assets/emojies/e1606.png
new file mode 100644
index 0000000..53d1f1e
Binary files /dev/null and b/assets/emojies/e1606.png differ
diff --git a/assets/emojies/e1607.png b/assets/emojies/e1607.png
new file mode 100644
index 0000000..0119a5a
Binary files /dev/null and b/assets/emojies/e1607.png differ
diff --git a/assets/emojies/e1608.png b/assets/emojies/e1608.png
new file mode 100644
index 0000000..8aa9a8c
Binary files /dev/null and b/assets/emojies/e1608.png differ
diff --git a/assets/emojies/e1609.png b/assets/emojies/e1609.png
new file mode 100644
index 0000000..35ad789
Binary files /dev/null and b/assets/emojies/e1609.png differ
diff --git a/assets/emojies/e1610.png b/assets/emojies/e1610.png
new file mode 100644
index 0000000..7adfdad
Binary files /dev/null and b/assets/emojies/e1610.png differ
diff --git a/assets/emojies/e1611.png b/assets/emojies/e1611.png
new file mode 100644
index 0000000..f500519
Binary files /dev/null and b/assets/emojies/e1611.png differ
diff --git a/assets/emojies/e1612.png b/assets/emojies/e1612.png
new file mode 100644
index 0000000..0c9822d
Binary files /dev/null and b/assets/emojies/e1612.png differ
diff --git a/assets/emojies/e1613.png b/assets/emojies/e1613.png
new file mode 100644
index 0000000..68a62ab
Binary files /dev/null and b/assets/emojies/e1613.png differ
diff --git a/assets/emojies/e1614.png b/assets/emojies/e1614.png
new file mode 100644
index 0000000..635dc49
Binary files /dev/null and b/assets/emojies/e1614.png differ
diff --git a/assets/emojies/e1615.png b/assets/emojies/e1615.png
new file mode 100644
index 0000000..e44a846
Binary files /dev/null and b/assets/emojies/e1615.png differ
diff --git a/assets/emojies/e1616.png b/assets/emojies/e1616.png
new file mode 100644
index 0000000..c183b92
Binary files /dev/null and b/assets/emojies/e1616.png differ
diff --git a/assets/emojies/e1617.png b/assets/emojies/e1617.png
new file mode 100644
index 0000000..56c8476
Binary files /dev/null and b/assets/emojies/e1617.png differ
diff --git a/assets/emojies/e1618.png b/assets/emojies/e1618.png
new file mode 100644
index 0000000..cdb2052
Binary files /dev/null and b/assets/emojies/e1618.png differ
diff --git a/assets/emojies/e1619.png b/assets/emojies/e1619.png
new file mode 100644
index 0000000..838af09
Binary files /dev/null and b/assets/emojies/e1619.png differ
diff --git a/assets/emojies/e1620.png b/assets/emojies/e1620.png
new file mode 100644
index 0000000..f7413d7
Binary files /dev/null and b/assets/emojies/e1620.png differ
diff --git a/assets/emojies/e1621.png b/assets/emojies/e1621.png
new file mode 100644
index 0000000..09d14e7
Binary files /dev/null and b/assets/emojies/e1621.png differ
diff --git a/assets/emojies/e1622.png b/assets/emojies/e1622.png
new file mode 100644
index 0000000..c2815b1
Binary files /dev/null and b/assets/emojies/e1622.png differ
diff --git a/assets/emojies/e1623.png b/assets/emojies/e1623.png
new file mode 100644
index 0000000..ab4508b
Binary files /dev/null and b/assets/emojies/e1623.png differ
diff --git a/assets/emojies/e1624.png b/assets/emojies/e1624.png
new file mode 100644
index 0000000..d5e20fd
Binary files /dev/null and b/assets/emojies/e1624.png differ
diff --git a/assets/emojies/e1625.png b/assets/emojies/e1625.png
new file mode 100644
index 0000000..0f831fe
Binary files /dev/null and b/assets/emojies/e1625.png differ
diff --git a/assets/emojies/e1626.png b/assets/emojies/e1626.png
new file mode 100644
index 0000000..91e7302
Binary files /dev/null and b/assets/emojies/e1626.png differ
diff --git a/assets/emojies/e1627.png b/assets/emojies/e1627.png
new file mode 100644
index 0000000..24667fd
Binary files /dev/null and b/assets/emojies/e1627.png differ
diff --git a/assets/emojies/e1628.png b/assets/emojies/e1628.png
new file mode 100644
index 0000000..d0c8a25
Binary files /dev/null and b/assets/emojies/e1628.png differ
diff --git a/assets/emojies/e1629.png b/assets/emojies/e1629.png
new file mode 100644
index 0000000..e2a89bd
Binary files /dev/null and b/assets/emojies/e1629.png differ
diff --git a/assets/emojies/e1630.png b/assets/emojies/e1630.png
new file mode 100644
index 0000000..97a8889
Binary files /dev/null and b/assets/emojies/e1630.png differ
diff --git a/assets/emojies/e1631.png b/assets/emojies/e1631.png
new file mode 100644
index 0000000..2f4e28c
Binary files /dev/null and b/assets/emojies/e1631.png differ
diff --git a/assets/emojies/e1632.png b/assets/emojies/e1632.png
new file mode 100644
index 0000000..43bbfdb
Binary files /dev/null and b/assets/emojies/e1632.png differ
diff --git a/assets/emojies/e1633.png b/assets/emojies/e1633.png
new file mode 100644
index 0000000..51d26d0
Binary files /dev/null and b/assets/emojies/e1633.png differ
diff --git a/assets/emojies/e1634.png b/assets/emojies/e1634.png
new file mode 100644
index 0000000..1f97158
Binary files /dev/null and b/assets/emojies/e1634.png differ
diff --git a/assets/emojies/e1635.png b/assets/emojies/e1635.png
new file mode 100644
index 0000000..843a73a
Binary files /dev/null and b/assets/emojies/e1635.png differ
diff --git a/assets/emojies/e1636.png b/assets/emojies/e1636.png
new file mode 100644
index 0000000..b2d038f
Binary files /dev/null and b/assets/emojies/e1636.png differ
diff --git a/assets/emojies/e1637.png b/assets/emojies/e1637.png
new file mode 100644
index 0000000..77e85ed
Binary files /dev/null and b/assets/emojies/e1637.png differ
diff --git a/assets/emojies/e1638.png b/assets/emojies/e1638.png
new file mode 100644
index 0000000..d9dbcd8
Binary files /dev/null and b/assets/emojies/e1638.png differ
diff --git a/assets/emojies/e1639.png b/assets/emojies/e1639.png
new file mode 100644
index 0000000..9733056
Binary files /dev/null and b/assets/emojies/e1639.png differ
diff --git a/assets/emojies/e1640.png b/assets/emojies/e1640.png
new file mode 100644
index 0000000..b64622f
Binary files /dev/null and b/assets/emojies/e1640.png differ
diff --git a/assets/emojies/e1641.png b/assets/emojies/e1641.png
new file mode 100644
index 0000000..3c26887
Binary files /dev/null and b/assets/emojies/e1641.png differ
diff --git a/assets/emojies/e1642.png b/assets/emojies/e1642.png
new file mode 100644
index 0000000..bd3d4cd
Binary files /dev/null and b/assets/emojies/e1642.png differ
diff --git a/assets/emojies/e1643.png b/assets/emojies/e1643.png
new file mode 100644
index 0000000..ed9f0d2
Binary files /dev/null and b/assets/emojies/e1643.png differ
diff --git a/assets/emojies/e1644.png b/assets/emojies/e1644.png
new file mode 100644
index 0000000..b794e26
Binary files /dev/null and b/assets/emojies/e1644.png differ
diff --git a/assets/emojies/e1645.png b/assets/emojies/e1645.png
new file mode 100644
index 0000000..d4c285e
Binary files /dev/null and b/assets/emojies/e1645.png differ
diff --git a/assets/emojies/e1646.png b/assets/emojies/e1646.png
new file mode 100644
index 0000000..b777568
Binary files /dev/null and b/assets/emojies/e1646.png differ
diff --git a/assets/emojies/e1647.png b/assets/emojies/e1647.png
new file mode 100644
index 0000000..45d0290
Binary files /dev/null and b/assets/emojies/e1647.png differ
diff --git a/assets/emojies/e1648.png b/assets/emojies/e1648.png
new file mode 100644
index 0000000..7c8d989
Binary files /dev/null and b/assets/emojies/e1648.png differ
diff --git a/assets/emojies/e1649.png b/assets/emojies/e1649.png
new file mode 100644
index 0000000..523605d
Binary files /dev/null and b/assets/emojies/e1649.png differ
diff --git a/assets/emojies/e1650.png b/assets/emojies/e1650.png
new file mode 100644
index 0000000..6bc399b
Binary files /dev/null and b/assets/emojies/e1650.png differ
diff --git a/assets/emojies/e1651.png b/assets/emojies/e1651.png
new file mode 100644
index 0000000..cdd4358
Binary files /dev/null and b/assets/emojies/e1651.png differ
diff --git a/assets/emojies/e1652.png b/assets/emojies/e1652.png
new file mode 100644
index 0000000..dc22db5
Binary files /dev/null and b/assets/emojies/e1652.png differ
diff --git a/assets/emojies/e1653.png b/assets/emojies/e1653.png
new file mode 100644
index 0000000..5a88393
Binary files /dev/null and b/assets/emojies/e1653.png differ
diff --git a/assets/emojies/e1654.png b/assets/emojies/e1654.png
new file mode 100644
index 0000000..7f45770
Binary files /dev/null and b/assets/emojies/e1654.png differ
diff --git a/assets/emojies/e1655.png b/assets/emojies/e1655.png
new file mode 100644
index 0000000..cc18d06
Binary files /dev/null and b/assets/emojies/e1655.png differ
diff --git a/assets/emojies/e1656.png b/assets/emojies/e1656.png
new file mode 100644
index 0000000..e421041
Binary files /dev/null and b/assets/emojies/e1656.png differ
diff --git a/assets/emojies/e1657.png b/assets/emojies/e1657.png
new file mode 100644
index 0000000..b1c8509
Binary files /dev/null and b/assets/emojies/e1657.png differ
diff --git a/assets/emojies/e1658.png b/assets/emojies/e1658.png
new file mode 100644
index 0000000..c4c5b47
Binary files /dev/null and b/assets/emojies/e1658.png differ
diff --git a/assets/emojies/e1659.png b/assets/emojies/e1659.png
new file mode 100644
index 0000000..ce417d6
Binary files /dev/null and b/assets/emojies/e1659.png differ
diff --git a/assets/emojies/e1660.png b/assets/emojies/e1660.png
new file mode 100644
index 0000000..70dbed9
Binary files /dev/null and b/assets/emojies/e1660.png differ
diff --git a/assets/emojies/e1661.png b/assets/emojies/e1661.png
new file mode 100644
index 0000000..9a064cc
Binary files /dev/null and b/assets/emojies/e1661.png differ
diff --git a/assets/emojies/e1662.png b/assets/emojies/e1662.png
new file mode 100644
index 0000000..4dc7035
Binary files /dev/null and b/assets/emojies/e1662.png differ
diff --git a/assets/emojies/e1663.png b/assets/emojies/e1663.png
new file mode 100644
index 0000000..b9fbc24
Binary files /dev/null and b/assets/emojies/e1663.png differ
diff --git a/assets/emojies/e1664.png b/assets/emojies/e1664.png
new file mode 100644
index 0000000..e6d891d
Binary files /dev/null and b/assets/emojies/e1664.png differ
diff --git a/assets/emojies/e1665.png b/assets/emojies/e1665.png
new file mode 100644
index 0000000..c12853f
Binary files /dev/null and b/assets/emojies/e1665.png differ
diff --git a/assets/emojies/e1666.png b/assets/emojies/e1666.png
new file mode 100644
index 0000000..9559979
Binary files /dev/null and b/assets/emojies/e1666.png differ
diff --git a/assets/emojies/e1757.png b/assets/emojies/e1757.png
new file mode 100644
index 0000000..2704209
Binary files /dev/null and b/assets/emojies/e1757.png differ
diff --git a/assets/emojies/e1758.png b/assets/emojies/e1758.png
new file mode 100644
index 0000000..9043915
Binary files /dev/null and b/assets/emojies/e1758.png differ
diff --git a/assets/emojies/e1759.png b/assets/emojies/e1759.png
new file mode 100644
index 0000000..3900cb4
Binary files /dev/null and b/assets/emojies/e1759.png differ
diff --git a/assets/emojies/e1760.png b/assets/emojies/e1760.png
new file mode 100644
index 0000000..173d402
Binary files /dev/null and b/assets/emojies/e1760.png differ
diff --git a/assets/emojies/e1761.png b/assets/emojies/e1761.png
new file mode 100644
index 0000000..3c4027f
Binary files /dev/null and b/assets/emojies/e1761.png differ
diff --git a/assets/emojies/e1762.png b/assets/emojies/e1762.png
new file mode 100644
index 0000000..a21de52
Binary files /dev/null and b/assets/emojies/e1762.png differ
diff --git a/assets/emojies/e1763.png b/assets/emojies/e1763.png
new file mode 100644
index 0000000..29e2271
Binary files /dev/null and b/assets/emojies/e1763.png differ
diff --git a/assets/emojies/e1764.png b/assets/emojies/e1764.png
new file mode 100644
index 0000000..1f47d1f
Binary files /dev/null and b/assets/emojies/e1764.png differ
diff --git a/assets/emojies/e1765.png b/assets/emojies/e1765.png
new file mode 100644
index 0000000..cddaa95
Binary files /dev/null and b/assets/emojies/e1765.png differ
diff --git a/assets/emojies/e1766.png b/assets/emojies/e1766.png
new file mode 100644
index 0000000..03f8a12
Binary files /dev/null and b/assets/emojies/e1766.png differ
diff --git a/assets/emojies/e1767.png b/assets/emojies/e1767.png
new file mode 100644
index 0000000..730ba8b
Binary files /dev/null and b/assets/emojies/e1767.png differ
diff --git a/assets/emojies/e1768.png b/assets/emojies/e1768.png
new file mode 100644
index 0000000..6ade0b8
Binary files /dev/null and b/assets/emojies/e1768.png differ
diff --git a/assets/emojies/e1769.png b/assets/emojies/e1769.png
new file mode 100644
index 0000000..7480e17
Binary files /dev/null and b/assets/emojies/e1769.png differ
diff --git a/assets/emojies/e1779.png b/assets/emojies/e1779.png
new file mode 100644
index 0000000..3c60281
Binary files /dev/null and b/assets/emojies/e1779.png differ
diff --git a/assets/emojies/e1780.png b/assets/emojies/e1780.png
new file mode 100644
index 0000000..c5de7c5
Binary files /dev/null and b/assets/emojies/e1780.png differ
diff --git a/assets/emojies/e1781.png b/assets/emojies/e1781.png
new file mode 100644
index 0000000..23d10c9
Binary files /dev/null and b/assets/emojies/e1781.png differ
diff --git a/assets/emojies/e1782.png b/assets/emojies/e1782.png
new file mode 100644
index 0000000..da12e14
Binary files /dev/null and b/assets/emojies/e1782.png differ
diff --git a/assets/emojies/e1783.png b/assets/emojies/e1783.png
new file mode 100644
index 0000000..50c061b
Binary files /dev/null and b/assets/emojies/e1783.png differ
diff --git a/assets/emojies/e1784.png b/assets/emojies/e1784.png
new file mode 100644
index 0000000..8ae305b
Binary files /dev/null and b/assets/emojies/e1784.png differ
diff --git a/assets/emojies/e1785.png b/assets/emojies/e1785.png
new file mode 100644
index 0000000..0754ded
Binary files /dev/null and b/assets/emojies/e1785.png differ
diff --git a/assets/emojies/e1791.png b/assets/emojies/e1791.png
new file mode 100644
index 0000000..4d6639a
Binary files /dev/null and b/assets/emojies/e1791.png differ
diff --git a/assets/emojies/e1792.png b/assets/emojies/e1792.png
new file mode 100644
index 0000000..00af374
Binary files /dev/null and b/assets/emojies/e1792.png differ
diff --git a/assets/emojies/e1793.png b/assets/emojies/e1793.png
new file mode 100644
index 0000000..6eea76d
Binary files /dev/null and b/assets/emojies/e1793.png differ
diff --git a/assets/emojies/e1794.png b/assets/emojies/e1794.png
new file mode 100644
index 0000000..f313339
Binary files /dev/null and b/assets/emojies/e1794.png differ
diff --git a/assets/emojies/e1795.png b/assets/emojies/e1795.png
new file mode 100644
index 0000000..3588d85
Binary files /dev/null and b/assets/emojies/e1795.png differ
diff --git a/assets/emojies/e1796.png b/assets/emojies/e1796.png
new file mode 100644
index 0000000..cae9006
Binary files /dev/null and b/assets/emojies/e1796.png differ
diff --git a/assets/emojies/e1799.png b/assets/emojies/e1799.png
new file mode 100644
index 0000000..209d8c4
Binary files /dev/null and b/assets/emojies/e1799.png differ
diff --git a/assets/emojies/e1819.png b/assets/emojies/e1819.png
new file mode 100644
index 0000000..1ff93bc
Binary files /dev/null and b/assets/emojies/e1819.png differ
diff --git a/assets/emojies/e1820.png b/assets/emojies/e1820.png
new file mode 100644
index 0000000..43a5113
Binary files /dev/null and b/assets/emojies/e1820.png differ
diff --git a/assets/emojies/e1821.png b/assets/emojies/e1821.png
new file mode 100644
index 0000000..13a6746
Binary files /dev/null and b/assets/emojies/e1821.png differ
diff --git a/assets/emojies/e1822.png b/assets/emojies/e1822.png
new file mode 100644
index 0000000..c0ab108
Binary files /dev/null and b/assets/emojies/e1822.png differ
diff --git a/assets/emojies/e1823.png b/assets/emojies/e1823.png
new file mode 100644
index 0000000..1596b2b
Binary files /dev/null and b/assets/emojies/e1823.png differ
diff --git a/assets/emojies/e1824.png b/assets/emojies/e1824.png
new file mode 100644
index 0000000..d2deddf
Binary files /dev/null and b/assets/emojies/e1824.png differ
diff --git a/assets/fonts/angkor/Angkor-Regular.ttf b/assets/fonts/angkor/Angkor-Regular.ttf
new file mode 100644
index 0000000..fe8e9eb
Binary files /dev/null and b/assets/fonts/angkor/Angkor-Regular.ttf differ
diff --git a/assets/fonts/dancing_script/DancingScript-Medium.ttf b/assets/fonts/dancing_script/DancingScript-Medium.ttf
new file mode 100644
index 0000000..748131f
Binary files /dev/null and b/assets/fonts/dancing_script/DancingScript-Medium.ttf differ
diff --git a/assets/fonts/dancing_script/DancingScript-Regular.ttf b/assets/fonts/dancing_script/DancingScript-Regular.ttf
new file mode 100644
index 0000000..b6f096b
Binary files /dev/null and b/assets/fonts/dancing_script/DancingScript-Regular.ttf differ
diff --git a/assets/fonts/lato/Lato-Regular.ttf b/assets/fonts/lato/Lato-Regular.ttf
new file mode 100644
index 0000000..bb2e887
Binary files /dev/null and b/assets/fonts/lato/Lato-Regular.ttf differ
diff --git a/assets/fonts/lora/Lora-Medium.ttf b/assets/fonts/lora/Lora-Medium.ttf
new file mode 100644
index 0000000..85ca5a2
Binary files /dev/null and b/assets/fonts/lora/Lora-Medium.ttf differ
diff --git a/assets/fonts/lora/Lora-Regular.ttf b/assets/fonts/lora/Lora-Regular.ttf
new file mode 100644
index 0000000..2b1dab4
Binary files /dev/null and b/assets/fonts/lora/Lora-Regular.ttf differ
diff --git a/assets/fonts/madimiOne/MadimiOne-Regular.ttf b/assets/fonts/madimiOne/MadimiOne-Regular.ttf
new file mode 100644
index 0000000..4b9a51d
Binary files /dev/null and b/assets/fonts/madimiOne/MadimiOne-Regular.ttf differ
diff --git a/assets/fonts/merriweather/Merriweather-Regular.ttf b/assets/fonts/merriweather/Merriweather-Regular.ttf
new file mode 100644
index 0000000..3fecc77
Binary files /dev/null and b/assets/fonts/merriweather/Merriweather-Regular.ttf differ
diff --git a/assets/fonts/montserrat/Montserrat-Medium.ttf b/assets/fonts/montserrat/Montserrat-Medium.ttf
new file mode 100644
index 0000000..4012225
Binary files /dev/null and b/assets/fonts/montserrat/Montserrat-Medium.ttf differ
diff --git a/assets/fonts/montserrat/Montserrat-Regular.ttf b/assets/fonts/montserrat/Montserrat-Regular.ttf
new file mode 100644
index 0000000..f4a266d
Binary files /dev/null and b/assets/fonts/montserrat/Montserrat-Regular.ttf differ
diff --git a/assets/fonts/oswald/Oswald-Medium.ttf b/assets/fonts/oswald/Oswald-Medium.ttf
new file mode 100644
index 0000000..187ecee
Binary files /dev/null and b/assets/fonts/oswald/Oswald-Medium.ttf differ
diff --git a/assets/fonts/oswald/Oswald-Regular.ttf b/assets/fonts/oswald/Oswald-Regular.ttf
new file mode 100644
index 0000000..5903df4
Binary files /dev/null and b/assets/fonts/oswald/Oswald-Regular.ttf differ
diff --git a/assets/fonts/pacifico/Pacifico-Regular.ttf b/assets/fonts/pacifico/Pacifico-Regular.ttf
new file mode 100644
index 0000000..e7def95
Binary files /dev/null and b/assets/fonts/pacifico/Pacifico-Regular.ttf differ
diff --git a/assets/fonts/raleway/Raleway-Medium.ttf b/assets/fonts/raleway/Raleway-Medium.ttf
new file mode 100644
index 0000000..015d810
Binary files /dev/null and b/assets/fonts/raleway/Raleway-Medium.ttf differ
diff --git a/assets/fonts/raleway/Raleway-Regular.ttf b/assets/fonts/raleway/Raleway-Regular.ttf
new file mode 100644
index 0000000..9a70667
Binary files /dev/null and b/assets/fonts/raleway/Raleway-Regular.ttf differ
diff --git a/assets/fonts/roboto/Roboto-Medium.ttf b/assets/fonts/roboto/Roboto-Medium.ttf
new file mode 100644
index 0000000..ac0f908
Binary files /dev/null and b/assets/fonts/roboto/Roboto-Medium.ttf differ
diff --git a/assets/fonts/roboto/Roboto-Regular.ttf b/assets/fonts/roboto/Roboto-Regular.ttf
new file mode 100644
index 0000000..ddf4bfa
Binary files /dev/null and b/assets/fonts/roboto/Roboto-Regular.ttf differ
diff --git a/assets/images/01_Cuppy_smile.webp b/assets/images/01_Cuppy_smile.webp
new file mode 100644
index 0000000..e8bd74b
Binary files /dev/null and b/assets/images/01_Cuppy_smile.webp differ
diff --git a/assets/images/02_Cuppy_lol.webp b/assets/images/02_Cuppy_lol.webp
new file mode 100644
index 0000000..7357797
Binary files /dev/null and b/assets/images/02_Cuppy_lol.webp differ
diff --git a/assets/images/03_Cuppy_rofl.webp b/assets/images/03_Cuppy_rofl.webp
new file mode 100644
index 0000000..0706b21
Binary files /dev/null and b/assets/images/03_Cuppy_rofl.webp differ
diff --git a/assets/images/04_Cuppy_sad.webp b/assets/images/04_Cuppy_sad.webp
new file mode 100644
index 0000000..2e19bc5
Binary files /dev/null and b/assets/images/04_Cuppy_sad.webp differ
diff --git a/assets/images/05_Cuppy_cry.webp b/assets/images/05_Cuppy_cry.webp
new file mode 100644
index 0000000..e7dbf9b
Binary files /dev/null and b/assets/images/05_Cuppy_cry.webp differ
diff --git a/assets/images/06_Cuppy_love.webp b/assets/images/06_Cuppy_love.webp
new file mode 100644
index 0000000..f6279f5
Binary files /dev/null and b/assets/images/06_Cuppy_love.webp differ
diff --git a/assets/images/07_Cuppy_hate.webp b/assets/images/07_Cuppy_hate.webp
new file mode 100644
index 0000000..20f93db
Binary files /dev/null and b/assets/images/07_Cuppy_hate.webp differ
diff --git a/assets/images/08_Cuppy_lovewithmug.webp b/assets/images/08_Cuppy_lovewithmug.webp
new file mode 100644
index 0000000..2819dc9
Binary files /dev/null and b/assets/images/08_Cuppy_lovewithmug.webp differ
diff --git a/assets/images/09_Cuppy_lovewithcookie.webp b/assets/images/09_Cuppy_lovewithcookie.webp
new file mode 100644
index 0000000..02cb316
Binary files /dev/null and b/assets/images/09_Cuppy_lovewithcookie.webp differ
diff --git a/assets/images/10_Cuppy_hmm.webp b/assets/images/10_Cuppy_hmm.webp
new file mode 100644
index 0000000..f516400
Binary files /dev/null and b/assets/images/10_Cuppy_hmm.webp differ
diff --git a/assets/images/11_Cuppy_upset.webp b/assets/images/11_Cuppy_upset.webp
new file mode 100644
index 0000000..05968c1
Binary files /dev/null and b/assets/images/11_Cuppy_upset.webp differ
diff --git a/assets/images/12_Cuppy_angry.webp b/assets/images/12_Cuppy_angry.webp
new file mode 100644
index 0000000..fa98eb8
Binary files /dev/null and b/assets/images/12_Cuppy_angry.webp differ
diff --git a/assets/images/13_Cuppy_curious.webp b/assets/images/13_Cuppy_curious.webp
new file mode 100644
index 0000000..61a0592
Binary files /dev/null and b/assets/images/13_Cuppy_curious.webp differ
diff --git a/assets/images/14_Cuppy_weird.webp b/assets/images/14_Cuppy_weird.webp
new file mode 100644
index 0000000..8800ff5
Binary files /dev/null and b/assets/images/14_Cuppy_weird.webp differ
diff --git a/assets/images/15_Cuppy_bluescreen.webp b/assets/images/15_Cuppy_bluescreen.webp
new file mode 100644
index 0000000..3c4a7ec
Binary files /dev/null and b/assets/images/15_Cuppy_bluescreen.webp differ
diff --git a/assets/images/16_Cuppy_angry.webp b/assets/images/16_Cuppy_angry.webp
new file mode 100644
index 0000000..3feefa5
Binary files /dev/null and b/assets/images/16_Cuppy_angry.webp differ
diff --git a/assets/images/17_Cuppy_tired.webp b/assets/images/17_Cuppy_tired.webp
new file mode 100644
index 0000000..4cf79bc
Binary files /dev/null and b/assets/images/17_Cuppy_tired.webp differ
diff --git a/assets/images/18_Cuppy_workhard.webp b/assets/images/18_Cuppy_workhard.webp
new file mode 100644
index 0000000..7b3d3d8
Binary files /dev/null and b/assets/images/18_Cuppy_workhard.webp differ
diff --git a/assets/images/19_Cuppy_shine.webp b/assets/images/19_Cuppy_shine.webp
new file mode 100644
index 0000000..1cc2173
Binary files /dev/null and b/assets/images/19_Cuppy_shine.webp differ
diff --git a/assets/images/20_Cuppy_disgusting.webp b/assets/images/20_Cuppy_disgusting.webp
new file mode 100644
index 0000000..d2f8339
Binary files /dev/null and b/assets/images/20_Cuppy_disgusting.webp differ
diff --git a/assets/images/21_Cuppy_hi.webp b/assets/images/21_Cuppy_hi.webp
new file mode 100644
index 0000000..306c6de
Binary files /dev/null and b/assets/images/21_Cuppy_hi.webp differ
diff --git a/assets/images/22_Cuppy_bye.webp b/assets/images/22_Cuppy_bye.webp
new file mode 100644
index 0000000..01c72da
Binary files /dev/null and b/assets/images/22_Cuppy_bye.webp differ
diff --git a/assets/images/23_Cuppy_greentea.webp b/assets/images/23_Cuppy_greentea.webp
new file mode 100644
index 0000000..ec7175f
Binary files /dev/null and b/assets/images/23_Cuppy_greentea.webp differ
diff --git a/assets/images/24_Cuppy_phone.webp b/assets/images/24_Cuppy_phone.webp
new file mode 100644
index 0000000..6f7c89f
Binary files /dev/null and b/assets/images/24_Cuppy_phone.webp differ
diff --git a/assets/images/25_Cuppy_battery.webp b/assets/images/25_Cuppy_battery.webp
new file mode 100644
index 0000000..f216343
Binary files /dev/null and b/assets/images/25_Cuppy_battery.webp differ
diff --git a/assets/images/tray_Cuppy.png b/assets/images/tray_Cuppy.png
new file mode 100644
index 0000000..f54a95f
Binary files /dev/null and b/assets/images/tray_Cuppy.png differ
diff --git a/example/example.dart b/example/example.dart
new file mode 100644
index 0000000..f85ca78
--- /dev/null
+++ b/example/example.dart
@@ -0,0 +1,123 @@
+
+import 'dart:io';
+
+import 'package:file_picker/file_picker.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_story_editor/flutter_story_editor.dart';
+import 'package:flutter_story_editor/src/controller/controller.dart';
+
+import 'package:path/path.dart' as path;
+
+class FlutterStoryEditorExample extends StatefulWidget {
+ // final User? user;
+ const FlutterStoryEditorExample({super.key});
+
+ @override
+ State createState() => _FlutterStoryEditorExampleState();
+}
+
+class _FlutterStoryEditorExampleState extends State with SingleTickerProviderStateMixin {
+
+ FlutterStoryEditorController controller = FlutterStoryEditorController();
+
+ final TextEditingController _captionController = TextEditingController();
+
+
+
+ List? _selectedMedia;
+
+ List? _mediaTypes; // To store the type of each selected file
+
+ Future selectMedia() async {
+ setState(() {
+ _selectedMedia = null;
+ _mediaTypes = null;
+ });
+
+ try {
+ final result = await FilePicker.platform.pickFiles(
+ type: FileType.media,
+ allowMultiple: true,
+ );
+ if (result != null) {
+ _selectedMedia = result.files.map((file) => File(file.path!)).toList();
+
+ // Initialize the media types list
+ _mediaTypes = List.filled(_selectedMedia!.length, '');
+
+ // Determine the type of each selected file
+ for (int i = 0; i < _selectedMedia!.length; i++) {
+ String extension = path.extension(_selectedMedia![i].path)
+ .toLowerCase();
+ if (extension == '.jpg' || extension == '.jpeg' ||
+ extension == '.png') {
+ _mediaTypes![i] = 'image';
+ } else if (extension == '.mp4' || extension == '.mov' ||
+ extension == '.avi') {
+ _mediaTypes![i] = 'video';
+ }
+ }
+
+ setState(() {});
+ } else {
+ print("No file is selected.");
+ }
+ } catch (e) {
+ print("Error while picking file: $e");
+ }
+ }
+
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+
+ Center(
+ child: IconButton(onPressed: () {
+
+
+ selectMedia().then(
+ (value) {
+
+ if (_selectedMedia != null && _selectedMedia!.isNotEmpty) {
+ showModalBottomSheet(
+ isScrollControlled: true,
+ isDismissible: false,
+ enableDrag: false,
+ context: context,
+ builder: (context) {
+
+ return FlutterStoryEditor(
+ controller: controller,
+ captionController: _captionController,
+ selectedFiles: _selectedMedia,
+ onSaveClickListener: (files) {
+ // Navigator.push(
+ // context,
+ // MaterialPageRoute(
+ // builder: (context) => ImagesPage(files: files),
+ // ),
+ // );
+ }
+ );
+ },
+ );
+ }
+
+ },
+ );
+ }, icon: const Icon(Icons.upload, size: 50,)),
+ ),
+
+
+ const SizedBox(height: 10),
+ const Text("Pick Files & Play with them")
+
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/flutter_story_editor.dart b/lib/flutter_story_editor.dart
new file mode 100644
index 0000000..f0d730c
--- /dev/null
+++ b/lib/flutter_story_editor.dart
@@ -0,0 +1,396 @@
+library flutter_story_editor;
+
+import 'dart:async';
+import 'dart:io';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
+import 'package:flutter_story_editor/src/controller/controller.dart';
+import 'package:flutter_story_editor/src/utils/utils.dart';
+
+import 'src/const/filters.dart';
+import 'src/enums/story_editing_modes.dart';
+import 'src/models/stroke.dart';
+import 'src/views/main_control_views/image_view.dart';
+import 'src/views/main_control_views/main_controls_view.dart';
+import 'src/views/main_control_views/trimmer_view.dart';
+import 'src/views/paint_control_views/paint_controls_view.dart';
+import 'src/views/sticker_control_views/sticker_control_view.dart';
+import 'src/widgets/draggable_sticker_widget.dart';
+import 'src/widgets/draggable_text_widget.dart';
+
+
+class FlutterStoryEditor extends StatefulWidget {
+ final List? selectedFiles; // Holds the files selected for editing.
+ final Function(List)? onSaveClickListener; // Callback when save action is triggered.
+ final TextEditingController? captionController; // Controller for handling caption text input.
+ final FlutterStoryEditorController controller; // Custom controller for managing editor state.
+ final bool? trimVideoOnAdjust; // Flag to determine if video should be trimmed when adjusted.
+ const FlutterStoryEditor(
+ {super.key, this.selectedFiles, this.onSaveClickListener, this.captionController, required this.controller, this.trimVideoOnAdjust=false});
+
+ @override
+ State createState() => _FlutterStoryEditorState();
+}
+
+class _FlutterStoryEditorState extends State {
+ StreamController drawingUndoController = StreamController.broadcast();
+
+ List editActions = [];
+
+
+ @override
+ void dispose() {
+ // Cleans up resources and controllers on widget disposal.
+ drawingUndoController.close();
+ widget.controller.setStoryEditingModeSelected = StoryEditingModes.NONE;
+ keyboardSubscription.cancel();
+ super.dispose();
+ }
+
+ void undo(List lines) {
+ // Removes the last drawn line from the list, effectively undoing the last draw action.
+ if (lines.isNotEmpty) {
+ setState(() {
+ lines.removeLast();
+ });
+ }
+ }
+
+ void onUndoClick() {
+ // Trigger for undo action, adds a signal to the drawingUndoController.
+ drawingUndoController.add(true);
+ }
+
+ List _imageKeys = []; // Keys for uniquely identifying image widgets within the editor.
+ final Map _thumbnails = {}; // Cache for storing generated thumbnails.
+ int currentPageIndex = 0; // Tracks the current page index within the story editor.
+ List> selectedFilters = []; // Stores filters applied to each story page.
+ List? uiViewEditableFiles; // Holds the editable files for UI display.
+ bool isSaving = false; // Flag to indicate save operation is in progress.
+ bool isKeyboardFocused = false; // Tracks the keyboard visibility state.
+ late StreamSubscription keyboardSubscription; // Subscription to keyboard visibility changes.
+
+
+ @override
+ void initState() {
+ super.initState();
+
+ drawingUndoController.stream.listen((_) => undo(widget.controller.uiEditableFileLines[currentPageIndex]));
+
+ uiViewEditableFiles = List.from(widget.selectedFiles!);
+
+ widget.controller.initializeUiEditableFileLines(widget.selectedFiles!.length);
+
+ _imageKeys = List.generate(widget.selectedFiles!.length, (index) => GlobalKey());
+
+ selectedFilters = List.generate(widget.selectedFiles!.length, (index) => NO_FILTER);
+
+ textList = ValueNotifier(List.generate(widget.selectedFiles!.length, (index) => []));
+ stickersList = ValueNotifier(List.generate(widget.selectedFiles!.length, (index) => []));
+
+ captionFocusNode.addListener(() {
+ if (captionFocusNode.hasFocus) {
+ keyboardSubscription.cancel();
+ }
+ });
+
+ var keyboardVisibilityController = KeyboardVisibilityController();
+
+ keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) {
+ setState(() {
+ isKeyboardFocused = visible;
+ });
+ });
+ }
+
+ // textList to store Text for each page.
+ ValueNotifier>> textList = ValueNotifier>>([]);
+ // stickersList to store Stickers for each page.
+ ValueNotifier>> stickersList = ValueNotifier>>([]);
+
+ final PageController _pageController = PageController();
+
+ FocusNode captionFocusNode = FocusNode();
+
+ @override
+ Widget build(BuildContext context) {
+ return ValueListenableBuilder(
+ valueListenable: stickersList,
+ builder: (context, stickerListValue, child) {
+ return ValueListenableBuilder(
+ valueListenable: textList,
+ builder: (context, textListValue, child) {
+ return ValueListenableBuilder(
+ valueListenable: widget.controller.editingModeNotifier,
+ builder: (BuildContext context, StoryEditingModes mode, Widget? child) {
+ return PopScope(
+ canPop: mode == StoryEditingModes.NONE,
+ onPopInvoked: (bool isSystemPop) {
+ if (!isSystemPop) {
+ widget.controller.setStoryEditingModeSelected = StoryEditingModes.NONE;
+ }
+ },
+ child: Container(
+ color: Colors.black,
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ SizedBox(
+ width: double.infinity,
+ child: PageView(
+ physics: isKeyboardFocused || widget.controller.editingModeSelected != StoryEditingModes.NONE
+ ? const NeverScrollableScrollPhysics()
+ : const ScrollPhysics(),
+ controller: _pageController,
+ onPageChanged: (index) {
+ setState(() {
+ currentPageIndex = index;
+ });
+ },
+ children: uiViewEditableFiles!.map((singleStory) {
+ int storyIndex = uiViewEditableFiles!.indexOf(singleStory);
+ if (isVideo(singleStory)) {
+ return TrimmerView(
+ lines: widget.controller.uiEditableFileLines[storyIndex],
+ trimOnAdjust: widget.trimVideoOnAdjust,
+ onTrimCompleted: (file) async {
+ await generateThumbnail(file)
+ .then((generatedThumbnail) {
+ setState(() {
+ _thumbnails[file] = generatedThumbnail;
+ });
+ });
+ setState(() {
+ widget.selectedFiles![storyIndex] = file;
+ });
+ },
+ key: ValueKey(singleStory.path),
+ file: singleStory,
+ pageController: _pageController,
+ pageIndex: storyIndex,
+ );
+ } else {
+ return RepaintBoundary(
+ key: _imageKeys[storyIndex],
+ child: ImageView(
+ storyIndex: storyIndex,
+ textList: textListValue,
+ stickerList: stickerListValue,
+ lines: widget.controller.uiEditableFileLines[storyIndex],
+ controller: widget.controller,
+ file: singleStory,
+ filter: selectedFilters[storyIndex],
+ ),
+ );
+ }
+ }).toList(),
+ )),
+ if (mode == StoryEditingModes.PAINT)
+ PaintControlsView(
+ onDoneClickListener: () async {
+ widget.controller.setStoryEditingModeSelected = StoryEditingModes.NONE;
+
+ await generateThumbnail(uiViewEditableFiles![currentPageIndex]).then((generatedThumbnail) {
+ setState(() {
+ _thumbnails[uiViewEditableFiles![currentPageIndex]] = generatedThumbnail;
+ });
+ });
+ },
+ onUndoClickListener: () {
+ undo(widget.controller.uiEditableFileLines[currentPageIndex]);
+ onUndoClick();
+
+ setState(() {
+ if (widget.controller.uiEditableFileLines[currentPageIndex].isNotEmpty) {
+ widget.controller.uiEditableFileLines[currentPageIndex] =
+ List.from(widget.controller.uiEditableFileLines[currentPageIndex])..removeLast();
+ widget.controller.setUiEditableFileLines(
+ currentPageIndex, widget.controller.uiEditableFileLines[currentPageIndex]);
+ }
+ });
+ },
+ uiEditableFileLines: widget.controller.uiEditableFileLines[currentPageIndex],
+ onPointerDownUpdate: (newLine) {
+ setState(() {
+ editActions.add(EditAction(item: newLine, type: 'line', pageIndex: currentPageIndex));
+
+ widget.controller.uiEditableFileLines[currentPageIndex] = [
+ ...widget.controller.uiEditableFileLines[currentPageIndex],
+ newLine
+ ];
+ widget.controller.setUiEditableFileLines(
+ currentPageIndex, widget.controller.uiEditableFileLines[currentPageIndex]);
+ });
+
+ },
+ controller: widget.controller,
+ selectedFile: widget.selectedFiles![currentPageIndex],
+ )
+ else if (mode == StoryEditingModes.STICKERS)
+ StickerControlView(
+ controller: widget.controller,
+ onStickerClickListener: (stickerPath) {
+
+ widget.controller.setStoryEditingModeSelected = StoryEditingModes.NONE;
+
+ setState(() {
+
+
+
+ if (stickersList.value.length <= currentPageIndex) {
+ stickersList.value.add([]);
+ }
+
+ final draggableSticker = DraggableStickerWidget(
+ stickerPath: stickerPath,
+ key: UniqueKey(),
+ );
+
+ stickersList.value[currentPageIndex].add(
+ draggableSticker,
+ );
+
+ editActions.add(EditAction(item: draggableSticker, type: 'sticker', pageIndex: currentPageIndex));
+
+ });
+
+ }
+ )
+ else if (mode == StoryEditingModes.TEXT)
+ Container()
+ else
+ MainControlsView(
+ stickerList: stickerListValue,
+ onStickersClickListener: () {
+ widget.controller.setStoryEditingModeSelected = StoryEditingModes.STICKERS;
+ },
+ captionFocusNode: captionFocusNode,
+ textList: textListValue,
+ isFocused: isKeyboardFocused,
+ lines: widget.controller.uiEditableFileLines[currentPageIndex],
+ onTextClickListener: () {
+ widget.controller.setStoryEditingModeSelected = StoryEditingModes.TEXT;
+
+ setState(() {
+ if (textList.value.length <= currentPageIndex) {
+ textList.value.add([]);
+ }
+
+ final draggableText = DraggableTextWidget(
+ controller: widget.controller,
+ textList: textList.value[currentPageIndex],
+ key: UniqueKey(),
+ );
+
+ textList.value[currentPageIndex].add(
+ draggableText
+ );
+
+ editActions.add(EditAction(item: draggableText, type: 'text', pageIndex: currentPageIndex));
+
+ });
+ },
+ onPaintClickListener: () {
+ widget.controller.setFileSelected = widget.selectedFiles![currentPageIndex];
+ widget.controller.setFilterSelected = selectedFilters[currentPageIndex];
+
+ widget.controller.setStoryEditingModeSelected = StoryEditingModes.PAINT;
+ },
+ currentPageIndex: currentPageIndex,
+ pageController: _pageController,
+ onUndoClickListener: () {
+
+ if (editActions.isNotEmpty) {
+ EditAction lastAction = editActions.removeLast();
+ setState(() {
+ switch (lastAction.type) {
+ case 'text':
+ textList.value[currentPageIndex].remove(lastAction.item);
+ break;
+ case 'sticker':
+ stickersList.value[currentPageIndex].remove(lastAction.item);
+ break;
+ case 'filter':
+ selectedFilters[currentPageIndex] = NO_FILTER;
+ break;
+ case 'line':
+ undo(widget.controller.uiEditableFileLines[currentPageIndex]);
+ onUndoClick();
+ widget.controller.uiEditableFileLines[currentPageIndex] =
+ List.from(widget.controller.uiEditableFileLines[currentPageIndex])..remove(lastAction.item);
+ widget.controller.setUiEditableFileLines(
+ currentPageIndex, widget.controller.uiEditableFileLines[currentPageIndex]);
+
+ break;
+ }
+
+ });
+ }
+ },
+ onImageCrop: (croppedImage) {
+ setState(() {
+ uiViewEditableFiles![currentPageIndex] = croppedImage;
+ });
+ },
+ onFilterChange: (filter) {
+ setState(() {
+ editActions.add(EditAction(item: filter, type: 'filter', pageIndex: currentPageIndex));
+ selectedFilters[currentPageIndex] = filter;
+ });
+ },
+ selectedFilters: selectedFilters,
+ uiViewEditableFiles: uiViewEditableFiles!,
+ onSaveClickListener: () async {
+ setState(() => isSaving = true);
+
+ for (int i = 0; i < widget.selectedFiles!.length; i++) {
+ if (!isVideo(widget.selectedFiles![i])) {
+ await _pageController.animateToPage(i,
+ duration: const Duration(milliseconds: 300), curve: Curves.ease);
+
+ // Waiting for page transition
+ await Future.delayed(const Duration(milliseconds: 500));
+
+ File? snapshotFile = await convertWidgetToImage(_imageKeys[i]);
+ if (snapshotFile != null) {
+ setState(() {
+ widget.selectedFiles![i] = snapshotFile;
+ });
+ }
+ }
+ }
+
+ setState(() => isSaving = false);
+
+ if (widget.selectedFiles != null && widget.selectedFiles!.isNotEmpty) {
+ widget.onSaveClickListener!(widget.selectedFiles!);
+ }
+ },
+ selectedFiles: widget.selectedFiles,
+ controller: widget.controller,
+ captionController: widget.captionController,
+ isSaving: isSaving,
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ },
+ );
+ }
+ );
+ }
+}
+
+
+class EditAction {
+ final dynamic item;
+ final String type;
+ final int pageIndex;
+
+ EditAction({required this.item, required this.type, required this.pageIndex});
+}
+
diff --git a/lib/src/const/const.dart b/lib/src/const/const.dart
new file mode 100644
index 0000000..4dabaf7
--- /dev/null
+++ b/lib/src/const/const.dart
@@ -0,0 +1,247 @@
+
+
+
+
+import 'filters.dart';
+
+class Consts {
+
+
+ static List filterNames = ["None", "Pop", "B&W", "Cool", "Chrome", "Film"];
+
+
+ static List> filters = [
+ NO_FILTER,
+ POP,
+ BLACK_AND_WHITE,
+ COOL,
+ CHROME,
+ FILM
+ ];
+
+ static List stickers = [
+ '01_Cuppy_smile.webp',
+ '02_Cuppy_lol.webp',
+ '03_Cuppy_rofl.webp',
+ '04_Cuppy_sad.webp',
+ '05_Cuppy_cry.webp',
+ '06_Cuppy_love.webp',
+ '07_Cuppy_hate.webp',
+ '08_Cuppy_lovewithmug.webp',
+ '09_Cuppy_lovewithcookie.webp',
+ '10_Cuppy_hmm.webp',
+ '11_Cuppy_upset.webp',
+ '12_Cuppy_angry.webp',
+ '13_Cuppy_curious.webp',
+ '14_Cuppy_weird.webp',
+ '15_Cuppy_bluescreen.webp',
+ '16_Cuppy_angry.webp',
+ '17_Cuppy_tired.webp',
+ '18_Cuppy_workhard.webp',
+ '19_Cuppy_shine.webp',
+ '20_Cuppy_disgusting.webp',
+ '21_Cuppy_hi.webp',
+ '22_Cuppy_bye.webp',
+ '23_Cuppy_greentea.webp',
+ '24_Cuppy_phone.webp',
+ '25_Cuppy_battery.webp',
+ 'tray_Cuppy.png',
+ ];
+
+
+ static List emojies = [
+ 'e1511.png',
+ 'e1512.png',
+ 'e1513.png',
+ 'e1514.png',
+ 'e1515.png',
+ 'e1516.png',
+ 'e1517.png',
+ 'e1518.png',
+ 'e1519.png',
+ 'e1520.png',
+ 'e1521.png',
+ 'e1522.png',
+ 'e1523.png',
+ 'e1524.png',
+ 'e1525.png',
+ 'e1526.png',
+ 'e1527.png',
+ 'e1528.png',
+ 'e1529.png',
+ 'e1530.png',
+ 'e1531.png',
+ 'e1532.png',
+ 'e1533.png',
+ 'e1534.png',
+ 'e1535.png',
+ 'e1536.png',
+ 'e1537.png',
+ 'e1538.png',
+ 'e1539.png',
+ 'e1540.png',
+ 'e1541.png',
+ 'e1542.png',
+ 'e1543.png',
+ 'e1544.png',
+ 'e1545.png',
+ 'e1546.png',
+ 'e1547.png',
+ 'e1548.png',
+ 'e1549.png',
+ 'e1550.png',
+ 'e1551.png',
+ 'e1552.png',
+ 'e1553.png',
+ 'e1554.png',
+ 'e1555.png',
+ 'e1556.png',
+ 'e1557.png',
+ 'e1558.png',
+ 'e1559.png',
+ 'e1560.png',
+ 'e1561.png',
+ 'e1562.png',
+ 'e1563.png',
+ 'e1564.png',
+ 'e1565.png',
+ 'e1566.png',
+ 'e1567.png',
+ 'e1568.png',
+ 'e1569.png',
+ 'e1570.png',
+ 'e1571.png',
+ 'e1572.png',
+ 'e1573.png',
+ 'e1574.png',
+ 'e1575.png',
+ 'e1576.png',
+ 'e1577.png',
+ 'e1578.png',
+ 'e1579.png',
+ 'e1580.png',
+ 'e1581.png',
+ 'e1582.png',
+ 'e1583.png',
+ 'e1584.png',
+ 'e1585.png',
+ 'e1586.png',
+ 'e1587.png',
+ 'e1588.png',
+ 'e1589.png',
+ 'e1590.png',
+ 'e1591.png',
+ 'e1592.png',
+ 'e1593.png',
+ 'e1594.png',
+ 'e1595.png',
+ 'e1596.png',
+ 'e1597.png',
+ 'e1598.png',
+ 'e1599.png',
+ 'e1600.png',
+ 'e1600.png',
+ 'e1601.png',
+ 'e1602.png',
+ 'e1603.png',
+ 'e1604.png',
+ 'e1605.png',
+ 'e1606.png',
+ 'e1607.png',
+ 'e1608.png',
+ 'e1609.png',
+ 'e1610.png',
+ 'e1611.png',
+ 'e1612.png',
+ 'e1613.png',
+ 'e1614.png',
+ 'e1615.png',
+ 'e1616.png',
+ 'e1617.png',
+ 'e1618.png',
+ 'e1619.png',
+ 'e1620.png',
+ 'e1621.png',
+ 'e1622.png',
+ 'e1623.png',
+ 'e1624.png',
+ 'e1625.png',
+ 'e1626.png',
+ 'e1627.png',
+ 'e1628.png',
+ 'e1629.png',
+ 'e1630.png',
+ 'e1631.png',
+ 'e1632.png',
+ 'e1633.png',
+ 'e1634.png',
+ 'e1635.png',
+ 'e1636.png',
+ 'e1637.png',
+ 'e1638.png',
+ 'e1639.png',
+ 'e1640.png',
+ 'e1641.png',
+ 'e1642.png',
+ 'e1643.png',
+ 'e1644.png',
+ 'e1645.png',
+ 'e1646.png',
+ 'e1647.png',
+ 'e1648.png',
+ 'e1649.png',
+ 'e1650.png',
+ 'e1651.png',
+ 'e1652.png',
+ 'e1653.png',
+ 'e1654.png',
+ 'e1655.png',
+ 'e1656.png',
+ 'e1657.png',
+ 'e1658.png',
+ 'e1659.png',
+ 'e1660.png',
+ 'e1661.png',
+ 'e1662.png',
+ 'e1663.png',
+ 'e1664.png',
+ 'e1665.png',
+ 'e1666.png',
+ 'e1757.png',
+ 'e1758.png',
+ 'e1759.png',
+ 'e1760.png',
+ 'e1761.png',
+ 'e1762.png',
+ 'e1763.png',
+ 'e1764.png',
+ 'e1765.png',
+ 'e1766.png',
+ 'e1767.png',
+ 'e1768.png',
+ 'e1769.png',
+ 'e1779.png',
+ 'e1780.png',
+ 'e1781.png',
+ 'e1782.png',
+ 'e1783.png',
+ 'e1784.png',
+ 'e1785.png',
+ 'e1791.png',
+ 'e1791.png',
+ 'e1792.png',
+ 'e1793.png',
+ 'e1794.png',
+ 'e1795.png',
+ 'e1796.png',
+ 'e1799.png',
+ 'e1819.png',
+ 'e1820.png',
+ 'e1821.png',
+ 'e1822.png',
+ 'e1823.png',
+ 'e1824.png',
+
+ ];
+
+}
\ No newline at end of file
diff --git a/lib/src/const/filters.dart b/lib/src/const/filters.dart
new file mode 100644
index 0000000..4efb574
--- /dev/null
+++ b/lib/src/const/filters.dart
@@ -0,0 +1,83 @@
+// No Filter: Identity Matrix
+import 'package:flutter/material.dart';
+
+const NO_FILTER = [
+ 1.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0, 0.0
+];
+
+// Black & White
+const BLACK_AND_WHITE = [
+ 0.3, 0.6, 0.1, 0.0, 0.0,
+ 0.3, 0.6, 0.1, 0.0, 0.0,
+ 0.3, 0.6, 0.1, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0, 0.0
+];
+
+
+// Pop: Increase saturation
+const POP = [
+ 1.3, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 1.3, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 1.3, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0, 0.0
+];
+
+// Cool: Add a blue tint
+const COOL = [
+ 1.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 1.2, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0, 0.0
+];
+
+// Chrome: Increase contrast
+const CHROME = [
+ 1.5, 0.0, 0.0, 0.0, -0.2,
+ 0.0, 1.5, 0.0, 0.0, -0.2,
+ 0.0, 0.0, 1.5, 0.0, -0.2,
+ 0.0, 0.0, 0.0, 1.0, 0.0
+];
+
+// Film: Decrease saturation
+const FILM = [
+ 0.8, 0.2, 0.2, 0.0, 0.0,
+ 0.2, 0.8, 0.2, 0.0, 0.0,
+ 0.2, 0.2, 0.8, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0, 0.0
+];
+
+final List textFilterColors = [
+ Colors.white,
+ Colors.black,
+ Colors.red,
+ Colors.orange,
+ Colors.yellow,
+ Colors.green,
+ Colors.blue,
+ Colors.indigo,
+ Colors.purple, // Violet
+ Colors.brown,
+];
+
+List fontStyles = [
+ const TextStyle(fontFamily: 'Roboto', color: Colors.white),
+ const TextStyle(fontFamily: 'Merriweather', color: Colors.white),
+ const TextStyle(fontFamily: 'Madimi One', fontWeight: FontWeight.bold, color: Colors.white),
+
+ // Serif fonts (with small "tails" on letters):
+ const TextStyle(fontFamily: 'Dancing Script', color: Colors.white),
+ const TextStyle(fontFamily: 'Angkor', color: Colors.white),
+ const TextStyle(fontFamily: 'Pacifico', color: Colors.white),
+
+ // Sans-serif fonts (clean, without tails):
+ const TextStyle(fontFamily: 'Montserrat', color: Colors.white),
+ const TextStyle(fontFamily: 'Lato', color: Colors.white),
+
+ // More stylized choices:
+ const TextStyle(fontFamily: 'Oswald', color: Colors.white),
+ const TextStyle(fontFamily: 'Raleway', color: Colors.white),
+ const TextStyle(fontFamily: 'Lora', color: Colors.white),
+];
diff --git a/lib/src/controller/controller.dart b/lib/src/controller/controller.dart
new file mode 100644
index 0000000..561ee8d
--- /dev/null
+++ b/lib/src/controller/controller.dart
@@ -0,0 +1,53 @@
+
+
+import 'dart:io';
+import 'package:flutter/material.dart';
+import 'package:flutter_story_editor/src/const/filters.dart';
+import 'package:flutter_story_editor/src/enums/story_editing_modes.dart';
+import 'package:flutter_story_editor/src/models/stroke.dart';
+
+
+class FlutterStoryEditorController extends ChangeNotifier {
+
+ // Editing Mode
+ final editingModeNotifier = ValueNotifier(StoryEditingModes.NONE);
+ StoryEditingModes get editingModeSelected => editingModeNotifier.value;
+ set setStoryEditingModeSelected(StoryEditingModes newStoryEditingSelectedMode) {
+ editingModeNotifier.value = newStoryEditingSelectedMode;
+ notifyListeners();
+ }
+
+ // File
+ final fileNotifier = ValueNotifier(null);
+ File? get fileSelected => fileNotifier.value;
+ set setFileSelected(File? newFile) {
+ fileNotifier.value = newFile;
+ notifyListeners();
+ }
+
+ // Filter
+ final filterNotifier = ValueNotifier>(NO_FILTER);
+ List? get filterSelected => filterNotifier.value;
+ set setFilterSelected(List newFilter) {
+ filterNotifier.value = newFilter;
+ notifyListeners();
+ }
+
+ // uiEditableFileLines
+
+ final ValueNotifier>> uiEditableFileLinesNotifier = ValueNotifier>>([]);
+
+ List> get uiEditableFileLines => uiEditableFileLinesNotifier.value;
+
+ void setUiEditableFileLines(int index, List newLines) {
+ uiEditableFileLinesNotifier.value[index] = newLines;
+ uiEditableFileLinesNotifier.value = [...uiEditableFileLines];
+ uiEditableFileLinesNotifier.notifyListeners();
+ }
+
+ void initializeUiEditableFileLines(int count) {
+ uiEditableFileLinesNotifier.value = List.generate(count, (index) => []);
+ notifyListeners();
+ }
+
+}
\ No newline at end of file
diff --git a/lib/src/enums/story_editing_modes.dart b/lib/src/enums/story_editing_modes.dart
new file mode 100644
index 0000000..04ba91c
--- /dev/null
+++ b/lib/src/enums/story_editing_modes.dart
@@ -0,0 +1,9 @@
+
+
+enum StoryEditingModes {
+ NONE,
+ FILTERS,
+ PAINT,
+ TEXT,
+ STICKERS,
+}
\ No newline at end of file
diff --git a/lib/src/enums/stroke_type.dart b/lib/src/enums/stroke_type.dart
new file mode 100644
index 0000000..d7715b1
--- /dev/null
+++ b/lib/src/enums/stroke_type.dart
@@ -0,0 +1,2 @@
+
+enum StrokeType { pen, marker, neon }
\ No newline at end of file
diff --git a/lib/src/models/simple_sketecher.dart b/lib/src/models/simple_sketecher.dart
new file mode 100644
index 0000000..e650cb3
--- /dev/null
+++ b/lib/src/models/simple_sketecher.dart
@@ -0,0 +1,66 @@
+import 'package:flutter/material.dart';
+import 'package:perfect_freehand/perfect_freehand.dart';
+
+import 'stroke.dart';
+
+
+
+class SimpleSketcher extends CustomPainter {
+ final List lines;
+
+ SimpleSketcher(this.lines);
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ Paint paint = Paint()
+ ..strokeJoin = StrokeJoin.round
+ ..strokeCap = StrokeCap.round
+ ..strokeMiterLimit = 5
+ ..filterQuality = FilterQuality.high
+ ..style = PaintingStyle.fill;
+
+ for (int i = 0; i < lines.length; ++i) {
+ final outlinePoints = getStroke(
+ lines[i].points,
+ options: StrokeOptions(
+ size: lines[i].options.size,
+ thinning: lines[i].options.thinning,
+ smoothing: lines[i].options.smoothing,
+ streamline: lines[i].options.streamline,
+ start: lines[i].options.start,
+ end: lines[i].options.end,
+ simulatePressure: lines[i].options.simulatePressure,
+ isComplete: lines[i].options.isComplete,
+ ),
+
+ );
+
+ paint.color = lines[i].color;
+
+ final path = Path();
+ if (outlinePoints.isEmpty) {
+ return;
+ } else if (outlinePoints.length < 2) {
+ path.addOval(Rect.fromCircle(
+ center: Offset(outlinePoints[0].dx, outlinePoints[0].dy), radius: 1));
+ } else {
+ path.moveTo(outlinePoints[0].dx, outlinePoints[0].dy);
+ for (int i = 1; i < outlinePoints.length - 1; ++i) {
+ final p0 = outlinePoints[i];
+ final p1 = outlinePoints[i + 1];
+ path.quadraticBezierTo(
+ p0.dx, p0.dy, (p0.dx + p1.dx) / 2, (p0.dy + p1.dy) / 2);
+ }
+ }
+ canvas.drawPath(path, paint);
+ }
+ }
+
+ @override
+ bool shouldRepaint(SimpleSketcher oldDelegate) {
+ return oldDelegate.lines != lines;
+ }
+}
+
+
+
diff --git a/lib/src/models/stroke.dart b/lib/src/models/stroke.dart
new file mode 100644
index 0000000..7adf1c9
--- /dev/null
+++ b/lib/src/models/stroke.dart
@@ -0,0 +1,10 @@
+
+import 'package:flutter/material.dart';
+import 'package:perfect_freehand/perfect_freehand.dart';
+
+class Stroke {
+ final List points;
+ final Color color;
+ final StrokeOptions options;
+ const Stroke(this.points, this.color, this.options);
+}
\ No newline at end of file
diff --git a/lib/src/models/stroke_options.dart b/lib/src/models/stroke_options.dart
new file mode 100644
index 0000000..9ca8d0c
--- /dev/null
+++ b/lib/src/models/stroke_options.dart
@@ -0,0 +1,60 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_story_editor/src/enums/stroke_type.dart';
+
+class StrokeOptions {
+ /// The base size (diameter) of the stroke.
+ /// Range: [0,100]
+ double size;
+
+ /// The effect of pressure on the stroke's size.
+ /// Range: [-1,1]
+ double thinning;
+
+ /// Controls the density of points along the stroke's edges.
+ /// Range: [0,1]
+ double smoothing;
+
+ /// Controls the level of variation allowed in the input points.
+ /// Range: [0,1]
+ double streamline;
+
+ // Whether to simulate pressure or use the point's provided pressures.
+ final bool simulatePressure;
+
+ // The distance to taper the front of the stroke.
+ // Range: [0,100]
+ double taperStart;
+
+ // The distance to taper the end of the stroke.
+ // Range: [0,100]
+ double taperEnd;
+
+ // Whether to add a cap to the start of the stroke.
+ final bool capStart;
+
+ // Whether to add a cap to the end of the stroke.
+ final bool capEnd;
+
+ // Whether the line is complete.
+ final bool isComplete;
+
+ //color of line
+ Color color;
+
+ StrokeType strokeType;
+
+ StrokeOptions(
+ {this.size = 3,
+ this.thinning = 0.2,
+ this.smoothing = 0.5,
+ this.streamline = 0.5,
+ this.taperStart = 0.0,
+ this.capStart = true,
+ this.taperEnd = 0.0,
+ this.capEnd = true,
+ this.simulatePressure = true,
+ this.isComplete = false,
+ this.color = Colors.white,
+ this.strokeType = StrokeType.pen
+ });
+}
\ No newline at end of file
diff --git a/lib/src/theme/style.dart b/lib/src/theme/style.dart
new file mode 100644
index 0000000..bad284a
--- /dev/null
+++ b/lib/src/theme/style.dart
@@ -0,0 +1,5 @@
+import 'package:flutter/material.dart';
+
+const darkGreenColor = Color.fromRGBO(31, 44, 52, 1);
+const tealColor = Color.fromRGBO(0, 167, 131, 1);
+
diff --git a/lib/src/utils/matrix_gesture_detector.dart b/lib/src/utils/matrix_gesture_detector.dart
new file mode 100644
index 0000000..c836af9
--- /dev/null
+++ b/lib/src/utils/matrix_gesture_detector.dart
@@ -0,0 +1,259 @@
+
+import 'dart:math';
+
+import 'package:flutter/widgets.dart';
+
+typedef MatrixGestureDetectorCallback = void Function(
+ Matrix4 matrix,
+ Matrix4 translationDeltaMatrix,
+ Matrix4 scaleDeltaMatrix,
+ Matrix4 rotationDeltaMatrix);
+
+/// [MatrixGestureDetector] detects translation, scale and rotation gestures
+/// and combines them into [Matrix4] object that can be used by [Transform] widget
+/// or by low level [CustomPainter] code. You can customize types of reported
+/// gestures by passing [shouldTranslate], [shouldScale] and [shouldRotate]
+/// parameters.
+///
+class MatrixGestureDetector extends StatefulWidget {
+ /// [Matrix4] change notification callback
+ ///
+ final MatrixGestureDetectorCallback onMatrixUpdate;
+
+ /// The [child] contained by this detector.
+ ///
+ /// {@macro flutter.widgets.child}
+ ///
+ final Widget child;
+
+ /// Whether to detect translation gestures during the event processing.
+ ///
+ /// Defaults to true.
+ ///
+ final bool shouldTranslate;
+
+ /// Whether to detect scale gestures during the event processing.
+ ///
+ /// Defaults to true.
+ ///
+ final bool shouldScale;
+
+ /// Whether to detect rotation gestures during the event processing.
+ ///
+ /// Defaults to true.
+ ///
+ final bool shouldRotate;
+
+ /// Whether [ClipRect] widget should clip [child] widget.
+ ///
+ /// Defaults to true.
+ ///
+ final bool clipChild;
+
+ /// The hit test behavior, passed to the underlying GestureDetector.
+ ///
+ /// Defaults to HitTestBehavior.deferToChild
+ ///
+ final HitTestBehavior behavior;
+
+ /// When set, it will be used for computing a "fixed" focal point
+ /// aligned relative to the size of this widget.
+ final Alignment? focalPointAlignment;
+
+ const MatrixGestureDetector({
+ super.key,
+ required this.onMatrixUpdate,
+ required this.child,
+ this.shouldTranslate = true,
+ this.shouldScale = true,
+ this.shouldRotate = true,
+ this.clipChild = true,
+ this.focalPointAlignment,
+ this.behavior = HitTestBehavior.deferToChild,
+ });
+
+ @override
+ _MatrixGestureDetectorState createState() => _MatrixGestureDetectorState();
+
+ ///
+ /// Compose the matrix from translation, scale and rotation matrices - you can
+ /// pass a null to skip any matrix from composition.
+ ///
+ /// If [matrix] is not null the result of the composing will be concatenated
+ /// to that [matrix], otherwise the identity matrix will be used.
+ ///
+ static Matrix4 compose(Matrix4? matrix, Matrix4? translationMatrix,
+ Matrix4? scaleMatrix, Matrix4? rotationMatrix) {
+ matrix ??= Matrix4.identity();
+ if (translationMatrix != null) matrix = translationMatrix * matrix;
+ if (scaleMatrix != null) matrix = scaleMatrix * matrix;
+ if (rotationMatrix != null) matrix = rotationMatrix * matrix;
+ return matrix!;
+ }
+
+ ///
+ /// Decomposes [matrix] into [MatrixDecomposedValues.translation],
+ /// [MatrixDecomposedValues.scale] and [MatrixDecomposedValues.rotation] components.
+ ///
+ static MatrixDecomposedValues decomposeToValues(Matrix4 matrix) {
+ var array = matrix.applyToVector3Array([0, 0, 0, 1, 0, 0]);
+ Offset translation = Offset(array[0], array[1]);
+ Offset delta = Offset(array[3] - array[0], array[4] - array[1]);
+ double scale = delta.distance;
+ double rotation = delta.direction;
+ return MatrixDecomposedValues(translation, scale, rotation);
+ }
+}
+
+class _MatrixGestureDetectorState extends State {
+ Matrix4 translationDeltaMatrix = Matrix4.identity();
+ Matrix4 scaleDeltaMatrix = Matrix4.identity();
+ Matrix4 rotationDeltaMatrix = Matrix4.identity();
+ Matrix4 matrix = Matrix4.identity();
+
+ @override
+ Widget build(BuildContext context) {
+ Widget child =
+ widget.clipChild ? ClipRect(child: widget.child) : widget.child;
+ return GestureDetector(
+ behavior: widget.behavior,
+ onScaleStart: onScaleStart,
+ onScaleUpdate: onScaleUpdate,
+ child: child,
+ );
+ }
+
+ _ValueUpdater translationUpdater = _ValueUpdater(
+ value: Offset.zero,
+ onUpdate: (oldVal, newVal) => newVal - oldVal,
+ );
+ _ValueUpdater scaleUpdater = _ValueUpdater(
+ value: 1.0,
+ onUpdate: (oldVal, newVal) => newVal / oldVal,
+ );
+ _ValueUpdater rotationUpdater = _ValueUpdater(
+ value: 0.0,
+ onUpdate: (oldVal, newVal) => newVal - oldVal,
+ );
+
+ void onScaleStart(ScaleStartDetails details) {
+ translationUpdater.value = details.focalPoint;
+ scaleUpdater.value = 1.0;
+ rotationUpdater.value = 0.0;
+ }
+
+ void onScaleUpdate(ScaleUpdateDetails details) {
+ translationDeltaMatrix = Matrix4.identity();
+ scaleDeltaMatrix = Matrix4.identity();
+ rotationDeltaMatrix = Matrix4.identity();
+
+ // handle matrix translating
+ if (widget.shouldTranslate) {
+ Offset translationDelta = translationUpdater.update(details.focalPoint);
+ translationDeltaMatrix = _translate(translationDelta);
+ matrix = translationDeltaMatrix * matrix;
+ }
+
+ final focalPointAlignment = widget.focalPointAlignment;
+ final focalPoint = focalPointAlignment == null
+ ? details.localFocalPoint
+ : focalPointAlignment.alongSize(context.size!);
+
+ // handle matrix scaling
+ if (widget.shouldScale && details.scale != 1.0) {
+ double scaleDelta = scaleUpdater.update(details.scale);
+ scaleDeltaMatrix = _scale(scaleDelta, focalPoint);
+ matrix = scaleDeltaMatrix * matrix;
+ }
+
+ // handle matrix rotating
+ if (widget.shouldRotate && details.rotation != 0.0) {
+ double rotationDelta = rotationUpdater.update(details.rotation);
+ rotationDeltaMatrix = _rotate(rotationDelta, focalPoint);
+ matrix = rotationDeltaMatrix * matrix;
+ }
+
+ widget.onMatrixUpdate(
+ matrix, translationDeltaMatrix, scaleDeltaMatrix, rotationDeltaMatrix);
+ }
+
+ Matrix4 _translate(Offset translation) {
+ var dx = translation.dx;
+ var dy = translation.dy;
+
+ // ..[0] = 1 # x scale
+ // ..[5] = 1 # y scale
+ // ..[10] = 1 # diagonal "one"
+ // ..[12] = dx # x translation
+ // ..[13] = dy # y translation
+ // ..[15] = 1 # diagonal "one"
+ return Matrix4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, dx, dy, 0, 1);
+ }
+
+ Matrix4 _scale(double scale, Offset focalPoint) {
+ var dx = (1 - scale) * focalPoint.dx;
+ var dy = (1 - scale) * focalPoint.dy;
+
+ // ..[0] = scale # x scale
+ // ..[5] = scale # y scale
+ // ..[10] = 1 # diagonal "one"
+ // ..[12] = dx # x translation
+ // ..[13] = dy # y translation
+ // ..[15] = 1 # diagonal "one"
+ return Matrix4(scale, 0, 0, 0, 0, scale, 0, 0, 0, 0, 1, 0, dx, dy, 0, 1);
+ }
+
+ Matrix4 _rotate(double angle, Offset focalPoint) {
+ var c = cos(angle);
+ var s = sin(angle);
+ var dx = (1 - c) * focalPoint.dx + s * focalPoint.dy;
+ var dy = (1 - c) * focalPoint.dy - s * focalPoint.dx;
+
+ // ..[0] = c # x scale
+ // ..[1] = s # y skew
+ // ..[4] = -s # x skew
+ // ..[5] = c # y scale
+ // ..[10] = 1 # diagonal "one"
+ // ..[12] = dx # x translation
+ // ..[13] = dy # y translation
+ // ..[15] = 1 # diagonal "one"
+ return Matrix4(c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, dx, dy, 0, 1);
+ }
+}
+
+typedef _OnUpdate = T Function(T oldValue, T newValue);
+
+class _ValueUpdater {
+ final _OnUpdate onUpdate;
+ T value;
+
+ _ValueUpdater({
+ required this.value,
+ required this.onUpdate,
+ });
+
+ T update(T newValue) {
+ T updated = onUpdate(value, newValue);
+ value = newValue;
+ return updated;
+ }
+}
+
+class MatrixDecomposedValues {
+ /// Translation, in most cases useful only for matrices that are nothing but
+ /// a translation (no scale and no rotation).
+ final Offset translation;
+
+ /// Scaling factor.
+ final double scale;
+
+ /// Rotation in radians, (-pi..pi) range.
+ final double rotation;
+
+ MatrixDecomposedValues(this.translation, this.scale, this.rotation);
+
+ @override
+ String toString() {
+ return 'MatrixDecomposedValues(translation: $translation, scale: ${scale.toStringAsFixed(3)}, rotation: ${rotation.toStringAsFixed(3)})';
+ }
+}
\ No newline at end of file
diff --git a/lib/src/utils/utils.dart b/lib/src/utils/utils.dart
new file mode 100644
index 0000000..b8878f4
--- /dev/null
+++ b/lib/src/utils/utils.dart
@@ -0,0 +1,147 @@
+import 'dart:io';
+import 'dart:typed_data';
+import 'dart:ui' as ui;
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter_story_editor/src/theme/style.dart';
+import 'package:image_cropper/image_cropper.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:video_thumbnail/video_thumbnail.dart';
+
+Future generateThumbnail(File? file) async {
+ // If the thumbnail is already generated, just return.
+ if (file == null) {
+ return null;
+ }
+
+ // Generate the thumbnail.
+ Uint8List? thumbnail;
+ if (file.path.endsWith('.mp4') ||
+ file.path.endsWith('.mov') ||
+ file.path.endsWith('.avi')) {
+ thumbnail = await VideoThumbnail.thumbnailData(
+ video: file.path,
+ imageFormat: ImageFormat.JPEG,
+ maxWidth: 128,
+ quality: 15,
+ );
+ }
+
+ // return thumbnail.
+
+ return thumbnail;
+}
+
+
+
+Future convertWidgetToImage(GlobalKey key) async {
+ RenderRepaintBoundary? repaintBoundary =
+ key.currentContext?.findRenderObject() as RenderRepaintBoundary?;
+
+ if (repaintBoundary != null) {
+ ui.Image boxImage = await repaintBoundary.toImage(pixelRatio: 3.0);
+ ByteData? byteData =
+ await boxImage.toByteData(format: ui.ImageByteFormat.png);
+
+ if (byteData != null) {
+ Uint8List uint8list = byteData.buffer.asUint8List();
+ // Write the bytes to a file.
+ final tempDir = await getTemporaryDirectory();
+ final file = await File('${tempDir.path}/image${DateTime.now().millisecondsSinceEpoch}.png').create();
+ await file.writeAsBytes(uint8list);
+
+ return file;
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+}
+
+Future?> convertWidgetsToImages(List keys) async {
+ List files = [];
+
+ for (GlobalKey key in keys) {
+ RenderRepaintBoundary? repaintBoundary =
+ key.currentContext?.findRenderObject() as RenderRepaintBoundary?;
+
+ if (repaintBoundary != null) {
+ ui.Image boxImage = await repaintBoundary.toImage(pixelRatio: 3.0);
+ ByteData? byteData =
+ await boxImage.toByteData(format: ui.ImageByteFormat.png);
+
+ if (byteData != null) {
+ Uint8List uint8list = byteData.buffer.asUint8List();
+ final tempDir = await getTemporaryDirectory();
+ final file = await File('${tempDir.path}/image${DateTime.now().millisecondsSinceEpoch}.png').create();
+ await file.writeAsBytes(uint8list);
+ files.add(file);
+ } else {
+ throw Exception("ByteData is null for key: ${key.toString()}");
+ }
+ } else {
+ throw Exception("RepaintBoundary is null for key: ${key.toString()}");
+ }
+ }
+
+ return files;
+}
+
+
+Future cropImage(BuildContext context,
+ {required File file}) async {
+ CroppedFile? croppedFile = await ImageCropper.platform.cropImage(
+ sourcePath: file.path,
+ aspectRatioPresets: Platform.isAndroid
+ ? [
+ CropAspectRatioPreset.square,
+ CropAspectRatioPreset.ratio3x2,
+ CropAspectRatioPreset.original,
+ CropAspectRatioPreset.ratio4x3,
+ CropAspectRatioPreset.ratio16x9
+ ]
+ : [
+ CropAspectRatioPreset.original,
+ CropAspectRatioPreset.square,
+ CropAspectRatioPreset.ratio3x2,
+ CropAspectRatioPreset.ratio4x3,
+ CropAspectRatioPreset.ratio5x3,
+ CropAspectRatioPreset.ratio5x4,
+ CropAspectRatioPreset.ratio7x5,
+ CropAspectRatioPreset.ratio16x9
+ ],
+ uiSettings: [
+ AndroidUiSettings(
+ toolbarTitle: 'Crop Image',
+ toolbarColor: darkGreenColor,
+ toolbarWidgetColor: Colors.white,
+ activeControlsWidgetColor: tealColor,
+ initAspectRatio: CropAspectRatioPreset.original,
+ lockAspectRatio: false),
+ IOSUiSettings(
+ title: 'Crop Image',
+ ),
+ WebUiSettings(
+ context: context,
+ ),
+ ]);
+
+ if (croppedFile != null) {
+ return croppedFile;
+ } else {
+ return null;
+ }
+}
+
+bool isVideo(File file) {
+ if (file.path.endsWith('.mp4') ||
+ file.path.endsWith('.mov') ||
+ file.path.endsWith('.avi')) {
+ return true;
+ } else {
+ return false;
+ }
+}
+
+
diff --git a/lib/src/views/main_control_views/caption_view.dart b/lib/src/views/main_control_views/caption_view.dart
new file mode 100644
index 0000000..f4aaf40
--- /dev/null
+++ b/lib/src/views/main_control_views/caption_view.dart
@@ -0,0 +1,93 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_story_editor/src/theme/style.dart';
+import 'package:font_awesome_flutter/font_awesome_flutter.dart';
+
+class CaptionView extends StatelessWidget {
+ final TextEditingController captionController;
+ final VoidCallback onSaveClickListener;
+ final FocusNode? focusNode;
+ final bool isSaving;
+ const CaptionView(
+ {super.key,
+ required this.captionController,
+ required this.onSaveClickListener,
+ required this.isSaving, this.focusNode});
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ Container(
+ margin: const EdgeInsets.symmetric(horizontal: 10),
+ width: double.infinity,
+ height: 50,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(25), color: darkGreenColor),
+ child: TextFormField(
+ focusNode: focusNode,
+ controller: captionController,
+ style: const TextStyle(fontSize: 18),
+ cursorColor: tealColor,
+ decoration: const InputDecoration(
+ border: InputBorder.none,
+ prefixIcon: Icon(
+ Icons.emoji_emotions_outlined,
+ color: Colors.white,
+ size: 28,
+ ),
+ hintText: "Add a caption...",
+ contentPadding: EdgeInsets.symmetric(vertical: 15),
+ hintStyle: TextStyle(color: Colors.white)),
+ ),
+ ),
+ Container(
+ margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(20),
+ color: darkGreenColor,
+ ),
+ child: const Row(
+ children: [
+ Icon(FontAwesomeIcons.circle, color: Colors.white,),
+ SizedBox(
+ width: 5,
+ ),
+ Text("Status (Contacts)", style: TextStyle(color: Colors.white),)
+ ],
+ ),
+ ),
+ GestureDetector(
+ onTap: onSaveClickListener,
+ child: Container(
+ width: 45,
+ height: 45,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(25), color: tealColor),
+ child: Center(
+ child: isSaving
+ ? const SizedBox(
+ width: 20,
+ height: 20,
+ child: CircularProgressIndicator(
+ color: Colors.white,
+ ),
+ )
+ : const Icon(
+ Icons.send_outlined,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/src/views/main_control_views/filter_text_view.dart b/lib/src/views/main_control_views/filter_text_view.dart
new file mode 100644
index 0000000..c30ecde
--- /dev/null
+++ b/lib/src/views/main_control_views/filter_text_view.dart
@@ -0,0 +1,62 @@
+
+import 'package:flutter/material.dart';
+import 'package:flutter_story_editor/src/enums/story_editing_modes.dart';
+
+class FilterTextView extends StatefulWidget {
+
+ StoryEditingModes showFilters = StoryEditingModes.NONE;
+ final Function(StoryEditingModes) onChange;
+ FilterTextView({super.key, this.showFilters = StoryEditingModes.NONE, required this.onChange});
+
+ @override
+ State createState() => _FilterTextViewState();
+}
+
+class _FilterTextViewState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onVerticalDragUpdate: (details) {
+ if (details.delta.dy < 0) {
+ setState(() {
+ widget.showFilters =
+ StoryEditingModes.FILTERS; // show filters when swiped up
+ });
+ widget.onChange(widget.showFilters);
+ } else if (details.delta.dy > 0) {
+ setState(() {
+ widget.showFilters =
+ StoryEditingModes.FILTERS; // hide filters when swiped down
+ });
+ widget.onChange(widget.showFilters);
+ }
+ },
+ child: AnimatedOpacity(
+ duration: const Duration(milliseconds: 200),
+ opacity: widget.showFilters ==StoryEditingModes.FILTERS ? 0 : 1,
+ child: AnimatedContainer(
+ height: widget.showFilters ==StoryEditingModes.FILTERS
+ ? 100
+ : 50, // change height based on showFilters
+ duration: const Duration(milliseconds: 300),
+ child: const Column(
+ children: [
+ Icon(
+ Icons.keyboard_arrow_up,
+ size: 25,
+ color: Colors.white,
+ ),
+ Text(
+ "Filters",
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w400,
+ color: Colors.white),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/views/main_control_views/filters_view.dart b/lib/src/views/main_control_views/filters_view.dart
new file mode 100644
index 0000000..566a8e4
--- /dev/null
+++ b/lib/src/views/main_control_views/filters_view.dart
@@ -0,0 +1,113 @@
+
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_story_editor/src/const/const.dart';
+import 'package:flutter_story_editor/src/theme/style.dart';
+
+class FiltersView extends StatefulWidget {
+
+ List> selectedFilters = [];
+ final List? selectedFiles;
+ final int currentPageIndex;
+ final Function(List) onFilterChange;
+
+ FiltersView({super.key, this.selectedFilters = const [], this.selectedFiles, required this.currentPageIndex, required this.onFilterChange});
+
+ @override
+ State createState() => _FiltersViewState();
+}
+
+class _FiltersViewState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ height: 120,
+ decoration: const BoxDecoration(color: darkGreenColor),
+ child: ListView.builder(
+ padding: const EdgeInsets.only(right: 10, left: 5),
+ itemCount: Consts.filters.length,
+ scrollDirection: Axis.horizontal,
+ itemBuilder: (context, index) {
+ return GestureDetector(
+ onTap: () {
+ setState(() {
+ widget.selectedFilters[widget.currentPageIndex] =
+ Consts.filters[index];
+ });
+ widget.onFilterChange(widget.selectedFilters[widget.currentPageIndex]);
+ },
+ child: Container(
+ margin: const EdgeInsets.only(
+ top: 10, bottom: 10, left: 8),
+ width: 65,
+ height: 100,
+ child: Stack(
+ children: [
+ Positioned(
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ child: ColorFiltered(
+ colorFilter: ColorFilter.matrix(
+ Consts.filters[index]),
+ child: Image.file(
+ widget.selectedFiles![widget.currentPageIndex],
+ fit: BoxFit.cover,
+ ),
+ ),
+ ),
+ widget.selectedFilters[widget.currentPageIndex] ==
+ Consts.filters[index]
+ ? Align(
+ alignment: Alignment.topRight,
+ child: Container(
+ width: 20,
+ height: 20,
+ margin: const EdgeInsets.symmetric(
+ horizontal: 5,
+ vertical: 5),
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ border: Border.all(
+ width: 1.5,
+ color: Colors.black),
+ color: tealColor,
+ ),
+ child: const Center(
+ child: Icon(
+ Icons.done,
+ size: 15,
+ color: Colors.black,
+ ),
+ ),
+ ),
+ )
+ : Container(),
+ Align(
+ alignment: Alignment.bottomCenter,
+ child: Container(
+ width: 70,
+ height: 25,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 5, vertical: 5),
+ decoration: BoxDecoration(
+ color: Colors.black
+ .withOpacity(.4)),
+ child: Text(
+ Consts.filterNames[index],
+ style: const TextStyle(
+ fontWeight: FontWeight.bold, color: Colors.white),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ ),
+ );
+ }
+}
diff --git a/lib/src/views/main_control_views/image_view.dart b/lib/src/views/main_control_views/image_view.dart
new file mode 100644
index 0000000..514668a
--- /dev/null
+++ b/lib/src/views/main_control_views/image_view.dart
@@ -0,0 +1,79 @@
+import 'dart:io';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_story_editor/src/const/filters.dart';
+import 'package:flutter_story_editor/src/controller/controller.dart';
+import 'package:flutter_story_editor/src/models/simple_sketecher.dart';
+import 'package:flutter_story_editor/src/models/stroke.dart';
+import 'package:flutter_story_editor/src/widgets/draggable_sticker_widget.dart';
+import 'package:flutter_story_editor/src/widgets/draggable_text_widget.dart';
+
+class ImageView extends StatefulWidget {
+ final File file;
+ final List? filter;
+ final FlutterStoryEditorController controller;
+ final List lines;
+ final int storyIndex;
+
+ final List> textList;
+ final List> stickerList;
+
+ const ImageView(
+ {super.key,
+ required this.file,
+ this.filter,
+ required this.controller,
+ required this.lines, required this.textList, required this.storyIndex, required this.stickerList});
+
+ @override
+ State createState() => _ImageViewState();
+}
+
+class _ImageViewState extends State {
+
+ @override
+ Widget build(BuildContext context) {
+
+ return Stack(
+
+ alignment: Alignment.center,
+ children: [
+
+ Row(
+ children: [
+ Expanded(child: ColorFiltered(colorFilter: ColorFilter.matrix(widget.filter ?? NO_FILTER),child: Image.file(widget.file, fit: BoxFit.cover))),
+ ],
+ ),
+
+ CustomPaint(
+ painter: SimpleSketcher(widget.lines),
+ child: Container(),
+ ),
+
+ ...widget.stickerList[widget.storyIndex].map((draggableStickerWidget) {
+ return draggableStickerWidget;
+ }),
+
+ ...widget.textList[widget.storyIndex].map((draggableTextWidget) {
+ return draggableTextWidget;
+ }),
+
+ ],
+ );
+ }
+
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/src/views/main_control_views/main_controls_view.dart b/lib/src/views/main_control_views/main_controls_view.dart
new file mode 100644
index 0000000..d0d9580
--- /dev/null
+++ b/lib/src/views/main_control_views/main_controls_view.dart
@@ -0,0 +1,233 @@
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_story_editor/src/controller/controller.dart';
+import 'package:flutter_story_editor/src/enums/story_editing_modes.dart';
+import 'package:flutter_story_editor/src/models/stroke.dart';
+import 'package:flutter_story_editor/src/utils/utils.dart';
+import 'package:flutter_story_editor/src/views/main_control_views/filters_view.dart';
+import 'package:flutter_story_editor/src/widgets/draggable_sticker_widget.dart';
+import 'package:flutter_story_editor/src/widgets/draggable_text_widget.dart';
+
+import 'caption_view.dart';
+import 'filter_text_view.dart';
+import 'thumbnail_view.dart';
+import 'top_view.dart';
+
+class MainControlsView extends StatefulWidget {
+ final List? selectedFiles;
+ final VoidCallback? onSaveClickListener;
+ final TextEditingController? captionController;
+ final FlutterStoryEditorController controller;
+ final List uiViewEditableFiles;
+ final List> selectedFilters;
+ final List> textList;
+ final List> stickerList;
+ final Function(List) onFilterChange;
+ final Function(File) onImageCrop;
+ final VoidCallback onUndoClickListener;
+ final VoidCallback onPaintClickListener;
+ final VoidCallback onTextClickListener;
+ final VoidCallback onStickersClickListener;
+ final PageController pageController;
+ final int currentPageIndex;
+ final List lines;
+ final bool isSaving;
+ final bool isFocused;
+ final FocusNode? captionFocusNode;
+
+ const MainControlsView(
+ {super.key,
+ this.selectedFiles,
+ this.onSaveClickListener,
+ this.captionController,
+ required this.controller,
+ required this.isFocused,
+ required this.uiViewEditableFiles,
+ required this.selectedFilters,
+ required this.onFilterChange,
+ required this.onImageCrop,
+ required this.onUndoClickListener,
+ required this.pageController,
+ required this.currentPageIndex,
+ required this.onPaintClickListener,
+ required this.onTextClickListener,
+ required this.lines,
+ required this.isSaving,
+ required this.textList,
+ this.captionFocusNode,
+ required this.onStickersClickListener, required this.stickerList,
+ });
+
+ @override
+ State createState() => _MainControlsViewState();
+}
+
+class _MainControlsViewState extends State {
+ final Map _thumbnails = {};
+
+ List? originalFiles;
+
+ static const double maxVideoSizeMB = 50; // Maximum allowed video size in MB
+ static const double maxImageSizeMB = 5; // Maximum allowed image size in MB
+
+ void generateVideoFilesThumbnails() async {
+ for (var file in widget.selectedFiles ?? []) {
+ if (isVideo(file)) {
+ var generatedThumbnail = await generateThumbnail(file);
+ if (mounted) {
+ setState(() {
+ _thumbnails[file] = generatedThumbnail;
+ });
+ }
+ }
+ }
+ }
+
+ @override
+ void initState() {
+ super.initState();
+
+ generateVideoFilesThumbnails();
+
+ for (var file in widget.selectedFiles!) {
+ final bytes = file.readAsBytesSync();
+ final sizeInMB = bytes.lengthInBytes / (1024 * 1024);
+
+ if (isVideo(file) && sizeInMB > maxVideoSizeMB) {
+ // Handle large video
+ Navigator.pop(context);
+ // Perhaps remove from the list or show a dialog
+ }
+
+ if (!isVideo(file) && sizeInMB > maxImageSizeMB) {
+ // Handle large image
+ Navigator.pop(context);
+ // Perhaps remove from the list or show a dialog
+ }
+ }
+
+ originalFiles = List.from(widget.selectedFiles!);
+ }
+
+ void _cropImage(BuildContext context) {
+ cropImage(
+ context,
+ file: originalFiles![widget.currentPageIndex],
+ ).then((croppedFile) async {
+ if (croppedFile != null) {
+ File croppedImage = File(croppedFile.path);
+ widget.onImageCrop(croppedImage);
+ }
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Stack(
+ alignment: Alignment.center,
+ children: [
+ for (File file in widget.selectedFiles!)
+ if (!(isVideo(file)))
+ Align(
+ alignment: Alignment.topCenter,
+ child: _buildTop(),
+ ),
+ Align(
+ alignment: Alignment.bottomCenter,
+ child: _buildBottom(),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildTop() {
+ return TopView(
+ stickerList: widget.stickerList,
+ textList: widget.textList,
+ controller: widget.controller,
+ lines: widget.lines,
+ onTextClickListener: () {
+ widget.onTextClickListener();
+ },
+ onStickersClickListener: () {
+ widget.onStickersClickListener();
+ },
+ onPaintClickListener: () {
+ widget.onPaintClickListener();
+ },
+ onUndoClickListener: widget.onUndoClickListener,
+ currentPageIndex: widget.currentPageIndex,
+ selectedFilters: widget.selectedFilters,
+ selectedFile: widget.selectedFiles![widget.currentPageIndex],
+ onTapCropListener: () {
+ _cropImage(context);
+ },
+ );
+ }
+
+ Widget _buildBottom() {
+ return AnimatedPadding(
+ duration: const Duration(milliseconds: 200),
+ padding: EdgeInsets.only(
+ bottom: MediaQuery.of(context).viewInsets.bottom,
+ ),
+ child: Column(
+ children: [
+ Expanded(
+ child: Container(),
+ ),
+ if(widget.isFocused == false)
+ isVideo(widget.selectedFiles![widget.currentPageIndex])
+ ? Container()
+ : FilterTextView(
+ showFilters: widget.controller.editingModeSelected,
+ onChange: (showFiltersView) {
+ widget.controller.setStoryEditingModeSelected = showFiltersView;
+ },
+ ),
+ const SizedBox(
+ height: 5,
+ ),
+ Column(
+ children: [
+ if(widget.isFocused == false)
+ ThumbnailView(
+ controller: widget.controller,
+ onThumbnailTapListener: (thumbnailItemIndex) {
+ widget.pageController.jumpToPage(thumbnailItemIndex);
+ },
+ currentPageIndex: widget.currentPageIndex,
+ thumbnails: _thumbnails,
+ selectedFiles: widget.uiViewEditableFiles,
+ selectedFilters: widget.selectedFilters,
+ ),
+ const SizedBox(
+ height: 10,
+ ),
+
+ if (widget.controller.editingModeSelected == StoryEditingModes.FILTERS)
+ FiltersView(
+ onFilterChange: (filter) {
+ widget.onFilterChange(filter);
+ },
+ selectedFilters: widget.selectedFilters,
+ currentPageIndex: widget.currentPageIndex,
+ selectedFiles: originalFiles,
+ )
+ else
+ if(widget.isFocused == false)
+ CaptionView(
+ focusNode: widget.captionFocusNode,
+ isSaving: widget.isSaving,
+ captionController: widget.captionController!,
+ onSaveClickListener: widget.onSaveClickListener!,
+ )
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/src/views/main_control_views/thumbnail_view.dart b/lib/src/views/main_control_views/thumbnail_view.dart
new file mode 100644
index 0000000..07bda57
--- /dev/null
+++ b/lib/src/views/main_control_views/thumbnail_view.dart
@@ -0,0 +1,183 @@
+import 'dart:io';
+import 'dart:math';
+import 'dart:typed_data';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_story_editor/src/controller/controller.dart';
+import 'package:flutter_story_editor/src/models/simple_sketecher.dart';
+import 'package:flutter_story_editor/src/models/stroke.dart';
+import 'package:flutter_story_editor/src/theme/style.dart';
+import 'package:flutter_story_editor/src/utils/utils.dart';
+import 'package:perfect_freehand/perfect_freehand.dart';
+
+class ThumbnailView extends StatefulWidget {
+ final List selectedFiles;
+ final int currentPageIndex;
+ final Map? thumbnails;
+ final List>? selectedFilters;
+ final Function(int) onThumbnailTapListener;
+ final FlutterStoryEditorController controller;
+ const ThumbnailView({
+ super.key,
+ required this.selectedFiles,
+ required this.currentPageIndex,
+ required this.onThumbnailTapListener,
+ this.thumbnails,
+ this.selectedFilters,
+ required this.controller,
+ });
+
+ @override
+ State createState() => _ThumbnailViewState();
+}
+
+class _ThumbnailViewState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 5.0),
+ child: Row(
+ children: widget.selectedFiles.map((e) {
+ int fileIndex = widget.selectedFiles.indexOf(e);
+ return Container(
+ width: 50,
+ height: 50,
+ decoration: BoxDecoration(
+ border: Border.all(
+ width: 1.5,
+ color: widget.currentPageIndex == fileIndex
+ ? tealColor
+ : Colors.transparent),
+ ),
+ child: GestureDetector(
+ onTap: () => widget.onThumbnailTapListener(fileIndex),
+ child: ThumbnailViewItem(
+ controller: widget.controller,
+ index: fileIndex,
+ image: e,
+ selectedFiles: widget.selectedFiles,
+ selectedFilters: widget.selectedFilters,
+ thumbnails: widget.thumbnails,
+ ),
+ ),
+ );
+ }).toList(),
+ )),
+ );
+ }
+}
+
+class ThumbnailViewItem extends StatefulWidget {
+ final File image;
+ final FlutterStoryEditorController controller;
+ final int index;
+ final Map? thumbnails;
+ final List>? selectedFilters;
+ final List? selectedFiles;
+ const ThumbnailViewItem(
+ {super.key,
+ required this.index,
+ this.thumbnails,
+ this.selectedFilters,
+ this.selectedFiles,
+ required this.image,
+ required this.controller});
+
+ @override
+ State createState() => _ThumbnailViewItemState();
+}
+
+class _ThumbnailViewItemState extends State {
+
+ @override
+ Widget build(BuildContext context) {
+
+
+ double scaleFactor = min(
+ 50.0 / MediaQuery.of(context).size.width,
+ 50.0 / MediaQuery.of(context).size.height,
+ );
+
+ return ValueListenableBuilder>>(
+ valueListenable: widget.controller.uiEditableFileLinesNotifier,
+ builder: (BuildContext context, List> lines, Widget? child) {
+
+
+ List scaledLines = lines[widget.index].map((line) {
+
+
+
+ return Stroke(
+ line.points.map((point) {
+ return PointVector(
+ point.x * scaleFactor * 1.8, point.y * scaleFactor * 0.9, point.pressure);
+ }).toList(),
+ line.color,
+ StrokeOptions(
+ size: 1
+ ),
+ );
+ }).toList();
+
+ if (isVideo(widget.image)) {
+ if (widget.thumbnails != null) {
+ return widget.thumbnails![widget.image] == null
+ ? Container()
+ : Stack(
+ children: [
+ Positioned(
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ child: Image.memory(
+ widget.thumbnails![widget.image]!,
+ fit: BoxFit.cover,
+ ),
+ ),
+ CustomPaint(
+
+ painter: SimpleSketcher(scaledLines),
+ child: Container(),
+ )
+ ],
+ );
+ } else {
+ return Container(); // If thumbnail is not ready yet, just display an empty container.
+ }
+ } else {
+ return ColorFiltered(
+ colorFilter:
+ ColorFilter.matrix(widget.selectedFilters![widget.index]),
+ child: Stack(
+ children: [
+ Positioned(
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ child: Image.file(
+ widget.selectedFiles != null
+ ? widget.selectedFiles![widget.index]
+ : widget.image,
+ fit: BoxFit.cover,
+ ),
+ ),
+ CustomPaint(
+ size: Size(
+ 50.0 * MediaQuery.of(context).size.width / MediaQuery.of(context).size.height,
+ 50.0,
+ ),
+ painter: SimpleSketcher(scaledLines),
+ child: Container(),
+ )
+ ],
+ ),
+ );
+ }
+ },
+ );
+ }
+}
diff --git a/lib/src/views/main_control_views/top_view.dart b/lib/src/views/main_control_views/top_view.dart
new file mode 100644
index 0000000..57cccb4
--- /dev/null
+++ b/lib/src/views/main_control_views/top_view.dart
@@ -0,0 +1,115 @@
+
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_story_editor/src/const/filters.dart';
+import 'package:flutter_story_editor/src/controller/controller.dart';
+import 'package:flutter_story_editor/src/models/stroke.dart';
+import 'package:flutter_story_editor/src/utils/utils.dart';
+import 'package:flutter_story_editor/src/widgets/draggable_sticker_widget.dart';
+import 'package:flutter_story_editor/src/widgets/draggable_text_widget.dart';
+class TopView extends StatefulWidget {
+ final File selectedFile;
+ final VoidCallback onTapCropListener;
+ final int currentPageIndex;
+ final List> selectedFilters;
+ final List> textList;
+ final List> stickerList;
+ final VoidCallback onUndoClickListener;
+ final VoidCallback onPaintClickListener;
+ final VoidCallback onTextClickListener;
+ final VoidCallback onStickersClickListener;
+ final List lines;
+ final FlutterStoryEditorController controller;
+ const TopView({super.key, required this.selectedFile, required this.onTapCropListener, required this.currentPageIndex, required this.selectedFilters, required this.onUndoClickListener, required this.onPaintClickListener, required this.lines, required this.controller, required this.onTextClickListener, required this.textList, required this.onStickersClickListener, required this.stickerList});
+
+ @override
+ State createState() => _TopViewState();
+}
+
+class _TopViewState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding:
+ EdgeInsets.symmetric(horizontal: 15, vertical: Platform.isIOS ? 60 : 40),
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ GestureDetector(
+ onTap: () {
+ Navigator.of(context).pop();
+ },
+ child: const Icon(
+ Icons.close_outlined,
+ size: 30,
+ color: Colors.white,
+ )),
+ if(!isVideo(widget.selectedFile))
+ Row(
+ children: [
+ widget.selectedFilters[widget.currentPageIndex] != NO_FILTER || widget.lines.isNotEmpty || widget.textList[widget.currentPageIndex].isNotEmpty || widget.stickerList[widget.currentPageIndex].isNotEmpty ? GestureDetector(
+ onTap: widget.onUndoClickListener,
+ child: const Icon(
+ Icons.undo,
+ size: 30,
+ color: Colors.white,
+ ),
+ ) : Container(),
+ const SizedBox(
+ width: 20,
+ ),
+ isVideo(widget.selectedFile)
+ ? const Text("")
+ : GestureDetector(
+ onTap:widget.onTapCropListener,
+ child: const Icon(
+ Icons.crop,
+ size: 30,
+ color: Colors.white,
+ )),
+ const SizedBox(
+ width: 20,
+ ),
+ GestureDetector(
+ onTap: widget.onStickersClickListener,
+ child: const Icon(
+ Icons.emoji_emotions_outlined,
+ size: 30,
+ color: Colors.white,
+ ),
+ ),
+ const SizedBox(
+ width: 20,
+ ),
+ GestureDetector(
+ onTap: widget.onTextClickListener,
+ child: const Icon(
+ Icons.title,
+ size: 30,
+ color: Colors.white,
+ ),
+ ),
+ const SizedBox(
+ width: 20,
+ ),
+ GestureDetector(
+ onTap: widget.onPaintClickListener,
+ child: const Icon(
+ Icons.edit_outlined,
+ size: 30,
+ color: Colors.white,
+ ),
+ ),
+ ],
+ )
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+
+}
diff --git a/lib/src/views/main_control_views/trimmer_view.dart b/lib/src/views/main_control_views/trimmer_view.dart
new file mode 100644
index 0000000..f2d491b
--- /dev/null
+++ b/lib/src/views/main_control_views/trimmer_view.dart
@@ -0,0 +1,193 @@
+import 'dart:async';
+import 'dart:io';
+import 'package:flutter/material.dart';
+import 'package:flutter_story_editor/src/models/simple_sketecher.dart';
+import 'package:flutter_story_editor/src/models/stroke.dart';
+import 'package:flutter_story_editor/src/theme/style.dart';
+
+import 'package:video_trimmer/video_trimmer.dart';
+
+class TrimmerView extends StatefulWidget {
+ final File file;
+ final int pageIndex;
+ final PageController pageController;
+ final Function(File) onTrimCompleted;
+ final bool? trimOnAdjust;
+ final List lines;
+ const TrimmerView(
+ {super.key,
+ required this.file,
+ required this.pageIndex,
+ required this.pageController,
+ required this.onTrimCompleted,
+ this.trimOnAdjust = false, required this.lines});
+
+ @override
+ TrimmerViewState createState() => TrimmerViewState();
+}
+
+class TrimmerViewState extends State
+ with AutomaticKeepAliveClientMixin {
+ @override
+ bool get wantKeepAlive => true;
+
+ double _startValue = 0.0;
+ double _endValue = 0.0;
+ final Trimmer _trimmer = Trimmer();
+
+ bool _isPlaying = false;
+ bool _progressVisibility = false;
+ Timer? _debounce;
+
+ Future _trimVideo() async {
+ setState(() {
+ _progressVisibility = true;
+ });
+
+ String? value;
+
+ await _trimmer.saveTrimmedVideo(
+ startValue: _startValue,
+ endValue: _endValue,
+ onSave: (value) {
+ widget.onTrimCompleted(File(value!));
+ }).then((value) {
+ setState(() {
+ _progressVisibility = false;
+ });
+ });
+
+ return value;
+ }
+
+ void _debounceTrim() {
+ if (_debounce?.isActive ?? false) _debounce?.cancel();
+ _debounce = Timer(const Duration(milliseconds: 500), () {
+ _trimVideo();
+ });
+ }
+
+ void _loadVideo() {
+ _trimmer.loadVideo(videoFile: widget.file);
+ }
+
+ @override
+ void initState() {
+ super.initState();
+
+ _loadVideo();
+
+ widget.pageController.addListener(() async {
+ if (widget.pageController.page!.round() != widget.pageIndex &&
+ _isPlaying) {
+ await _trimmer.videoPlaybackControl(
+ startValue: _startValue, endValue: _endValue); // Add this line
+ setState(() => _isPlaying = false);
+ }
+ });
+ }
+
+ @override
+ void dispose() {
+ _trimmer.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ super.build(context);
+
+ return Scaffold(
+ body: Builder(
+ builder: (context) => Center(
+ child: Container(
+ color: Colors.black,
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ VideoViewer(trimmer: _trimmer),
+ CustomPaint(
+ painter: SimpleSketcher(widget.lines),
+ child: Container(),
+ ),
+ Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ mainAxisSize: MainAxisSize.max,
+ children: [
+ Container(
+ margin: const EdgeInsets.only(top: 80, left: 6, right: 6),
+ child: Center(
+ child: TrimViewer(
+ editorProperties: const TrimEditorProperties(
+ ),
+ areaProperties: const TrimAreaProperties(),
+ trimmer: _trimmer,
+ viewerHeight: 50.0,
+ viewerWidth: MediaQuery.of(context).size.width,
+ maxVideoLength: const Duration(seconds: 30),
+ onChangeStart: (value) => _startValue = value,
+ onChangeEnd: (value) {
+ _endValue = value;
+ widget.trimOnAdjust == true
+ ? _debounceTrim()
+ : null;
+ },
+ onChangePlaybackState: (value) {
+ setState(() => _isPlaying = value);
+ }),
+ ),
+ ),
+ const SizedBox(height: 10),
+ if (widget.trimOnAdjust == false)
+ Column(
+ children: [
+ Visibility(
+ visible: _progressVisibility,
+ child: const LinearProgressIndicator(
+ backgroundColor: tealColor,
+ ),
+ ),
+ ElevatedButton(
+ onPressed: _progressVisibility
+ ? null
+ : () async {
+ _trimVideo();
+ },
+ child: const Text("SAVE"),
+ ),
+ ],
+ )
+ ],
+ ),
+
+ TextButton(
+ child: _isPlaying
+ ? Container()
+ : const Icon(
+ Icons.play_arrow,
+ size: 80.0,
+ color: Colors.white,
+ ),
+ onPressed: () async {
+ bool playbackState = await _trimmer.videoPlaybackControl(
+ startValue: _startValue,
+ endValue: _endValue,
+ );
+ if (mounted) {
+ setState(() {
+ _isPlaying = false;
+ });
+ }
+ },
+ ),
+
+
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
diff --git a/lib/src/views/paint_control_views/paint_controls_view.dart b/lib/src/views/paint_control_views/paint_controls_view.dart
new file mode 100644
index 0000000..fd2fbbc
--- /dev/null
+++ b/lib/src/views/paint_control_views/paint_controls_view.dart
@@ -0,0 +1,182 @@
+import 'dart:io';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_story_editor/src/controller/controller.dart';
+import 'package:flutter_story_editor/src/models/simple_sketecher.dart';
+import 'package:flutter_story_editor/src/models/stroke.dart';
+import 'package:flutter_story_editor/src/widgets/hue_color_picker_slider.dart';
+import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
+import 'package:perfect_freehand/perfect_freehand.dart';
+
+import 'paint_top_view.dart';
+
+class PaintControlsView extends StatefulWidget {
+ final File selectedFile;
+ final List uiEditableFileLines;
+ final VoidCallback onUndoClickListener;
+ final Function(Stroke) onPointerDownUpdate;
+ final FlutterStoryEditorController controller;
+ final VoidCallback onDoneClickListener;
+
+ const PaintControlsView(
+ {super.key,
+ required this.selectedFile,
+ required this.controller,
+ required this.uiEditableFileLines,
+ required this.onPointerDownUpdate,
+ required this.onUndoClickListener,
+ required this.onDoneClickListener});
+
+ @override
+ PaintControlsViewState createState() => PaintControlsViewState();
+}
+
+class PaintControlsViewState extends State {
+ HSVColor _pencilColor = HSVColor.fromColor(Colors.tealAccent);
+ Stroke? line;
+
+ double size = 3;
+
+ void onPointerDown(PointerDownEvent details) {
+ final box = context.findRenderObject() as RenderBox;
+ final offset = box.globalToLocal(details.position);
+ final point = details.kind == PointerDeviceKind.stylus
+ ? PointVector(
+ offset.dx,
+ offset.dy / 2,
+ (details.pressure - details.pressureMin) / (details.pressureMax - details.pressureMin),
+ )
+ : PointVector(offset.dx, offset.dy);
+ final points = [point];
+ line = Stroke(points, _pencilColor.toColor(), StrokeOptions(size: size));
+ setState(() {
+ widget.uiEditableFileLines.add(line!);
+ });
+ }
+
+ void onPointerMove(PointerMoveEvent details) {
+ final box = context.findRenderObject() as RenderBox;
+ final offset = box.globalToLocal(details.position);
+ final point = details.kind == PointerDeviceKind.stylus
+ ? PointVector(
+ offset.dx,
+ offset.dy,
+ (details.pressure - details.pressureMin) / (details.pressureMax - details.pressureMin),
+ )
+ : PointVector(offset.dx, offset.dy);
+ setState(() {
+ line!.points.add(point);
+ });
+ }
+
+ void onPointerUp(PointerUpEvent details) {
+ widget.onPointerDownUpdate(line!);
+ line = null;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Stack(
+ alignment: Alignment.center,
+ children: [
+ Listener(
+ onPointerDown: onPointerDown,
+ onPointerUp: onPointerUp,
+ onPointerMove: onPointerMove,
+ child: CustomPaint(
+ painter: SimpleSketcher(widget.uiEditableFileLines),
+ child: Container(),
+ ),
+ ),
+ Align(
+ alignment: Alignment.topCenter,
+ child: PaintTopView(
+ lines: widget.uiEditableFileLines,
+ onDoneClickListener: () {
+ widget.onDoneClickListener();
+ },
+ controller: widget.controller,
+ onUndoClickListener: widget.onUndoClickListener,
+ selectedFile: widget.selectedFile,
+ pencilColor: _pencilColor,
+ ),
+ ),
+ Align(
+ alignment: Alignment.bottomCenter,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 20.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ bottomIcon(MdiIcons.sizeM, isSelected: size == 3, onTap: () {
+ setState(() {
+ size = 3;
+ });
+ }),
+ bottomIcon(MdiIcons.sizeL, isSelected: size == 6,onTap: () {
+ setState(() {
+ size = 6;
+ });
+ }),
+ bottomIcon(MdiIcons.sizeXl, isSelected: size == 9,onTap: () {
+ setState(() {
+ size = 9;
+ });
+ }),
+ bottomIcon(MdiIcons.sizeXxl, isSelected: size == 12,onTap: () {
+ setState(() {
+ size = 12;
+ });
+ }),
+ ],
+ ),
+ ),
+ ),
+ Positioned(
+ top: 100,
+ right: 28,
+ child: HueColorPickerSlider(
+ onChanged: (hsvColor) {
+ setState(() {
+ _pencilColor = hsvColor;
+ });
+ },
+ ),
+ ),
+ ],
+ );
+ }
+
+ bottomIcon(IconData icon, {VoidCallback? onTap, bool? isSelected}) {
+ return GestureDetector(
+ onTap: onTap,
+ child: Container(
+ width: 40,
+ height: 40,
+ decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: Colors.white.withOpacity(.2), border: isSelected == true ? Border.all(width: 1, color: Colors.white) : null),
+ child: Icon(
+ icon,
+ size: 30,
+ color: Colors.white,
+ ),
+ ),
+ );
+ }
+}
+
+// Align(
+// alignment: Alignment.center,
+// child: LayoutBuilder(
+// builder: (BuildContext context, BoxConstraints constraints) {
+// final paintingAreaWidth = constraints.maxWidth * 0.75; // 75% of screen width
+// final paintingAreaHeight = constraints.maxHeight * 0.75; // 75% of screen height
+// final offsetX = (constraints.maxWidth - paintingAreaWidth) / 2; // horizontal offset
+// final offsetY = (constraints.maxHeight - paintingAreaHeight) / 2; // vertical offset
+// return Container(
+// width: paintingAreaWidth,
+// height: paintingAreaHeight,
+// child:
+// );
+// },
+// ),
+// ),
diff --git a/lib/src/views/paint_control_views/paint_top_view.dart b/lib/src/views/paint_control_views/paint_top_view.dart
new file mode 100644
index 0000000..0c512b5
--- /dev/null
+++ b/lib/src/views/paint_control_views/paint_top_view.dart
@@ -0,0 +1,85 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_story_editor/src/controller/controller.dart';
+import 'package:flutter_story_editor/src/models/stroke.dart';
+
+class PaintTopView extends StatefulWidget {
+ final File selectedFile;
+ final VoidCallback onUndoClickListener;
+ final FlutterStoryEditorController controller;
+ final HSVColor? pencilColor;
+ final VoidCallback onDoneClickListener;
+ final List lines;
+ const PaintTopView(
+ {super.key,
+ required this.selectedFile,
+ required this.onUndoClickListener,
+ required this.controller,
+ this.pencilColor, required this.onDoneClickListener, required this.lines,
+});
+
+ @override
+ State createState() => _PaintTopViewState();
+}
+
+class _PaintTopViewState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 40),
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ GestureDetector(
+ onTap: widget.onDoneClickListener,
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(width: 1, color: Colors.white)
+ ),
+ child: const Center(
+ child: Text("Done", style: TextStyle(fontSize: 15, color: Colors.white),),
+ ),
+ ),
+ ),
+ Row(
+ children: [
+ widget.lines.isNotEmpty ?GestureDetector(
+ onTap: widget.onUndoClickListener,
+ child: const Icon(
+ Icons.undo,
+ size: 30,
+ color: Colors.white,
+ ),
+ ) : Container(),
+ const SizedBox(
+ width: 20,
+ ),
+ GestureDetector(
+ child: Container(
+ width: 40,
+ height: 40,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(20),
+ color: widget.pencilColor!.toColor()
+ ),
+ child: const Icon(
+ Icons.edit_outlined,
+ size: 25,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ ],
+ )
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/src/views/sticker_control_views/sticker_control_view.dart b/lib/src/views/sticker_control_views/sticker_control_view.dart
new file mode 100644
index 0000000..2e5ceaa
--- /dev/null
+++ b/lib/src/views/sticker_control_views/sticker_control_view.dart
@@ -0,0 +1,142 @@
+import 'dart:ui';
+import 'package:flutter/material.dart';
+import 'package:flutter_story_editor/src/const/const.dart';
+import 'package:flutter_story_editor/src/controller/controller.dart';
+
+import 'sticker_top_view.dart';
+
+class StickerControlView extends StatefulWidget {
+ final FlutterStoryEditorController controller;
+ final Function(String) onStickerClickListener;
+ const StickerControlView({super.key, required this.controller, required this.onStickerClickListener});
+
+ @override
+ State createState() => _StickerControlViewState();
+}
+
+class _StickerControlViewState extends State {
+
+ bool isEmoji = false;
+ @override
+ Widget build(BuildContext context) {
+ return Stack(
+ children: [
+ BackdropFilter(
+ filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
+ child: Container(
+ color: Colors.black12.withOpacity(0.2),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(left: 20, right: 20.0, top: 60),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ StickerTopView(controller: widget.controller),
+ const SizedBox(
+ height: 20,
+ ),
+ Center(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Expanded(
+ child: GestureDetector(
+ onTap: () {
+ setState(() {
+ isEmoji = false;
+ });
+ },
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 15),
+ decoration: BoxDecoration(
+ color: isEmoji == false? Colors.white : const Color.fromRGBO(30, 36, 40, 1),
+ borderRadius: const BorderRadius.only(
+ topLeft: Radius.circular(40),
+ bottomLeft: Radius.circular(40),
+ ),
+ ),
+ child: Center(
+ child: Text("Stickers", style: TextStyle(color: isEmoji == false ? Colors.black : Colors.white, fontSize: 15),),
+ ),
+ ),
+ ),
+ ),
+ Expanded(
+ child: GestureDetector(
+ onTap: () {
+ setState(() {
+ isEmoji = true;
+ });
+ },
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 15),
+ decoration: BoxDecoration(
+ color: isEmoji == true ? Colors.white : const Color.fromRGBO(30, 36, 40, 1),
+ borderRadius: const BorderRadius.only(
+ topRight: Radius.circular(40),
+ bottomRight: Radius.circular(40),
+ ),
+ ),
+ child: Center(
+ child: Text("Emoji", style: TextStyle(color: isEmoji == true ? Colors.black : Colors.white, fontSize: 15),),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(
+ height: 20,
+ ),
+ Text(
+ isEmoji == true ? "Emojis" :"Cuppy",
+ style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.grey),
+ ),
+ const SizedBox(height: 20),
+ if(isEmoji == false)
+ Expanded(
+ child: GridView.builder(
+ gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: 4, mainAxisSpacing: 20, crossAxisSpacing: 20, childAspectRatio: 1.2),
+ physics: const ScrollPhysics(),
+ itemCount: Consts.stickers.length,
+ itemBuilder: (context, index) {
+ final String sticker = Consts.stickers[index];
+
+ return GestureDetector(
+ onTap: () {
+ widget.onStickerClickListener("assets/images/$sticker");
+ },
+ child: Image.asset("assets/images/$sticker"),
+ );
+ },
+ ),
+ )
+ else
+ Expanded(
+ child: GridView.builder(
+ gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: 4, mainAxisSpacing: 20, crossAxisSpacing: 20, childAspectRatio: 1.2),
+ physics: const ScrollPhysics(),
+ itemCount: Consts.emojies.length,
+ itemBuilder: (context, index) {
+ final String emoji = Consts.emojies[index];
+
+ return GestureDetector(
+ onTap: () {
+ widget.onStickerClickListener("assets/emojies/$emoji");
+ },
+ child: Image.asset("assets/emojies/$emoji"),
+ );
+ },
+ ),
+ )
+ ],
+ ),
+ )
+ ],
+ );
+ }
+}
diff --git a/lib/src/views/sticker_control_views/sticker_top_view.dart b/lib/src/views/sticker_control_views/sticker_top_view.dart
new file mode 100644
index 0000000..8c7ae6e
--- /dev/null
+++ b/lib/src/views/sticker_control_views/sticker_top_view.dart
@@ -0,0 +1,33 @@
+
+import 'package:flutter/material.dart';
+import 'package:flutter_story_editor/src/controller/controller.dart';
+import 'package:flutter_story_editor/src/enums/story_editing_modes.dart';
+import 'package:flutter_story_editor/src/theme/style.dart';
+
+class StickerTopView extends StatelessWidget {
+ final FlutterStoryEditorController controller;
+ const StickerTopView({super.key, required this.controller});
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ GestureDetector(onTap: () {
+ controller.setStoryEditingModeSelected = StoryEditingModes.NONE;
+ },child: const Icon(Icons.arrow_back, size: 25, color: Colors.white,)),
+ Container(
+ width: 40,
+ height: 40,
+ decoration: const BoxDecoration(
+ shape: BoxShape.circle,
+ color: tealColor,
+ ),
+ child: const Center(
+ child: Icon(Icons.emoji_emotions_outlined, color: Colors.white,),
+ ),
+ )
+ ],
+ );
+ }
+}
diff --git a/lib/src/views/text_control_views/text_control_view.dart b/lib/src/views/text_control_views/text_control_view.dart
new file mode 100644
index 0000000..bbc5c00
--- /dev/null
+++ b/lib/src/views/text_control_views/text_control_view.dart
@@ -0,0 +1,27 @@
+
+import 'package:flutter/material.dart';
+import 'package:flutter_story_editor/src/controller/controller.dart';
+
+import 'text_top_view.dart';
+class TextControlView extends StatelessWidget {
+ final FlutterStoryEditorController controller;
+ final VoidCallback? onAlignChangeClickListener;
+ final IconData? icon;
+ const TextControlView({super.key, required this.controller, this.onAlignChangeClickListener, this.icon});
+
+ @override
+ Widget build(BuildContext context) {
+ return Stack(
+ children: [
+ Align(
+ alignment: Alignment.topCenter,
+ child: TextTopView(
+ icon: icon,
+ onAlignChangeClickListener: onAlignChangeClickListener,
+ controller: controller,
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/src/views/text_control_views/text_top_view.dart b/lib/src/views/text_control_views/text_top_view.dart
new file mode 100644
index 0000000..fc4b0c8
--- /dev/null
+++ b/lib/src/views/text_control_views/text_top_view.dart
@@ -0,0 +1,57 @@
+
+import 'package:flutter/material.dart';
+import 'package:flutter_story_editor/src/controller/controller.dart';
+import 'package:flutter_story_editor/src/enums/story_editing_modes.dart';
+
+
+class TextTopView extends StatelessWidget {
+ final FlutterStoryEditorController controller;
+ final VoidCallback? onAlignChangeClickListener;
+ final IconData? icon;
+ const TextTopView({super.key, required this.controller, this.onAlignChangeClickListener, this.icon});
+
+ @override
+ Widget build(BuildContext context) {
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 40),
+ child: Column(
+ children: [
+ Row(
+ children: [
+ GestureDetector(
+ onTap: () {
+ FocusScope.of(context).unfocus();
+
+ controller.setStoryEditingModeSelected = StoryEditingModes.NONE;
+
+ },
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(width: 1, color: Colors.white)
+ ),
+ child: const Center(
+ child: Text("Done", style: TextStyle(fontSize: 15, color: Colors.white),),
+ ),
+ ),
+ ),
+ const SizedBox(width: 50),
+ GestureDetector(
+ onTap: () {
+ onAlignChangeClickListener!();
+ },
+ child: Row(
+ children: [
+ Icon(icon ?? Icons.format_align_center, size: 30, color: Colors.white,)
+ ],
+ ),
+ )
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/src/widgets/draggable_sticker_widget.dart b/lib/src/widgets/draggable_sticker_widget.dart
new file mode 100644
index 0000000..31113cb
--- /dev/null
+++ b/lib/src/widgets/draggable_sticker_widget.dart
@@ -0,0 +1,34 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_story_editor/src/utils/matrix_gesture_detector.dart';
+
+class DraggableStickerWidget extends StatefulWidget {
+ final String stickerPath;
+ const DraggableStickerWidget({super.key, required this.stickerPath});
+
+ @override
+ State createState() => _DraggableStickerWidgetState();
+}
+
+class _DraggableStickerWidgetState extends State {
+ Offset offset = const Offset(0, 0);
+
+ @override
+ Widget build(BuildContext context) {
+ final ValueNotifier notifier = ValueNotifier(Matrix4.identity());
+ return MatrixGestureDetector(
+ onMatrixUpdate: (m, tm, sm, rm) {
+ notifier.value = m;
+ },
+ child: AnimatedBuilder(
+ animation: notifier,
+ builder: (BuildContext context, Widget? child) {
+ return Transform(
+ transform: notifier.value,
+ child: Align(alignment: Alignment.center, child: Image.asset(widget.stickerPath)),
+ );
+ },
+
+ ),
+ );
+ }
+}
diff --git a/lib/src/widgets/draggable_text_widget.dart b/lib/src/widgets/draggable_text_widget.dart
new file mode 100644
index 0000000..66483ba
--- /dev/null
+++ b/lib/src/widgets/draggable_text_widget.dart
@@ -0,0 +1,419 @@
+import 'dart:async';
+import 'package:flutter/material.dart';
+import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
+import 'package:flutter_story_editor/src/const/filters.dart';
+import 'package:flutter_story_editor/src/controller/controller.dart';
+import 'package:flutter_story_editor/src/enums/story_editing_modes.dart';
+import 'package:flutter_story_editor/src/theme/style.dart';
+import 'package:flutter_story_editor/src/views/text_control_views/text_control_view.dart';
+import 'package:flutter_story_editor/src/widgets/hue_color_picker_slider.dart';
+
+class DraggableTextWidget extends StatefulWidget {
+ final List textList;
+ final FlutterStoryEditorController controller;
+ const DraggableTextWidget({super.key, required this.textList, required this.controller});
+
+ @override
+ State createState() => _DraggableTextWidgetState();
+}
+
+class _DraggableTextWidgetState extends State with AutomaticKeepAliveClientMixin {
+ FocusNode focusNode = FocusNode();
+ final TextEditingController _textEditingController = TextEditingController();
+
+ late final MaterialStatesController _statesController;
+
+ bool isKeyboardFocused = false;
+ bool isFocusField = false;
+
+ bool isAlignedLeft = false;
+ bool isAlignedRight = false;
+ TextStyle selectedTextStyle = fontStyles[0];
+
+ Offset offset = const Offset(0, 0);
+
+ double leftPosition = 0.0;
+
+ HSVColor textColor = HSVColor.fromColor(Colors.white);
+
+ double fontSize = 20;
+
+ late StreamSubscription keyboardSubscription;
+
+ @override
+ void initState() {
+
+ super.initState();
+ Future.delayed(const Duration(milliseconds: 100), () => focusNode.requestFocus());
+
+ // focusNode.addListener(() {
+ // setState(() {
+ // if(focusNode.hasFocus) {
+ // isFocusField = true;
+ // } else {
+ // isFocusField = false;
+ //
+ // }
+ // });
+ // });
+
+ _statesController = MaterialStatesController();
+
+ // Hypothetical listener setup to respond to state changes
+
+ _statesController.addListener(() {
+ Set states = _statesController.value;
+ if (states.contains(MaterialState.focused)) {
+
+ if(mounted) {
+ setState(() {
+ isFocusField = true;
+ offset = const Offset(0.0, 0.0);
+ if(isAlignedLeft == true) {
+ leftPosition = -150.0;
+ } else if(isAlignedRight == true) {
+ leftPosition = 150.0;
+ } else {
+ leftPosition = 0.0;
+ }
+ });
+ }
+
+ } else {
+
+ setState(() {
+ isFocusField = false;
+ });
+
+ if(mounted) {
+ setState(() {
+ if(_textEditingController.text.isEmpty) {
+ widget.textList.removeLast();
+ }
+ });
+ }
+
+ }
+ });
+
+
+ var keyboardVisibilityController = KeyboardVisibilityController();
+
+ keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) {
+
+ if(mounted) {
+ setState(() {
+ isKeyboardFocused = visible;
+ });
+ }
+
+ });
+
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ _textEditingController.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+
+ super.build(context);
+ return ValueListenableBuilder(
+ valueListenable: widget.controller.editingModeNotifier,
+ builder: (context, mode, child) {
+
+ return Stack(
+ children: [
+ if (mode == StoryEditingModes.TEXT && isFocusField)
+ Container(
+ color: Colors.black.withOpacity(.5),
+ ),
+ Positioned(
+ left: leftPosition,
+ top: offset.dy,
+ right: offset == const Offset(0, 0) ? 0 : null,
+ bottom: offset == const Offset(0, 0) ? 100 : null,
+ child: Align(
+ alignment: Alignment.center,
+ child: Draggable(
+ feedback: Material(
+ color: Colors.transparent,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 50.0),
+ child: IntrinsicWidth(
+ child: TextField(
+ maxLines: 2,
+ textInputAction: TextInputAction.newline,
+ statesController: _statesController,
+ textAlign: TextAlign.center,
+ onTap: () {
+ widget.controller.setStoryEditingModeSelected = StoryEditingModes.TEXT;
+ },
+ onTapOutside: (event) {
+ if (_textEditingController.text.isEmpty) {
+ widget.controller.setStoryEditingModeSelected = StoryEditingModes.NONE;
+ focusNode.unfocus();
+ }
+ },
+ style: TextStyle(fontSize: fontSize, fontWeight: FontWeight.w500, color: textColor.toColor())
+ .merge(selectedTextStyle).copyWith(color: textColor.toColor()),
+ focusNode: focusNode,
+ controller: _textEditingController,
+ autofocus: false,
+ cursorColor: tealColor,
+ decoration: const InputDecoration(
+ border: InputBorder.none,
+ hintText: "Add text",
+ hintStyle: TextStyle(fontSize: 20, color: Colors.grey)
+ ),
+ ),
+ ),
+ ),
+ ),
+ childWhenDragging: Container(),
+ onDragEnd: (details) {
+ setState(() {
+ offset = Offset(details.offset.dx, details.offset.dy);
+ leftPosition = details.offset.dx;
+ focusNode.unfocus();
+ });
+ },
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 50.0),
+ child: IntrinsicWidth(
+ child: TextField(
+ maxLines: 2,
+ textInputAction: TextInputAction.newline,
+ statesController: _statesController,
+ textAlign: TextAlign.center,
+ onTap: () {
+ widget.controller.setStoryEditingModeSelected = StoryEditingModes.TEXT;
+
+ },
+ onTapOutside: (event) {
+ if (_textEditingController.text.isEmpty) {
+ widget.controller.setStoryEditingModeSelected = StoryEditingModes.NONE;
+ focusNode.unfocus();
+ }
+ },
+ autofocus: offset == const Offset(0, 0),
+ focusNode: focusNode,
+ style: TextStyle(fontSize: fontSize, fontWeight: FontWeight.w500, color: textColor.toColor())
+ .merge(selectedTextStyle).copyWith(color: textColor.toColor()),
+ controller: _textEditingController,
+
+ cursorColor: tealColor,
+ decoration: const InputDecoration(
+ border: InputBorder.none,
+ hintText: "Add text",
+ hintStyle: TextStyle(fontSize: 20, color: Colors.grey)),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+
+
+ if(isKeyboardFocused && isFocusField)
+ TextControlView(
+ controller: widget.controller,
+ onAlignChangeClickListener: () {
+
+ setState(() {
+ if(leftPosition == 0.0) {
+ leftPosition = -150;
+ isAlignedLeft = true;
+ isAlignedRight = false;
+ } else if (leftPosition == -150) {
+ leftPosition = 150;
+ isAlignedLeft = false;
+ isAlignedRight = true;
+ } else {
+ leftPosition = 0.0;
+ isAlignedLeft = false;
+ isAlignedRight = false;
+ }
+ });
+ },
+ icon: alignIcon(),
+ ),
+
+
+
+ if (isKeyboardFocused && isFocusField)
+ AnimatedPadding(
+ duration: const Duration(milliseconds: 200),
+ padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
+ child: Align(
+ alignment: Alignment.bottomCenter,
+ child: Container(
+ height: 30,
+ margin: const EdgeInsets.only(left: 10, right: 10, bottom: 60),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ GestureDetector(
+ onTap: () {
+ if (fontSize >= 60) return;
+ setState(() {
+ fontSize += 5;
+ });
+ },
+ child: Container(
+ margin: const EdgeInsets.symmetric(horizontal: 5),
+ padding: const EdgeInsets.symmetric(horizontal: 10),
+ decoration: BoxDecoration(
+ color: Colors.black,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Center(child: Icon(Icons.text_increase, size: 25, color: Colors.white,)),
+ ),
+ ),
+ GestureDetector(
+ onTap: () {
+ if (fontSize <= 15) return;
+ setState(() {
+ fontSize -= 5;
+ });
+ },
+ child: Container(
+ margin: const EdgeInsets.symmetric(horizontal: 5),
+ padding: const EdgeInsets.symmetric(horizontal: 10),
+ decoration: BoxDecoration(
+ color: Colors.black,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Center(child: Icon(Icons.text_decrease, size: 25, color: Colors.white)),
+ ),
+ ),
+ ],
+ )),
+ ),
+ ),
+
+ if (isKeyboardFocused && isFocusField)
+ AnimatedPadding(
+ duration: const Duration(milliseconds: 200),
+ padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
+ child: Align(
+ alignment: Alignment.bottomCenter,
+ child: Container(
+ height: 40,
+ margin: const EdgeInsets.only(left: 10, right: 10, bottom: 10),
+ child: ListView.builder(
+ scrollDirection: Axis.horizontal,
+ itemCount: fontStyles.length,
+ itemBuilder: (context, index) {
+ final singleTextStyle = fontStyles[index];
+ return GestureDetector(
+ onTap: () {
+ setState(() {
+ selectedTextStyle = singleTextStyle;
+ });
+ },
+ child: Container(
+ margin: const EdgeInsets.symmetric(horizontal: 5),
+ padding: const EdgeInsets.symmetric(horizontal: 10),
+ decoration: BoxDecoration(
+ color: Colors.black,
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(
+ width: 1.5,
+ color: selectedTextStyle == singleTextStyle ? Colors.white : Colors.transparent,
+ ),
+ ),
+ child: Center(
+ child: Text(
+ "Aa",
+ style: fontStyles[index],
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ),
+ ),
+
+ if (isKeyboardFocused && isFocusField)
+ Positioned(
+ top: 100,
+ right: 28,
+ child: HueColorPickerSlider(
+ onChanged: (hsvColor) {
+ setState(() {
+ textColor = hsvColor;
+ });
+ },
+ ),
+ ),
+
+ // ! Horizontal color picker (deprecated)
+ // AnimatedPadding(
+ // duration: const Duration(milliseconds: 200),
+ // padding: EdgeInsets.only(
+ // bottom: MediaQuery.of(context).viewInsets.bottom,
+ // ),
+ // child:
+ //
+ //
+ // Align(
+ // alignment: Alignment.bottomCenter,
+ // child: Container(
+ // height: 40,
+ // margin: const EdgeInsets.all(8),
+ // child: ListView(
+ // scrollDirection: Axis.horizontal,
+ // children: textFilterColors.map((color) {
+ //
+ // return GestureDetector(
+ // onTap: () {
+ // setState(() {
+ // selectedColor = color;
+ // });
+ // },
+ // child: Container(
+ // margin: const EdgeInsets.symmetric(horizontal: 5),
+ // width: 40,
+ // height: 40,
+ // decoration: BoxDecoration(
+ // color: color,
+ // borderRadius: BorderRadius.circular(20),
+ // border: Border.all(
+ // width: 1.5,
+ // color: selectedColor == color
+ // ? Colors.white
+ // : Colors.transparent,
+ // ),
+ // ),
+ // ),
+ // );
+ // }).toList(),
+ // ),
+ // ),
+ // ),
+ // ),
+ ],
+ );
+ },
+ );
+ }
+
+ IconData alignIcon() {
+ if(isAlignedLeft == true) {
+ return Icons.format_align_left;
+ } else if (isAlignedRight == true) {
+ return Icons.format_align_right;
+ } else {
+ return Icons.format_align_center;
+ }
+ }
+
+ @override
+ bool get wantKeepAlive => true;
+}
diff --git a/lib/src/widgets/hue_color_picker_slider.dart b/lib/src/widgets/hue_color_picker_slider.dart
new file mode 100644
index 0000000..de6249e
--- /dev/null
+++ b/lib/src/widgets/hue_color_picker_slider.dart
@@ -0,0 +1,27 @@
+
+import 'package:flutter/material.dart';
+import 'package:hsv_color_pickers/hsv_color_pickers.dart';
+
+class HueColorPickerSlider extends StatefulWidget {
+ final Function(HSVColor) onChanged;
+ const HueColorPickerSlider({super.key, required this.onChanged});
+
+ @override
+ State createState() => _HueColorPickerSliderState();
+}
+
+class _HueColorPickerSliderState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return RotatedBox(
+ quarterTurns: 1,
+ child: HuePicker(
+ trackHeight: 10,
+ controller: HueController(HSVColor.fromColor(Colors.tealAccent)),
+ onChanged: (HSVColor color) {
+ widget.onChanged(color);
+ },
+ ),
+ );
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..dd507cc
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,90 @@
+name: flutter_story_editor
+description: "This package is created using style of the WhatsApp story image/video editor, with which you can edit images and videos both together. You can add texts, stickers, freehand finger drawing, apply filter, and undo"
+version: 0.0.1
+homepage:
+
+environment:
+ sdk: '>=3.3.0 <4.0.0'
+ flutter: ">=1.17.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+ image_cropper: ^5.0.1
+ video_player: ^2.8.6
+ video_trimmer: ^3.0.1
+ video_thumbnail: ^0.5.3
+ hsv_color_pickers: 0.3.0
+ perfect_freehand: ^2.3.2
+
+ file_picker: ^8.0.1
+ path: ^1.9.0
+ flutter_keyboard_visibility: ^6.0.0
+ vector_math: ^2.1.4
+ font_awesome_flutter: ^10.7.0
+ material_design_icons_flutter: ^7.0.7296
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+ flutter_lints: ^3.0.2
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter packages.
+flutter:
+
+ # To add assets to your package, add an assets section, like this:
+ assets:
+ - assets/images/
+ - assets/emojies/
+
+ # An image asset can refer to one or more resolution-specific "variants", see
+ # https://flutter.dev/assets-and-images/#resolution-aware
+
+ # For details regarding adding assets from package dependencies, see
+ # https://flutter.dev/assets-and-images/#from-packages
+
+ # To add custom fonts to your application, add a fonts section here,
+ # in this "flutter" section. Each entry in this list should have a
+ # "family" key with the font family name, and a "fonts" key with a
+ # list giving the asset and other descriptors for the font. For
+ # example:
+ fonts:
+ - family: Roboto
+ fonts:
+ - asset: assets/fonts/roboto/Roboto-Regular.ttf
+ - family: Merriweather
+ fonts:
+ - asset: assets/fonts/merriweather/Merriweather-Regular.ttf
+ - family: Mandimi One
+ fonts:
+ - asset: assets/fonts/madimiOne/MadimiOne-Regular.ttf
+ - family: Dancing Script
+ fonts:
+ - asset: assets/fonts/dancing_script/DancingScript-Regular.ttf
+ - family: Angkor
+ fonts:
+ - asset: assets/fonts/angkor/Angkor-Regular.ttf
+ - family: Montserrat
+ fonts:
+ - asset: assets/fonts/montserrat/Montserrat-Regular.ttf
+ - family: Lato
+ fonts:
+ - asset: assets/fonts/lato/Lato-Regular.ttf
+ - family: Oswald
+ fonts:
+ - asset: assets/fonts/oswald/Oswald-Regular.ttf
+ - family: Raleway
+ fonts:
+ - asset: assets/fonts/raleway/Raleway-Regular.ttf
+ - family: Lora
+ fonts:
+ - asset: assets/fonts/lora/Lora-Regular.ttf
+ - family: Pacifico
+ fonts:
+ - asset: assets/fonts/pacifico/Pacifico-Regular.ttf
+ #
+ # For details regarding fonts in packages, see
+ # https://flutter.dev/custom-fonts/#from-packages
diff --git a/screenshots/CropAndPaint.gif b/screenshots/CropAndPaint.gif
new file mode 100644
index 0000000..928079f
Binary files /dev/null and b/screenshots/CropAndPaint.gif differ
diff --git a/screenshots/Text.gif b/screenshots/Text.gif
new file mode 100644
index 0000000..37153b3
Binary files /dev/null and b/screenshots/Text.gif differ
diff --git a/screenshots/VideoEditing.gif b/screenshots/VideoEditing.gif
new file mode 100644
index 0000000..daa6143
Binary files /dev/null and b/screenshots/VideoEditing.gif differ
diff --git a/screenshots/image_1.png b/screenshots/image_1.png
new file mode 100644
index 0000000..c6ef376
Binary files /dev/null and b/screenshots/image_1.png differ
diff --git a/screenshots/image_10.png b/screenshots/image_10.png
new file mode 100644
index 0000000..c46d7ce
Binary files /dev/null and b/screenshots/image_10.png differ
diff --git a/screenshots/image_11.png b/screenshots/image_11.png
new file mode 100644
index 0000000..aef8790
Binary files /dev/null and b/screenshots/image_11.png differ
diff --git a/screenshots/image_12.png b/screenshots/image_12.png
new file mode 100644
index 0000000..6140392
Binary files /dev/null and b/screenshots/image_12.png differ
diff --git a/screenshots/image_2.png b/screenshots/image_2.png
new file mode 100644
index 0000000..064e8b7
Binary files /dev/null and b/screenshots/image_2.png differ
diff --git a/screenshots/image_3.png b/screenshots/image_3.png
new file mode 100644
index 0000000..df9e496
Binary files /dev/null and b/screenshots/image_3.png differ
diff --git a/screenshots/image_4.png b/screenshots/image_4.png
new file mode 100644
index 0000000..0af8aff
Binary files /dev/null and b/screenshots/image_4.png differ
diff --git a/screenshots/image_5.png b/screenshots/image_5.png
new file mode 100644
index 0000000..1c22be0
Binary files /dev/null and b/screenshots/image_5.png differ
diff --git a/screenshots/image_6.png b/screenshots/image_6.png
new file mode 100644
index 0000000..bca56cf
Binary files /dev/null and b/screenshots/image_6.png differ
diff --git a/screenshots/image_7.png b/screenshots/image_7.png
new file mode 100644
index 0000000..3914a84
Binary files /dev/null and b/screenshots/image_7.png differ
diff --git a/screenshots/image_8.png b/screenshots/image_8.png
new file mode 100644
index 0000000..54aeddc
Binary files /dev/null and b/screenshots/image_8.png differ
diff --git a/test/flutter_story_editor_test.dart b/test/flutter_story_editor_test.dart
new file mode 100644
index 0000000..e69de29