creek

The AI Image Editor of 2030

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}