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:
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