creek

The AI Image Editor of 2030
commit 2e76d6eb43027c56c7edc1635d5e45f4df28057f
parent cb01c3a0fd3d9f4c1889c302d9b5f915db9dda09
Author: ajcoder13 <avnijhalani@gmail.com>
Date:   Wed,  3 Dec 2025 00:22:44 +0530

Added AI models switching functionality in canvas magic draw

Diffstat:
Mlib/services/flask_service.dart | 52+++++++++++++++++++++++++++++++++++++++-------------
Mlib/ui/pages/canvas_board_page.dart | 68+++++++++++++++++++++++++++++++++++++-------------------------------
Mlib/ui/pages/canvas_toolbar/magic_draw_overlay.dart | 497+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
3 files changed, 384 insertions(+), 233 deletions(-)

diff --git a/lib/services/flask_service.dart b/lib/services/flask_service.dart @@ -61,7 +61,7 @@ class FlaskService { "$stylePrompt. $userPrompt. The image features: $sketchDescription"; debugPrint("🔗 [Pipeline] Generating base image..."); - + final String? generatedImagePath = await generateAndSaveImage(globalPrompt); if (generatedImagePath == null) { @@ -71,7 +71,7 @@ class FlaskService { // 3. Remove Background (Pipeline Extension) debugPrint("🔗 [Pipeline] Removing background from generated result..."); - + // This returns the path to the no-background version return generateAsset(imagePath: generatedImagePath); } @@ -114,34 +114,58 @@ class FlaskService { ); } + /// [Inpainting] + Future<String?> inpaintApiImage({ + required String imagePath, + required String maskPath, + required String prompt, + }) async { + final String? base64Image = await _encodeFile(imagePath); + final String? base64Mask = await _encodeFile(maskPath); + + if (base64Image == null || base64Mask == null) return null; + + return _performImageOperation( + endpoint: '/inpainting-api', + logPrefix: '🖌️ Inpainting', + body: { + 'prompt': prompt, + 'negative_prompt': 'blurry, bad quality, low res, ugly', + 'image': base64Image, + 'mask_image': base64Mask, + }, + filenamePrefix: 'inpaint_$prompt', + ); + } + /// [Background Removal] Future<String?> generateAsset({required String imagePath}) async { // 1. Prepare and Upload final String? base64Image = await _encodeFile(imagePath); if (base64Image == null) return null; - + final String? generatedAssetPath = await _performImageOperation( fullUrl: _urlAsset, logPrefix: '✂️ Asset Gen', body: {'image': base64Image}, filenamePrefix: 'asset', ); - + // 2. Resolve Project ID and Save if (generatedAssetPath != null) { int? projectId; - + // --- CHECK 1: Is this a Main Image? --- final imageModel = await _imageRepo.getByFilePath(imagePath); if (imageModel != null) { projectId = imageModel.projectId; } - + // --- CHECK 2: Is this a Note Crop? (NEW) --- if (projectId == null) { // You need a method in NoteRepo to find a note by its crop path - final noteModel = await _noteRepo.getByCropPath(imagePath); - + final noteModel = await _noteRepo.getByCropPath(imagePath); + if (noteModel != null) { // Traverse up: Note -> Parent Image -> Project final parentImage = await _imageRepo.getById(noteModel.imageId); @@ -151,7 +175,7 @@ class FlaskService { } } } - + // --- CHECK 3: Is this a generic File? --- if (projectId == null) { final fileModel = await _fileRepo.getByFilePath(imagePath); @@ -159,12 +183,12 @@ class FlaskService { projectId = fileModel.projectId; } } - + // 3. Update the Project if (projectId != null) { final project = await _projectRepo.getProjectById(projectId); if (project != null) { - project.assetsPath.add(generatedAssetPath); + project.assetsPath.add(generatedAssetPath); await _projectRepo.updateAssets(projectId, project.assetsPath); debugPrint("✅ Asset path saved to Project DB: $generatedAssetPath"); } @@ -172,7 +196,7 @@ class FlaskService { debugPrint("⚠️ Asset generated but could not link to a Project ID."); } } - + return generatedAssetPath; } @@ -226,7 +250,9 @@ class FlaskService { return _saveImageFromResponse(response, filenamePrefix); } - debugPrint("❌ $logPrefix Failed: ${response?.statusCode ?? 'No Connection'}"); + debugPrint( + "❌ $logPrefix Failed: ${response?.statusCode ?? 'No Connection'}", + ); return null; } diff --git a/lib/ui/pages/canvas_board_page.dart b/lib/ui/pages/canvas_board_page.dart @@ -1244,7 +1244,9 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { onColorChanged: (c) => setState(() => _selectedColor = c), onWidthChanged: (w) => setState(() => _strokeWidth = w), onEraserToggle: (e) => setState(() => _isEraser = e), - onPromptSubmit: (prompt) => _processInpainting(prompt), + onPromptSubmit: + (prompt, serviceId) => + _processInpainting(prompt, serviceId), isProcessing: _isInpainting, onMagicPanelActivityToggle: (disabled) => @@ -1438,7 +1440,7 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { }); } - Future<void> _processInpainting(String prompt) async { + Future<void> _processInpainting(String prompt, String serviceId) async { if (prompt.isEmpty) { ScaffoldMessenger.of( context, @@ -1450,44 +1452,48 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { _resetInactivityTimer(); try { - // 1. Check for Image Layers bool hasImageLayers = elements.any((e) => e['type'] == 'file_image'); - if (hasImageLayers) { - // --- INPAINTING FLOW (Existing) --- - if (_tempBaseImage == null) { - _tempBaseImage = await _captureCanvasToFile(); - } - - if (_tempBaseImage == null) return; + if (_tempBaseImage == null) { + _tempBaseImage = await _captureCanvasToFile(); + } + if (_tempBaseImage == null) return; - File? maskFile = await _generateMaskImageFromPaths( - _magicPaths, - _canvasSize, - _tempBaseImage, - ); + File? maskFile = await _generateMaskImageFromPaths( + _magicPaths, + _canvasSize, + _tempBaseImage, + ); + if (maskFile == null) throw Exception("Failed to generate mask"); - if (maskFile == null) throw Exception("Failed to generate mask"); + String? newImageUrl; - final String? newImageUrl = await FlaskService().inpaintImage( + // --- SWITCH SERVICE --- + if (serviceId == 'api') { + // Call the dedicated API endpoint + newImageUrl = await FlaskService().inpaintApiImage( imagePath: _tempBaseImage!.path, maskPath: maskFile.path, prompt: prompt, ); - - _addGeneratedImage(newImageUrl); } else { - // --- SKETCH-TO-IMAGE FLOW (New) --- - // Capture the entire canvas (strokes only since no images exist) - File? sketchFile = await _captureCanvasToFile(); - if (sketchFile == null) throw Exception("Failed to capture sketch"); - - final String? newImageUrl = await FlaskService().sketchToImage( - sketchPath: sketchFile.path, - userPrompt: prompt, - stylePrompt: "high quality, realistic", // Default style - ); + // Default 'flask' service logic + if (hasImageLayers) { + newImageUrl = await FlaskService().inpaintImage( + imagePath: _tempBaseImage!.path, + maskPath: maskFile.path, + prompt: prompt, + ); + } else { + newImageUrl = await FlaskService().sketchToImage( + sketchPath: _tempBaseImage!.path, + userPrompt: prompt, + stylePrompt: "high quality, realistic", + ); + } + } + if (newImageUrl != null) { _addGeneratedImage(newImageUrl); } } catch (e) { @@ -1500,7 +1506,7 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { _isInpainting = false; _tempBaseImage = null; _magicPaths.clear(); - _magicDrawChangeStack.clear(); // <--- ADD THIS + _magicDrawChangeStack.clear(); }); } } @@ -2134,7 +2140,7 @@ class _ManipulatingBoxState extends State<_ManipulatingBox> { } Widget _cornerHandle({ - required double size, + required double size, required Function(DragUpdateDetails) onDrag, }) { return GestureDetector( diff --git a/lib/ui/pages/canvas_toolbar/magic_draw_overlay.dart b/lib/ui/pages/canvas_toolbar/magic_draw_overlay.dart @@ -1,8 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; -// Note: You must ensure flutter_svg is imported in this file and installed in pubspec.yaml import 'package:flutter_svg/flutter_svg.dart'; +// --- HELPER CLASS FOR DROPDOWN --- +class AIModelOption { + final String id; // 'flask' or 'api' + final String name; // Display Name + final String? badge; // Optional badge + + AIModelOption({required this.id, required this.name, this.badge}); +} + class MagicDrawTools extends StatefulWidget { final bool isActive; final Color selectedColor; @@ -12,7 +20,10 @@ class MagicDrawTools extends StatefulWidget { final Function(double) onWidthChanged; final Function(bool) onEraserToggle; final VoidCallback onClose; - final Function(String) onPromptSubmit; + + // CHANGED: Accepts Prompt AND Model ID + final Function(String prompt, String modelId) onPromptSubmit; + final bool isProcessing; final List<Color> brandColors; final Function(bool) onViewModeToggle; @@ -55,6 +66,22 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { bool _isViewMode = false; + // --- DROPDOWN STATE --- + bool _showModelMenu = false; + late AIModelOption _selectedModel; + + // --- DEFINE YOUR SERVICES HERE --- + final List<AIModelOption> _aiModels = [ + AIModelOption(id: 'flask', name: 'Inpainting', badge: null), + AIModelOption(id: 'api', name: 'Inpainting API', badge: null), + ]; + + @override + void initState() { + super.initState(); + _selectedModel = _aiModels.first; // Default to first option + } + @override void dispose() { if (_isViewMode) { @@ -68,7 +95,8 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { if (_promptController.text.trim().isNotEmpty && !widget.isProcessing && !widget.isMagicPanelDisabled) { - widget.onPromptSubmit(_promptController.text.trim()); + // PASS SELECTED MODEL ID + widget.onPromptSubmit(_promptController.text.trim(), _selectedModel.id); } } @@ -76,10 +104,36 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { final newViewMode = !_isViewMode; setState(() { _isViewMode = newViewMode; + _showStrokeSlider = false; + _showModelMenu = false; // Close menu if view mode toggled }); widget.onViewModeToggle(newViewMode); } + // NEW: Toggle Dropdown Visibility + void _toggleModelMenu() { + setState(() { + if (!_showModelMenu) + _showStrokeSlider = false; // Close slider if menu opens + _showModelMenu = !_showModelMenu; + }); + } + + void _handleToolTap(VoidCallback toolAction) { + setState(() { + _showStrokeSlider = false; + _showModelMenu = false; // Close menu on tool tap + if (_isViewMode) { + _isViewMode = false; + widget.onViewModeToggle(false); + } + if (widget.isMagicPanelDisabled) { + widget.onMagicPanelActivityToggle(false); + } + }); + toolAction(); + } + @override Widget build(BuildContext context) { if (!widget.isActive) return const SizedBox.shrink(); @@ -93,7 +147,30 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { children: [ if (_showStrokeSlider) _buildTaperedStrokeSlider(), const SizedBox(height: 8), - _buildMagicDrawPanel(), + + // WRAPPER CONTAINER FOR MENU + PANEL + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 12, + offset: Offset(0, 4), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // RENDER DROPDOWN ON TOP + if (_showModelMenu) _buildModelDropdown(), + + _buildMagicDrawPanel(), + ], + ), + ), ], ), ); @@ -127,228 +204,270 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { ); } - Widget _buildMagicDrawPanel() { + // --- NEW: DROPDOWN MENU UI --- + Widget _buildModelDropdown() { return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: const [ - BoxShadow( - color: Colors.black12, - blurRadius: 12, - offset: Offset(0, 4), + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Local', + style: TextStyle(color: Colors.grey, fontSize: 12), ), + const SizedBox(height: 4), + + ..._aiModels.map((model) { + final isSelected = model.id == _selectedModel.id; + return GestureDetector( + onTap: () { + setState(() { + _selectedModel = model; + _showModelMenu = false; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + Icon( + Icons.star_half, + color: isSelected ? const Color(0xFFD8705D) : Colors.grey, + size: 18, + ), + const SizedBox(width: 8), + Text( + model.name, + style: TextStyle( + fontWeight: + isSelected ? FontWeight.bold : FontWeight.normal, + color: Colors.black87, + ), + ), + const Spacer(), + if (model.badge != null) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + model.badge!, + style: TextStyle( + color: Colors.blue.shade900, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ); + }).toList(), + Divider(height: 1, color: Colors.grey.shade100), ], ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Container( + ); + } + + Widget _buildMagicDrawPanel() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + // --- TRIGGER ICON FOR DROPDOWN --- + GestureDetector( + onTap: _toggleModelMenu, + child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Colors.white, + color: _showModelMenu ? Colors.grey.shade100 : Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8), ), child: const Icon( - Icons.emergency_recording, + Icons.star_half, // Using star icon as requested color: Color(0xFFD8705D), size: 20, ), ), - const SizedBox(width: 12), - Expanded( - child: TextField( - controller: _promptController, - onSubmitted: (_) => _handleSubmit(), - decoration: const InputDecoration.collapsed( - hintText: "tap imagination...", - hintStyle: TextStyle(fontSize: 14, color: Colors.black54), - ), + ), + + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: _promptController, + onSubmitted: (_) => _handleSubmit(), + decoration: const InputDecoration.collapsed( + hintText: "tap imagination...", + hintStyle: TextStyle(fontSize: 14, color: Colors.black54), ), ), - const Icon(Icons.mic_none, color: Colors.grey), - const SizedBox(width: 8), - GestureDetector( - onTap: _handleSubmit, - child: Container( - width: 44, - height: 44, - decoration: const BoxDecoration( - color: Color(0xFF2B2B2B), - shape: BoxShape.circle, - ), - child: - widget.isProcessing - ? const Padding( - padding: EdgeInsets.all(12.0), - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, - ), - ) - : const Icon( - Icons.auto_awesome, + ), + const Icon(Icons.mic_none, color: Colors.grey), + const SizedBox(width: 8), + GestureDetector( + onTap: _handleSubmit, + child: Container( + width: 44, + height: 44, + decoration: const BoxDecoration( + color: Color(0xFF2B2B2B), + shape: BoxShape.circle, + ), + child: + widget.isProcessing + ? const Padding( + padding: EdgeInsets.all(12.0), + child: CircularProgressIndicator( color: Colors.white, - size: 20, + strokeWidth: 2, ), - ), + ) + : const Icon( + Icons.auto_awesome, + color: Colors.white, + size: 20, + ), ), - ], - ), + ), + ], ), - Divider(height: 1, color: Colors.grey.shade200), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildToolIcon(Icons.pan_tool, widget.isMagicPanelDisabled, () { - setState(() => _showStrokeSlider = false); - - // Deactivate drawing tools when hand icon is activated - - if (!widget.isMagicPanelDisabled) { - widget.onEraserToggle(false); - } - - widget.onMagicPanelActivityToggle( + ), + Divider(height: 1, color: Colors.grey.shade200), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildToolIcon(Icons.pan_tool, widget.isMagicPanelDisabled, () { + setState(() => _showStrokeSlider = false); + _showModelMenu = false; // Close menu + + if (!widget.isMagicPanelDisabled) { + widget.onEraserToggle(false); + } + + widget.onMagicPanelActivityToggle(!widget.isMagicPanelDisabled); + }), + _buildToolIcon( + 'assets/icons/brush-line.svg', + !widget.isEraser && + !_isViewMode && !widget.isMagicPanelDisabled, - ); - }), - _buildToolIcon( - 'assets/icons/brush-line.svg', - - !widget.isEraser && - !_isViewMode && - !widget.isMagicPanelDisabled, - - () { - setState(() => _showStrokeSlider = false); - - if (_isViewMode) { - setState(() => _isViewMode = false); - - widget.onViewModeToggle(false); - } - - // Deactivate hand icon when brush is activated - - if (widget.isMagicPanelDisabled) { - widget.onMagicPanelActivityToggle(false); - } - - widget.onEraserToggle(false); - }, - ), - - GestureDetector( - onTap: () { - setState(() => _showStrokeSlider = false); - - if (_isViewMode) { - setState(() => _isViewMode = false); - - widget.onViewModeToggle(false); - } - - _showAdvancedColorPicker(context); - }, - - child: Container( - width: 28, - - height: 28, - - decoration: BoxDecoration( - color: widget.selectedColor, + () { + setState(() { + _showStrokeSlider = false; + _showModelMenu = false; // Close menu + }); + + if (_isViewMode) { + setState(() => _isViewMode = false); + widget.onViewModeToggle(false); + } + if (widget.isMagicPanelDisabled) { + widget.onMagicPanelActivityToggle(false); + } - shape: BoxShape.circle, + widget.onEraserToggle(false); + }, + ), - border: Border.all(color: Colors.white, width: 2), + GestureDetector( + onTap: () { + setState(() { + _showStrokeSlider = false; + _showModelMenu = false; + }); - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), + if (_isViewMode) { + setState(() => _isViewMode = false); + widget.onViewModeToggle(false); + } - blurRadius: 4, - ), - ], - ), + _showAdvancedColorPicker(context); + }, + child: Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: widget.selectedColor, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + ), + ], ), ), + ), - GestureDetector( - onTap: () { - if (_isViewMode) { - setState(() => _isViewMode = false); - - widget.onViewModeToggle(false); - } - - setState(() => _showStrokeSlider = !_showStrokeSlider); - }, + GestureDetector( + onTap: () { + if (_isViewMode) { + setState(() => _isViewMode = false); + widget.onViewModeToggle(false); + } + setState(() { + _showStrokeSlider = !_showStrokeSlider; + _showModelMenu = false; // Close menu + }); + }, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: + _showStrokeSlider + ? Colors.grey.shade200 + : Colors.transparent, + shape: BoxShape.circle, + ), child: Container( - padding: const EdgeInsets.all(8), - - decoration: BoxDecoration( - color: - _showStrokeSlider - ? Colors.grey.shade200 - : Colors.transparent, - + width: 10, + height: 10, + decoration: const BoxDecoration( + color: Colors.black87, shape: BoxShape.circle, ), - - child: Container( - width: 10, - - height: 10, - - decoration: const BoxDecoration( - color: Colors.black87, - - shape: BoxShape.circle, - ), - ), ), ), + ), - _buildToolIcon( - 'assets/icons/eraser-line.svg', - - widget.isEraser && - !_isViewMode && - !widget.isMagicPanelDisabled, - - () { - setState(() => _showStrokeSlider = false); - - if (_isViewMode) { - setState(() => _isViewMode = false); - - widget.onViewModeToggle(false); - } - - // Deactivate hand icon when eraser is activated - - if (widget.isMagicPanelDisabled) { - widget.onMagicPanelActivityToggle(false); - } + _buildToolIcon( + 'assets/icons/eraser-line.svg', + widget.isEraser && !_isViewMode && !widget.isMagicPanelDisabled, + () { + setState(() { + _showStrokeSlider = false; + _showModelMenu = false; + }); + + if (_isViewMode) { + setState(() => _isViewMode = false); + widget.onViewModeToggle(false); + } + if (widget.isMagicPanelDisabled) { + widget.onMagicPanelActivityToggle(false); + } - widget.onEraserToggle(true); - }, - ), - ], - ), + widget.onEraserToggle(true); + }, + ), + ], ), - ], - ), + ), + ], ); }