creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
lib/ui/widgets/project_card.dart (8697B)
1import 'dart:io';
2import 'package:flutter/material.dart';
3import 'package:creekui/data/models/project_model.dart';
4import 'package:creekui/ui/styles/variables.dart';
5
6class ProjectCard extends StatelessWidget {
7 final ProjectModel project;
8 final List<String> previewImages;
9 final VoidCallback onTap;
10 final VoidCallback? onRename;
11 final VoidCallback? onDelete;
12 final bool isHorizontal;
13 final bool showGrid;
14
15 const ProjectCard({
16 super.key,
17 required this.project,
18 required this.previewImages,
19 required this.onTap,
20 this.onRename,
21 this.onDelete,
22 this.isHorizontal = true,
23 this.showGrid = false,
24 });
25
26 @override
27 Widget build(BuildContext context) {
28 final theme = Theme.of(context);
29 final isDark = theme.brightness == Brightness.dark;
30
31 return GestureDetector(
32 onTap: onTap,
33 child: Container(
34 width: isHorizontal ? 130 : null,
35 decoration: BoxDecoration(
36 color: theme.scaffoldBackgroundColor,
37 borderRadius: BorderRadius.circular(Variables.radiusMedium),
38 border: Border.all(
39 color: isDark ? Variables.borderDark : Variables.borderSubtle,
40 width: 1,
41 ),
42 ),
43 child: Column(
44 crossAxisAlignment: CrossAxisAlignment.start,
45 children: [
46 Expanded(
47 child: ClipRRect(
48 borderRadius: const BorderRadius.vertical(
49 top: Radius.circular(Variables.radiusMedium),
50 ),
51 child:
52 showGrid
53 ? Padding(
54 padding: const EdgeInsets.all(4.0),
55 child: _buildGridPreview(isDark),
56 )
57 : (previewImages.isNotEmpty
58 ? Image.file(
59 File(previewImages.first),
60 fit: BoxFit.cover,
61 width: double.infinity,
62 errorBuilder:
63 (context, error, stackTrace) =>
64 _buildErrorPlaceholder(isDark, theme),
65 )
66 : _buildEmptyPlaceholder(isDark, theme)),
67 ),
68 ),
69 Padding(
70 padding: const EdgeInsets.all(10),
71 child: Row(
72 children: [
73 Expanded(
74 child: Text(
75 project.title,
76 style: Variables.bodyStyle.copyWith(
77 fontSize: 13,
78 fontWeight: FontWeight.w600,
79 color: theme.colorScheme.onSurface,
80 ),
81 maxLines: 1,
82 overflow: TextOverflow.ellipsis,
83 ),
84 ),
85 const SizedBox(width: 4),
86 if (onRename != null || onDelete != null)
87 SizedBox(
88 height: 24,
89 width: 24,
90 child: PopupMenuButton<String>(
91 icon: Icon(
92 Icons.more_vert,
93 size: 16,
94 color: theme.colorScheme.onSurface.withValues(
95 alpha: 0.6,
96 ),
97 ),
98 padding: EdgeInsets.zero,
99 shape: RoundedRectangleBorder(
100 borderRadius: BorderRadius.circular(12),
101 ),
102 onSelected: (value) {
103 if (value == 'rename' && onRename != null) {
104 onRename!();
105 } else if (value == 'delete' && onDelete != null) {
106 onDelete!();
107 }
108 },
109 itemBuilder:
110 (context) => [
111 if (onRename != null)
112 const PopupMenuItem(
113 value: 'rename',
114 child: Row(
115 children: [
116 Icon(Icons.edit_outlined, size: 18),
117 SizedBox(width: 12),
118 Text(
119 'Rename',
120 style: TextStyle(
121 fontFamily: 'GeneralSans',
122 ),
123 ),
124 ],
125 ),
126 ),
127 if (onDelete != null)
128 const PopupMenuItem(
129 value: 'delete',
130 child: Row(
131 children: [
132 Icon(
133 Icons.delete_outline,
134 size: 18,
135 color: Colors.red,
136 ),
137 SizedBox(width: 12),
138 Text(
139 'Delete',
140 style: TextStyle(
141 fontFamily: 'GeneralSans',
142 color: Colors.red,
143 ),
144 ),
145 ],
146 ),
147 ),
148 ],
149 ),
150 ),
151 ],
152 ),
153 ),
154 ],
155 ),
156 ),
157 );
158 }
159
160 Widget _buildGridPreview(bool isDark) {
161 return Column(
162 children: [
163 Expanded(
164 child: Row(
165 children: [
166 Expanded(
167 child: _buildPreviewItem(
168 previewImages.isNotEmpty ? previewImages[0] : null,
169 isDark,
170 ),
171 ),
172 const SizedBox(width: 4),
173 Expanded(
174 child: _buildPreviewItem(
175 previewImages.length > 1 ? previewImages[1] : null,
176 isDark,
177 ),
178 ),
179 ],
180 ),
181 ),
182 const SizedBox(height: 4),
183 Expanded(
184 child: Row(
185 children: [
186 Expanded(
187 child: _buildPreviewItem(
188 previewImages.length > 2 ? previewImages[2] : null,
189 isDark,
190 ),
191 ),
192 const SizedBox(width: 4),
193 Expanded(
194 child: _buildPreviewItem(
195 previewImages.length > 3 ? previewImages[3] : null,
196 isDark,
197 ),
198 ),
199 ],
200 ),
201 ),
202 ],
203 );
204 }
205
206 Widget _buildPreviewItem(String? imagePath, bool isDark) {
207 return Container(
208 decoration: BoxDecoration(
209 color: isDark ? Variables.surfaceDark : Variables.surfaceSubtle,
210 borderRadius: BorderRadius.circular(6),
211 ),
212 clipBehavior: Clip.antiAlias,
213 child:
214 imagePath != null
215 ? Image.file(
216 File(imagePath),
217 fit: BoxFit.cover,
218 width: double.infinity,
219 height: double.infinity,
220 errorBuilder:
221 (_, __, ___) => const Icon(
222 Icons.broken_image,
223 size: 16,
224 color: Colors.grey,
225 ),
226 )
227 : null,
228 );
229 }
230
231 Widget _buildErrorPlaceholder(bool isDark, ThemeData theme) {
232 return Container(
233 color: isDark ? Variables.surfaceDark : Variables.surfaceSubtle,
234 child: Icon(
235 Icons.broken_image,
236 color: theme.colorScheme.onSurface.withValues(alpha: 0.3),
237 ),
238 );
239 }
240
241 Widget _buildEmptyPlaceholder(bool isDark, ThemeData theme) {
242 return Container(
243 color: isDark ? Variables.surfaceDark : Variables.surfaceSubtle,
244 child: Center(
245 child: Icon(
246 Icons.folder_outlined,
247 size: 32,
248 color: theme.colorScheme.onSurface.withValues(alpha: 0.3),
249 ),
250 ),
251 );
252 }
253}