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:
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,
+ ),
+ ),
+ ],
),
),
],