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:
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),
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}