creek

The AI Image Editor of 2030
commit 8c99ca2761b4b7abd7b84f826e242da9acd74ace
parent d854e4692eda0ca3fa70bbf835d157ebaa259e52
Author: aditya-samal <samaladitya2004@gmail.com>
Date:   Sun, 30 Nov 2025 15:44:23 +0530

Home Screen Done

Diffstat:
Mlib/data/repos/file_repo.dart | 10++++++++++
Mlib/ui/pages/home_page.dart | 670+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
2 files changed, 426 insertions(+), 254 deletions(-)

diff --git a/lib/data/repos/file_repo.dart b/lib/data/repos/file_repo.dart @@ -69,4 +69,14 @@ class FileRepo { ); return res.map((e) => e['file_path'] as String).toList(); } + + Future<List<FileModel>> getRecentFiles({int limit = 10}) async { + final db = await AppDatabase.db; + final res = await db.query( + 'files', + orderBy: 'last_updated DESC', + limit: limit, + ); + return res.map((e) => FileModel.fromMap(e)).toList(); + } } diff --git a/lib/ui/pages/home_page.dart b/lib/ui/pages/home_page.dart @@ -1,9 +1,13 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:adobe/data/models/project_model.dart'; +import 'package:adobe/data/models/file_model.dart'; import 'package:adobe/data/repos/project_repo.dart'; import 'package:adobe/data/repos/image_repo.dart'; +import 'package:adobe/data/repos/file_repo.dart'; import 'package:adobe/services/project_service.dart'; +import 'package:image/image.dart' as img; +import 'package:intl/intl.dart'; import 'project_detail_page.dart'; import 'image_analysis_page.dart'; import 'define_brand_page.dart'; @@ -18,15 +22,16 @@ class HomePage extends StatefulWidget { class _HomePageState extends State<HomePage> { final _projectRepo = ProjectRepo(); final _imageRepo = ImageRepo(); + final _fileRepo = FileRepo(); final _projectService = ProjectService(); - List<ProjectModel> _recentProjects = []; List<ProjectModel> _allProjects = []; - Map<int, List<String>> _projectPreviews = - {}; // projectId -> list of image paths + List<FileModel> _recentFiles = []; + Map<int, ProjectModel> _projectMap = {}; + Map<int, List<String>> _projectPreviews = {}; + Map<String, String> _fileDimensions = {}; bool _isLoading = true; - final String _userName = - "User"; // Default name, can be loaded from preferences later + final String _userName = "Alex"; // Can be loaded from preferences later @override void initState() { @@ -37,27 +42,39 @@ class _HomePageState extends State<HomePage> { Future<void> _loadData() async { setState(() => _isLoading = true); try { - // Load recent projects (limit to 10 for horizontal scroll) - final recent = await _projectRepo.getRecentProjects(10); - - // Load all projects - final all = await _projectRepo.getAllProjects(); - - // Load preview images for each project - final Map<int, List<String>> previews = {}; - for (final project in all) { + // Load all projects and create a map for quick lookup + final allProjects = await _projectRepo.getAllProjects(); + final Map<int, ProjectModel> projectMap = {}; + for (final project in allProjects) { if (project.id != null) { + projectMap[project.id!] = project; + // Load preview images for each project final images = await _imageRepo.getImages(project.id!); - previews[project.id!] = + _projectPreviews[project.id!] = images.take(4).map((img) => img.filePath).toList(); } } + // Load recent files + final recentFiles = await _fileRepo.getRecentFiles(limit: 10); + + // Load dimensions for files + final Map<String, String> fileDimensions = {}; + for (final file in recentFiles) { + try { + final dimensions = await _getImageDimensions(file.filePath); + fileDimensions[file.id] = dimensions; + } catch (e) { + debugPrint('Error loading dimensions for ${file.id}: $e'); + } + } + if (mounted) { setState(() { - _recentProjects = recent; - _allProjects = all; - _projectPreviews = previews; + _allProjects = allProjects; + _recentFiles = recentFiles; + _projectMap = projectMap; + _fileDimensions = fileDimensions; _isLoading = false; }); } @@ -67,6 +84,48 @@ 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); + + if (difference.inDays > 0) { + return 'Edited ${difference.inDays} ${difference.inDays == 1 ? 'day' : 'days'} ago'; + } else if (difference.inHours > 0) { + return 'Edited ${difference.inHours} ${difference.inHours == 1 ? 'hour' : 'hours'} ago'; + } else if (difference.inMinutes > 0) { + return 'Edited ${difference.inMinutes} ${difference.inMinutes == 1 ? 'minute' : 'minutes'} ago'; + } else { + return 'Edited just now'; + } + } + + String _getProjectBreadcrumb(FileModel file) { + final project = _projectMap[file.projectId]; + if (project == null) return ''; + + // Check if project is an event (has parentId) + if (project.isEvent) { + final parentProject = _projectMap[project.parentId!]; + if (parentProject != null) { + return '${parentProject.title} / ${project.title}'; + } + } + return project.title; + } + Future<void> _createNewProject() async { Navigator.push( context, @@ -97,6 +156,14 @@ class _HomePageState extends State<HomePage> { } } + void _openFile(FileModel file) { + // Navigate to file detail or project detail + final project = _projectMap[file.projectId]; + if (project != null) { + _openProject(project); + } + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -115,51 +182,34 @@ class _HomePageState extends State<HomePage> { // Header Section SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), + padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), child: Row( children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Hi, $_userName", - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w600, - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface, - ), - ), - ], - ), - const Spacer(), - // Image Analysis Button - IconButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const ImageAnalysisPage(), - ), - ); - }, - icon: Icon( - Icons.analytics_outlined, + Text( + "Hello, $_userName!", + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w500, + fontFamily: 'GeneralSans', color: theme.colorScheme.onSurface, ), - tooltip: 'Image Analysis', ), - const SizedBox(width: 8), - // Profile Picture Placeholder + const Spacer(), + // Profile Picture Container( - width: 48, - height: 48, + width: 30, + height: 30, decoration: BoxDecoration( color: theme.colorScheme.primaryContainer, shape: BoxShape.circle, + border: Border.all( + color: theme.scaffoldBackgroundColor, + width: 1.25, + ), ), child: Icon( Icons.person, + size: 16, color: theme.colorScheme.onPrimaryContainer, ), ), @@ -168,119 +218,108 @@ class _HomePageState extends State<HomePage> { ), ), - // Recent Section - if (_recentProjects.isNotEmpty) ...[ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 8, 20, 12), - child: Text( - "Recent", + // Search Bar + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + decoration: BoxDecoration( + color: isDark ? Colors.grey[800] : Colors.grey[200], + borderRadius: BorderRadius.circular(8), + ), + child: TextField( + decoration: InputDecoration( + hintText: 'Search', + hintStyle: TextStyle( + fontSize: 12, + color: theme.colorScheme.onSurface.withOpacity(0.5), + fontFamily: 'GeneralSans', + ), + prefixIcon: Icon( + Icons.search, + size: 18, + color: theme.colorScheme.onSurface.withOpacity(0.5), + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, + fontSize: 12, fontFamily: 'GeneralSans', color: theme.colorScheme.onSurface, ), ), ), ), - SliverToBoxAdapter( - child: SizedBox( - height: 140, - child: ListView.builder( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric( - horizontal: 20, - ), - itemCount: _recentProjects.length, - itemBuilder: (context, index) { - final project = _recentProjects[index]; - return _buildRecentCard(project, theme, isDark); - }, - ), - ), - ), - const SliverToBoxAdapter(child: SizedBox(height: 24)), - ], + ), + + const SliverToBoxAdapter(child: SizedBox(height: 12)), - // Your Project Section + // Content Sections SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.fromLTRB(20, 8, 20, 16), - child: Row( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Your Projects", - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface, + // Recent Files Section + if (_recentFiles.isNotEmpty) ...[ + _buildSectionHeader( + 'Recent Files', + theme, + onTap: () { + // Navigate to recent files page + }, ), + const SizedBox(height: 12), + ...(_recentFiles.take(2).map((file) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildRecentFileCard(file, theme, isDark), + )).toList()), + const SizedBox(height: 24), + ], + + // Projects Section + _buildSectionHeader( + 'Projects', + theme, + onTap: () { + // Navigate to all projects + }, + ), + const SizedBox(height: 12), + SizedBox( + height: 100, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _allProjects.length, + itemBuilder: (context, index) { + final project = _allProjects[index]; + return _buildProjectCard(project, theme, isDark); + }, + ), + ), + const SizedBox(height: 24), + + // Explore Templates Section + _buildSectionHeader( + 'Explore templates', + theme, + onTap: () { + // Navigate to templates + }, ), + const SizedBox(height: 12), + _buildTemplatesSection(theme, isDark), + const SizedBox(height: 24), ], ), ), ), - // Projects Grid - if (_allProjects.isEmpty) - SliverFillRemaining( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.folder_outlined, - size: 64, - color: theme.colorScheme.onSurface - .withOpacity(0.3), - ), - const SizedBox(height: 16), - Text( - "No projects yet", - style: TextStyle( - fontSize: 18, - color: theme.colorScheme.onSurface - .withOpacity(0.6), - fontFamily: 'GeneralSans', - ), - ), - const SizedBox(height: 8), - Text( - "Tap the + button to create your first project", - style: TextStyle( - fontSize: 14, - color: theme.colorScheme.onSurface - .withOpacity(0.5), - fontFamily: 'GeneralSans', - ), - ), - ], - ), - ), - ) - else - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 20), - sliver: SliverGrid( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - childAspectRatio: 0.85, - ), - delegate: SliverChildBuilderDelegate(( - context, - index, - ) { - final project = _allProjects[index]; - return _buildProjectCard(project, theme, isDark); - }, childCount: _allProjects.length), - ), - ), - // Bottom padding const SliverToBoxAdapter(child: SizedBox(height: 100)), ], @@ -289,85 +328,148 @@ class _HomePageState extends State<HomePage> { ), floatingActionButton: FloatingActionButton( onPressed: _createNewProject, - backgroundColor: theme.colorScheme.primary, - foregroundColor: theme.colorScheme.onPrimary, - child: const Icon(Icons.add), + backgroundColor: isDark ? Colors.grey[900] : Colors.grey[900], + foregroundColor: Colors.white, + child: const Icon(Icons.add, size: 24), ), ); } - Widget _buildRecentCard(ProjectModel project, ThemeData theme, bool isDark) { - final previewImages = - project.id != null ? (_projectPreviews[project.id!] ?? []) : []; + Widget _buildSectionHeader(String title, ThemeData theme, {VoidCallback? onTap}) { + return Row( + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface, + ), + ), + const Spacer(), + if (onTap != null) + GestureDetector( + onTap: onTap, + child: Icon( + Icons.chevron_right, + size: 24, + color: theme.colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ); + } + + Widget _buildRecentFileCard(FileModel file, ThemeData theme, bool isDark) { + final breadcrumb = _getProjectBreadcrumb(file); + final dimensions = _fileDimensions[file.id] ?? 'Unknown'; + final timeAgo = _formatTimeAgo(file.lastUpdated); return GestureDetector( - onTap: () => _openProject(project), + onTap: () => _openFile(file), child: Container( - width: 200, - margin: const EdgeInsets.only(right: 12), decoration: BoxDecoration( - color: isDark ? Colors.grey[800] : Colors.grey[100], - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isDark ? Colors.grey[700]! : Colors.grey[300]!, - width: 1, - ), + color: theme.scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(8), ), - child: Column( + child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Preview Image - Expanded( + // Thumbnail + Container( + width: 104, + height: 106, + decoration: BoxDecoration( + color: isDark ? Colors.grey[800] : Colors.grey[200], + borderRadius: BorderRadius.circular(8), + ), child: ClipRRect( - borderRadius: const BorderRadius.vertical( - top: Radius.circular(16), + borderRadius: BorderRadius.circular(8), + child: Image.file( + File(file.filePath), + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => Container( + color: isDark ? Colors.grey[800] : Colors.grey[200], + child: Icon( + Icons.broken_image, + size: 24, + color: theme.colorScheme.onSurface.withOpacity(0.3), + ), + ), ), - child: - previewImages.isEmpty - ? Container( - color: isDark ? Colors.grey[700] : Colors.grey[200], - child: Center( - child: Icon( - Icons.image_outlined, - size: 40, - color: theme.colorScheme.onSurface.withOpacity( - 0.3, - ), - ), - ), - ) - : Image.file( - File(previewImages.first), - fit: BoxFit.cover, - width: double.infinity, - errorBuilder: - (context, error, stackTrace) => Container( - color: - isDark - ? Colors.grey[700] - : Colors.grey[200], - child: Icon( - Icons.broken_image, - color: theme.colorScheme.onSurface - .withOpacity(0.3), + ), + ), + const SizedBox(width: 8), + // File Info + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (breadcrumb.isNotEmpty) + Text( + breadcrumb, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w500, + fontFamily: 'Inter', + color: theme.colorScheme.onSurface.withOpacity(0.6), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), + const SizedBox(height: 4), + Text( + file.name, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), + ], + ), ), - ), - ), - // Project Title - Padding( - padding: const EdgeInsets.all(12), - child: Text( - project.title, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface, + const SizedBox(width: 3), + Icon( + Icons.more_vert, + size: 16, + color: theme.colorScheme.onSurface.withOpacity(0.6), + ), + ], + ), + const SizedBox(height: 8), + Text( + dimensions, + style: TextStyle( + fontSize: 10, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface.withOpacity(0.6), + ), + ), + const SizedBox(height: 2), + Text( + timeAgo, + style: TextStyle( + fontSize: 10, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface.withOpacity(0.4), + ), + ), + ], ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), ), ], @@ -383,9 +485,11 @@ class _HomePageState extends State<HomePage> { return GestureDetector( onTap: () => _openProject(project), child: Container( + width: 156, + margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( - color: isDark ? Colors.grey[800] : Colors.grey[100], - borderRadius: BorderRadius.circular(16), + color: theme.scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(8), border: Border.all( color: isDark ? Colors.grey[700]! : Colors.grey[300]!, width: 1, @@ -394,29 +498,27 @@ class _HomePageState extends State<HomePage> { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Preview Image(s) + // Preview Image Expanded( - flex: 3, child: ClipRRect( borderRadius: const BorderRadius.vertical( - top: Radius.circular(16), + top: Radius.circular(8), ), child: previewImages.isEmpty ? Container( - color: isDark ? Colors.grey[700] : Colors.grey[200], + color: isDark ? Colors.grey[800] : Colors.grey[200], child: Center( child: Icon( Icons.folder_outlined, - size: 48, + size: 32, color: theme.colorScheme.onSurface.withOpacity( 0.3, ), ), ), ) - : previewImages.length == 1 - ? Image.file( + : Image.file( File(previewImages.first), fit: BoxFit.cover, width: double.infinity, @@ -424,7 +526,7 @@ class _HomePageState extends State<HomePage> { (context, error, stackTrace) => Container( color: isDark - ? Colors.grey[700] + ? Colors.grey[800] : Colors.grey[200], child: Icon( Icons.broken_image, @@ -432,45 +534,34 @@ class _HomePageState extends State<HomePage> { .withOpacity(0.3), ), ), - ) - : GridView.builder( - physics: const NeverScrollableScrollPhysics(), - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - mainAxisSpacing: 2, - crossAxisSpacing: 2, - ), - itemCount: previewImages.length.clamp(0, 4), - itemBuilder: (context, index) { - return Image.file( - File(previewImages[index]), - fit: BoxFit.cover, - errorBuilder: - (context, error, stackTrace) => Container( - color: - isDark - ? Colors.grey[700] - : Colors.grey[200], - ), - ); - }, ), ), ), - // Project Title + // Project Title and Actions Padding( - padding: const EdgeInsets.all(12), - child: Text( - project.title, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, + padding: const EdgeInsets.all(8), + child: Row( + children: [ + Expanded( + child: Text( + project.title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + Icon( + Icons.more_vert, + size: 16, + color: theme.colorScheme.onSurface.withOpacity(0.6), + ), + ], ), ), ], @@ -478,4 +569,74 @@ class _HomePageState extends State<HomePage> { ), ); } -} + + Widget _buildTemplatesSection(ThemeData theme, bool isDark) { + // Static templates for now - can be made dynamic later + final templates = [ + {'title': 'Diwali Lights', 'subtitle': 'Instagram Post'}, + {'title': 'Business Opening', 'subtitle': 'Flyer'}, + {'title': 'Birthday Party', 'subtitle': 'Invitation'}, + ]; + + return SizedBox( + height: 160, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: templates.length, + itemBuilder: (context, index) { + final template = templates[index]; + return Container( + width: 104, + margin: const EdgeInsets.only(right: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 104, + height: 106, + decoration: BoxDecoration( + color: isDark ? Colors.grey[800] : Colors.grey[200], + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Icon( + Icons.image_outlined, + size: 32, + color: theme.colorScheme.onSurface.withOpacity(0.3), + ), + ), + ), + const SizedBox(height: 4), + Text( + template['title']!, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface, + height: 1.2, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + template['subtitle']!, + style: TextStyle( + fontSize: 12, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface.withOpacity(0.6), + height: 1.2, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + }, + ), + ); + } +} +\ No newline at end of file