creek

The AI Image Editor of 2030
commit 975178067a60a85b4ab5d4cd506de1a6f1ae4d28
parent 31760e47dd45330e990f422eaa047077b8b9f97b
Author: maydayv7 <maydayv7@gmail.com>
Date:   Thu,  4 Dec 2025 09:33:03 +0530

Rename app and add icon

Also update imports and rename canvas

Diffstat:
M.env | 9+++++++++
M.github/workflows/release.yml | 2+-
MREADME.md | 2+-
Mandroid/app/build.gradle.kts | 4++--
Mandroid/app/src/main/AndroidManifest.xml | 13+++++--------
Mandroid/app/src/main/kotlin/com/example/adobe/ImageAnalyzer.kt | 2+-
Mandroid/app/src/main/kotlin/com/example/adobe/MainActivity.kt | 4++--
Aandroid/app/src/main/res/mipmap-hdpi/launcher_icon.png | 0
Aandroid/app/src/main/res/mipmap-mdpi/launcher_icon.png | 0
Aandroid/app/src/main/res/mipmap-xhdpi/launcher_icon.png | 0
Aandroid/app/src/main/res/mipmap-xxhdpi/launcher_icon.png | 0
Aandroid/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png | 0
Aassets/app_icon.png | 0
Mflask/modal_app.py | 2+-
Mios/Runner.xcodeproj/project.pbxproj | 16++++++++--------
Mios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json | 124++-----------------------------------------------------------------------------
Mios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png | 0
Mios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png | 0
Mios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png | 0
Mios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png | 0
Mios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png | 0
Mios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png | 0
Mios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png | 0
Mios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png | 0
Mios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png | 0
Mios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png | 0
Aios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png | 0
Aios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png | 0
Aios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png | 0
Aios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png | 0
Mios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png | 0
Mios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png | 0
Aios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png | 0
Aios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png | 0
Mios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png | 0
Mios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png | 0
Mios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png | 0
Mios/Runner/Info.plist | 4++--
Mlib/main.dart | 10+++++-----
Mlib/services/analysis_queue_manager.dart | 8++++----
Mlib/services/analyze/embedding.dart | 4++--
Mlib/services/analyze/image_analyzer.dart | 2+-
Mlib/services/analyze/texture.dart | 2+-
Mlib/services/file_service.dart | 4++--
Mlib/services/flask_service.dart | 16++++++++--------
Mlib/services/image_service.dart | 4++--
Mlib/services/note_service.dart | 4++--
Mlib/services/project_service.dart | 8++++----
Mlib/services/python_service.dart | 2+-
Dlib/ui/pages/canvas_board_page.dart | 2813-------------------------------------------------------------------------------
Alib/ui/pages/canvas_page.dart | 2813+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/ui/pages/canvas_toolbar/magic_draw_overlay.dart | 6+++---
Mlib/ui/pages/create_file_page.dart | 14+++++---------
Mlib/ui/pages/define_brand_page.dart | 4++--
Mlib/ui/pages/home_page.dart | 16++++++++--------
Mlib/ui/pages/image_analysis_page.dart | 4++--
Mlib/ui/pages/image_details_page.dart | 10+++++-----
Mlib/ui/pages/image_save_page.dart | 4++--
Mlib/ui/pages/project_board_page.dart | 18+++++++++---------
Mlib/ui/pages/project_board_page_alternate.dart | 8++++----
Mlib/ui/pages/project_detail_page.dart | 10+++++-----
Mlib/ui/pages/project_file_page.dart | 20+++++++++-----------
Mlib/ui/pages/project_tag_page.dart | 16++++++++--------
Mlib/ui/pages/share_handler_page.dart | 10+++++-----
Mlib/ui/pages/share_to_file_page.dart | 14++++++--------
Mlib/ui/pages/share_to_moodboard_page.dart | 14++++----------
Mlib/ui/pages/stylesheet_page.dart | 16++++++++--------
Mlib/ui/widgets/bottom_bar.dart | 8++++----
Mlib/ui/widgets/image_context_menu.dart | 6+++---
Mlib/ui/widgets/top_bar.dart | 6+++---
Mlib/utils/clip_image_processor.dart | 2--
Mlib/utils/image_actions_helper.dart | 8++++----
Mlib/utils/image_utils.dart | 2--
Mlinux/CMakeLists.txt | 4++--
Mlinux/runner/my_application.cc | 74++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mmacos/Runner.xcodeproj/project.pbxproj | 18+++++++++---------
Mmacos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme | 8++++----
Mmacos/Runner/Configs/AppInfo.xcconfig | 4++--
Mpubspec.yaml | 8+++++++-
Mweb/index.html | 40++++++++++++++++++++--------------------
Mweb/manifest.json | 66+++++++++++++++++++++++++++++++++---------------------------------
Mwindows/CMakeLists.txt | 4++--
Mwindows/runner/Runner.rc | 8++++----
Mwindows/runner/main.cpp | 12++++++++----
84 files changed, 3113 insertions(+), 3221 deletions(-)

