creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
commit 3bd9114473c592db6d4bae9acb51d082f6ad9426 parent aa6874fd76f8729e5cd549f671ec97843897666f Author: maydayv7 <maydayv7@gmail.com> Date: Fri, 28 Nov 2025 14:27:29 +0530 Merge remote-tracking branch 'origin/abhinav'
Diffstat:
| A | lib/ui/pages/project_board_page.dart | | | 344 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | lib/ui/pages/project_detail_page.dart | | | 679 | ++++++++++++++++++++++++++++++++----------------------------------------------- |
| A | lib/ui/pages/project_tag_page.dart | | | 137 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
3 files changed, 758 insertions(+), 402 deletions(-)
diff --git a/lib/ui/pages/project_board_page.dart b/lib/ui/pages/project_board_page.dart @@ -0,0 +1,343 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import '../../data/models/project_model.dart'; +import '../../data/models/image_model.dart'; +import '../../data/repos/project_repo.dart'; +import '../../data/repos/image_repo.dart'; +import 'project_tag_page.dart'; + +class ProjectBoardPage extends StatefulWidget { + final int projectId; + + const ProjectBoardPage({super.key, required this.projectId}); + + @override + State<ProjectBoardPage> createState() => _ProjectBoardPageState(); +} + +class _ProjectBoardPageState extends State<ProjectBoardPage> { + final _projectRepo = ProjectRepo(); + final _imageRepo = ImageRepo(); + + ProjectModel? _mainProject; + List<ProjectModel> _events = []; + ProjectModel? _selectedProject; + + Map<String, List<ImageModel>> _categorizedImages = {}; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _initData(); + } + + Future<void> _initData() async { + final events = await _projectRepo.getEvents(widget.projectId); + + _mainProject = ProjectModel( + id: widget.projectId, + title: "Main Project", + lastAccessedAt: DateTime.now(), + createdAt: DateTime.now(), + ); + + _events = events; + _selectedProject = _mainProject; + + await _loadImagesForSelected(); + } + + Future<void> _loadImagesForSelected() async { + if (_selectedProject?.id == null) return; + + setState(() => _isLoading = true); + + final images = await _imageRepo.getImages(_selectedProject!.id!); + _categorizeImages(images); + + setState(() => _isLoading = false); + } + + void _categorizeImages(List<ImageModel> images) { + _categorizedImages.clear(); + + for (var img in images) { + if (img.tags.isEmpty) { + if (!_categorizedImages.containsKey('Uncategorized')) { + _categorizedImages['Uncategorized'] = []; + } + _categorizedImages['Uncategorized']!.add(img); + } else { + for (var tag in img.tags) { + if (!_categorizedImages.containsKey(tag)) { + _categorizedImages[tag] = []; + } + _categorizedImages[tag]!.add(img); + } + } + } + } + + void _onProjectChanged(ProjectModel? newValue) { + if (newValue != null && newValue != _selectedProject) { + setState(() { + _selectedProject = newValue; + }); + _loadImagesForSelected(); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + final buttonColor = isDark ? Colors.white.withOpacity(0.1) : Colors.grey[100]; + + final List<DropdownMenuItem<ProjectModel>> dropdownItems = []; + if (_mainProject != null) { + dropdownItems.add( + DropdownMenuItem( + value: _mainProject, + child: Text( + _mainProject!.title, + style: const TextStyle(fontFamily: 'GeneralSans'), + ), + ), + ); + } + for (var event in _events) { + dropdownItems.add( + DropdownMenuItem( + value: event, + child: Text( + event.title, + style: const TextStyle(fontFamily: 'GeneralSans'), + ), + ), + ); + } + + return Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, + appBar: AppBar( + backgroundColor: theme.appBarTheme.backgroundColor, + centerTitle: false, + title: Text( + _mainProject?.title ?? "Project", + style: TextStyle( + fontFamily: 'GeneralSans', + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), + ), + actions: [ + IconButton( + icon: Icon(Icons.settings_outlined, color: theme.iconTheme.color), + onPressed: () {}, + ), + const SizedBox(width: 8), + ], + ), + body: Column( + children: [ + // Controls Row + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + children: [ + Expanded( + child: Container( + height: 40, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: buttonColor, + borderRadius: BorderRadius.circular(10), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton<ProjectModel>( + value: _selectedProject, + isExpanded: true, + items: dropdownItems, + onChanged: _onProjectChanged, + style: TextStyle( + color: theme.colorScheme.onSurface, + fontFamily: 'GeneralSans', + fontWeight: FontWeight.w500, + fontSize: 14, + ), + icon: Icon(Icons.keyboard_arrow_down, size: 18, color: theme.iconTheme.color), + dropdownColor: theme.cardColor, + ), + ), + ), + ), + const SizedBox(width: 8), + _buildControlIcon( + theme, + buttonColor, + Icons.palette_outlined, + "Stylesheet", + () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Stylesheet")), + ); + } + ), + const SizedBox(width: 8), + _buildControlIcon( + theme, + buttonColor, + Icons.tune_outlined, + "Filter", + () {} + ), + ], + ), + ), + + // Main Content + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _categorizedImages.isEmpty + ? Center( + child: Text( + "No images found", + style: TextStyle( + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface.withOpacity(0.6), + ), + ), + ) + : ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + itemCount: _categorizedImages.keys.length, + itemBuilder: (context, index) { + final category = _categorizedImages.keys.elementAt(index); + final images = _categorizedImages[category]!; + + return GestureDetector( + onTap: () { + if (_selectedProject?.id != null) { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ProjectTagPage( + projectId: _selectedProject!.id!, + tag: category, + ), + ), + ); + } + }, + child: Container( + margin: const EdgeInsets.only(bottom: 16), + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: isDark ? const Color(0xFF1E1E1E) : Colors.white, + border: Border.all(color: Colors.grey.withOpacity(0.3)), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Category Header + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + category.toUpperCase(), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + letterSpacing: 1.0, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface, + ), + ), + Icon( + Icons.arrow_forward, + size: 16, + color: theme.colorScheme.onSurface.withOpacity(0.4), + ), + ], + ), + ), + + // Image List + SizedBox( + height: 140, // Adjusted height since footer is gone + child: ListView.separated( + clipBehavior: Clip.none, + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + scrollDirection: Axis.horizontal, + itemCount: images.length, + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemBuilder: (context, imgIndex) { + final image = images[imgIndex]; + return _buildImageCard(image, theme, isDark); + }, + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildControlIcon( + ThemeData theme, + Color? bgColor, + IconData icon, + String tooltip, + VoidCallback onTap + ) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, size: 20, color: theme.iconTheme.color), + ), + ); + } + + Widget _buildImageCard(ImageModel image, ThemeData theme, bool isDark) { + return Container( + width: 120, + clipBehavior: Clip.antiAlias, // Ensures image doesn't overflow rounded corners + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: isDark ? Colors.black26 : Colors.grey[200], + border: Border.all(color: theme.dividerColor.withOpacity(0.1)), + ), + child: Stack( + fit: StackFit.expand, // Forces image to fill the entire container + children: [ + Image.file( + File(image.filePath), + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + color: Colors.grey[300], + child: const Icon(Icons.broken_image, color: Colors.grey), + ), + ), + ], + ), + ); + } +} +\ No newline at end of file diff --git a/lib/ui/pages/project_detail_page.dart b/lib/ui/pages/project_detail_page.dart @@ -1,13 +1,9 @@ import 'dart:io'; -import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:adobe/data/models/project_model.dart'; -import 'package:adobe/data/models/image_model.dart'; -import 'package:adobe/data/models/file_model.dart'; -import 'package:adobe/data/repos/project_repo.dart'; -import 'package:adobe/data/repos/image_repo.dart'; -import 'package:adobe/data/repos/file_repo.dart'; -import 'package:adobe/services/project_service.dart'; +import '../../data/models/project_model.dart'; +import '../../data/repos/project_repo.dart'; +import '../../services/project_service.dart'; +import 'project_board_page.dart'; // Import the board page class ProjectDetailPage extends StatefulWidget { final int projectId; @@ -18,56 +14,42 @@ class ProjectDetailPage extends StatefulWidget { State<ProjectDetailPage> createState() => _ProjectDetailPageState(); } -class _ProjectDetailPageState extends State<ProjectDetailPage> - with SingleTickerProviderStateMixin { +class _ProjectDetailPageState extends State<ProjectDetailPage> { final _projectRepo = ProjectRepo(); - final _imageRepo = ImageRepo(); - final _fileRepo = FileRepo(); final _projectService = ProjectService(); ProjectModel? _project; - List<ImageModel> _images = []; - List<FileModel> _files = []; List<ProjectModel> _events = []; bool _isLoading = true; - late TabController _tabController; @override void initState() { super.initState(); - _tabController = TabController(length: 3, vsync: this); _loadData(); } - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - Future<void> _loadData() async { setState(() => _isLoading = true); try { final project = await _projectRepo.getProjectById(widget.projectId); + + // If project not found, handle exit if (project == null) { if (mounted) { Navigator.pop(context); - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Project not found'))); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Project not found')), + ); } return; } - final images = await _imageRepo.getImages(widget.projectId); - final files = await _fileRepo.getFiles(widget.projectId); + // Fetch sub-events final events = await _projectRepo.getEvents(widget.projectId); if (mounted) { setState(() { _project = project; - _images = images; - _files = files; _events = events; _isLoading = false; }); @@ -76,9 +58,6 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> debugPrint('Error loading project data: $e'); if (mounted) { setState(() => _isLoading = false); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Error loading project: $e'))); } } } @@ -93,81 +72,87 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> await showDialog( context: context, - builder: - (context) => AlertDialog( - title: const Text( - "Create New Event", - style: TextStyle( - fontFamily: 'GeneralSans', - fontWeight: FontWeight.w600, + builder: (context) => AlertDialog( + title: const Text( + "Create New Event", + style: TextStyle( + fontFamily: 'GeneralSans', + fontWeight: FontWeight.w600, + ), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: nameController, + decoration: const InputDecoration( + hintText: "Event Name", + labelText: "Name", + border: OutlineInputBorder(), ), + autofocus: true, + style: const TextStyle(fontFamily: 'GeneralSans'), ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: nameController, - decoration: const InputDecoration( - hintText: "Event Name", - labelText: "Name", - border: OutlineInputBorder(), - ), - autofocus: true, - style: const TextStyle(fontFamily: 'GeneralSans'), - ), - const SizedBox(height: 16), - TextField( - controller: descriptionController, - decoration: const InputDecoration( - hintText: "Description (optional)", - labelText: "Description", - border: OutlineInputBorder(), - ), - maxLines: 3, - style: const TextStyle(fontFamily: 'GeneralSans'), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text( - "Cancel", - style: TextStyle(fontFamily: 'GeneralSans'), - ), + const SizedBox(height: 16), + TextField( + controller: descriptionController, + decoration: const InputDecoration( + hintText: "Description (optional)", + labelText: "Description", + border: OutlineInputBorder(), ), - ElevatedButton( - onPressed: () async { - if (nameController.text.trim().isNotEmpty) { - try { - await _projectService.createProject( - nameController.text.trim(), - description: - descriptionController.text.trim().isEmpty - ? null - : descriptionController.text.trim(), - parentId: _project!.id, - ); - if (context.mounted) { - Navigator.pop(context); - _loadData(); - } - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error creating event: $e')), - ); - } - } + maxLines: 3, + style: const TextStyle(fontFamily: 'GeneralSans'), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text("Cancel"), + ), + ElevatedButton( + onPressed: () async { + if (nameController.text.trim().isNotEmpty) { + try { + await _projectService.createProject( + nameController.text.trim(), + description: descriptionController.text.trim().isEmpty + ? null + : descriptionController.text.trim(), + parentId: _project!.id, + ); + if (context.mounted) { + Navigator.pop(context); + _loadData(); // Refresh to show new event } - }, - child: const Text( - "Create", - style: TextStyle(fontFamily: 'GeneralSans'), - ), - ), - ], + } catch (e) { + debugPrint("Error creating event: $e"); + } + } + }, + child: const Text("Create"), ), + ], + ), + ); + } + + void _navigateToBoard(int projectId) { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ProjectBoardPage(projectId: projectId), + ), + ).then((_) => _loadData()); // Refresh on return (e.g. last accessed update) + } + + void _showPlaceholder(String feature) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('$feature coming soon!'), + duration: const Duration(seconds: 1), + ), ); } @@ -179,365 +164,255 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> if (_isLoading) { return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, - appBar: AppBar( - title: const Text("Project"), - backgroundColor: theme.appBarTheme.backgroundColor, - ), + appBar: AppBar(title: const Text("Loading...")), body: const Center(child: CircularProgressIndicator()), ); } - if (_project == null) { - return Scaffold( - backgroundColor: theme.scaffoldBackgroundColor, - appBar: AppBar( - title: const Text("Project"), - backgroundColor: theme.appBarTheme.backgroundColor, - ), - body: const Center(child: Text("Project not found")), - ); - } + if (_project == null) return const Scaffold(body: SizedBox()); return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, appBar: AppBar( title: Text(_project!.title), backgroundColor: theme.appBarTheme.backgroundColor, - bottom: TabBar( - controller: _tabController, - tabs: const [ - Tab(text: "Moodboard"), - Tab(text: "Stylesheet"), - Tab(text: "Files"), - ], - labelStyle: const TextStyle( - fontFamily: 'GeneralSans', - fontWeight: FontWeight.w600, - ), - ), ), - body: Column( - children: [ - // Tab Content - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - _buildMoodboardTab(theme, isDark), - _buildStylesheetTab(theme, isDark), - _buildFilesTab(theme, isDark), - ], - ), - ), - // Events Section - Container( - decoration: BoxDecoration( - color: isDark ? Colors.grey[850] : Colors.grey[50], - border: Border( - top: BorderSide( - color: isDark ? Colors.grey[700]! : Colors.grey[300]!, - width: 1, + body: RefreshIndicator( + onRefresh: _loadData, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 1. Main Project Details & Actions + _buildSectionHeader("Project Overview", theme), + const SizedBox(height: 8), + if (_project!.description != null && _project!.description!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text( + _project!.description!, + style: TextStyle( + fontSize: 14, + color: theme.colorScheme.onSurface.withOpacity(0.7), + fontFamily: 'GeneralSans', + ), + ), ), + + // Actions for Main Project + _buildActionRow(_project!.id!, theme, isDark), + + const SizedBox(height: 32), + + // 2. Events Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildSectionHeader("Events", theme), + IconButton( + onPressed: _createEventDialog, + icon: Icon(Icons.add_circle, color: theme.colorScheme.primary), + tooltip: "Add Event", + ) + ], ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(20, 16, 20, 12), - child: Row( + const SizedBox(height: 8), + + // 3. Events List + if (_events.isEmpty) + Container( + width: double.infinity, + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: isDark ? Colors.grey[850] : Colors.grey[100], + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDark ? Colors.grey[800]! : Colors.grey[300]!, + ), + ), + child: Column( children: [ + Icon(Icons.event_note, size: 48, color: Colors.grey[400]), + const SizedBox(height: 8), Text( - "Events", - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface, - ), - ), - const Spacer(), - IconButton( - onPressed: _createEventDialog, - icon: const Icon(Icons.add), - tooltip: 'Create Event', + "No events created yet", + style: TextStyle(color: Colors.grey[500]), ), ], ), + ) + else + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _events.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final event = _events[index]; + return _buildEventCard(event, theme, isDark); + }, ), - SizedBox( - height: 120, - child: - _events.isEmpty - ? Center( - child: Text( - "No events yet. Tap + to create one.", - style: TextStyle( - color: theme.colorScheme.onSurface.withOpacity( - 0.6, - ), - fontFamily: 'GeneralSans', - ), - ), - ) - : ListView.builder( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 20), - itemCount: _events.length, - itemBuilder: (context, index) { - final event = _events[index]; - return _buildEventCard(event, theme, isDark); - }, - ), - ), - const SizedBox(height: 8), - ], - ), + + const SizedBox(height: 40), + ], ), - ], + ), ), ); } - Widget _buildMoodboardTab(ThemeData theme, bool isDark) { - if (_images.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.image_outlined, - size: 64, - color: theme.colorScheme.onSurface.withOpacity(0.3), - ), - const SizedBox(height: 16), - Text( - "No images in moodboard", - style: TextStyle( - fontSize: 18, - color: theme.colorScheme.onSurface.withOpacity(0.6), - fontFamily: 'GeneralSans', - ), - ), - ], - ), - ); - } + // --- Widgets --- - return GridView.builder( - padding: const EdgeInsets.all(16), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - childAspectRatio: 0.85, + Widget _buildSectionHeader(String title, ThemeData theme) { + return Text( + title, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface, ), - itemCount: _images.length, - itemBuilder: (context, index) { - final image = _images[index]; - return ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.file( - File(image.filePath), - fit: BoxFit.cover, - errorBuilder: - (context, error, stackTrace) => Container( - color: isDark ? Colors.grey[700] : Colors.grey[200], - child: Icon( - Icons.broken_image, - color: theme.colorScheme.onSurface.withOpacity(0.3), - ), - ), - ), - ); - }, ); } - Widget _buildStylesheetTab(ThemeData theme, bool isDark) { - final stylesheet = _project?.styleSheetMap ?? {}; - - return SingleChildScrollView( - padding: const EdgeInsets.all(20), + Widget _buildEventCard(ProjectModel event, ThemeData theme, bool isDark) { + return Container( + decoration: BoxDecoration( + color: isDark ? Colors.grey[850] : Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isDark ? Colors.grey[700]! : Colors.grey[300]!, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 8, + offset: const Offset(0, 2), + ) + ], + ), + padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Global Stylesheet", - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface, - ), - ), - const SizedBox(height: 16), - if (stylesheet.isEmpty) - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: isDark ? Colors.grey[800] : Colors.grey[100], - borderRadius: BorderRadius.circular(12), - ), - child: Center( + Row( + children: [ + Icon(Icons.event, size: 20, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Expanded( child: Text( - "No stylesheet data yet", - style: TextStyle( - color: theme.colorScheme.onSurface.withOpacity(0.6), + event.title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, fontFamily: 'GeneralSans', ), ), ), - ) - else - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: isDark ? Colors.grey[800] : Colors.grey[100], - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isDark ? Colors.grey[700]! : Colors.grey[300]!, - ), - ), - child: SelectableText( - const JsonEncoder.withIndent(' ').convert(stylesheet), - style: TextStyle( - fontFamily: 'monospace', - fontSize: 12, - color: theme.colorScheme.onSurface, - ), + ], + ), + if (event.description != null && event.description!.isNotEmpty) ...[ + const SizedBox(height: 6), + Text( + event.description!, + style: TextStyle( + fontSize: 13, + color: theme.colorScheme.onSurface.withOpacity(0.6), ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), + ], + const SizedBox(height: 16), + // Actions for this specific Event + _buildActionRow(event.id!, theme, isDark, isSmall: true), ], ), ); } - Widget _buildFilesTab(ThemeData theme, bool isDark) { - if (_files.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.folder_outlined, - size: 64, - color: theme.colorScheme.onSurface.withOpacity(0.3), - ), - const SizedBox(height: 16), - Text( - "No files yet", - style: TextStyle( - fontSize: 18, - color: theme.colorScheme.onSurface.withOpacity(0.6), - fontFamily: 'GeneralSans', - ), - ), - ], + /// Reusable row of actions (Moodboard, Stylesheet, Files) + Widget _buildActionRow(int targetId, ThemeData theme, bool isDark, {bool isSmall = false}) { + return Row( + children: [ + Expanded( + child: _buildActionButton( + label: "Moodboard", + icon: Icons.dashboard_outlined, + color: Colors.purple, // Distinct color for main action + theme: theme, + isDark: isDark, + isSmall: isSmall, + onTap: () => _navigateToBoard(targetId), + ), ), - ); - } - - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: _files.length, - itemBuilder: (context, index) { - final file = _files[index]; - return Card( - margin: const EdgeInsets.only(bottom: 12), - color: isDark ? Colors.grey[800] : Colors.grey[100], - child: ListTile( - leading: Icon( - Icons.insert_drive_file, - color: theme.colorScheme.primary, - ), - title: Text( - file.name, - style: const TextStyle( - fontFamily: 'GeneralSans', - fontWeight: FontWeight.w600, - ), - ), - subtitle: - file.description != null - ? Text( - file.description!, - style: const TextStyle(fontFamily: 'GeneralSans'), - ) - : null, - trailing: Icon( - Icons.chevron_right, - color: theme.colorScheme.onSurface.withOpacity(0.5), - ), - onTap: () { - // TODO: Open file viewer/editor - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Opening ${file.name}...')), - ); - }, + const SizedBox(width: 8), + Expanded( + child: _buildActionButton( + label: "Stylesheet", + icon: Icons.palette_outlined, + color: Colors.blue, + theme: theme, + isDark: isDark, + isSmall: isSmall, + onTap: () => _showPlaceholder("Stylesheet"), + ), + ), + const SizedBox(width: 8), + Expanded( + child: _buildActionButton( + label: "Files", + icon: Icons.folder_open_outlined, + color: Colors.orange, + theme: theme, + isDark: isDark, + isSmall: isSmall, + onTap: () => _showPlaceholder("Files"), ), - ); - }, + ), + ], ); } - Widget _buildEventCard(ProjectModel event, ThemeData theme, bool isDark) { - return GestureDetector( - onTap: () { - // Navigate to event detail (could be same page or different) - if (event.id != null) { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ProjectDetailPage(projectId: event.id!), + Widget _buildActionButton({ + required String label, + required IconData icon, + required Color color, + required ThemeData theme, + required bool isDark, + required VoidCallback onTap, + bool isSmall = false, + }) { + return Material( + color: isDark ? Colors.grey[800] : Colors.grey[100], + borderRadius: BorderRadius.circular(12), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: EdgeInsets.symmetric(vertical: isSmall ? 10 : 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDark ? Colors.grey[700]! : Colors.grey[300]!, ), - ).then((_) => _loadData()); - } - }, - child: Container( - width: 200, - margin: const EdgeInsets.only(right: 12), - decoration: BoxDecoration( - color: isDark ? Colors.grey[800] : Colors.grey[100], - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isDark ? Colors.grey[700]! : Colors.grey[300]!, - width: 1, ), - ), - child: Padding( - padding: const EdgeInsets.all(16), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.event, size: 32, color: theme.colorScheme.primary), - const SizedBox(height: 8), + Icon(icon, size: isSmall ? 20 : 24, color: color), + const SizedBox(height: 6), Text( - event.title, + label, style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - fontFamily: 'GeneralSans', + fontSize: isSmall ? 11 : 13, + fontWeight: FontWeight.w500, color: theme.colorScheme.onSurface, + fontFamily: 'GeneralSans', ), - maxLines: 2, - overflow: TextOverflow.ellipsis, ), - if (event.description != null && - event.description!.isNotEmpty) ...[ - const SizedBox(height: 4), - Text( - event.description!, - style: TextStyle( - fontSize: 12, - color: theme.colorScheme.onSurface.withOpacity(0.6), - fontFamily: 'GeneralSans', - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], ], ), ), diff --git a/lib/ui/pages/project_tag_page.dart b/lib/ui/pages/project_tag_page.dart @@ -0,0 +1,136 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import '../../data/models/image_model.dart'; +import '../../data/repos/image_repo.dart'; + +class ProjectTagPage extends StatefulWidget { + final int projectId; + final String tag; + + const ProjectTagPage({ + super.key, + required this.projectId, + required this.tag, + }); + + @override + State<ProjectTagPage> createState() => _ProjectTagPageState(); +} + +class _ProjectTagPageState extends State<ProjectTagPage> { + final _imageRepo = ImageRepo(); + List<ImageModel> _images = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadImages(); + } + + Future<void> _loadImages() async { + final allImages = await _imageRepo.getImages(widget.projectId); + + final filtered = allImages.where((img) { + if (widget.tag == 'Uncategorized') { + return img.tags.isEmpty; + } + return img.tags.contains(widget.tag); + }).toList(); + + if (mounted) { + setState(() { + _images = filtered; + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + return Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, + appBar: AppBar( + title: Text( + widget.tag.toUpperCase(), + style: const TextStyle( + fontFamily: 'GeneralSans', + fontWeight: FontWeight.w600, + letterSpacing: 1.0, + ), + ), + centerTitle: false, // Changed to false for left alignment + backgroundColor: theme.appBarTheme.backgroundColor, + elevation: 0, + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _images.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.image_not_supported_outlined, + size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + "No images found for '${widget.tag}'", + style: TextStyle( + fontFamily: 'GeneralSans', + fontSize: 16, + color: Colors.grey[600], + ), + ), + ], + ), + ) + : GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 0.8, + ), + itemCount: _images.length, + itemBuilder: (context, index) { + final image = _images[index]; + return GestureDetector( + onTap: () { + // Navigate to detail view + }, + child: Container( + decoration: BoxDecoration( + color: isDark ? const Color(0xFF1E1E1E) : Colors.grey[200], + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isDark ? Colors.white10 : Colors.transparent, + ), + ), + clipBehavior: Clip.antiAlias, + child: Stack( + fit: StackFit.expand, + children: [ + Image.file( + File(image.filePath), + fit: BoxFit.cover, + width: double.infinity, + errorBuilder: (_, __, ___) => Container( + color: Colors.grey[300], + child: const Center( + child: Icon(Icons.broken_image, color: Colors.grey), + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} +\ No newline at end of file