creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
lib/ui/pages/home_page.dart (33232B)
1import 'dart:io';
2import 'dart:convert';
3import 'package:flutter/material.dart';
4import 'package:image/image.dart' as img;
5import 'package:shared_preferences/shared_preferences.dart';
6import 'package:creekui/data/models/project_model.dart';
7import 'package:creekui/data/models/file_model.dart';
8import 'package:creekui/data/repos/project_repo.dart';
9import 'package:creekui/data/repos/image_repo.dart';
10import 'package:creekui/data/repos/file_repo.dart';
11import 'package:creekui/services/project_service.dart';
12import 'package:creekui/services/file_service.dart';
13import 'package:creekui/ui/styles/variables.dart';
14import 'package:creekui/ui/widgets/search_bar.dart';
15import 'package:creekui/ui/widgets/file_card.dart';
16import 'package:creekui/ui/widgets/project_card.dart';
17import 'package:creekui/ui/widgets/empty_state.dart';
18import 'package:creekui/ui/widgets/section_header.dart';
19import 'package:creekui/ui/widgets/dialog.dart';
20import 'package:creekui/ui/widgets/text_field.dart';
21import 'package:creekui/ui/pages/settings_page.dart';
22import 'project_detail_page.dart';
23import 'define_brand_page.dart';
24import 'canvas_page.dart';
25
26class HomePage extends StatefulWidget {
27 const HomePage({super.key});
28
29 @override
30 State<HomePage> createState() => _HomePageState();
31}
32
33class _HomePageState extends State<HomePage> {
34 final _projectRepo = ProjectRepo();
35 final _imageRepo = ImageRepo();
36 final _fileRepo = FileRepo();
37 final _projectService = ProjectService();
38 final _fileService = FileService();
39
40 final TextEditingController _searchController = TextEditingController();
41 String _searchQuery = '';
42
43 List<ProjectModel> _allProjects = [];
44 List<FileModel> _recentFiles = [];
45 Map<int, ProjectModel> _projectMap = {};
46 final Map<int, List<String>> _projectPreviews = {};
47 Map<String, Map<String, String>> _fileMetadata = {};
48 bool _isLoading = true;
49 String _userName = "Alex"; // Default
50
51 @override
52 void initState() {
53 super.initState();
54 _loadData();
55 }
56
57 @override
58 void dispose() {
59 _searchController.dispose();
60 super.dispose();
61 }
62
63 Future<void> _loadData() async {
64 setState(() => _isLoading = true);
65 try {
66 // Load User Name
67 final prefs = await SharedPreferences.getInstance();
68 final storedName = prefs.getString('user_name') ?? "Alex";
69
70 final allProjects = await _projectRepo.getAllProjects();
71 final Map<int, ProjectModel> projectMap = {};
72 for (final project in allProjects) {
73 if (project.id != null) {
74 projectMap[project.id!] = project;
75 final images = await _imageRepo.getImages(project.id!);
76 _projectPreviews[project.id!] =
77 images.take(1).map((img) => img.filePath).toList();
78 }
79 }
80
81 final recentFiles = await _fileRepo.getRecentFiles(limit: 10);
82 final Map<String, Map<String, String>> fileMetadata = {};
83
84 for (final file in recentFiles) {
85 final meta = await _fileService.getFileMetadata(file.filePath);
86 fileMetadata[file.id] = {
87 'dimensions': meta.dimensions,
88 'preview': meta.previewPath ?? '',
89 };
90 }
91
92 if (mounted) {
93 setState(() {
94 _userName = storedName;
95 _allProjects = allProjects;
96 _recentFiles = recentFiles;
97 _projectMap = projectMap;
98 _fileMetadata = fileMetadata;
99 _isLoading = false;
100 });
101 }
102 } catch (e) {
103 debugPrint('Error loading home data: $e');
104 if (mounted) setState(() => _isLoading = false);
105 }
106 }
107
108 String _formatTimeAgo(DateTime dateTime) {
109 final now = DateTime.now();
110 final difference = now.difference(dateTime);
111 if (difference.inDays > 0)
112 return 'Edited ${difference.inDays} ${difference.inDays == 1 ? 'day' : 'days'} ago';
113 if (difference.inHours > 0)
114 return 'Edited ${difference.inHours} ${difference.inHours == 1 ? 'hour' : 'hours'} ago';
115 if (difference.inMinutes > 0)
116 return 'Edited ${difference.inMinutes} ${difference.inMinutes == 1 ? 'minute' : 'minutes'} ago';
117 return 'Edited just now';
118 }
119
120 String _getProjectBreadcrumb(FileModel file) {
121 final project = _projectMap[file.projectId];
122 if (project == null) return '';
123
124 // If the project has a parent, it's an event. Show Parent / Event
125 if (project.parentId != null) {
126 final parentProject = _projectMap[project.parentId!];
127 if (parentProject != null)
128 return '${parentProject.title} / ${project.title}';
129 }
130 // Otherwise just the project name
131 return project.title;
132 }
133
134 Future<void> _createNewProject() async {
135 Navigator.push(
136 context,
137 MaterialPageRoute(
138 builder:
139 (_) => const DefineBrandPage(
140 projectName: "",
141 projectDescription: null,
142 ),
143 ),
144 ).then((_) => _loadData());
145 }
146
147 void _openProject(ProjectModel project) {
148 if (project.id != null) {
149 _projectService.openProject(project.id!);
150 Navigator.push(
151 context,
152 MaterialPageRoute(
153 builder: (_) => ProjectDetailPage(projectId: project.id!),
154 ),
155 ).then((_) => _loadData());
156 }
157 }
158
159 // Rename Project Logic
160 Future<void> _renameProject(ProjectModel project) async {
161 final controller = TextEditingController(text: project.title);
162
163 await ShowDialog.show(
164 context,
165 title: 'Rename Project',
166 primaryButtonText: 'Save',
167 content: CommonTextField(
168 hintText: 'Enter new name',
169 controller: controller,
170 autoFocus: true,
171 ),
172 onPrimaryPressed: () async {
173 if (controller.text.trim().isNotEmpty) {
174 try {
175 await _projectService.updateProjectDetails(
176 project.id!,
177 title: controller.text.trim(),
178 );
179 if (mounted) {
180 Navigator.pop(context);
181 ScaffoldMessenger.of(
182 context,
183 ).showSnackBar(const SnackBar(content: Text('Project renamed')));
184 _loadData();
185 }
186 } catch (e) {
187 if (mounted)
188 ScaffoldMessenger.of(
189 context,
190 ).showSnackBar(SnackBar(content: Text('Error renaming: $e')));
191 }
192 }
193 },
194 );
195 }
196
197 // Delete Project Logic
198 Future<void> _deleteProject(ProjectModel project) async {
199 await ShowDialog.show(
200 context,
201 title: 'Delete Project',
202 description:
203 'Are you sure you want to delete "${project.title}"? This cannot be undone.',
204 primaryButtonText: 'Delete',
205 isDestructive: true,
206 onPrimaryPressed: () async {
207 try {
208 await _projectService.deleteProject(project.id!);
209 if (mounted) {
210 Navigator.pop(context);
211 ScaffoldMessenger.of(context).showSnackBar(
212 const SnackBar(content: Text('Project deleted successfully')),
213 );
214 _loadData();
215 }
216 } catch (e) {
217 if (mounted)
218 ScaffoldMessenger.of(context).showSnackBar(
219 SnackBar(content: Text('Error deleting project: $e')),
220 );
221 }
222 },
223 );
224 }
225
226 Future<void> _openFile(FileModel file) async {
227 // Default values
228 double width = 1080;
229 double height = 1920;
230
231 // File Metadata
232 final meta = await _fileService.getFileMetadata(file.filePath);
233 if (meta.width > 0 && meta.height > 0) {
234 width = meta.width;
235 height = meta.height;
236 }
237
238 if (mounted) {
239 _projectService.openProject(file.projectId);
240 Navigator.push(
241 context,
242 MaterialPageRoute(
243 builder:
244 (_) => CanvasPage(
245 projectId: file.projectId,
246 width: width,
247 height: height,
248 existingFile: file,
249 ),
250 ),
251 ).then((_) => _loadData());
252 }
253 }
254
255 void _navigateToSeeAll(String title, bool isProjects) {
256 Navigator.push(
257 context,
258 MaterialPageRoute(
259 builder:
260 (_) => _SeeAllPage(
261 title: title,
262 isProjects: isProjects,
263 projectRepo: _projectRepo,
264 fileRepo: _fileRepo,
265 imageRepo: _imageRepo,
266 onProjectTap: _openProject,
267 onFileTap: _openFile,
268 onProjectDelete: _deleteProject,
269 onProjectRename: _renameProject,
270 ),
271 ),
272 ).then((_) => _loadData());
273 }
274
275 void _showComingSoon() {
276 ScaffoldMessenger.of(context).showSnackBar(
277 const SnackBar(
278 content: Text('Coming Soon'),
279 duration: Duration(seconds: 1),
280 ),
281 );
282 }
283
284 @override
285 Widget build(BuildContext context) {
286 final theme = Theme.of(context);
287 final isDark = theme.brightness == Brightness.dark;
288 final bool isSearching = _searchQuery.isNotEmpty;
289
290 final List<FileModel> filteredFiles =
291 isSearching
292 ? _recentFiles
293 .where(
294 (f) =>
295 f.name.toLowerCase().contains(
296 _searchQuery.toLowerCase(),
297 ) ||
298 _getProjectBreadcrumb(
299 f,
300 ).toLowerCase().contains(_searchQuery.toLowerCase()),
301 )
302 .toList()
303 : _recentFiles;
304 final List<ProjectModel> filteredProjects =
305 isSearching
306 ? _allProjects
307 .where(
308 (p) => p.title.toLowerCase().contains(
309 _searchQuery.toLowerCase(),
310 ),
311 )
312 .toList()
313 : _allProjects;
314
315 return Scaffold(
316 backgroundColor: Variables.background,
317 body:
318 _isLoading
319 ? const Center(child: CircularProgressIndicator())
320 : SafeArea(
321 child: RefreshIndicator(
322 onRefresh: _loadData,
323 child: CustomScrollView(
324 slivers: [
325 // Header Section
326 SliverToBoxAdapter(
327 child: Padding(
328 padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
329 child: Row(
330 children: [
331 Text(
332 "Hello, $_userName!",
333 style: Variables.headerStyle.copyWith(
334 fontSize: 24,
335 color: theme.colorScheme.onSurface,
336 ),
337 ),
338 const Spacer(),
339 GestureDetector(
340 onTap: () {
341 Navigator.push(
342 context,
343 MaterialPageRoute(
344 builder: (_) => const SettingsPage(),
345 ),
346 ).then((_) {
347 _loadData(); // Reload to update name
348 });
349 },
350 behavior: HitTestBehavior.opaque,
351 child: Container(
352 width: 30,
353 height: 30,
354 decoration: BoxDecoration(
355 color: theme.colorScheme.primaryContainer,
356 shape: BoxShape.circle,
357 border: Border.all(
358 color: Variables.background,
359 width: 1.25,
360 ),
361 ),
362 child: Icon(
363 Icons.person,
364 size: 16,
365 color: theme.colorScheme.onPrimaryContainer,
366 ),
367 ),
368 ),
369 ],
370 ),
371 ),
372 ),
373
374 // Search Bar
375 SliverToBoxAdapter(
376 child: Padding(
377 padding: const EdgeInsets.symmetric(horizontal: 16),
378 child: CommonSearchBar(
379 controller: _searchController,
380 onChanged:
381 (value) =>
382 setState(() => _searchQuery = value.trim()),
383 ),
384 ),
385 ),
386 const SliverToBoxAdapter(child: SizedBox(height: 12)),
387 if (isSearching)
388 SliverPadding(
389 padding: const EdgeInsets.symmetric(horizontal: 16),
390 sliver: SliverList(
391 delegate: SliverChildListDelegate([
392 if (filteredFiles.isEmpty &&
393 filteredProjects.isEmpty)
394 const EmptyState(
395 icon: Icons.search_off,
396 title: "No results found",
397 subtitle: "Try searching for something else",
398 ),
399 if (filteredProjects.isNotEmpty) ...[
400 Text(
401 "Projects & Events",
402 style: Variables.bodyStyle.copyWith(
403 fontWeight: FontWeight.w600,
404 color: theme.colorScheme.onSurface
405 .withOpacity(0.7),
406 ),
407 ),
408 const SizedBox(height: 8),
409 ...filteredProjects.map(
410 (p) => Padding(
411 padding: const EdgeInsets.only(bottom: 8),
412 child: SizedBox(
413 height: 180,
414 child: ProjectCard(
415 project: p,
416 previewImages:
417 _projectPreviews[p.id] ?? [],
418 onTap: () => _openProject(p),
419 onRename: () => _renameProject(p),
420 onDelete: () => _deleteProject(p),
421 isHorizontal: false,
422 showGrid: false,
423 ),
424 ),
425 ),
426 ),
427 const SizedBox(height: 16),
428 ],
429 if (filteredFiles.isNotEmpty) ...[
430 Text(
431 "Files",
432 style: Variables.bodyStyle.copyWith(
433 fontWeight: FontWeight.w600,
434 color: theme.colorScheme.onSurface
435 .withOpacity(0.7),
436 ),
437 ),
438 const SizedBox(height: 8),
439 ...filteredFiles.map((f) {
440 final meta = _fileMetadata[f.id] ?? {};
441 return Padding(
442 padding: const EdgeInsets.only(bottom: 12),
443 child: FileCard(
444 file: f,
445 breadcrumb: _getProjectBreadcrumb(f),
446 dimensions:
447 meta['dimensions'] ?? 'Unknown',
448 previewPath: meta['preview'] ?? '',
449 timeAgo: _formatTimeAgo(f.lastUpdated),
450 onTap: () => _openFile(f),
451 onMenuAction: null,
452 ),
453 );
454 }),
455 ],
456 ]),
457 ),
458 )
459 else
460 SliverToBoxAdapter(
461 child: Padding(
462 padding: const EdgeInsets.symmetric(horizontal: 16),
463 child: Column(
464 crossAxisAlignment: CrossAxisAlignment.start,
465 children: [
466 if (_recentFiles.isNotEmpty) ...[
467 SectionHeader(
468 title: 'Recent Files',
469 onTap:
470 () => _navigateToSeeAll(
471 'Recent Files',
472 false,
473 ),
474 ),
475 const SizedBox(height: 12),
476 ...(_recentFiles.take(2).map((file) {
477 final meta = _fileMetadata[file.id] ?? {};
478 return Padding(
479 padding: const EdgeInsets.only(
480 bottom: 12,
481 ),
482 child: FileCard(
483 file: file,
484 breadcrumb: _getProjectBreadcrumb(file),
485 dimensions:
486 meta['dimensions'] ?? 'Unknown',
487 previewPath: meta['preview'] ?? '',
488 timeAgo: _formatTimeAgo(
489 file.lastUpdated,
490 ),
491 onTap: () => _openFile(file),
492 onMenuAction: null,
493 ),
494 );
495 }).toList()),
496 const SizedBox(height: 12),
497 ],
498 SectionHeader(
499 title: 'Projects',
500 onTap:
501 () => _navigateToSeeAll(
502 'All Projects',
503 true,
504 ),
505 ),
506 const SizedBox(height: 12),
507
508 // Show "Create Project" if empty, else list
509 _allProjects.isEmpty
510 ? _buildCreateProjectCard(theme, isDark)
511 : SizedBox(
512 height: 140,
513 child: ListView.builder(
514 scrollDirection: Axis.horizontal,
515 itemCount: _allProjects.length,
516 itemBuilder: (context, index) {
517 final project = _allProjects[index];
518 return Padding(
519 padding: const EdgeInsets.only(
520 right: 12,
521 ),
522 child: ProjectCard(
523 project: project,
524 previewImages:
525 _projectPreviews[project
526 .id] ??
527 [],
528 onTap:
529 () => _openProject(project),
530 onRename:
531 () => _renameProject(project),
532 onDelete:
533 () => _deleteProject(project),
534 showGrid: false,
535 isHorizontal: true,
536 ),
537 );
538 },
539 ),
540 ),
541 const SizedBox(height: 24),
542 const SectionHeader(title: 'Explore templates'),
543 const SizedBox(height: 12),
544 _buildTemplatesSection(theme, isDark),
545 const SizedBox(height: 24),
546 ],
547 ),
548 ),
549 ),
550 const SliverToBoxAdapter(child: SizedBox(height: 100)),
551 ],
552 ),
553 ),
554 ),
555 floatingActionButton:
556 _allProjects.isEmpty
557 ? null
558 : FloatingActionButton(
559 onPressed: _createNewProject,
560 backgroundColor: Colors.grey[900],
561 foregroundColor: Colors.white,
562 child: const Icon(Icons.add, size: 24),
563 ),
564 );
565 }
566
567 Widget _buildCreateProjectCard(ThemeData theme, bool isDark) {
568 return GestureDetector(
569 onTap: _createNewProject,
570 child: Container(
571 height: 130,
572 width: 130,
573 margin: const EdgeInsets.only(right: 12),
574 decoration: BoxDecoration(
575 color: theme.scaffoldBackgroundColor,
576 borderRadius: BorderRadius.circular(Variables.radiusMedium),
577 border: Border.all(
578 color: isDark ? Variables.borderDark : Variables.borderSubtle,
579 width: 1,
580 ),
581 ),
582 child: Column(
583 mainAxisAlignment: MainAxisAlignment.center,
584 children: [
585 Icon(
586 Icons.add_circle_outline,
587 size: 32,
588 color: theme.colorScheme.primary.withOpacity(0.7),
589 ),
590 const SizedBox(height: 8),
591 Text(
592 "Create Project",
593 style: Variables.bodyStyle.copyWith(
594 fontSize: 12,
595 fontWeight: FontWeight.w500,
596 color: theme.colorScheme.onSurface.withOpacity(0.7),
597 ),
598 ),
599 ],
600 ),
601 ),
602 );
603 }
604
605 Widget _buildTemplatesSection(ThemeData theme, bool isDark) {
606 final templates = [
607 {
608 'title': 'Diwali Lights',
609 'subtitle': 'Instagram Post',
610 'image': 'assets/templates/diwali.png',
611 },
612 {
613 'title': 'Business Opening',
614 'subtitle': 'Flyer',
615 'image': 'assets/templates/business.png',
616 },
617 {
618 'title': 'Birthday Party',
619 'subtitle': 'Invitation',
620 'image': 'assets/templates/party.png',
621 },
622 ];
623
624 return SizedBox(
625 height: 140,
626 child: ListView.builder(
627 scrollDirection: Axis.horizontal,
628 itemCount: templates.length,
629 itemBuilder: (context, index) {
630 final template = templates[index];
631 return GestureDetector(
632 onTap: _showComingSoon,
633 child: Container(
634 width: 100,
635 margin: const EdgeInsets.only(right: 12),
636 child: Column(
637 crossAxisAlignment: CrossAxisAlignment.start,
638 mainAxisSize: MainAxisSize.min,
639 children: [
640 Container(
641 width: 100,
642 height: 100,
643 decoration: BoxDecoration(
644 color: isDark ? Colors.grey[800] : Colors.grey[200],
645 borderRadius: BorderRadius.circular(8),
646 ),
647 child: ClipRRect(
648 borderRadius: BorderRadius.circular(8),
649 child: Image.asset(
650 template['image']!,
651 fit: BoxFit.cover,
652 errorBuilder: (context, error, stackTrace) {
653 return Center(
654 child: Icon(
655 Icons.image_outlined,
656 size: 32,
657 color: theme.colorScheme.onSurface.withOpacity(
658 0.3,
659 ),
660 ),
661 );
662 },
663 ),
664 ),
665 ),
666 const SizedBox(height: 4),
667 Text(
668 template['title']!,
669 style: Variables.bodyStyle.copyWith(
670 fontSize: 13,
671 fontWeight: FontWeight.w500,
672 height: 1.2,
673 ),
674 maxLines: 1,
675 overflow: TextOverflow.ellipsis,
676 ),
677 const SizedBox(height: 2),
678 Text(
679 template['subtitle']!,
680 style: Variables.captionStyle.copyWith(
681 fontSize: 11,
682 color: theme.colorScheme.onSurface.withOpacity(0.6),
683 height: 1.2,
684 ),
685 maxLines: 1,
686 overflow: TextOverflow.ellipsis,
687 ),
688 ],
689 ),
690 ),
691 );
692 },
693 ),
694 );
695 }
696}
697
698class _SeeAllPage extends StatefulWidget {
699 final String title;
700 final bool isProjects;
701 final ProjectRepo projectRepo;
702 final FileRepo fileRepo;
703 final ImageRepo imageRepo;
704 final Function(ProjectModel) onProjectTap;
705 final Function(FileModel) onFileTap;
706 final Function(ProjectModel) onProjectRename;
707 final Function(ProjectModel) onProjectDelete;
708
709 const _SeeAllPage({
710 required this.title,
711 required this.isProjects,
712 required this.projectRepo,
713 required this.fileRepo,
714 required this.imageRepo,
715 required this.onProjectTap,
716 required this.onFileTap,
717 required this.onProjectRename,
718 required this.onProjectDelete,
719 });
720
721 @override
722 State<_SeeAllPage> createState() => _SeeAllPageState();
723}
724
725class _SeeAllPageState extends State<_SeeAllPage> {
726 final _fileService = FileService();
727 bool _isLoading = true;
728 List<dynamic> _items = [];
729 Map<int, ProjectModel> _projectMap = {};
730 Map<int, List<String>> _projectPreviews = {};
731 Map<String, Map<String, String>> _fileMetadata = {};
732
733 @override
734 void initState() {
735 super.initState();
736 _fetchData();
737 }
738
739 Future<void> _fetchData() async {
740 setState(() => _isLoading = true);
741 try {
742 if (widget.isProjects) {
743 final projects = await widget.projectRepo.getAllProjects();
744 // Fetch Child Event Images for Grid Preview
745 for (final p in projects) {
746 if (p.id != null) {
747 final events = await widget.projectRepo.getEvents(p.id!);
748 List<String> eventImages = [];
749 for (final event in events.take(4)) {
750 if (event.id != null) {
751 final imgs = await widget.imageRepo.getImages(event.id!);
752 if (imgs.isNotEmpty) eventImages.add(imgs.first.filePath);
753 }
754 }
755 _projectPreviews[p.id!] = eventImages;
756 }
757 }
758 _items = projects;
759 } else {
760 final files = await widget.fileRepo.getRecentFiles(limit: 50);
761 final allProjects = await widget.projectRepo.getAllProjects();
762 _projectMap = {for (var p in allProjects) p.id!: p};
763 for (final file in files) {
764 final meta = await _fileService.getFileMetadata(file.filePath);
765 _fileMetadata[file.id] = {
766 'dimensions': meta.dimensions,
767 'preview': meta.previewPath ?? '',
768 };
769 }
770 _items = files;
771 }
772 } catch (e) {
773 debugPrint("Error loading see all data: $e");
774 } finally {
775 if (mounted) setState(() => _isLoading = false);
776 }
777 }
778
779 String _getBreadcrumb(FileModel file) {
780 final project = _projectMap[file.projectId];
781 if (project == null) return '';
782 if (project.parentId != null) {
783 final parent = _projectMap[project.parentId!];
784 if (parent != null) return '${parent.title} / ${project.title}';
785 }
786 return project.title;
787 }
788
789 String _timeAgo(DateTime dt) {
790 final diff = DateTime.now().difference(dt);
791 if (diff.inDays > 0) return 'Edited ${diff.inDays} days ago';
792 return 'Edited recently';
793 }
794
795 @override
796 Widget build(BuildContext context) {
797 final theme = Theme.of(context);
798 return Scaffold(
799 appBar: AppBar(
800 title: Text(
801 widget.title,
802 style: Variables.headerStyle.copyWith(
803 fontSize: 18,
804 fontWeight: FontWeight.w500,
805 color: theme.colorScheme.onSurface,
806 ),
807 ),
808 backgroundColor: Variables.background,
809 elevation: 0,
810 iconTheme: IconThemeData(color: theme.colorScheme.onSurface),
811 ),
812 backgroundColor: Variables.background,
813 body:
814 _isLoading
815 ? const Center(child: CircularProgressIndicator())
816 : Padding(
817 padding: const EdgeInsets.all(16.0),
818 child:
819 widget.isProjects
820 ? GridView.builder(
821 gridDelegate:
822 const SliverGridDelegateWithFixedCrossAxisCount(
823 crossAxisCount: 2,
824 childAspectRatio: 0.8,
825 crossAxisSpacing: 12,
826 mainAxisSpacing: 12,
827 ),
828 itemCount: _items.length,
829 itemBuilder: (context, index) {
830 final project = _items[index] as ProjectModel;
831 return ProjectCard(
832 project: project,
833 previewImages: _projectPreviews[project.id] ?? [],
834 onTap: () => widget.onProjectTap(project),
835 // Chain callbacks, reload data on completion
836 onRename: () async {
837 await widget.onProjectRename(project);
838 _fetchData();
839 },
840 onDelete: () async {
841 await widget.onProjectDelete(project);
842 _fetchData();
843 },
844 isHorizontal: false,
845 showGrid: true,
846 );
847 },
848 )
849 : ListView.builder(
850 itemCount: _items.length,
851 itemBuilder: (context, index) {
852 final file = _items[index] as FileModel;
853 final meta = _fileMetadata[file.id] ?? {};
854 return Padding(
855 padding: const EdgeInsets.only(bottom: 12),
856 child: FileCard(
857 file: file,
858 breadcrumb: _getBreadcrumb(file),
859 dimensions: meta['dimensions'] ?? 'Unknown',
860 previewPath: meta['preview'] ?? '',
861 timeAgo: _timeAgo(file.lastUpdated),
862 onTap: () => widget.onFileTap(file),
863 onMenuAction: null,
864 ),
865 );
866 },
867 ),
868 ),
869 );
870 }
871}