creek

The AI Image Editor of 2030
commit bd47c5692b85dcc6b60732da16cf5989cc5fece5
parent 396f82c406eeb2436b26675bdeca2065cf1d42ad
Author: maydayv7 <maydayv7@gmail.com>
Date:   Thu, 27 Nov 2025 17:10:03 +0530

Merge branch 'abhinav'

Diffstat:

Mlib/data/models/image_model.dart | 47++++++++++++++++++++++++++++++++++++++++++++---
Alib/ui/pages/image_details_page.dart | 384+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpubspec.lock | 24++++++++++++++++--------
Mpubspec.yaml | 6+++---
4 files changed, 447 insertions(+), 14 deletions(-)

diff --git a/lib/data/models/image_model.dart b/lib/data/models/image_model.dart @@ -1,19 +1,48 @@ -// lib/data/models/image_model.dart - import 'dart:convert'; class ImageModel { final String id; final String filePath; final String? analysisData; + final String? name; // Optional name + final List<String>? tags; // Optional list of tags - ImageModel({required this.id, required this.filePath, this.analysisData}); + ImageModel({ + required this.id, + required this.filePath, + this.analysisData, + this.name, + this.tags, + }); factory ImageModel.fromMap(Map<String, dynamic> m) { + // Helper to parse tags from JSON string or List + List<String>? parseTags(dynamic tagsData) { + if (tagsData == null) return null; + if (tagsData is List) { + return tagsData.map((e) => e.toString()).toList(); + } + if (tagsData is String) { + try { + // If stored as JSON string ["tag1", "tag2"] + final decoded = json.decode(tagsData); + if (decoded is List) { + return decoded.map((e) => e.toString()).toList(); + } + } catch (e) { + // Fallback: maybe comma separated? + return tagsData.split(',').map((e) => e.trim()).toList(); + } + } + return null; + } + return ImageModel( id: m['id'], filePath: m['filePath'], analysisData: m['analysis_data'], + name: m['name'], + tags: parseTags(m['tags']), ); } @@ -30,4 +59,16 @@ class ImageModel { return null; } } + + // You might want a toMap for saving back to DB if not already present elsewhere + Map<String, dynamic> toMap() { + return { + 'id': id, + 'filePath': filePath, + 'analysis_data': analysisData, + 'name': name, + // Store tags as JSON string in SQLite usually + 'tags': tags != null ? json.encode(tags) : null, + }; + } } diff --git a/lib/ui/pages/image_details_page.dart b/lib/ui/pages/image_details_page.dart @@ -0,0 +1,383 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import '../../data/models/image_model.dart'; +import '../../data/models/comment_model.dart'; +import '../../data/repos/image_repo.dart'; +import '../../data/repos/comment_repo.dart'; +import 'package:intl/intl.dart'; + +class ImageDetailPage extends StatefulWidget { + final String imageId; + + const ImageDetailPage({super.key, required this.imageId}); + + @override + State<ImageDetailPage> createState() => _ImageDetailPageState(); +} + +class _ImageDetailPageState extends State<ImageDetailPage> { + final _imageRepo = ImageRepository(); + final _commentRepo = CommentRepository(); + + ImageModel? _image; + List<Comment> _comments = []; + bool _isLoading = true; + + // Input controllers + final _commentController = TextEditingController(); + CommentType _selectedType = CommentType.layout; // Default type + + @override + void initState() { + super.initState(); + _loadData(); + } + + Future<void> _loadData() async { + setState(() => _isLoading = true); + try { + // 1. Fetch detailed image info (including comments if repo supports it, + // or fetch separately if not fully integrated yet) + final data = await _imageRepo.getImageDetails(widget.imageId); + + if (data != null) { + // Parse Image + _image = ImageModel.fromMap(data); + + // Parse Comments from the joined data + if (data['comments'] != null) { + final commentsList = data['comments'] as List; + _comments = commentsList.map((c) => Comment.fromMap(c)).toList(); + } + } + } catch (e) { + debugPrint("Error loading details: $e"); + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + Future<void> _addComment() async { + if (_commentController.text.trim().isEmpty || _image == null) return; + + final newComment = Comment( + imageId: _image!.id, + content: _commentController.text.trim(), + type: _selectedType, + createdAt: DateTime.now().toIso8601String(), + ); + + try { + await _commentRepo.addComment(newComment); + _commentController.clear(); + // Refresh data to show new comment + await _loadData(); + } catch (e) { + debugPrint("Error adding comment: $e"); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Failed to add comment: $e")), + ); + } + } + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return const Scaffold( + backgroundColor: Colors.white, + body: Center(child: CircularProgressIndicator()), + ); + } + + if (_image == null) { + return Scaffold( + appBar: AppBar(), + body: const Center(child: Text("Image not found")), + ); + } + + return Scaffold( + 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), + ), + actions: [ + IconButton( + icon: const Icon(Icons.share_outlined, color: Colors.black), + onPressed: () { + // Share logic + }, + ), + IconButton( + icon: const Icon(Icons.more_horiz, color: Colors.black), + onPressed: () {}, + ), + ], + ), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 1. IMAGE DISPLAY + Center( + child: Container( + constraints: const BoxConstraints(maxHeight: 500), + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ) + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: Image.file( + File(_image!.filePath), + fit: BoxFit.contain, + errorBuilder: (c, e, s) => Container( + height: 300, + color: Colors.grey[200], + child: const Icon(Icons.broken_image, size: 50, color: Colors.grey), + ), + ), + ), + ), + ), + + const SizedBox(height: 16), + + // 2. INFO (Name & Link) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _image!.name ?? "Untitled Image", + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + fontFamily: 'GeneralSans', + ), + ), + const SizedBox(height: 4), + Text( + "https://www.adobe.com/project/...", // Placeholder or real link if you have it + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + decoration: TextDecoration.underline, + fontFamily: 'GeneralSans', + ), + ), + ], + ), + ), + + const SizedBox(height: 24), + + // 3. COMMENTS SECTION + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + const Text( + "Comments", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + fontFamily: 'GeneralSans', + ), + ), + const Spacer(), + // Analysis / Tag icons + const Icon(Icons.local_offer_outlined, size: 20, color: Colors.black), + const SizedBox(width: 12), + const Icon(Icons.fullscreen, size: 24, color: Colors.black), + ], + ), + ), + + const SizedBox(height: 12), + + if (_comments.isEmpty) + const Padding( + padding: EdgeInsets.all(20.0), + child: Center( + child: Text( + "No comments yet. Add one below!", + style: TextStyle(color: Colors.grey), + ), + ), + ) + else + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 20), + itemCount: _comments.length, + separatorBuilder: (_, __) => const SizedBox(height: 16), + itemBuilder: (context, index) { + return _buildCommentItem(_comments[index]); + }, + ), + + const SizedBox(height: 100), // Space for bottom sheet + ], + ), + ), + ), + ], + ), + // 4. ADD COMMENT INPUT + bottomSheet: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + // Type Selector (Dropdown or similar) + Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton<CommentType>( + value: _selectedType, + icon: const Icon(Icons.arrow_drop_down), + items: CommentType.values.map((type) { + return DropdownMenuItem( + value: type, + child: Text( + type.name.toUpperCase(), + style: const TextStyle(fontSize: 12, fontFamily: 'GeneralSans'), + ), + ); + }).toList(), + onChanged: (val) { + if (val != null) setState(() => _selectedType = val); + }, + ), + ), + ), + const SizedBox(width: 12), + // Text Input + Expanded( + child: TextField( + controller: _commentController, + decoration: const InputDecoration( + hintText: "Add a comment...", + border: InputBorder.none, + ), + ), + ), + // Send Button + IconButton( + icon: const Icon(Icons.send, color: Colors.blue), + onPressed: _addComment, + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildCommentItem(Comment comment) { + final date = DateTime.tryParse(comment.createdAt); + final timeStr = date != null ? DateFormat.jm().format(date) : ""; + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Vertical Line indicator + Container( + width: 4, + height: 40, + decoration: BoxDecoration( + color: Colors.black87, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + // Comment Type Tag (Pill) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(4), + ), + child: Text( + comment.type.name, // e.g. "Layout", "Colour" + style: TextStyle( + fontSize: 12, + color: Colors.grey[700], + fontWeight: FontWeight.w500, + fontFamily: 'GeneralSans', + ), + ), + ), + const SizedBox(width: 8), + // Time + Text( + timeStr, + style: TextStyle( + fontSize: 12, + color: Colors.grey[400], + fontFamily: 'GeneralSans', + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + comment.content, + style: const TextStyle( + fontSize: 14, + color: Colors.black87, + height: 1.4, + fontFamily: 'GeneralSans', + ), + ), + ], + ), + ), + ], + ); + } +} +\ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" boolean_selector: dependency: transitive description: @@ -85,10 +85,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -280,14 +280,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -641,10 +649,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: provider: ^6.1.5+1 image: ^4.5.4 flutter_onnxruntime: ^1.6.0 + intl: ^0.20.2 dev_dependencies: flutter_test: @@ -41,7 +42,7 @@ flutter: - assets/style_embeddings.json - assets/emotion_centroids.json - assets/lighting_centroids.json - + fonts: - family: GeneralSans fonts: @@ -54,4 +55,4 @@ flutter: - asset: assets/fonts/GeneralSans-Medium.otf weight: 500 - asset: assets/fonts/GeneralSans-Semibold.otf - weight: 600 -\ No newline at end of file + weight: 600