creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
commit cc13ef49d3938c317764d586a6121cbeead3f11d parent 49f6880575e305ba8d2783079825ab34790fb5d2 Author: aditya-samal <samaladitya2004@gmail.com> Date: Tue, 25 Nov 2025 09:14:59 +0530 Done
Diffstat:
| M | .gitignore | | | 2 | ++ |
| M | README.md | | | 58 | +++++++++++++++++++++++++++++++++++++++++++++++++--------- |
| M | android/app/build.gradle.kts | | | 30 | ++++++++++++++++++++++++++++++ |
| M | android/app/src/main/AndroidManifest.xml | | | 3 | +++ |
| A | android/app/src/main/kotlin/com/example/adobe/ImageAnalyzer.kt | | | 31 | +++++++++++++++++++++++++++++++ |
| M | android/app/src/main/kotlin/com/example/adobe/MainActivity.kt | | | 40 | +++++++++++++++++++++++++++++++++++++++- |
| A | android/app/src/main/python/analyzer.py | | | 250 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | android/settings.gradle.kts | | | 2 | ++ |
| M | ios/Flutter/Debug.xcconfig | | | 1 | + |
| M | ios/Flutter/Release.xcconfig | | | 1 | + |
| A | ios/Podfile | | | 43 | +++++++++++++++++++++++++++++++++++++++++++ |
| A | lib/services/image_analyzer_service.dart | | | 29 | +++++++++++++++++++++++++++++ |
| M | lib/ui/pages/home_page.dart | | | 75 | +++++++++++++++++++++++++++++++++++++++++++-------------------------------- |
| A | lib/ui/pages/image_analysis_page.dart | | | 346 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | macos/Flutter/Flutter-Debug.xcconfig | | | 1 | + |
| M | macos/Flutter/Flutter-Release.xcconfig | | | 1 | + |
| A | macos/Podfile | | | 42 | ++++++++++++++++++++++++++++++++++++++++++ |
| M | pubspec.lock | | | 26 | +++++++++++++------------- |
| A | setup_python_env.sh | | | 58 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
19 files changed, 984 insertions(+), 55 deletions(-)
diff --git a/.gitignore b/.gitignore @@ -43,3 +43,5 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release +# Python virtual environment +python_env/ diff --git a/README.md b/README.md @@ -1,16 +1,56 @@ # adobe -A new Flutter project. +A Flutter project with Python image analysis using Chaquopy. -## Getting Started +## Prerequisites -This project is a starting point for a Flutter application. +- Flutter SDK +- Python 3.11 (for build-time) +- Android SDK -A few resources to get you started if this is your first Flutter project: +## Setup -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) +### 1. Install Python 3.11 -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +**macOS:** + +```bash +brew install python@3.11 +``` + +**Linux:** + +```bash +sudo apt-get install python3.11 python3.11-venv +``` + +**Windows:** +Download from [python.org](https://www.python.org/downloads/) + +### 2. Set up Python Virtual Environment + +Run the setup script to create a Python virtual environment in the repository: + +```bash +./setup_python_env.sh +``` + +This creates a `python_env` directory with Python 3.11, which will be used by the build system. This ensures all developers use the same Python version. + +### 3. Build the APK + +```bash +flutter build apk --release +``` + +## Project Structure + +- `android/app/src/main/python/` - Python code (image analysis) +- `python_env/` - Python virtual environment (created by setup script) +- `lib/` - Flutter/Dart code + +## Notes + +- The Python runtime version (embedded in the app) is **3.8** (configured in `android/app/build.gradle.kts`) +- The build-time Python version is **3.11** (from the virtual environment) +- The virtual environment is used only during build, not at runtime diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts @@ -1,6 +1,19 @@ +buildscript { + repositories { + google() + mavenCentral() + maven { url = uri("https://chaquo.com/maven") } + } + dependencies { + classpath("com.android.tools.build:gradle:8.7.0") + classpath("com.chaquo.python:gradle:15.0.1") + } +} + plugins { id("com.android.application") id("kotlin-android") + id("com.chaquo.python") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") } @@ -28,6 +41,11 @@ android { targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName + + ndk { + // On Apple silicon, you can omit x86_64. + abiFilters += listOf("arm64-v8a", "x86_64") + } } buildTypes { @@ -39,6 +57,18 @@ android { } } +chaquopy { + defaultConfig { + version = "3.8" + buildPython(file("../../python_env/bin/python").absolutePath) + pip { + install("pillow") + install("numpy") + install("opencv-python-headless") + } + } +} + flutter { source = "../.." } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,8 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <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.CAMERA"/> <application android:label="adobe" android:name="${applicationName}" diff --git a/android/app/src/main/kotlin/com/example/adobe/ImageAnalyzer.kt b/android/app/src/main/kotlin/com/example/adobe/ImageAnalyzer.kt @@ -0,0 +1,31 @@ +package com.example.adobe + +import com.chaquo.python.Python +import com.chaquo.python.android.AndroidPlatform + +object ImageAnalyzer { + private var pythonInitialized = false + + fun initializePython(context: android.content.Context) { + if (!pythonInitialized) { + if (!Python.isStarted()) { + Python.start(AndroidPlatform(context)) + } + pythonInitialized = true + } + } + + fun analyzeImage(imagePath: String): String? { + return try { + val py = Python.getInstance() + val module = py.getModule("analyzer") + val result = module.callAttr("analyze_single_image", imagePath) + result.toString() + } catch (e: Exception) { + e.printStackTrace() + null + } + } +} + + 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,5 +1,43 @@ package com.example.adobe +import android.os.Bundle import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel +import org.json.JSONObject -class MainActivity : FlutterActivity() +class MainActivity : FlutterActivity() { + private val CHANNEL = "com.example.adobe/image_analyzer" + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + + // Initialize Python + ImageAnalyzer.initializePython(this) + + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> + when (call.method) { + "analyzeImage" -> { + val imagePath = call.argument<String>("imagePath") + if (imagePath != null) { + try { + val analysisResult = ImageAnalyzer.analyzeImage(imagePath) + if (analysisResult != null) { + result.success(analysisResult) + } else { + result.error("ANALYSIS_ERROR", "Failed to analyze image", null) + } + } catch (e: Exception) { + result.error("ANALYSIS_ERROR", e.message, null) + } + } else { + result.error("INVALID_ARGUMENT", "Image path is null", null) + } + } + else -> { + result.notImplemented() + } + } + } + } +} diff --git a/android/app/src/main/python/analyzer.py b/android/app/src/main/python/analyzer.py @@ -0,0 +1,250 @@ +import cv2 +import numpy as np +import os +import json +from math import hypot + +# ========================================== +# 1. HELPER FUNCTIONS +# ========================================== + +def resize_for_fast_processing(img, max_side=640): + h, w = img.shape[:2] + scale = max_side / max(h, w) if max(h, w) > max_side else 1.0 + if scale != 1.0: + img = cv2.resize(img, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA) + return img, scale + +def compute_saliency_gray(img): + sal = None + try: + salience = cv2.saliency.StaticSaliencySpectralResidual_create() + success, sal = salience.computeSaliency(img) + if not success: sal = None + except Exception: + sal = None + if sal is None: + # Fallback: Simple Gaussian Difference + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + blur1 = cv2.GaussianBlur(gray, (3, 3), 0) + blur2 = cv2.GaussianBlur(gray, (21, 21), 0) + sal = cv2.absdiff(blur1, blur2) + sal = sal.astype(np.float32) / (sal.max() + 1e-9) + else: + sal = sal.astype(np.float32) + if sal.max() > 0: sal /= sal.max() + return sal + +def saliency_centroid(sal): + h, w = sal.shape + M = sal.sum() + if M <= 1e-6: return (w / 2, h / 2) + ys, xs = np.indices(sal.shape) + cx = (xs * sal).sum() / M + cy = (ys * sal).sum() / M + return (float(cx), float(cy)) + +def detect_lines(gray, canny_thresh1=50, canny_thresh2=150, hough_thresh=50): + edges = cv2.Canny(gray, canny_thresh1, canny_thresh2) + lines = cv2.HoughLinesP(edges, rho=1, theta=np.pi / 180, threshold=hough_thresh, + minLineLength=min(gray.shape) // 10, maxLineGap=10) + if lines is None: return edges, [] + lines = [tuple(l[0]) for l in lines] + return edges, lines + +# ========================================== +# 2. SCORING FUNCTIONS +# ========================================== + +def saturation_score(img_bgr): + hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV).astype(np.float32) + sat = hsv[:, :, 1] / 255.0 + return float(np.mean(sat)) + +def balance_score(img_bgr, sal): + img_lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB).astype(np.float32) + L = img_lab[:,:,0] / 255.0 + img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV).astype(np.float32) + S = img_hsv[:,:,1] / 255.0 + weight = sal * (L + 0.5 * S) + M = weight.sum() + h,w = sal.shape + if M <= 1e-6: return 0.0 + ys, xs = np.indices(sal.shape) + cx = (xs * weight).sum() / M + cy = (ys * weight).sum() / M + dx = abs(cx - w/2) / (w/2) + dy = abs(cy - h/2) / (h/2) + dist = np.sqrt(dx*dx + dy*dy) / np.sqrt(2) + return float(max(0.0, 1.0 - dist*1.4)) + +def depth_score(img_gray, sal): + mask = sal > 0.3 + if mask.sum() == 0: return 0.0 + ys,xs = np.where(mask) + minx, maxx = xs.min(), xs.max() + miny, maxy = ys.min(), ys.max() + lap = cv2.Laplacian(img_gray, cv2.CV_64F) + inside = lap[miny:maxy+1, minx:maxx+1] + outside_mask = np.ones_like(img_gray, dtype=bool) + outside_mask[miny:maxy+1, minx:maxx+1] = False + outside = lap[outside_mask] + var_in = float(np.var(inside)) if inside.size>0 else 0.0 + var_out = float(np.var(outside)) if outside.size>0 else 0.0 + diff = var_in - var_out + score = (diff / (abs(var_out) + 1e-6)) if var_out>1e-6 else (1.0 if diff>0 else 0.0) + score = np.tanh(score) + return float(max(0.0, min(1.0, (score+1)/2))) + +def diagonals_triangles_score(lines, contours, centroid, img_shape): + if not lines: + diag_strength = 0.0 + else: + angles = [] + for (x1,y1,x2,y2) in lines: + dx = x2 - x1; dy = y2 - y1 + ang = abs(np.arctan2(dy, dx)) + ang = min(ang, np.pi - ang) + angles.append(1.0 - abs(ang - np.pi/4) / (np.pi/4)) + diag_strength = float(np.mean(angles)) if angles else 0.0 + h,w = img_shape[0], img_shape[1] + cx,cy = int(centroid[0]), int(centroid[1]) + tri_score = 0.0 + for c in contours: + area = cv2.contourArea(c) + if area < 0.01*w*h: continue + peri = cv2.arcLength(c, True) + approx = cv2.approxPolyDP(c, 0.04 * peri, True) + if len(approx) == 3: + inside = cv2.pointPolygonTest(c, (cx,cy), False) + dist_to_centroid = 0 if inside>=0 else min([np.linalg.norm(np.array(pt[0]) - np.array([cx,cy])) for pt in approx]) + tri_score = max(tri_score, max(0.0, 1.0 - dist_to_centroid / max(w,h))) + combined = 0.6*diag_strength + 0.4*tri_score + return float(max(0.0, min(1.0, combined))) + +def symmetry_score(img_gray): + h,w = img_gray.shape + left = img_gray[:, :w//2] + right = img_gray[:, w - w//2:] + right_flipped = cv2.flip(right, 1) + if left.shape != right_flipped.shape: + right_flipped = cv2.resize(right_flipped, (left.shape[1], left.shape[0])) + leftf = left.astype(np.float32); rightf = right_flipped.astype(np.float32) + if leftf.std() < 1e-3 or rightf.std() < 1e-3: return 0.0 + corr = np.corrcoef(leftf.flatten(), rightf.flatten())[0,1] + return float(max(0.0, min(1.0, (corr + 1)/2))) + +def fill_frame_score(sal, threshold=0.5): + mask = sal > threshold + if mask.sum() == 0: return 0.0 + ys, xs = np.where(mask) + h,w = sal.shape + box_area = (xs.max()-xs.min()+1)*(ys.max()-ys.min()+1) + frac = box_area / (w*h) + return float(min(1.0, frac*2.0)) + +def negative_space_score(sal): + low = (sal < 0.15).sum() / sal.size + return float(min(1.0, low)) + +def thirds_score(sal, centroid): + h,w = sal.shape + cx, cy = centroid + thirds_pts = [(w/3, h/3), (w/3, 2*h/3), (2*w/3, h/3), (2*w/3, 2*h/3)] + dists = [hypot(cx-x, cy-y) for (x,y) in thirds_pts] + maxd = hypot(w, h) + score = 1.0 - min(dists)/maxd*1.2 + vline_dist = min(abs(cx - w/3), abs(cx - 2*w/3)) / w + hline_dist = min(abs(cy - h/3), abs(cy - 2*h/3)) / h + line_score = 1.0 - min(vline_dist, hline_dist) + return float(max(0.0, min(1.0, 0.6*score + 0.4*line_score))) + +def golden_ratio_score(sal, centroid): + h,w = sal.shape + gx = [0.382, 0.618]; gy = [0.382, 0.618] + cx, cy = centroid + pts = [(g*w, g2*h) for g in gx for g2 in gy] + dists = [hypot(cx-x, cy-y) for x,y in pts] + maxd = hypot(w, h) + return float(max(0.0, min(1.0, 1.0 - min(dists)/ (maxd*0.9)))) + +def center_score(sal, centroid): + h,w = sal.shape + cx, cy = centroid + dx = abs(cx - w/2) / (w/2) + dy = abs(cy - h/2) / (h/2) + dist = np.sqrt(dx*dx + dy*dy) / np.sqrt(2) + return float(max(0.0, 1.0 - dist*1.2)) + + +# ========================================== +# 3. SINGLE IMAGE ANALYZER +# ========================================== + +def analyze_single_image(img_path): + if not os.path.exists(img_path): + return json.dumps({ + "error": f"File not found at {img_path}", + "success": False + }) + + # Load Image + img_full = cv2.imread(img_path) + if img_full is None: + return json.dumps({ + "error": "Could not load image. Check format.", + "success": False + }) + + try: + # 1. Preprocessing + img, scale = resize_for_fast_processing(img_full, max_side=640) + h, w = img.shape[:2] + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + + # 2. Saliency Map + sal = compute_saliency_gray(img) + if sal.shape != gray.shape: + sal = cv2.resize(sal, (w, h), interpolation=cv2.INTER_LINEAR) + centroid = saliency_centroid(sal) + + # 3. Geometric Features (Lines & Contours) + edges, lines = detect_lines(gray) + _, thr = cv2.threshold((gray).astype(np.uint8), 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV) + contours, _ = cv2.findContours(thr, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + # 4. Calculate Scores + scores = { + 'Rule of Thirds': thirds_score(sal, centroid), + 'Golden Ratio': golden_ratio_score(sal, centroid), + 'Symmetry': symmetry_score(gray), + 'Fill Frame': fill_frame_score(sal, threshold=0.45), + 'Negative Space': negative_space_score(sal), + 'Center Composition': center_score(sal, centroid), + 'Visual Balance': balance_score(img, sal), + 'Depth': depth_score(gray, sal), + 'Saturation': saturation_score(img), + 'Diagonals & Triangles': diagonals_triangles_score(lines, contours, centroid, img.shape) + } + + # 5. Sort and get top 5 + sorted_features = sorted(scores.items(), key=lambda x: x[1], reverse=True) + top5 = [{"name": k, "score": v} for k, v in sorted_features[:5]] + + # 6. Return JSON result + result = { + "success": True, + "scores": {k: float(v) for k, v in scores.items()}, + "top5": top5, + "image_name": os.path.basename(img_path) + } + + return json.dumps(result) + + except Exception as e: + return json.dumps({ + "error": str(e), + "success": False + }) + + diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts @@ -13,6 +13,7 @@ pluginManagement { google() mavenCentral() gradlePluginPortal() + maven { url = uri("https://chaquo.com/maven") } } } @@ -20,6 +21,7 @@ plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.7.0" apply false id("org.jetbrains.kotlin.android") version "1.8.22" apply false + id("com.chaquo.python") version "15.0.1" apply false } include(":app") diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/lib/services/image_analyzer_service.dart b/lib/services/image_analyzer_service.dart @@ -0,0 +1,29 @@ +import 'package:flutter/services.dart'; +import 'dart:convert'; + +class ImageAnalyzerService { + static const MethodChannel _channel = MethodChannel( + 'com.example.adobe/image_analyzer', + ); + + static Future<Map<String, dynamic>?> analyzeImage(String imagePath) async { + try { + final String? result = await _channel.invokeMethod('analyzeImage', { + 'imagePath': imagePath, + }); + + if (result != null) { + final Map<String, dynamic> jsonResult = json.decode(result); + return jsonResult; + } + return null; + } on PlatformException catch (e) { + print("Error analyzing image: ${e.message}"); + return {'success': false, 'error': e.message ?? 'Unknown error occurred'}; + } catch (e) { + print("Unexpected error: $e"); + return {'success': false, 'error': e.toString()}; + } + } +} + diff --git a/lib/ui/pages/home_page.dart b/lib/ui/pages/home_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:adobe/data/repos/board_repo.dart'; import 'package:adobe/data/models/board_model.dart'; import 'package:adobe/ui/pages/board_detail_page.dart'; +import 'package:adobe/ui/pages/image_analysis_page.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -36,6 +37,18 @@ class _HomePageState extends State<HomePage> { appBar: AppBar( title: const Text("My Boards"), centerTitle: true, + actions: [ + IconButton( + icon: const Icon(Icons.analytics), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const ImageAnalysisPage()), + ); + }, + tooltip: 'Image Analysis', + ), + ], ), floatingActionButton: FloatingActionButton( onPressed: _showCreateBoardDialog, @@ -61,7 +74,7 @@ class _HomePageState extends State<HomePage> { } final boards = snapshot.data!; - + return GridView.builder( padding: const EdgeInsets.all(12), itemCount: boards.length, @@ -88,9 +101,7 @@ class _HomePageState extends State<HomePage> { // Navigate to the details of this board Navigator.push( context, - MaterialPageRoute( - builder: (_) => BoardDetailPage(board: board), - ), + MaterialPageRoute(builder: (_) => BoardDetailPage(board: board)), ).then((_) => _refreshBoards()); // Refresh when coming back }, child: Container( @@ -102,7 +113,7 @@ class _HomePageState extends State<HomePage> { color: Colors.black.withValues(alpha: 0.05), blurRadius: 5, offset: const Offset(0, 4), - ) + ), ], ), child: Column( @@ -112,10 +123,7 @@ class _HomePageState extends State<HomePage> { const SizedBox(height: 8), Text( board.name, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), const SizedBox(height: 4), @@ -133,27 +141,31 @@ class _HomePageState extends State<HomePage> { final controller = TextEditingController(); showDialog( context: context, - builder: (c) => AlertDialog( - title: const Text("New Board"), - content: TextField( - controller: controller, - decoration: const InputDecoration(hintText: "Board Name"), - autofocus: true, - ), - actions: [ - TextButton(onPressed: () => Navigator.pop(c), child: const Text("Cancel")), - ElevatedButton( - onPressed: () async { - if (controller.text.isNotEmpty) { - await _boardRepo.createBoard(controller.text); - if (c.mounted) Navigator.pop(c); - _refreshBoards(); - } - }, - child: const Text("Create"), - ) - ], - ), + builder: + (c) => AlertDialog( + title: const Text("New Board"), + content: TextField( + controller: controller, + decoration: const InputDecoration(hintText: "Board Name"), + autofocus: true, + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(c), + child: const Text("Cancel"), + ), + ElevatedButton( + onPressed: () async { + if (controller.text.isNotEmpty) { + await _boardRepo.createBoard(controller.text); + if (c.mounted) Navigator.pop(c); + _refreshBoards(); + } + }, + child: const Text("Create"), + ), + ], + ), ); } -} -\ No newline at end of file +} diff --git a/lib/ui/pages/image_analysis_page.dart b/lib/ui/pages/image_analysis_page.dart @@ -0,0 +1,346 @@ +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/image_analyzer_service.dart'; + +class ImageAnalysisPage extends StatefulWidget { + const ImageAnalysisPage({super.key}); + + @override + State<ImageAnalysisPage> createState() => _ImageAnalysisPageState(); +} + +class _ImageAnalysisPageState extends State<ImageAnalysisPage> { + File? _selectedImage; + Map<String, dynamic>? _analysisResult; + bool _isAnalyzing = false; + String? _errorMessage; + + Future<void> _pickImage() async { + try { + final ImagePicker picker = ImagePicker(); + final XFile? image = await picker.pickImage(source: ImageSource.gallery); + + if (image != null) { + setState(() { + _selectedImage = File(image.path); + _analysisResult = null; + _errorMessage = null; + }); + } + } catch (e) { + setState(() { + _errorMessage = 'Error picking image: $e'; + }); + } + } + + Future<void> _takePhoto() async { + try { + final ImagePicker picker = ImagePicker(); + final XFile? image = await picker.pickImage(source: ImageSource.camera); + + if (image != null) { + setState(() { + _selectedImage = File(image.path); + _analysisResult = null; + _errorMessage = null; + }); + } + } catch (e) { + setState(() { + _errorMessage = 'Error taking photo: $e'; + }); + } + } + + Future<void> _analyzeImage() async { + if (_selectedImage == null) return; + + setState(() { + _isAnalyzing = true; + _errorMessage = null; + _analysisResult = null; + }); + + try { + // Copy image to a location accessible by the Python script + final appDir = await getApplicationDocumentsDirectory(); + final fileName = _selectedImage!.path.split('/').last; + final targetPath = '${appDir.path}/$fileName'; + + await _selectedImage!.copy(targetPath); + + final result = await ImageAnalyzerService.analyzeImage(targetPath); + + setState(() { + _isAnalyzing = false; + if (result != null && result['success'] == true) { + _analysisResult = result; + } else { + _errorMessage = result?['error'] ?? 'Failed to analyze image'; + } + }); + } catch (e) { + setState(() { + _isAnalyzing = false; + _errorMessage = 'Error analyzing image: $e'; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Image Composition Analyzer'), + centerTitle: true, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Image Selection Section + if (_selectedImage == null) + Container( + height: 200, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey[300]!), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.image, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + 'No image selected', + style: TextStyle(color: Colors.grey[600]), + ), + ], + ), + ), + ) + else + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.file( + _selectedImage!, + height: 300, + fit: BoxFit.cover, + ), + ), + + const SizedBox(height: 16), + + // Action Buttons + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: _pickImage, + icon: const Icon(Icons.photo_library), + label: const Text('Gallery'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: OutlinedButton.icon( + onPressed: _takePhoto, + icon: const Icon(Icons.camera_alt), + label: const Text('Camera'), + ), + ), + ], + ), + + const SizedBox(height: 16), + + // Analyze Button + ElevatedButton( + onPressed: + _selectedImage != null && !_isAnalyzing + ? _analyzeImage + : null, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: + _isAnalyzing + ? const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + SizedBox(width: 12), + Text('Analyzing...'), + ], + ) + : const Text('Analyze Image'), + ), + + // Error Message + if (_errorMessage != null) + Padding( + padding: const EdgeInsets.only(top: 16), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red[50], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.red[200]!), + ), + child: Row( + children: [ + Icon(Icons.error_outline, color: Colors.red[700]), + const SizedBox(width: 8), + Expanded( + child: Text( + _errorMessage!, + style: TextStyle(color: Colors.red[700]), + ), + ), + ], + ), + ), + ), + + // Analysis Results + if (_analysisResult != null) ...[ + const SizedBox(height: 24), + const Divider(), + const SizedBox(height: 16), + Text( + 'Analysis Results', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + + // Top 5 Features + if (_analysisResult!['top5'] != null) ...[ + Text( + '🏆 Top 5 Features', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + ...(_analysisResult!['top5'] as List).map((feature) { + return _buildFeatureCard( + feature['name'] as String, + feature['score'] as double, + ); + }), + const SizedBox(height: 24), + ], + + // All Scores + Text( + 'All Scores', + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + if (_analysisResult!['scores'] != null) + ...(_analysisResult!['scores'] as Map<String, dynamic>).entries + .map((entry) { + return _buildScoreCard(entry.key, entry.value as double); + }), + ], + ], + ), + ), + ); + } + + Widget _buildFeatureCard(String name, double score) { + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Expanded( + child: Text( + name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + Text( + (score * 100).toStringAsFixed(1) + '%', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: _getScoreColor(score), + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 100, + child: LinearProgressIndicator( + value: score, + backgroundColor: Colors.grey[200], + valueColor: AlwaysStoppedAnimation<Color>( + _getScoreColor(score), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildScoreCard(String name, double score) { + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Expanded(child: Text(name, style: const TextStyle(fontSize: 14))), + Text( + score.toStringAsFixed(3), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: _getScoreColor(score), + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 80, + child: LinearProgressIndicator( + value: score, + backgroundColor: Colors.grey[200], + valueColor: AlwaysStoppedAnimation<Color>( + _getScoreColor(score), + ), + ), + ), + ], + ), + ), + ); + } + + Color _getScoreColor(double score) { + if (score >= 0.7) return Colors.green; + if (score >= 0.4) return Colors.orange; + return Colors.red; + } +} + diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Podfile b/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/pubspec.lock b/pubspec.lock @@ -77,10 +77,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -252,26 +252,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -489,10 +489,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" typed_data: dependency: transitive description: @@ -513,10 +513,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -542,5 +542,5 @@ packages: source: hosted version: "1.1.0" sdks: - dart: ">=3.7.2 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.29.0" diff --git a/setup_python_env.sh b/setup_python_env.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# Setup script for Python virtual environment +# This ensures all developers use the same Python version for builds + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VENV_DIR="$SCRIPT_DIR/python_env" + +echo "Setting up Python virtual environment for Chaquopy builds..." + +# Check if Python 3.11 is available +if command -v python3.11 &> /dev/null; then + PYTHON_CMD="python3.11" +elif command -v python3 &> /dev/null; then + PYTHON_VERSION=$(python3 --version | cut -d' ' -f2 | cut -d'.' -f1,2) + if [ "$PYTHON_VERSION" = "3.11" ]; then + PYTHON_CMD="python3" + else + echo "Error: Python 3.11 is required but not found." + echo "Please install Python 3.11:" + echo " macOS: brew install python@3.11" + echo " Linux: sudo apt-get install python3.11 python3.11-venv" + echo " Or download from https://www.python.org/downloads/" + exit 1 + fi +else + echo "Error: Python 3.11 is required but not found." + echo "Please install Python 3.11 first." + exit 1 +fi + +echo "Using Python: $($PYTHON_CMD --version)" + +# Create virtual environment if it doesn't exist +if [ ! -d "$VENV_DIR" ]; then + echo "Creating virtual environment at $VENV_DIR..." + $PYTHON_CMD -m venv "$VENV_DIR" + echo "Virtual environment created successfully!" +else + echo "Virtual environment already exists at $VENV_DIR" +fi + +# Verify the venv +if [ -f "$VENV_DIR/bin/python" ]; then + VENV_PYTHON_VERSION=$("$VENV_DIR/bin/python" --version) + echo "Virtual environment Python version: $VENV_PYTHON_VERSION" + echo "" + echo "✓ Python virtual environment is ready!" + echo " Location: $VENV_DIR" + echo "" + echo "The build system will automatically use this Python environment." +else + echo "Error: Failed to create virtual environment" + exit 1 +fi +