diff --git a/.env b/.env @@ -1,6 +1,15 @@ +# Modal Server URL_ASSET=https://samaladitya2004--adobe-flask-modelbackend-asset.modal.run URL_DESCRIBE=https://samaladitya2004--adobe-flask-modelbackend-describe.modal.run URL_GENERATE=https://samaladitya2004--adobe-flask-modelbackend-generate.modal.run URL_INPAINTING=https://samaladitya2004--adobe-flask-modelbackend-inpainting.modal.run URL_INPAINTING_API=https://samaladitya2004--adobe-flask-modelbackend-inpainting-api.modal.run URL_SKETCH_API=https://samaladitya2004--adobe-flask-modelbackend-sketch-api.modal.run + +# Testing +# URL_ASSET=https://locustlike-trieciously-rudolph.ngrok-free.dev/asset +# URL_DESCRIBE=https://locustlike-trieciously-rudolph.ngrok-free.dev/describe +# URL_GENERATE=https://locustlike-trieciously-rudolph.ngrok-free.dev/generate +# URL_INPAINTING=https://locustlike-trieciously-rudolph.ngrok-free.dev/inpainting +# URL_INPAINTING_API=https://locustlike-trieciously-rudolph.ngrok-free.dev/inpainting-api +# URL_SKETCH_API=https://locustlike-trieciously-rudolph.ngrok-free.dev/sketch-api diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml @@ -69,4 +69,4 @@ jobs: with: files: build/app/outputs/flutter-apk/app-release.apk generate_release_notes: false - body: "adobe" + body: "creekui" diff --git a/README.md b/README.md @@ -1,4 +1,4 @@ -# adobe +# CreekUI A Flutter project with AI-based image analysis diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts @@ -29,7 +29,7 @@ if (keystorePropertiesFile.exists()) { } android { - namespace = "com.example.adobe" + namespace = "com.creek.ui" compileSdk = flutter.compileSdkVersion ndkVersion = "27.0.12077973" @@ -46,7 +46,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.adobe" + applicationId = "com.creek.ui" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = 24 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml @@ -1,19 +1,16 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.example.adobe"> + package="com.creek.ui"> <uses-permission android:name="android.permission.INTERNET"/> - <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32"/> - <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/> - <uses-permission android:name="android.permission.CAMERA"/> <application - android:label="Adobe" + android:label="CreekUI" android:name="${applicationName}" - android:icon="@mipmap/ic_launcher"> + android:icon="@mipmap/launcher_icon"> <activity android:name=".MainActivity" @@ -39,7 +36,7 @@ android:name=".ShareToFiles" android:targetActivity=".MainActivity" android:label="Files" - android:icon="@mipmap/ic_launcher" + android:icon="@mipmap/launcher_icon" android:exported="true"> <intent-filter> <action android:name="android.intent.action.SEND" /> @@ -58,7 +55,7 @@ android:name=".ShareToMoodboards" android:targetActivity=".MainActivity" android:label="Moodboards" - android:icon="@mipmap/ic_launcher" + android:icon="@mipmap/launcher_icon" android:exported="true"> <intent-filter> <action android:name="android.intent.action.SEND" /> diff --git a/android/app/src/main/kotlin/com/example/adobe/ImageAnalyzer.kt b/android/app/src/main/kotlin/com/example/adobe/ImageAnalyzer.kt @@ -1,4 +1,4 @@ -package com.example.adobe +package com.creek.ui import android.content.Context import com.chaquo.python.Python diff --git a/android/app/src/main/kotlin/com/example/adobe/MainActivity.kt b/android/app/src/main/kotlin/com/example/adobe/MainActivity.kt @@ -1,4 +1,4 @@ -package com.example.adobe +package com.creek.ui import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine @@ -10,7 +10,7 @@ import kotlinx.coroutines.withContext import android.content.Intent class MainActivity : FlutterActivity() { - private val CHANNEL = "com.example.adobe/methods" + private val CHANNEL = "com.creek.ui/methods" private val scope = CoroutineScope(Dispatchers.Default) override fun onNewIntent(intent: Intent) { diff --git a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png Binary files differ. diff --git a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png Binary files differ. diff --git a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png Binary files differ. diff --git a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png Binary files differ. diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png Binary files differ. diff --git a/assets/app_icon.png b/assets/app_icon.png Binary files differ. diff --git a/flask/modal_app.py b/flask/modal_app.py @@ -43,7 +43,7 @@ image = ( .add_local_dir("BiRefNet", remote_path="/root/BiRefNet") ) -app = modal.App("adobe-flask", image=image) +app = modal.App("creekui-flask", image=image) # ============================================================================== diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj @@ -368,7 +368,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.adobe; + PRODUCT_BUNDLE_IDENTIFIER = com.creek.ui; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -384,7 +384,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.adobe.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.creek.ui.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -401,7 +401,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.adobe.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.creek.ui.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -416,7 +416,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.adobe.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.creek.ui.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -427,7 +427,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -484,7 +484,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -547,7 +547,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.adobe; + PRODUCT_BUNDLE_IDENTIFIER = com.creek.ui; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -569,7 +569,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.adobe; + PRODUCT_BUNDLE_IDENTIFIER = com.creek.ui; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,122 +1 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} +{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} +\ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png Binary files differ. diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png Binary files differ. diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist @@ -5,7 +5,7 @@ <key>CFBundleDevelopmentRegion</key> <string>$(DEVELOPMENT_LANGUAGE)</string> <key>CFBundleDisplayName</key> - <string>Adobe</string> + <string>CreekUI</string> <key>CFBundleExecutable</key> <string>$(EXECUTABLE_NAME)</string> <key>CFBundleIdentifier</key> @@ -13,7 +13,7 @@ <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> <key>CFBundleName</key> - <string>adobe</string> + <string>creekui</string> <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleShortVersionString</key> diff --git a/lib/main.dart b/lib/main.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; -import 'package:adobe/services/theme_service.dart'; -import 'package:adobe/ui/pages/share_handler_page.dart'; -import 'package:adobe/ui/pages/home_page.dart'; -import 'package:adobe/services/analysis_queue_manager.dart'; +import 'package:creekui/services/theme_service.dart'; +import 'package:creekui/ui/pages/share_handler_page.dart'; +import 'package:creekui/ui/pages/home_page.dart'; +import 'package:creekui/services/analysis_queue_manager.dart'; import 'package:flutter/services.dart'; void main() async { @@ -77,7 +77,7 @@ class _MyAppState extends State<MyApp> { // 1. Determine destination (Files vs Moodboards) String shareDestination = 'moodboards'; // Default try { - const platform = MethodChannel('com.example.adobe/methods'); + const platform = MethodChannel('com.creek.ui/methods'); final result = await platform.invokeMethod<String>('getShareSource'); if (result != null) shareDestination = result; } catch (e) { diff --git a/lib/services/analysis_queue_manager.dart b/lib/services/analysis_queue_manager.dart @@ -3,10 +3,10 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:image/image.dart' as img; -import '../data/models/image_model.dart'; -import '../data/models/note_model.dart'; -import '../data/repos/image_repo.dart'; -import '../data/repos/note_repo.dart'; +import 'package:creekui/data/models/image_model.dart'; +import 'package:creekui/data/models/note_model.dart'; +import 'package:creekui/data/repos/image_repo.dart'; +import 'package:creekui/data/repos/note_repo.dart'; import 'analyze/image_analyzer.dart'; import 'package:path/path.dart' as p; diff --git a/lib/services/analyze/embedding.dart b/lib/services/analyze/embedding.dart @@ -2,8 +2,8 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:onnxruntime/onnxruntime.dart'; -import 'package:adobe/utils/image_utils.dart'; -import 'package:adobe/utils/clip_image_processor.dart'; +import 'package:creekui/utils/image_utils.dart'; +import 'package:creekui/utils/clip_image_processor.dart'; class EmbeddingAnalyzerService { OrtSession? _session; diff --git a/lib/services/analyze/image_analyzer.dart b/lib/services/analyze/image_analyzer.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'dart:isolate'; import 'dart:ui'; import 'dart:developer' as dev; -import 'package:adobe/services/flask_service.dart'; +import 'package:creekui/services/flask_service.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:path_provider/path_provider.dart'; diff --git a/lib/services/analyze/texture.dart b/lib/services/analyze/texture.dart @@ -1,6 +1,6 @@ import 'dart:io'; import 'dart:convert'; -import 'package:adobe/utils/image_utils.dart'; +import 'package:creekui/utils/image_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:image/image.dart' as img; import 'package:onnxruntime/onnxruntime.dart'; diff --git a/lib/services/file_service.dart b/lib/services/file_service.dart @@ -1,8 +1,8 @@ import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid.dart'; -import '../data/repos/file_repo.dart'; -import '../data/models/file_model.dart'; +import 'package:creekui/data/repos/file_repo.dart'; +import 'package:creekui/data/models/file_model.dart'; class FileService { final _repo = FileRepo(); diff --git a/lib/services/flask_service.dart b/lib/services/flask_service.dart @@ -1,15 +1,15 @@ import 'dart:convert'; import 'dart:io'; -import 'package:adobe/data/repos/file_repo.dart'; -import 'package:adobe/data/repos/image_repo.dart'; -import 'package:adobe/data/repos/note_repo.dart'; -import 'package:adobe/data/repos/project_repo.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:http/http.dart' as http; import 'package:path/path.dart' as p; +import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:adobe/services/python_service.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:creekui/data/repos/project_repo.dart'; +import 'package:creekui/data/repos/image_repo.dart'; +import 'package:creekui/data/repos/note_repo.dart'; +import 'package:creekui/data/repos/file_repo.dart'; +import 'package:creekui/services/python_service.dart'; class FlaskService { // =========================================================================== @@ -320,7 +320,7 @@ class FlaskService { // LOG TO FILE HELPER // Use following command to see logs: - // adb -d shell "run-as com.example.adobe cat /data/user/0/com.example.adobe/app_flutter/debug_magic_prompt.txt" + // adb -d shell "run-as com.creek.ui cat /data/user/0/com.creek.ui/app_flutter/debug_magic_prompt.txt" Future<void> _logToFile(String filename, String content) async { try { final dir = await getApplicationDocumentsDirectory(); diff --git a/lib/services/image_service.dart b/lib/services/image_service.dart @@ -4,8 +4,8 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid.dart'; -import '../data/repos/image_repo.dart'; -import '../data/models/image_model.dart'; +import 'package:creekui/data/repos/image_repo.dart'; +import 'package:creekui/data/models/image_model.dart'; import 'analysis_queue_manager.dart'; class ImageService { diff --git a/lib/services/note_service.dart b/lib/services/note_service.dart @@ -1,5 +1,5 @@ -import '../data/repos/note_repo.dart'; -import '../data/models/note_model.dart'; +import 'package:creekui/data/repos/note_repo.dart'; +import 'package:creekui/data/models/note_model.dart'; import 'analysis_queue_manager.dart'; class NoteService { diff --git a/lib/services/project_service.dart b/lib/services/project_service.dart @@ -2,10 +2,10 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/rendering.dart'; -import '../data/repos/project_repo.dart'; -import '../data/repos/image_repo.dart'; -import '../data/repos/file_repo.dart'; -import '../data/models/project_model.dart'; +import 'package:creekui/data/repos/project_repo.dart'; +import 'package:creekui/data/repos/image_repo.dart'; +import 'package:creekui/data/repos/file_repo.dart'; +import 'package:creekui/data/models/project_model.dart'; class ProjectService { final _projectRepo = ProjectRepo(); diff --git a/lib/services/python_service.dart b/lib/services/python_service.dart @@ -4,7 +4,7 @@ import 'package:flutter/services.dart'; class PythonService { static const MethodChannel _channel = MethodChannel( - 'com.example.adobe/methods', + 'com.creek.ui/methods', ); // 1. Layout Analysis (OpenCV) diff --git a/lib/ui/pages/canvas_board_page.dart b/lib/ui/pages/canvas_board_page.dart @@ -1,2813 +0,0 @@ -import 'dart:io'; -import 'dart:convert'; -import 'dart:async'; -import 'dart:math' as math; -import 'dart:ui' as ui; -import 'dart:typed_data'; -import 'package:flutter/material.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/rendering.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:undo/undo.dart'; -import 'package:share_plus/share_plus.dart'; -import 'package:image/image.dart' as img; -import 'package:path/path.dart' as p; -import './canvas_toolbar/magic_draw_overlay.dart'; -import './canvas_toolbar/text_tools_overlay.dart'; -import '../../data/repos/project_repo.dart'; -import '../../data/models/file_model.dart'; -import '../../services/stylesheet_service.dart'; -import '../../services/file_service.dart'; -import '../../services/flask_service.dart'; -import 'project_file_page.dart'; - -// --- MODELS WITH JSON SUPPORT --- - -class DrawingPoint { - final Offset offset; - final Paint paint; - const DrawingPoint({required this.offset, required this.paint}); - - Map<String, dynamic> toMap() => {'dx': offset.dx, 'dy': offset.dy}; - - factory DrawingPoint.fromMap(Map<String, dynamic> map) { - return DrawingPoint( - offset: Offset(map['dx'] ?? 0, map['dy'] ?? 0), - paint: Paint(), - ); - } -} - -class DrawingPath { - final List<DrawingPoint> points; - final Color color; - final double strokeWidth; - final bool isEraser; - - DrawingPath({ - required this.points, - required this.color, - required this.strokeWidth, - required this.isEraser, - }); - - Map<String, dynamic> toMap() { - return { - 'points': points.map((p) => p.toMap()).toList(), - 'color': color.value, - 'strokeWidth': strokeWidth, - 'isEraser': isEraser, - }; - } - - factory DrawingPath.fromMap(Map<String, dynamic> map) { - return DrawingPath( - points: - (map['points'] as List).map((p) => DrawingPoint.fromMap(p)).toList(), - color: Color(map['color']), - strokeWidth: (map['strokeWidth'] as num).toDouble(), - isEraser: map['isEraser'] ?? false, - ); - } -} - -// Helper to snapshot the entire canvas state for undo/redo -class CanvasState { - final List<Map<String, dynamic>> elements; - final List<DrawingPath> paths; - CanvasState(this.elements, this.paths); -} - -class CanvasBoardPage extends StatefulWidget { - final int projectId; - final double width; - final double height; - final File? initialImage; - final FileModel? existingFile; - final File? injectedMedia; - - const CanvasBoardPage({ - super.key, - required this.projectId, - required this.width, - required this.height, - this.initialImage, - this.existingFile, - this.injectedMedia, - }); - - @override - State<CanvasBoardPage> createState() => _CanvasBoardPageState(); -} - -class _CanvasBoardPageState extends State<CanvasBoardPage> { - // --- SERVICES --- - final FileService _fileService = FileService(); - final ChangeStack _changeStack = ChangeStack(); - final ImagePicker _picker = ImagePicker(); - final ChangeStack _magicDrawChangeStack = ChangeStack(); - - // --- STATE --- - bool _hasUnsavedChanges = false; - List<Map<String, dynamic>> elements = []; - List<DrawingPath> _paths = []; // Keeps normal drawing strokes - List<DrawingPath> _magicPaths = []; // Keeps temporary magic draw mask strokes - - // Snapshots for undo grouping - CanvasState? _gestureStartSnapshot; - - // Detection Timer & AI Analysis - Timer? _inactivityTimer; - DateTime _lastAnalysisTime = DateTime.now(); - final GlobalKey _canvasGlobalKey = GlobalKey(); - String? _aiDescription; - bool _isAnalyzing = false; - bool _isDescriptionExpanded = false; // Track expansion state - - // Magic Draw / Inpainting State - File? _tempBaseImage; - bool _isInpainting = false; - bool _isCapturingBase = false; // Hide strokes during capture - - // Background Removal Banner State - bool _showBgRemovalBanner = false; - String? _bgRemovalTargetId; - String? _bgRemovalTargetPath; - Timer? _bgRemovalBannerTimer; - bool _isRemovingBg = false; - - String? selectedId; - late Size _canvasSize; - List<Color> _brandColors = []; - - // --- TOOLS --- - bool _isMagicDrawActive = false; - bool _isTextToolsActive = false; - - bool _isMagicPanelDisabled = false; - bool _isViewMode = false; - - // --- EDITING --- - bool _isEditingText = false; - final TextEditingController _textEditingController = TextEditingController(); - final FocusNode _textFocusNode = FocusNode(); - - // --- DRAWING --- - List<DrawingPoint> _currentPoints = []; - Color _selectedColor = const Color(0xFFFF4081); - double _strokeWidth = 10.0; - bool _isEraser = false; - final GlobalKey _drawingKey = GlobalKey(); - - // --- VIEWPORT --- - final TransformationController _transformationController = - TransformationController(); - bool _hasInitializedView = false; - - @override - void initState() { - super.initState(); - _canvasSize = Size(widget.width, widget.height); - _fetchBrandColors(); - - if (widget.existingFile != null) { - _loadCanvasFromFile(); - } else if (widget.initialImage != null) { - final double imageWidth = _canvasSize.width * 0.4; // 40% of canvas width - final double imageHeight = - _canvasSize.height * 0.4; // 40% of canvas height - final Offset centeredPosition = Offset( - (_canvasSize.width - imageWidth) / 2, - (_canvasSize.height - imageHeight) / 2, - ); - elements.add({ - 'id': 'bg_${DateTime.now().millisecondsSinceEpoch}', - 'type': 'file_image', - 'content': widget.initialImage!.path, - 'position': centeredPosition, - 'size': Size(imageWidth, imageHeight), - 'rotation': 0.0, - }); - _hasUnsavedChanges = true; - } - - // Note: injectedMedia handling is done inside _loadCanvasFromFile to ensure - // it happens after file content is loaded, avoiding race conditions. - } - - @override - void dispose() { - _inactivityTimer?.cancel(); - _bgRemovalBannerTimer?.cancel(); - _textEditingController.dispose(); - _textFocusNode.dispose(); - _transformationController.dispose(); - super.dispose(); - } - - // Handle inactivity timer logic - void _resetInactivityTimer() { - _inactivityTimer?.cancel(); - _inactivityTimer = Timer(const Duration(seconds: 2), () { - _analyzeCanvas(); - }); - } - - Future<bool> _confirmDiscardMagicDraw() async { - if (_magicPaths.isEmpty) return true; // nothing drawn – no popup - - final result = await showDialog<bool>( - context: context, - builder: - (context) => AlertDialog( - title: const Text("Discard Magic Draw?"), - content: const Text( - "Leaving Magic Draw will remove your sketch. Continue?", - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text("Stay"), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: const Text("Discard"), - ), - ], - ), - ); - - return result == true; - } - - Future<void> _analyzeCanvas() async { - if (_isAnalyzing || _isInpainting) return; // Skip if busy - setState(() => _isAnalyzing = true); - - try { - debugPrint("🧠 [AI] Starting canvas analysis..."); - - File? imageFile = await _captureCanvasToFile(); - if (imageFile == null) return; - - // Call the service - final description = await FlaskService().describeImage( - imagePath: imageFile.path, - ); - - debugPrint("🤖 [AI] Service Response: $description"); - - if (description != null && mounted) { - setState(() { - _aiDescription = description; - _isDescriptionExpanded = - false; // Reset to collapsed on new description - }); - } - } catch (e) { - debugPrint("❌ [AI] Analysis Failed: $e"); - } finally { - if (mounted) setState(() => _isAnalyzing = false); - } - } - - Future<File?> _captureCanvasToFile() async { - try { - RenderRepaintBoundary? boundary = - _canvasGlobalKey.currentContext?.findRenderObject() - as RenderRepaintBoundary?; - if (boundary == null) return null; - - ui.Image image = await boundary.toImage(pixelRatio: 1.0); - ByteData? byteData = await image.toByteData( - format: ui.ImageByteFormat.png, - ); - if (byteData == null) return null; - Uint8List pngBytes = byteData.buffer.asUint8List(); - - final tempDir = await getTemporaryDirectory(); - final tempFile = File( - '${tempDir.path}/canvas_capture_${DateTime.now().millisecondsSinceEpoch}.png', - ); - await tempFile.writeAsBytes(pngBytes); - return tempFile; - } catch (e) { - debugPrint("Error capturing canvas: $e"); - return null; - } - } - - Future<void> _fetchBrandColors() async { - try { - final int? pId = widget.projectId; - if (pId == null) return; - - final project = await ProjectRepo().getProjectById(pId); - if (project != null && project.globalStylesheet != null) { - final styleData = StylesheetService().parse(project.globalStylesheet); - if (mounted && styleData.colors.isNotEmpty) { - setState(() { - _brandColors = styleData.colors; - }); - } - } - } catch (e) { - debugPrint("Error loading brand colors: $e"); - } - } - - // --- UNDO/REDO --- - // --- MAGIC DRAW UNDO/REDO (NEW) --- - void _recordMagicChange(List<DrawingPath> oldMagicPaths) { - final newMagicPaths = List<DrawingPath>.from(_magicPaths); - _magicDrawChangeStack.add( - Change( - oldMagicPaths, - () => setState(() => _magicPaths = List.from(newMagicPaths)), // Redo - (val) => setState(() => _magicPaths = List.from(val)), // Undo - ), - ); - } - - void _recordChange(CanvasState oldState) { - _resetInactivityTimer(); // Reset timer on undoable actions - final newState = CanvasState( - _deepCopyElements(elements), - List.from(_paths), - ); - - _changeStack.add( - Change( - oldState, - () { - // REDO - setState(() { - elements = _deepCopyElements(newState.elements); - _paths = List.from(newState.paths); - _hasUnsavedChanges = true; - }); - }, - (val) { - // UNDO - setState(() { - elements = _deepCopyElements(val.elements); - _paths = List.from(val.paths); - _hasUnsavedChanges = true; - }); - }, - ), - ); - setState(() => _hasUnsavedChanges = true); - } - - CanvasState _getCurrentState() { - return CanvasState(_deepCopyElements(elements), List.from(_paths)); - } - - // --- ACTIONS --- - - void _toggleTextTools() { - setState(() { - _isTextToolsActive = !_isTextToolsActive; - _isMagicDrawActive = false; - if (!_isTextToolsActive) { - _exitEditMode(); - selectedId = null; - } - }); - } - - void _addTextElement() { - final oldState = _getCurrentState(); - final id = 'text_${DateTime.now().millisecondsSinceEpoch}'; - final double defaultFontSize = (_canvasSize.width / 25).clamp(24.0, 96.0); - final initialPos = Offset( - _canvasSize.width / 2 - 150, - _canvasSize.height / 2 - 50, - ); - - final newElement = { - 'id': id, - 'type': 'text', - 'content': 'Double tap to edit', - 'position': initialPos, - 'size': Size(300, defaultFontSize * 2), - 'rotation': 0.0, - 'style_color': Colors.black.value, - 'style_fontSize': defaultFontSize, - }; - - setState(() { - elements.add(newElement); - // Bring to front - elements.remove(newElement); - elements.add(newElement); - selectedId = id; - _isTextToolsActive = true; - }); - - _recordChange(oldState); - _enterEditMode(newElement); - } - - void _enterEditMode(Map<String, dynamic> element) { - setState(() { - selectedId = element['id']; - _isEditingText = true; - _textEditingController.text = element['content']; - _textEditingController.selection = TextSelection.fromPosition( - TextPosition(offset: _textEditingController.text.length), - ); - }); - - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_textFocusNode.canRequestFocus) { - _textFocusNode.requestFocus(); - } - }); - } - - void _exitEditMode() { - if (_isEditingText && selectedId != null) { - final index = elements.indexWhere((e) => e['id'] == selectedId); - if (index != -1) { - if (elements[index]['content'] != _textEditingController.text) { - final oldState = _getCurrentState(); - setState(() { - elements[index]['content'] = _textEditingController.text; - _isEditingText = false; - }); - _recordChange(oldState); - } else { - setState(() => _isEditingText = false); - } - } else { - setState(() => _isEditingText = false); - } - _textFocusNode.unfocus(); - } - } - - void _updateSelectedTextProperty(String key, dynamic value) { - if (selectedId == null) return; - final index = elements.indexWhere((e) => e['id'] == selectedId); - if (index == -1) return; - - final oldState = _getCurrentState(); - setState(() { - elements[index][key] = value; - }); - _recordChange(oldState); - } - - void _deleteSelectedElement() { - if (selectedId == null) return; - final oldState = _getCurrentState(); - setState(() { - elements.removeWhere((e) => e['id'] == selectedId); - selectedId = null; - _isEditingText = false; - _isTextToolsActive = false; - }); - _textFocusNode.unfocus(); - _recordChange(oldState); - } - - void _handleGestureStart() { - if (_isMagicDrawActive) { - bool hasImageLayers = elements.any((e) => e['type'] == 'file_image'); - if (hasImageLayers && !_isInpainting && _tempBaseImage == null) { - _ensureBaseImageCaptured(); - } - return; - } - _gestureStartSnapshot = _getCurrentState(); - } - - // Captures the base image by temporarily hiding the magic strokes - Future<void> _ensureBaseImageCaptured() async { - if (_tempBaseImage != null) return; - - debugPrint("📸 [Magic Draw] Hiding strokes to capture clean base..."); - - // 1. Hide strokes by updating state - setState(() => _isCapturingBase = true); - - // 2. Wait for frame to render - await Future.delayed(const Duration(milliseconds: 50)); - - // 3. Capture - try { - _tempBaseImage = await _captureCanvasToFile(); - debugPrint("✅ [Magic Draw] Base image captured."); - } catch (e) { - debugPrint("❌ [Magic Draw] Failed to capture base: $e"); - } finally { - // 4. Show strokes again - if (mounted) setState(() => _isCapturingBase = false); - } - } - - void _handleElementUpdate( - String id, - Offset newPos, - Size newSize, - double newRotation, - ) { - _resetInactivityTimer(); // Reset timer while dragging/resizing - final index = elements.indexWhere((e) => e['id'] == id); - if (index == -1) return; - - setState(() { - elements[index]['position'] = newPos; - elements[index]['size'] = newSize; - elements[index]['rotation'] = newRotation; - _hasUnsavedChanges = true; - }); - } - - void _handleGestureEnd() { - if (_gestureStartSnapshot != null) { - _recordChange(_gestureStartSnapshot!); - _gestureStartSnapshot = null; - } - } - - Future<void> _exportProject() async { - try { - // 1. Show loading or feedback - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text("Generating image..."))); - - // 2. Capture the Full Canvas using the global key - final boundary = - _canvasGlobalKey.currentContext?.findRenderObject() - as RenderRepaintBoundary?; - if (boundary == null) return; - - // 3. Convert to Image (High pixel ratio for quality) - final ui.Image image = await boundary.toImage(pixelRatio: 3.0); - final ByteData? byteData = await image.toByteData( - format: ui.ImageByteFormat.png, - ); - - if (byteData == null) return; - - final Uint8List pngBytes = byteData.buffer.asUint8List(); - - // 4. Convert PNG to JPG using 'image' package - // We do this in a compute isolate ideally, but for simplicity here on main thread - final img.Image? decodedImage = img.decodePng(pngBytes); - - if (decodedImage == null) { - throw Exception("Failed to decode image"); - } - - // Encode to JPG (Quality 90) - final Uint8List jpgBytes = img.encodeJpg(decodedImage, quality: 90); - - // 5. Save to Temporary File - final directory = await getTemporaryDirectory(); - final String fileName = - "export_${DateTime.now().millisecondsSinceEpoch}.jpg"; - final String filePath = '${directory.path}/$fileName'; - - final File imgFile = File(filePath); - await imgFile.writeAsBytes(jpgBytes); - - // 6. Trigger System Share/Save Dialog - // This allows the user to "Save to Device", "Share to Instagram", etc. - await Share.shareXFiles([ - XFile(filePath, mimeType: 'image/jpeg'), - ], text: 'Check out my design created with Adobe Clone!'); - } catch (e) { - debugPrint("Export Error: $e"); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text("Export failed: $e"))); - } - } - - // =========================================================================== - // SAVE & LOAD LOGIC - // =========================================================================== - Future<void> _handleMagicDrawExit() async { - // Use the helper method you already wrote: _confirmDiscardMagicDraw - if (_magicPaths.isNotEmpty && !_isInpainting) { - final confirm = await _confirmDiscardMagicDraw(); - if (confirm) { - _saveAndCloseMagicDraw(); - } - // If false, do nothing (stay) - } else { - _saveAndCloseMagicDraw(); - } - } - - void _handleBackNavigation() { - // If in magic draw mode, just close tool first - if (_isMagicDrawActive) { - _saveAndCloseMagicDraw(); - return; - } - - if (!_hasUnsavedChanges && widget.existingFile != null) { - Navigator.pop(context); - return; - } - - showDialog( - context: context, - builder: - (context) => AlertDialog( - title: const Text("Save Changes?"), - content: const Text( - "Do you want to save your canvas before leaving?", - ), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); // Close dialog - Navigator.pop(context); // Leave page - }, - child: const Text( - "Discard", - style: TextStyle(color: Colors.red), - ), - ), - FilledButton( - onPressed: () async { - Navigator.pop(context); // Close dialog - await _saveCanvas(); // Save - // Note: The redirect logic is handled in _saveCanvas for new files. - // For existing files, we pop here. - if (mounted && widget.existingFile != null) { - Navigator.pop(context); - } - }, - child: const Text("Save"), - ), - ], - ), - ); - } - - Future<String?> _showNameDialog() async { - TextEditingController nameController = TextEditingController( - text: "Untitled Canvas", - ); - return showDialog<String>( - context: context, - builder: - (ctx) => AlertDialog( - title: const Text("Save Canvas"), - content: TextField( - controller: nameController, - autofocus: true, - decoration: const InputDecoration( - labelText: "Canvas Name", - hintText: "Enter a name for your file", - border: OutlineInputBorder(), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: const Text("Cancel"), - ), - FilledButton( - onPressed: () => Navigator.pop(ctx, nameController.text.trim()), - child: const Text("Save"), - ), - ], - ), - ); - } - - // Helper to generate preview image - Future<String?> _generatePreviewImage() async { - try { - final boundary = - _canvasGlobalKey.currentContext?.findRenderObject() - as RenderRepaintBoundary?; - if (boundary == null) return null; - - // Capture image with lower pixel ratio for preview thumbnail - final ui.Image image = await boundary.toImage(pixelRatio: 1.0); - final ByteData? byteData = await image.toByteData( - format: ui.ImageByteFormat.png, - ); - - if (byteData == null) return null; - final Uint8List pngBytes = byteData.buffer.asUint8List(); - - final directory = await getApplicationDocumentsDirectory(); - final previewDir = Directory('${directory.path}/previews'); - if (!await previewDir.exists()) { - await previewDir.create(recursive: true); - } - - final String fileName = - "preview_${DateTime.now().millisecondsSinceEpoch}.png"; - final String filePath = '${previewDir.path}/$fileName'; - - final File imgFile = File(filePath); - await imgFile.writeAsBytes(pngBytes); - return filePath; - } catch (e) { - debugPrint("Error generating preview: $e"); - return null; - } - } - - Future<void> _saveCanvas() async { - try { - String fileName = "Canvas ${DateTime.now().toString().split(' ')[0]}"; - bool isNewFile = widget.existingFile == null; - - // 1. IF NEW FILE: Ask user for name - if (isNewFile) { - final userFileName = await _showNameDialog(); - if (userFileName == null || userFileName.isEmpty) return; // Cancelled - fileName = userFileName; - } - - // 2. Generate Preview - final String? previewPath = await _generatePreviewImage(); - - // 3. Serialize Elements AND Paths (Drawing) to JSON - final jsonList = _elementsToJson(elements); - final pathsJson = _paths.map((p) => p.toMap()).toList(); - - final saveData = { - 'elements': jsonList, - 'paths': pathsJson, - 'width': _canvasSize.width, // Saving Width - 'height': _canvasSize.height, // Saving Height - 'preview_path': previewPath, // Saving Preview Path - }; - final jsonString = jsonEncode(saveData); - - final directory = await getTemporaryDirectory(); - final tempFile = File( - '${directory.path}/canvas_temp_${DateTime.now().millisecondsSinceEpoch}.json', - ); - await tempFile.writeAsString(jsonString); - - if (widget.existingFile != null) { - // Overwrite existing file - final existingFile = File(widget.existingFile!.filePath); - await existingFile.writeAsString(jsonString); - await _fileService.openFile(widget.existingFile!.id); - } else { - // Save as new file - await _fileService.saveFile( - tempFile, - widget.projectId, - name: fileName, - description: "Editable Canvas Board", - ); - } - - if (await tempFile.exists()) await tempFile.delete(); - - setState(() => _hasUnsavedChanges = false); - - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Canvas Saved Successfully")), - ); - if (isNewFile) { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (_) => ProjectFilePage(projectId: widget.projectId), - ), - ); - } - } - } catch (e) { - debugPrint("Save Error: $e"); - } - } - - Future<void> _loadCanvasFromFile() async { - try { - final file = File(widget.existingFile!.filePath); - if (await file.exists()) { - final jsonString = await file.readAsString(); - final dynamic decoded = jsonDecode(jsonString); - - setState(() { - if (decoded is Map && decoded.containsKey('elements')) { - // New format with dimensions - elements = _jsonToElements(decoded['elements']); - if (decoded['paths'] != null) { - _paths = - (decoded['paths'] as List) - .map((p) => DrawingPath.fromMap(p)) - .toList(); - } else { - _paths = []; - } - // [FIX] LOAD CANVAS DIMENSIONS & RESET VIEW INIT - if (decoded['width'] != null && decoded['height'] != null) { - _canvasSize = Size( - (decoded['width'] as num).toDouble(), - (decoded['height'] as num).toDouble(), - ); - _hasInitializedView = false; // FORCE RE-CENTERING - } - } else if (decoded is List) { - // Legacy support - elements = _jsonToElements(decoded); - _paths = []; - _hasInitializedView = false; - } - _hasUnsavedChanges = false; - }); - - // Handle Injected Media (e.g. from Share) - if (widget.injectedMedia != null) { - final oldState = _getCurrentState(); - setState(() { - final double imageWidth = - _canvasSize.width * 0.4; // 40% of canvas width - final double imageHeight = - _canvasSize.height * 0.4; // 40% of canvas height - final Offset centeredPosition = Offset( - (_canvasSize.width - imageWidth) / 2, - (_canvasSize.height - imageHeight) / 2, - ); - elements.add({ - 'id': 'shared_${DateTime.now().millisecondsSinceEpoch}', - 'type': 'file_image', - 'content': widget.injectedMedia!.path, - 'position': centeredPosition, - 'size': Size(imageWidth, imageHeight), - 'rotation': 0.0, - }); - }); - _recordChange(oldState); - } - } - } catch (e) { - debugPrint("Error loading canvas: $e"); - } - } - - List<Map<String, dynamic>> _deepCopyElements( - List<Map<String, dynamic>> source, - ) { - return source.map((e) { - final copy = Map<String, dynamic>.from(e); - if (e['position'] is Offset) { - copy['position'] = Offset( - (e['position'] as Offset).dx, - (e['position'] as Offset).dy, - ); - } - if (e['size'] is Size) { - copy['size'] = Size( - (e['size'] as Size).width, - (e['size'] as Size).height, - ); - } - return copy; - }).toList(); - } - - List<Map<String, dynamic>> _elementsToJson( - List<Map<String, dynamic>> elements, - ) { - return elements.map((e) { - final copy = Map<String, dynamic>.from(e); - if (e['position'] is Offset) { - copy['position'] = { - 'dx': (e['position'] as Offset).dx, - 'dy': (e['position'] as Offset).dy, - }; - } - if (e['size'] is Size) { - copy['size'] = { - 'width': (e['size'] as Size).width, - 'height': (e['size'] as Size).height, - }; - } - return copy; - }).toList(); - } - - List<Map<String, dynamic>> _jsonToElements(List<dynamic> jsonList) { - return jsonList.map((item) { - final e = Map<String, dynamic>.from(item); - if (e['position'] is Map) { - e['position'] = Offset(e['position']['dx'], e['position']['dy']); - } - if (e['size'] is Map) { - e['size'] = Size(e['size']['width'], e['size']['height']); - } - e['rotation'] = (e['rotation'] as num).toDouble(); - return e; - }).toList(); - } - - void _openLayers() => _showComingSoon('Layers'); - void _openSettings() => _showComingSoon('Settings'); - void _showComingSoon([dynamic feature]) => ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Coming soon'))); - - // [UPDATED] New Bottom Sheet for Assets - void _openStylesheet() { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: - (context) => DraggableScrollableSheet( - initialChildSize: 0.7, - minChildSize: 0.5, - maxChildSize: 0.9, - builder: - (_, controller) => _AssetPickerSheet( - projectId: widget.projectId, - scrollController: controller, - onAddAssets: (List<String> paths) { - _addAssetsToCanvas(paths); - Navigator.pop(context); - }, - ), - ), - ); - } - - void _addAssetsToCanvas(List<String> paths) { - if (paths.isEmpty) return; - final oldState = _getCurrentState(); - setState(() { - for (var path in paths) { - final double imageWidth = - _canvasSize.width * 0.4; // 40% of canvas width - final double imageHeight = - _canvasSize.height * 0.4; // 40% of canvas height - final Offset centeredPosition = Offset( - (_canvasSize.width - imageWidth) / 2, - (_canvasSize.height - imageHeight) / 2, - ); - elements.add({ - 'id': - 'asset_${DateTime.now().millisecondsSinceEpoch}_${math.Random().nextInt(1000)}', - 'type': 'file_image', - 'content': path, - 'position': centeredPosition, - 'size': Size(imageWidth, imageHeight), - 'rotation': 0.0, - }); - } - _hasUnsavedChanges = true; - }); - _recordChange(oldState); - } - - // =========================================================================== - // UI BUILDER - // =========================================================================== - - @override - Widget build(BuildContext context) { - Map<String, dynamic>? selectedEl; - try { - selectedEl = elements.firstWhere((e) => e['id'] == selectedId); - } catch (_) {} - - final bool isTextSelected = - selectedEl != null && selectedEl['type'] == 'text'; - final bool showTextOverlay = _isTextToolsActive || isTextSelected; - - return PopScope( - canPop: false, - onPopInvoked: (didPop) { - if (didPop) return; - _handleBackNavigation(); - }, - child: Scaffold( - backgroundColor: const Color(0xFFE0E0E0), - appBar: _buildAppBar(), - body: LayoutBuilder( - builder: (context, constraints) { - if (!_hasInitializedView) { - _hasInitializedView = true; - final double scaleX = - (constraints.maxWidth - 40) / _canvasSize.width; - final double scaleY = - (constraints.maxHeight - 40) / _canvasSize.height; - final double initialScale = math - .min(scaleX, scaleY) - .clamp(0.01, 1.0); - - final double transX = - (constraints.maxWidth - (_canvasSize.width * initialScale)) / - 2; - final double transY = - (constraints.maxHeight - - (_canvasSize.height * initialScale)) / - 2; - - _transformationController.value = - Matrix4.identity() - ..translate(transX, transY) - ..scale(initialScale); - } - - return Stack( - children: [ - GestureDetector( - onTap: () { - if (!_isMagicDrawActive || _isMagicPanelDisabled) { - _exitEditMode(); - setState(() => selectedId = null); - } - }, - behavior: HitTestBehavior.translucent, - child: InteractiveViewer( - transformationController: _transformationController, - constrained: false, - boundaryMargin: const EdgeInsets.all(double.infinity), - minScale: 0.01, - maxScale: 10.0, - scaleEnabled: !_isMagicDrawActive || _isMagicPanelDisabled, - panEnabled: !_isMagicDrawActive || _isMagicPanelDisabled, - child: RepaintBoundary( - key: _canvasGlobalKey, - child: SizedBox( - width: _canvasSize.width, - height: _canvasSize.height, - child: Stack( - children: [ - Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.15), - blurRadius: 40, - offset: const Offset(0, 10), - ), - ], - ), - ), - ...elements.map((e) { - final bool isSelected = selectedId == e['id']; - return _ManipulatingBox( - key: ValueKey(e['id']), - id: e['id'], - position: e['position'], - size: e['size'], - rotation: e['rotation'], - type: e['type'], - content: e['content'], - styleData: e, - isSelected: isSelected && !_isMagicDrawActive, - isEditing: - isSelected && - _isEditingText && - e['type'] == 'text', - viewScale: - _transformationController.value - .getMaxScaleOnAxis(), - onTap: () { - if (!_isMagicDrawActive) { - if (_isEditingText && selectedId != e['id']) - _exitEditMode(); - setState(() { - selectedId = e['id']; - if (e['type'] == 'text') - _isTextToolsActive = true; - }); - setState(() { - elements.remove(e); - elements.add(e); - }); - } - }, - onDoubleTap: () { - if (e['type'] == 'text') _enterEditMode(e); - }, - onDragStart: _handleGestureStart, - onUpdate: - (newPos, newSize, newRot) => - _handleElementUpdate( - e['id'], - newPos, - newSize, - newRot, - ), - onDragEnd: (newPos, newSize, newRot) { - _handleElementUpdate( - e['id'], - newPos, - newSize, - newRot, - ); - _handleGestureEnd(); - }, - textController: - isSelected ? _textEditingController : null, - focusNode: isSelected ? _textFocusNode : null, - transformationController: - _transformationController, - ); - }), - // Drawing Layer - IgnorePointer( - ignoring: - !_isMagicDrawActive || _isMagicPanelDisabled, - child: RepaintBoundary( - key: _drawingKey, - child: GestureDetector( - onPanStart: (_) => _handleGestureStart(), - onPanUpdate: _onPanUpdate, - onPanEnd: (details) { - _onPanEnd(details); - _handleGestureEnd(); - }, - child: CustomPaint( - size: Size.infinite, - painter: CanvasPainter( - paths: _paths, - magicPaths: - _isCapturingBase ? [] : _magicPaths, - currentPoints: - _isCapturingBase - ? [] - : _currentPoints, - currentColor: - _isEraser - ? Colors.transparent - : _selectedColor, - currentWidth: _strokeWidth, - isEraser: _isEraser, - ), - ), - ), - ), - ), - ], - ), - ), - ), - ), - ), - - if (_aiDescription != null && _isMagicDrawActive) - Positioned( - top: 10, - left: 16, - right: 16, - child: GestureDetector( - onTap: () { - setState(() { - _isDescriptionExpanded = !_isDescriptionExpanded; - }); - }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.95), - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 2.0), - child: Icon( - Icons.auto_awesome, - size: 20, - color: Colors.indigo.shade400, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - _aiDescription!, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.black87, - ), - maxLines: _isDescriptionExpanded ? null : 1, - overflow: - _isDescriptionExpanded - ? TextOverflow.visible - : TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 8), - Icon( - _isDescriptionExpanded - ? Icons.keyboard_arrow_up - : Icons.keyboard_arrow_down, - size: 20, - color: Colors.grey[600], - ), - ], - ), - ), - ), - ), - - MagicDrawTools( - isActive: _isMagicDrawActive, - selectedColor: _selectedColor, - strokeWidth: _strokeWidth, - isEraser: _isEraser, - brandColors: _brandColors, - onClose: _handleMagicDrawExit, - onColorChanged: (c) => setState(() => _selectedColor = c), - onWidthChanged: (w) => setState(() => _strokeWidth = w), - onEraserToggle: (e) => setState(() => _isEraser = e), - onPromptSubmit: - (prompt, serviceId) => - _processInpainting(prompt, serviceId), - isProcessing: _isInpainting, - onMagicPanelActivityToggle: - (disabled) => - setState(() => _isMagicPanelDisabled = disabled), - isMagicPanelDisabled: _isMagicPanelDisabled, - onViewModeToggle: - (enabled) => setState(() => _isViewMode = enabled), - hasImageLayers: elements.any( - (e) => e['type'] == 'file_image', - ), - ), - - TextToolsOverlay( - isActive: showTextOverlay && !_isMagicDrawActive, - isTextSelected: isTextSelected, - currentColor: Color( - selectedEl?['style_color'] ?? Colors.black.value, - ), - currentFontSize: - (selectedEl?['style_fontSize'] ?? 24.0) as double, - onClose: () { - _exitEditMode(); - setState(() { - _isTextToolsActive = false; - selectedId = null; - }); - }, - onAddText: _addTextElement, - onColorChanged: - (c) => - _updateSelectedTextProperty('style_color', c.value), - onFontSizeChanged: - (s) => _updateSelectedTextProperty('style_fontSize', s), - ), - - // Background Removal Banner - if (_showBgRemovalBanner) - Positioned( - top: SafeArea(child: Container()).minimum.top + 10, - left: 0, - right: 0, - child: Center( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.black87, - borderRadius: BorderRadius.circular(30), - boxShadow: [ - BoxShadow(color: Colors.black26, blurRadius: 8, offset: Offset(0, 2)) - ] - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (_isRemovingBg) ...[ - const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)), - const SizedBox(width: 8), - const Text("Removing...", style: TextStyle(color: Colors.white, fontFamily: 'GeneralSans', fontSize: 14)), - ] else ...[ - const Text( - "Remove background?", - style: TextStyle(color: Colors.white, fontFamily: 'GeneralSans', fontSize: 14), - ), - const SizedBox(width: 8), - InkWell( - onTap: _confirmRemoveBackground, - child: Container( - padding: const EdgeInsets.all(4), - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: const Icon(Icons.check, size: 16, color: Colors.black), - ), - ), - const SizedBox(width: 8), - InkWell( - onTap: () => setState(() => _showBgRemovalBanner = false), - child: const Icon(Icons.close, size: 18, color: Colors.white54), - ), - ] - ], - ), - ), - ), - ), - - Positioned( - bottom: 0, - left: 0, - right: 0, - child: CanvasBottomBar( - activeItem: - _isMagicDrawActive - ? "Magic Draw" - : (_isTextToolsActive ? "Text" : null), - onMagicDraw: () async { - if (_isMagicDrawActive) { - final confirm = await _confirmDiscardMagicDraw(); - if (!confirm) return; - setState(() { - _magicPaths.clear(); - _magicDrawChangeStack.clear(); - _tempBaseImage = null; - _isMagicDrawActive = false; - }); - return; - } - setState(() { - _isMagicDrawActive = true; - _isTextToolsActive = false; - _exitEditMode(); - _magicPaths.clear(); - _magicDrawChangeStack.clear(); - _tempBaseImage = null; - }); - }, - onMedia: () async { - if (_isMagicDrawActive) { - final confirm = await _confirmDiscardMagicDraw(); - if (!confirm) return; - setState(() { - _magicPaths.clear(); - _magicDrawChangeStack.clear(); - _tempBaseImage = null; - _isMagicDrawActive = false; - }); - } - _pickImageFromGallery(); - }, - onStylesheet: () async { - if (_isMagicDrawActive) { - final confirm = await _confirmDiscardMagicDraw(); - if (!confirm) return; - setState(() { - _magicPaths.clear(); - _magicDrawChangeStack.clear(); - _tempBaseImage = null; - _isMagicDrawActive = false; - }); - } - _openStylesheet(); - }, - onTools: () async { - if (_isMagicDrawActive) { - final confirm = await _confirmDiscardMagicDraw(); - if (!confirm) return; - setState(() { - _magicPaths.clear(); - _magicDrawChangeStack.clear(); - _tempBaseImage = null; - _isMagicDrawActive = false; - }); - } - _showComingSoon('Tools'); - }, - onText: () async { - if (_isMagicDrawActive) { - final confirm = await _confirmDiscardMagicDraw(); - if (!confirm) return; - setState(() { - _magicPaths.clear(); - _magicDrawChangeStack.clear(); - _tempBaseImage = null; - _isMagicDrawActive = false; - }); - } - _toggleTextTools(); - }, - onSelect: () async { - if (_isMagicDrawActive) { - final confirm = await _confirmDiscardMagicDraw(); - if (!confirm) return; - setState(() { - _magicPaths.clear(); - _magicDrawChangeStack.clear(); - _tempBaseImage = null; - _isMagicDrawActive = false; - }); - } - _showComingSoon('Select'); - }, - onPlugins: () async { - if (_isMagicDrawActive) { - final confirm = await _confirmDiscardMagicDraw(); - if (!confirm) return; - setState(() { - _magicPaths.clear(); - _magicDrawChangeStack.clear(); - _tempBaseImage = null; - _isMagicDrawActive = false; - }); - } - _showComingSoon('Plugins'); - }, - ), - ), - ], - ); - }, - ), - ), - ); - } - - // --- DRAWING HELPERS --- - - void _onPanUpdate(DragUpdateDetails details) { - setState(() { - _currentPoints.add( - DrawingPoint(offset: details.localPosition, paint: Paint()), - ); - }); - - // Throttle Analysis: Trigger "Describe" every 2.5s while actively drawing - if (_isMagicDrawActive && - !_isAnalyzing && - !_isInpainting && - DateTime.now().difference(_lastAnalysisTime).inMilliseconds > 2500) { - _lastAnalysisTime = DateTime.now(); - _analyzeCanvas(); - } - } - - Future<void> _onPanEnd(DragEndDetails details) async { - setState(() { - if (_currentPoints.isNotEmpty) { - if (_isMagicDrawActive) { - // ADD TO MAGIC PATHS (Temporary mask) - final oldMagicPaths = List<DrawingPath>.from(_magicPaths); - _magicPaths.add( - DrawingPath( - points: List.from(_currentPoints), - color: _selectedColor, - strokeWidth: _strokeWidth, - isEraser: _isEraser, - ), - ); - _recordMagicChange(oldMagicPaths); - _currentPoints = []; - } else { - // NORMAL DRAWING - _paths.add( - DrawingPath( - points: List.from(_currentPoints), - color: _selectedColor, - strokeWidth: _strokeWidth, - isEraser: _isEraser, - ), - ); - _currentPoints = []; - _hasUnsavedChanges = true; - _resetInactivityTimer(); - } - } - }); - } - - // --- BACKGROUND REMOVAL LOGIC --- - - void _triggerBgRemovalBanner(String elementId, String imagePath) { - _bgRemovalBannerTimer?.cancel(); - setState(() { - _showBgRemovalBanner = true; - _bgRemovalTargetId = elementId; - _bgRemovalTargetPath = imagePath; - }); - - // Automatically hide after 10 seconds - _bgRemovalBannerTimer = Timer(const Duration(seconds: 10), () { - if (mounted) { - setState(() => _showBgRemovalBanner = false); - } - }); - } - - Future<void> _confirmRemoveBackground() async { - if (_bgRemovalTargetId == null || _bgRemovalTargetPath == null) return; - - _bgRemovalBannerTimer?.cancel(); - - setState(() { - _isRemovingBg = true; - }); - - try { - // Call service to remove background - final newPath = await FlaskService().generateAsset(imagePath: _bgRemovalTargetPath!); - - if (newPath != null && mounted) { - // Find the element and update its content - final index = elements.indexWhere((e) => e['id'] == _bgRemovalTargetId); - if (index != -1) { - final oldState = _getCurrentState(); - setState(() { - elements[index]['content'] = newPath; // Replace with transparent png - _showBgRemovalBanner = false; - _isRemovingBg = false; - _hasUnsavedChanges = true; - }); - _recordChange(oldState); - } - } - } catch (e) { - debugPrint("BG Removal Failed: $e"); - } finally { - if (mounted) setState(() { - _isRemovingBg = false; - _showBgRemovalBanner = false; - }); - } - } - - Future<void> _processInpainting(String prompt, String modelId) async { - // Lock editing and hide keyboard - FocusScope.of(context).unfocus(); - - setState(() => _isInpainting = true); - _resetInactivityTimer(); - - try { - // Check if there are image layers (which dictates the context) - bool hasImageLayers = elements.any((e) => e['type'] == 'file_image'); - - if (_tempBaseImage == null) { - _tempBaseImage = await _captureCanvasToFile(); - } - if (_tempBaseImage == null) return; - - File? maskFile = await _generateMaskImageFromPaths( - _magicPaths, - _canvasSize, - _tempBaseImage, - ); - - if (maskFile == null) throw Exception("Failed to generate mask"); - - String? newImageUrl; - - // --- LOGIC SWITCHING BASED ON MODEL ID --- - - // CASE 1: INPAINTING (When hasImageLayers is TRUE) - if (hasImageLayers) { - if (modelId == 'inpaint_api') { - // Call API Inpainting - newImageUrl = await FlaskService().inpaintApiImage( - imagePath: _tempBaseImage!.path, - maskPath: maskFile.path, - prompt: prompt, - ); - } else { - // Local Inpainting - newImageUrl = await FlaskService().inpaintImage( - imagePath: _tempBaseImage!.path, - maskPath: maskFile.path, - prompt: prompt, - ); - } - } - // CASE 2: SKETCH TO IMAGE (When hasImageLayers is FALSE) - else { - if(modelId == 'sketch_fusion'){ - newImageUrl = await FlaskService().sketchToImage( - projectId: widget.projectId, - sketchPath: _tempBaseImage!.path, - userPrompt: prompt, - imageDescription: _aiDescription, - ); - } - else if(modelId == 'sketch_advanced'){ - newImageUrl = await FlaskService().sketchToImageAPI( - projectId: widget.projectId, - sketchPath: _tempBaseImage!.path, - userPrompt: prompt, - option: 1, - imageDescription: _aiDescription, - ); - } - else if (modelId == 'sketch_creative') { - newImageUrl = await FlaskService().sketchToImageAPI( - projectId: widget.projectId, - sketchPath: _tempBaseImage!.path, - userPrompt: prompt, - option: 2, - imageDescription: _aiDescription, - ); - } - } - - if (newImageUrl != null) { - final id = _addGeneratedImage(newImageUrl); - setState(() { - _isMagicDrawActive = false; - }); - - // Trigger Banner for Background Removal (Only if it was a generation, not inpainting) - if (!hasImageLayers) { - _triggerBgRemovalBanner(id, newImageUrl); - } - } - } catch (e) { - debugPrint("Generation Error: $e"); - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text("Generation failed."))); - } finally { - setState(() { - _isInpainting = false; - _tempBaseImage = null; - _magicPaths.clear(); - _magicDrawChangeStack.clear(); - }); - } - } - - String _addGeneratedImage(String? newImageUrl) { - String id = ''; - if (newImageUrl != null) { - debugPrint("✅ Adding generated image to canvas: $newImageUrl"); - id = 'gen_${DateTime.now().millisecondsSinceEpoch}'; - setState(() { - elements.add({ - 'id': id, - 'type': 'file_image', - 'content': newImageUrl, - 'position': const Offset(0, 0), - 'size': _canvasSize, - 'rotation': 0.0, - }); - _magicPaths.clear(); - }); - } - return id; - } - - // Updated to generate mask as: Base Image + Drawing Strokes - Future<File?> _generateMaskImageFromPaths( - List<DrawingPath> paths, - Size size, - File? baseImageFile, - ) async { - try { - final recorder = ui.PictureRecorder(); - final canvas = Canvas( - recorder, - Rect.fromLTWH(0, 0, size.width, size.height), - ); - - // 1. Draw Base Image First (if available) - if (baseImageFile != null) { - final data = await baseImageFile.readAsBytes(); - final codec = await ui.instantiateImageCodec(data); - final frameInfo = await codec.getNextFrame(); - final baseImage = frameInfo.image; - - paintImage( - canvas: canvas, - rect: Rect.fromLTWH(0, 0, size.width, size.height), - image: baseImage, - fit: BoxFit.cover, // Or contain, depending on your logic - ); - } else { - // Fallback to black if no base image (shouldn't happen based on logic) - canvas.drawRect( - Rect.fromLTWH(0, 0, size.width, size.height), - Paint()..color = Colors.black, - ); - } - - // 2. Draw Strokes ON TOP of the base image - for (final path in paths) { - final paint = - Paint() - ..color = - path - .color // Use the drawing color (e.g. Blue) - ..strokeWidth = path.strokeWidth - ..strokeCap = StrokeCap.round - ..strokeJoin = StrokeJoin.round - ..style = PaintingStyle.stroke; - - if (path.points.length > 1) { - final Path p = Path(); - p.moveTo(path.points.first.offset.dx, path.points.first.offset.dy); - for (int i = 1; i < path.points.length; i++) { - p.lineTo(path.points[i].offset.dx, path.points[i].offset.dy); - } - canvas.drawPath(p, paint); - } else if (path.points.isNotEmpty) { - canvas.drawPoints(ui.PointMode.points, [ - path.points.first.offset, - ], paint); - } - } - - final picture = recorder.endRecording(); - final img = await picture.toImage( - size.width.toInt(), - size.height.toInt(), - ); - final byteData = await img.toByteData(format: ui.ImageByteFormat.png); - if (byteData == null) return null; - - final tempDir = await getTemporaryDirectory(); - final file = File( - '${tempDir.path}/mask_${DateTime.now().millisecondsSinceEpoch}.png', - ); - await file.writeAsBytes(byteData.buffer.asUint8List()); - return file; - } catch (e) { - debugPrint("Mask Generation Error: $e"); - return null; - } - } - - Future<void> _saveAndCloseMagicDraw() async { - setState(() { - _isMagicDrawActive = false; - _magicPaths.clear(); - _magicDrawChangeStack.clear(); - _tempBaseImage = null; - }); - } - - PreferredSizeWidget _buildAppBar() { - final bool canUndo = - _isMagicDrawActive - ? _magicDrawChangeStack.canUndo - : _changeStack.canUndo; - final bool canRedo = - _isMagicDrawActive - ? _magicDrawChangeStack.canRedo - : _changeStack.canRedo; - return AppBar( - leadingWidth: 160, - leading: SafeArea( - child: Row( - children: [ - IconButton( - icon: SvgPicture.asset( - 'assets/icons/arrow-left-s-line.svg', - width: 22, - colorFilter: const ColorFilter.mode( - Colors.black, - BlendMode.srcIn, - ), - ), - onPressed: () { - if (_isMagicDrawActive) { - _handleMagicDrawExit(); - } else { - _handleBackNavigation(); - } - }, - ), - IconButton( - icon: SvgPicture.asset( - 'assets/icons/arrow-go-back-line.svg', - width: 22, - colorFilter: ColorFilter.mode( - canUndo ? Colors.black : Colors.grey[400]!, - BlendMode.srcIn, - ), - ), - onPressed: - canUndo - ? () => setState( - () => - _isMagicDrawActive - ? _magicDrawChangeStack.undo() - : _changeStack.undo(), - ) - : null, - ), - IconButton( - icon: SvgPicture.asset( - 'assets/icons/arrow-go-forward-line.svg', - width: 22, - colorFilter: ColorFilter.mode( - canRedo ? Colors.black : Colors.grey[400]!, - BlendMode.srcIn, - ), - ), - onPressed: - canRedo - ? () => setState( - () => - _isMagicDrawActive - ? _magicDrawChangeStack.redo() - : _changeStack.redo(), - ) - : null, - ), - ], - ), - ), - backgroundColor: Colors.white, - foregroundColor: Colors.black, - elevation: 0, - actions: [ - SafeArea( - child: Row( - children: [ - if (selectedId != null) - IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.red), - onPressed: _deleteSelectedElement, - tooltip: "Delete Selected", - ), - IconButton( - icon: SvgPicture.asset( - 'assets/icons/save-3-line.svg', - width: 22, - ), - onPressed: _saveCanvas, - ), - IconButton( - icon: SvgPicture.asset( - 'assets/icons/upload-2-line.svg', - width: 22, - ), - onPressed: _exportProject, - ), - IconButton( - icon: SvgPicture.asset( - 'assets/icons/settings-line.svg', - width: 22, - ), - onPressed: _openSettings, - ), - const SizedBox(width: 8), - ], - ), - ), - ], - ); - } - - Future<void> _pickImageFromGallery() async { - final List<XFile> images = await _picker.pickMultiImage(); - if (images.isNotEmpty) { - final oldState = _getCurrentState(); - setState(() { - for (int i = 0; i < images.length; i++) { - final double imageWidth = - _canvasSize.width * 0.4; // 40% of canvas width - final double imageHeight = - _canvasSize.height * 0.4; // 40% of canvas height - final Offset centeredPosition = Offset( - (_canvasSize.width - imageWidth) / 2, - (_canvasSize.height - imageHeight) / 2, - ); - elements.add({ - 'id': '${DateTime.now().millisecondsSinceEpoch}_$i', - 'type': 'file_image', - 'content': images[i].path, - 'position': centeredPosition, - 'size': Size(imageWidth, imageHeight), - 'rotation': 0.0, - }); - } - _hasUnsavedChanges = true; - }); - _recordChange(oldState); - } - } -} - -// ============================================================================= -// MANIPULATING BOX WIDGET -// ============================================================================= - -class _ManipulatingBox extends StatefulWidget { - final String id; - final Offset position; - final Size size; - final double rotation; - final String type; - final String content; - final Map<String, dynamic> styleData; - final bool isSelected; - final bool isEditing; - final double viewScale; - final TransformationController transformationController; - final VoidCallback onTap; - final VoidCallback onDoubleTap; - final VoidCallback onDragStart; - final Function(Offset, Size, double) onUpdate; - final Function(Offset, Size, double) onDragEnd; - final TextEditingController? textController; - final FocusNode? focusNode; - - const _ManipulatingBox({ - Key? key, - required this.id, - required this.position, - required this.size, - required this.rotation, - required this.type, - required this.content, - required this.styleData, - required this.isSelected, - required this.isEditing, - required this.viewScale, - required this.transformationController, - required this.onTap, - required this.onDoubleTap, - required this.onDragStart, - required this.onUpdate, - required this.onDragEnd, - this.textController, - this.focusNode, - }) : super(key: key); - - @override - State<_ManipulatingBox> createState() => _ManipulatingBoxState(); -} - -class _ManipulatingBoxState extends State<_ManipulatingBox> { - late Offset _pos; - late Size _size; - late double _rot; - - // Gesture state - double _initialRotation = 0.0; - double _initialScale = 1.0; - bool _isTwoFingerGesture = false; - Offset _previousFocalPoint = Offset.zero; - - @override - void initState() { - super.initState(); - _updateInternalState(); - } - - @override - void didUpdateWidget(_ManipulatingBox oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.position != oldWidget.position || - widget.size != oldWidget.size || - widget.rotation != oldWidget.rotation) { - _updateInternalState(); - } - } - - void _updateInternalState() { - _pos = widget.position; - _size = widget.size; - _rot = widget.rotation; - } - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: widget.transformationController, - builder: (context, matrix, child) { - final double zoom = matrix.getMaxScaleOnAxis(); - final double handleScale = (1 / zoom).clamp(0.2, 5.0); - final double edgeThickness = 18 * handleScale; - - return Positioned( - left: _pos.dx, - top: _pos.dy, - child: Transform.rotate( - angle: _rot, - child: Stack( - clipBehavior: Clip.none, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: widget.onTap, - onDoubleTap: widget.onDoubleTap, - // Handle both single-finger drag and two-finger rotate/zoom using scale gestures - onScaleStart: (details) { - if (widget.isSelected && !widget.isEditing) { - _previousFocalPoint = details.focalPoint; - if (details.pointerCount == 2) { - // Two-finger gesture: rotate and zoom - _isTwoFingerGesture = true; - _initialRotation = _rot; - _initialScale = _size.width * _size.height; - } else { - // Single-finger gesture: prepare for drag - _isTwoFingerGesture = false; - } - widget.onDragStart(); - } - }, - onScaleUpdate: (details) { - if (widget.isSelected && !widget.isEditing) { - if (_isTwoFingerGesture && details.pointerCount == 2) { - // Two-finger: handle rotation and zoom - final newRotation = _initialRotation + details.rotation; - - // Handle scale (zoom) - maintain aspect ratio - final scaleFactor = details.scale; - final newArea = - _initialScale * scaleFactor * scaleFactor; - final aspectRatio = _size.width / _size.height; - final newWidth = math - .sqrt(newArea * aspectRatio) - .clamp(20.0, 5000.0); - final newHeight = newWidth / aspectRatio; - - setState(() { - _rot = newRotation % (2 * math.pi); - _size = Size(newWidth, newHeight); - }); - - widget.onUpdate(_pos, _size, _rot); - } else if (!_isTwoFingerGesture && - details.pointerCount == 1) { - // Single-finger: handle drag using incremental focal point delta - final currentFocalPoint = details.focalPoint; - final delta = currentFocalPoint - _previousFocalPoint; - // Convert to canvas coordinates by dividing by zoom - final zoom = widget.viewScale; - final scaledDelta = delta / zoom; - // Rotate delta to account for element rotation - final rotated = _rotateVector(scaledDelta, -_rot); - setState(() { - _pos += scaledDelta; - _previousFocalPoint = - currentFocalPoint; // Update for next frame - }); - widget.onUpdate(_pos, _size, _rot); - } - } - }, - onScaleEnd: (details) { - if (widget.isSelected && !widget.isEditing) { - _isTwoFingerGesture = false; - widget.onDragEnd(_pos, _size, _rot); - } - }, - child: Container( - width: _size.width, - height: _size.height, - decoration: - widget.isSelected - ? BoxDecoration( - border: Border.all( - color: Color(0xFFB44CFF), - width: 2 * handleScale, - ), - ) - : null, - child: - widget.type == "file_image" - ? Image.file( - File(widget.content), - fit: BoxFit.contain, - ) - : _buildText(), - ), - ), - - // ====================== - // RESIZE EDGES (4 SIDES) - // ====================== - - // RIGHT edge - if (widget.isSelected && !widget.isEditing) - Positioned( - right: -edgeThickness / 2, - top: 0, - bottom: 0, - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onPanUpdate: (d) { - setState(() { - _size = Size(_size.width + d.delta.dx, _size.height); - }); - widget.onUpdate(_pos, _size, _rot); - }, - onPanStart: (_) => widget.onDragStart(), - onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot), - child: Container( - width: edgeThickness, - color: Colors.transparent, - ), - ), - ), - - // LEFT edge - if (widget.isSelected && !widget.isEditing) - Positioned( - left: -edgeThickness / 2, - top: 0, - bottom: 0, - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onPanUpdate: (d) { - setState(() { - _pos += Offset(d.delta.dx, 0); - _size = Size(_size.width - d.delta.dx, _size.height); - }); - widget.onUpdate(_pos, _size, _rot); - }, - onPanStart: (_) => widget.onDragStart(), - onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot), - child: Container( - width: edgeThickness, - color: Colors.transparent, - ), - ), - ), - - // TOP edge - if (widget.isSelected && !widget.isEditing) - Positioned( - top: -edgeThickness / 2, - left: 0, - right: 0, - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onPanUpdate: (d) { - setState(() { - _pos += Offset(0, d.delta.dy); - _size = Size(_size.width, _size.height - d.delta.dy); - }); - widget.onUpdate(_pos, _size, _rot); - }, - onPanStart: (_) => widget.onDragStart(), - onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot), - child: Container( - height: edgeThickness, - color: Colors.transparent, - ), - ), - ), - - // BOTTOM edge - if (widget.isSelected && !widget.isEditing) - Positioned( - bottom: -edgeThickness / 2, - left: 0, - right: 0, - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onPanUpdate: (d) { - setState(() { - _size = Size(_size.width, _size.height + d.delta.dy); - }); - widget.onUpdate(_pos, _size, _rot); - }, - onPanStart: (_) => widget.onDragStart(), - onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot), - child: Container( - height: edgeThickness, - color: Colors.transparent, - ), - ), - ), - ], - ), - ), - ); - }, - ); - } - - Widget _buildCircleButton({ - required double size, - required IconData icon, - required double iconSize, - required Function(DragUpdateDetails) onDrag, - }) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onPanStart: (details) => widget.onDragStart(), - onPanUpdate: onDrag, - onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot), - child: Container( - width: size, - height: size, - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.15), - blurRadius: 4, - offset: Offset(0, 2), - ), - ], - ), - child: Center(child: Icon(icon, size: iconSize, color: Colors.black87)), - ), - ); - } - - Widget _buildText() { - final style = TextStyle( - fontSize: (widget.styleData['style_fontSize'] ?? 24.0) as double, - color: Color(widget.styleData['style_color'] ?? Colors.black.value), - fontFamily: 'GeneralSans', - ); - - if (widget.isEditing) { - return Center( - child: IntrinsicWidth( - child: TextField( - controller: widget.textController, - focusNode: widget.focusNode, - autofocus: true, - maxLines: null, - textAlign: TextAlign.center, - style: style, - decoration: const InputDecoration( - border: InputBorder.none, - isDense: true, - contentPadding: EdgeInsets.zero, - ), - onChanged: (text) { - final span = TextSpan(text: text, style: style); - final tp = TextPainter( - text: span, - textDirection: TextDirection.ltr, - ); - tp.layout(maxWidth: 10000); - setState(() { - _size = Size(tp.width + 40, tp.height + 40); - }); - widget.onUpdate(_pos, _size, _rot); - }, - ), - ), - ); - } - return Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text(widget.content, textAlign: TextAlign.center, style: style), - ), - ); - } - - Widget _buildHandle({ - required double touchSize, - required double visualSize, - required IconData icon, - required Color color, - required Function(Offset) onDrag, - }) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onPanStart: (_) => widget.onDragStart(), - onPanUpdate: (details) => onDrag(details.delta), - onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot), - child: Container( - width: touchSize, - height: touchSize, - alignment: Alignment.center, - color: Colors.transparent, - child: Container( - width: visualSize, - height: visualSize, - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow(color: Colors.black.withOpacity(0.26), blurRadius: 4), - ], - ), - child: Icon(icon, size: visualSize * 0.6, color: Colors.white), - ), - ), - ); - } - - Offset _rotateVector(Offset vector, double angle) { - final cosA = math.cos(angle); - final sinA = math.sin(angle); - return Offset( - vector.dx * cosA - vector.dy * sinA, - vector.dx * sinA + vector.dy * cosA, - ); - } -} - -// --- PAINTER --- -class CanvasPainter extends CustomPainter { - final List<DrawingPath> paths; - final List<DrawingPath> magicPaths; // ADDED: Magic paths for temporary mask - final List<DrawingPoint> currentPoints; - final Color currentColor; - final double currentWidth; - final bool isEraser; - CanvasPainter({ - required this.paths, - this.magicPaths = const [], // ADDED - required this.currentPoints, - required this.currentColor, - required this.currentWidth, - required this.isEraser, - }); - - @override - void paint(Canvas canvas, Size size) { - canvas.saveLayer(Rect.fromLTWH(0, 0, size.width, size.height), Paint()); - - // Draw normal paths - for (final path in paths) { - _drawPath(canvas, path); - } - - // Draw magic paths (unless hidden by empty list passed in) - for (final path in magicPaths) { - _drawPath(canvas, path); - } - - // Draw current points - if (currentPoints.isNotEmpty) { - final paint = - Paint() - ..color = isEraser ? Colors.transparent : currentColor - ..blendMode = isEraser ? BlendMode.clear : BlendMode.srcOver - ..strokeWidth = currentWidth - ..strokeCap = StrokeCap.round - ..strokeJoin = StrokeJoin.round - ..style = PaintingStyle.stroke; - final Path p = Path(); - p.moveTo(currentPoints.first.offset.dx, currentPoints.first.offset.dy); - for (int i = 1; i < currentPoints.length; i++) - p.lineTo(currentPoints[i].offset.dx, currentPoints[i].offset.dy); - canvas.drawPath(p, paint); - } - canvas.restore(); - } - - void _drawPath(Canvas canvas, DrawingPath path) { - final paint = - Paint() - ..color = path.isEraser ? Colors.transparent : path.color - ..blendMode = path.isEraser ? BlendMode.clear : BlendMode.srcOver - ..strokeWidth = path.strokeWidth - ..strokeCap = StrokeCap.round - ..strokeJoin = StrokeJoin.round - ..style = PaintingStyle.stroke; - - if (path.points.length > 1) { - final Path p = Path(); - p.moveTo(path.points.first.offset.dx, path.points.first.offset.dy); - for (int i = 1; i < path.points.length; i++) - p.lineTo(path.points[i].offset.dx, path.points[i].offset.dy); - canvas.drawPath(p, paint); - } else if (path.points.isNotEmpty) { - canvas.drawPoints(ui.PointMode.points, [path.points.first.offset], paint); - } - } - - @override - bool shouldRepaint(covariant CanvasPainter oldDelegate) => true; -} - -class CanvasBottomBar extends StatelessWidget { - final String? activeItem; - final VoidCallback onMagicDraw; - final VoidCallback onMedia; - final VoidCallback onStylesheet; - final VoidCallback onTools; - final VoidCallback onText; - final VoidCallback onSelect; - final VoidCallback onPlugins; - const CanvasBottomBar({ - super.key, - this.activeItem, - required this.onMagicDraw, - required this.onMedia, - required this.onStylesheet, - required this.onTools, - required this.onText, - required this.onSelect, - required this.onPlugins, - }); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Colors.white, - border: Border(top: BorderSide(color: Colors.grey[200]!)), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, -5), - ), - ], - ), - child: SafeArea( - top: false, // We don't need top SafeArea as it's a bottom bar - child: Container( - padding: const EdgeInsets.symmetric(vertical: 10), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - _BottomBarItem( - label: "Magic Draw", - iconPath: "assets/icons/magic_draw.svg", - onTap: onMagicDraw, - isActive: activeItem == "Magic Draw", - ), - const SizedBox(width: 24), - _BottomBarItem( - label: "Media", - iconPath: "assets/icons/media.svg", - onTap: onMedia, - ), - const SizedBox(width: 24), - _BottomBarItem( - label: "Stylesheet", - iconPath: "assets/icons/stylesheet.svg", - onTap: onStylesheet, - ), - const SizedBox(width: 24), - _BottomBarItem( - label: "Tools", - iconPath: "assets/icons/tools.svg", - onTap: onTools, - ), - const SizedBox(width: 24), - _BottomBarItem( - label: "Text", - iconPath: "assets/icons/text.svg", - onTap: onText, - isActive: activeItem == "Text", - ), - const SizedBox(width: 24), - _BottomBarItem( - label: "Select", - iconPath: "assets/icons/select.svg", - onTap: onSelect, - ), - const SizedBox(width: 24), - _BottomBarItem( - label: "Plugins", - iconPath: "assets/icons/plugins.svg", - onTap: onPlugins, - ), - ], - ), - ), - ), - ), - ); - } -} - -class _BottomBarItem extends StatelessWidget { - final String label; - final String iconPath; - final VoidCallback onTap; - final bool isActive; - const _BottomBarItem({ - required this.label, - required this.iconPath, - required this.onTap, - this.isActive = false, - }); - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - iconPath, - width: 24, - colorFilter: ColorFilter.mode( - isActive ? const Color(0xFF27272A) : const Color(0xFF9F9FA9), - BlendMode.srcIn, - ), - ), - const SizedBox(height: 6), - Text( - label, - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w500, - color: - isActive - ? const Color(0xFF27272A) - : const Color(0xFF9F9FA9), - ), - ), - ], - ), - ), - ); - } -} - -class _AssetPickerSheet extends StatefulWidget { - final int projectId; - final ScrollController scrollController; - final Function(List<String>) onAddAssets; // Accepts List - - const _AssetPickerSheet({ - required this.projectId, - required this.scrollController, - required this.onAddAssets, - }); - - @override - State<_AssetPickerSheet> createState() => _AssetPickerSheetState(); -} - -class _AssetPickerSheetState extends State<_AssetPickerSheet> { - List<String> _assets = []; - bool _isLoading = true; - Set<String> _selectedPaths = {}; // Supports multi-select - - @override - void initState() { - super.initState(); - _loadAssets(); - } - - Future<void> _loadAssets() async { - try { - final project = await ProjectRepo().getProjectById(widget.projectId); - if (mounted) { - setState(() { - _assets = project?.assetsPath ?? []; - _isLoading = false; - }); - } - } catch (e) { - debugPrint("Error loading assets: $e"); - if (mounted) setState(() => _isLoading = false); - } - } - - Future<File?> _resolveFile(String path) async { - final file = File(path); - if (await file.exists()) return file; - try { - final filename = p.basename(path); - final dir = await getApplicationDocumentsDirectory(); - final fixedPath = '${dir.path}/generated_images/$filename'; - final fixedFile = File(fixedPath); - if (await fixedFile.exists()) return fixedFile; - } catch (e) { - debugPrint("Error resolving file: $e"); - } - return null; - } - - @override - Widget build(BuildContext context) { - return Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(24)), - ), - padding: const EdgeInsets.fromLTRB(20, 12, 20, 0), - child: Column( - children: [ - // Handle - Container( - width: 40, - height: 4, - margin: const EdgeInsets.only(bottom: 20), - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(2), - ), - ), - - // Search Bar - Container( - height: 40, - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - const SizedBox(width: 12), - const Icon(Icons.search, color: Colors.grey), - const SizedBox(width: 8), - const Text( - "Search Stylesheet", - style: TextStyle( - fontFamily: 'GeneralSans', - color: Colors.grey, - ), - ), - ], - ), - ), - - // Category Tabs - Container( - margin: const EdgeInsets.only(bottom: 16), - child: Row( - children: [ - _buildFilterChip("Assets", true), - const SizedBox(width: 12), - _buildFilterChip("Backgrounds & Texture", false), - ], - ), - ), - - // Grid - Expanded( - child: - _isLoading - ? const Center(child: CircularProgressIndicator()) - : _assets.isEmpty - ? Center( - child: Text( - "No assets found in stylesheet", - style: TextStyle(color: Colors.grey[500]), - ), - ) - : Stack( - children: [ - GridView.builder( - controller: widget.scrollController, - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 12, - mainAxisSpacing: 12, - childAspectRatio: 1.0, - ), - itemCount: _assets.length, - itemBuilder: (context, index) { - final assetPath = _assets[index]; - final isSelected = _selectedPaths.contains( - assetPath, - ); - - return FutureBuilder<File?>( - future: _resolveFile(assetPath), - builder: (context, snapshot) { - final file = snapshot.data; - return _buildAssetTile( - child: - file != null - ? Image.file(file, fit: BoxFit.cover) - : const Icon( - Icons.broken_image, - color: Colors.grey, - ), - isSelected: isSelected, - onTap: () { - if (file != null) { - setState(() { - if (isSelected) { - _selectedPaths.remove(assetPath); - } else { - _selectedPaths.add(assetPath); - } - }); - } - }, - ); - }, - ); - }, - ), - ], - ), - ), - - // Bottom CTA - SafeArea( - top: false, - child: Container( - width: double.infinity, - margin: const EdgeInsets.only(top: 16, bottom: 16), - child: ElevatedButton( - onPressed: () => widget.onAddAssets(_selectedPaths.toList()), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF27272A), - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), - ), - ), - child: const Text( - "Add to File", - style: TextStyle( - fontFamily: 'GeneralSans', - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ), - ], - ), - ); - } - - Widget _buildFilterChip(String label, bool isSelected) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: isSelected ? const Color(0xFFF4F4F5) : Colors.transparent, - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: isSelected ? Colors.transparent : Colors.grey[300]!, - ), - ), - child: Text( - label, - style: TextStyle( - fontFamily: 'GeneralSans', - fontSize: 14, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, - color: isSelected ? Colors.black : Colors.grey[600], - ), - ), - ); - } - - Widget _buildAssetTile({ - required Widget child, - required VoidCallback onTap, - bool isSelected = false, - }) { - return GestureDetector( - onTap: onTap, - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isSelected ? Colors.blue : Colors.grey[200]!, - width: isSelected ? 2 : 1, - ), - ), - clipBehavior: Clip.antiAlias, - child: Stack( - fit: StackFit.expand, - children: [ - child, - if (isSelected) - Container( - color: Colors.blue.withOpacity(0.1), - child: const Center( - child: Icon(Icons.check_circle, color: Colors.blue), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/ui/pages/canvas_page.dart b/lib/ui/pages/canvas_page.dart @@ -0,0 +1,2813 @@ +import 'dart:io'; +import 'dart:convert'; +import 'dart:async'; +import 'dart:math' as math; +import 'dart:ui' as ui; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:undo/undo.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:image/image.dart' as img; +import 'package:path/path.dart' as p; +import 'package:creekui/data/repos/project_repo.dart'; +import 'package:creekui/data/models/file_model.dart'; +import 'package:creekui/services/stylesheet_service.dart'; +import 'package:creekui/services/file_service.dart'; +import 'package:creekui/services/flask_service.dart'; +import './canvas_toolbar/magic_draw_overlay.dart'; +import './canvas_toolbar/text_tools_overlay.dart'; +import 'project_file_page.dart'; + +// --- MODELS WITH JSON SUPPORT --- + +class DrawingPoint { + final Offset offset; + final Paint paint; + const DrawingPoint({required this.offset, required this.paint}); + + Map<String, dynamic> toMap() => {'dx': offset.dx, 'dy': offset.dy}; + + factory DrawingPoint.fromMap(Map<String, dynamic> map) { + return DrawingPoint( + offset: Offset(map['dx'] ?? 0, map['dy'] ?? 0), + paint: Paint(), + ); + } +} + +class DrawingPath { + final List<DrawingPoint> points; + final Color color; + final double strokeWidth; + final bool isEraser; + + DrawingPath({ + required this.points, + required this.color, + required this.strokeWidth, + required this.isEraser, + }); + + Map<String, dynamic> toMap() { + return { + 'points': points.map((p) => p.toMap()).toList(), + 'color': color.value, + 'strokeWidth': strokeWidth, + 'isEraser': isEraser, + }; + } + + factory DrawingPath.fromMap(Map<String, dynamic> map) { + return DrawingPath( + points: + (map['points'] as List).map((p) => DrawingPoint.fromMap(p)).toList(), + color: Color(map['color']), + strokeWidth: (map['strokeWidth'] as num).toDouble(), + isEraser: map['isEraser'] ?? false, + ); + } +} + +// Helper to snapshot the entire canvas state for undo/redo +class CanvasState { + final List<Map<String, dynamic>> elements; + final List<DrawingPath> paths; + CanvasState(this.elements, this.paths); +} + +class CanvasPage extends StatefulWidget { + final int projectId; + final double width; + final double height; + final File? initialImage; + final FileModel? existingFile; + final File? injectedMedia; + + const CanvasPage({ + super.key, + required this.projectId, + required this.width, + required this.height, + this.initialImage, + this.existingFile, + this.injectedMedia, + }); + + @override + State<CanvasPage> createState() => _CanvasPageState(); +} + +class _CanvasPageState extends State<CanvasPage> { + // --- SERVICES --- + final FileService _fileService = FileService(); + final ChangeStack _changeStack = ChangeStack(); + final ImagePicker _picker = ImagePicker(); + final ChangeStack _magicDrawChangeStack = ChangeStack(); + + // --- STATE --- + bool _hasUnsavedChanges = false; + List<Map<String, dynamic>> elements = []; + List<DrawingPath> _paths = []; // Keeps normal drawing strokes + List<DrawingPath> _magicPaths = []; // Keeps temporary magic draw mask strokes + + // Snapshots for undo grouping + CanvasState? _gestureStartSnapshot; + + // Detection Timer & AI Analysis + Timer? _inactivityTimer; + DateTime _lastAnalysisTime = DateTime.now(); + final GlobalKey _canvasGlobalKey = GlobalKey(); + String? _aiDescription; + bool _isAnalyzing = false; + bool _isDescriptionExpanded = false; // Track expansion state + + // Magic Draw / Inpainting State + File? _tempBaseImage; + bool _isInpainting = false; + bool _isCapturingBase = false; // Hide strokes during capture + + // Background Removal Banner State + bool _showBgRemovalBanner = false; + String? _bgRemovalTargetId; + String? _bgRemovalTargetPath; + Timer? _bgRemovalBannerTimer; + bool _isRemovingBg = false; + + String? selectedId; + late Size _canvasSize; + List<Color> _brandColors = []; + + // --- TOOLS --- + bool _isMagicDrawActive = false; + bool _isTextToolsActive = false; + + bool _isMagicPanelDisabled = false; + bool _isViewMode = false; + + // --- EDITING --- + bool _isEditingText = false; + final TextEditingController _textEditingController = TextEditingController(); + final FocusNode _textFocusNode = FocusNode(); + + // --- DRAWING --- + List<DrawingPoint> _currentPoints = []; + Color _selectedColor = const Color(0xFFFF4081); + double _strokeWidth = 10.0; + bool _isEraser = false; + final GlobalKey _drawingKey = GlobalKey(); + + // --- VIEWPORT --- + final TransformationController _transformationController = + TransformationController(); + bool _hasInitializedView = false; + + @override + void initState() { + super.initState(); + _canvasSize = Size(widget.width, widget.height); + _fetchBrandColors(); + + if (widget.existingFile != null) { + _loadCanvasFromFile(); + } else if (widget.initialImage != null) { + final double imageWidth = _canvasSize.width * 0.4; // 40% of canvas width + final double imageHeight = + _canvasSize.height * 0.4; // 40% of canvas height + final Offset centeredPosition = Offset( + (_canvasSize.width - imageWidth) / 2, + (_canvasSize.height - imageHeight) / 2, + ); + elements.add({ + 'id': 'bg_${DateTime.now().millisecondsSinceEpoch}', + 'type': 'file_image', + 'content': widget.initialImage!.path, + 'position': centeredPosition, + 'size': Size(imageWidth, imageHeight), + 'rotation': 0.0, + }); + _hasUnsavedChanges = true; + } + + // Note: injectedMedia handling is done inside _loadCanvasFromFile to ensure + // it happens after file content is loaded, avoiding race conditions. + } + + @override + void dispose() { + _inactivityTimer?.cancel(); + _bgRemovalBannerTimer?.cancel(); + _textEditingController.dispose(); + _textFocusNode.dispose(); + _transformationController.dispose(); + super.dispose(); + } + + // Handle inactivity timer logic + void _resetInactivityTimer() { + _inactivityTimer?.cancel(); + _inactivityTimer = Timer(const Duration(seconds: 2), () { + _analyzeCanvas(); + }); + } + + Future<bool> _confirmDiscardMagicDraw() async { + if (_magicPaths.isEmpty) return true; // nothing drawn – no popup + + final result = await showDialog<bool>( + context: context, + builder: + (context) => AlertDialog( + title: const Text("Discard Magic Draw?"), + content: const Text( + "Leaving Magic Draw will remove your sketch. Continue?", + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text("Stay"), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text("Discard"), + ), + ], + ), + ); + + return result == true; + } + + Future<void> _analyzeCanvas() async { + if (_isAnalyzing || _isInpainting) return; // Skip if busy + setState(() => _isAnalyzing = true); + + try { + debugPrint("🧠 [AI] Starting canvas analysis..."); + + File? imageFile = await _captureCanvasToFile(); + if (imageFile == null) return; + + // Call the service + final description = await FlaskService().describeImage( + imagePath: imageFile.path, + ); + + debugPrint("🤖 [AI] Service Response: $description"); + + if (description != null && mounted) { + setState(() { + _aiDescription = description; + _isDescriptionExpanded = + false; // Reset to collapsed on new description + }); + } + } catch (e) { + debugPrint("❌ [AI] Analysis Failed: $e"); + } finally { + if (mounted) setState(() => _isAnalyzing = false); + } + } + + Future<File?> _captureCanvasToFile() async { + try { + RenderRepaintBoundary? boundary = + _canvasGlobalKey.currentContext?.findRenderObject() + as RenderRepaintBoundary?; + if (boundary == null) return null; + + ui.Image image = await boundary.toImage(pixelRatio: 1.0); + ByteData? byteData = await image.toByteData( + format: ui.ImageByteFormat.png, + ); + if (byteData == null) return null; + Uint8List pngBytes = byteData.buffer.asUint8List(); + + final tempDir = await getTemporaryDirectory(); + final tempFile = File( + '${tempDir.path}/canvas_capture_${DateTime.now().millisecondsSinceEpoch}.png', + ); + await tempFile.writeAsBytes(pngBytes); + return tempFile; + } catch (e) { + debugPrint("Error capturing canvas: $e"); + return null; + } + } + + Future<void> _fetchBrandColors() async { + try { + final int? pId = widget.projectId; + if (pId == null) return; + + final project = await ProjectRepo().getProjectById(pId); + if (project != null && project.globalStylesheet != null) { + final styleData = StylesheetService().parse(project.globalStylesheet); + if (mounted && styleData.colors.isNotEmpty) { + setState(() { + _brandColors = styleData.colors; + }); + } + } + } catch (e) { + debugPrint("Error loading brand colors: $e"); + } + } + + // --- UNDO/REDO --- + // --- MAGIC DRAW UNDO/REDO (NEW) --- + void _recordMagicChange(List<DrawingPath> oldMagicPaths) { + final newMagicPaths = List<DrawingPath>.from(_magicPaths); + _magicDrawChangeStack.add( + Change( + oldMagicPaths, + () => setState(() => _magicPaths = List.from(newMagicPaths)), // Redo + (val) => setState(() => _magicPaths = List.from(val)), // Undo + ), + ); + } + + void _recordChange(CanvasState oldState) { + _resetInactivityTimer(); // Reset timer on undoable actions + final newState = CanvasState( + _deepCopyElements(elements), + List.from(_paths), + ); + + _changeStack.add( + Change( + oldState, + () { + // REDO + setState(() { + elements = _deepCopyElements(newState.elements); + _paths = List.from(newState.paths); + _hasUnsavedChanges = true; + }); + }, + (val) { + // UNDO + setState(() { + elements = _deepCopyElements(val.elements); + _paths = List.from(val.paths); + _hasUnsavedChanges = true; + }); + }, + ), + ); + setState(() => _hasUnsavedChanges = true); + } + + CanvasState _getCurrentState() { + return CanvasState(_deepCopyElements(elements), List.from(_paths)); + } + + // --- ACTIONS --- + + void _toggleTextTools() { + setState(() { + _isTextToolsActive = !_isTextToolsActive; + _isMagicDrawActive = false; + if (!_isTextToolsActive) { + _exitEditMode(); + selectedId = null; + } + }); + } + + void _addTextElement() { + final oldState = _getCurrentState(); + final id = 'text_${DateTime.now().millisecondsSinceEpoch}'; + final double defaultFontSize = (_canvasSize.width / 25).clamp(24.0, 96.0); + final initialPos = Offset( + _canvasSize.width / 2 - 150, + _canvasSize.height / 2 - 50, + ); + + final newElement = { + 'id': id, + 'type': 'text', + 'content': 'Double tap to edit', + 'position': initialPos, + 'size': Size(300, defaultFontSize * 2), + 'rotation': 0.0, + 'style_color': Colors.black.value, + 'style_fontSize': defaultFontSize, + }; + + setState(() { + elements.add(newElement); + // Bring to front + elements.remove(newElement); + elements.add(newElement); + selectedId = id; + _isTextToolsActive = true; + }); + + _recordChange(oldState); + _enterEditMode(newElement); + } + + void _enterEditMode(Map<String, dynamic> element) { + setState(() { + selectedId = element['id']; + _isEditingText = true; + _textEditingController.text = element['content']; + _textEditingController.selection = TextSelection.fromPosition( + TextPosition(offset: _textEditingController.text.length), + ); + }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_textFocusNode.canRequestFocus) { + _textFocusNode.requestFocus(); + } + }); + } + + void _exitEditMode() { + if (_isEditingText && selectedId != null) { + final index = elements.indexWhere((e) => e['id'] == selectedId); + if (index != -1) { + if (elements[index]['content'] != _textEditingController.text) { + final oldState = _getCurrentState(); + setState(() { + elements[index]['content'] = _textEditingController.text; + _isEditingText = false; + }); + _recordChange(oldState); + } else { + setState(() => _isEditingText = false); + } + } else { + setState(() => _isEditingText = false); + } + _textFocusNode.unfocus(); + } + } + + void _updateSelectedTextProperty(String key, dynamic value) { + if (selectedId == null) return; + final index = elements.indexWhere((e) => e['id'] == selectedId); + if (index == -1) return; + + final oldState = _getCurrentState(); + setState(() { + elements[index][key] = value; + }); + _recordChange(oldState); + } + + void _deleteSelectedElement() { + if (selectedId == null) return; + final oldState = _getCurrentState(); + setState(() { + elements.removeWhere((e) => e['id'] == selectedId); + selectedId = null; + _isEditingText = false; + _isTextToolsActive = false; + }); + _textFocusNode.unfocus(); + _recordChange(oldState); + } + + void _handleGestureStart() { + if (_isMagicDrawActive) { + bool hasImageLayers = elements.any((e) => e['type'] == 'file_image'); + if (hasImageLayers && !_isInpainting && _tempBaseImage == null) { + _ensureBaseImageCaptured(); + } + return; + } + _gestureStartSnapshot = _getCurrentState(); + } + + // Captures the base image by temporarily hiding the magic strokes + Future<void> _ensureBaseImageCaptured() async { + if (_tempBaseImage != null) return; + + debugPrint("📸 [Magic Draw] Hiding strokes to capture clean base..."); + + // 1. Hide strokes by updating state + setState(() => _isCapturingBase = true); + + // 2. Wait for frame to render + await Future.delayed(const Duration(milliseconds: 50)); + + // 3. Capture + try { + _tempBaseImage = await _captureCanvasToFile(); + debugPrint("✅ [Magic Draw] Base image captured."); + } catch (e) { + debugPrint("❌ [Magic Draw] Failed to capture base: $e"); + } finally { + // 4. Show strokes again + if (mounted) setState(() => _isCapturingBase = false); + } + } + + void _handleElementUpdate( + String id, + Offset newPos, + Size newSize, + double newRotation, + ) { + _resetInactivityTimer(); // Reset timer while dragging/resizing + final index = elements.indexWhere((e) => e['id'] == id); + if (index == -1) return; + + setState(() { + elements[index]['position'] = newPos; + elements[index]['size'] = newSize; + elements[index]['rotation'] = newRotation; + _hasUnsavedChanges = true; + }); + } + + void _handleGestureEnd() { + if (_gestureStartSnapshot != null) { + _recordChange(_gestureStartSnapshot!); + _gestureStartSnapshot = null; + } + } + + Future<void> _exportProject() async { + try { + // 1. Show loading or feedback + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text("Generating image..."))); + + // 2. Capture the Full Canvas using the global key + final boundary = + _canvasGlobalKey.currentContext?.findRenderObject() + as RenderRepaintBoundary?; + if (boundary == null) return; + + // 3. Convert to Image (High pixel ratio for quality) + final ui.Image image = await boundary.toImage(pixelRatio: 3.0); + final ByteData? byteData = await image.toByteData( + format: ui.ImageByteFormat.png, + ); + + if (byteData == null) return; + + final Uint8List pngBytes = byteData.buffer.asUint8List(); + + // 4. Convert PNG to JPG using 'image' package + // We do this in a compute isolate ideally, but for simplicity here on main thread + final img.Image? decodedImage = img.decodePng(pngBytes); + + if (decodedImage == null) { + throw Exception("Failed to decode image"); + } + + // Encode to JPG (Quality 90) + final Uint8List jpgBytes = img.encodeJpg(decodedImage, quality: 90); + + // 5. Save to Temporary File + final directory = await getTemporaryDirectory(); + final String fileName = + "export_${DateTime.now().millisecondsSinceEpoch}.jpg"; + final String filePath = '${directory.path}/$fileName'; + + final File imgFile = File(filePath); + await imgFile.writeAsBytes(jpgBytes); + + // 6. Trigger System Share/Save Dialog + // This allows the user to "Save to Device", "Share to Instagram", etc. + await Share.shareXFiles([ + XFile(filePath, mimeType: 'image/jpeg'), + ], text: 'Check out my design created with CreekUI!'); + } catch (e) { + debugPrint("Export Error: $e"); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text("Export failed: $e"))); + } + } + + // =========================================================================== + // SAVE & LOAD LOGIC + // =========================================================================== + Future<void> _handleMagicDrawExit() async { + // Use the helper method you already wrote: _confirmDiscardMagicDraw + if (_magicPaths.isNotEmpty && !_isInpainting) { + final confirm = await _confirmDiscardMagicDraw(); + if (confirm) { + _saveAndCloseMagicDraw(); + } + // If false, do nothing (stay) + } else { + _saveAndCloseMagicDraw(); + } + } + + void _handleBackNavigation() { + // If in magic draw mode, just close tool first + if (_isMagicDrawActive) { + _saveAndCloseMagicDraw(); + return; + } + + if (!_hasUnsavedChanges && widget.existingFile != null) { + Navigator.pop(context); + return; + } + + showDialog( + context: context, + builder: + (context) => AlertDialog( + title: const Text("Save Changes?"), + content: const Text( + "Do you want to save your canvas before leaving?", + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); // Close dialog + Navigator.pop(context); // Leave page + }, + child: const Text( + "Discard", + style: TextStyle(color: Colors.red), + ), + ), + FilledButton( + onPressed: () async { + Navigator.pop(context); // Close dialog + await _saveCanvas(); // Save + // Note: The redirect logic is handled in _saveCanvas for new files. + // For existing files, we pop here. + if (mounted && widget.existingFile != null) { + Navigator.pop(context); + } + }, + child: const Text("Save"), + ), + ], + ), + ); + } + + Future<String?> _showNameDialog() async { + TextEditingController nameController = TextEditingController( + text: "Untitled Canvas", + ); + return showDialog<String>( + context: context, + builder: + (ctx) => AlertDialog( + title: const Text("Save Canvas"), + content: TextField( + controller: nameController, + autofocus: true, + decoration: const InputDecoration( + labelText: "Canvas Name", + hintText: "Enter a name for your file", + border: OutlineInputBorder(), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text("Cancel"), + ), + FilledButton( + onPressed: () => Navigator.pop(ctx, nameController.text.trim()), + child: const Text("Save"), + ), + ], + ), + ); + } + + // Helper to generate preview image + Future<String?> _generatePreviewImage() async { + try { + final boundary = + _canvasGlobalKey.currentContext?.findRenderObject() + as RenderRepaintBoundary?; + if (boundary == null) return null; + + // Capture image with lower pixel ratio for preview thumbnail + final ui.Image image = await boundary.toImage(pixelRatio: 1.0); + final ByteData? byteData = await image.toByteData( + format: ui.ImageByteFormat.png, + ); + + if (byteData == null) return null; + final Uint8List pngBytes = byteData.buffer.asUint8List(); + + final directory = await getApplicationDocumentsDirectory(); + final previewDir = Directory('${directory.path}/previews'); + if (!await previewDir.exists()) { + await previewDir.create(recursive: true); + } + + final String fileName = + "preview_${DateTime.now().millisecondsSinceEpoch}.png"; + final String filePath = '${previewDir.path}/$fileName'; + + final File imgFile = File(filePath); + await imgFile.writeAsBytes(pngBytes); + return filePath; + } catch (e) { + debugPrint("Error generating preview: $e"); + return null; + } + } + + Future<void> _saveCanvas() async { + try { + String fileName = "Canvas ${DateTime.now().toString().split(' ')[0]}"; + bool isNewFile = widget.existingFile == null; + + // 1. IF NEW FILE: Ask user for name + if (isNewFile) { + final userFileName = await _showNameDialog(); + if (userFileName == null || userFileName.isEmpty) return; // Cancelled + fileName = userFileName; + } + + // 2. Generate Preview + final String? previewPath = await _generatePreviewImage(); + + // 3. Serialize Elements AND Paths (Drawing) to JSON + final jsonList = _elementsToJson(elements); + final pathsJson = _paths.map((p) => p.toMap()).toList(); + + final saveData = { + 'elements': jsonList, + 'paths': pathsJson, + 'width': _canvasSize.width, // Saving Width + 'height': _canvasSize.height, // Saving Height + 'preview_path': previewPath, // Saving Preview Path + }; + final jsonString = jsonEncode(saveData); + + final directory = await getTemporaryDirectory(); + final tempFile = File( + '${directory.path}/canvas_temp_${DateTime.now().millisecondsSinceEpoch}.json', + ); + await tempFile.writeAsString(jsonString); + + if (widget.existingFile != null) { + // Overwrite existing file + final existingFile = File(widget.existingFile!.filePath); + await existingFile.writeAsString(jsonString); + await _fileService.openFile(widget.existingFile!.id); + } else { + // Save as new file + await _fileService.saveFile( + tempFile, + widget.projectId, + name: fileName, + description: "Editable Canvas Board", + ); + } + + if (await tempFile.exists()) await tempFile.delete(); + + setState(() => _hasUnsavedChanges = false); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Canvas Saved Successfully")), + ); + if (isNewFile) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => ProjectFilePage(projectId: widget.projectId), + ), + ); + } + } + } catch (e) { + debugPrint("Save Error: $e"); + } + } + + Future<void> _loadCanvasFromFile() async { + try { + final file = File(widget.existingFile!.filePath); + if (await file.exists()) { + final jsonString = await file.readAsString(); + final dynamic decoded = jsonDecode(jsonString); + + setState(() { + if (decoded is Map && decoded.containsKey('elements')) { + // New format with dimensions + elements = _jsonToElements(decoded['elements']); + if (decoded['paths'] != null) { + _paths = + (decoded['paths'] as List) + .map((p) => DrawingPath.fromMap(p)) + .toList(); + } else { + _paths = []; + } + // [FIX] LOAD CANVAS DIMENSIONS & RESET VIEW INIT + if (decoded['width'] != null && decoded['height'] != null) { + _canvasSize = Size( + (decoded['width'] as num).toDouble(), + (decoded['height'] as num).toDouble(), + ); + _hasInitializedView = false; // FORCE RE-CENTERING + } + } else if (decoded is List) { + // Legacy support + elements = _jsonToElements(decoded); + _paths = []; + _hasInitializedView = false; + } + _hasUnsavedChanges = false; + }); + + // Handle Injected Media (e.g. from Share) + if (widget.injectedMedia != null) { + final oldState = _getCurrentState(); + setState(() { + final double imageWidth = + _canvasSize.width * 0.4; // 40% of canvas width + final double imageHeight = + _canvasSize.height * 0.4; // 40% of canvas height + final Offset centeredPosition = Offset( + (_canvasSize.width - imageWidth) / 2, + (_canvasSize.height - imageHeight) / 2, + ); + elements.add({ + 'id': 'shared_${DateTime.now().millisecondsSinceEpoch}', + 'type': 'file_image', + 'content': widget.injectedMedia!.path, + 'position': centeredPosition, + 'size': Size(imageWidth, imageHeight), + 'rotation': 0.0, + }); + }); + _recordChange(oldState); + } + } + } catch (e) { + debugPrint("Error loading canvas: $e"); + } + } + + List<Map<String, dynamic>> _deepCopyElements( + List<Map<String, dynamic>> source, + ) { + return source.map((e) { + final copy = Map<String, dynamic>.from(e); + if (e['position'] is Offset) { + copy['position'] = Offset( + (e['position'] as Offset).dx, + (e['position'] as Offset).dy, + ); + } + if (e['size'] is Size) { + copy['size'] = Size( + (e['size'] as Size).width, + (e['size'] as Size).height, + ); + } + return copy; + }).toList(); + } + + List<Map<String, dynamic>> _elementsToJson( + List<Map<String, dynamic>> elements, + ) { + return elements.map((e) { + final copy = Map<String, dynamic>.from(e); + if (e['position'] is Offset) { + copy['position'] = { + 'dx': (e['position'] as Offset).dx, + 'dy': (e['position'] as Offset).dy, + }; + } + if (e['size'] is Size) { + copy['size'] = { + 'width': (e['size'] as Size).width, + 'height': (e['size'] as Size).height, + }; + } + return copy; + }).toList(); + } + + List<Map<String, dynamic>> _jsonToElements(List<dynamic> jsonList) { + return jsonList.map((item) { + final e = Map<String, dynamic>.from(item); + if (e['position'] is Map) { + e['position'] = Offset(e['position']['dx'], e['position']['dy']); + } + if (e['size'] is Map) { + e['size'] = Size(e['size']['width'], e['size']['height']); + } + e['rotation'] = (e['rotation'] as num).toDouble(); + return e; + }).toList(); + } + + void _openLayers() => _showComingSoon('Layers'); + void _openSettings() => _showComingSoon('Settings'); + void _showComingSoon([dynamic feature]) => ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Coming soon'))); + + // [UPDATED] New Bottom Sheet for Assets + void _openStylesheet() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: + (context) => DraggableScrollableSheet( + initialChildSize: 0.7, + minChildSize: 0.5, + maxChildSize: 0.9, + builder: + (_, controller) => _AssetPickerSheet( + projectId: widget.projectId, + scrollController: controller, + onAddAssets: (List<String> paths) { + _addAssetsToCanvas(paths); + Navigator.pop(context); + }, + ), + ), + ); + } + + void _addAssetsToCanvas(List<String> paths) { + if (paths.isEmpty) return; + final oldState = _getCurrentState(); + setState(() { + for (var path in paths) { + final double imageWidth = + _canvasSize.width * 0.4; // 40% of canvas width + final double imageHeight = + _canvasSize.height * 0.4; // 40% of canvas height + final Offset centeredPosition = Offset( + (_canvasSize.width - imageWidth) / 2, + (_canvasSize.height - imageHeight) / 2, + ); + elements.add({ + 'id': + 'asset_${DateTime.now().millisecondsSinceEpoch}_${math.Random().nextInt(1000)}', + 'type': 'file_image', + 'content': path, + 'position': centeredPosition, + 'size': Size(imageWidth, imageHeight), + 'rotation': 0.0, + }); + } + _hasUnsavedChanges = true; + }); + _recordChange(oldState); + } + + // =========================================================================== + // UI BUILDER + // =========================================================================== + + @override + Widget build(BuildContext context) { + Map<String, dynamic>? selectedEl; + try { + selectedEl = elements.firstWhere((e) => e['id'] == selectedId); + } catch (_) {} + + final bool isTextSelected = + selectedEl != null && selectedEl['type'] == 'text'; + final bool showTextOverlay = _isTextToolsActive || isTextSelected; + + return PopScope( + canPop: false, + onPopInvoked: (didPop) { + if (didPop) return; + _handleBackNavigation(); + }, + child: Scaffold( + backgroundColor: const Color(0xFFE0E0E0), + appBar: _buildAppBar(), + body: LayoutBuilder( + builder: (context, constraints) { + if (!_hasInitializedView) { + _hasInitializedView = true; + final double scaleX = + (constraints.maxWidth - 40) / _canvasSize.width; + final double scaleY = + (constraints.maxHeight - 40) / _canvasSize.height; + final double initialScale = math + .min(scaleX, scaleY) + .clamp(0.01, 1.0); + + final double transX = + (constraints.maxWidth - (_canvasSize.width * initialScale)) / + 2; + final double transY = + (constraints.maxHeight - + (_canvasSize.height * initialScale)) / + 2; + + _transformationController.value = + Matrix4.identity() + ..translate(transX, transY) + ..scale(initialScale); + } + + return Stack( + children: [ + GestureDetector( + onTap: () { + if (!_isMagicDrawActive || _isMagicPanelDisabled) { + _exitEditMode(); + setState(() => selectedId = null); + } + }, + behavior: HitTestBehavior.translucent, + child: InteractiveViewer( + transformationController: _transformationController, + constrained: false, + boundaryMargin: const EdgeInsets.all(double.infinity), + minScale: 0.01, + maxScale: 10.0, + scaleEnabled: !_isMagicDrawActive || _isMagicPanelDisabled, + panEnabled: !_isMagicDrawActive || _isMagicPanelDisabled, + child: RepaintBoundary( + key: _canvasGlobalKey, + child: SizedBox( + width: _canvasSize.width, + height: _canvasSize.height, + child: Stack( + children: [ + Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 40, + offset: const Offset(0, 10), + ), + ], + ), + ), + ...elements.map((e) { + final bool isSelected = selectedId == e['id']; + return _ManipulatingBox( + key: ValueKey(e['id']), + id: e['id'], + position: e['position'], + size: e['size'], + rotation: e['rotation'], + type: e['type'], + content: e['content'], + styleData: e, + isSelected: isSelected && !_isMagicDrawActive, + isEditing: + isSelected && + _isEditingText && + e['type'] == 'text', + viewScale: + _transformationController.value + .getMaxScaleOnAxis(), + onTap: () { + if (!_isMagicDrawActive) { + if (_isEditingText && selectedId != e['id']) + _exitEditMode(); + setState(() { + selectedId = e['id']; + if (e['type'] == 'text') + _isTextToolsActive = true; + }); + setState(() { + elements.remove(e); + elements.add(e); + }); + } + }, + onDoubleTap: () { + if (e['type'] == 'text') _enterEditMode(e); + }, + onDragStart: _handleGestureStart, + onUpdate: + (newPos, newSize, newRot) => + _handleElementUpdate( + e['id'], + newPos, + newSize, + newRot, + ), + onDragEnd: (newPos, newSize, newRot) { + _handleElementUpdate( + e['id'], + newPos, + newSize, + newRot, + ); + _handleGestureEnd(); + }, + textController: + isSelected ? _textEditingController : null, + focusNode: isSelected ? _textFocusNode : null, + transformationController: + _transformationController, + ); + }), + // Drawing Layer + IgnorePointer( + ignoring: + !_isMagicDrawActive || _isMagicPanelDisabled, + child: RepaintBoundary( + key: _drawingKey, + child: GestureDetector( + onPanStart: (_) => _handleGestureStart(), + onPanUpdate: _onPanUpdate, + onPanEnd: (details) { + _onPanEnd(details); + _handleGestureEnd(); + }, + child: CustomPaint( + size: Size.infinite, + painter: CanvasPainter( + paths: _paths, + magicPaths: + _isCapturingBase ? [] : _magicPaths, + currentPoints: + _isCapturingBase + ? [] + : _currentPoints, + currentColor: + _isEraser + ? Colors.transparent + : _selectedColor, + currentWidth: _strokeWidth, + isEraser: _isEraser, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + + if (_aiDescription != null && _isMagicDrawActive) + Positioned( + top: 10, + left: 16, + right: 16, + child: GestureDetector( + onTap: () { + setState(() { + _isDescriptionExpanded = !_isDescriptionExpanded; + }); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.95), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 2.0), + child: Icon( + Icons.auto_awesome, + size: 20, + color: Colors.indigo.shade400, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + _aiDescription!, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + maxLines: _isDescriptionExpanded ? null : 1, + overflow: + _isDescriptionExpanded + ? TextOverflow.visible + : TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + Icon( + _isDescriptionExpanded + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down, + size: 20, + color: Colors.grey[600], + ), + ], + ), + ), + ), + ), + + MagicDrawTools( + isActive: _isMagicDrawActive, + selectedColor: _selectedColor, + strokeWidth: _strokeWidth, + isEraser: _isEraser, + brandColors: _brandColors, + onClose: _handleMagicDrawExit, + onColorChanged: (c) => setState(() => _selectedColor = c), + onWidthChanged: (w) => setState(() => _strokeWidth = w), + onEraserToggle: (e) => setState(() => _isEraser = e), + onPromptSubmit: + (prompt, serviceId) => + _processInpainting(prompt, serviceId), + isProcessing: _isInpainting, + onMagicPanelActivityToggle: + (disabled) => + setState(() => _isMagicPanelDisabled = disabled), + isMagicPanelDisabled: _isMagicPanelDisabled, + onViewModeToggle: + (enabled) => setState(() => _isViewMode = enabled), + hasImageLayers: elements.any( + (e) => e['type'] == 'file_image', + ), + ), + + TextToolsOverlay( + isActive: showTextOverlay && !_isMagicDrawActive, + isTextSelected: isTextSelected, + currentColor: Color( + selectedEl?['style_color'] ?? Colors.black.value, + ), + currentFontSize: + (selectedEl?['style_fontSize'] ?? 24.0) as double, + onClose: () { + _exitEditMode(); + setState(() { + _isTextToolsActive = false; + selectedId = null; + }); + }, + onAddText: _addTextElement, + onColorChanged: + (c) => + _updateSelectedTextProperty('style_color', c.value), + onFontSizeChanged: + (s) => _updateSelectedTextProperty('style_fontSize', s), + ), + + // Background Removal Banner + if (_showBgRemovalBanner) + Positioned( + top: SafeArea(child: Container()).minimum.top + 10, + left: 0, + right: 0, + child: Center( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.black87, + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow(color: Colors.black26, blurRadius: 8, offset: Offset(0, 2)) + ] + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (_isRemovingBg) ...[ + const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)), + const SizedBox(width: 8), + const Text("Removing...", style: TextStyle(color: Colors.white, fontFamily: 'GeneralSans', fontSize: 14)), + ] else ...[ + const Text( + "Remove background?", + style: TextStyle(color: Colors.white, fontFamily: 'GeneralSans', fontSize: 14), + ), + const SizedBox(width: 8), + InkWell( + onTap: _confirmRemoveBackground, + child: Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: const Icon(Icons.check, size: 16, color: Colors.black), + ), + ), + const SizedBox(width: 8), + InkWell( + onTap: () => setState(() => _showBgRemovalBanner = false), + child: const Icon(Icons.close, size: 18, color: Colors.white54), + ), + ] + ], + ), + ), + ), + ), + + Positioned( + bottom: 0, + left: 0, + right: 0, + child: CanvasBottomBar( + activeItem: + _isMagicDrawActive + ? "Magic Draw" + : (_isTextToolsActive ? "Text" : null), + onMagicDraw: () async { + if (_isMagicDrawActive) { + final confirm = await _confirmDiscardMagicDraw(); + if (!confirm) return; + setState(() { + _magicPaths.clear(); + _magicDrawChangeStack.clear(); + _tempBaseImage = null; + _isMagicDrawActive = false; + }); + return; + } + setState(() { + _isMagicDrawActive = true; + _isTextToolsActive = false; + _exitEditMode(); + _magicPaths.clear(); + _magicDrawChangeStack.clear(); + _tempBaseImage = null; + }); + }, + onMedia: () async { + if (_isMagicDrawActive) { + final confirm = await _confirmDiscardMagicDraw(); + if (!confirm) return; + setState(() { + _magicPaths.clear(); + _magicDrawChangeStack.clear(); + _tempBaseImage = null; + _isMagicDrawActive = false; + }); + } + _pickImageFromGallery(); + }, + onStylesheet: () async { + if (_isMagicDrawActive) { + final confirm = await _confirmDiscardMagicDraw(); + if (!confirm) return; + setState(() { + _magicPaths.clear(); + _magicDrawChangeStack.clear(); + _tempBaseImage = null; + _isMagicDrawActive = false; + }); + } + _openStylesheet(); + }, + onTools: () async { + if (_isMagicDrawActive) { + final confirm = await _confirmDiscardMagicDraw(); + if (!confirm) return; + setState(() { + _magicPaths.clear(); + _magicDrawChangeStack.clear(); + _tempBaseImage = null; + _isMagicDrawActive = false; + }); + } + _showComingSoon('Tools'); + }, + onText: () async { + if (_isMagicDrawActive) { + final confirm = await _confirmDiscardMagicDraw(); + if (!confirm) return; + setState(() { + _magicPaths.clear(); + _magicDrawChangeStack.clear(); + _tempBaseImage = null; + _isMagicDrawActive = false; + }); + } + _toggleTextTools(); + }, + onSelect: () async { + if (_isMagicDrawActive) { + final confirm = await _confirmDiscardMagicDraw(); + if (!confirm) return; + setState(() { + _magicPaths.clear(); + _magicDrawChangeStack.clear(); + _tempBaseImage = null; + _isMagicDrawActive = false; + }); + } + _showComingSoon('Select'); + }, + onPlugins: () async { + if (_isMagicDrawActive) { + final confirm = await _confirmDiscardMagicDraw(); + if (!confirm) return; + setState(() { + _magicPaths.clear(); + _magicDrawChangeStack.clear(); + _tempBaseImage = null; + _isMagicDrawActive = false; + }); + } + _showComingSoon('Plugins'); + }, + ), + ), + ], + ); + }, + ), + ), + ); + } + + // --- DRAWING HELPERS --- + + void _onPanUpdate(DragUpdateDetails details) { + setState(() { + _currentPoints.add( + DrawingPoint(offset: details.localPosition, paint: Paint()), + ); + }); + + // Throttle Analysis: Trigger "Describe" every 2.5s while actively drawing + if (_isMagicDrawActive && + !_isAnalyzing && + !_isInpainting && + DateTime.now().difference(_lastAnalysisTime).inMilliseconds > 2500) { + _lastAnalysisTime = DateTime.now(); + _analyzeCanvas(); + } + } + + Future<void> _onPanEnd(DragEndDetails details) async { + setState(() { + if (_currentPoints.isNotEmpty) { + if (_isMagicDrawActive) { + // ADD TO MAGIC PATHS (Temporary mask) + final oldMagicPaths = List<DrawingPath>.from(_magicPaths); + _magicPaths.add( + DrawingPath( + points: List.from(_currentPoints), + color: _selectedColor, + strokeWidth: _strokeWidth, + isEraser: _isEraser, + ), + ); + _recordMagicChange(oldMagicPaths); + _currentPoints = []; + } else { + // NORMAL DRAWING + _paths.add( + DrawingPath( + points: List.from(_currentPoints), + color: _selectedColor, + strokeWidth: _strokeWidth, + isEraser: _isEraser, + ), + ); + _currentPoints = []; + _hasUnsavedChanges = true; + _resetInactivityTimer(); + } + } + }); + } + + // --- BACKGROUND REMOVAL LOGIC --- + + void _triggerBgRemovalBanner(String elementId, String imagePath) { + _bgRemovalBannerTimer?.cancel(); + setState(() { + _showBgRemovalBanner = true; + _bgRemovalTargetId = elementId; + _bgRemovalTargetPath = imagePath; + }); + + // Automatically hide after 10 seconds + _bgRemovalBannerTimer = Timer(const Duration(seconds: 10), () { + if (mounted) { + setState(() => _showBgRemovalBanner = false); + } + }); + } + + Future<void> _confirmRemoveBackground() async { + if (_bgRemovalTargetId == null || _bgRemovalTargetPath == null) return; + + _bgRemovalBannerTimer?.cancel(); + + setState(() { + _isRemovingBg = true; + }); + + try { + // Call service to remove background + final newPath = await FlaskService().generateAsset(imagePath: _bgRemovalTargetPath!); + + if (newPath != null && mounted) { + // Find the element and update its content + final index = elements.indexWhere((e) => e['id'] == _bgRemovalTargetId); + if (index != -1) { + final oldState = _getCurrentState(); + setState(() { + elements[index]['content'] = newPath; // Replace with transparent png + _showBgRemovalBanner = false; + _isRemovingBg = false; + _hasUnsavedChanges = true; + }); + _recordChange(oldState); + } + } + } catch (e) { + debugPrint("BG Removal Failed: $e"); + } finally { + if (mounted) setState(() { + _isRemovingBg = false; + _showBgRemovalBanner = false; + }); + } + } + + Future<void> _processInpainting(String prompt, String modelId) async { + // Lock editing and hide keyboard + FocusScope.of(context).unfocus(); + + setState(() => _isInpainting = true); + _resetInactivityTimer(); + + try { + // Check if there are image layers (which dictates the context) + bool hasImageLayers = elements.any((e) => e['type'] == 'file_image'); + + if (_tempBaseImage == null) { + _tempBaseImage = await _captureCanvasToFile(); + } + if (_tempBaseImage == null) return; + + File? maskFile = await _generateMaskImageFromPaths( + _magicPaths, + _canvasSize, + _tempBaseImage, + ); + + if (maskFile == null) throw Exception("Failed to generate mask"); + + String? newImageUrl; + + // --- LOGIC SWITCHING BASED ON MODEL ID --- + + // CASE 1: INPAINTING (When hasImageLayers is TRUE) + if (hasImageLayers) { + if (modelId == 'inpaint_api') { + // Call API Inpainting + newImageUrl = await FlaskService().inpaintApiImage( + imagePath: _tempBaseImage!.path, + maskPath: maskFile.path, + prompt: prompt, + ); + } else { + // Local Inpainting + newImageUrl = await FlaskService().inpaintImage( + imagePath: _tempBaseImage!.path, + maskPath: maskFile.path, + prompt: prompt, + ); + } + } + // CASE 2: SKETCH TO IMAGE (When hasImageLayers is FALSE) + else { + if(modelId == 'sketch_fusion'){ + newImageUrl = await FlaskService().sketchToImage( + projectId: widget.projectId, + sketchPath: _tempBaseImage!.path, + userPrompt: prompt, + imageDescription: _aiDescription, + ); + } + else if(modelId == 'sketch_advanced'){ + newImageUrl = await FlaskService().sketchToImageAPI( + projectId: widget.projectId, + sketchPath: _tempBaseImage!.path, + userPrompt: prompt, + option: 1, + imageDescription: _aiDescription, + ); + } + else if (modelId == 'sketch_creative') { + newImageUrl = await FlaskService().sketchToImageAPI( + projectId: widget.projectId, + sketchPath: _tempBaseImage!.path, + userPrompt: prompt, + option: 2, + imageDescription: _aiDescription, + ); + } + } + + if (newImageUrl != null) { + final id = _addGeneratedImage(newImageUrl); + setState(() { + _isMagicDrawActive = false; + }); + + // Trigger Banner for Background Removal (Only if it was a generation, not inpainting) + if (!hasImageLayers) { + _triggerBgRemovalBanner(id, newImageUrl); + } + } + } catch (e) { + debugPrint("Generation Error: $e"); + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text("Generation failed."))); + } finally { + setState(() { + _isInpainting = false; + _tempBaseImage = null; + _magicPaths.clear(); + _magicDrawChangeStack.clear(); + }); + } + } + + String _addGeneratedImage(String? newImageUrl) { + String id = ''; + if (newImageUrl != null) { + debugPrint("✅ Adding generated image to canvas: $newImageUrl"); + id = 'gen_${DateTime.now().millisecondsSinceEpoch}'; + setState(() { + elements.add({ + 'id': id, + 'type': 'file_image', + 'content': newImageUrl, + 'position': const Offset(0, 0), + 'size': _canvasSize, + 'rotation': 0.0, + }); + _magicPaths.clear(); + }); + } + return id; + } + + // Updated to generate mask as: Base Image + Drawing Strokes + Future<File?> _generateMaskImageFromPaths( + List<DrawingPath> paths, + Size size, + File? baseImageFile, + ) async { + try { + final recorder = ui.PictureRecorder(); + final canvas = Canvas( + recorder, + Rect.fromLTWH(0, 0, size.width, size.height), + ); + + // 1. Draw Base Image First (if available) + if (baseImageFile != null) { + final data = await baseImageFile.readAsBytes(); + final codec = await ui.instantiateImageCodec(data); + final frameInfo = await codec.getNextFrame(); + final baseImage = frameInfo.image; + + paintImage( + canvas: canvas, + rect: Rect.fromLTWH(0, 0, size.width, size.height), + image: baseImage, + fit: BoxFit.cover, // Or contain, depending on your logic + ); + } else { + // Fallback to black if no base image (shouldn't happen based on logic) + canvas.drawRect( + Rect.fromLTWH(0, 0, size.width, size.height), + Paint()..color = Colors.black, + ); + } + + // 2. Draw Strokes ON TOP of the base image + for (final path in paths) { + final paint = + Paint() + ..color = + path + .color // Use the drawing color (e.g. Blue) + ..strokeWidth = path.strokeWidth + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round + ..style = PaintingStyle.stroke; + + if (path.points.length > 1) { + final Path p = Path(); + p.moveTo(path.points.first.offset.dx, path.points.first.offset.dy); + for (int i = 1; i < path.points.length; i++) { + p.lineTo(path.points[i].offset.dx, path.points[i].offset.dy); + } + canvas.drawPath(p, paint); + } else if (path.points.isNotEmpty) { + canvas.drawPoints(ui.PointMode.points, [ + path.points.first.offset, + ], paint); + } + } + + final picture = recorder.endRecording(); + final img = await picture.toImage( + size.width.toInt(), + size.height.toInt(), + ); + final byteData = await img.toByteData(format: ui.ImageByteFormat.png); + if (byteData == null) return null; + + final tempDir = await getTemporaryDirectory(); + final file = File( + '${tempDir.path}/mask_${DateTime.now().millisecondsSinceEpoch}.png', + ); + await file.writeAsBytes(byteData.buffer.asUint8List()); + return file; + } catch (e) { + debugPrint("Mask Generation Error: $e"); + return null; + } + } + + Future<void> _saveAndCloseMagicDraw() async { + setState(() { + _isMagicDrawActive = false; + _magicPaths.clear(); + _magicDrawChangeStack.clear(); + _tempBaseImage = null; + }); + } + + PreferredSizeWidget _buildAppBar() { + final bool canUndo = + _isMagicDrawActive + ? _magicDrawChangeStack.canUndo + : _changeStack.canUndo; + final bool canRedo = + _isMagicDrawActive + ? _magicDrawChangeStack.canRedo + : _changeStack.canRedo; + return AppBar( + leadingWidth: 160, + leading: SafeArea( + child: Row( + children: [ + IconButton( + icon: SvgPicture.asset( + 'assets/icons/arrow-left-s-line.svg', + width: 22, + colorFilter: const ColorFilter.mode( + Colors.black, + BlendMode.srcIn, + ), + ), + onPressed: () { + if (_isMagicDrawActive) { + _handleMagicDrawExit(); + } else { + _handleBackNavigation(); + } + }, + ), + IconButton( + icon: SvgPicture.asset( + 'assets/icons/arrow-go-back-line.svg', + width: 22, + colorFilter: ColorFilter.mode( + canUndo ? Colors.black : Colors.grey[400]!, + BlendMode.srcIn, + ), + ), + onPressed: + canUndo + ? () => setState( + () => + _isMagicDrawActive + ? _magicDrawChangeStack.undo() + : _changeStack.undo(), + ) + : null, + ), + IconButton( + icon: SvgPicture.asset( + 'assets/icons/arrow-go-forward-line.svg', + width: 22, + colorFilter: ColorFilter.mode( + canRedo ? Colors.black : Colors.grey[400]!, + BlendMode.srcIn, + ), + ), + onPressed: + canRedo + ? () => setState( + () => + _isMagicDrawActive + ? _magicDrawChangeStack.redo() + : _changeStack.redo(), + ) + : null, + ), + ], + ), + ), + backgroundColor: Colors.white, + foregroundColor: Colors.black, + elevation: 0, + actions: [ + SafeArea( + child: Row( + children: [ + if (selectedId != null) + IconButton( + icon: const Icon(Icons.delete_outline, color: Colors.red), + onPressed: _deleteSelectedElement, + tooltip: "Delete Selected", + ), + IconButton( + icon: SvgPicture.asset( + 'assets/icons/save-3-line.svg', + width: 22, + ), + onPressed: _saveCanvas, + ), + IconButton( + icon: SvgPicture.asset( + 'assets/icons/upload-2-line.svg', + width: 22, + ), + onPressed: _exportProject, + ), + IconButton( + icon: SvgPicture.asset( + 'assets/icons/settings-line.svg', + width: 22, + ), + onPressed: _openSettings, + ), + const SizedBox(width: 8), + ], + ), + ), + ], + ); + } + + Future<void> _pickImageFromGallery() async { + final List<XFile> images = await _picker.pickMultiImage(); + if (images.isNotEmpty) { + final oldState = _getCurrentState(); + setState(() { + for (int i = 0; i < images.length; i++) { + final double imageWidth = + _canvasSize.width * 0.4; // 40% of canvas width + final double imageHeight = + _canvasSize.height * 0.4; // 40% of canvas height + final Offset centeredPosition = Offset( + (_canvasSize.width - imageWidth) / 2, + (_canvasSize.height - imageHeight) / 2, + ); + elements.add({ + 'id': '${DateTime.now().millisecondsSinceEpoch}_$i', + 'type': 'file_image', + 'content': images[i].path, + 'position': centeredPosition, + 'size': Size(imageWidth, imageHeight), + 'rotation': 0.0, + }); + } + _hasUnsavedChanges = true; + }); + _recordChange(oldState); + } + } +} + +// ============================================================================= +// MANIPULATING BOX WIDGET +// ============================================================================= + +class _ManipulatingBox extends StatefulWidget { + final String id; + final Offset position; + final Size size; + final double rotation; + final String type; + final String content; + final Map<String, dynamic> styleData; + final bool isSelected; + final bool isEditing; + final double viewScale; + final TransformationController transformationController; + final VoidCallback onTap; + final VoidCallback onDoubleTap; + final VoidCallback onDragStart; + final Function(Offset, Size, double) onUpdate; + final Function(Offset, Size, double) onDragEnd; + final TextEditingController? textController; + final FocusNode? focusNode; + + const _ManipulatingBox({ + Key? key, + required this.id, + required this.position, + required this.size, + required this.rotation, + required this.type, + required this.content, + required this.styleData, + required this.isSelected, + required this.isEditing, + required this.viewScale, + required this.transformationController, + required this.onTap, + required this.onDoubleTap, + required this.onDragStart, + required this.onUpdate, + required this.onDragEnd, + this.textController, + this.focusNode, + }) : super(key: key); + + @override + State<_ManipulatingBox> createState() => _ManipulatingBoxState(); +} + +class _ManipulatingBoxState extends State<_ManipulatingBox> { + late Offset _pos; + late Size _size; + late double _rot; + + // Gesture state + double _initialRotation = 0.0; + double _initialScale = 1.0; + bool _isTwoFingerGesture = false; + Offset _previousFocalPoint = Offset.zero; + + @override + void initState() { + super.initState(); + _updateInternalState(); + } + + @override + void didUpdateWidget(_ManipulatingBox oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.position != oldWidget.position || + widget.size != oldWidget.size || + widget.rotation != oldWidget.rotation) { + _updateInternalState(); + } + } + + void _updateInternalState() { + _pos = widget.position; + _size = widget.size; + _rot = widget.rotation; + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: widget.transformationController, + builder: (context, matrix, child) { + final double zoom = matrix.getMaxScaleOnAxis(); + final double handleScale = (1 / zoom).clamp(0.2, 5.0); + final double edgeThickness = 18 * handleScale; + + return Positioned( + left: _pos.dx, + top: _pos.dy, + child: Transform.rotate( + angle: _rot, + child: Stack( + clipBehavior: Clip.none, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: widget.onTap, + onDoubleTap: widget.onDoubleTap, + // Handle both single-finger drag and two-finger rotate/zoom using scale gestures + onScaleStart: (details) { + if (widget.isSelected && !widget.isEditing) { + _previousFocalPoint = details.focalPoint; + if (details.pointerCount == 2) { + // Two-finger gesture: rotate and zoom + _isTwoFingerGesture = true; + _initialRotation = _rot; + _initialScale = _size.width * _size.height; + } else { + // Single-finger gesture: prepare for drag + _isTwoFingerGesture = false; + } + widget.onDragStart(); + } + }, + onScaleUpdate: (details) { + if (widget.isSelected && !widget.isEditing) { + if (_isTwoFingerGesture && details.pointerCount == 2) { + // Two-finger: handle rotation and zoom + final newRotation = _initialRotation + details.rotation; + + // Handle scale (zoom) - maintain aspect ratio + final scaleFactor = details.scale; + final newArea = + _initialScale * scaleFactor * scaleFactor; + final aspectRatio = _size.width / _size.height; + final newWidth = math + .sqrt(newArea * aspectRatio) + .clamp(20.0, 5000.0); + final newHeight = newWidth / aspectRatio; + + setState(() { + _rot = newRotation % (2 * math.pi); + _size = Size(newWidth, newHeight); + }); + + widget.onUpdate(_pos, _size, _rot); + } else if (!_isTwoFingerGesture && + details.pointerCount == 1) { + // Single-finger: handle drag using incremental focal point delta + final currentFocalPoint = details.focalPoint; + final delta = currentFocalPoint - _previousFocalPoint; + // Convert to canvas coordinates by dividing by zoom + final zoom = widget.viewScale; + final scaledDelta = delta / zoom; + // Rotate delta to account for element rotation + final rotated = _rotateVector(scaledDelta, -_rot); + setState(() { + _pos += scaledDelta; + _previousFocalPoint = + currentFocalPoint; // Update for next frame + }); + widget.onUpdate(_pos, _size, _rot); + } + } + }, + onScaleEnd: (details) { + if (widget.isSelected && !widget.isEditing) { + _isTwoFingerGesture = false; + widget.onDragEnd(_pos, _size, _rot); + } + }, + child: Container( + width: _size.width, + height: _size.height, + decoration: + widget.isSelected + ? BoxDecoration( + border: Border.all( + color: Color(0xFFB44CFF), + width: 2 * handleScale, + ), + ) + : null, + child: + widget.type == "file_image" + ? Image.file( + File(widget.content), + fit: BoxFit.contain, + ) + : _buildText(), + ), + ), + + // ====================== + // RESIZE EDGES (4 SIDES) + // ====================== + + // RIGHT edge + if (widget.isSelected && !widget.isEditing) + Positioned( + right: -edgeThickness / 2, + top: 0, + bottom: 0, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onPanUpdate: (d) { + setState(() { + _size = Size(_size.width + d.delta.dx, _size.height); + }); + widget.onUpdate(_pos, _size, _rot); + }, + onPanStart: (_) => widget.onDragStart(), + onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot), + child: Container( + width: edgeThickness, + color: Colors.transparent, + ), + ), + ), + + // LEFT edge + if (widget.isSelected && !widget.isEditing) + Positioned( + left: -edgeThickness / 2, + top: 0, + bottom: 0, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onPanUpdate: (d) { + setState(() { + _pos += Offset(d.delta.dx, 0); + _size = Size(_size.width - d.delta.dx, _size.height); + }); + widget.onUpdate(_pos, _size, _rot); + }, + onPanStart: (_) => widget.onDragStart(), + onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot), + child: Container( + width: edgeThickness, + color: Colors.transparent, + ), + ), + ), + + // TOP edge + if (widget.isSelected && !widget.isEditing) + Positioned( + top: -edgeThickness / 2, + left: 0, + right: 0, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onPanUpdate: (d) { + setState(() { + _pos += Offset(0, d.delta.dy); + _size = Size(_size.width, _size.height - d.delta.dy); + }); + widget.onUpdate(_pos, _size, _rot); + }, + onPanStart: (_) => widget.onDragStart(), + onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot), + child: Container( + height: edgeThickness, + color: Colors.transparent, + ), + ), + ), + + // BOTTOM edge + if (widget.isSelected && !widget.isEditing) + Positioned( + bottom: -edgeThickness / 2, + left: 0, + right: 0, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onPanUpdate: (d) { + setState(() { + _size = Size(_size.width, _size.height + d.delta.dy); + }); + widget.onUpdate(_pos, _size, _rot); + }, + onPanStart: (_) => widget.onDragStart(), + onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot), + child: Container( + height: edgeThickness, + color: Colors.transparent, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildCircleButton({ + required double size, + required IconData icon, + required double iconSize, + required Function(DragUpdateDetails) onDrag, + }) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onPanStart: (details) => widget.onDragStart(), + onPanUpdate: onDrag, + onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot), + child: Container( + width: size, + height: size, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Center(child: Icon(icon, size: iconSize, color: Colors.black87)), + ), + ); + } + + Widget _buildText() { + final style = TextStyle( + fontSize: (widget.styleData['style_fontSize'] ?? 24.0) as double, + color: Color(widget.styleData['style_color'] ?? Colors.black.value), + fontFamily: 'GeneralSans', + ); + + if (widget.isEditing) { + return Center( + child: IntrinsicWidth( + child: TextField( + controller: widget.textController, + focusNode: widget.focusNode, + autofocus: true, + maxLines: null, + textAlign: TextAlign.center, + style: style, + decoration: const InputDecoration( + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + onChanged: (text) { + final span = TextSpan(text: text, style: style); + final tp = TextPainter( + text: span, + textDirection: TextDirection.ltr, + ); + tp.layout(maxWidth: 10000); + setState(() { + _size = Size(tp.width + 40, tp.height + 40); + }); + widget.onUpdate(_pos, _size, _rot); + }, + ), + ), + ); + } + return Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(widget.content, textAlign: TextAlign.center, style: style), + ), + ); + } + + Widget _buildHandle({ + required double touchSize, + required double visualSize, + required IconData icon, + required Color color, + required Function(Offset) onDrag, + }) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onPanStart: (_) => widget.onDragStart(), + onPanUpdate: (details) => onDrag(details.delta), + onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot), + child: Container( + width: touchSize, + height: touchSize, + alignment: Alignment.center, + color: Colors.transparent, + child: Container( + width: visualSize, + height: visualSize, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.26), blurRadius: 4), + ], + ), + child: Icon(icon, size: visualSize * 0.6, color: Colors.white), + ), + ), + ); + } + + Offset _rotateVector(Offset vector, double angle) { + final cosA = math.cos(angle); + final sinA = math.sin(angle); + return Offset( + vector.dx * cosA - vector.dy * sinA, + vector.dx * sinA + vector.dy * cosA, + ); + } +} + +// --- PAINTER --- +class CanvasPainter extends CustomPainter { + final List<DrawingPath> paths; + final List<DrawingPath> magicPaths; // ADDED: Magic paths for temporary mask + final List<DrawingPoint> currentPoints; + final Color currentColor; + final double currentWidth; + final bool isEraser; + CanvasPainter({ + required this.paths, + this.magicPaths = const [], // ADDED + required this.currentPoints, + required this.currentColor, + required this.currentWidth, + required this.isEraser, + }); + + @override + void paint(Canvas canvas, Size size) { + canvas.saveLayer(Rect.fromLTWH(0, 0, size.width, size.height), Paint()); + + // Draw normal paths + for (final path in paths) { + _drawPath(canvas, path); + } + + // Draw magic paths (unless hidden by empty list passed in) + for (final path in magicPaths) { + _drawPath(canvas, path); + } + + // Draw current points + if (currentPoints.isNotEmpty) { + final paint = + Paint() + ..color = isEraser ? Colors.transparent : currentColor + ..blendMode = isEraser ? BlendMode.clear : BlendMode.srcOver + ..strokeWidth = currentWidth + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round + ..style = PaintingStyle.stroke; + final Path p = Path(); + p.moveTo(currentPoints.first.offset.dx, currentPoints.first.offset.dy); + for (int i = 1; i < currentPoints.length; i++) + p.lineTo(currentPoints[i].offset.dx, currentPoints[i].offset.dy); + canvas.drawPath(p, paint); + } + canvas.restore(); + } + + void _drawPath(Canvas canvas, DrawingPath path) { + final paint = + Paint() + ..color = path.isEraser ? Colors.transparent : path.color + ..blendMode = path.isEraser ? BlendMode.clear : BlendMode.srcOver + ..strokeWidth = path.strokeWidth + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round + ..style = PaintingStyle.stroke; + + if (path.points.length > 1) { + final Path p = Path(); + p.moveTo(path.points.first.offset.dx, path.points.first.offset.dy); + for (int i = 1; i < path.points.length; i++) + p.lineTo(path.points[i].offset.dx, path.points[i].offset.dy); + canvas.drawPath(p, paint); + } else if (path.points.isNotEmpty) { + canvas.drawPoints(ui.PointMode.points, [path.points.first.offset], paint); + } + } + + @override + bool shouldRepaint(covariant CanvasPainter oldDelegate) => true; +} + +class CanvasBottomBar extends StatelessWidget { + final String? activeItem; + final VoidCallback onMagicDraw; + final VoidCallback onMedia; + final VoidCallback onStylesheet; + final VoidCallback onTools; + final VoidCallback onText; + final VoidCallback onSelect; + final VoidCallback onPlugins; + const CanvasBottomBar({ + super.key, + this.activeItem, + required this.onMagicDraw, + required this.onMedia, + required this.onStylesheet, + required this.onTools, + required this.onText, + required this.onSelect, + required this.onPlugins, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Colors.grey[200]!)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: SafeArea( + top: false, // We don't need top SafeArea as it's a bottom bar + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + _BottomBarItem( + label: "Magic Draw", + iconPath: "assets/icons/magic_draw.svg", + onTap: onMagicDraw, + isActive: activeItem == "Magic Draw", + ), + const SizedBox(width: 24), + _BottomBarItem( + label: "Media", + iconPath: "assets/icons/media.svg", + onTap: onMedia, + ), + const SizedBox(width: 24), + _BottomBarItem( + label: "Stylesheet", + iconPath: "assets/icons/stylesheet.svg", + onTap: onStylesheet, + ), + const SizedBox(width: 24), + _BottomBarItem( + label: "Tools", + iconPath: "assets/icons/tools.svg", + onTap: onTools, + ), + const SizedBox(width: 24), + _BottomBarItem( + label: "Text", + iconPath: "assets/icons/text.svg", + onTap: onText, + isActive: activeItem == "Text", + ), + const SizedBox(width: 24), + _BottomBarItem( + label: "Select", + iconPath: "assets/icons/select.svg", + onTap: onSelect, + ), + const SizedBox(width: 24), + _BottomBarItem( + label: "Plugins", + iconPath: "assets/icons/plugins.svg", + onTap: onPlugins, + ), + ], + ), + ), + ), + ), + ); + } +} + +class _BottomBarItem extends StatelessWidget { + final String label; + final String iconPath; + final VoidCallback onTap; + final bool isActive; + const _BottomBarItem({ + required this.label, + required this.iconPath, + required this.onTap, + this.isActive = false, + }); + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + iconPath, + width: 24, + colorFilter: ColorFilter.mode( + isActive ? const Color(0xFF27272A) : const Color(0xFF9F9FA9), + BlendMode.srcIn, + ), + ), + const SizedBox(height: 6), + Text( + label, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: + isActive + ? const Color(0xFF27272A) + : const Color(0xFF9F9FA9), + ), + ), + ], + ), + ), + ); + } +} + +class _AssetPickerSheet extends StatefulWidget { + final int projectId; + final ScrollController scrollController; + final Function(List<String>) onAddAssets; // Accepts List + + const _AssetPickerSheet({ + required this.projectId, + required this.scrollController, + required this.onAddAssets, + }); + + @override + State<_AssetPickerSheet> createState() => _AssetPickerSheetState(); +} + +class _AssetPickerSheetState extends State<_AssetPickerSheet> { + List<String> _assets = []; + bool _isLoading = true; + Set<String> _selectedPaths = {}; // Supports multi-select + + @override + void initState() { + super.initState(); + _loadAssets(); + } + + Future<void> _loadAssets() async { + try { + final project = await ProjectRepo().getProjectById(widget.projectId); + if (mounted) { + setState(() { + _assets = project?.assetsPath ?? []; + _isLoading = false; + }); + } + } catch (e) { + debugPrint("Error loading assets: $e"); + if (mounted) setState(() => _isLoading = false); + } + } + + Future<File?> _resolveFile(String path) async { + final file = File(path); + if (await file.exists()) return file; + try { + final filename = p.basename(path); + final dir = await getApplicationDocumentsDirectory(); + final fixedPath = '${dir.path}/generated_images/$filename'; + final fixedFile = File(fixedPath); + if (await fixedFile.exists()) return fixedFile; + } catch (e) { + debugPrint("Error resolving file: $e"); + } + return null; + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + padding: const EdgeInsets.fromLTRB(20, 12, 20, 0), + child: Column( + children: [ + // Handle + Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + + // Search Bar + Container( + height: 40, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + const SizedBox(width: 12), + const Icon(Icons.search, color: Colors.grey), + const SizedBox(width: 8), + const Text( + "Search Stylesheet", + style: TextStyle( + fontFamily: 'GeneralSans', + color: Colors.grey, + ), + ), + ], + ), + ), + + // Category Tabs + Container( + margin: const EdgeInsets.only(bottom: 16), + child: Row( + children: [ + _buildFilterChip("Assets", true), + const SizedBox(width: 12), + _buildFilterChip("Backgrounds & Texture", false), + ], + ), + ), + + // Grid + Expanded( + child: + _isLoading + ? const Center(child: CircularProgressIndicator()) + : _assets.isEmpty + ? Center( + child: Text( + "No assets found in stylesheet", + style: TextStyle(color: Colors.grey[500]), + ), + ) + : Stack( + children: [ + GridView.builder( + controller: widget.scrollController, + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 1.0, + ), + itemCount: _assets.length, + itemBuilder: (context, index) { + final assetPath = _assets[index]; + final isSelected = _selectedPaths.contains( + assetPath, + ); + + return FutureBuilder<File?>( + future: _resolveFile(assetPath), + builder: (context, snapshot) { + final file = snapshot.data; + return _buildAssetTile( + child: + file != null + ? Image.file(file, fit: BoxFit.cover) + : const Icon( + Icons.broken_image, + color: Colors.grey, + ), + isSelected: isSelected, + onTap: () { + if (file != null) { + setState(() { + if (isSelected) { + _selectedPaths.remove(assetPath); + } else { + _selectedPaths.add(assetPath); + } + }); + } + }, + ); + }, + ); + }, + ), + ], + ), + ), + + // Bottom CTA + SafeArea( + top: false, + child: Container( + width: double.infinity, + margin: const EdgeInsets.only(top: 16, bottom: 16), + child: ElevatedButton( + onPressed: () => widget.onAddAssets(_selectedPaths.toList()), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF27272A), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: const Text( + "Add to File", + style: TextStyle( + fontFamily: 'GeneralSans', + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildFilterChip(String label, bool isSelected) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: isSelected ? const Color(0xFFF4F4F5) : Colors.transparent, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isSelected ? Colors.transparent : Colors.grey[300]!, + ), + ), + child: Text( + label, + style: TextStyle( + fontFamily: 'GeneralSans', + fontSize: 14, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, + color: isSelected ? Colors.black : Colors.grey[600], + ), + ), + ); + } + + Widget _buildAssetTile({ + required Widget child, + required VoidCallback onTap, + bool isSelected = false, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected ? Colors.blue : Colors.grey[200]!, + width: isSelected ? 2 : 1, + ), + ), + clipBehavior: Clip.antiAlias, + child: Stack( + fit: StackFit.expand, + children: [ + child, + if (isSelected) + Container( + color: Colors.blue.withOpacity(0.1), + child: const Center( + child: Icon(Icons.check_circle, color: Colors.blue), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui/pages/canvas_toolbar/magic_draw_overlay.dart b/lib/ui/pages/canvas_toolbar/magic_draw_overlay.dart @@ -71,23 +71,23 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { // --- 1. LIST FOR INPAINTING (When Image Exists) --- final List<AIModelOption> _inpaintingModels = [ - AIModelOption(id: 'inpaint_api', name: 'FLUX LoRa Fill', badge: null), AIModelOption( id: 'inpaint_standard', name: 'Stable Diffusion Inpainting', badge: "Recommended", ), + AIModelOption(id: 'inpaint_api', name: 'FLUX LoRa Fill', badge: null), ]; // --- 2. LIST FOR SKETCH TO IMAGE (When No Image Exists) --- final List<AIModelOption> _sketchModels = [ - AIModelOption(id: 'sketch_fusion', name: 'Stable Diffusion v1.5', badge: null), - AIModelOption(id: 'sketch_creative', name: 'FLUX Dev', badge: 'Fast'), AIModelOption( id: 'sketch_advanced', name: 'Nano Banana', badge: 'Premium', ), + AIModelOption(id: 'sketch_fusion', name: 'Stable Diffusion v1.5', badge: null), + AIModelOption(id: 'sketch_creative', name: 'FLUX Dev', badge: 'Fast'), ]; @override diff --git a/lib/ui/pages/create_file_page.dart b/lib/ui/pages/create_file_page.dart @@ -1,16 +1,12 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'canvas_board_page.dart'; +import 'package:creekui/services/project_service.dart'; +import 'package:creekui/services/image_service.dart'; +import 'package:creekui/data/models/project_model.dart'; +import 'canvas_page.dart'; import 'define_brand_page.dart'; -// Services -import '../../services/project_service.dart'; -import '../../services/image_service.dart'; - -// Models -import '../../data/models/project_model.dart'; - class CreateFilePage extends StatefulWidget { final File? file; final int? projectId; // ⭐ MAKE NULLABLE @@ -172,7 +168,7 @@ class _CreateFilePageState extends State<CreateFilePage> { context, MaterialPageRoute( builder: - (_) => CanvasBoardPage( + (_) => CanvasPage( projectId: _selectedProjectId!, // ⭐ use selected project width: width.toDouble(), height: height.toDouble(), diff --git a/lib/ui/pages/define_brand_page.dart b/lib/ui/pages/define_brand_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:adobe/ui/styles/variables.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import '../../services/project_service.dart'; +import 'package:creekui/services/project_service.dart'; +import 'package:creekui/ui/styles/variables.dart'; import 'project_detail_page.dart'; class DefineBrandPage extends StatefulWidget { diff --git a/lib/ui/pages/home_page.dart b/lib/ui/pages/home_page.dart @@ -1,16 +1,16 @@ import 'dart:io'; import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:adobe/data/models/project_model.dart'; -import 'package:adobe/data/models/file_model.dart'; -import 'package:adobe/data/repos/project_repo.dart'; -import 'package:adobe/data/repos/image_repo.dart'; -import 'package:adobe/data/repos/file_repo.dart'; -import 'package:adobe/services/project_service.dart'; import 'package:image/image.dart' as img; +import 'package:creekui/data/models/project_model.dart'; +import 'package:creekui/data/models/file_model.dart'; +import 'package:creekui/data/repos/project_repo.dart'; +import 'package:creekui/data/repos/image_repo.dart'; +import 'package:creekui/data/repos/file_repo.dart'; +import 'package:creekui/services/project_service.dart'; import 'project_detail_page.dart'; import 'define_brand_page.dart'; -import 'canvas_board_page.dart'; +import 'canvas_page.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -223,7 +223,7 @@ class _HomePageState extends State<HomePage> { Navigator.push( context, MaterialPageRoute( - builder: (_) => CanvasBoardPage( + builder: (_) => CanvasPage( projectId: file.projectId, width: width, height: height, diff --git a/lib/ui/pages/image_analysis_page.dart b/lib/ui/pages/image_analysis_page.dart @@ -3,8 +3,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:adobe/services/analyze/image_analyzer.dart'; -import '../styles/variables.dart'; +import 'package:creekui/services/analyze/image_analyzer.dart'; +import 'package:creekui/styles/variables.dart'; class ImageAnalysisPage extends StatefulWidget { const ImageAnalysisPage({super.key}); diff --git a/lib/ui/pages/image_details_page.dart b/lib/ui/pages/image_details_page.dart @@ -2,11 +2,11 @@ import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import '../../services/image_service.dart'; -import '../../services/note_service.dart'; -import '../../data/models/note_model.dart'; -import '../../data/models/image_model.dart'; -import '../../utils/image_actions_helper.dart'; +import 'package:creekui/services/image_service.dart'; +import 'package:creekui/services/note_service.dart'; +import 'package:creekui/data/models/note_model.dart'; +import 'package:creekui/data/models/image_model.dart'; +import 'package:creekui/utils/image_actions_helper.dart'; // --- STATE MACHINE FOR SELECTION MODE --- enum DragHandle { diff --git a/lib/ui/pages/image_save_page.dart b/lib/ui/pages/image_save_page.dart @@ -2,8 +2,8 @@ import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import '../../services/image_service.dart'; -import '../../services/note_service.dart'; +import 'package:creekui/services/image_service.dart'; +import 'package:creekui/services/note_service.dart'; import 'project_board_page.dart'; // --- HELPER CLASS FOR TEMPORARY NOTES --- diff --git a/lib/ui/pages/project_board_page.dart b/lib/ui/pages/project_board_page.dart @@ -1,20 +1,20 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:adobe/ui/styles/variables.dart'; -import 'package:adobe/ui/widgets/top_bar.dart'; -import 'package:adobe/ui/widgets/bottom_bar.dart'; -import '../../data/models/project_model.dart'; -import '../../data/models/image_model.dart'; -import '../../data/repos/project_repo.dart'; -import '../../data/repos/note_repo.dart'; -import '../../data/repos/image_repo.dart'; +import 'package:creekui/ui/styles/variables.dart'; +import 'package:creekui/ui/widgets/top_bar.dart'; +import 'package:creekui/ui/widgets/bottom_bar.dart'; +import 'package:creekui/data/models/project_model.dart'; +import 'package:creekui/data/models/image_model.dart'; +import 'package:creekui/data/repos/project_repo.dart'; +import 'package:creekui/data/repos/note_repo.dart'; +import 'package:creekui/data/repos/image_repo.dart'; +import 'package:creekui/ui/widgets/image_context_menu.dart'; import 'project_tag_page.dart'; import 'project_board_page_alternate.dart'; import 'image_save_page.dart'; import 'image_details_page.dart'; import 'stylesheet_page.dart'; -import '../widgets/image_context_menu.dart'; class ProjectBoardPage extends StatefulWidget { final int projectId; diff --git a/lib/ui/pages/project_board_page_alternate.dart b/lib/ui/pages/project_board_page_alternate.dart @@ -1,10 +1,10 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:adobe/ui/styles/variables.dart'; -import '../../data/models/image_model.dart'; -import '../../data/repos/image_repo.dart'; +import 'package:creekui/ui/styles/variables.dart'; +import 'package:creekui/data/models/image_model.dart'; +import 'package:creekui/data/repos/image_repo.dart'; +import 'package:creekui/ui/widgets/image_context_menu.dart'; import 'image_details_page.dart'; -import '../widgets/image_context_menu.dart'; class ProjectBoardPageAlternate extends StatefulWidget { final int projectId; diff --git a/lib/ui/pages/project_detail_page.dart b/lib/ui/pages/project_detail_page.dart @@ -1,11 +1,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import '../../data/models/project_model.dart'; -import '../../data/models/image_model.dart'; -import '../../data/repos/project_repo.dart'; -import '../../data/repos/image_repo.dart'; -import '../../services/project_service.dart'; +import 'package:creekui/data/models/project_model.dart'; +import 'package:creekui/data/models/image_model.dart'; +import 'package:creekui/data/repos/project_repo.dart'; +import 'package:creekui/data/repos/image_repo.dart'; +import 'package:creekui/services/project_service.dart'; import 'project_board_page.dart'; import 'stylesheet_page.dart'; import 'project_file_page.dart'; diff --git a/lib/ui/pages/project_file_page.dart b/lib/ui/pages/project_file_page.dart @@ -1,18 +1,16 @@ import 'dart:io'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:image/image.dart' as img; import 'package:intl/intl.dart'; - -import '../../data/models/file_model.dart'; -import '../../data/models/project_model.dart'; -import '../../services/file_service.dart'; -import '../../data/repos/project_repo.dart'; -import '../widgets/bottom_bar.dart'; +import 'package:creekui/data/models/file_model.dart'; +import 'package:creekui/data/models/project_model.dart'; +import 'package:creekui/services/file_service.dart'; +import 'package:creekui/data/repos/project_repo.dart'; +import 'package:creekui/ui/widgets/top_bar.dart'; +import 'package:creekui/ui/widgets/bottom_bar.dart'; import 'create_file_page.dart'; -import 'canvas_board_page.dart'; -import '../widgets/top_bar.dart'; - -import 'package:image/image.dart' as img; +import 'canvas_page.dart'; class ProjectFilePage extends StatefulWidget { final int projectId; @@ -138,7 +136,7 @@ class _ProjectFilePageState extends State<ProjectFilePage> { context, MaterialPageRoute( builder: - (_) => CanvasBoardPage( + (_) => CanvasPage( projectId: file.projectId, width: 1080, height: 1920, diff --git a/lib/ui/pages/project_tag_page.dart b/lib/ui/pages/project_tag_page.dart @@ -1,16 +1,16 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:adobe/ui/styles/variables.dart'; -import 'package:adobe/ui/widgets/top_bar.dart'; -import 'package:adobe/ui/widgets/bottom_bar.dart'; -import '../../data/models/image_model.dart'; -import '../../data/repos/image_repo.dart'; -import '../../data/repos/project_repo.dart'; -import '../../data/repos/note_repo.dart'; +import 'package:creekui/ui/styles/variables.dart'; +import 'package:creekui/ui/widgets/top_bar.dart'; +import 'package:creekui/ui/widgets/bottom_bar.dart'; +import 'package:creekui/data/models/image_model.dart'; +import 'package:creekui/data/repos/image_repo.dart'; +import 'package:creekui/data/repos/project_repo.dart'; +import 'package:creekui/data/repos/note_repo.dart'; +import 'package:creekui/ui/widgets/image_context_menu.dart'; import 'image_save_page.dart'; import 'image_details_page.dart'; -import '../widgets/image_context_menu.dart'; class ProjectTagPage extends StatefulWidget { final int projectId; diff --git a/lib/ui/pages/share_handler_page.dart b/lib/ui/pages/share_handler_page.dart @@ -1,10 +1,10 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:adobe/services/download_service.dart'; -import 'package:adobe/services/instagram_download_service.dart'; -import 'package:adobe/services/image_service.dart'; -import 'package:adobe/ui/pages/share_to_moodboard_page.dart'; -import 'package:adobe/ui/pages/share_to_file_page.dart'; +import 'package:creekui/services/download_service.dart'; +import 'package:creekui/services/instagram_download_service.dart'; +import 'package:creekui/services/image_service.dart'; +import 'share_to_moodboard_page.dart'; +import 'share_to_file_page.dart'; class ShareHandlerPage extends StatefulWidget { final String sharedText; diff --git a/lib/ui/pages/share_to_file_page.dart b/lib/ui/pages/share_to_file_page.dart @@ -1,15 +1,13 @@ import 'dart:io'; import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:image/image.dart' as img; - -import '../../services/file_service.dart'; -import '../../services/project_service.dart'; -import '../../data/models/file_model.dart'; -import '../../data/models/project_model.dart'; +import 'package:creekui/services/file_service.dart'; +import 'package:creekui/services/project_service.dart'; +import 'package:creekui/data/models/file_model.dart'; +import 'package:creekui/data/models/project_model.dart'; import 'create_file_page.dart'; -import 'canvas_board_page.dart'; +import 'canvas_page.dart'; class ShareToFilePage extends StatefulWidget { final File sharedImage; @@ -241,7 +239,7 @@ class _ShareToFilePageState extends State<ShareToFilePage> { context, MaterialPageRoute( builder: - (context) => CanvasBoardPage( + (context) => CanvasPage( projectId: file.projectId, width: width, height: height, diff --git a/lib/ui/pages/share_to_moodboard_page.dart b/lib/ui/pages/share_to_moodboard_page.dart @@ -1,16 +1,10 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; - -// Models & Repos -import '../../data/models/project_model.dart'; -import '../../data/repos/project_repo.dart'; -import '../../data/repos/image_repo.dart'; - -// Services -import '../../services/project_service.dart'; - -// Pages +import 'package:creekui/data/models/project_model.dart'; +import 'package:creekui/data/repos/project_repo.dart'; +import 'package:creekui/data/repos/image_repo.dart'; +import 'package:creekui/services/project_service.dart'; import 'image_save_page.dart'; // --- VIEW MODELS --- diff --git a/lib/ui/pages/stylesheet_page.dart b/lib/ui/pages/stylesheet_page.dart @@ -6,14 +6,14 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; -import 'package:adobe/ui/styles/variables.dart'; -import 'package:adobe/ui/widgets/bottom_bar.dart'; -import 'package:adobe/ui/widgets/top_bar.dart'; -import 'package:adobe/data/repos/project_repo.dart'; -import 'package:adobe/data/repos/image_repo.dart'; -import 'package:adobe/data/repos/note_repo.dart'; -import 'package:adobe/services/stylesheet_service.dart'; -import 'package:adobe/services/python_service.dart'; +import 'package:creekui/data/repos/project_repo.dart'; +import 'package:creekui/data/repos/image_repo.dart'; +import 'package:creekui/data/repos/note_repo.dart'; +import 'package:creekui/services/stylesheet_service.dart'; +import 'package:creekui/services/python_service.dart'; +import 'package:creekui/ui/widgets/bottom_bar.dart'; +import 'package:creekui/ui/widgets/top_bar.dart'; +import 'package:creekui/ui/styles/variables.dart'; class StylesheetPage extends StatefulWidget { final int projectId; diff --git a/lib/ui/widgets/bottom_bar.dart b/lib/ui/widgets/bottom_bar.dart @@ -1,10 +1,10 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:adobe/ui/styles/variables.dart'; -import 'package:adobe/ui/pages/project_board_page.dart'; -import 'package:adobe/ui/pages/stylesheet_page.dart'; -import 'package:adobe/ui/pages/project_file_page.dart'; +import 'package:creekui/ui/styles/variables.dart'; +import 'package:creekui/ui/pages/project_board_page.dart'; +import 'package:creekui/ui/pages/stylesheet_page.dart'; +import 'package:creekui/ui/pages/project_file_page.dart'; enum BottomBarItem { moodboard, stylesheet, files } diff --git a/lib/ui/widgets/image_context_menu.dart b/lib/ui/widgets/image_context_menu.dart @@ -1,9 +1,9 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import '../../data/models/image_model.dart'; -import '../../utils/image_actions_helper.dart'; -import '../styles/variables.dart'; +import 'package:creekui/data/models/image_model.dart'; +import 'package:creekui/utils/image_actions_helper.dart'; +import 'package:creekui/ui/styles/variables.dart'; class ImageContextMenu extends StatelessWidget { final ImageModel image; diff --git a/lib/ui/widgets/top_bar.dart b/lib/ui/widgets/top_bar.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:adobe/data/models/project_model.dart'; -import 'package:adobe/data/repos/project_repo.dart'; -import '../styles/variables.dart'; +import 'package:creekui/data/models/project_model.dart'; +import 'package:creekui/data/repos/project_repo.dart'; +import 'package:creekui/ui/styles/variables.dart'; class TopBar extends StatefulWidget implements PreferredSizeWidget { final int currentProjectId; diff --git a/lib/utils/clip_image_processor.dart b/lib/utils/clip_image_processor.dart @@ -1,5 +1,3 @@ -// lib/utils/clip_image_processor.dart - import 'dart:typed_data'; import 'dart:io'; import 'package:image/image.dart' as img; diff --git a/lib/utils/image_actions_helper.dart b/lib/utils/image_actions_helper.dart @@ -1,10 +1,10 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:share_plus/share_plus.dart'; -import '../data/models/image_model.dart'; -import '../services/image_service.dart'; -import '../ui/pages/share_to_file_page.dart'; -import '../ui/styles/variables.dart'; +import 'package:creekui/data/models/image_model.dart'; +import 'package:creekui/services/image_service.dart'; +import 'package:creekui/ui/pages/share_to_file_page.dart'; +import 'package:creekui/ui/styles/variables.dart'; class ImageActionsHelper { static Future<void> shareImage(BuildContext context, String filePath) async { diff --git a/lib/utils/image_utils.dart b/lib/utils/image_utils.dart @@ -1,5 +1,3 @@ -// lib/utils/image_utils.dart - import 'dart:math'; List<double> l2Normalize(List<double> vec) { diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt @@ -4,10 +4,10 @@ project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "adobe") +set(BINARY_NAME "creekui") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.adobe") +set(APPLICATION_ID "com.creek.ui") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc @@ -7,17 +7,19 @@ #include "flutter/generated_plugin_registrant.h" -struct _MyApplication { +struct _MyApplication +{ GtkApplication parent_instance; - char** dart_entrypoint_arguments; + char **dart_entrypoint_arguments; }; G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) // Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); - GtkWindow* window = +static void my_application_activate(GApplication *application) +{ + MyApplication *self = MY_APPLICATION(application); + GtkWindow *window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); // Use a header bar when running in GNOME as this is the common style used @@ -29,22 +31,27 @@ static void my_application_activate(GApplication* application) { // if future cases occur). gboolean use_header_bar = TRUE; #ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + GdkScreen *screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) + { + const gchar *wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) + { use_header_bar = FALSE; } } #endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + if (use_header_bar) + { + GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "adobe"); + gtk_header_bar_set_title(header_bar, "creekui"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "adobe"); + } + else + { + gtk_window_set_title(window, "creekui"); } gtk_window_set_default_size(window, 1280, 720); @@ -53,7 +60,7 @@ static void my_application_activate(GApplication* application) { g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); - FlView* view = fl_view_new(project); + FlView *view = fl_view_new(project); gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); @@ -63,16 +70,18 @@ static void my_application_activate(GApplication* application) { } // Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { - MyApplication* self = MY_APPLICATION(application); +static gboolean my_application_local_command_line(GApplication *application, gchar ***arguments, int *exit_status) +{ + MyApplication *self = MY_APPLICATION(application); // Strip out the first argument as it is the binary name. self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; + if (!g_application_register(application, nullptr, &error)) + { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; } g_application_activate(application); @@ -82,8 +91,9 @@ static gboolean my_application_local_command_line(GApplication* application, gch } // Implements GApplication::startup. -static void my_application_startup(GApplication* application) { - //MyApplication* self = MY_APPLICATION(object); +static void my_application_startup(GApplication *application) +{ + // MyApplication* self = MY_APPLICATION(object); // Perform any actions required at application startup. @@ -91,8 +101,9 @@ static void my_application_startup(GApplication* application) { } // Implements GApplication::shutdown. -static void my_application_shutdown(GApplication* application) { - //MyApplication* self = MY_APPLICATION(object); +static void my_application_shutdown(GApplication *application) +{ + // MyApplication* self = MY_APPLICATION(object); // Perform any actions required at application shutdown. @@ -100,13 +111,15 @@ static void my_application_shutdown(GApplication* application) { } // Implements GObject::dispose. -static void my_application_dispose(GObject* object) { - MyApplication* self = MY_APPLICATION(object); +static void my_application_dispose(GObject *object) +{ + MyApplication *self = MY_APPLICATION(object); g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); G_OBJECT_CLASS(my_application_parent_class)->dispose(object); } -static void my_application_class_init(MyApplicationClass* klass) { +static void my_application_class_init(MyApplicationClass *klass) +{ G_APPLICATION_CLASS(klass)->activate = my_application_activate; G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; G_APPLICATION_CLASS(klass)->startup = my_application_startup; @@ -114,9 +127,10 @@ static void my_application_class_init(MyApplicationClass* klass) { G_OBJECT_CLASS(klass)->dispose = my_application_dispose; } -static void my_application_init(MyApplication* self) {} +static void my_application_init(MyApplication *self) {} -MyApplication* my_application_new() { +MyApplication *my_application_new() +{ // Set the program name to the application ID, which helps various systems // like GTK and desktop environments map this running application to its // corresponding .desktop file. This ensures better integration by allowing diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj @@ -64,7 +64,7 @@ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; }; - 33CC10ED2044A3C60003C045 /* adobe.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "adobe.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* creekui.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "creekui.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; }; @@ -131,7 +131,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* adobe.app */, + 33CC10ED2044A3C60003C045 /* creekui.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; @@ -217,7 +217,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* adobe.app */; + productReference = 33CC10ED2044A3C60003C045 /* creekui.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -385,10 +385,10 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.adobe.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.creek.ui.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/adobe.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/adobe"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/creekui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/creekui"; }; name = Debug; }; @@ -399,10 +399,10 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.adobe.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.creek.ui.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/adobe.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/adobe"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/creekui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/creekui"; }; name = Release; }; @@ -413,10 +413,10 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.adobe.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.creek.ui.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/adobe.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/adobe"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/creekui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/creekui"; }; name = Profile; }; diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "33CC10EC2044A3C60003C045" - BuildableName = "adobe.app" + BuildableName = "creekui.app" BlueprintName = "Runner" ReferencedContainer = "container:Runner.xcodeproj"> </BuildableReference> @@ -31,7 +31,7 @@ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "33CC10EC2044A3C60003C045" - BuildableName = "adobe.app" + BuildableName = "creekui.app" BlueprintName = "Runner" ReferencedContainer = "container:Runner.xcodeproj"> </BuildableReference> @@ -66,7 +66,7 @@ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "33CC10EC2044A3C60003C045" - BuildableName = "adobe.app" + BuildableName = "creekui.app" BlueprintName = "Runner" ReferencedContainer = "container:Runner.xcodeproj"> </BuildableReference> @@ -83,7 +83,7 @@ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "33CC10EC2044A3C60003C045" - BuildableName = "adobe.app" + BuildableName = "creekui.app" BlueprintName = "Runner" ReferencedContainer = "container:Runner.xcodeproj"> </BuildableReference> diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig @@ -5,10 +5,10 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = adobe +PRODUCT_NAME = creekui // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.adobe +PRODUCT_BUNDLE_IDENTIFIER = com.creek.ui // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/pubspec.yaml b/pubspec.yaml @@ -1,4 +1,4 @@ -name: adobe +name: creekui description: "AI-based image editor" publish_to: "none" version: 1.0.0+1 @@ -32,6 +32,7 @@ dependencies: dotted_border: ^2.0.0 share_plus: ^12.0.1 flutter_dotenv: ^6.0.0 + flutter_launcher_icons: ^0.14.4 dev_dependencies: flutter_test: @@ -77,3 +78,8 @@ flutter: weight: 500 - asset: assets/fonts/GeneralSans-Semibold.otf weight: 600 + +flutter_launcher_icons: + android: "launcher_icon" + ios: true + image_path: "assets/app_icon.png" diff --git a/web/index.html b/web/index.html @@ -1,7 +1,7 @@ -<!DOCTYPE html> +<!doctype html> <html> -<head> - <!-- + <head> + <!-- If you are serving your web app in a path other than the root, change the href value below to reflect the base path you are serving from. @@ -14,25 +14,25 @@ This is a placeholder for base href that will be replaced by the value of the `--base-href` argument provided to `flutter build`. --> - <base href="$FLUTTER_BASE_HREF"> + <base href="$FLUTTER_BASE_HREF" /> - <meta charset="UTF-8"> - <meta content="IE=Edge" http-equiv="X-UA-Compatible"> - <meta name="description" content="A new Flutter project."> + <meta charset="UTF-8" /> + <meta content="IE=Edge" http-equiv="X-UA-Compatible" /> + <meta name="description" content="A new Flutter project." /> - <!-- iOS meta tags & icons --> - <meta name="mobile-web-app-capable" content="yes"> - <meta name="apple-mobile-web-app-status-bar-style" content="black"> - <meta name="apple-mobile-web-app-title" content="adobe"> - <link rel="apple-touch-icon" href="icons/Icon-192.png"> + <!-- iOS meta tags & icons --> + <meta name="mobile-web-app-capable" content="yes" /> + <meta name="apple-mobile-web-app-status-bar-style" content="black" /> + <meta name="apple-mobile-web-app-title" content="creekui" /> + <link rel="apple-touch-icon" href="icons/Icon-192.png" /> - <!-- Favicon --> - <link rel="icon" type="image/png" href="favicon.png"/> + <!-- Favicon --> + <link rel="icon" type="image/png" href="favicon.png" /> - <title>adobe</title> - <link rel="manifest" href="manifest.json"> -</head> -<body> - <script src="flutter_bootstrap.js" async></script> -</body> + <title>creekui</title> + <link rel="manifest" href="manifest.json" /> + </head> + <body> + <script src="flutter_bootstrap.js" async></script> + </body> </html> diff --git a/web/manifest.json b/web/manifest.json @@ -1,35 +1,35 @@ { - "name": "adobe", - "short_name": "adobe", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] + "name": "creekui", + "short_name": "creekui", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "AI-based image editor", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] } diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt @@ -1,10 +1,10 @@ # Project-level configuration. cmake_minimum_required(VERSION 3.14) -project(adobe LANGUAGES CXX) +project(creekui LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "adobe") +set(BINARY_NAME "creekui") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc @@ -90,12 +90,12 @@ BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "adobe" "\0" + VALUE "FileDescription", "creekui" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "adobe" "\0" + VALUE "InternalName", "creekui" "\0" VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" - VALUE "OriginalFilename", "adobe.exe" "\0" - VALUE "ProductName", "adobe" "\0" + VALUE "OriginalFilename", "creekui.exe" "\0" + VALUE "ProductName", "creekui" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp @@ -6,10 +6,12 @@ #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { + _In_ wchar_t *command_line, _In_ int show_command) +{ // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) + { CreateAndAttachConsole(); } @@ -27,13 +29,15 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.Create(L"adobe", origin, size)) { + if (!window.Create(L"creekui", origin, size)) + { return EXIT_FAILURE; } window.SetQuitOnClose(true); ::MSG msg; - while (::GetMessage(&msg, nullptr, 0, 0)) { + while (::GetMessage(&msg, nullptr, 0, 0)) + { ::TranslateMessage(&msg); ::DispatchMessage(&msg); }