creek

The AI Image Editor of 2030
commit d43ee485c9706647e24f33e05bf579217e08d3cc
parent 1302ec383f3f2b0dd5005d9dc4ad6c9dc0bb1b4c
Author: aditya-samal <samaladitya2004@gmail.com>
Date:   Tue, 25 Nov 2025 10:36:05 +0530

Python integration completed

Diffstat:

Mandroid/app/build.gradle.kts | 7++++---
Mandroid/app/src/main/kotlin/com/example/adobe/ImageAnalyzer.kt | 12++++++++++++
Mandroid/app/src/main/kotlin/com/example/adobe/MainActivity.kt | 27+++++++++++++++++++++++++++
Aandroid/app/src/main/python/instagram_downloader.py | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mandroid/build.gradle.kts | 7+++++++
Mlib/data/database.dart | 21+++++++++++++++++++--
Mlib/data/models/image_model.dart | 25+++++++++++++++++++++++--
Mlib/data/repos/image_repo.dart | 19+++++++++++++++++--
Mlib/main.dart | 28++++++++++++++++------------
Alib/services/instagram_download_service.dart | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/ui/pages/board_detail_page.dart | 268++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mlib/ui/pages/share_handler_page.dart | 237++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
12 files changed, 713 insertions(+), 86 deletions(-)

diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts @@ -24,12 +24,12 @@ android { ndkVersion = "27.0.12077973" compileOptions { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_21.toString() + jvmTarget = JavaVersion.VERSION_17.toString() } defaultConfig { @@ -65,6 +65,7 @@ chaquopy { install("pillow") install("numpy") install("opencv-python-headless") + install("instaloader") } } } diff --git a/android/app/src/main/kotlin/com/example/adobe/ImageAnalyzer.kt b/android/app/src/main/kotlin/com/example/adobe/ImageAnalyzer.kt @@ -26,6 +26,18 @@ object ImageAnalyzer { null } } + + fun downloadInstagramImage(url: String, outputDir: String): String? { + return try { + val py = Python.getInstance() + val module = py.getModule("instagram_downloader") + val result = module.callAttr("download_instagram_image", url, outputDir) + 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 @@ -8,6 +8,7 @@ import org.json.JSONObject class MainActivity : FlutterActivity() { private val CHANNEL = "com.example.adobe/image_analyzer" + private val INSTAGRAM_CHANNEL = "com.example.adobe/instagram_downloader" override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) @@ -39,5 +40,31 @@ class MainActivity : FlutterActivity() { } } } + + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, INSTAGRAM_CHANNEL).setMethodCallHandler { call, result -> + when (call.method) { + "downloadInstagramImage" -> { + val url = call.argument<String>("url") + val outputDir = call.argument<String>("outputDir") + if (url != null && outputDir != null) { + try { + val downloadResult = ImageAnalyzer.downloadInstagramImage(url, outputDir) + if (downloadResult != null) { + result.success(downloadResult) + } else { + result.error("DOWNLOAD_ERROR", "Failed to download Instagram image", null) + } + } catch (e: Exception) { + result.error("DOWNLOAD_ERROR", e.message, null) + } + } else { + result.error("INVALID_ARGUMENT", "URL or output directory is null", null) + } + } + else -> { + result.notImplemented() + } + } + } } } diff --git a/android/app/src/main/python/instagram_downloader.py b/android/app/src/main/python/instagram_downloader.py @@ -0,0 +1,83 @@ +import instaloader +import re +import os +import json +from pathlib import Path + +def download_instagram_image(url, output_dir): + """ + Downloads an image from an Instagram URL using Instaloader. + + Args: + url (str): The full Instagram URL (e.g., https://www.instagram.com/p/ShortCode/) + output_dir (str): Directory where the image should be saved + + Returns: + dict: Result with success status and file path or error message + """ + try: + # Initialize Instaloader + L = instaloader.Instaloader( + download_pictures=True, + download_videos=False, + download_video_thumbnails=False, + download_geotags=False, + download_comments=False, + save_metadata=False, + compress_json=False + ) + + # Extract shortcode from URL + # Matches /p/ShortCode, /reel/ShortCode, etc. + shortcode_match = re.search(r'instagram\.com/(?:p|reel|tv)/([^/?#&]+)', url) + + if not shortcode_match: + return json.dumps({ + "success": False, + "error": "Could not extract shortcode from URL. Please ensure the URL is in the format: https://www.instagram.com/p/ShortCode/" + }) + + shortcode = shortcode_match.group(1) + + # Load the post using the shortcode + post = instaloader.Post.from_shortcode(L.context, shortcode) + + # Create output directory if it doesn't exist + os.makedirs(output_dir, exist_ok=True) + + # Download the post + L.download_post(post, target=output_dir) + + # Find the downloaded image file + # Instaloader saves files with pattern: {shortcode}_*.jpg or {shortcode}_*.png + output_path = Path(output_dir) + image_files = list(output_path.glob(f"{shortcode}_*.jpg")) + list(output_path.glob(f"{shortcode}_*.png")) + + if not image_files: + # Try alternative pattern + image_files = list(output_path.glob(f"{shortcode}.*")) + + if image_files: + image_path = str(image_files[0]) + return json.dumps({ + "success": True, + "file_path": image_path, + "shortcode": shortcode + }) + else: + return json.dumps({ + "success": False, + "error": "Image downloaded but file not found" + }) + + except instaloader.exceptions.InstaloaderException as e: + return json.dumps({ + "success": False, + "error": f"Instaloader error: {str(e)}. Note: This works best for PUBLIC posts. Private posts require login." + }) + except Exception as e: + return json.dumps({ + "success": False, + "error": f"Unexpected error: {str(e)}" + }) + diff --git a/android/build.gradle.kts b/android/build.gradle.kts @@ -29,4 +29,11 @@ subprojects { compileOptions.javaClass.getMethod("setTargetCompatibility", JavaVersion::class.java).invoke(compileOptions, JavaVersion.VERSION_17) } } + + // Set Kotlin JVM target for all projects + tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { + kotlinOptions { + jvmTarget = "17" + } + } } \ No newline at end of file diff --git a/lib/data/database.dart b/lib/data/database.dart @@ -17,13 +17,14 @@ class AppDatabase { return await openDatabase( path, - version: 1, + version: 2, // Incremented version to trigger migration onCreate: (db, version) async { await db.execute(''' CREATE TABLE images ( id TEXT PRIMARY KEY, filePath TEXT, - createdAt TEXT + createdAt TEXT, + analysis_data TEXT ) '''); @@ -44,6 +45,22 @@ class AppDatabase { ) '''); }, + onUpgrade: (db, oldVersion, newVersion) async { + // Migrate from version 1 to version 2: Add analysis_data column + if (oldVersion < 2) { + // Check if column exists before adding + final tableInfo = await db.rawQuery('PRAGMA table_info(images)'); + final hasAnalysisData = tableInfo.any( + (column) => column['name'] == 'analysis_data', + ); + + if (!hasAnalysisData) { + await db.execute(''' + ALTER TABLE images ADD COLUMN analysis_data TEXT + '''); + } + } + }, ); } } diff --git a/lib/data/models/image_model.dart b/lib/data/models/image_model.dart @@ -1,12 +1,33 @@ // lib/data/models/image_model.dart +import 'dart:convert'; + class ImageModel { final String id; final String filePath; + final String? analysisData; - ImageModel({required this.id, required this.filePath}); + ImageModel({required this.id, required this.filePath, this.analysisData}); factory ImageModel.fromMap(Map<String, dynamic> m) { - return ImageModel(id: m['id'], filePath: m['filePath']); + return ImageModel( + id: m['id'], + filePath: m['filePath'], + analysisData: m['analysis_data'], + ); + } + + Map<String, dynamic>? get analysis { + if (analysisData == null) return null; + try { + // analysisData is a JSON string, so we need to decode it + if (analysisData is String) { + return json.decode(analysisData as String) as Map<String, dynamic>; + } + // If it's already a Map, convert it + return Map<String, dynamic>.from(analysisData as Map); + } catch (e) { + return null; + } } } diff --git a/lib/data/repos/image_repo.dart b/lib/data/repos/image_repo.dart @@ -3,15 +3,30 @@ import 'package:adobe/data/database.dart'; class ImageRepository { - Future<void> insertImage(String id, String filePath) async { + Future<void> insertImage( + String id, + String filePath, { + String? analysisData, + }) async { final db = await AppDatabase.db; await db.insert("images", { "id": id, "filePath": filePath, - "createdAt": DateTime.now().toString() + "createdAt": DateTime.now().toString(), + "analysis_data": analysisData, }); } + Future<void> updateImageAnalysis(String id, String analysisData) async { + final db = await AppDatabase.db; + await db.update( + "images", + {"analysis_data": analysisData}, + where: "id = ?", + whereArgs: [id], + ); + } + Future<List<Map<String, dynamic>>> getAllImages() async { final db = await AppDatabase.db; return await db.query("images"); diff --git a/lib/main.dart b/lib/main.dart @@ -26,20 +26,25 @@ class _MyAppState extends State<MyApp> { super.initState(); // 1. LISTEN: App is running in memory - _intentSub = ReceiveSharingIntent.instance.getMediaStream().listen((List<SharedMediaFile> value) { - if (value.isNotEmpty) { - // The URL or Text is inside the 'path' property - _navigateToSharePage(value.first.path); - } - }, onError: (err) { - debugPrint("Share Error: $err"); - }); + _intentSub = ReceiveSharingIntent.instance.getMediaStream().listen( + (List<SharedMediaFile> value) { + if (value.isNotEmpty) { + // The URL, Text, or file path is inside the 'path' property + _navigateToSharePage(value.first.path); + } + }, + onError: (err) { + debugPrint("Share Error: $err"); + }, + ); // 2. LISTEN: App is closed and opened via Share - ReceiveSharingIntent.instance.getInitialMedia().then((List<SharedMediaFile> value) { + ReceiveSharingIntent.instance.getInitialMedia().then(( + List<SharedMediaFile> value, + ) { if (value.isNotEmpty) { _navigateToSharePage(value.first.path); - + // Optional: Clear the intent so it doesn't re-trigger on reload ReceiveSharingIntent.instance.reset(); } @@ -72,4 +77,4 @@ class _MyAppState extends State<MyApp> { home: const HomePage(), ); } -} -\ No newline at end of file +} diff --git a/lib/services/instagram_download_service.dart b/lib/services/instagram_download_service.dart @@ -0,0 +1,65 @@ +import 'dart:io'; +import 'dart:convert'; +import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:uuid/uuid.dart'; +import 'package:adobe/data/repos/image_repo.dart'; + +class InstagramDownloadService { + static const MethodChannel _channel = MethodChannel( + 'com.example.adobe/instagram_downloader', + ); + final _repo = ImageRepository(); + final _uuid = const Uuid(); + + Future<String?> downloadInstagramImage(String url) async { + try { + // Get output directory + final dir = await getApplicationDocumentsDirectory(); + final instagramDir = Directory('${dir.path}/instagram_downloads'); + if (!await instagramDir.exists()) { + await instagramDir.create(recursive: true); + } + + // Call Python function + final String? result = await _channel.invokeMethod( + 'downloadInstagramImage', + {'url': url, 'outputDir': instagramDir.path}, + ); + + if (result != null) { + final Map<String, dynamic> jsonResult = json.decode(result); + + if (jsonResult['success'] == true) { + final downloadedPath = jsonResult['file_path'] as String; + + // Copy to images directory + final imagesDir = Directory('${dir.path}/images'); + if (!await imagesDir.exists()) { + await imagesDir.create(recursive: true); + } + + final String imageId = _uuid.v4(); + final extension = downloadedPath.split('.').last; + final String targetPath = '${imagesDir.path}/$imageId.$extension'; + + // Copy file + final sourceFile = File(downloadedPath); + await sourceFile.copy(targetPath); + + // Save to database + await _repo.insertImage(imageId, targetPath); + + return imageId; + } else { + throw Exception(jsonResult['error'] ?? 'Unknown error'); + } + } + return null; + } on PlatformException catch (e) { + throw Exception("Error downloading Instagram image: ${e.message}"); + } catch (e) { + throw Exception("Unexpected error: $e"); + } + } +} diff --git a/lib/ui/pages/board_detail_page.dart b/lib/ui/pages/board_detail_page.dart @@ -1,10 +1,16 @@ // lib/ui/pages/board_detail_page.dart import 'dart:io'; +import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:uuid/uuid.dart'; import 'package:adobe/data/models/board_model.dart'; import 'package:adobe/data/models/image_model.dart'; import 'package:adobe/data/repos/board_image_repo.dart'; +import 'package:adobe/data/repos/image_repo.dart'; +import 'package:adobe/services/image_analyzer_service.dart'; class BoardDetailPage extends StatefulWidget { final Board board; @@ -17,6 +23,9 @@ class BoardDetailPage extends StatefulWidget { class _BoardDetailPageState extends State<BoardDetailPage> { final _boardImageRepo = BoardImageRepository(); + final _imageRepo = ImageRepository(); + final _imagePicker = ImagePicker(); + final _uuid = const Uuid(); late Future<List<ImageModel>> _imagesFuture; @override @@ -30,10 +39,195 @@ class _BoardDetailPageState extends State<BoardDetailPage> { return rawData.map((e) => ImageModel.fromMap(e)).toList(); } + Future<void> _pickImageFromGallery() async { + try { + final XFile? image = await _imagePicker.pickImage( + source: ImageSource.gallery, + ); + if (image == null) return; + + // Copy to images directory + final dir = await getApplicationDocumentsDirectory(); + final imagesDir = Directory('${dir.path}/images'); + if (!await imagesDir.exists()) { + await imagesDir.create(recursive: true); + } + + final extension = image.path.split('.').last; + final imageId = _uuid.v4(); + final targetPath = '${imagesDir.path}/$imageId.$extension'; + + await File(image.path).copy(targetPath); + await _imageRepo.insertImage(imageId, targetPath); + await _boardImageRepo.saveToBoard(widget.board.id, imageId); + + // Trigger analysis + _analyzeImage(imageId, targetPath); + + if (mounted) { + setState(() { + _imagesFuture = _fetchImages(); + }); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Error picking image: $e'))); + } + } + } + + Future<void> _analyzeImage(String imageId, String imagePath) async { + try { + final result = await ImageAnalyzerService.analyzeImage(imagePath); + if (result != null && result['success'] == true) { + await _imageRepo.updateImageAnalysis(imageId, json.encode(result)); + if (mounted) { + setState(() { + _imagesFuture = _fetchImages(); + }); + } + } + } catch (e) { + debugPrint('Error analyzing image: $e'); + } + } + + void _showAnalysisDialog(ImageModel image) { + if (image.analysisData == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Analysis not available for this image')), + ); + return; + } + + try { + final analysis = json.decode(image.analysisData!); + if (analysis['success'] != true) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Analysis failed for this image')), + ); + return; + } + + final scores = analysis['scores'] as Map<String, dynamic>; + final top5 = analysis['top5'] as List; + + showDialog( + context: context, + builder: + (context) => Dialog( + child: Container( + padding: const EdgeInsets.all(16), + constraints: const BoxConstraints( + maxWidth: 400, + maxHeight: 600, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Image Analysis', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + const Text( + 'Top 5 Features:', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Expanded( + child: ListView.builder( + shrinkWrap: true, + itemCount: top5.length, + itemBuilder: (context, index) { + final item = top5[index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(item['name']), + Text( + '${(item['score'] * 100).toStringAsFixed(1)}%', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + }, + ), + ), + const SizedBox(height: 16), + const Text( + 'All Scores:', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Expanded( + child: ListView.builder( + shrinkWrap: true, + itemCount: scores.length, + itemBuilder: (context, index) { + final entry = scores.entries.elementAt(index); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(entry.key), + Text( + '${(entry.value * 100).toStringAsFixed(1)}%', + ), + ], + ), + ); + }, + ), + ), + const SizedBox(height: 16), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + ), + ), + ); + } catch (e) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Error displaying analysis: $e'))); + } + } + @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text(widget.board.name)), + appBar: AppBar( + title: Text(widget.board.name), + actions: [ + IconButton( + icon: const Icon(Icons.photo_library), + onPressed: _pickImageFromGallery, + tooltip: 'Add from Gallery', + ), + ], + ), body: FutureBuilder<List<ImageModel>>( future: _imagesFuture, builder: (context, snapshot) { @@ -42,7 +236,9 @@ class _BoardDetailPageState extends State<BoardDetailPage> { } if (!snapshot.hasData || snapshot.data!.isEmpty) { - return const Center(child: Text("No images saved to this board yet.")); + return const Center( + child: Text("No images saved to this board yet."), + ); } final images = snapshot.data!; @@ -60,24 +256,55 @@ class _BoardDetailPageState extends State<BoardDetailPage> { final img = images[index]; final file = File(img.filePath); - return ClipRRect( - borderRadius: BorderRadius.circular(12), - child: file.existsSync() - ? Image.file( - file, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container( - color: Colors.grey[300], - alignment: Alignment.center, - child: const Icon(Icons.broken_image, color: Colors.grey), - ); - }, - ) - : Container( - color: Colors.grey[300], - child: const Icon(Icons.error, color: Colors.red), + return GestureDetector( + onTap: () => _showAnalysisDialog(img), + child: Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: + file.existsSync() + ? Image.file( + file, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.grey[300], + alignment: Alignment.center, + child: const Icon( + Icons.broken_image, + color: Colors.grey, + ), + ); + }, + ) + : Container( + color: Colors.grey[300], + child: const Icon( + Icons.error, + color: Colors.red, + ), + ), + ), + if (img.analysisData != null) + Positioned( + top: 8, + right: 8, + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.8), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.analytics, + color: Colors.white, + size: 16, + ), + ), ), + ], + ), ); }, ); @@ -85,4 +312,4 @@ class _BoardDetailPageState extends State<BoardDetailPage> { ), ); } -} -\ No newline at end of file +} diff --git a/lib/ui/pages/share_handler_page.dart b/lib/ui/pages/share_handler_page.dart @@ -1,9 +1,16 @@ // lib/ui/pages/share_handler_page.dart +import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:uuid/uuid.dart'; import 'package:adobe/services/download_service.dart'; +import 'package:adobe/services/instagram_download_service.dart'; import 'package:adobe/data/repos/board_repo.dart'; import 'package:adobe/data/repos/board_image_repo.dart'; +import 'package:adobe/data/repos/image_repo.dart'; +import 'package:adobe/services/image_analyzer_service.dart'; +import 'dart:convert'; class ShareHandlerPage extends StatefulWidget { final String sharedText; // The URL passed from Android @@ -16,12 +23,16 @@ class ShareHandlerPage extends StatefulWidget { class _ShareHandlerPageState extends State<ShareHandlerPage> { final _downloadService = DownloadService(); + final _instagramService = InstagramDownloadService(); final _boardRepo = BoardRepository(); final _boardImageRepo = BoardImageRepository(); + final _imageRepo = ImageRepository(); + final _uuid = const Uuid(); String? _downloadedImageId; bool _isDownloading = true; bool _hasError = false; + String? _errorMessage; List<Map<String, dynamic>> _boards = []; @override @@ -33,43 +44,157 @@ class _ShareHandlerPageState extends State<ShareHandlerPage> { void _startProcess() async { // 1. Start loading boards immediately _boards = await _boardRepo.getBoards(); - - // 2. Parse URL (Simple regex to find a URL in the shared text) - final urlRegExp = RegExp(r'(https?://\S+)'); - final match = urlRegExp.firstMatch(widget.sharedText); - - if (match != null) { - final url = match.group(0)!; - // 3. Download in background - final id = await _downloadService.downloadAndSaveImage(url); - - if (mounted) { - setState(() { - _downloadedImageId = id; - _isDownloading = false; - if (id == null) _hasError = true; - }); + + // 2. Check if it's a file path (direct image share) or URL + final sharedPath = widget.sharedText.trim(); + + if (File(sharedPath).existsSync()) { + // Direct image file - copy it to our images directory + try { + final id = await _handleDirectImage(sharedPath); + if (mounted) { + setState(() { + _downloadedImageId = id; + _isDownloading = false; + if (id == null) { + _hasError = true; + _errorMessage = "Failed to save image"; + } + }); + } + } catch (e) { + if (mounted) { + setState(() { + _hasError = true; + _errorMessage = "Error: $e"; + _isDownloading = false; + }); + } } } else { - if (mounted) setState(() => _hasError = true); + // It's a URL - check if it's Instagram + final urlRegExp = RegExp(r'(https?://\S+)'); + final match = urlRegExp.firstMatch(sharedPath); + + if (match != null) { + final url = match.group(0)!; + + // Check if it's an Instagram URL + if (url.contains('instagram.com')) { + try { + final id = await _instagramService.downloadInstagramImage(url); + if (mounted) { + setState(() { + _downloadedImageId = id; + _isDownloading = false; + if (id == null) { + _hasError = true; + _errorMessage = "Failed to download from Instagram"; + } + }); + } + } catch (e) { + if (mounted) { + setState(() { + _hasError = true; + _errorMessage = "Error: $e"; + _isDownloading = false; + }); + } + } + } else { + // Regular URL - use existing download service + final id = await _downloadService.downloadAndSaveImage(url); + if (mounted) { + setState(() { + _downloadedImageId = id; + _isDownloading = false; + if (id == null) { + _hasError = true; + _errorMessage = "Failed to download image"; + } + }); + } + } + } else { + if (mounted) { + setState(() { + _hasError = true; + _errorMessage = "Invalid URL or file path"; + _isDownloading = false; + }); + } + } } } + Future<String?> _handleDirectImage(String filePath) async { + // Import needed packages + final file = File(filePath); + if (!await file.exists()) return null; + + // Copy to images directory + final dir = await getApplicationDocumentsDirectory(); + final imagesDir = Directory('${dir.path}/images'); + if (!await imagesDir.exists()) { + await imagesDir.create(recursive: true); + } + + final extension = filePath.split('.').last; + final imageId = _uuid.v4(); + final targetPath = '${imagesDir.path}/$imageId.$extension'; + + await file.copy(targetPath); + await _imageRepo.insertImage(imageId, targetPath); + + return imageId; + } + Future<void> _saveToBoard(int boardId) async { if (_downloadedImageId == null) return; await _boardImageRepo.saveToBoard(boardId, _downloadedImageId!); - + + // Get image path and trigger analysis + final images = await _imageRepo.getAllImages(); + final imageData = images.firstWhere( + (img) => img['id'] == _downloadedImageId, + orElse: () => <String, dynamic>{}, + ); + + if (imageData.isNotEmpty && imageData['filePath'] != null) { + _analyzeImageInBackground( + _downloadedImageId!, + imageData['filePath'] as String, + ); + } + if (!mounted) return; - + ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Saved to board!")), + const SnackBar( + content: Text("Saved to board! Analysis will run in background."), + ), ); - + // Close the app or go back to main screen Navigator.popUntil(context, (route) => route.isFirst); } + Future<void> _analyzeImageInBackground( + String imageId, + String imagePath, + ) async { + try { + final result = await ImageAnalyzerService.analyzeImage(imagePath); + if (result != null && result['success'] == true) { + await _imageRepo.updateImageAnalysis(imageId, json.encode(result)); + } + } catch (e) { + debugPrint('Error analyzing image in background: $e'); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -80,7 +205,25 @@ class _ShareHandlerPageState extends State<ShareHandlerPage> { Widget _buildBody() { if (_hasError) { - return Center(child: Text("Could not download image from link.")); + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error, color: Colors.red, size: 48), + const SizedBox(height: 16), + Text( + _errorMessage ?? "Could not process image", + style: const TextStyle(fontSize: 16), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => Navigator.pop(context), + child: const Text("Go Back"), + ), + ], + ), + ); } // While downloading, show loader but ALSO show boards (disabled or overlay) @@ -141,26 +284,33 @@ class _ShareHandlerPageState extends State<ShareHandlerPage> { final controller = TextEditingController(); showDialog( context: context, - builder: (c) => AlertDialog( - title: Text("New Board"), - content: TextField(controller: controller, decoration: InputDecoration(hintText: "Name")), - actions: [ - TextButton(onPressed: () => Navigator.pop(c), child: Text("Cancel")), - ElevatedButton( - onPressed: () async { - if (controller.text.isNotEmpty) { - // Create board - final newId = await _boardRepo.createBoard(controller.text); - if(!c.mounted) return; - Navigator.pop(c); // Close dialog - // Save image to new board - _saveToBoard(newId); - } - }, - child: Text("Create"), - ) - ], - ), + builder: + (c) => AlertDialog( + title: Text("New Board"), + content: TextField( + controller: controller, + decoration: InputDecoration(hintText: "Name"), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(c), + child: Text("Cancel"), + ), + ElevatedButton( + onPressed: () async { + if (controller.text.isNotEmpty) { + // Create board + final newId = await _boardRepo.createBoard(controller.text); + if (!c.mounted) return; + Navigator.pop(c); // Close dialog + // Save image to new board + _saveToBoard(newId); + } + }, + child: Text("Create"), + ), + ], + ), ); } -} -\ No newline at end of file +}