creek

The AI Image Editor of 2030
commit dfe1a3d56e61f957881f08376be5847b3660091c
parent 6a6713a59a3875805a1a0ec97204422629aef83a
Author: maydayv7 <maydayv7@gmail.com>
Date:   Sat,  6 Dec 2025 01:30:31 +0530

Make widgets for search bar, empty state, file and project cards

Diffstat:
Mlib/ui/pages/create_file_page.dart | 119++++++++++++++++++++-----------------------------------------------------------
Mlib/ui/pages/home_page.dart | 635+++++++------------------------------------------------------------------------
Mlib/ui/pages/project_detail_page.dart | 262++++++++++++++++++++-----------------------------------------------------------
Mlib/ui/pages/project_file_page.dart | 328+++++++++++++++++--------------------------------------------------------------
Mlib/ui/pages/stylesheet_page.dart | 181++++++++++++++++++++++++++++---------------------------------------------------
Mlib/ui/styles/variables.dart | 13+++++++++----
Alib/ui/widgets/empty_state.dart | 47+++++++++++++++++++++++++++++++++++++++++++++++
Alib/ui/widgets/file_card.dart | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/ui/widgets/project_card.dart | 254+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/ui/widgets/search_bar.dart | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/ui/widgets/section_header.dart | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 921 insertions(+), 1250 deletions(-)

