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:
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);
+ },
+ ),
+ ],
),
- ],
- ),
+ ),
+ ],
);
}