creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
commit 2d00a0482dd41dd87418d0ade5664764ba3646b0 parent 2287021ee109f8564d2f9036271a4ac2ac35f32c Author: ajcoder13 <avnijhalani@gmail.com> Date: Fri, 28 Nov 2025 16:47:20 +0530 UI fixed
Diffstat:
| M | lib/ui/pages/share_to_moodboard_page.dart | | | 470 | +++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------- |
1 file changed, 314 insertions(+), 156 deletions(-)
diff --git a/lib/ui/pages/share_to_moodboard_page.dart b/lib/ui/pages/share_to_moodboard_page.dart @@ -5,6 +5,7 @@ import 'package:receive_sharing_intent/receive_sharing_intent.dart'; // Models & Repos import '../../data/models/project_model.dart'; import '../../data/repos/project_repo.dart'; +import '../../data/repos/image_repo.dart'; // Services import '../../services/project_service.dart'; @@ -12,20 +13,37 @@ import '../../services/project_service.dart'; // Pages import 'image_save_page.dart'; -// Helper model for UI rendering +// --- VIEW MODELS --- + class ProjectItemViewModel { final ProjectModel item; final String? parentTitle; + final String? coverPath; - ProjectItemViewModel({required this.item, this.parentTitle}); + ProjectItemViewModel({required this.item, this.parentTitle, this.coverPath}); String get title => item.title; bool get isEvent => item.isEvent; int get id => item.id!; } +class ProjectGroup { + final ProjectModel project; + final List<ProjectItemViewModel> events; + final String? coverPath; + bool isExpanded; + + ProjectGroup({ + required this.project, + this.events = const [], + this.coverPath, + this.isExpanded = false, + }); +} + +// --- WIDGET --- + class ShareToMoodboardPage extends StatefulWidget { - // CHANGED: Now accepts a List of files instead of a single file final List<File> imageFiles; const ShareToMoodboardPage({super.key, required this.imageFiles}); @@ -36,11 +54,12 @@ class ShareToMoodboardPage extends StatefulWidget { class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> { final ProjectRepo _projectRepo = ProjectRepo(); + final ImageRepo _imageRepo = ImageRepo(); final ProjectService _projectService = ProjectService(); List<ProjectItemViewModel> _recentViewModels = []; - List<ProjectItemViewModel> _allViewModels = []; - List<ProjectItemViewModel> _filteredViewModels = []; + List<ProjectGroup> _groupedProjects = []; + List<ProjectGroup> _filteredGroupedProjects = []; bool _isLoading = true; String _searchQuery = ""; @@ -58,54 +77,105 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> { super.dispose(); } + /// Helper to fetch the latest image for a project to use as cover + Future<String?> _getProjectCover(int projectId) async { + try { + final images = await _imageRepo.getImages(projectId); + if (images.isNotEmpty) { + return images.first.filePath; + } + } catch (e) { + debugPrint("Error fetching cover for project $projectId: $e"); + } + return null; + } + Future<void> _loadData() async { setState(() => _isLoading = true); - // Fetch raw data + // 1. Fetch Raw Data final recentItems = await _projectRepo.getRecentProjectsAndEvents(); - final allItems = await _projectRepo.getAllProjectsAndEvents(); - - // Build view models with parent titles - _recentViewModels = await _buildViewModels(recentItems.take(3).toList()); - _allViewModels = await _buildViewModels(allItems); - _filteredViewModels = _allViewModels; + final allProjects = await _projectRepo.getAllProjects(); - setState(() => _isLoading = false); - } - - Future<List<ProjectItemViewModel>> _buildViewModels( - List<ProjectModel> items, - ) async { - final List<ProjectItemViewModel> viewModels = []; - for (var item in items) { + // 2. Build Recent View Models + final List<ProjectItemViewModel> recents = []; + for (var item in recentItems.take(3)) { String? parentTitle; if (item.parentId != null) { final parent = await _projectRepo.getProjectById(item.parentId!); parentTitle = parent?.title; } - viewModels.add( - ProjectItemViewModel(item: item, parentTitle: parentTitle), + + final cover = await _getProjectCover(item.id!); + + recents.add( + ProjectItemViewModel( + item: item, + parentTitle: parentTitle, + coverPath: cover, + ), ); } - return viewModels; + _recentViewModels = recents; + + // 3. Build Grouped Projects + final List<ProjectGroup> groups = []; + for (final p in allProjects) { + // Fetch Events + final rawEvents = await _projectRepo.getEvents(p.id!); + + // Convert Events to ViewModels (to get their covers) + final List<ProjectItemViewModel> eventVMs = []; + for (final e in rawEvents) { + final eCover = await _getProjectCover(e.id!); + eventVMs.add(ProjectItemViewModel(item: e, coverPath: eCover)); + } + + final pCover = await _getProjectCover(p.id!); + + groups.add(ProjectGroup(project: p, events: eventVMs, coverPath: pCover)); + } + + _groupedProjects = groups; + _filteredGroupedProjects = groups; + + if (mounted) setState(() => _isLoading = false); } void _filterProjects(String query) { setState(() { _searchQuery = query; if (query.isEmpty) { - _filteredViewModels = _allViewModels; + _filteredGroupedProjects = _groupedProjects; } else { - _filteredViewModels = - _allViewModels.where((vm) { - final titleMatches = vm.title.toLowerCase().contains( - query.toLowerCase(), - ); - final parentMatches = - vm.parentTitle?.toLowerCase().contains(query.toLowerCase()) ?? - false; - return titleMatches || parentMatches; - }).toList(); + final q = query.toLowerCase(); + final List<ProjectGroup> filtered = []; + for (final g in _groupedProjects) { + final projectMatch = g.project.title.toLowerCase().contains(q); + final matchingEvents = + g.events.where((e) => e.title.toLowerCase().contains(q)).toList(); + + if (projectMatch) { + filtered.add( + ProjectGroup( + project: g.project, + events: g.events, + isExpanded: true, + coverPath: g.coverPath, + ), + ); + } else if (matchingEvents.isNotEmpty) { + filtered.add( + ProjectGroup( + project: g.project, + events: matchingEvents, + isExpanded: true, + coverPath: g.coverPath, + ), + ); + } + } + _filteredGroupedProjects = filtered; } }); } @@ -137,30 +207,25 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> { ); if (title != null && title.isNotEmpty) { - // Create the project first final newId = await _projectService.createProject(title); + await _loadData(); _navigateToSavePage(newId, title); } } void _navigateToSavePage(int projectId, String projectName) { - // 1. Update "Recently Used" status _projectService.openProject(projectId); - - // 2. Reset Share Intent ReceiveSharingIntent.instance.reset(); - // 3. Navigate to ImageSavePage with the LIST of paths Navigator.push( context, MaterialPageRoute( builder: (_) => ImageSavePage( - // Convert List<File> to List<String> paths imagePaths: widget.imageFiles.map((f) => f.path).toList(), projectId: projectId, projectName: projectName, - isFromShare: true, // This enables SystemNavigator.pop() on finish + isFromShare: true, ), ), ); @@ -194,7 +259,7 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> { ? const Center(child: CircularProgressIndicator()) : Column( children: [ - // --- Search Bar --- + // Search Bar Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: TextField( @@ -237,7 +302,7 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // --- Recent Projects/Events Section --- + // --- RECENT SECTION --- if (_searchQuery.isEmpty && _recentViewModels.isNotEmpty) ...[ const Padding( @@ -254,27 +319,21 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> { ), ), ), - SizedBox( - height: 90, - child: ListView.separated( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - itemCount: _recentViewModels.length, - separatorBuilder: - (_, __) => const SizedBox(width: 12), - itemBuilder: (context, index) { - return _buildRecentItem( - _recentViewModels[index], - ); - }, + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Column( + children: + _recentViewModels + .map((vm) => _buildRecentItem(vm)) + .toList(), ), ), const SizedBox(height: 24), ], - // --- All Projects/Events Section --- + // --- ALL PROJECTS SECTION --- Padding( padding: const EdgeInsets.symmetric( horizontal: 16, @@ -291,13 +350,19 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> { ), ), ), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: _filteredViewModels.length, - itemBuilder: (context, index) { - return _buildAllItem(_filteredViewModels[index]); - }, + + // Add padding here so the cards don't touch the edges of the screen + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _filteredGroupedProjects.length, + itemBuilder: + (context, index) => _buildProjectGroup( + _filteredGroupedProjects[index], + ), + ), ), const SizedBox(height: 20), ], @@ -309,110 +374,203 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> { ); } - // --- Widget for Recent Items (Horizontal List) --- + // --- WIDGETS --- + Widget _buildRecentItem(ProjectItemViewModel vm) { - return InkWell( - onTap: () => _navigateToSavePage(vm.id, vm.title), - borderRadius: BorderRadius.circular(16), - child: Container( - width: 220, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.grey.shade200), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - children: [ - Container( - width: 60, - height: 60, - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(12), + return Container( + margin: const EdgeInsets.only(bottom: 12), + child: InkWell( + onTap: () => _navigateToSavePage(vm.id, vm.title), + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), ), - child: Icon(Icons.image, color: Colors.grey[400], size: 30), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (vm.isEvent && vm.parentTitle != null) + ], + ), + child: Row( + children: [ + // Cover Image + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(12), + image: + vm.coverPath != null + ? DecorationImage( + image: FileImage(File(vm.coverPath!)), + fit: BoxFit.cover, + ) + : null, + ), + child: + vm.coverPath == null + ? Icon(Icons.image, color: Colors.grey[400], size: 30) + : null, + ), + const SizedBox(width: 16), + // Text Content + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (vm.isEvent && vm.parentTitle != null) + Text( + vm.parentTitle!, + style: TextStyle(fontSize: 13, color: Colors.grey[600]), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), Text( - vm.parentTitle!, - style: TextStyle(fontSize: 13, color: Colors.grey[600]), - maxLines: 1, + vm.title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 2, overflow: TextOverflow.ellipsis, ), - Text( - vm.title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], + ], + ), ), - ), - ], + ], + ), ), ), ); } - // --- Widget for All Items (Vertical List) --- - Widget _buildAllItem(ProjectItemViewModel vm) { - return ListTile( - onTap: () => _navigateToSavePage(vm.id, vm.title), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - leading: Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - vm.isEvent ? Icons.event : Icons.folder, - color: Colors.grey[500], - ), + // UPDATED: Full Border + Light Grey Background for Sub-events + Widget _buildProjectGroup(ProjectGroup g) { + final project = g.project; + final hasEvents = g.events.isNotEmpty; + + return Container( + margin: const EdgeInsets.only(bottom: 12), // Separate cards + clipBehavior: + Clip.antiAlias, // Clip children (like the grey background) to the rounded corners + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.shade200), // FULL BORDER ), - title: - vm.isEvent && vm.parentTitle != null - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - vm.parentTitle!, - style: TextStyle(fontSize: 13, color: Colors.grey[600]), - ), - Text( - vm.title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ], - ) - : Text( - vm.title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, + child: Column( + children: [ + // PARENT PROJECT + ListTile( + onTap: + () => + hasEvents + ? setState(() => g.isExpanded = !g.isExpanded) + : _navigateToSavePage(project.id!, project.title), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + leading: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(12), + image: + g.coverPath != null + ? DecorationImage( + image: FileImage(File(g.coverPath!)), + fit: BoxFit.cover, + ) + : null, + ), + child: + g.coverPath == null + ? Icon(Icons.folder, color: Colors.grey[500]) + : null, + ), + title: Text( + project.title, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + trailing: + hasEvents + ? Icon( + g.isExpanded + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down, + color: Colors.grey[600], + ) + : null, + ), + + // CHILDREN (EVENTS) with GREY BACKGROUND + if (hasEvents) + AnimatedCrossFade( + firstChild: const SizedBox.shrink(), + secondChild: Container( + width: double.infinity, // Stretch to full width + color: Colors.grey[50], // LIGHT GREY BACKGROUND + child: Column( + children: + g.events.map((e) { + return ListTile( + onTap: () => _navigateToSavePage(e.id, e.title), + contentPadding: const EdgeInsets.symmetric( + horizontal: 28, + vertical: 6, + ), + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: + Colors + .white, // White box on grey bg looks nice + borderRadius: BorderRadius.circular(10), + image: + e.coverPath != null + ? DecorationImage( + image: FileImage(File(e.coverPath!)), + fit: BoxFit.cover, + ) + : null, + ), + child: + e.coverPath == null + ? const Icon( + Icons.event, + color: Colors.grey, + ) + : null, + ), + title: Text( + e.title, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ); + }).toList(), ), ), + crossFadeState: + g.isExpanded + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 200), + ), + ], + ), ); } }