diff --git a/lib/ui/pages/create_file_page.dart b/lib/ui/pages/create_file_page.dart @@ -4,12 +4,14 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:creekui/services/project_service.dart'; import 'package:creekui/services/image_service.dart'; import 'package:creekui/data/models/project_model.dart'; +import 'package:creekui/ui/styles/variables.dart'; +import 'package:creekui/ui/widgets/search_bar.dart'; import 'canvas_page.dart'; import 'define_brand_page.dart'; class CreateFilePage extends StatefulWidget { final File? file; - final int? projectId; // ⭐ MAKE NULLABLE + final int? projectId; const CreateFilePage({super.key, this.file, this.projectId}); @@ -20,11 +22,9 @@ class CreateFilePage extends StatefulWidget { class _CreateFilePageState extends State<CreateFilePage> { final TextEditingController _searchController = TextEditingController(); - // Project Selection int? _selectedProjectId; String _selectedProjectTitle = "Select Project"; - // Presets final List<CanvasPreset> _allPresets = [ CanvasPreset( name: 'Custom', @@ -92,7 +92,7 @@ class _CreateFilePageState extends State<CreateFilePage> { super.initState(); _filteredPresets = _allPresets; - // ⭐ If an existing projectId was passed, lock to that project + // If an existing projectId was passed, lock to that project _selectedProjectId = widget.projectId; if (_selectedProjectId != null) { _selectedProjectTitle = "Current Project"; @@ -110,7 +110,6 @@ class _CreateFilePageState extends State<CreateFilePage> { setState(() => _filteredPresets = _allPresets); return; } - setState(() { _filteredPresets = _allPresets @@ -121,7 +120,7 @@ class _CreateFilePageState extends State<CreateFilePage> { }); } - // ⭐ Select project (only for ShareToFilePage flow) + // Select project void _openProjectSelection() { showModalBottomSheet( context: context, @@ -155,7 +154,7 @@ class _CreateFilePageState extends State<CreateFilePage> { ); } - // ⭐ Go to canvas with selected project + // Go to canvas with selected project void _navigateToEditor(int width, int height) { if (_selectedProjectId == null) { ScaffoldMessenger.of(context).showSnackBar( @@ -169,7 +168,7 @@ class _CreateFilePageState extends State<CreateFilePage> { MaterialPageRoute( builder: (_) => CanvasPage( - projectId: _selectedProjectId!, // ⭐ use selected project + projectId: _selectedProjectId!, width: width.toDouble(), height: height.toDouble(), initialImage: widget.file, @@ -189,16 +188,12 @@ class _CreateFilePageState extends State<CreateFilePage> { icon: const Icon(Icons.arrow_back, color: Colors.black), onPressed: () => Navigator.pop(context), ), - title: const Text( + title: Text( 'Create Files', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.black, - ), + style: Variables.headerStyle.copyWith(fontSize: 16), ), - // ⭐ Show project chooser ONLY when projectId was NOT passed + // Show project chooser only when ID not passed actions: [ if (widget.projectId == null) Padding( @@ -224,67 +219,30 @@ class _CreateFilePageState extends State<CreateFilePage> { ), label: Text( _selectedProjectTitle, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Colors.black, - ), + style: Variables.bodyStyle.copyWith(fontSize: 12), ), ), ), ], ), - body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Search + // Search Bar Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(10), - ), - child: TextField( - controller: _searchController, - onChanged: _runFilter, - decoration: InputDecoration( - hintText: 'Search sizes', - prefixIcon: Icon( - Icons.search, - color: Colors.grey[400], - size: 20, - ), - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - suffixIcon: - _searchController.text.isNotEmpty - ? IconButton( - icon: const Icon( - Icons.clear, - color: Colors.grey, - size: 20, - ), - onPressed: () { - _searchController.clear(); - _runFilter(''); - }, - ) - : null, - ), - ), + child: CommonSearchBar( + controller: _searchController, + hintText: 'Search sizes', + onChanged: _runFilter, ), ), - const Padding( - padding: EdgeInsets.fromLTRB(16, 0, 16, 8), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: Text( 'Canvas Sizes', - style: TextStyle( + style: Variables.bodyStyle.copyWith( color: Colors.grey, fontSize: 15, fontWeight: FontWeight.w600, @@ -314,14 +272,12 @@ class _CreateFilePageState extends State<CreateFilePage> { // Card widget Widget _buildPresetCard(CanvasPreset preset) { final bool isCustom = preset.name == 'Custom'; - return InkWell( - onTap: () { - _navigateToEditor( - isCustom ? 1000 : preset.width, - isCustom ? 1000 : preset.height, - ); - }, + onTap: + () => _navigateToEditor( + isCustom ? 1000 : preset.width, + isCustom ? 1000 : preset.height, + ), borderRadius: BorderRadius.circular(12), child: Container( decoration: BoxDecoration( @@ -376,15 +332,14 @@ class _CreateFilePageState extends State<CreateFilePage> { preset.name, maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.w600, + style: Variables.bodyStyle.copyWith( fontSize: 11, - color: Colors.black, + fontWeight: FontWeight.w600, ), ), Text( preset.displaySize, - style: const TextStyle(color: Colors.grey, fontSize: 9), + style: Variables.captionStyle.copyWith(fontSize: 9), ), ], ), @@ -535,12 +490,10 @@ class _ProjectSelectionModalState extends State<ProjectSelectionModal> { void _filterProjects(String query) { setState(() { _searchQuery = query; - if (query.isEmpty) { _filteredGroupedProjects = _groupedProjects; return; } - final q = query.toLowerCase(); final List<ProjectGroup> filtered = []; @@ -569,7 +522,6 @@ class _ProjectSelectionModalState extends State<ProjectSelectionModal> { ); } } - _filteredGroupedProjects = filtered; }); } @@ -619,22 +571,13 @@ class _ProjectSelectionModalState extends State<ProjectSelectionModal> { ), ), - // Search bar + // Search Bar Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: TextField( + child: CommonSearchBar( controller: _searchController, + hintText: "Search Projects", onChanged: _filterProjects, - decoration: InputDecoration( - hintText: "Search Projects", - prefixIcon: const Icon(Icons.search, size: 20), - filled: true, - fillColor: const Color(0xFFE4E4E7), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - ), ), ), @@ -665,7 +608,6 @@ class _ProjectSelectionModalState extends State<ProjectSelectionModal> { style: TextStyle(fontSize: 14), ), ), - ..._filteredGroupedProjects.map(_buildProjectGroup), ], ), @@ -730,7 +672,6 @@ class _ProjectSelectionModalState extends State<ProjectSelectionModal> { ) : null, ), - if (hasEvents && g.isExpanded) Container( color: Colors.grey[100], diff --git a/lib/ui/pages/home_page.dart b/lib/ui/pages/home_page.dart @@ -8,6 +8,12 @@ import 'package:creekui/data/repos/project_repo.dart'; import 'package:creekui/data/repos/image_repo.dart'; import 'package:creekui/data/repos/file_repo.dart'; import 'package:creekui/services/project_service.dart'; +import 'package:creekui/ui/styles/variables.dart'; +import 'package:creekui/ui/widgets/search_bar.dart'; +import 'package:creekui/ui/widgets/file_card.dart'; +import 'package:creekui/ui/widgets/project_card.dart'; +import 'package:creekui/ui/widgets/empty_state.dart'; +import 'package:creekui/ui/widgets/section_header.dart'; import 'project_detail_page.dart'; import 'define_brand_page.dart'; import 'canvas_page.dart'; @@ -203,19 +209,12 @@ class _HomePageState extends State<HomePage> { backgroundColor: theme.cardColor, title: Text( 'Rename Project', - style: TextStyle( - fontFamily: 'GeneralSans', - fontSize: 18, - color: theme.colorScheme.onSurface, - ), + style: Variables.headerStyle.copyWith(fontSize: 18), ), content: TextField( controller: controller, autofocus: true, - style: TextStyle( - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface, - ), + style: Variables.bodyStyle, decoration: InputDecoration( hintText: 'Enter new name', hintStyle: TextStyle( @@ -236,7 +235,7 @@ class _HomePageState extends State<HomePage> { TextButton( child: Text( 'Cancel', - style: TextStyle( + style: Variables.bodyStyle.copyWith( color: theme.colorScheme.onSurface.withValues(alpha: 0.7), ), ), @@ -245,7 +244,9 @@ class _HomePageState extends State<HomePage> { TextButton( child: Text( 'Save', - style: TextStyle(color: theme.colorScheme.primary), + style: Variables.bodyStyle.copyWith( + color: theme.colorScheme.primary, + ), ), onPressed: () { if (controller.text.trim().isNotEmpty) { @@ -291,16 +292,11 @@ class _HomePageState extends State<HomePage> { backgroundColor: theme.cardColor, title: Text( 'Delete Project', - style: TextStyle( - fontFamily: 'GeneralSans', - fontSize: 18, - color: theme.colorScheme.onSurface, - ), + style: Variables.headerStyle.copyWith(fontSize: 18), ), content: Text( 'Are you sure you want to delete "${project.title}"? This cannot be undone.', - style: TextStyle( - fontFamily: 'GeneralSans', + style: Variables.bodyStyle.copyWith( color: theme.colorScheme.onSurface.withValues(alpha: 0.8), ), ), @@ -308,16 +304,16 @@ class _HomePageState extends State<HomePage> { TextButton( child: Text( 'Cancel', - style: TextStyle( + style: Variables.bodyStyle.copyWith( color: theme.colorScheme.onSurface.withValues(alpha: 0.7), ), ), onPressed: () => Navigator.pop(ctx, false), ), TextButton( - child: const Text( + child: Text( 'Delete', - style: TextStyle(color: Colors.red), + style: Variables.bodyStyle.copyWith(color: Colors.red), ), onPressed: () => Navigator.pop(ctx, true), ), @@ -468,10 +464,8 @@ class _HomePageState extends State<HomePage> { children: [ Text( "Hello, $_userName!", - style: TextStyle( + style: Variables.headerStyle.copyWith( fontSize: 24, - fontWeight: FontWeight.w500, - fontFamily: 'GeneralSans', color: theme.colorScheme.onSurface, ), ), @@ -502,58 +496,13 @@ class _HomePageState extends State<HomePage> { SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextField( + child: CommonSearchBar( controller: _searchController, onChanged: (value) { setState(() { _searchQuery = value.trim(); }); }, - decoration: InputDecoration( - hintText: 'Search', - hintStyle: TextStyle( - fontSize: 12, - color: theme.colorScheme.onSurface.withValues( - alpha: 0.5, - ), - fontFamily: 'GeneralSans', - ), - prefixIcon: Icon( - Icons.search, - size: 18, - color: theme.colorScheme.onSurface.withValues( - alpha: 0.5, - ), - ), - prefixIconConstraints: const BoxConstraints( - minWidth: 50, - minHeight: 18, - ), - filled: true, - fillColor: - isDark ? Colors.grey[800] : Colors.grey[200], - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - ), - style: TextStyle( - fontSize: 12, - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface, - ), ), ), ), @@ -567,26 +516,17 @@ class _HomePageState extends State<HomePage> { delegate: SliverChildListDelegate([ if (filteredFiles.isEmpty && filteredProjects.isEmpty) - Padding( - padding: const EdgeInsets.only(top: 40), - child: Center( - child: Text( - "No results found", - style: TextStyle( - color: theme.colorScheme.onSurface - .withValues(alpha: 0.5), - fontFamily: 'GeneralSans', - ), - ), - ), + const EmptyState( + icon: Icons.search_off, + title: "No results found", + subtitle: "Try searching for something else", ), + if (filteredProjects.isNotEmpty) ...[ Text( "Projects & Events", - style: TextStyle( - fontSize: 14, + style: Variables.bodyStyle.copyWith( fontWeight: FontWeight.w600, - fontFamily: 'GeneralSans', color: theme.colorScheme.onSurface .withValues(alpha: 0.7), ), @@ -597,10 +537,8 @@ class _HomePageState extends State<HomePage> { padding: const EdgeInsets.only(bottom: 8), child: SizedBox( height: 180, - child: _ProjectCard( + child: ProjectCard( project: p, - theme: theme, - isDark: isDark, previewImages: _projectPreviews[p.id] ?? [], onTap: () => _openProject(p), @@ -617,10 +555,8 @@ class _HomePageState extends State<HomePage> { if (filteredFiles.isNotEmpty) ...[ Text( "Files", - style: TextStyle( - fontSize: 14, + style: Variables.bodyStyle.copyWith( fontWeight: FontWeight.w600, - fontFamily: 'GeneralSans', color: theme.colorScheme.onSurface .withValues(alpha: 0.7), ), @@ -630,16 +566,15 @@ class _HomePageState extends State<HomePage> { final meta = _fileMetadata[f.id] ?? {}; return Padding( padding: const EdgeInsets.only(bottom: 12), - child: _FileCard( + child: FileCard( file: f, - theme: theme, - isDark: isDark, breadcrumb: _getProjectBreadcrumb(f), dimensions: meta['dimensions'] ?? 'Unknown', previewPath: meta['preview'] ?? '', timeAgo: _formatTimeAgo(f.lastUpdated), onTap: () => _openFile(f), + onMenuAction: null, ), ); }), @@ -655,9 +590,8 @@ class _HomePageState extends State<HomePage> { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (_recentFiles.isNotEmpty) ...[ - _buildSectionHeader( - 'Recent Files', - theme, + SectionHeader( + title: 'Recent Files', onTap: () => _navigateToSeeAll( 'Recent Files', @@ -671,10 +605,8 @@ class _HomePageState extends State<HomePage> { padding: const EdgeInsets.only( bottom: 12, ), - child: _FileCard( + child: FileCard( file: file, - theme: theme, - isDark: isDark, breadcrumb: _getProjectBreadcrumb(file), dimensions: meta['dimensions'] ?? 'Unknown', @@ -683,14 +615,14 @@ class _HomePageState extends State<HomePage> { file.lastUpdated, ), onTap: () => _openFile(file), + onMenuAction: null, ), ); }).toList()), const SizedBox(height: 24), ], - _buildSectionHeader( - 'Projects', - theme, + SectionHeader( + title: 'Projects', onTap: () => _navigateToSeeAll( 'All Projects', @@ -703,7 +635,7 @@ class _HomePageState extends State<HomePage> { _allProjects.isEmpty ? _buildCreateProjectCard(theme, isDark) : SizedBox( - height: 140, // Reduced height + height: 140, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: _allProjects.length, @@ -713,10 +645,8 @@ class _HomePageState extends State<HomePage> { padding: const EdgeInsets.only( right: 12, ), - child: _ProjectCard( + child: ProjectCard( project: project, - theme: theme, - isDark: isDark, previewImages: _projectPreviews[project .id] ?? @@ -728,15 +658,15 @@ class _HomePageState extends State<HomePage> { onDelete: () => _deleteProject(project), showGrid: false, + isHorizontal: true, ), ); }, ), ), const SizedBox(height: 24), - _buildSectionHeader( - 'Explore templates', - theme, + SectionHeader( + title: 'Explore templates', onTap: () {}, ), const SizedBox(height: 12), @@ -763,43 +693,6 @@ class _HomePageState extends State<HomePage> { ); } - Widget _buildSectionHeader( - String title, - ThemeData theme, { - VoidCallback? onTap, - }) { - return Row( - children: [ - Text( - title, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface, - ), - ), - const Spacer(), - if (onTap != null) - Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(20), - onTap: onTap, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - Icons.chevron_right, - size: 24, - color: theme.colorScheme.onSurface.withValues(alpha: 0.6), - ), - ), - ), - ), - ], - ); - } - Widget _buildCreateProjectCard(ThemeData theme, bool isDark) { return GestureDetector( onTap: _createNewProject, @@ -809,9 +702,9 @@ class _HomePageState extends State<HomePage> { margin: const EdgeInsets.only(right: 12), decoration: BoxDecoration( color: theme.scaffoldBackgroundColor, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(Variables.radiusMedium), border: Border.all( - color: isDark ? Colors.grey[800]! : Colors.grey[300]!, + color: isDark ? Variables.borderDark : Variables.borderSubtle, width: 1, style: BorderStyle.solid, ), @@ -827,10 +720,9 @@ class _HomePageState extends State<HomePage> { const SizedBox(height: 8), Text( "Create Project", - style: TextStyle( + style: Variables.bodyStyle.copyWith( fontSize: 12, fontWeight: FontWeight.w500, - fontFamily: 'GeneralSans', color: theme.colorScheme.onSurface.withValues(alpha: 0.7), ), ), @@ -904,11 +796,9 @@ class _HomePageState extends State<HomePage> { const SizedBox(height: 4), Text( template['title']!, - style: TextStyle( + style: Variables.bodyStyle.copyWith( fontSize: 13, fontWeight: FontWeight.w500, - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface, height: 1.2, ), maxLines: 1, @@ -917,9 +807,8 @@ class _HomePageState extends State<HomePage> { const SizedBox(height: 2), Text( template['subtitle']!, - style: TextStyle( + style: Variables.captionStyle.copyWith( fontSize: 11, - fontFamily: 'GeneralSans', color: theme.colorScheme.onSurface.withValues(alpha: 0.6), height: 1.2, ), @@ -936,426 +825,6 @@ class _HomePageState extends State<HomePage> { } } -// ----------------------------------------------------------------------------- -// REUSABLE WIDGETS & PAGES -// ----------------------------------------------------------------------------- - -class _FileCard extends StatelessWidget { - final FileModel file; - final ThemeData theme; - final bool isDark; - final String breadcrumb; - final String dimensions; - final String previewPath; - final String timeAgo; - final VoidCallback onTap; - - const _FileCard({ - required this.file, - required this.theme, - required this.isDark, - required this.breadcrumb, - required this.dimensions, - required this.previewPath, - required this.timeAgo, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - decoration: BoxDecoration( - color: theme.scaffoldBackgroundColor, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - width: 88, - height: 88, - child: Center( - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - color: isDark ? Colors.grey[800] : Colors.grey[200], - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.12), - blurRadius: 6, - offset: const Offset(0, 3), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: - previewPath.isNotEmpty - ? Image.file( - File(previewPath), - fit: BoxFit.cover, - errorBuilder: - (context, error, stackTrace) => Container( - color: - isDark - ? Colors.grey[800] - : Colors.grey[200], - child: Icon( - Icons.broken_image, - size: 20, - color: theme.colorScheme.onSurface - .withValues(alpha: 0.3), - ), - ), - ) - : Container( - color: - isDark ? Colors.grey[800] : Colors.grey[200], - child: Icon( - Icons.image, - size: 24, - color: theme.colorScheme.onSurface.withValues( - alpha: 0.3, - ), - ), - ), - ), - ), - ), - ), - const SizedBox(width: 16), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: - MainAxisSize.min, // Keep height minimal for centering - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Project Breadcrumb - if (breadcrumb.isNotEmpty) - Text( - breadcrumb, - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w500, - fontFamily: 'Inter', - color: theme.colorScheme.onSurface - .withValues(alpha: 0.6), - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - // File Name - Text( - file.name, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - const SizedBox(width: 8), - Icon( - Icons.more_vert, - size: 20, - color: theme.colorScheme.onSurface.withValues( - alpha: 0.6, - ), - ), - ], - ), - const SizedBox(height: 6), - Text( - dimensions, - style: TextStyle( - fontSize: 11, - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface.withValues( - alpha: 0.6, - ), - ), - ), - const SizedBox(height: 4), - Text( - timeAgo, - style: TextStyle( - fontSize: 11, - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface.withValues( - alpha: 0.4, - ), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} - -class _ProjectCard extends StatelessWidget { - final ProjectModel project; - final ThemeData theme; - final bool isDark; - final List<String> previewImages; - final VoidCallback onTap; - final VoidCallback? onRename; - final VoidCallback? onDelete; - final bool isHorizontal; - final bool showGrid; - - const _ProjectCard({ - required this.project, - required this.theme, - required this.isDark, - required this.previewImages, - required this.onTap, - this.onRename, - this.onDelete, - this.isHorizontal = true, - this.showGrid = false, - }); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - width: isHorizontal ? 130 : null, - decoration: BoxDecoration( - color: theme.scaffoldBackgroundColor, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isDark ? Colors.grey[800]! : Colors.grey[300]!, - width: 1, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: ClipRRect( - borderRadius: const BorderRadius.vertical( - top: Radius.circular(12), - ), - child: - showGrid - ? Padding( - padding: const EdgeInsets.all(4.0), - child: _buildGridPreview(), - ) - : (previewImages.isNotEmpty - ? Image.file( - File(previewImages.first), - fit: BoxFit.cover, - width: double.infinity, - errorBuilder: - (context, error, stackTrace) => Container( - color: - isDark - ? Colors.grey[800] - : Colors.grey[200], - child: Icon( - Icons.broken_image, - color: theme.colorScheme.onSurface - .withValues(alpha: 0.3), - ), - ), - ) - : Container( - color: - isDark ? Colors.grey[800] : Colors.grey[200], - child: Center( - child: Icon( - Icons.folder_outlined, - size: 32, - color: theme.colorScheme.onSurface.withValues( - alpha: 0.3, - ), - ), - ), - )), - ), - ), - Padding( - padding: const EdgeInsets.all(10), - child: Row( - children: [ - Expanded( - child: Text( - project.title, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 4), - SizedBox( - height: 24, - width: 24, - child: PopupMenuButton<String>( - icon: Icon( - Icons.more_vert, - size: 16, - color: theme.colorScheme.onSurface.withValues( - alpha: 0.6, - ), - ), - padding: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - onSelected: (value) { - if (value == 'rename' && onRename != null) { - onRename!(); - } else if (value == 'delete' && onDelete != null) { - onDelete!(); - } - }, - itemBuilder: - (context) => [ - const PopupMenuItem( - value: 'rename', - child: Row( - children: [ - Icon(Icons.edit_outlined, size: 18), - SizedBox(width: 12), - Text( - 'Rename', - style: TextStyle(fontFamily: 'GeneralSans'), - ), - ], - ), - ), - const PopupMenuItem( - value: 'delete', - child: Row( - children: [ - Icon( - Icons.delete_outline, - size: 18, - color: Colors.red, - ), - SizedBox(width: 12), - Text( - 'Delete', - style: TextStyle( - fontFamily: 'GeneralSans', - color: Colors.red, - ), - ), - ], - ), - ), - ], - ), - ), - ], - ), - ), - ], - ), - ), - ); - } - - // Grid Preview Logic - Widget _buildGridPreview() { - return Column( - children: [ - Expanded( - child: Row( - children: [ - Expanded( - child: _buildPreviewItem( - previewImages.isNotEmpty ? previewImages[0] : null, - ), - ), - const SizedBox(width: 4), - Expanded( - child: _buildPreviewItem( - previewImages.length > 1 ? previewImages[1] : null, - ), - ), - ], - ), - ), - const SizedBox(height: 4), - Expanded( - child: Row( - children: [ - Expanded( - child: _buildPreviewItem( - previewImages.length > 2 ? previewImages[2] : null, - ), - ), - const SizedBox(width: 4), - Expanded( - child: _buildPreviewItem( - previewImages.length > 3 ? previewImages[3] : null, - ), - ), - ], - ), - ), - ], - ); - } - - Widget _buildPreviewItem(String? imagePath) { - return Container( - decoration: BoxDecoration( - color: isDark ? Colors.grey[800] : Colors.grey[200], - borderRadius: BorderRadius.circular(6), // Rounded grid items - ), - clipBehavior: Clip.antiAlias, - child: - imagePath != null - ? Image.file( - File(imagePath), - fit: BoxFit.cover, - width: double.infinity, - height: double.infinity, - errorBuilder: - (context, error, stackTrace) => const Icon( - Icons.broken_image, - size: 16, - color: Colors.grey, - ), - ) - : null, // Empty placeholder - ); - } -} - class _SeeAllPage extends StatefulWidget { final String title; final bool isProjects; @@ -1495,11 +964,10 @@ class _SeeAllPageState extends State<_SeeAllPage> { appBar: AppBar( title: Text( widget.title, - style: TextStyle( - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface, + style: Variables.headerStyle.copyWith( fontSize: 18, fontWeight: FontWeight.w500, + color: theme.colorScheme.onSurface, ), ), backgroundColor: theme.scaffoldBackgroundColor, @@ -1524,10 +992,8 @@ class _SeeAllPageState extends State<_SeeAllPage> { itemCount: _items.length, itemBuilder: (context, index) { final project = _items[index] as ProjectModel; - return _ProjectCard( + return ProjectCard( project: project, - theme: theme, - isDark: isDark, previewImages: _projectPreviews[project.id] ?? [], onTap: () => widget.onProjectTap(project), // Chain callbacks, reload data on completion @@ -1551,15 +1017,14 @@ class _SeeAllPageState extends State<_SeeAllPage> { final meta = _fileMetadata[file.id] ?? {}; return Padding( padding: const EdgeInsets.only(bottom: 12), - child: _FileCard( + child: FileCard( file: file, - theme: theme, - isDark: isDark, breadcrumb: _getBreadcrumb(file), dimensions: meta['dimensions'] ?? 'Unknown', previewPath: meta['preview'] ?? '', timeAgo: _timeAgo(file.lastUpdated), onTap: () => widget.onFileTap(file), + onMenuAction: null, ), ); }, diff --git a/lib/ui/pages/project_detail_page.dart b/lib/ui/pages/project_detail_page.dart @@ -6,6 +6,9 @@ import 'package:creekui/data/models/image_model.dart'; import 'package:creekui/data/repos/project_repo.dart'; import 'package:creekui/data/repos/image_repo.dart'; import 'package:creekui/services/project_service.dart'; +import 'package:creekui/ui/styles/variables.dart'; +import 'package:creekui/ui/widgets/empty_state.dart'; +import 'package:creekui/ui/widgets/section_header.dart'; import 'project_board_page.dart'; import 'stylesheet_page.dart'; import 'project_file_page.dart'; @@ -59,7 +62,6 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { for (final event in events) { if (event.id != null) { final images = await _imageRepo.getImages(event.id!); - // Get the 3 most recent images (already ordered by created_at DESC) eventImagesMap[event.id!] = images.take(3).toList(); } } @@ -74,9 +76,7 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { } } catch (e) { debugPrint('Error loading project data: $e'); - if (mounted) { - setState(() => _isLoading = false); - } + if (mounted) setState(() => _isLoading = false); } } @@ -109,7 +109,6 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Drag handle Container( padding: const EdgeInsets.only(top: 8, bottom: 16), child: Center( @@ -123,7 +122,6 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { ), ), ), - // Header section Container( padding: const EdgeInsets.symmetric( horizontal: 16, @@ -134,24 +132,13 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { bottom: BorderSide(color: Color(0xFFE4E4E7), width: 1), ), ), - child: const Text( - "Event Details", - style: TextStyle( - fontFamily: 'GeneralSans', - fontSize: 14, - fontWeight: FontWeight.normal, - color: Color(0xFF27272A), - height: 20 / 14, - ), - ), + child: Text("Event Details", style: Variables.bodyStyle), ), - // Content section Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Name field Container( height: 40, decoration: BoxDecoration( @@ -161,34 +148,22 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { child: TextField( controller: nameController, autofocus: true, - style: const TextStyle( - fontFamily: 'GeneralSans', - fontSize: 12, - fontWeight: FontWeight.normal, - color: Color(0xFF27272A), - height: 16 / 12, - ), - decoration: const InputDecoration( + style: Variables.bodyStyle.copyWith(fontSize: 12), + decoration: InputDecoration( hintText: "Add Name*", - hintStyle: TextStyle( - fontFamily: 'GeneralSans', + hintStyle: Variables.bodyStyle.copyWith( fontSize: 12, - fontWeight: FontWeight.normal, - color: Color(0xFF71717B), - height: 16 / 12, + color: const Color(0xFF71717B), ), - contentPadding: EdgeInsets.symmetric( + contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, ), ), ), const SizedBox(height: 16), - // Description field Container( height: 80, decoration: BoxDecoration( @@ -200,41 +175,28 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { maxLines: null, expands: true, textAlignVertical: TextAlignVertical.top, - style: const TextStyle( - fontFamily: 'GeneralSans', - fontSize: 12, - fontWeight: FontWeight.normal, - color: Color(0xFF27272A), - height: 16 / 12, - ), - decoration: const InputDecoration( + style: Variables.bodyStyle.copyWith(fontSize: 12), + decoration: InputDecoration( hintText: "Add Description", - hintStyle: TextStyle( - fontFamily: 'GeneralSans', + hintStyle: Variables.bodyStyle.copyWith( fontSize: 12, - fontWeight: FontWeight.normal, - color: Color(0xFF71717B), - height: 16 / 12, + color: const Color(0xFF71717B), ), - contentPadding: EdgeInsets.symmetric( + contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, ), ), ), ], ), ), - // Bottom actions Container( padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), child: Row( children: [ - // Cancel button Expanded( child: GestureDetector( onTap: () => Navigator.pop(context), @@ -244,16 +206,11 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { color: const Color(0xFFE4E4E7), borderRadius: BorderRadius.circular(1000), ), - child: const Center( + child: Center( child: Text( "Cancel", - style: TextStyle( - fontFamily: 'GeneralSans', - fontSize: 14, - fontWeight: FontWeight.w500, - color: Color(0xFF27272A), - height: 20 / 14, - letterSpacing: 0.25, + style: Variables.buttonTextStyle.copyWith( + color: Variables.textPrimary, ), ), ), @@ -261,7 +218,6 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { ), ), const SizedBox(width: 8), - // Add Event button Expanded( child: GestureDetector( onTap: () async { @@ -279,7 +235,7 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { ); if (context.mounted) { Navigator.pop(context); - _loadData(); // Refresh to show new event + _loadData(); } } catch (e) { debugPrint("Error creating event: $e"); @@ -292,17 +248,10 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { color: const Color(0xFF27272A), borderRadius: BorderRadius.circular(1000), ), - child: const Center( + child: Center( child: Text( "Add Event", - style: TextStyle( - fontFamily: 'GeneralSans', - fontSize: 14, - fontWeight: FontWeight.w500, - color: Color(0xFFFAFAFA), - height: 20 / 14, - letterSpacing: 0.25, - ), + style: Variables.buttonTextStyle, ), ), ), @@ -339,24 +288,12 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { ).then((_) => _loadData()); } - void _showPlaceholder(String feature) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('$feature coming soon!'), - duration: const Duration(seconds: 1), - ), - ); - } - @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; - if (_isLoading) { - return Scaffold( - backgroundColor: const Color(0xFFFAFAFA), - body: const Center(child: CircularProgressIndicator()), + return const Scaffold( + backgroundColor: Color(0xFFFAFAFA), + body: Center(child: CircularProgressIndicator()), ); } @@ -365,15 +302,7 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { return Scaffold( backgroundColor: const Color(0xFFFAFAFA), appBar: AppBar( - title: Text( - _project!.title, - style: const TextStyle( - fontFamily: 'GeneralSans', - fontSize: 20, - fontWeight: FontWeight.w500, - color: Color(0xFF27272A), - ), - ), + title: Text(_project!.title, style: Variables.headerStyle), backgroundColor: const Color(0xFFFAFAFA), elevation: 0, leadingWidth: 40, @@ -384,6 +313,10 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { 'assets/icons/settings-line.svg', width: 24, height: 24, + colorFilter: const ColorFilter.mode( + Variables.textPrimary, + BlendMode.srcIn, + ), ), onPressed: () {}, ), @@ -398,43 +331,36 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 16), - - // Action Cards Row _buildActionCardsRow(_project!.id!), - - const SizedBox(height: 16), - - // Events Header - _buildEventsHeader(), - const SizedBox(height: 16), - - // Events List - if (_events.isEmpty) - Center( - child: Container( - width: 328, - padding: const EdgeInsets.all(32), - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey[300]!), - ), - child: Column( - children: [ - Icon( - Icons.event_note, - size: 48, - color: Colors.grey[400], + SectionHeader( + title: 'Events', + trailing: Material( + color: Colors.transparent, + child: InkWell( + onTap: _createEventDialog, + borderRadius: BorderRadius.circular(20), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + 'assets/icons/add-line.svg', + width: 24, + height: 24, + colorFilter: const ColorFilter.mode( + Variables.textPrimary, + BlendMode.srcIn, ), - const SizedBox(height: 8), - Text( - "No events created yet", - style: TextStyle(color: Colors.grey[500]), - ), - ], + ), ), ), + ), + ), + const SizedBox(height: 16), + if (_events.isEmpty) + const EmptyState( + icon: Icons.event_note, + title: "No events created yet", + subtitle: "Create an event to start organizing", ) else ListView.separated( @@ -447,7 +373,6 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { return Center(child: _buildEventCard(event)); }, ), - const SizedBox(height: 40), ], ), @@ -456,8 +381,7 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { ); } - // --- Widgets --- - + // Widgets Widget _buildActionCardsRow(int projectId) { return Row( children: [ @@ -529,9 +453,9 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { child: Image.asset( blobImage, fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container(color: const Color(0xFF27272A)); - }, + errorBuilder: + (context, error, stackTrace) => + Container(color: const Color(0xFF27272A)), ), ), ), @@ -581,12 +505,9 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { // Label Text( label, - style: const TextStyle( - fontFamily: 'GeneralSans', - fontSize: 12, + style: Variables.bodyStyle.copyWith( fontWeight: FontWeight.w500, - color: Color(0xFF27272A), - letterSpacing: 0.4, + fontSize: 12, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -600,57 +521,11 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { ); } - Widget _buildEventsHeader() { - return Container( - padding: const EdgeInsets.only(bottom: 8, top: 16), - decoration: const BoxDecoration( - border: Border(bottom: BorderSide(color: Color(0xFFE4E4E7), width: 1)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text( - 'Events', - style: TextStyle( - fontFamily: 'GeneralSans', - fontSize: 16, - fontWeight: FontWeight.w500, - color: Color(0xFF27272A), - ), - ), - Material( - color: Colors.transparent, - child: InkWell( - onTap: _createEventDialog, - borderRadius: BorderRadius.circular(20), - child: Container( - padding: const EdgeInsets.all(8), - child: SvgPicture.asset( - 'assets/icons/add-line.svg', - width: 24, - height: 24, - colorFilter: const ColorFilter.mode( - Color(0xFF27272A), - BlendMode.srcIn, - ), - ), - ), - ), - ), - ], - ), - ); - } - Widget _buildEventCard(ProjectModel event) { final eventImages = _eventImages[event.id] ?? []; - return GestureDetector( onTap: () { - if (event.id != null) { - _navigateToBoard(event.id!); - } + if (event.id != null) _navigateToBoard(event.id!); }, child: Container( width: 328, @@ -662,13 +537,12 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Three images side by side (or fewer if not available) + // 3 images side by side (or fewer if not available) Container( height: 115, padding: const EdgeInsets.only(bottom: 4), child: Row( children: [ - // Show up to 3 images for (int i = 0; i < 3; i++) ...[ Expanded( child: ClipRRect( @@ -678,9 +552,9 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { ? Image.file( File(eventImages[i].filePath), fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container(color: Colors.grey[300]); - }, + errorBuilder: + (_, __, ___) => + Container(color: Colors.grey[300]), ) : Container(color: Colors.grey[200]), ), @@ -695,12 +569,8 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), child: Text( event.title, - style: const TextStyle( - fontFamily: 'GeneralSans', - fontSize: 14, + style: Variables.bodyStyle.copyWith( fontWeight: FontWeight.w500, - color: Color(0xFF27272A), - letterSpacing: 0.25, ), ), ), diff --git a/lib/ui/pages/project_file_page.dart b/lib/ui/pages/project_file_page.dart @@ -2,13 +2,16 @@ import 'dart:io'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:image/image.dart' as img; -import 'package:intl/intl.dart'; import 'package:creekui/data/models/file_model.dart'; import 'package:creekui/data/models/project_model.dart'; import 'package:creekui/services/file_service.dart'; import 'package:creekui/data/repos/project_repo.dart'; import 'package:creekui/ui/widgets/top_bar.dart'; import 'package:creekui/ui/widgets/bottom_bar.dart'; +import 'package:creekui/ui/styles/variables.dart'; +import 'package:creekui/ui/widgets/search_bar.dart'; +import 'package:creekui/ui/widgets/file_card.dart'; +import 'package:creekui/ui/widgets/empty_state.dart'; import 'create_file_page.dart'; import 'canvas_page.dart'; @@ -24,6 +27,7 @@ class ProjectFilePage extends StatefulWidget { class _ProjectFilePageState extends State<ProjectFilePage> { final _fileService = FileService(); final _projectRepo = ProjectRepo(); + final TextEditingController _searchController = TextEditingController(); List<FileModel> _allFiles = []; List<ProjectModel> _events = []; @@ -41,6 +45,12 @@ class _ProjectFilePageState extends State<ProjectFilePage> { _loadEverything(); } + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + // --------------------------------------- // LOAD EVERYTHING // --------------------------------------- @@ -169,10 +179,14 @@ class _ProjectFilePageState extends State<ProjectFilePage> { context: context, builder: (context) { return AlertDialog( - title: const Text("Rename File"), + title: Text( + "Rename File", + style: Variables.headerStyle.copyWith(fontSize: 18), + ), content: TextField( controller: controller, autofocus: true, + style: Variables.bodyStyle, decoration: const InputDecoration( labelText: "New file name", border: OutlineInputBorder(), @@ -180,11 +194,11 @@ class _ProjectFilePageState extends State<ProjectFilePage> { ), actions: [ TextButton( - child: const Text("Cancel"), + child: Text("Cancel", style: Variables.bodyStyle), onPressed: () => Navigator.pop(context), ), FilledButton( - child: const Text("Save"), + child: Text("Save", style: Variables.buttonTextStyle), onPressed: () => Navigator.pop(context, controller.text.trim()), ), ], @@ -193,11 +207,7 @@ class _ProjectFilePageState extends State<ProjectFilePage> { ); if (newName == null || newName.isEmpty) return; - - // ✔ Update DB await _fileService.renameFile(file.id, newName); - - // ✔ Reload UI await _loadEverything(); } @@ -206,16 +216,22 @@ class _ProjectFilePageState extends State<ProjectFilePage> { context: context, builder: (_) => AlertDialog( - title: const Text("Delete File?"), - content: Text("This will permanently remove ${file.name}."), + title: Text( + "Delete File?", + style: Variables.headerStyle.copyWith(fontSize: 18), + ), + content: Text( + "This will permanently remove ${file.name}.", + style: Variables.bodyStyle, + ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text("Cancel"), + child: Text("Cancel", style: Variables.bodyStyle), ), FilledButton( onPressed: () => Navigator.pop(context, true), - child: const Text("Delete"), + child: Text("Delete", style: Variables.buttonTextStyle), ), ], ), @@ -225,7 +241,6 @@ class _ProjectFilePageState extends State<ProjectFilePage> { try { await _fileService.deleteFile(file.id!); - final disk = File(file.filePath); if (await disk.exists()) await disk.delete(); @@ -270,128 +285,21 @@ class _ProjectFilePageState extends State<ProjectFilePage> { return "just now"; } - // --------------------------------------- - // FILE CARD (Same UI but thumbnail updated) - // --------------------------------------- - Widget _fileCard(FileModel file) { + Widget _buildFileCard(FileModel file) { final meta = _fileMetadata[file.id] ?? {}; final preview = meta["preview"] ?? ""; final dimensions = meta["dimensions"] ?? "Unknown"; - final realPreview = - preview.isNotEmpty && File(preview).existsSync() - ? preview - : file.filePath; - - return GestureDetector( - onTap: () => _openFile(file), - child: Container( - margin: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFFE4E4E7)), - ), - - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // ---------- THUMBNAIL ---------- - ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - bottomLeft: Radius.circular(16), - ), - child: Image.file( - File(realPreview), - width: 120, - height: 120, - fit: BoxFit.cover, - errorBuilder: - (_, __, ___) => Container( - width: 120, - height: 120, - color: Colors.grey[300], - child: const Icon(Icons.broken_image), - ), - ), - ), - - const SizedBox(width: 12), - - // ---------- RIGHT SIDE TEXT ---------- - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 14), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Breadcrumb - Text( - _breadcrumbFor(file), - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - fontFamily: 'GeneralSans', - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - - // Name - Text( - file.name, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - fontFamily: 'GeneralSans', - ), - ), - - const SizedBox(height: 6), - - // Dimensions - Text( - dimensions, - style: TextStyle( - fontSize: 13, - color: Colors.grey[700], - fontFamily: 'GeneralSans', - ), - ), - - const SizedBox(height: 6), - - // Updated time - Text( - "Last edited • ${_formatRelative(file.lastUpdated)}", - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - fontFamily: 'GeneralSans', - ), - ), - ], - ), - ), - ), - - // ---------- MENU ---------- - PopupMenuButton<String>( - onSelected: (value) => _handleFileMenuAction(file, value), - itemBuilder: - (_) => const [ - PopupMenuItem(value: "open", child: Text("Open")), - PopupMenuItem(value: "rename", child: Text("Rename")), - PopupMenuItem(value: "delete", child: Text("Delete")), - ], - icon: const Icon(Icons.more_vert, size: 20), - ), - - const SizedBox(width: 4), - ], - ), + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: FileCard( + file: file, + breadcrumb: _breadcrumbFor(file), + dimensions: dimensions, + previewPath: preview, + timeAgo: "Last edited • ${_formatRelative(file.lastUpdated)}", + onTap: () => _openFile(file), + onMenuAction: (value) => _handleFileMenuAction(file, value), ), ); } @@ -439,8 +347,7 @@ class _ProjectFilePageState extends State<ProjectFilePage> { onBack: () => Navigator.pop(context), titleOverride: "Project Files", onProjectChanged: (project) { - // When user switches project from dropdown, - // refresh this page with the new projectId + // When user switches project from dropdown, refresh page with new ID Navigator.pushReplacement( context, MaterialPageRoute( @@ -494,7 +401,12 @@ class _ProjectFilePageState extends State<ProjectFilePage> { children: [ const SizedBox(height: 12), - _buildSearchBar(), + // Search Bar + CommonSearchBar( + controller: _searchController, + hintText: "Search your files", + onChanged: (v) => setState(() => _search = v.trim()), + ), const SizedBox(height: 24), @@ -514,48 +426,22 @@ class _ProjectFilePageState extends State<ProjectFilePage> { const SizedBox(height: 12), if (_filteredAllFiles().isEmpty) - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 48), - child: Column( - children: [ - Icon( - Icons.folder_outlined, - size: 64, - color: Colors.grey[400], - ), - const SizedBox(height: 16), - Text( - _search.isEmpty - ? "No files yet" - : "No files found", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - fontFamily: 'GeneralSans', - color: Colors.grey[600], - ), - ), - const SizedBox(height: 8), - Text( - _search.isEmpty - ? "Create your first file to get started" - : "Try a different search term", - style: TextStyle( - fontSize: 14, - fontFamily: 'GeneralSans', - color: Colors.grey[500], - ), - textAlign: TextAlign.center, - ), - ], - ), + EmptyState( + icon: Icons.folder_outlined, + title: + _search.isEmpty + ? "No files yet" + : "No files found", + subtitle: + _search.isEmpty + ? "Create your first file to get started" + : "Try a different search term", ) else Column( children: _filteredAllFiles() - .map((f) => _fileCard(f)) + .map((f) => _buildFileCard(f)) .toList(), ), @@ -577,90 +463,33 @@ class _ProjectFilePageState extends State<ProjectFilePage> { const SizedBox(height: 12), if (_events.isEmpty) - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 48), - child: Column( - children: [ - Icon( - Icons.event_outlined, - size: 64, - color: Colors.grey[400], - ), - const SizedBox(height: 16), - Text( - "No events yet", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - fontFamily: 'GeneralSans', - color: Colors.grey[600], - ), - ), - const SizedBox(height: 8), - Text( - "Create an event to organize your files", - style: TextStyle( - fontSize: 14, - fontFamily: 'GeneralSans', - color: Colors.grey[500], - ), - textAlign: TextAlign.center, - ), - ], - ), + const EmptyState( + icon: Icons.event_outlined, + title: "No events yet", + subtitle: "Create an event to organize your files", ) else Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ----------------------- - // DROPDOWN ONLY + // DROPDOWN // ----------------------- _buildEventDropdown(), const SizedBox(height: 16), _eventFiles.isEmpty - ? Container( - width: double.infinity, - padding: const EdgeInsets.symmetric( - vertical: 48, - ), - child: Column( - children: [ - Icon( - Icons.insert_drive_file_outlined, - size: 64, - color: Colors.grey[400], - ), - const SizedBox(height: 16), - Text( - "No files in this event yet", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - fontFamily: 'GeneralSans', - color: Colors.grey[600], - ), - ), - const SizedBox(height: 8), - Text( - "Create a file to add it to this event", - style: TextStyle( - fontSize: 14, - fontFamily: 'GeneralSans', - color: Colors.grey[500], - ), - textAlign: TextAlign.center, - ), - ], - ), + ? const EmptyState( + icon: Icons.insert_drive_file_outlined, + title: "No files in this event yet", + subtitle: + "Create a file to add it to this event", ) : Column( children: _eventFiles - .map((f) => _fileCard(f)) + .map((f) => _buildFileCard(f)) .toList(), ), ], @@ -675,25 +504,6 @@ class _ProjectFilePageState extends State<ProjectFilePage> { } // --------------------------------------- - // SEARCH - // --------------------------------------- - Widget _buildSearchBar() { - return TextField( - onChanged: (v) => setState(() => _search = v.trim()), - decoration: InputDecoration( - filled: true, - fillColor: Colors.grey[200], - hintText: "Search your files", - prefixIcon: Icon(Icons.search, color: Colors.grey[600]), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - ), - ); - } - - // --------------------------------------- // DROPDOWN // --------------------------------------- Widget _buildEventDropdown() { @@ -702,7 +512,7 @@ class _ProjectFilePageState extends State<ProjectFilePage> { padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: const Color(0xFFE4E4E7), - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(Variables.radiusSmall), ), child: DropdownButton<ProjectModel>( value: _selectedEvent, diff --git a/lib/ui/pages/stylesheet_page.dart b/lib/ui/pages/stylesheet_page.dart @@ -14,6 +14,7 @@ import 'package:creekui/services/python_service.dart'; import 'package:creekui/ui/widgets/bottom_bar.dart'; import 'package:creekui/ui/widgets/top_bar.dart'; import 'package:creekui/ui/styles/variables.dart'; +import 'package:creekui/ui/widgets/section_header.dart'; class StylesheetPage extends StatefulWidget { final int projectId; @@ -32,7 +33,6 @@ class StylesheetPage extends StatefulWidget { class _StylesheetPageState extends State<StylesheetPage> { late int _currentProjectId; bool _isLoading = false; - // State to toggle hex code visibility bool _showHexCodes = false; StylesheetData? _stylesheetData; @@ -43,7 +43,7 @@ class _StylesheetPageState extends State<StylesheetPage> { final ImagePicker _picker = ImagePicker(); final StylesheetService _stylesheetService = StylesheetService(); - // --- ASSET MAPPING --- + // Asset mapping final Map<String, String> _lightingAssets = { 'backlit': 'assets/stylesheet/lighting/backlit.png', 'diffused': 'assets/stylesheet/lighting/diffused.png', @@ -94,8 +94,7 @@ class _StylesheetPageState extends State<StylesheetPage> { } } - // --- UTILS --- - + // Utils String _formatLabel(String label) { String clean = label.replaceAll(RegExp(r'[-_]'), ' '); List<String> words = clean.split(' '); @@ -314,65 +313,7 @@ class _StylesheetPageState extends State<StylesheetPage> { ); } - // Modified to support a custom trailing widget (e.g. Eye Icon) - Widget _buildSectionHeader( - String title, { - bool showArrow = true, - Widget? trailing, - }) { - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - title, - style: const TextStyle( - fontFamily: 'GeneralSans', - fontSize: 16, - fontWeight: FontWeight.w500, - height: 24 / 16, - color: Variables.textPrimary, - ), - ), - ), - if (trailing != null) - trailing - else if (showArrow) - Transform.rotate( - angle: 3.14159, - child: SvgPicture.asset( - 'assets/icons/arrow-left-s-line.svg', - width: 24, - height: 24, - colorFilter: const ColorFilter.mode( - Variables.textPrimary, - BlendMode.srcIn, - ), - ), - ), - ], - ), - ); - } - - // --- Logos --- - Future<void> _pickLogo() async { - try { - final XFile? pickedFile = await _picker.pickImage( - source: ImageSource.gallery, - ); - if (pickedFile != null) { - setState(() { - _logoPaths.add(pickedFile.path); - }); - } - } catch (e) { - debugPrint("Error picking logo: $e"); - } - } - + // Logos Widget _buildLogosSection(dynamic data) { List<String> logoPaths = List.from(_logoPaths); @@ -389,7 +330,10 @@ class _StylesheetPageState extends State<StylesheetPage> { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionHeader("Logos"), + const Padding( + padding: EdgeInsets.only(bottom: 12), + child: SectionHeader(title: "Logos"), + ), SizedBox( height: 76, child: ListView.separated( @@ -430,7 +374,22 @@ class _StylesheetPageState extends State<StylesheetPage> { ); } - // --- Graphics --- + Future<void> _pickLogo() async { + try { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource.gallery, + ); + if (pickedFile != null) { + setState(() { + _logoPaths.add(pickedFile.path); + }); + } + } catch (e) { + debugPrint("Error picking logo: $e"); + } + } + + // Graphics Widget _buildGraphicsSection(List<String> extractedGraphics) { List<String> graphicPaths = List.from(_projectAssets); graphicPaths.addAll(extractedGraphics); @@ -440,7 +399,10 @@ class _StylesheetPageState extends State<StylesheetPage> { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionHeader("Graphics"), + const Padding( + padding: EdgeInsets.only(bottom: 12), + child: SectionHeader(title: "Graphics"), + ), SizedBox( height: 106, child: ListView.separated( @@ -487,14 +449,17 @@ class _StylesheetPageState extends State<StylesheetPage> { ); } - // --- Fonts --- + // Fonts Widget _buildFontsSection(List<String> fontNames) { if (fontNames.isEmpty) return const SizedBox(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionHeader("Fonts"), + const Padding( + padding: EdgeInsets.only(bottom: 12), + child: SectionHeader(title: "Fonts"), + ), SizedBox( height: 120, child: ListView.separated( @@ -511,7 +476,6 @@ class _StylesheetPageState extends State<StylesheetPage> { Widget _buildFontCard(String resolvedFontName) { final String displayFontName = _formatLabel(resolvedFontName); - TextStyle sampleStyle; try { sampleStyle = GoogleFonts.getFont(resolvedFontName); @@ -544,36 +508,31 @@ class _StylesheetPageState extends State<StylesheetPage> { displayFontName, maxLines: 2, overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontFamily: 'GeneralSans', - fontSize: 12, - height: 16 / 12, - color: Variables.textSecondary, - fontWeight: FontWeight.w400, - ), + style: Variables.captionStyle, ), ], ), ); } - // --- Colors --- + // Colors Widget _buildColorsSection(List<Color> colors) { if (colors.isEmpty) return const SizedBox(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Using custom trailing widget for the Eye Icon toggle - _buildSectionHeader( - "Colors", - showArrow: false, - trailing: GestureDetector( - onTap: () => setState(() => _showHexCodes = !_showHexCodes), - child: Icon( - _showHexCodes ? Icons.visibility : Icons.visibility_outlined, - color: Variables.textPrimary, - size: 20, + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: SectionHeader( + title: "Colors", + trailing: GestureDetector( + onTap: () => setState(() => _showHexCodes = !_showHexCodes), + child: Icon( + _showHexCodes ? Icons.visibility : Icons.visibility_outlined, + color: Variables.textPrimary, + size: 20, + ), ), ), ), @@ -602,7 +561,6 @@ class _StylesheetPageState extends State<StylesheetPage> { borderRadius: BorderRadius.circular(8), ), alignment: Alignment.center, - // Only show text if _showHexCodes is true child: _showHexCodes ? Text( @@ -618,7 +576,6 @@ class _StylesheetPageState extends State<StylesheetPage> { ); } - // --- Unified Card Builder --- Widget _buildUnifiedCard(String label, String? assetPath) { final formattedLabel = _formatLabel(label); @@ -711,7 +668,10 @@ class _StylesheetPageState extends State<StylesheetPage> { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionHeader(title), + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: SectionHeader(title: title), + ), SizedBox( height: (assetMap != null) ? 106 : 54, child: ListView.separated( @@ -731,31 +691,18 @@ class _StylesheetPageState extends State<StylesheetPage> { ); } - Widget _buildCompositionsSection(List<String> data) { - return _buildAttributeSection("Compositions", data, null); - } - - Widget _buildMaterialLookSection(List<String> data) { - return _buildAttributeSection("Material Look", data, _materialAssets); - } - - Widget _buildTexturesSection(List<String> data) { - return _buildAttributeSection("Textures", data, _textureAssets); - } - - Widget _buildLightingSection(List<String> data) { - return _buildAttributeSection("Lighting", data, _lightingAssets); - } - - Widget _buildStyleSection(List<String> data) { - return _buildAttributeSection("Style", data, null); - } - - Widget _buildEraSection(List<String> data) { - return _buildAttributeSection("Era", data, null); - } - - Widget _buildEmotionsSection(List<String> data) { - return _buildAttributeSection("Emotions", data, null); - } + Widget _buildCompositionsSection(List<String> data) => + _buildAttributeSection("Compositions", data, null); + Widget _buildMaterialLookSection(List<String> data) => + _buildAttributeSection("Material Look", data, _materialAssets); + Widget _buildTexturesSection(List<String> data) => + _buildAttributeSection("Textures", data, _textureAssets); + Widget _buildLightingSection(List<String> data) => + _buildAttributeSection("Lighting", data, _lightingAssets); + Widget _buildStyleSection(List<String> data) => + _buildAttributeSection("Style", data, null); + Widget _buildEraSection(List<String> data) => + _buildAttributeSection("Era", data, null); + Widget _buildEmotionsSection(List<String> data) => + _buildAttributeSection("Emotions", data, null); } diff --git a/lib/ui/styles/variables.dart b/lib/ui/styles/variables.dart @@ -9,21 +9,26 @@ class Variables { static const Color surfaceSubtle = Color(0xFFF4F4F5); static const Color background = Colors.white; - static const Color borderSubtle = Color(0xFFE4E4E7); + // Dark Mode + static const Color surfaceDark = Color(0xFF27272A); + static const Color backgroundDark = Color(0xFF18181B); + static const Color borderDark = Color(0xFF3F3F46); + // Dimensions static const double fontSizeHeader = 20.0; static const double lineHeightHeader = 24.0; - static const double fontSizeBody = 14.0; static const double lineHeightBody = 20.0; static const double trackingBody = 0.25; - static const double fontSizeSmall = 12.0; - static const double fontSizeCaption = 10.0; + // Radius + static const double radiusSmall = 8.0; + static const double radiusMedium = 12.0; + // Text Styles static TextStyle get headerStyle => const TextStyle( fontFamily: 'GeneralSans', diff --git a/lib/ui/widgets/empty_state.dart b/lib/ui/widgets/empty_state.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +class EmptyState extends StatelessWidget { + final IconData icon; + final String title; + final String subtitle; + + const EmptyState({ + super.key, + required this.icon, + required this.title, + required this.subtitle, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 48), + child: Column( + children: [ + Icon(icon, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + fontFamily: 'GeneralSans', + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + subtitle, + style: TextStyle( + fontSize: 14, + fontFamily: 'GeneralSans', + color: Colors.grey[500], + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/file_card.dart b/lib/ui/widgets/file_card.dart @@ -0,0 +1,215 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:creekui/data/models/file_model.dart'; +import 'package:creekui/ui/styles/variables.dart'; + +class FileCard extends StatelessWidget { + final FileModel file; + final String breadcrumb; + final String dimensions; + final String previewPath; + final String timeAgo; + final VoidCallback onTap; + + // Optional actions for the context menu. + // If null, the menu icon is hidden or disabled. + final Function(String)? onMenuAction; + + const FileCard({ + super.key, + required this.file, + required this.breadcrumb, + required this.dimensions, + required this.previewPath, + required this.timeAgo, + required this.onTap, + this.onMenuAction, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + // Resolve valid image path + final bool hasPreview = + previewPath.isNotEmpty && File(previewPath).existsSync(); + final ImageProvider? imageProvider = + hasPreview ? FileImage(File(previewPath)) : null; + + return GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: theme.scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(Variables.radiusSmall), + // Optional: Add subtle border if needed to match FilePage style + border: Border.all( + color: isDark ? Variables.borderDark : Colors.transparent, + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Thumbnail + SizedBox( + width: 88, + height: 88, + child: Center( + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: + isDark + ? Variables.surfaceDark + : Variables.surfaceSubtle, + borderRadius: BorderRadius.circular(Variables.radiusSmall), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.12), + blurRadius: 6, + offset: const Offset(0, 3), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(Variables.radiusSmall), + child: + hasPreview + ? Image( + image: imageProvider!, + fit: BoxFit.cover, + errorBuilder: + (_, __, ___) => _buildPlaceholder(theme), + ) + : _buildPlaceholder(theme), + ), + ), + ), + ), + const SizedBox(width: 16), + // Info Column + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (breadcrumb.isNotEmpty) + Text( + breadcrumb, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface + .withValues(alpha: 0.6), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + file.name, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(width: 8), + // Menu + if (onMenuAction != null) + PopupMenuButton<String>( + padding: EdgeInsets.zero, + icon: Icon( + Icons.more_vert, + size: 20, + color: theme.colorScheme.onSurface.withValues( + alpha: 0.6, + ), + ), + onSelected: onMenuAction, + itemBuilder: + (_) => const [ + PopupMenuItem( + value: "open", + child: Text("Open"), + ), + PopupMenuItem( + value: "rename", + child: Text("Rename"), + ), + PopupMenuItem( + value: "delete", + child: Text("Delete"), + ), + ], + ) + else + Icon( + Icons.more_vert, + size: 20, + color: theme.colorScheme.onSurface.withValues( + alpha: 0.6, + ), + ), + ], + ), + const SizedBox(height: 6), + Text( + dimensions, + style: TextStyle( + fontSize: 11, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface.withValues( + alpha: 0.6, + ), + ), + ), + const SizedBox(height: 4), + Text( + timeAgo, + style: TextStyle( + fontSize: 11, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface.withValues( + alpha: 0.4, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildPlaceholder(ThemeData theme) { + return Center( + child: Icon( + Icons.image, + size: 24, + color: theme.colorScheme.onSurface.withValues(alpha: 0.3), + ), + ); + } +} diff --git a/lib/ui/widgets/project_card.dart b/lib/ui/widgets/project_card.dart @@ -0,0 +1,254 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:creekui/data/models/project_model.dart'; +import 'package:creekui/ui/styles/variables.dart'; + +class ProjectCard extends StatelessWidget { + final ProjectModel project; + final List<String> previewImages; + final VoidCallback onTap; + final VoidCallback? onRename; + final VoidCallback? onDelete; + final bool isHorizontal; + final bool showGrid; + + const ProjectCard({ + super.key, + required this.project, + required this.previewImages, + required this.onTap, + this.onRename, + this.onDelete, + this.isHorizontal = true, + this.showGrid = false, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + return GestureDetector( + onTap: onTap, + child: Container( + width: isHorizontal ? 130 : null, + decoration: BoxDecoration( + color: theme.scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(Variables.radiusMedium), + border: Border.all( + color: isDark ? Variables.borderDark : Variables.borderSubtle, + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ClipRRect( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(Variables.radiusMedium), + ), + child: + showGrid + ? Padding( + padding: const EdgeInsets.all(4.0), + child: _buildGridPreview(isDark), + ) + : (previewImages.isNotEmpty + ? Image.file( + File(previewImages.first), + fit: BoxFit.cover, + width: double.infinity, + errorBuilder: + (context, error, stackTrace) => + _buildErrorPlaceholder(isDark, theme), + ) + : _buildEmptyPlaceholder(isDark, theme)), + ), + ), + Padding( + padding: const EdgeInsets.all(10), + child: Row( + children: [ + Expanded( + child: Text( + project.title, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 4), + if (onRename != null || onDelete != null) + SizedBox( + height: 24, + width: 24, + child: PopupMenuButton<String>( + icon: Icon( + Icons.more_vert, + size: 16, + color: theme.colorScheme.onSurface.withValues( + alpha: 0.6, + ), + ), + padding: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + onSelected: (value) { + if (value == 'rename' && onRename != null) { + onRename!(); + } else if (value == 'delete' && onDelete != null) { + onDelete!(); + } + }, + itemBuilder: + (context) => [ + if (onRename != null) + const PopupMenuItem( + value: 'rename', + child: Row( + children: [ + Icon(Icons.edit_outlined, size: 18), + SizedBox(width: 12), + Text( + 'Rename', + style: TextStyle( + fontFamily: 'GeneralSans', + ), + ), + ], + ), + ), + if (onDelete != null) + const PopupMenuItem( + value: 'delete', + child: Row( + children: [ + Icon( + Icons.delete_outline, + size: 18, + color: Colors.red, + ), + SizedBox(width: 12), + Text( + 'Delete', + style: TextStyle( + fontFamily: 'GeneralSans', + color: Colors.red, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildGridPreview(bool isDark) { + return Column( + children: [ + Expanded( + child: Row( + children: [ + Expanded( + child: _buildPreviewItem( + previewImages.isNotEmpty ? previewImages[0] : null, + isDark, + ), + ), + const SizedBox(width: 4), + Expanded( + child: _buildPreviewItem( + previewImages.length > 1 ? previewImages[1] : null, + isDark, + ), + ), + ], + ), + ), + const SizedBox(height: 4), + Expanded( + child: Row( + children: [ + Expanded( + child: _buildPreviewItem( + previewImages.length > 2 ? previewImages[2] : null, + isDark, + ), + ), + const SizedBox(width: 4), + Expanded( + child: _buildPreviewItem( + previewImages.length > 3 ? previewImages[3] : null, + isDark, + ), + ), + ], + ), + ), + ], + ); + } + + Widget _buildPreviewItem(String? imagePath, bool isDark) { + return Container( + decoration: BoxDecoration( + color: isDark ? Variables.surfaceDark : Variables.surfaceSubtle, + borderRadius: BorderRadius.circular(6), + ), + clipBehavior: Clip.antiAlias, + child: + imagePath != null + ? Image.file( + File(imagePath), + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: + (_, __, ___) => const Icon( + Icons.broken_image, + size: 16, + color: Colors.grey, + ), + ) + : null, + ); + } + + Widget _buildErrorPlaceholder(bool isDark, ThemeData theme) { + return Container( + color: isDark ? Variables.surfaceDark : Variables.surfaceSubtle, + child: Icon( + Icons.broken_image, + color: theme.colorScheme.onSurface.withValues(alpha: 0.3), + ), + ); + } + + Widget _buildEmptyPlaceholder(bool isDark, ThemeData theme) { + return Container( + color: isDark ? Variables.surfaceDark : Variables.surfaceSubtle, + child: Center( + child: Icon( + Icons.folder_outlined, + size: 32, + color: theme.colorScheme.onSurface.withValues(alpha: 0.3), + ), + ), + ); + } +} diff --git a/lib/ui/widgets/search_bar.dart b/lib/ui/widgets/search_bar.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:creekui/ui/styles/variables.dart'; + +class CommonSearchBar extends StatelessWidget { + final TextEditingController controller; + final ValueChanged<String>? onChanged; + final String hintText; + + const CommonSearchBar({ + super.key, + required this.controller, + this.onChanged, + this.hintText = 'Search', + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + return TextField( + controller: controller, + onChanged: onChanged, + decoration: InputDecoration( + hintText: hintText, + hintStyle: TextStyle( + fontSize: 12, + color: theme.colorScheme.onSurface.withValues(alpha: 0.5), + fontFamily: 'GeneralSans', + ), + prefixIcon: Icon( + Icons.search, + size: 18, + color: theme.colorScheme.onSurface.withValues(alpha: 0.5), + ), + prefixIconConstraints: const BoxConstraints( + minWidth: 50, + minHeight: 18, + ), + filled: true, + fillColor: isDark ? Variables.surfaceDark : Variables.surfaceSubtle, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(Variables.radiusSmall), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(Variables.radiusSmall), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(Variables.radiusSmall), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + style: TextStyle( + fontSize: 12, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface, + ), + ); + } +} diff --git a/lib/ui/widgets/section_header.dart b/lib/ui/widgets/section_header.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:creekui/ui/styles/variables.dart'; + +class SectionHeader extends StatelessWidget { + final String title; + final VoidCallback? onTap; + final Widget? trailing; + + const SectionHeader({ + super.key, + required this.title, + this.onTap, + this.trailing, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Row( + children: [ + Text( + title, + style: Variables.headerStyle.copyWith( + fontSize: 16, + color: theme.colorScheme.onSurface, + ), + ), + const Spacer(), + if (trailing != null) + trailing! + else if (onTap != null) + Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.chevron_right, + size: 24, + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + ), + ), + ], + ); + } +}