creek

The AI Image Editor of 2030
commit 2e1dd4c208e31daaedb7f20e3a66f835dd40ba27
parent 5da521d254c7a51b3257450601856c635af0bb7e
Author: Sanjeebani Parida <83271316+sanjeebani14@users.noreply.github.com>
Date:   Sat, 29 Nov 2025 01:19:42 +0530

Merge pull request #16 from nilotpal-n7/sanjeebani

Sanjeebani
Diffstat:
Mlib/services/image_service.dart | 4++++
Mlib/ui/pages/image_details_page.dart | 39++++++++++++++++++++++++---------------
Mlib/ui/pages/image_save_page.dart | 801++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mlib/ui/pages/project_board_page.dart | 284++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mlib/ui/pages/project_tag_page.dart | 165++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
5 files changed, 686 insertions(+), 607 deletions(-)

diff --git a/lib/services/image_service.dart b/lib/services/image_service.dart @@ -110,4 +110,8 @@ class ImageService { await _repo.deleteImage(id); } } + + Future<List<ImageModel>> getImages(int projectId) async { + return await _repo.getImages(projectId); + } } diff --git a/lib/ui/pages/image_details_page.dart b/lib/ui/pages/image_details_page.dart @@ -1,8 +1,8 @@ import 'dart:io'; -import 'dart:ui' as ui; // Fixed import +import 'dart:ui' as ui; import 'package:flutter/material.dart'; -// --- IMPORTS --- +//services,models import '../../services/image_service.dart'; import '../../services/note_service.dart'; import '../../data/models/note_model.dart'; @@ -47,14 +47,17 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> { // Master List of Tags final List<String> _allAvailableTags = [ + 'Compositions', + 'Subject', 'Fonts', + 'Background', + 'Texture', 'Colours', - 'Everything!', - 'Compositions', - 'Textures', - 'Layout', - 'Dark Mode', - 'Minimal', + 'Material Look', + 'Lighting', + 'Style', + 'Era', + 'Emotion', ]; @override @@ -163,7 +166,7 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> { // --- ADD NOTE INPUT DIALOG --- void _showAddNoteInputDialog() { final TextEditingController newNoteController = TextEditingController(); - String newCategory = 'Typography'; + String newCategory = 'Compositions'; showModalBottomSheet( context: context, @@ -208,11 +211,17 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> { ), items: [ - 'Typography', - 'Color Palette', - 'Layout', - 'Design Style', - 'General', + 'Compositions', + 'Subject', + 'Fonts', + 'Background', + 'Texture', + 'Colours', + 'Material Look', + 'Lighting', + 'Style', + 'Era', + 'Emotion', ] .map( (c) => @@ -592,7 +601,7 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> { BorderRadius.circular(20), ), child: const Text( - "Drag to add note", + "Drag on image to select area", style: TextStyle( color: Colors.white, fontSize: 12, diff --git a/lib/ui/pages/image_save_page.dart b/lib/ui/pages/image_save_page.dart @@ -1,12 +1,9 @@ import 'dart:io'; -import 'dart:ui'; // Required for PathMetric +import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - -//services import '../../services/image_service.dart'; import '../../services/note_service.dart'; -import '../../services/analyze/image_analyzer.dart'; // --- HELPER CLASS FOR TEMPORARY NOTES --- class TempNote { @@ -51,46 +48,80 @@ class _ImageSavePageState extends State<ImageSavePage> { final NoteService _noteService = NoteService(); // --- STATE --- + late PageController _pageController; int _currentImageIndex = 0; - final Set<String> _selectedTags = {}; - final TextEditingController _commentController = TextEditingController(); - // List to store multiple notes before saving - final List<TempNote> _addedNotes = []; + // Data storage per image + final Map<int, Set<String>> _tagsPerImage = {}; + final Map<int, List<TempNote>> _notesPerImage = {}; + + // CRITICAL: A unique key for EACH image to calculate drawing coordinates correctly + late List<GlobalKey> _imageKeys; - String _selectedCategory = 'Typography'; + final TextEditingController _commentController = TextEditingController(); + String _selectedCategory = 'Compositions'; bool _isSaving = false; - // --- INTERACTION STATE --- + // --- DRAWING STATE --- bool _isDrawMode = false; - final GlobalKey _imageKey = GlobalKey(); - Offset? _startPos; Offset? _currentPos; - Rect? _finalSelectionRect; // Current drawing - Size? _imageRenderSize; // Size of image on screen + Rect? _finalSelectionRect; + + // FIXED: Store render size per image index + final Map<int, Size> _imageRenderSizes = {}; final List<String> _availableTags = [ + 'Compositions', + 'Subject', 'Fonts', + 'Background', + 'Texture', 'Colours', - 'Everything!', - 'Compositions', - 'Textures', - 'Layout', - 'Dark Mode', - 'Minimal', + 'Material Look', + 'Lighting', + 'Style', + 'Era', + 'Emotion', ]; final List<String> _categories = [ - 'Typography', - 'Color Palette', - 'Layout', - 'Design Style', - 'General', + 'Compositions', + 'Subject', + 'Fonts', + 'Background', + 'Texture', + 'Colours', + 'Material Look', + 'Lighting', + 'Style', + 'Era', + 'Emotion', ]; - // --- ACTIONS --- + @override + void initState() { + super.initState(); + _pageController = PageController(); + + // Generate a unique key for every image path + _imageKeys = List.generate(widget.imagePaths.length, (_) => GlobalKey()); + + // Initialize data maps + for (int i = 0; i < widget.imagePaths.length; i++) { + _tagsPerImage[i] = {}; + _notesPerImage[i] = []; + } + } + @override + void dispose() { + _pageController.dispose(); + _commentController.dispose(); + super.dispose(); + } + + // --- ACTIONS --- void _activateSelectionMode() { setState(() { _isDrawMode = true; @@ -102,41 +133,65 @@ class _ImageSavePageState extends State<ImageSavePage> { void _toggleTag(String tag) { setState(() { - if (_selectedTags.contains(tag)) { - _selectedTags.remove(tag); + final currentTags = _tagsPerImage[_currentImageIndex]!; + if (currentTags.contains(tag)) { + currentTags.remove(tag); } else { - _selectedTags.add(tag); + currentTags.add(tag); } }); } - // --- GESTURES --- + // --- DRAWING GESTURES (FIXED LOGIC) --- + // Helper: Convert global screen touch to local image coordinates + Offset? _getLocalPosition(Offset globalPosition) { + // Get the key for the currently visible image + final currentKey = _imageKeys[_currentImageIndex]; + final RenderBox? box = + currentKey.currentContext?.findRenderObject() as RenderBox?; + if (box == null) return null; + + // FIXED: Store the render size for this specific image + _imageRenderSizes[_currentImageIndex] = box.size; + + // Convert global to local + final local = box.globalToLocal(globalPosition); + + // Clamp coordinates to ensure we don't draw outside the image + final dx = local.dx.clamp(0.0, box.size.width); + final dy = local.dy.clamp(0.0, box.size.height); + + return Offset(dx, dy); + } void _onPanStart(DragStartDetails details) { if (!_isDrawMode) return; + + final pos = _getLocalPosition(details.globalPosition); + if (pos == null) return; + setState(() { - _startPos = details.localPosition; - _currentPos = details.localPosition; + _startPos = pos; + _currentPos = pos; }); } void _onPanUpdate(DragUpdateDetails details) { if (!_isDrawMode) return; + + final pos = _getLocalPosition(details.globalPosition); + if (pos == null) return; + setState(() { - _currentPos = details.localPosition; + _currentPos = pos; }); } void _onPanEnd(DragEndDetails details) { if (!_isDrawMode || _startPos == null || _currentPos == null) return; + // Create the rect from the corrected local positions final rect = Rect.fromPoints(_startPos!, _currentPos!); - final RenderBox? box = - _imageKey.currentContext?.findRenderObject() as RenderBox?; - if (box != null) { - _imageRenderSize = box.size; - } - _finalSelectionRect = rect; setState(() { @@ -148,11 +203,9 @@ class _ImageSavePageState extends State<ImageSavePage> { _showNoteModal(); } - // --- MODAL --- + // --- ADD NOTE MODAL --- void _showNoteModal() { - // Clear controller for new note _commentController.clear(); - showModalBottomSheet( context: context, isScrollControlled: true, @@ -173,108 +226,65 @@ class _ImageSavePageState extends State<ImageSavePage> { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Center( - child: Container( - width: 40, - height: 4, - margin: const EdgeInsets.only(bottom: 20), - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(2), - ), - ), + const Text( + "Add Note", + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), ), - Row( - children: [ - const CircleAvatar( - radius: 16, - backgroundColor: Colors.black12, - child: Icon(Icons.person, color: Colors.grey), + const SizedBox(height: 16), + // Category Dropdown + DropdownButtonFormField<String>( + value: _selectedCategory, + decoration: InputDecoration( + labelText: "Category", + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), ), - const SizedBox(width: 10), - const Text( - "You", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, ), - const Spacer(), - - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 4, - ), - decoration: BoxDecoration( - color: const Color(0xFFE8EAF6), - borderRadius: BorderRadius.circular(20), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton<String>( - value: _selectedCategory, - isDense: true, - icon: const Icon( - Icons.keyboard_arrow_down, - size: 18, - ), - style: const TextStyle( - color: Colors.black87, - fontWeight: FontWeight.w500, - fontSize: 13, - ), - items: - _categories - .map( - (c) => DropdownMenuItem( - value: c, - child: Text(c), - ), - ) - .toList(), - onChanged: - (v) => setState(() => _selectedCategory = v!), - ), - ), + ), + items: + _categories + .map( + (c) => DropdownMenuItem(value: c, child: Text(c)), + ) + .toList(), + onChanged: (v) => setState(() => _selectedCategory = v!), + ), + const SizedBox(height: 12), + // Note Text + TextField( + controller: _commentController, + autofocus: true, + maxLines: 2, + decoration: InputDecoration( + hintText: "Enter details...", + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), ), - ], + ), ), const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(30), - ), - child: TextField( - controller: _commentController, - autofocus: true, - style: const TextStyle(fontSize: 14), - decoration: const InputDecoration( - hintText: "Add your note...", - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric( - vertical: 12, - ), - ), - ), + SizedBox( + width: double.infinity, + height: 50, + child: FilledButton( + style: FilledButton.styleFrom( + backgroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), ), - const SizedBox(width: 8), - // ADD NOTE BUTTON - IconButton( - onPressed: () { - // 1. Add to Temp List - _addTempNote(); - Navigator.pop(context); - }, - icon: const Icon(Icons.check_circle, size: 30), - color: Colors.black, + onPressed: () { + _addTempNote(); + Navigator.pop(context); + }, + child: const Text( + "Save Note", + style: TextStyle(fontWeight: FontWeight.bold), ), - ], + ), ), ], ), @@ -285,16 +295,18 @@ class _ImageSavePageState extends State<ImageSavePage> { ); } - // --- LOGIC TO ADD TEMP NOTE --- void _addTempNote() { + // FIXED: Use the stored render size for the current image index + final imageSize = _imageRenderSizes[_currentImageIndex]; + if (_finalSelectionRect != null && - _imageRenderSize != null && + imageSize != null && _commentController.text.isNotEmpty) { - // Calculate Normalized Coords - final nX = _finalSelectionRect!.center.dx / _imageRenderSize!.width; - final nY = _finalSelectionRect!.center.dy / _imageRenderSize!.height; - final nW = _finalSelectionRect!.width / _imageRenderSize!.width; - final nH = _finalSelectionRect!.height / _imageRenderSize!.height; + // Calculate Normalized Coordinates (0.0 - 1.0) + final nX = _finalSelectionRect!.center.dx / imageSize.width; + final nY = _finalSelectionRect!.center.dy / imageSize.height; + final nW = _finalSelectionRect!.width / imageSize.width; + final nH = _finalSelectionRect!.height / imageSize.height; final newNote = TempNote( normX: nX, @@ -306,30 +318,33 @@ class _ImageSavePageState extends State<ImageSavePage> { ); setState(() { - _addedNotes.add(newNote); + _notesPerImage[_currentImageIndex]?.add(newNote); }); } } - // --- SAVE FINAL (ALL NOTES) --- + // --- FINAL SAVE --- Future<void> _onSaveToMoodboard() async { setState(() => _isSaving = true); try { - for (String path in widget.imagePaths) { + for (int i = 0; i < widget.imagePaths.length; i++) { + String path = widget.imagePaths[i]; final file = File(path); if (!file.existsSync()) continue; - // 1. Save Image (Once) + // 1. Save Image final imageId = await _imageService.saveImage(file, widget.projectId); - // 2. Save Tags - if (_selectedTags.isNotEmpty) { - await _imageService.updateTags(imageId, _selectedTags.toList()); + // 2. Save Tags for this specific image + final tags = _tagsPerImage[i] ?? {}; + if (tags.isNotEmpty) { + await _imageService.updateTags(imageId, tags.toList()); } - // 3. Save ALL Added Notes - for (var note in _addedNotes) { + // 3. Save Notes for this specific image + final notes = _notesPerImage[i] ?? []; + for (var note in notes) { await _noteService.addNote( imageId, note.content, @@ -340,22 +355,21 @@ class _ImageSavePageState extends State<ImageSavePage> { normHeight: note.normHeight, ); } - - // 4. Analyze - _analyzeInBackground(imageId, file.path); } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Saved to ${widget.projectName}!'), + const SnackBar( + content: Text('All images saved successfully!'), backgroundColor: Colors.green, ), ); - if (widget.isFromShare) + + if (widget.isFromShare) { SystemNavigator.pop(); - else + } else { Navigator.popUntil(context, (route) => route.isFirst); + } } } catch (e) { if (mounted) { @@ -367,17 +381,10 @@ class _ImageSavePageState extends State<ImageSavePage> { } } - Future<void> _analyzeInBackground(String imageId, String originalPath) async { - try { - final result = await ImageAnalyzerService.analyzeFullSuite(originalPath); - await _imageService.updateAnalysis(imageId, result); - } catch (e) { - debugPrint("Analysis Error: $e"); - } - } - @override Widget build(BuildContext context) { + final currentTags = _tagsPerImage[_currentImageIndex] ?? {}; + return Scaffold( backgroundColor: Colors.white, appBar: AppBar( @@ -399,164 +406,198 @@ class _ImageSavePageState extends State<ImageSavePage> { fontWeight: FontWeight.w600, ), ), - centerTitle: false, - titleSpacing: 0, ), body: Column( children: [ - // --- IMAGE AREA --- + // --- HORIZONTAL IMAGE CAROUSEL --- Expanded( child: Container( width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), + color: Colors.black, borderRadius: BorderRadius.circular(16), ), child: ClipRRect( borderRadius: BorderRadius.circular(16), - child: LayoutBuilder( - // Needed to position dots accurately - builder: (context, constraints) { - return Stack( - fit: StackFit.expand, - children: [ - InteractiveViewer( - panEnabled: !_isDrawMode, - scaleEnabled: !_isDrawMode, - child: Center( - child: GestureDetector( - onPanStart: _isDrawMode ? _onPanStart : null, - onPanUpdate: _isDrawMode ? _onPanUpdate : null, - onPanEnd: _isDrawMode ? _onPanEnd : null, - child: Stack( - children: [ - Image.file( - File(widget.imagePaths[_currentImageIndex]), - key: _imageKey, - fit: BoxFit.contain, + child: Stack( + fit: StackFit.expand, + children: [ + PageView.builder( + controller: _pageController, + itemCount: widget.imagePaths.length, + onPageChanged: (index) { + setState(() { + _currentImageIndex = index; + _isDrawMode = false; + _finalSelectionRect = null; + }); + }, + itemBuilder: (context, index) { + return LayoutBuilder( + builder: (context, constraints) { + return Stack( + fit: StackFit.expand, + children: [ + InteractiveViewer( + panEnabled: !_isDrawMode, + scaleEnabled: !_isDrawMode, + child: Center( + child: GestureDetector( + onPanStart: + _isDrawMode ? _onPanStart : null, + onPanUpdate: + _isDrawMode ? _onPanUpdate : null, + onPanEnd: _isDrawMode ? _onPanEnd : null, + child: Stack( + children: [ + // THE IMAGE WITH UNIQUE KEY + Image.file( + File(widget.imagePaths[index]), + key: _imageKeys[index], + fit: BoxFit.contain, + width: double.infinity, + ), + // DRAWING OVERLAY (Only if drawing on THIS page) + if (_isDrawMode && + index == _currentImageIndex && + _startPos != null && + _currentPos != null) + Positioned.fill( + child: CustomPaint( + painter: + SelectionOverlayPainter( + rect: Rect.fromPoints( + _startPos!, + _currentPos!, + ), + ), + ), + ), + ], + ), + ), ), - - // 1. Current Drawing Overlay - if (_isDrawMode && - _startPos != null && - _currentPos != null) - Positioned.fill( - child: CustomPaint( - painter: SelectionOverlayPainter( - rect: Rect.fromPoints( - _startPos!, - _currentPos!, + ), + // EXISTING NOTE INDICATORS (Dots) + ...(_notesPerImage[index] ?? []).map((note) { + return Positioned( + left: + (note.normX * constraints.maxWidth) - + 10, + top: + (note.normY * constraints.maxHeight) - + 10, + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity( + 0.3, + ), + blurRadius: 4, + offset: const Offset(0, 1), ), + ], + border: Border.all( + color: const Color(0xFF7C4DFF), + width: 2, ), ), ), - ], - ), - ), - ), - ), - - // 2. RENDER PERSISTENT DOTS FOR ADDED NOTES - ..._addedNotes.map((note) { - return Positioned( - left: (note.normX * constraints.maxWidth) - 10, - top: (note.normY * constraints.maxHeight) - 10, - child: Container( - width: 20, - height: 20, + ); + }).toList(), + ], + ); + }, + ); + }, + ), + // PAGE DOTS + if (widget.imagePaths.length > 1) + Positioned( + bottom: 12, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + widget.imagePaths.length, + (index) => Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + width: 8, + height: 8, decoration: BoxDecoration( - color: Colors.white, shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.3), - blurRadius: 4, - offset: const Offset(0, 1), - ), - ], - // Removed borders, just pure white dots per request + color: + _currentImageIndex == index + ? Colors.blue + : Colors.white.withOpacity(0.5), ), ), - ); - }).toList(), - - // 3. Notes Button - Positioned( - bottom: 12, // Moved closer to corner (was 16) - right: 12, // Moved closer to corner (was 16) - child: ElevatedButton( - onPressed: _activateSelectionMode, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: Colors.black, - elevation: 2, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - side: BorderSide(color: Colors.grey[200]!), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: const [ - Text( - "Notes", - style: TextStyle(fontWeight: FontWeight.w600), - ), - SizedBox(width: 8), - Icon(Icons.assignment_outlined, size: 18), - ], - ), ), ), - - // 4. "TOP" INSTRUCTION POP UP - if (_isDrawMode && _startPos == null) - Positioned( - top: 20, - left: 0, - right: 0, - child: Center( - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - decoration: BoxDecoration( - color: Colors.black87, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: const Text( - "Drag on image to select area", - style: TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), + ), + // NOTES BUTTON + Positioned( + bottom: 24, + right: 12, + child: ElevatedButton.icon( + onPressed: _activateSelectionMode, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + icon: const Icon(Icons.assignment_outlined, size: 18), + label: const Text( + "Notes", + style: TextStyle(fontWeight: FontWeight.w600), + ), + ), + ), + // INSTRUCTION OVERLAY + if (_isDrawMode && _startPos == null) + Positioned( + top: 20, + left: 0, + right: 0, + child: Center( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.black87, + borderRadius: BorderRadius.circular(20), + ), + child: const Text( + "Drag on image to select area", + style: TextStyle( + color: Colors.white, + fontSize: 12, ), ), ), - ], - ); - }, + ), + ), + ], ), ), ), ), - - // --- BOTTOM FORM AREA --- + // --- BOTTOM FORM (TAGS & SAVE) --- Container( width: double.infinity, padding: const EdgeInsets.all(24), @@ -574,107 +615,79 @@ class _ImageSavePageState extends State<ImageSavePage> { ], ), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - // --- THE BOX WRAPPER --- - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.grey[300]!, width: 1), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'What do you like about this image?', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Tags', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, ), - const SizedBox(height: 12), - - const Divider(height: 1, color: Color(0xFFEEEEEE)), - - const SizedBox(height: 16), - - // Tags - Wrap( - spacing: 8, - runSpacing: 8, - children: - _availableTags.map((tag) { - final isSelected = _selectedTags.contains(tag); - return GestureDetector( - onTap: () => _toggleTag(tag), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 8, - ), - decoration: BoxDecoration( - color: - isSelected - ? const Color(0xFFEEF0FF) - : Colors.white, - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: - isSelected - ? const Color(0xFF7C4DFF) - : Colors.grey[400]!, - width: 1.0, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (isSelected) ...[ - const Icon( - Icons.close, - size: 14, - color: Color(0xFF7C4DFF), - ), - const SizedBox(width: 6), - ], - Text( - tag, - style: TextStyle( - fontSize: 13, - fontWeight: - isSelected - ? FontWeight.w600 - : FontWeight.normal, - color: - isSelected - ? const Color(0xFF7C4DFF) - : Colors.black87, - ), - ), - ], - ), - ), - ); - }).toList(), + ), + if (widget.imagePaths.length > 1) + Text( + "Image ${_currentImageIndex + 1}/${widget.imagePaths.length}", + style: TextStyle(color: Colors.grey[600], fontSize: 12), ), - ], - ), + ], + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: + _availableTags.map((tag) { + final isSelected = currentTags.contains(tag); + return GestureDetector( + onTap: () => _toggleTag(tag), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 8, + ), + decoration: BoxDecoration( + color: + isSelected + ? const Color(0xFFEEF0FF) + : Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: + isSelected + ? const Color(0xFF7C4DFF) + : Colors.grey[300]!, + ), + ), + child: Text( + tag, + style: TextStyle( + fontSize: 13, + fontWeight: + isSelected + ? FontWeight.w600 + : FontWeight.normal, + color: + isSelected + ? const Color(0xFF7C4DFF) + : Colors.black87, + ), + ), + ), + ); + }).toList(), ), - const SizedBox(height: 24), - - // Save Button SizedBox( width: double.infinity, height: 54, child: ElevatedButton( onPressed: _isSaving ? null : _onSaveToMoodboard, style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF212121), + backgroundColor: Colors.black, foregroundColor: Colors.white, - elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(27), ), @@ -689,16 +702,15 @@ class _ImageSavePageState extends State<ImageSavePage> { strokeWidth: 2, ), ) - : const Text( - 'Save to Moodboard', - style: TextStyle( + : Text( + 'Save ${widget.imagePaths.length > 1 ? "All" : ""} to Moodboard', + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), ), ), - const SizedBox(height: 8), ], ), ), @@ -708,6 +720,7 @@ class _ImageSavePageState extends State<ImageSavePage> { } } +// --- OVERLAY PAINTER --- class SelectionOverlayPainter extends CustomPainter { final Rect rect; @@ -717,11 +730,9 @@ class SelectionOverlayPainter extends CustomPainter { void paint(Canvas canvas, Size size) { final Path backgroundPath = Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height)); - final Path holePath = Path()..addRect(rect); - final Path overlayPath = Path.combine( - PathOperation.difference, + ui.PathOperation.difference, backgroundPath, holePath, ); @@ -738,7 +749,7 @@ class SelectionOverlayPainter extends CustomPainter { double dashSpace = 4; Path borderPath = Path()..addRect(rect); - for (PathMetric pathMetric in borderPath.computeMetrics()) { + for (ui.PathMetric pathMetric in borderPath.computeMetrics()) { double distance = 0.0; while (distance < pathMetric.length) { canvas.drawPath( @@ -753,6 +764,7 @@ class SelectionOverlayPainter extends CustomPainter { Paint() ..color = Colors.white ..style = PaintingStyle.fill; + canvas.drawCircle( rect.center, 8, @@ -764,7 +776,6 @@ class SelectionOverlayPainter extends CustomPainter { } @override - bool shouldRepaint(covariant SelectionOverlayPainter oldDelegate) { - return rect != oldDelegate.rect; - } + bool shouldRepaint(covariant SelectionOverlayPainter oldDelegate) => + rect != oldDelegate.rect; } diff --git a/lib/ui/pages/project_board_page.dart b/lib/ui/pages/project_board_page.dart @@ -35,10 +35,10 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> { Future<void> _initData() async { final events = await _projectRepo.getEvents(widget.projectId); - + _mainProject = ProjectModel( id: widget.projectId, - title: "Main Project", + title: "Main Project", lastAccessedAt: DateTime.now(), createdAt: DateTime.now(), ); @@ -51,7 +51,7 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> { Future<void> _loadImagesForSelected() async { if (_selectedProject?.id == null) return; - + setState(() => _isLoading = true); final images = await _imageRepo.getImages(_selectedProject!.id!); @@ -62,7 +62,7 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> { void _categorizeImages(List<ImageModel> images) { _categorizedImages.clear(); - + for (var img in images) { if (img.tags.isEmpty) { if (!_categorizedImages.containsKey('Uncategorized')) { @@ -89,12 +89,32 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> { } } + // Navigate to image details and refresh on return + Future<void> _navigateToImageDetails(ImageModel image) async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: + (_) => ImageDetailsPage( + imagePath: image.filePath, + imageId: image.id, + projectId: widget.projectId, + ), + ), + ); + + // Refresh the board when returning from image details + // The result can be anything or null - we always refresh + await _loadImagesForSelected(); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; - final buttonColor = isDark ? Colors.white.withOpacity(0.1) : Colors.grey[100]; + final buttonColor = + isDark ? Colors.white.withOpacity(0.1) : Colors.grey[100]; final List<DropdownMenuItem<ProjectModel>> dropdownItems = []; if (_mainProject != null) { @@ -145,7 +165,10 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> { children: [ // Controls Row Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), child: Row( children: [ Expanded( @@ -168,7 +191,11 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> { fontWeight: FontWeight.w500, fontSize: 14, ), - icon: Icon(Icons.keyboard_arrow_down, size: 18, color: theme.iconTheme.color), + icon: Icon( + Icons.keyboard_arrow_down, + size: 18, + color: theme.iconTheme.color, + ), dropdownColor: theme.cardColor, ), ), @@ -176,23 +203,23 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> { ), const SizedBox(width: 8), _buildControlIcon( - theme, - buttonColor, + theme, + buttonColor, Icons.palette_outlined, - "Stylesheet", + "Stylesheet", () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Stylesheet")), - ); - } + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text("Stylesheet"))); + }, ), const SizedBox(width: 8), _buildControlIcon( - theme, - buttonColor, + theme, + buttonColor, Icons.tune_outlined, "Filter", - () {} + () {}, ), ], ), @@ -200,96 +227,125 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> { // Main Content Expanded( - child: _isLoading - ? const Center(child: CircularProgressIndicator()) - : _categorizedImages.isEmpty + child: + _isLoading + ? const Center(child: CircularProgressIndicator()) + : _categorizedImages.isEmpty ? Center( - child: Text( - "No images found", - style: TextStyle( - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface.withOpacity(0.6), - ), + child: Text( + "No images found", + style: TextStyle( + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface.withOpacity(0.6), ), - ) + ), + ) : ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - itemCount: _categorizedImages.keys.length, - itemBuilder: (context, index) { - final category = _categorizedImages.keys.elementAt(index); - final images = _categorizedImages[category]!; + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + itemCount: _categorizedImages.keys.length, + itemBuilder: (context, index) { + final category = _categorizedImages.keys.elementAt( + index, + ); + final images = _categorizedImages[category]!; - return GestureDetector( - onTap: () { - if (_selectedProject?.id != null) { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ProjectTagPage( - projectId: _selectedProject!.id!, - tag: category, - ), - ), - ); - } - }, - child: Container( - margin: const EdgeInsets.only(bottom: 16), - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: isDark ? const Color(0xFF1E1E1E) : Colors.white, - border: Border.all(color: Colors.grey.withOpacity(0.3)), - borderRadius: BorderRadius.circular(16), + return GestureDetector( + onTap: () { + if (_selectedProject?.id != null) { + Navigator.push( + context, + MaterialPageRoute( + builder: + (_) => ProjectTagPage( + projectId: _selectedProject!.id!, + tag: category, + ), + ), + ).then((_) => _loadImagesForSelected()); + } + }, + child: Container( + margin: const EdgeInsets.only(bottom: 16), + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: + isDark + ? const Color(0xFF1E1E1E) + : Colors.white, + border: Border.all( + color: Colors.grey.withOpacity(0.3), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Category Header - Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - category.toUpperCase(), - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - letterSpacing: 1.0, - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface, - ), - ), - Icon( - Icons.arrow_forward, - size: 16, - color: theme.colorScheme.onSurface.withOpacity(0.4), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Category Header + Padding( + padding: const EdgeInsets.fromLTRB( + 16, + 12, + 16, + 8, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + category.toUpperCase(), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + letterSpacing: 1.0, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface, ), - ], - ), + ), + Icon( + Icons.arrow_forward, + size: 16, + color: theme.colorScheme.onSurface + .withOpacity(0.4), + ), + ], ), - - // Image List - SizedBox( - height: 140, // Adjusted height since footer is gone - child: ListView.separated( - clipBehavior: Clip.none, - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - scrollDirection: Axis.horizontal, - itemCount: images.length, - separatorBuilder: (_, __) => const SizedBox(width: 8), - itemBuilder: (context, imgIndex) { - final image = images[imgIndex]; - return _buildImageCard(image, theme, isDark); - }, + ), + + // Image List + SizedBox( + height: 140, + child: ListView.separated( + clipBehavior: Clip.none, + padding: const EdgeInsets.fromLTRB( + 16, + 0, + 16, + 16, ), + scrollDirection: Axis.horizontal, + itemCount: images.length, + separatorBuilder: + (_, __) => const SizedBox(width: 8), + itemBuilder: (context, imgIndex) { + final image = images[imgIndex]; + return _buildImageCard( + image, + theme, + isDark, + ); + }, ), - ], - ), + ), + ], ), - ); - }, - ), + ), + ); + }, + ), ), ], ), @@ -297,11 +353,11 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> { } Widget _buildControlIcon( - ThemeData theme, - Color? bgColor, - IconData icon, + ThemeData theme, + Color? bgColor, + IconData icon, String tooltip, - VoidCallback onTap + VoidCallback onTap, ) { return GestureDetector( onTap: onTap, @@ -319,24 +375,7 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> { Widget _buildImageCard(ImageModel image, ThemeData theme, bool isDark) { return GestureDetector( - // <-- Added this wrapper - onTap: () { - // Navigate to Image Details - Navigator.push( - context, - MaterialPageRoute( - builder: - (_) => ImageDetailsPage( - imagePath: image.filePath, - imageId: image.id, - projectId: widget.projectId, - ), - ), - ).then((_) { - // Refresh board when returning (in case tags changed) - _loadImagesForSelected(); - }); - }, + onTap: () => _navigateToImageDetails(image), child: Container( width: 120, clipBehavior: Clip.antiAlias, @@ -349,9 +388,7 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> { fit: StackFit.expand, children: [ Hero( - // Optional: Add Hero for smooth zoom animation - tag: - 'image_${image.id}', // Make sure this matches the tag in ImageDetailsPage + tag: 'image_${image.id}', child: Image.file( File(image.filePath), fit: BoxFit.cover, @@ -367,4 +404,4 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> { ), ); } -} -\ No newline at end of file +} diff --git a/lib/ui/pages/project_tag_page.dart b/lib/ui/pages/project_tag_page.dart @@ -1,24 +1,21 @@ import 'dart:io'; import 'package:flutter/material.dart'; import '../../data/models/image_model.dart'; -import '../../data/repos/image_repo.dart'; +import '../../services/image_service.dart'; +import 'image_details_page.dart'; class ProjectTagPage extends StatefulWidget { final int projectId; final String tag; - const ProjectTagPage({ - super.key, - required this.projectId, - required this.tag, - }); + const ProjectTagPage({super.key, required this.projectId, required this.tag}); @override State<ProjectTagPage> createState() => _ProjectTagPageState(); } class _ProjectTagPageState extends State<ProjectTagPage> { - final _imageRepo = ImageRepo(); + final ImageService _imageService = ImageService(); List<ImageModel> _images = []; bool _isLoading = true; @@ -29,14 +26,15 @@ class _ProjectTagPageState extends State<ProjectTagPage> { } Future<void> _loadImages() async { - final allImages = await _imageRepo.getImages(widget.projectId); + final allImages = await _imageService.getImages(widget.projectId); - final filtered = allImages.where((img) { - if (widget.tag == 'Uncategorized') { - return img.tags.isEmpty; - } - return img.tags.contains(widget.tag); - }).toList(); + final filtered = + allImages.where((img) { + if (widget.tag == 'Uncategorized') { + return img.tags.isEmpty; + } + return img.tags.contains(widget.tag); + }).toList(); if (mounted) { setState(() { @@ -62,75 +60,97 @@ class _ProjectTagPageState extends State<ProjectTagPage> { letterSpacing: 1.0, ), ), - centerTitle: false, // Changed to false for left alignment + centerTitle: false, backgroundColor: theme.appBarTheme.backgroundColor, elevation: 0, ), - body: _isLoading - ? const Center(child: CircularProgressIndicator()) - : _images.isEmpty + body: + _isLoading + ? const Center(child: CircularProgressIndicator()) + : _images.isEmpty ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.image_not_supported_outlined, - size: 64, color: Colors.grey[400]), - const SizedBox(height: 16), - Text( - "No images found for '${widget.tag}'", - style: TextStyle( - fontFamily: 'GeneralSans', - fontSize: 16, - color: Colors.grey[600], - ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.image_not_supported_outlined, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + 'No images found for "${widget.tag}"', + style: TextStyle( + fontFamily: 'GeneralSans', + fontSize: 16, + color: Colors.grey[600], ), - ], - ), - ) + ), + ], + ), + ) : GridView.builder( - padding: const EdgeInsets.all(16), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 12, - mainAxisSpacing: 12, - childAspectRatio: 0.8, - ), - itemCount: _images.length, - itemBuilder: (context, index) { - final image = _images[index]; - return GestureDetector( - onTap: () { - // Navigate to detail view - }, - child: Container( - decoration: BoxDecoration( - color: isDark ? const Color(0xFF1E1E1E) : Colors.grey[200], - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isDark ? Colors.white10 : Colors.transparent, - ), + padding: const EdgeInsets.all(16), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 0.8, + ), + itemCount: _images.length, + itemBuilder: (context, index) { + final image = _images[index]; + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: + (_) => ImageDetailsPage( + imagePath: image.filePath, + imageId: image.id, + projectId: widget.projectId, + ), + ), + ).then((_) => _loadImages()); + }, + child: Container( + decoration: BoxDecoration( + color: + isDark ? const Color(0xFF1E1E1E) : Colors.grey[200], + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isDark ? Colors.white10 : Colors.transparent, ), - clipBehavior: Clip.antiAlias, - child: Stack( - fit: StackFit.expand, - children: [ - Image.file( + ), + clipBehavior: Clip.antiAlias, + child: Stack( + fit: StackFit.expand, + children: [ + Hero( + tag: 'image_${image.id}', + child: Image.file( File(image.filePath), fit: BoxFit.cover, width: double.infinity, - errorBuilder: (_, __, ___) => Container( - color: Colors.grey[300], - child: const Center( - child: Icon(Icons.broken_image, color: Colors.grey), - ), - ), + errorBuilder: + (_, __, ___) => Container( + color: Colors.grey[300], + child: const Center( + child: Icon( + Icons.broken_image, + color: Colors.grey, + ), + ), + ), ), - ], - ), + ), + ], ), - ); - }, - ), + ), + ); + }, + ), ); } -} -\ No newline at end of file +}