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