creek

The AI Image Editor of 2030
commit f42c4c7a3fe0eda2e6a6a0b8cfda7253f4def07a
parent fc1281c0c425fd8531329b1f7a49ec96e60f871e
Author: maydayv7 <maydayv7@gmail.com>
Date:   Mon,  1 Dec 2025 22:20:31 +0530

Fix home page, add preview image for files

Diffstat:
Aassets/templates/business.png | 0
Aassets/templates/diwali.png | 0
Aassets/templates/party.png | 0
Mlib/ui/pages/canvas_board_page.dart | 43+++++++++++++++++++++++++++++++++++++++++--
Mlib/ui/pages/home_page.dart | 1214++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mpubspec.yaml | 1+
6 files changed, 974 insertions(+), 284 deletions(-)

diff --git a/assets/templates/business.png b/assets/templates/business.png Binary files differ. diff --git a/assets/templates/diwali.png b/assets/templates/diwali.png Binary files differ. diff --git a/assets/templates/party.png b/assets/templates/party.png Binary files differ. diff --git a/lib/ui/pages/canvas_board_page.dart b/lib/ui/pages/canvas_board_page.dart @@ -498,6 +498,42 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { ); } + // Helper to generate preview image + Future<String?> _generatePreviewImage() async { + try { + final boundary = + _canvasGlobalKey.currentContext?.findRenderObject() + as RenderRepaintBoundary?; + if (boundary == null) return null; + + // Capture image with lower pixel ratio for preview thumbnail + final ui.Image image = await boundary.toImage(pixelRatio: 1.0); + final ByteData? byteData = await image.toByteData( + format: ui.ImageByteFormat.png, + ); + + if (byteData == null) return null; + final Uint8List pngBytes = byteData.buffer.asUint8List(); + + final directory = await getApplicationDocumentsDirectory(); + final previewDir = Directory('${directory.path}/previews'); + if (!await previewDir.exists()) { + await previewDir.create(recursive: true); + } + + final String fileName = + "preview_${DateTime.now().millisecondsSinceEpoch}.png"; + final String filePath = '${previewDir.path}/$fileName'; + + final File imgFile = File(filePath); + await imgFile.writeAsBytes(pngBytes); + return filePath; + } catch (e) { + debugPrint("Error generating preview: $e"); + return null; + } + } + Future<void> _saveCanvas() async { try { String fileName = "Canvas ${DateTime.now().toString().split(' ')[0]}"; @@ -510,16 +546,19 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { fileName = userFileName; } - // 2. Serialize Elements AND Paths (Drawing) to JSON + // 2. Generate Preview + final String? previewPath = await _generatePreviewImage(); + + // 3. Serialize Elements AND Paths (Drawing) to JSON final jsonList = _elementsToJson(elements); final pathsJson = _paths.map((p) => p.toMap()).toList(); - // [FIX] SAVE CANVAS DIMENSIONS final saveData = { 'elements': jsonList, 'paths': pathsJson, 'width': _canvasSize.width, // Saving Width 'height': _canvasSize.height, // Saving Height + 'preview_path': previewPath, // Saving Preview Path }; final jsonString = jsonEncode(saveData); diff --git a/lib/ui/pages/home_page.dart b/lib/ui/pages/home_page.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:adobe/data/models/project_model.dart'; import 'package:adobe/data/models/file_model.dart'; @@ -9,6 +10,7 @@ import 'package:adobe/services/project_service.dart'; import 'package:image/image.dart' as img; import 'project_detail_page.dart'; import 'define_brand_page.dart'; +import 'canvas_board_page.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -23,13 +25,16 @@ class _HomePageState extends State<HomePage> { final _fileRepo = FileRepo(); final _projectService = ProjectService(); + final TextEditingController _searchController = TextEditingController(); + String _searchQuery = ''; + List<ProjectModel> _allProjects = []; List<FileModel> _recentFiles = []; Map<int, ProjectModel> _projectMap = {}; final Map<int, List<String>> _projectPreviews = {}; - Map<String, String> _fileDimensions = {}; + Map<String, Map<String, String>> _fileMetadata = {}; bool _isLoading = true; - final String _userName = "Alex"; + final String _userName = "Alex"; @override void initState() { @@ -37,6 +42,12 @@ class _HomePageState extends State<HomePage> { _loadData(); } + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + Future<void> _loadData() async { setState(() => _isLoading = true); try { @@ -47,18 +58,51 @@ class _HomePageState extends State<HomePage> { projectMap[project.id!] = project; final images = await _imageRepo.getImages(project.id!); _projectPreviews[project.id!] = - images.take(4).map((img) => img.filePath).toList(); + images.take(1).map((img) => img.filePath).toList(); } } final recentFiles = await _fileRepo.getRecentFiles(limit: 10); - final Map<String, String> fileDimensions = {}; + final Map<String, Map<String, String>> fileMetadata = {}; + for (final file in recentFiles) { try { - final dimensions = await _getImageDimensions(file.filePath); - fileDimensions[file.id] = dimensions; + 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 dimensions for ${file.id}: $e'); + debugPrint('Error loading metadata for ${file.id}: $e'); } } @@ -67,7 +111,7 @@ class _HomePageState extends State<HomePage> { _allProjects = allProjects; _recentFiles = recentFiles; _projectMap = projectMap; - _fileDimensions = fileDimensions; + _fileMetadata = fileMetadata; _isLoading = false; }); } @@ -108,13 +152,15 @@ class _HomePageState extends State<HomePage> { String _getProjectBreadcrumb(FileModel file) { final project = _projectMap[file.projectId]; if (project == null) return ''; - - if (project.isEvent) { + + // If the project has a parent, it's an event. Show Parent / Event + if (project.parentId != null) { final parentProject = _projectMap[project.parentId!]; if (parentProject != null) { return '${parentProject.title} / ${project.title}'; } } + // Otherwise just the project name return project.title; } @@ -133,9 +179,6 @@ class _HomePageState extends State<HomePage> { }); } - // --- NEW METHOD: Test the Canvas Page --- - - void _openProject(ProjectModel project) { if (project.id != null) { _projectService.openProject(project.id!); @@ -148,72 +191,166 @@ class _HomePageState extends State<HomePage> { } } - void _openFile(FileModel file) { - final project = _projectMap[file.projectId]; - if (project != null) { - _openProject(project); + Future<void> _openFile(FileModel file) async { + 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"); + } + + if (mounted) { + _projectService.openProject(file.projectId); + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => CanvasBoardPage( + projectId: file.projectId, + width: width, + height: height, + existingFile: file, + ), + ), + ).then((_) => _loadData()); } } + void _navigateToSeeAll(String title, bool isProjects) { + Navigator.push( + context, + MaterialPageRoute( + builder: + (_) => _SeeAllPage( + title: title, + isProjects: isProjects, + projectRepo: _projectRepo, + fileRepo: _fileRepo, + imageRepo: _imageRepo, + onProjectTap: _openProject, + onFileTap: _openFile, + ), + ), + ).then((_) => _loadData()); + } + + void _showComingSoon() { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Coming Soon'), + duration: Duration(seconds: 1), + ), + ); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; + final bool isSearching = _searchQuery.isNotEmpty; + final List<FileModel> filteredFiles = + isSearching + ? _recentFiles + .where( + (f) => + f.name.toLowerCase().contains( + _searchQuery.toLowerCase(), + ) || + _getProjectBreadcrumb( + f, + ).toLowerCase().contains(_searchQuery.toLowerCase()), + ) + .toList() + : _recentFiles; + final List<ProjectModel> filteredProjects = + isSearching + ? _allProjects + .where( + (p) => p.title.toLowerCase().contains( + _searchQuery.toLowerCase(), + ), + ) + .toList() + : _allProjects; + return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, - body: _isLoading - ? const Center(child: CircularProgressIndicator()) - : SafeArea( - child: RefreshIndicator( - onRefresh: _loadData, - child: CustomScrollView( - slivers: [ - // Header Section - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), - child: Row( - children: [ - Text( - "Hello, $_userName!", - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w500, - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface, - ), - ), - const Spacer(), - - // Profile Picture - Container( - width: 30, - height: 30, - decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer, - shape: BoxShape.circle, - border: Border.all( - color: theme.scaffoldBackgroundColor, - width: 1.25, + body: + _isLoading + ? const Center(child: CircularProgressIndicator()) + : SafeArea( + child: RefreshIndicator( + onRefresh: _loadData, + child: CustomScrollView( + slivers: [ + // Header Section + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), + child: Row( + children: [ + Text( + "Hello, $_userName!", + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w500, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface, ), ), - child: Icon( - Icons.person, - size: 16, - color: theme.colorScheme.onPrimaryContainer, + const Spacer(), + Container( + 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, + ), ), - ), - ], + ], + ), ), ), - ), // Search Bar SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: TextField( + controller: _searchController, + onChanged: (value) { + setState(() { + _searchQuery = value.trim(); + }); + }, decoration: InputDecoration( hintText: 'Search', hintStyle: TextStyle( @@ -263,95 +400,206 @@ class _HomePageState extends State<HomePage> { ), ), - const SliverToBoxAdapter(child: SizedBox(height: 12)), + const SliverToBoxAdapter(child: SizedBox(height: 12)), - // Content Sections - SliverToBoxAdapter( - child: Padding( + if (isSearching) + SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Recent Files Section - if (_recentFiles.isNotEmpty) ...[ + sliver: SliverList( + delegate: SliverChildListDelegate([ + if (filteredFiles.isEmpty && + filteredProjects.isEmpty) + Padding( + padding: const EdgeInsets.only(top: 40), + child: Center( + child: Text( + "No results found", + style: TextStyle( + color: theme.colorScheme.onSurface + .withValues(alpha: 0.5), + fontFamily: 'GeneralSans', + ), + ), + ), + ), + if (filteredProjects.isNotEmpty) ...[ + Text( + "Projects & Events", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface + .withValues(alpha: 0.7), + ), + ), + const SizedBox(height: 8), + ...filteredProjects.map( + (p) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: SizedBox( + height: 180, + child: _ProjectCard( + project: p, + theme: theme, + isDark: isDark, + previewImages: + _projectPreviews[p.id] ?? [], + onTap: () => _openProject(p), + isHorizontal: false, + showGrid: false, + ), + ), + ), + ), + const SizedBox(height: 16), + ], + if (filteredFiles.isNotEmpty) ...[ + Text( + "Files", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface + .withValues(alpha: 0.7), + ), + ), + const SizedBox(height: 8), + ...filteredFiles.map( + (f) { + final meta = _fileMetadata[f.id] ?? {}; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _FileCard( + file: f, + theme: theme, + isDark: isDark, + breadcrumb: _getProjectBreadcrumb(f), + dimensions: meta['dimensions'] ?? 'Unknown', + previewPath: meta['preview'] ?? '', + timeAgo: _formatTimeAgo(f.lastUpdated), + onTap: () => _openFile(f), + ), + ); + }, + ), + ], + ]), + ), + ) + else + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_recentFiles.isNotEmpty) ...[ + _buildSectionHeader( + 'Recent Files', + theme, + onTap: + () => _navigateToSeeAll( + 'Recent Files', + false, + ), + ), + const SizedBox(height: 12), + ...(_recentFiles + .take(2) + .map( + (file) { + final meta = _fileMetadata[file.id] ?? {}; + return Padding( + padding: const EdgeInsets.only( + bottom: 12, + ), + child: _FileCard( + file: file, + theme: theme, + isDark: isDark, + breadcrumb: _getProjectBreadcrumb( + file, + ), + dimensions: meta['dimensions'] ?? 'Unknown', + previewPath: meta['preview'] ?? '', + timeAgo: _formatTimeAgo( + file.lastUpdated, + ), + onTap: () => _openFile(file), + ), + ); + }, + ) + .toList()), + const SizedBox(height: 24), + ], _buildSectionHeader( - 'Recent Files', + 'Projects', theme, - onTap: () { - // Navigate to recent files page - }, + onTap: + () => _navigateToSeeAll( + 'All Projects', + true, + ), ), const SizedBox(height: 12), - ...(_recentFiles - .take(2) - .map( - (file) => Padding( - padding: const EdgeInsets.only( - bottom: 12, - ), - child: _buildRecentFileCard( - file, - theme, - isDark, + + // Show "Create Project" if empty, else list + _allProjects.isEmpty + ? _buildCreateProjectCard(theme, isDark) + : SizedBox( + height: 140, // Reduced height + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _allProjects.length, + itemBuilder: (context, index) { + final project = _allProjects[index]; + return Padding( + padding: const EdgeInsets.only( + right: 12, + ), + child: _ProjectCard( + project: project, + theme: theme, + isDark: isDark, + previewImages: + _projectPreviews[project.id] ?? + [], + onTap: () => _openProject(project), + showGrid: false, + ), + ); + }, ), ), - ) - .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, - ); - }, + _buildSectionHeader( + 'Explore templates', + theme, + onTap: () {}, ), - ), - const SizedBox(height: 24), - - // Explore Templates Section - _buildSectionHeader( - 'Explore templates', - theme, - onTap: () {}, + const SizedBox(height: 12), + _buildTemplatesSection(theme, isDark), + const SizedBox(height: 24), + ], ), - const SizedBox(height: 12), - _buildTemplatesSection(theme, isDark), - const SizedBox(height: 24), - ], + ), ), - ), - ), - - // Bottom padding - const SliverToBoxAdapter(child: SizedBox(height: 100)), - ], + const SliverToBoxAdapter(child: SizedBox(height: 100)), + ], + ), ), ), + floatingActionButton: _allProjects.isEmpty + ? null + : FloatingActionButton( + onPressed: _createNewProject, + backgroundColor: isDark ? Colors.grey[900] : Colors.grey[900], + foregroundColor: Colors.white, + child: const Icon(Icons.add, size: 24), ), - floatingActionButton: FloatingActionButton( - onPressed: _createNewProject, - backgroundColor: isDark ? Colors.grey[900] : Colors.grey[900], - foregroundColor: Colors.white, - child: const Icon(Icons.add, size: 24), - ), ); } @@ -373,65 +621,257 @@ class _HomePageState extends State<HomePage> { ), const Spacer(), if (onTap != null) - GestureDetector( - onTap: onTap, - child: Icon( - Icons.chevron_right, - size: 24, - color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.chevron_right, + size: 24, + color: theme.colorScheme.onSurface.withValues(alpha: 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); + Widget _buildCreateProjectCard(ThemeData theme, bool isDark) { + return GestureDetector( + onTap: _createNewProject, + child: Container( + height: 130, + width: 130, + margin: const EdgeInsets.only(right: 12), + decoration: BoxDecoration( + color: theme.scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDark ? Colors.grey[800]! : Colors.grey[300]!, + width: 1, + style: BorderStyle.solid, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.add_circle_outline, + size: 32, + color: theme.colorScheme.primary.withValues(alpha: 0.7), + ), + const SizedBox(height: 8), + Text( + "Create Project", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + ], + ), + ), + ); + } + + Widget _buildTemplatesSection(ThemeData theme, bool isDark) { + final templates = [ + { + 'title': 'Diwali Lights', + 'subtitle': 'Instagram Post', + 'image': 'assets/templates/diwali.png', + }, + { + 'title': 'Business Opening', + 'subtitle': 'Flyer', + 'image': 'assets/templates/business.png', + }, + { + 'title': 'Birthday Party', + 'subtitle': 'Invitation', + 'image': 'assets/templates/party.png', + }, + ]; + + return SizedBox( + height: 140, // Reduced + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: templates.length, + itemBuilder: (context, index) { + final template = templates[index]; + return GestureDetector( + onTap: _showComingSoon, + child: Container( + width: 100, // Reduced + margin: const EdgeInsets.only(right: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: isDark ? Colors.grey[800] : Colors.grey[200], + borderRadius: BorderRadius.circular(8), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset( + template['image']!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Center( + child: Icon( + Icons.image_outlined, + size: 32, + color: theme.colorScheme.onSurface.withValues( + alpha: 0.3, + ), + ), + ); + }, + ), + ), + ), + const SizedBox(height: 4), + Text( + template['title']!, + style: TextStyle( + fontSize: 13, + 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: 11, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + height: 1.2, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + }, + ), + ); + } +} + +// ----------------------------------------------------------------------------- +// REUSABLE WIDGETS & PAGES +// ----------------------------------------------------------------------------- +class _FileCard extends StatelessWidget { + final FileModel file; + final ThemeData theme; + final bool isDark; + final String breadcrumb; + final String dimensions; + final String previewPath; + final String timeAgo; + final VoidCallback onTap; + + const _FileCard({ + required this.file, + required this.theme, + required this.isDark, + required this.breadcrumb, + required this.dimensions, + required this.previewPath, + required this.timeAgo, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { return GestureDetector( - onTap: () => _openFile(file), + onTap: onTap, child: Container( decoration: BoxDecoration( color: theme.scaffoldBackgroundColor, borderRadius: BorderRadius.circular(8), ), child: Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Container( - width: 104, - height: 106, - decoration: BoxDecoration( - color: isDark ? Colors.grey[800] : Colors.grey[200], - borderRadius: BorderRadius.circular(8), - ), - child: ClipRRect( - 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.withValues( - alpha: 0.3, - ), - ), + SizedBox( + width: 88, + height: 88, + child: Center( + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: isDark ? Colors.grey[800] : Colors.grey[200], + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.12), + blurRadius: 6, + offset: const Offset(0, 3), ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: previewPath.isNotEmpty + ? Image.file( + File(previewPath), + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + Container( + color: isDark ? Colors.grey[800] : Colors.grey[200], + child: Icon( + Icons.broken_image, + size: 20, + color: + theme.colorScheme.onSurface.withValues( + alpha: 0.3, + ), + ), + ), + ) + : Container( + color: isDark ? Colors.grey[800] : Colors.grey[200], + child: Icon( + Icons.image, + size: 24, + color: + theme.colorScheme.onSurface.withValues( + alpha: 0.3, + ), + ), + ), + ), ), ), ), - const SizedBox(width: 8), + const SizedBox(width: 16), Expanded( child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, // Keep height minimal for centering children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -440,11 +880,12 @@ class _HomePageState extends State<HomePage> { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Project Breadcrumb if (breadcrumb.isNotEmpty) Text( breadcrumb, style: TextStyle( - fontSize: 10, + fontSize: 11, fontWeight: FontWeight.w500, fontFamily: 'Inter', color: theme.colorScheme.onSurface @@ -454,11 +895,12 @@ class _HomePageState extends State<HomePage> { overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), + // File Name Text( file.name, style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, + fontSize: 15, + fontWeight: FontWeight.w600, fontFamily: 'GeneralSans', color: theme.colorScheme.onSurface, ), @@ -468,32 +910,32 @@ class _HomePageState extends State<HomePage> { ], ), ), - const SizedBox(width: 3), + const SizedBox(width: 8), Icon( Icons.more_vert, - size: 16, + size: 20, color: theme.colorScheme.onSurface.withValues( alpha: 0.6, ), ), ], ), - const SizedBox(height: 8), + const SizedBox(height: 6), Text( dimensions, style: TextStyle( - fontSize: 10, + fontSize: 11, fontFamily: 'GeneralSans', color: theme.colorScheme.onSurface.withValues( alpha: 0.6, ), ), ), - const SizedBox(height: 2), + const SizedBox(height: 4), Text( timeAgo, style: TextStyle( - fontSize: 10, + fontSize: 11, fontFamily: 'GeneralSans', color: theme.colorScheme.onSurface.withValues( alpha: 0.4, @@ -509,21 +951,38 @@ class _HomePageState extends State<HomePage> { ), ); } +} - Widget _buildProjectCard(ProjectModel project, ThemeData theme, bool isDark) { - final previewImages = - project.id != null ? (_projectPreviews[project.id!] ?? []) : []; +class _ProjectCard extends StatelessWidget { + final ProjectModel project; + final ThemeData theme; + final bool isDark; + final List<String> previewImages; + final VoidCallback onTap; + final bool isHorizontal; + final bool showGrid; + const _ProjectCard({ + required this.project, + required this.theme, + required this.isDark, + required this.previewImages, + required this.onTap, + this.isHorizontal = true, + this.showGrid = false, + }); + + @override + Widget build(BuildContext context) { return GestureDetector( - onTap: () => _openProject(project), + onTap: onTap, child: Container( - width: 156, - margin: const EdgeInsets.only(right: 8), + width: isHorizontal ? 130 : null, decoration: BoxDecoration( color: theme.scaffoldBackgroundColor, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), border: Border.all( - color: isDark ? Colors.grey[700]! : Colors.grey[300]!, + color: isDark ? Colors.grey[800]! : Colors.grey[300]!, width: 1, ), ), @@ -532,53 +991,52 @@ class _HomePageState extends State<HomePage> { children: [ Expanded( child: ClipRRect( - borderRadius: const BorderRadius.vertical( - top: Radius.circular(8), - ), - child: - previewImages.isEmpty - ? Container( - color: isDark ? Colors.grey[800] : Colors.grey[200], - child: Center( - child: Icon( - Icons.folder_outlined, - size: 32, - color: theme.colorScheme.onSurface.withValues( - alpha: 0.3, + borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), + child: showGrid + ? Padding( + padding: const EdgeInsets.all(4.0), + child: _buildGridPreview(), + ) + : (previewImages.isNotEmpty + ? Image.file( + File(previewImages.first), + fit: BoxFit.cover, + width: double.infinity, + errorBuilder: (context, error, stackTrace) => + Container( + color: isDark ? Colors.grey[800] : Colors.grey[200], + child: Icon( + Icons.broken_image, + color: theme.colorScheme.onSurface.withValues( + alpha: 0.3, + ), ), ), - ), - ) - : Image.file( - File(previewImages.first), - fit: BoxFit.cover, - width: double.infinity, - errorBuilder: - (context, error, stackTrace) => Container( - color: - isDark - ? Colors.grey[800] - : Colors.grey[200], - child: Icon( - Icons.broken_image, - color: theme.colorScheme.onSurface.withValues( - alpha: 0.3, - ), + ) + : Container( + color: isDark ? Colors.grey[800] : Colors.grey[200], + child: Center( + child: Icon( + Icons.folder_outlined, + size: 32, + color: theme.colorScheme.onSurface.withValues( + alpha: 0.3, ), ), - ), + ), + )), ), ), Padding( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(10), child: Row( children: [ Expanded( child: Text( project.title, style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, + fontSize: 13, + fontWeight: FontWeight.w600, fontFamily: 'GeneralSans', color: theme.colorScheme.onSurface, ), @@ -586,7 +1044,7 @@ class _HomePageState extends State<HomePage> { overflow: TextOverflow.ellipsis, ), ), - const SizedBox(width: 8), + const SizedBox(width: 4), Icon( Icons.more_vert, size: 16, @@ -601,72 +1059,264 @@ class _HomePageState extends State<HomePage> { ); } - Widget _buildTemplatesSection(ThemeData theme, bool isDark) { - 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.withValues(alpha: 0.3), - ), - ), + // Grid Preview Logic + Widget _buildGridPreview() { + return Column( + children: [ + Expanded( + child: Row( + children: [ + Expanded( + child: _buildPreviewItem( + previewImages.isNotEmpty ? previewImages[0] : null, ), - 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(width: 4), + Expanded( + child: _buildPreviewItem( + previewImages.length > 1 ? previewImages[1] : null, ), - const SizedBox(height: 2), - Text( - template['subtitle']!, - style: TextStyle( - fontSize: 12, - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface.withValues(alpha: 0.6), - height: 1.2, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(height: 4), + Expanded( + child: Row( + children: [ + Expanded( + child: _buildPreviewItem( + previewImages.length > 2 ? previewImages[2] : null, ), - ], - ), - ); - }, + ), + const SizedBox(width: 4), + Expanded( + child: _buildPreviewItem( + previewImages.length > 3 ? previewImages[3] : null, + ), + ), + ], + ), + ), + ], + ); + } + + Widget _buildPreviewItem(String? imagePath) { + return Container( + decoration: BoxDecoration( + color: isDark ? Colors.grey[800] : Colors.grey[200], + borderRadius: BorderRadius.circular(6), // Rounded grid items ), + clipBehavior: Clip.antiAlias, + child: imagePath != null + ? Image.file( + File(imagePath), + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.broken_image, size: 16, color: Colors.grey), + ) + : null, // Empty placeholder + ); + } +} + +class _SeeAllPage extends StatefulWidget { + final String title; + final bool isProjects; + final ProjectRepo projectRepo; + final FileRepo fileRepo; + final ImageRepo imageRepo; + final Function(ProjectModel) onProjectTap; + final Function(FileModel) onFileTap; + + const _SeeAllPage({ + required this.title, + required this.isProjects, + required this.projectRepo, + required this.fileRepo, + required this.imageRepo, + required this.onProjectTap, + required this.onFileTap, + }); + + @override + State<_SeeAllPage> createState() => _SeeAllPageState(); +} + +class _SeeAllPageState extends State<_SeeAllPage> { + bool _isLoading = true; + List<dynamic> _items = []; + Map<int, ProjectModel> _projectMap = {}; + Map<int, List<String>> _projectPreviews = {}; + Map<String, Map<String, String>> _fileMetadata = {}; + + @override + void initState() { + super.initState(); + _fetchData(); + } + + Future<void> _fetchData() async { + setState(() => _isLoading = true); + try { + if (widget.isProjects) { + final projects = await widget.projectRepo.getAllProjects(); + // Fetch Child Event Images for Grid Preview + for (final p in projects) { + if (p.id != null) { + final events = await widget.projectRepo.getEvents(p.id!); + List<String> eventImages = []; + // Get 1 image from up to 4 distinct events + for (final event in events.take(4)) { + if (event.id != null) { + final imgs = await widget.imageRepo.getImages(event.id!); + if (imgs.isNotEmpty) { + eventImages.add(imgs.first.filePath); + } + } + } + _projectPreviews[p.id!] = eventImages; + } + } + _items = projects; + } else { + final files = await widget.fileRepo.getRecentFiles(limit: 50); + 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 (_) {} + } + _items = files; + } + } catch (e) { + debugPrint("Error loading see all data: $e"); + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + String _getBreadcrumb(FileModel file) { + final project = _projectMap[file.projectId]; + if (project == null) return ''; + if (project.parentId != null) { + final parent = _projectMap[project.parentId!]; + if (parent != null) return '${parent.title} / ${project.title}'; + } + return project.title; + } + + String _timeAgo(DateTime dt) { + final diff = DateTime.now().difference(dt); + if (diff.inDays > 0) return 'Edited ${diff.inDays} days ago'; + return 'Edited recently'; + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + return Scaffold( + appBar: AppBar( + title: Text( + widget.title, + style: TextStyle( + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface, + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + backgroundColor: theme.scaffoldBackgroundColor, + elevation: 0, + iconTheme: IconThemeData(color: theme.colorScheme.onSurface), + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : Padding( + padding: const EdgeInsets.all(16.0), + child: widget.isProjects + ? GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.8, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: _items.length, + itemBuilder: (context, index) { + final project = _items[index] as ProjectModel; + return _ProjectCard( + project: project, + theme: theme, + isDark: isDark, + previewImages: _projectPreviews[project.id] ?? [], + onTap: () => widget.onProjectTap(project), + isHorizontal: false, + showGrid: true, // Show Grid for Projects here + ); + }, + ) + : ListView.builder( + itemCount: _items.length, + itemBuilder: (context, index) { + final file = _items[index] as FileModel; + final meta = _fileMetadata[file.id] ?? {}; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _FileCard( + file: file, + theme: theme, + isDark: isDark, + breadcrumb: _getBreadcrumb(file), + dimensions: meta['dimensions'] ?? 'Unknown', + previewPath: meta['preview'] ?? '', + timeAgo: _timeAgo(file.lastUpdated), + onTap: () => widget.onFileTap(file), + ), + ); + }, + ), + ), ); } } diff --git a/pubspec.yaml b/pubspec.yaml @@ -46,6 +46,7 @@ flutter: - .env - assets/fonts/ - assets/icons/ + - assets/templates/ - assets/moodboard_blob.png - assets/stylesheet_blob.png - assets/files_blob.png