creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
commit aa6874fd76f8729e5cd549f671ec97843897666f parent 6ab46e163b4d5c9881c83d4bba8b0538618010fd Author: maydayv7 <maydayv7@gmail.com> Date: Fri, 28 Nov 2025 05:18:48 +0530 Fix share handler And miscellaneous other fixes
Diffstat:
23 files changed, 492 insertions(+), 574 deletions(-)
diff --git a/lib/main.dart b/lib/main.dart @@ -1,9 +1,8 @@ import 'dart:async'; -import 'dart:io'; // Required for File handling import 'package:flutter/material.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:adobe/services/theme_service.dart'; -import 'package:adobe/ui/pages/share_to_moodboard_page.dart'; // Share Page +import 'package:adobe/ui/pages/share_handler_page.dart'; import 'package:adobe/ui/pages/home_page.dart'; void main() { @@ -19,10 +18,9 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State<MyApp> { - late StreamSubscription _intentSub; + late StreamSubscription _intentStreamSubscription; final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>(); - // Controls the loading screen bool _isReady = false; @override @@ -36,167 +34,73 @@ class _MyAppState extends State<MyApp> { try { await themeService.loadTheme(); } catch (e) { - debugPrint("⚠️ Theme load error (ignoring): $e"); + debugPrint("Theme load error (ignoring): $e"); } - // 2. Setup Sharing Intent Listener + // 2. Setup Share Listeneer try { - // Listen for shared files while app is running - _intentSub = ReceiveSharingIntent.instance.getMediaStream().listen( - (List<SharedMediaFile> value) { - if (value.isNotEmpty) { - _showShareOptions(value); - } - }, - onError: (err) { - debugPrint("⚠️ Share stream error: $err"); - }, - ); - - // Check if the app was launched via a share action (Cold Start) - final initialMedia = - await ReceiveSharingIntent.instance.getInitialMedia(); - if (initialMedia.isNotEmpty) { - _showShareOptions(initialMedia); - // Important: Reset immediately to avoid re-triggering on reload - ReceiveSharingIntent.instance.reset(); - } + _intentStreamSubscription = ReceiveSharingIntent.instance + .getMediaStream() + .listen((List<SharedMediaFile> value) { + if (value.isNotEmpty) { + _handleShare(value.first.path); + } + }, onError: (err) => debugPrint("Share error: $err")); + + // 3. Cold Start + ReceiveSharingIntent.instance.getInitialMedia().then((List<SharedMediaFile> value) { + if (value.isNotEmpty) { + _handleShare(value.first.path); + ReceiveSharingIntent.instance.reset(); + } + }); } catch (e) { - debugPrint("⚠️ Share intent error: $e"); + debugPrint("Share intent error: $e"); } - // Listen for theme changes to rebuild the UI dynamically themeService.addListener(_updateTheme); - if (mounted) { - setState(() { - _isReady = true; - }); - } + if (mounted) setState(() => _isReady = true); } - void _updateTheme() { - if (mounted) setState(() {}); - } - - // --- SHARE HANDLING LOGIC --- - - void _showShareOptions(List<SharedMediaFile> files) { - // We use the navigator key to get context because 'context' might not be valid in async callbacks + void _handleShare(String content) { final context = _navigatorKey.currentState?.overlay?.context; + if (context == null) return; - // Safety check: ensure we have context and a file - if (context == null || files.isEmpty) return; - - final File imageFile = File(files.first.path); - - showModalBottomSheet( - context: context, - backgroundColor: Colors.white, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + _navigatorKey.currentState?.push( + MaterialPageRoute( + builder: (_) => ShareHandlerPage(sharedText: content), ), - builder: (context) { - return SafeArea( - child: Wrap( - children: [ - const Padding( - padding: EdgeInsets.fromLTRB(20, 20, 20, 10), - child: Text( - "Save Image To...", - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ), - - // Option 1: Moodboard - ListTile( - leading: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.blueAccent.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.dashboard_customize, - color: Colors.blueAccent, - ), - ), - title: const Text("Moodboard"), - subtitle: const Text("Add to references & analysis"), - onTap: () { - Navigator.pop(context); // Close Bottom Sheet - - // Navigate to ShareToMoodboardPage - _navigatorKey.currentState?.push( - MaterialPageRoute( - builder: - (_) => ShareToMoodboardPage(imageFile: imageFile), - ), - ); - }, - ), - - // Option 2: Project Files - ListTile( - leading: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.orangeAccent.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.folder_copy, - color: Colors.orangeAccent, - ), - ), - title: const Text("Project Files"), - subtitle: const Text("Add to canvas assets"), - onTap: () { - Navigator.pop(context); // Close Bottom Sheet - - // Feature Placeholder - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Files feature coming soon!")), - ); - }, - ), - const SizedBox(height: 20), - ], - ), - ); - }, ); } + void _updateTheme() { + if (mounted) setState(() {}); + } + @override void dispose() { - try { - _intentSub.cancel(); - } catch (_) {} + _intentStreamSubscription.cancel(); themeService.removeListener(_updateTheme); super.dispose(); } @override Widget build(BuildContext context) { - // Loading Screen if (!_isReady) { return const MaterialApp( debugShowCheckedModeBanner: false, - home: Scaffold( - backgroundColor: Colors.white, - body: Center(child: CircularProgressIndicator()), - ), + home: Scaffold(backgroundColor: Colors.white, body: Center(child: CircularProgressIndicator())), ); } return MaterialApp( - navigatorKey: - _navigatorKey, // Critical for global navigation from Share Intent + navigatorKey: _navigatorKey, debugShowCheckedModeBanner: false, // Theme Configuration - themeMode: themeService.mode, + // themeMode: themeService.mode, + themeMode: ThemeMode.light, theme: ThemeData( fontFamily: 'GeneralSans', @@ -212,7 +116,6 @@ class _MyAppState extends State<MyApp> { surfaceTintColor: Colors.transparent, ), ), - darkTheme: ThemeData( fontFamily: 'GeneralSans', colorScheme: ColorScheme.fromSeed( @@ -227,7 +130,6 @@ class _MyAppState extends State<MyApp> { surfaceTintColor: Colors.transparent, ), ), - home: const HomePage(), ); } diff --git a/lib/services/analyze/color.dart b/lib/services/analyze/color.dart @@ -1,7 +1,5 @@ -// lib/services/color_analyzer_service.dart - import 'package:flutter/foundation.dart'; -import 'python_service.dart'; +import '../python_service.dart'; class ColorAnalyzerService { final PythonService _pythonService = PythonService(); diff --git a/lib/services/analyze/embedding.dart b/lib/services/analyze/embedding.dart @@ -1,11 +1,9 @@ -// lib/services/embedding_analyzer_service.dart - import 'dart:convert'; import 'dart:io'; -import 'package:adobe/utils/image_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:onnxruntime/onnxruntime.dart'; -import '../../utils/clip_image_processor.dart'; +import 'package:adobe/utils/image_utils.dart'; +import 'package:adobe/utils/clip_image_processor.dart'; class EmbeddingAnalyzerService { OrtSession? _session; diff --git a/lib/services/analyze/image_analyzer.dart b/lib/services/analyze/image_analyzer.dart @@ -1,5 +1,3 @@ -// lib/services/image_analyzer_service.dart - import 'dart:async'; import 'dart:io'; import 'dart:isolate'; @@ -236,7 +234,7 @@ class ImageAnalyzerService { 'Style': {"scores": results[3]['scores']}, 'Texture': {"scores": results[2]['scores']}, 'Lighting': {"scores": results[5]['scores']}, - 'Colour Pallete': {"scores": results[1]['scores']}, + 'Colour Palette': {"scores": results[1]['scores']}, 'Emotions': {"scores": results[4]['scores']}, 'Era': {"scores": results[6]['scores']}, 'Layout': {"scores": results[0]['scores']} @@ -257,7 +255,7 @@ class ImageAnalyzerService { 'Style': {"scores": {}}, 'Texture': {"scores": {}}, 'Lighting': {"scores": {}}, - 'Colour Pallete': {"scores": {}}, + 'Colour Palette': {"scores": {}}, 'Emotions': {"scores": {}}, 'Era': {"scores": {}}, } diff --git a/lib/services/analyze/layout.dart b/lib/services/analyze/layout.dart @@ -1,7 +1,5 @@ -// lib/services/layout_analyzer_service.dart - import 'package:flutter/foundation.dart'; -import 'python_service.dart'; +import '../python_service.dart'; class LayoutAnalyzerService { final PythonService _pythonService = PythonService(); diff --git a/lib/services/analyze/texture.dart b/lib/services/analyze/texture.dart @@ -1,11 +1,10 @@ -// lib/services/texture_analyzer_service.dart - import 'dart:io'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:image/image.dart' as img; import 'package:onnxruntime/onnxruntime.dart'; -import '../../utils/image_utils.dart'; +import 'package:adobe/utils/image_utils.dart'; +import 'package:adobe/utils/clip_image_processor.dart'; class TextureAnalyzerService { OrtSession? _session; diff --git a/lib/services/download_service.dart b/lib/services/download_service.dart @@ -3,11 +3,9 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid.dart'; -import 'package:adobe/data/repos/image_repo.dart'; import 'package:html/parser.dart' as parser; class DownloadService { - final _repo = ImageRepository(); final _uuid = const Uuid(); String _unescapeUrl(String s) { @@ -36,7 +34,7 @@ class DownloadService { } Future<String?> downloadAndSaveImage(String url) async { - debugPrint("📥 START PROCESS: $url"); + debugPrint("START DOWNLOAD: $url"); try { // Fetch URL @@ -56,16 +54,12 @@ class DownloadService { var contentType = response.headers['content-type']; debugPrint("Type: $contentType"); - // CASE A → HTML webpage (NOT a direct image) - if (contentType != null && !contentType.startsWith('image/')) { debugPrint("It's a website. Extracting real image..."); final document = parser.parse(response.body); - - // 1️⃣ GOOGLE PHOTOS / GOOGLEUSERCONTENT.COM - + // 1️1. GOOGLE PHOTOS / GOOGLEUSERCONTENT.COM RegExp googlePhotosRegex = RegExp( r'https:\/\/lh3\.googleusercontent\.com\/[a-zA-Z0-9\-\._=]+' ); @@ -73,50 +67,38 @@ class DownloadService { var matchPhotos = googlePhotosRegex.firstMatch(response.body); if (matchPhotos != null) { String cdn = matchPhotos.group(0)!; - // Force highest resolution cdn = _unescapeUrl(cdn); cdn = "${cdn.split("=").first}=s0"; debugPrint("Google Photos Image Found: $cdn"); - return downloadAndSaveImage(cdn); } - - // 2️⃣ GOOGLE IMAGE SEARCH (encrypted-tbn / gstatic) - - RegExp googleImageRegex = RegExp( + // 2️2. GOOGLE IMAGE SEARCH + RegExp googleImageRegex = RegExp( "https:\\/\\/(?:encrypted\\-tbn\\d\\.gstatic\\.com|(?:\\w+\\.)?gstatic\\.com)\\/[^\"'\\s<>]+", ); - - - var matchGImg = googleImageRegex.firstMatch(response.body); if (matchGImg != null) { String imgUrl = matchGImg.group(0)!; imgUrl = _unescapeUrl(imgUrl); - debugPrint("🔍 Google Images Real URL: $imgUrl"); - + debugPrint("Google Images Real URL: $imgUrl"); return downloadAndSaveImage(imgUrl); } - // 3️⃣ GOOGLE VIEWER internal imageUrl='https://...' - - RegExp viewerImageRegex = - RegExp(r"imageUrl='(https:\/\/[^']+)'"); + // 3️3. GOOGLE VIEWER + RegExp viewerImageRegex = RegExp(r"imageUrl='(https:\/\/[^']+)'"); var viewerMatch = viewerImageRegex.firstMatch(response.body); if (viewerMatch != null) { String imgUrl = viewerMatch.group(1)!; imgUrl = _unescapeUrl(imgUrl); debugPrint("Google Viewer Direct Image: $imgUrl"); - return downloadAndSaveImage(imgUrl); } - // 4️⃣ OG IMAGE → Pinterest / Instagram / Wikipedia - + // 4️4. OG IMAGE → Pinterest / Instagram / Wikipedia final metaTags = document.getElementsByTagName('meta'); for (var meta in metaTags) { if (meta.attributes['property'] == 'og:image') { @@ -133,7 +115,6 @@ class DownloadService { return null; } - // CASE B → DIRECT IMAGE FILE (content-type starts with image/) final dir = await getApplicationDocumentsDirectory(); final imagesDir = Directory('${dir.path}/images'); @@ -160,10 +141,8 @@ class DownloadService { final file = File(filePath); await file.writeAsBytes(response.bodyBytes); - await _repo.insertImage(imageId, filePath); - debugPrint("Saved Successfully! → $filePath"); - return imageId; + return filePath; } catch (e) { debugPrint("Error: $e"); return null; diff --git a/lib/services/image_service.dart b/lib/services/image_service.dart @@ -1,34 +1,72 @@ import 'dart:convert'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid.dart'; import '../data/repos/image_repo.dart'; import '../data/models/image_model.dart'; +import 'analyze/image_analyzer.dart'; class ImageService { - final _repo = ImageRepo(); + final ImageRepo _repo = ImageRepo(); + final Uuid _uuid = const Uuid(); - Future<String> saveImage(File file, int projectId) async { + Future<void> saveImage(File file, int projectId) async { + await saveImages([file], projectId); + } + + Future<void> saveImages(List<File> files, int projectId) async { + // 1. Prepare Directory final dir = await getApplicationDocumentsDirectory(); final folder = Directory("${dir.path}/images"); if (!await folder.exists()) await folder.create(recursive: true); - final id = const Uuid().v4(); - String ext = file.path.split('.').last; - final newPath = "${folder.path}/$id.$ext"; - - await file.copy(newPath); - - final image = ImageModel( - id: id, - projectId: projectId, - filePath: newPath, - name: file.path.split('/').last, - createdAt: DateTime.now(), - ); - - await _repo.addImage(image); - return id; + for (var file in files) { + final String id = _uuid.v4(); + + // 2. Determine Extension & Copy File to Permanent Storage + String ext = "jpg"; // Default + try { + if (file.path.contains('.')) { + ext = file.path.split('.').last; + } + } catch (_) {} + + final String newPath = "${folder.path}/$id.$ext"; + await file.copy(newPath); + + // 3. Create & Save Model + final image = ImageModel( + id: id, + projectId: projectId, + filePath: newPath, + name: file.path.split('/').last, + createdAt: DateTime.now(), + tags: [], + ); + + await _repo.addImage(image); + + // 4. Run Analysis in Background + _analyzeInBackground(id, newPath); + } + } + + Future<void> _analyzeInBackground(String imageId, String filePath) async { + try { + debugPrint("[Background] Starting analysis for $imageId..."); + final result = await ImageAnalyzerService.analyzeFullSuite(filePath); + if (result != null) { + // Convert Map to JSON String + final String jsonString = jsonEncode(result); + + // Update DB without user intervention + await _repo.updateAnalysis(imageId, jsonString); + debugPrint("[Background] Analysis saved for $imageId"); + } + } catch (e) { + debugPrint("[Background] Analysis failed: $e"); + } } Future<void> updateAnalysis(String id, Map<String, dynamic> analysis) async { @@ -44,7 +82,13 @@ class ImageService { final img = await _repo.getById(id); if (img != null) { final file = File(img.filePath); - if (await file.exists()) await file.delete(); + if (await file.exists()) { + try { + await file.delete(); + } catch (e) { + debugPrint("Error deleting file: $e"); + } + } await _repo.deleteImage(id); } } diff --git a/lib/services/instagram_download_service.dart b/lib/services/instagram_download_service.dart @@ -1,12 +1,10 @@ import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid.dart'; -import 'package:adobe/data/repos/image_repo.dart'; import 'python_service.dart'; class InstagramDownloadService { final _pythonService = PythonService(); - final _repo = ImageRepository(); final _uuid = const Uuid(); Future<List<String>?> downloadInstagramImage(String url) async { @@ -25,9 +23,9 @@ class InstagramDownloadService { if (jsonResult != null && jsonResult['success'] == true) { final List<dynamic> paths = jsonResult['file_paths']; - final List<String> savedImageIds = []; + final List<String> savedFilePaths = []; - // Save downloaded files to app storage and DB + // Save downloaded files to app storage for (var path in paths) { final String sourcePath = path as String; final File sourceFile = File(sourcePath); @@ -36,13 +34,11 @@ class InstagramDownloadService { final String imageId = _uuid.v4(); final extension = sourcePath.split('.').last; final String targetPath = '${imagesDir.path}/$imageId.$extension'; - await sourceFile.copy(targetPath); - await _repo.insertImage(imageId, targetPath); - savedImageIds.add(imageId); + savedFilePaths.add(targetPath); } } - return savedImageIds.isNotEmpty ? savedImageIds : null; + return savedFilePaths.isNotEmpty ? savedFilePaths : null; } return null; } catch (e) { diff --git a/lib/services/analyze/python_service.dart b/lib/services/python_service.dart diff --git a/lib/ui/pages/all_image_page.dart b/lib/ui/pages/all_image_page.dart @@ -1,3 +1,5 @@ +// BROKEN + import 'dart:io'; import 'package:flutter/material.dart'; import 'package:adobe/data/repos/image_repo.dart'; diff --git a/lib/ui/pages/board_detail_page.dart b/lib/ui/pages/board_detail_page.dart @@ -1,3 +1,5 @@ +// BROKEN + import 'dart:io'; import 'dart:convert'; import 'package:flutter/material.dart'; diff --git a/lib/ui/pages/board_list_page.dart b/lib/ui/pages/board_list_page.dart @@ -1,3 +1,5 @@ +// BROKEN + import 'dart:io'; import 'package:flutter/material.dart'; import '../../data/repos/board_repo.dart'; diff --git a/lib/ui/pages/board_page.dart b/lib/ui/pages/board_page.dart @@ -1,3 +1,5 @@ +// BROKEN + import 'dart:io'; import 'package:flutter/material.dart'; diff --git a/lib/ui/pages/home_page.dart b/lib/ui/pages/home_page.dart @@ -1,11 +1,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import '../../data/models/project_model.dart'; -import '../../data/repos/project_repo.dart'; -import '../../data/repos/image_repo.dart'; -import '../../services/project_service.dart'; -import 'image_analysis_page.dart'; +import 'package:adobe/data/models/project_model.dart'; +import 'package:adobe/data/repos/project_repo.dart'; +import 'package:adobe/data/repos/image_repo.dart'; +import 'package:adobe/services/project_service.dart'; import 'project_detail_page.dart'; +import 'image_analysis_page.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -279,7 +279,7 @@ class _HomePageState extends State<HomePage> { child: Row( children: [ Text( - "Your Project", + "Your Projects", style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, diff --git a/lib/ui/pages/image_analysis_page.dart b/lib/ui/pages/image_analysis_page.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; - import 'package:adobe/services/analyze/image_analyzer.dart'; class ImageAnalysisPage extends StatefulWidget { @@ -19,10 +18,11 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { Map<String, dynamic>? _analysisResult; String? _errorMessage; + final ImagePicker _picker = ImagePicker(); + Future<void> _pickImage(ImageSource source) async { try { - final ImagePicker picker = ImagePicker(); - final XFile? image = await picker.pickImage(source: source); + final XFile? image = await _picker.pickImage(source: source); if (image != null) { setState(() { _selectedImage = File(image.path); @@ -33,7 +33,12 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { _runAnalysis(image.path); } } catch (e) { - setState(() => _errorMessage = 'Error picking image: $e'); + if (mounted) { + setState(() => _errorMessage = 'Error picking image: $e'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error picking image: $e')), + ); + } } } @@ -44,16 +49,13 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { }); try { - // 1. Copy image to app doc dir (simulating real app behavior & safe access) final appDir = await getApplicationDocumentsDirectory(); final fileName = sourcePath.split('/').last; final targetPath = '${appDir.path}/$fileName'; - - // Copying ensures the analyzer service can access it freely + final file = File(sourcePath); await file.copy(targetPath); - // 2. Run the Full Suite Analysis final result = await ImageAnalyzerService.analyzeFullSuite(targetPath); if (mounted) { @@ -72,148 +74,295 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { } } + void _showSourceSelector() { + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (ctx) => SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.camera_alt_outlined, color: Colors.black), + title: const Text("Take Photo", style: TextStyle(fontFamily: 'GeneralSans')), + onTap: () { + Navigator.pop(ctx); + _pickImage(ImageSource.camera); + }, + ), + ListTile( + leading: const Icon(Icons.image_outlined, color: Colors.black), + title: const Text("Choose from Gallery", style: TextStyle(fontFamily: 'GeneralSans')), + onTap: () { + Navigator.pop(ctx); + _pickImage(ImageSource.gallery); + }, + ), + ], + ), + ), + ), + ); + } + @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Scaffold( - appBar: AppBar(title: const Text("Image Analysis Debug")), - body: Column( - children: [ - // --- Top Section: Image Preview & Controls --- - Container( - height: 220, - width: double.infinity, - color: Colors.grey[200], // Neutral grey background for image area - child: Stack( - fit: StackFit.expand, - children: [ - if (_selectedImage != null) - Image.file(_selectedImage!, fit: BoxFit.contain) - else - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.image_search, size: 48, color: Colors.grey), - const SizedBox(height: 8), - Text("No image selected", style: TextStyle(color: Colors.grey[600])), - ], - ), - - // Loading Overlay - if (_isAnalyzing) - Container( - color: Colors.black54, - child: const Center( - child: CircularProgressIndicator(color: Colors.white), + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black), + onPressed: () => Navigator.pop(context), + ), + title: const Text( + "Image Analysis", + style: TextStyle( + color: Colors.black, + fontFamily: 'GeneralSans', + fontWeight: FontWeight.bold, + ), + ), + actions: [ + if (_selectedImage != null) + IconButton( + icon: const Icon(Icons.refresh, color: Colors.black), + tooltip: "Re-analyze", + onPressed: () => _runAnalysis(_selectedImage!.path), + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Image Preview + Center( + child: Container( + width: double.infinity, + height: 300, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), ), - ), - - // Control Buttons (Bottom Right) - Positioned( - bottom: 16, - right: 16, - child: Row( - mainAxisSize: MainAxisSize.min, + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: Stack( + fit: StackFit.expand, children: [ - FloatingActionButton.small( - heroTag: "cam", - onPressed: () => _pickImage(ImageSource.camera), - child: const Icon(Icons.camera_alt), - ), - const SizedBox(width: 8), - FloatingActionButton.small( - heroTag: "gal", - onPressed: () => _pickImage(ImageSource.gallery), - child: const Icon(Icons.photo_library), - ), + if (_selectedImage != null) + Image.file(_selectedImage!, fit: BoxFit.contain) + else + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.add_photo_alternate_outlined, + size: 48, color: Colors.grey[400]), + const SizedBox(height: 12), + Text( + "Select an image to analyze", + style: TextStyle( + fontFamily: 'GeneralSans', + color: Colors.grey[500], + ), + ), + ], + ), + if (_isAnalyzing) + Container( + color: Colors.black26, + child: const Center( + child: CircularProgressIndicator(color: Colors.white), + ), + ), ], ), ), - ], - ), - ), - - // --- Bottom Section: Results or Error --- - Expanded( - child: Container( - width: double.infinity, - color: Colors.grey[100], // Light background for the scroll area - child: _buildContent(), + ), ), - ), - ], + const SizedBox(height: 24), + if (_errorMessage != null) + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.red[50], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.red[100]!), + ), + child: Text( + _errorMessage!, + style: const TextStyle(color: Colors.red, fontFamily: 'GeneralSans'), + ), + ), + if (_analysisResult != null) ...[ + const Text( + "Results", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + fontFamily: 'GeneralSans', + ), + ), + + const SizedBox(height: 16), + _buildFormattedResults(_analysisResult!), + const SizedBox(height: 32), + ExpansionTile( + title: const Text( + "View Raw JSON", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + fontFamily: 'GeneralSans', + color: Colors.grey, + ), + ), + children: [_buildJsonViewer(_analysisResult!)], + ), + ], + const SizedBox(height: 80), + ], + ), + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: _showSourceSelector, + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + icon: const Icon(Icons.camera_alt), + label: Text( + _selectedImage == null ? "Pick Image" : "Change Image", + style: const TextStyle(fontFamily: 'GeneralSans'), + ), ), ); } - Widget _buildContent() { - if (_errorMessage != null) { - return Center( - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Text( - "❌ Error:\n$_errorMessage", - style: const TextStyle(color: Colors.red), - textAlign: TextAlign.center, - ), - ), - ); + Widget _buildFormattedResults(Map<String, dynamic> root) { + final data = root['data']; + if (data == null || data['results'] == null) { + return const Text("No detailed results found."); } + final results = data['results'] as Map<String, dynamic>; - if (_analysisResult == null) { - return const Center( - child: Text("Upload an image to view raw analysis data."), - ); + return Column( + children: [ + _buildList("Style", results['Style']), + _buildList("Era", results['Era']), + _buildList("Emotions", results['Emotions']), + _buildList("Lighting", results['Lighting']), + _buildList("Layout Composition", results['Layout']), + _buildList("Color Palette", results['Colour Palette']), + _buildList("Texture", results['Texture']), + ], + ); + } + + Widget _buildList(String title, dynamic categoryData) { + if (categoryData == null || categoryData['scores'] == null) { + return const SizedBox.shrink(); } - // Pretty Print JSON - const encoder = JsonEncoder.withIndent(' '); - final String prettyJson = encoder.convert(_analysisResult); + final scoresMap = categoryData['scores'] as Map<String, dynamic>; + + final sortedEntries = scoresMap.entries.toList() + ..sort((a, b) => (b.value as num).compareTo(a.value as num)); - return SingleChildScrollView( + return Container( + margin: const EdgeInsets.only(bottom: 12), + width: double.infinity, padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - "RAW JSON OUTPUT", - style: TextStyle( - fontSize: 12, + Text( + title, + style: const TextStyle( + fontSize: 15, fontWeight: FontWeight.bold, - color: Colors.grey, - letterSpacing: 1.0, + color: Colors.black87, + fontFamily: 'GeneralSans', ), ), const SizedBox(height: 8), - - // --- JSON VIEWER CONTAINER --- - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, // FORCE WHITE BACKGROUND - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.shade300), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: SelectableText( - prettyJson, - style: const TextStyle( - fontFamily: 'monospace', - fontSize: 11, - color: Colors.black87, // FORCE DARK TEXT - height: 1.3, + const Divider(height: 1), + const SizedBox(height: 8), + ...sortedEntries.map((e) { + final val = e.value as num; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + e.key, + style: const TextStyle( + fontSize: 13, + color: Colors.black87, + fontFamily: 'GeneralSans', + ), + ), + ), + Text( + val.toStringAsFixed(4), // High precision for debugging + style: TextStyle( + fontSize: 13, + fontFamily: Platform.isIOS ? 'Courier' : 'monospace', + color: Colors.grey[700], + fontWeight: FontWeight.w500, + ), + ), + ], ), - ), - ), - const SizedBox(height: 20), + ); + }), ], ), ); } + + Widget _buildJsonViewer(Map<String, dynamic> data) { + const encoder = JsonEncoder.withIndent(' '); + final String prettyJson = encoder.convert(data); + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey[300]!), + ), + child: SelectableText( + prettyJson, + style: TextStyle( + fontFamily: Platform.isIOS ? 'Courier' : 'monospace', + fontSize: 11, + color: Colors.grey[800], + height: 1.3, + ), + ), + ); + } } diff --git a/lib/ui/pages/image_details_page.dart b/lib/ui/pages/image_details_page.dart @@ -1,3 +1,5 @@ +// BROKEN + import 'dart:io'; import 'package:flutter/material.dart'; import '../../data/models/image_model.dart'; diff --git a/lib/ui/pages/image_page.dart b/lib/ui/pages/image_page.dart @@ -0,0 +1 @@ +// BROKEN diff --git a/lib/ui/pages/image_picker_page.dart b/lib/ui/pages/image_picker_page.dart @@ -1,32 +0,0 @@ -import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:image_picker/image_picker.dart'; -import '../../services/image_service.dart'; - -class ImagePickerPage extends StatelessWidget { - ImagePickerPage({super.key}); - final picker = ImagePicker(); - final service = ImageService(); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text("Pick Image")), - body: Center( - child: ElevatedButton( - onPressed: () async { - final picked = await picker.pickImage(source: ImageSource.gallery); - - if (picked != null) { - await service.saveImage(File(picked.path)); - } - - if (!context.mounted) return; - Navigator.pop(context); - }, - child: Text("Pick From Gallery"), - ), - ), - ); - } -} diff --git a/lib/ui/pages/project_detail_page.dart b/lib/ui/pages/project_detail_page.dart @@ -1,13 +1,13 @@ import 'dart:io'; import 'dart:convert'; import 'package:flutter/material.dart'; -import '../../data/models/project_model.dart'; -import '../../data/models/image_model.dart'; -import '../../data/models/file_model.dart'; -import '../../data/repos/project_repo.dart'; -import '../../data/repos/image_repo.dart'; -import '../../data/repos/file_repo.dart'; -import '../../services/project_service.dart'; +import 'package:adobe/data/models/project_model.dart'; +import 'package:adobe/data/models/image_model.dart'; +import 'package:adobe/data/models/file_model.dart'; +import 'package:adobe/data/repos/project_repo.dart'; +import 'package:adobe/data/repos/image_repo.dart'; +import 'package:adobe/data/repos/file_repo.dart'; +import 'package:adobe/services/project_service.dart'; class ProjectDetailPage extends StatefulWidget { final int projectId; diff --git a/lib/ui/pages/share_handler_page.dart b/lib/ui/pages/share_handler_page.dart @@ -19,7 +19,6 @@ class _ShareHandlerPageState extends State<ShareHandlerPage> { final _instagramService = InstagramDownloadService(); // State - bool _isProcessing = true; bool _hasError = false; String? _errorMessage; @@ -31,12 +30,12 @@ class _ShareHandlerPageState extends State<ShareHandlerPage> { Future<void> _processSharedContent() async { final sharedContent = widget.sharedText.trim(); - File? finalFile; + List<File> finalFiles = []; try { // CASE A: It's a Local File Path if (await File(sharedContent).exists()) { - finalFile = File(sharedContent); + finalFiles.add(File(sharedContent)); } // CASE B: It's a URL else { @@ -48,20 +47,15 @@ class _ShareHandlerPageState extends State<ShareHandlerPage> { if (url.contains('instagram.com')) { // Instagram Logic - // Assuming the service returns a List of file paths - final downloadedPaths = await _instagramService - .downloadInstagramImage(url); - + final downloadedPaths = await _instagramService.downloadInstagramImage(url); if (downloadedPaths != null && downloadedPaths.isNotEmpty) { - finalFile = File(downloadedPaths.first); + finalFiles.addAll(downloadedPaths.map((path) => File(path))); } } else { // Generic Download Logic - // Ensure this service returns the String path of the saved file final savedPath = await _downloadService.downloadAndSaveImage(url); - if (savedPath != null) { - finalFile = File(savedPath); + finalFiles.add(File(savedPath)); } } } else { @@ -70,24 +64,23 @@ class _ShareHandlerPageState extends State<ShareHandlerPage> { } // SUCCESS: Navigate to ShareToMoodboardPage - if (finalFile != null && await finalFile.exists()) { + if (finalFiles.isNotEmpty) { if (mounted) { Navigator.pushReplacement( context, MaterialPageRoute( - builder: (_) => ShareToMoodboardPage(imageFile: finalFile!), + builder: (_) => ShareToMoodboardPage(imageFiles: finalFiles), ), ); } } else { - throw Exception("Could not retrieve image file."); + throw Exception("Could not retrieve any media files."); } } catch (e) { if (mounted) { setState(() { _hasError = true; _errorMessage = e.toString(); - _isProcessing = false; }); } } @@ -97,47 +90,38 @@ class _ShareHandlerPageState extends State<ShareHandlerPage> { Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, - appBar: AppBar(title: const Text("Processing...")), + appBar: AppBar(title: const Text("Processing")), body: Center( - child: - _hasError - ? Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.error_outline, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 16), - Text( - "Error processing media", - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - Text( - _errorMessage ?? "Unknown Error", - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.grey), - ), - const SizedBox(height: 24), - ElevatedButton( - onPressed: () => Navigator.pop(context), - child: const Text("Close"), - ), - ], - ), - ) - : const Column( + child: _hasError + ? Padding( + padding: const EdgeInsets.all(24.0), + child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator(), - SizedBox(height: 20), - Text("Analyzing & Preparing Image..."), + const Icon(Icons.error_outline, color: Colors.red, size: 50), + const SizedBox(height: 16), + Text("Error processing media", + style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 8), + Text(_errorMessage ?? "Unknown Error", + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.grey)), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () => Navigator.pop(context), + child: const Text("Close"), + ), ], ), + ) + : const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 20), + Text("Downloading media..."), + ], + ), ), ); } diff --git a/lib/ui/pages/share_to_moodboard_page.dart b/lib/ui/pages/share_to_moodboard_page.dart @@ -1,14 +1,10 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; - -// Models & Repos import '../../data/models/project_model.dart'; import '../../data/repos/project_repo.dart'; - -// Services import '../../services/project_service.dart'; -import '../../services/image_service.dart'; // <--- CHANGED: Import ImageService +import '../../services/image_service.dart'; // Helper model for UI rendering class ProjectItemViewModel { @@ -23,9 +19,8 @@ class ProjectItemViewModel { } class ShareToMoodboardPage extends StatefulWidget { - final File imageFile; - - const ShareToMoodboardPage({super.key, required this.imageFile}); + final List<File> imageFiles; + const ShareToMoodboardPage({super.key, required this.imageFiles}); @override State<ShareToMoodboardPage> createState() => _ShareToMoodboardPageState(); @@ -34,8 +29,6 @@ class ShareToMoodboardPage extends StatefulWidget { class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> { final ProjectRepo _projectRepo = ProjectRepo(); final ProjectService _projectService = ProjectService(); - - // FIXED: Use ImageService (which handles Moodboard Images) final ImageService _imageService = ImageService(); List<ProjectItemViewModel> _recentViewModels = []; @@ -53,30 +46,24 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> { _loadData(); } - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } - Future<void> _loadData() async { setState(() => _isLoading = true); + try { + final recentItems = await _projectRepo.getRecentProjectsAndEvents(); + final allItems = await _projectRepo.getAllProjectsAndEvents(); - // Fetch raw data - final recentItems = await _projectRepo.getRecentProjectsAndEvents(); - final allItems = await _projectRepo.getAllProjectsAndEvents(); - - // Build view models with parent titles - _recentViewModels = await _buildViewModels(recentItems.take(3).toList()); - _allViewModels = await _buildViewModels(allItems); - _filteredViewModels = _allViewModels; + _recentViewModels = await _buildViewModels(recentItems.take(3).toList()); + _allViewModels = await _buildViewModels(allItems); + _filteredViewModels = _allViewModels; - setState(() => _isLoading = false); + if (mounted) setState(() => _isLoading = false); + } catch (e) { + debugPrint("Error loading projects: $e"); + if (mounted) setState(() => _isLoading = false); + } } - Future<List<ProjectItemViewModel>> _buildViewModels( - List<ProjectModel> items, - ) async { + Future<List<ProjectItemViewModel>> _buildViewModels(List<ProjectModel> items) async { final List<ProjectItemViewModel> viewModels = []; for (var item in items) { String? parentTitle; @@ -84,9 +71,7 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> { final parent = await _projectRepo.getProjectById(item.parentId!); parentTitle = parent?.title; } - viewModels.add( - ProjectItemViewModel(item: item, parentTitle: parentTitle), - ); + viewModels.add(ProjectItemViewModel(item: item, parentTitle: parentTitle)); } return viewModels; } @@ -97,16 +82,11 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> { if (query.isEmpty) { _filteredViewModels = _allViewModels; } else { - _filteredViewModels = - _allViewModels.where((vm) { - final titleMatches = vm.title.toLowerCase().contains( - query.toLowerCase(), - ); - final parentMatches = - vm.parentTitle?.toLowerCase().contains(query.toLowerCase()) ?? - false; - return titleMatches || parentMatches; - }).toList(); + _filteredViewModels = _allViewModels.where((vm) { + final titleMatches = vm.title.toLowerCase().contains(query.toLowerCase()); + final parentMatches = vm.parentTitle?.toLowerCase().contains(query.toLowerCase()) ?? false; + return titleMatches || parentMatches; + }).toList(); } }); } @@ -115,30 +95,25 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> { final controller = TextEditingController(); final String? title = await showDialog<String>( context: context, - builder: - (context) => AlertDialog( - title: const Text("New Project"), - content: TextField( - controller: controller, - decoration: const InputDecoration(hintText: "Project Title"), - autofocus: true, - textCapitalization: TextCapitalization.sentences, - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text("Cancel"), - ), - FilledButton( - onPressed: () => Navigator.pop(context, controller.text.trim()), - child: const Text("Create"), - ), - ], + builder: (context) => AlertDialog( + title: const Text("New Project"), + content: TextField( + controller: controller, + decoration: const InputDecoration(hintText: "Project Title"), + autofocus: true, + textCapitalization: TextCapitalization.sentences, + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(context), child: const Text("Cancel")), + FilledButton( + onPressed: () => Navigator.pop(context, controller.text.trim()), + child: const Text("Create"), ), + ], + ), ); if (title != null && title.isNotEmpty) { - // Create the project and save the image to it immediately final newId = await _projectService.createProject(title); await _saveAndExit(newId); } @@ -148,8 +123,8 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> { setState(() => _isSaving = true); try { - // 1. Save Image to Moodboard (Using ImageService) - await _imageService.saveImage(widget.imageFile, projectId); + // 1. Save Images to Moodboard + await _imageService.saveImages(widget.imageFiles, projectId); // 2. Update "Recently Used" status await _projectService.openProject(projectId); @@ -158,24 +133,19 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> { ReceiveSharingIntent.instance.reset(); if (mounted) { + final count = widget.imageFiles.length; ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Saved to Moodboard!"), + SnackBar( + content: Text("Saved $count image${count > 1 ? 's' : ''} to Moodboard!"), backgroundColor: Colors.green, ), ); - Navigator.popUntil( - context, - (route) => route.isFirst, - ); // Go back to home + Navigator.popUntil(context, (route) => route.isFirst); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Error saving: $e"), - backgroundColor: Colors.red, - ), + SnackBar(content: Text("Error saving: $e"), backgroundColor: Colors.red), ); setState(() => _isSaving = false); } @@ -193,10 +163,7 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> { icon: const Icon(Icons.arrow_back, color: Colors.black), onPressed: () => Navigator.pop(context), ), - title: const Text( - "MoodBoards", - style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold), - ), + title: const Text("Select Moodboard", style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold)), actions: [ IconButton( icon: const Icon(Icons.add, color: Colors.black, size: 28), diff --git a/lib/ui/widgets/save_to_board_sheet.dart b/lib/ui/widgets/save_to_board_sheet.dart @@ -1,73 +0,0 @@ -// lib/ui/widgets/save_to_board_sheet.dart - -import 'package:flutter/material.dart'; -import '../../data/repos/board_repo.dart'; -import '../../data/repos/board_image_repo.dart'; - -class SaveToBoardSheet extends StatefulWidget { - final String imageId; - - const SaveToBoardSheet({super.key, required this.imageId}); - - @override - State<SaveToBoardSheet> createState() => _SaveToBoardSheetState(); -} - -class _SaveToBoardSheetState extends State<SaveToBoardSheet> { - final boardRepo = BoardRepository(); - final boardImgRepo = BoardImageRepository(); - - List<Map<String, dynamic>> boards = []; - - @override - void initState() { - loadBoards(); - super.initState(); - } - - Future<void> loadBoards() async { - boards = await boardRepo.getBoards(); - if (mounted) setState(() {}); - } - - @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.all(16), - height: 300, - child: Column( - children: [ - Text("Save to Board", style: TextStyle(fontSize: 20)), - ElevatedButton( - onPressed: () async { - await boardRepo.createBoard("New Board"); - await loadBoards(); - }, - child: Text("Create New Board"), - ), - SizedBox(height: 10), - Expanded( - child: ListView.builder( - itemCount: boards.length, - itemBuilder: (c, i) { - final board = boards[i]; - - return ListTile( - title: Text(board['name']), - onTap: () async { - await boardImgRepo.saveToBoard( - board['id'], - widget.imageId, - ); - if (!context.mounted) return; - Navigator.pop(context); - }, - ); - }, - ), - ) - ], - ), - ); - } -}