creek

The AI Image Editor of 2030

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}