creek

The AI Image Editor of 2030
commit 91c3e2d82649101fe46aaad6f2adf114253fabeb
parent 20640be858a358ca9299dd7139c738b54a47488c
Author: maydayv7 <maydayv7@gmail.com>
Date:   Tue,  9 Dec 2025 01:26:53 +0530

Unify file metadata save and fetch

Diffstat:
MDeliverables/README.md | 2++
Mlib/data/models/file_model.dart | 10++++++++++
Mlib/services/file_service.dart | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mlib/ui/pages/canvas_page.dart | 47+++++++++++++++++------------------------------
Mlib/ui/pages/home_page.dart | 122+++++++++++++------------------------------------------------------------------
Mlib/ui/pages/project_file_page.dart | 85+++++++++++++++++++++++++++----------------------------------------------------
Mlib/ui/pages/share_to_file_page.dart | 98+++++++++++--------------------------------------------------------------------
Mlib/ui/widgets/file_card.dart | 41++++++++++++++++++++++++++++++++++++-----
8 files changed, 216 insertions(+), 280 deletions(-)

diff --git a/Deliverables/README.md b/Deliverables/README.md @@ -17,6 +17,8 @@ Optional Creative Artefacts -> https://drive.google.com/file/d/1kSpf-y0LhQCn9-fF # Problem Statement +> **Inter IIT Tech Meet 14.0 - Adobe Mid Prep** + Design and prototype a lightweight, mobile-first AI image editor for 2030 that demonstrates how creative editing can become faster, more intuitive, and energy-efficient on low-compute devices. Your solution should combine design thinking, market research, and AI engineering across three integrated tracks: diff --git a/lib/data/models/file_model.dart b/lib/data/models/file_model.dart @@ -1,5 +1,15 @@ import 'dart:convert'; +class FileMetadataInfo { + final double width; + final double height; + final String? previewPath; + + FileMetadataInfo({this.width = 0, this.height = 0, this.previewPath}); + + String get dimensions => '${width.toInt()} x ${height.toInt()} px'; +} + class FileModel { final String id; final int projectId; diff --git a/lib/services/file_service.dart b/lib/services/file_service.dart @@ -1,6 +1,8 @@ import 'dart:io'; +import 'dart:convert'; import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid.dart'; +import 'package:image/image.dart' as img; import 'package:creekui/data/repos/file_repo.dart'; import 'package:creekui/data/models/file_model.dart'; @@ -46,8 +48,7 @@ class FileService { return await _repo.getRecentFiles(limit: limit); } - // Retained for backwards compatibility, though generally deprecated for a global file list view. - @Deprecated('Use getAllFiles() or getRecentFiles() for the file list page.') + @Deprecated('Use getAllFiles() or getRecentFiles() instead') Future<List<FileModel>> getFiles(int projectId) async { return await _repo.getFiles(projectId); } @@ -86,4 +87,90 @@ class FileService { Future<void> renameFile(String id, String newName) async { await _repo.updateDetails(id, name: newName); } + + // Saves canvas JSON structure to a file + Future<void> saveCanvasFile({ + required int projectId, + required Map<String, dynamic> canvasData, + String? fileId, + String? fileName, + }) async { + final jsonString = jsonEncode(canvasData); + + if (fileId != null) { + // Update existing file + final fileData = await _repo.getById(fileId); + if (fileData != null) { + final file = File(fileData.filePath); + await file.writeAsString(jsonString); + await _repo.touchFile(fileId); // Update last modified + } + } else { + // Create new file via temporary storage + final directory = await getTemporaryDirectory(); + final tempFile = File( + '${directory.path}/canvas_${DateTime.now().millisecondsSinceEpoch}.json', + ); + await tempFile.writeAsString(jsonString); + + await saveFile( + tempFile, + projectId, + name: fileName ?? "Untitled Canvas", + description: "Editable Canvas Board", + ); + + // Cleanup temp file + if (await tempFile.exists()) await tempFile.delete(); + } + } + + // Unified metadata extraction logic + Future<FileMetadataInfo> getFileMetadata(String filePath) async { + try { + final f = File(filePath); + if (!await f.exists()) return FileMetadataInfo(); + + if (filePath.toLowerCase().endsWith('.json')) { + try { + final content = await f.readAsString(); + final data = jsonDecode(content); + + if (data is Map) { + final width = (data['width'] as num?)?.toDouble() ?? 0; + final height = (data['height'] as num?)?.toDouble() ?? 0; + final previewPath = data['preview_path'] as String?; + + return FileMetadataInfo( + width: width, + height: height, + previewPath: previewPath, + ); + } + } catch (e) { + // ignore: avoid_print + print('Error parsing JSON metadata for $filePath: $e'); + } + } else { + // Attempt to decode image dimensions directly + try { + final bytes = await f.readAsBytes(); + final image = img.decodeImage(bytes); + if (image != null) { + return FileMetadataInfo( + width: image.width.toDouble(), + height: image.height.toDouble(), + previewPath: filePath, + ); + } + } catch (e) { + print('Error decoding image metadata for $filePath: $e'); + } + } + } catch (e) { + print('Error accessing file metadata for $filePath: $e'); + } + + return FileMetadataInfo(); + } } diff --git a/lib/ui/pages/canvas_page.dart b/lib/ui/pages/canvas_page.dart @@ -644,37 +644,20 @@ class _CanvasPageState extends State<CanvasPage> { final jsonList = _elementsToJson(elements); final pathsJson = _paths.map((p) => p.toMap()).toList(); - final saveData = { + final canvasData = { 'elements': jsonList, 'paths': pathsJson, 'width': _canvasSize.width, 'height': _canvasSize.height, 'preview_path': previewPath, }; - final jsonString = jsonEncode(saveData); - final directory = await getTemporaryDirectory(); - final tempFile = File( - '${directory.path}/canvas_temp_${DateTime.now().millisecondsSinceEpoch}.json', + await _fileService.saveCanvasFile( + projectId: widget.projectId, + canvasData: canvasData, + fileId: widget.existingFile?.id, + fileName: widget.existingFile == null ? fileName : null, ); - await tempFile.writeAsString(jsonString); - - if (widget.existingFile != null) { - // Overwrite existing file - final existingFile = File(widget.existingFile!.filePath); - await existingFile.writeAsString(jsonString); - await _fileService.openFile(widget.existingFile!.id); - } else { - // Save as new file - await _fileService.saveFile( - tempFile, - widget.projectId, - name: fileName, - description: "Editable Canvas Board", - ); - } - - if (await tempFile.exists()) await tempFile.delete(); setState(() => _hasUnsavedChanges = false); @@ -698,6 +681,17 @@ class _CanvasPageState extends State<CanvasPage> { Future<void> _loadCanvasFromFile() async { try { + // File Metadata + final meta = await _fileService.getFileMetadata( + widget.existingFile!.filePath, + ); + if (meta.width > 0 && meta.height > 0) { + setState(() { + _canvasSize = Size(meta.width, meta.height); + _hasInitializedView = false; + }); + } + final file = File(widget.existingFile!.filePath); if (await file.exists()) { final jsonString = await file.readAsString(); @@ -714,13 +708,6 @@ class _CanvasPageState extends State<CanvasPage> { } else { _paths = []; } - if (decoded['width'] != null && decoded['height'] != null) { - _canvasSize = Size( - (decoded['width'] as num).toDouble(), - (decoded['height'] as num).toDouble(), - ); - _hasInitializedView = false; - } } else if (decoded is List) { // Legacy support elements = _jsonToElements(decoded); diff --git a/lib/ui/pages/home_page.dart b/lib/ui/pages/home_page.dart @@ -9,6 +9,7 @@ import 'package:creekui/data/repos/project_repo.dart'; import 'package:creekui/data/repos/image_repo.dart'; import 'package:creekui/data/repos/file_repo.dart'; import 'package:creekui/services/project_service.dart'; +import 'package:creekui/services/file_service.dart'; import 'package:creekui/ui/styles/variables.dart'; import 'package:creekui/ui/widgets/search_bar.dart'; import 'package:creekui/ui/widgets/file_card.dart'; @@ -34,6 +35,7 @@ class _HomePageState extends State<HomePage> { final _imageRepo = ImageRepo(); final _fileRepo = FileRepo(); final _projectService = ProjectService(); + final _fileService = FileService(); final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; @@ -80,44 +82,11 @@ class _HomePageState extends State<HomePage> { final Map<String, Map<String, String>> fileMetadata = {}; for (final file in recentFiles) { - try { - final f = File(file.filePath); - if (await f.exists()) { - if (file.filePath.toLowerCase().endsWith('.json')) { - try { - final content = await f.readAsString(); - final data = jsonDecode(content); - String dims = 'Unknown'; - String? previewPath; - - if (data is Map) { - if (data['width'] != null && data['height'] != null) { - dims = - '${(data['width'] as num).toInt()} x ${(data['height'] as num).toInt()} px'; - } - if (data['preview_path'] != null) { - previewPath = data['preview_path']; - } - } - - fileMetadata[file.id] = { - 'dimensions': dims, - 'preview': previewPath ?? '', - }; - } catch (e) { - debugPrint('Error parsing JSON for ${file.id}: $e'); - } - } else { - final dimensions = await _getImageDimensions(file.filePath); - fileMetadata[file.id] = { - 'dimensions': dimensions, - 'preview': file.filePath, - }; - } - } - } catch (e) { - debugPrint('Error loading metadata for ${file.id}: $e'); - } + final meta = await _fileService.getFileMetadata(file.filePath); + fileMetadata[file.id] = { + 'dimensions': meta.dimensions, + 'preview': meta.previewPath ?? '', + }; } if (mounted) { @@ -136,17 +105,6 @@ class _HomePageState extends State<HomePage> { } } - Future<String> _getImageDimensions(String filePath) async { - try { - final bytes = await File(filePath).readAsBytes(); - final image = img.decodeImage(bytes); - if (image != null) return '${image.width} x ${image.height} px'; - } catch (e) { - debugPrint('Error getting dimensions: $e'); - } - return 'Unknown'; - } - String _formatTimeAgo(DateTime dateTime) { final now = DateTime.now(); final difference = now.difference(dateTime); @@ -266,29 +224,15 @@ class _HomePageState extends State<HomePage> { } Future<void> _openFile(FileModel file) async { + // Default values double width = 1080; double height = 1920; - try { - final f = File(file.filePath); - if (await f.exists()) { - if (file.filePath.toLowerCase().endsWith('.json')) { - final content = await f.readAsString(); - final data = jsonDecode(content); - if (data is Map) { - width = (data['width'] as num?)?.toDouble() ?? width; - height = (data['height'] as num?)?.toDouble() ?? height; - } - } else { - final bytes = await f.readAsBytes(); - final image = img.decodeImage(bytes); - if (image != null) { - width = image.width.toDouble(); - height = image.height.toDouble(); - } - } - } - } catch (e) { - debugPrint("Error detecting dimensions for open: $e"); + + // File Metadata + final meta = await _fileService.getFileMetadata(file.filePath); + if (meta.width > 0 && meta.height > 0) { + width = meta.width; + height = meta.height; } if (mounted) { @@ -779,6 +723,7 @@ class _SeeAllPage extends StatefulWidget { } class _SeeAllPageState extends State<_SeeAllPage> { + final _fileService = FileService(); bool _isLoading = true; List<dynamic> _items = []; Map<int, ProjectModel> _projectMap = {}; @@ -816,38 +761,11 @@ class _SeeAllPageState extends State<_SeeAllPage> { final allProjects = await widget.projectRepo.getAllProjects(); _projectMap = {for (var p in allProjects) p.id!: p}; for (final file in files) { - try { - final f = File(file.filePath); - if (await f.exists()) { - if (file.filePath.toLowerCase().endsWith('.json')) { - try { - final content = await f.readAsString(); - final data = jsonDecode(content); - String dims = 'Unknown'; - String? previewPath; - if (data is Map) { - if (data['width'] != null && data['height'] != null) - dims = - '${(data['width'] as num).toInt()} x ${(data['height'] as num).toInt()} px'; - if (data['preview_path'] != null) - previewPath = data['preview_path']; - } - _fileMetadata[file.id] = { - 'dimensions': dims, - 'preview': previewPath ?? '', - }; - } catch (_) {} - } else { - final bytes = await f.readAsBytes(); - final image = img.decodeImage(bytes); - if (image != null) - _fileMetadata[file.id] = { - 'dimensions': '${image.width} x ${image.height} px', - 'preview': file.filePath, - }; - } - } - } catch (_) {} + final meta = await _fileService.getFileMetadata(file.filePath); + _fileMetadata[file.id] = { + 'dimensions': meta.dimensions, + 'preview': meta.previewPath ?? '', + }; } _items = files; } diff --git a/lib/ui/pages/project_file_page.dart b/lib/ui/pages/project_file_page.dart @@ -80,49 +80,11 @@ class _ProjectFilePageState extends State<ProjectFilePage> { // File Metadata Future<void> _loadMetadata(List<FileModel> list) async { for (final file in list) { - try { - final f = File(file.filePath); - if (!await f.exists()) continue; - - if (file.filePath.toLowerCase().endsWith(".json")) { - final content = await f.readAsString(); - final data = jsonDecode(content); - - String dims = "Unknown"; - String preview = ""; - - if (data is Map) { - if (data["width"] != null && data["height"] != null) { - dims = "${data["width"]} x ${data["height"]} px"; - } - if (data["preview_path"] != null) { - preview = data["preview_path"]; - } - } - - // Fix relative preview path - if (preview.isNotEmpty && !File(preview).existsSync()) { - final base = f.parent.path; - final candidate = "$base/$preview"; - if (File(candidate).existsSync()) preview = candidate; - } - - _fileMetadata[file.id] = {"preview": preview, "dimensions": dims}; - } else { - final bytes = await f.readAsBytes(); - final decoded = img.decodeImage(bytes); - - String dims = "Unknown"; - if (decoded != null) { - dims = "${decoded.width} x ${decoded.height} px"; - } - - _fileMetadata[file.id] = { - "preview": file.filePath, - "dimensions": dims, - }; - } - } catch (_) {} + final meta = await _fileService.getFileMetadata(file.filePath); + _fileMetadata[file.id] = { + "preview": meta.previewPath ?? "", + "dimensions": meta.dimensions, + }; } } @@ -133,19 +95,30 @@ class _ProjectFilePageState extends State<ProjectFilePage> { setState(() {}); } - void _openFile(FileModel file) { - Navigator.push( - context, - MaterialPageRoute( - builder: - (_) => CanvasPage( - projectId: file.projectId, - width: 1080, - height: 1920, - existingFile: file, - ), - ), - ); + void _openFile(FileModel file) async { + // Default values + double width = 1080; + double height = 1920; + final meta = await _fileService.getFileMetadata(file.filePath); + if (meta.width > 0 && meta.height > 0) { + width = meta.width; + height = meta.height; + } + + if (mounted) { + Navigator.push( + context, + MaterialPageRoute( + builder: + (_) => CanvasPage( + projectId: file.projectId, + width: width, + height: height, + existingFile: file, + ), + ), + ); + } } void _handleFileMenuAction(FileModel file, String action) { diff --git a/lib/ui/pages/share_to_file_page.dart b/lib/ui/pages/share_to_file_page.dart @@ -80,54 +80,11 @@ class _ShareToFilePageState extends State<ShareToFilePage> { Future<void> _loadFileMetadata(List<FileModel> files) async { final Map<String, Map<String, String>> meta = {}; for (final fmodel in files) { - try { - final f = File(fmodel.filePath); - if (!await f.exists()) continue; - - if (fmodel.filePath.toLowerCase().endsWith('.json')) { - // File JSON - try { - final content = await f.readAsString(); - final data = jsonDecode(content); - String preview = ''; - String dims = 'Unknown'; - - if (data is Map) { - if (data['preview_path'] != null && - data['preview_path'].toString().isNotEmpty) { - preview = data['preview_path'].toString(); - // If preview is relative, try to resolve relative to JSON file - if (!File(preview).existsSync()) { - final parentDir = f.parent.path; - final candidate = File('$parentDir/$preview'); - if (candidate.existsSync()) preview = candidate.path; - } - } - if (data['width'] != null && data['height'] != null) { - final w = (data['width'] as num).toInt(); - final h = (data['height'] as num).toInt(); - dims = '$w x $h px'; - } - } - meta[fmodel.id] = {'preview': preview, 'dimensions': dims}; - } catch (e) { - debugPrint('Error parsing canvas json for ${fmodel.id}: $e'); - } - } else { - // Regular image file - assign path and try - String dims = 'Unknown'; - try { - final bytes = await f.readAsBytes(); - final image = img.decodeImage(bytes); - if (image != null) dims = '${image.width} x ${image.height} px'; - } catch (_) { - // ignore - } - meta[fmodel.id] = {'preview': fmodel.filePath, 'dimensions': dims}; - } - } catch (e) { - debugPrint('Error while loading metadata for ${fmodel.id}: $e'); - } + final info = await _fileService.getFileMetadata(fmodel.filePath); + meta[fmodel.id] = { + 'preview': info.previewPath ?? '', + 'dimensions': info.dimensions, + }; } _fileMetadata = meta; } @@ -150,7 +107,7 @@ class _ShareToFilePageState extends State<ShareToFilePage> { }); } - // Synchronous breadcrumb using cached project; returns empty if not cached + // Synchronous breadcrumb using cached project (returns empty if not cached) String _getProjectBreadcrumbSync(FileModel file) { final proj = _projectCache[file.projectId]; if (proj == null) return ''; @@ -212,19 +169,15 @@ class _ShareToFilePageState extends State<ShareToFilePage> { return; } + // Default values double width = 1080; double height = 1080; - if (file.filePath.toLowerCase().endsWith('.json')) { - final content = await f.readAsString(); - final data = jsonDecode(content); - - if (data is Map) { - if (data['width'] != null && data['height'] != null) { - width = (data['width'] as num).toDouble(); - height = (data['height'] as num).toDouble(); - } - } + // File Metadata + final meta = await _fileService.getFileMetadata(file.filePath); + if (meta.width > 0 && meta.height > 0) { + width = meta.width; + height = meta.height; } Navigator.push( @@ -253,31 +206,6 @@ class _ShareToFilePageState extends State<ShareToFilePage> { return '${date.day}/${date.month}/${date.year}'; } - // Fallback to original filePath if preview path empty/missing - String _resolvePreviewPath(FileModel file) { - final meta = _fileMetadata[file.id]; - if (meta == null) return file.filePath; - final preview = meta['preview'] ?? ''; - if (preview.isNotEmpty && File(preview).existsSync()) return preview; - - if (file.filePath.toLowerCase().endsWith('.json')) { - final f = File(file.filePath); - final base = f.uri.pathSegments.last; - final nameWithoutExt = base.split('.').first; - final parent = f.parent; - final candidates = [ - '${parent.path}/$nameWithoutExt.png', - '${parent.path}/$nameWithoutExt.jpg', - '${parent.path}/preview_$nameWithoutExt.png', - ]; - for (final c in candidates) { - if (File(c).existsSync()) return c; - } - } - if (File(file.filePath).existsSync()) return file.filePath; - return ''; - } - @override Widget build(BuildContext context) { return Scaffold( @@ -410,8 +338,8 @@ class _ShareToFilePageState extends State<ShareToFilePage> { } Widget _buildFileCard(FileModel file) { - final preview = _resolvePreviewPath(file); final meta = _fileMetadata[file.id] ?? {}; + final preview = meta['preview'] ?? ''; final dimensions = meta['dimensions'] ?? 'Unknown'; return FutureBuilder<String>( diff --git a/lib/ui/widgets/file_card.dart b/lib/ui/widgets/file_card.dart @@ -114,17 +114,48 @@ class FileCard extends StatelessWidget { (BuildContext context) => <PopupMenuEntry<String>>[ const PopupMenuItem<String>( value: 'open', - child: Text('Open'), + child: Row( + children: [ + Icon(Icons.file_open_outlined, size: 18), + SizedBox(width: 12), + Text( + 'Open', + style: TextStyle(fontFamily: 'GeneralSans'), + ), + ], + ), ), const PopupMenuItem<String>( value: 'rename', - child: Text('Rename'), + child: Row( + children: [ + Icon(Icons.edit_outlined, size: 18), + SizedBox(width: 12), + Text( + 'Rename', + style: TextStyle(fontFamily: 'GeneralSans'), + ), + ], + ), ), const PopupMenuItem<String>( value: 'delete', - child: Text( - 'Delete', - style: TextStyle(color: Colors.red), + child: Row( + children: [ + Icon( + Icons.delete_outline, + size: 18, + color: Colors.red, + ), + SizedBox(width: 12), + Text( + 'Delete', + style: TextStyle( + fontFamily: 'GeneralSans', + color: Colors.red, + ), + ), + ], ), ), ],