creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
lib/ui/pages/project_detail_page.dart (14466B)
1import 'dart:io';
2import 'package:flutter/material.dart';
3import 'package:flutter_svg/flutter_svg.dart';
4import 'package:creekui/data/models/project_model.dart';
5import 'package:creekui/data/models/image_model.dart';
6import 'package:creekui/data/repos/project_repo.dart';
7import 'package:creekui/data/repos/image_repo.dart';
8import 'package:creekui/services/project_service.dart';
9import 'package:creekui/ui/styles/variables.dart';
10import 'package:creekui/ui/widgets/empty_state.dart';
11import 'package:creekui/ui/widgets/section_header.dart';
12import 'package:creekui/ui/widgets/top_bar.dart';
13import 'package:creekui/ui/widgets/dialog.dart';
14import 'package:creekui/ui/widgets/text_field.dart';
15import 'package:creekui/ui/pages/settings_page.dart';
16import 'package:creekui/ui/pages/home_page.dart';
17import 'project_board_page.dart';
18import 'stylesheet_page.dart';
19import 'project_file_page.dart';
20
21class ProjectDetailPage extends StatefulWidget {
22 final int projectId;
23
24 const ProjectDetailPage({super.key, required this.projectId});
25
26 @override
27 State<ProjectDetailPage> createState() => _ProjectDetailPageState();
28}
29
30class _ProjectDetailPageState extends State<ProjectDetailPage> {
31 final _projectRepo = ProjectRepo();
32 final _projectService = ProjectService();
33 final _imageRepo = ImageRepo();
34
35 ProjectModel? _project;
36 List<ProjectModel> _events = [];
37 Map<int, List<ImageModel>> _eventImages = {}; // eventId -> recent images
38 bool _isLoading = true;
39
40 @override
41 void initState() {
42 super.initState();
43 _loadData();
44 }
45
46 Future<void> _loadData() async {
47 setState(() => _isLoading = true);
48 try {
49 final project = await _projectRepo.getProjectById(widget.projectId);
50
51 // If project not found, handle exit
52 if (project == null) {
53 if (mounted) {
54 Navigator.pop(context);
55 ScaffoldMessenger.of(
56 context,
57 ).showSnackBar(const SnackBar(content: Text('Project not found')));
58 }
59 return;
60 }
61
62 // Fetch sub-events
63 final events = await _projectRepo.getEvents(widget.projectId);
64
65 // Fetch recent images for each event (up to 3 most recent)
66 final Map<int, List<ImageModel>> eventImagesMap = {};
67 for (final event in events) {
68 if (event.id != null) {
69 final images = await _imageRepo.getImages(event.id!);
70 eventImagesMap[event.id!] = images.take(3).toList();
71 }
72 }
73
74 if (mounted) {
75 setState(() {
76 _project = project;
77 _events = events;
78 _eventImages = eventImagesMap;
79 _isLoading = false;
80 });
81 }
82 } catch (e) {
83 debugPrint('Error loading project data: $e');
84 if (mounted) setState(() => _isLoading = false);
85 }
86 }
87
88 Future<void> _createEventDialog() async {
89 if (_project?.id == null) return;
90
91 final nameController = TextEditingController();
92 final descriptionController = TextEditingController();
93
94 if (!mounted) return;
95
96 await ShowDialog.show(
97 context,
98 title: "Event Details",
99 primaryButtonText: "Add Event",
100 content: Column(
101 children: [
102 CommonTextField(
103 hintText: "Add Name*",
104 controller: nameController,
105 autoFocus: true,
106 ),
107 const SizedBox(height: 16),
108 CommonTextField(
109 hintText: "Add Description",
110 controller: descriptionController,
111 maxLines: 3,
112 ),
113 ],
114 ),
115 onPrimaryPressed: () async {
116 if (nameController.text.trim().isNotEmpty) {
117 try {
118 await _projectService.createProject(
119 nameController.text.trim(),
120 description:
121 descriptionController.text.trim().isEmpty
122 ? null
123 : descriptionController.text.trim(),
124 parentId: _project!.id,
125 );
126 if (context.mounted) {
127 Navigator.pop(context);
128 _loadData();
129 }
130 } catch (e) {
131 debugPrint("Error creating event: $e");
132 }
133 }
134 },
135 );
136 }
137
138 void _navigateToBoard(int projectId) {
139 Navigator.push(
140 context,
141 MaterialPageRoute(builder: (_) => ProjectBoardPage(projectId: projectId)),
142 ).then((_) => _loadData());
143 }
144
145 void _navigateToStylesheet(int projectId) {
146 Navigator.push(
147 context,
148 MaterialPageRoute(builder: (_) => StylesheetPage(projectId: projectId)),
149 ).then((_) => _loadData());
150 }
151
152 void _navigateToFiles(int projectId) {
153 Navigator.push(
154 context,
155 MaterialPageRoute(builder: (_) => ProjectFilePage(projectId: projectId)),
156 ).then((_) => _loadData());
157 }
158
159 @override
160 Widget build(BuildContext context) {
161 if (_isLoading) {
162 return const Scaffold(
163 backgroundColor: Variables.background,
164 body: Center(child: CircularProgressIndicator()),
165 );
166 }
167
168 if (_project == null) return const Scaffold(body: SizedBox());
169
170 return Scaffold(
171 backgroundColor: Variables.background,
172 appBar: TopBar(
173 currentProjectId: widget.projectId,
174 titleOverride: _project!.title,
175 hideSecondRow: true,
176 hideSelector: true,
177 showSettings: true,
178 onBack: () {
179 if (Navigator.canPop(context)) {
180 Navigator.pop(context);
181 } else {
182 Navigator.pushReplacement(
183 context,
184 MaterialPageRoute(builder: (_) => const HomePage()),
185 );
186 }
187 },
188 ),
189 body: RefreshIndicator(
190 onRefresh: _loadData,
191 child: SingleChildScrollView(
192 physics: const AlwaysScrollableScrollPhysics(),
193 padding: const EdgeInsets.symmetric(horizontal: 16),
194 child: Column(
195 crossAxisAlignment: CrossAxisAlignment.start,
196 children: [
197 const SizedBox(height: 16),
198 _buildActionCardsRow(_project!.id!),
199 const SizedBox(height: 16),
200 SectionHeader(
201 title: 'Events',
202 trailing: Material(
203 color: Colors.transparent,
204 child: InkWell(
205 onTap: _createEventDialog,
206 borderRadius: BorderRadius.circular(20),
207 child: Padding(
208 padding: const EdgeInsets.all(8.0),
209 child: SvgPicture.asset(
210 'assets/icons/add-line.svg',
211 width: 24,
212 height: 24,
213 colorFilter: const ColorFilter.mode(
214 Variables.textPrimary,
215 BlendMode.srcIn,
216 ),
217 ),
218 ),
219 ),
220 ),
221 ),
222 const SizedBox(height: 16),
223 if (_events.isEmpty)
224 const EmptyState(
225 icon: Icons.event_note,
226 title: "No events created yet",
227 subtitle: "Create an event to start organizing",
228 )
229 else
230 ListView.separated(
231 shrinkWrap: true,
232 physics: const NeverScrollableScrollPhysics(),
233 itemCount: _events.length,
234 separatorBuilder: (_, __) => const SizedBox(height: 16),
235 itemBuilder: (context, index) {
236 final event = _events[index];
237 return Center(child: _buildEventCard(event));
238 },
239 ),
240 const SizedBox(height: 40),
241 ],
242 ),
243 ),
244 ),
245 );
246 }
247
248 // Widgets
249 Widget _buildActionCardsRow(int projectId) {
250 return Row(
251 children: [
252 Expanded(
253 child: _buildActionCard(
254 blobImage: 'assets/moodboard_blob.png',
255 iconPath: 'assets/icons/moodboard_icon.svg',
256 label: 'Moodboard',
257 onTap: () => _navigateToBoard(projectId),
258 ),
259 ),
260 const SizedBox(width: 12),
261 Expanded(
262 child: _buildActionCard(
263 blobImage: 'assets/stylesheet_blob.png',
264 iconPath: 'assets/icons/stylesheet_icon.svg',
265 label: 'Stylesheet',
266 onTap: () => _navigateToStylesheet(projectId),
267 ),
268 ),
269 const SizedBox(width: 12),
270 Expanded(
271 child: _buildActionCard(
272 blobImage: 'assets/files_blob.png',
273 iconPath: 'assets/icons/files_icon.svg',
274 label: 'Files',
275 onTap: () => _navigateToFiles(projectId),
276 ),
277 ),
278 ],
279 );
280 }
281
282 Widget _buildActionCard({
283 required String blobImage,
284 required String iconPath,
285 required String label,
286 required VoidCallback onTap,
287 }) {
288 return GestureDetector(
289 onTap: onTap,
290 child: Container(
291 decoration: BoxDecoration(
292 borderRadius: BorderRadius.circular(16),
293 border: Border.all(color: Variables.borderSubtle, width: 1),
294 ),
295 child: Column(
296 crossAxisAlignment: CrossAxisAlignment.start,
297 children: [
298 // Blob image container
299 Container(
300 height: 115,
301 decoration: const BoxDecoration(
302 color: Color(0xFF27272A),
303 borderRadius: BorderRadius.only(
304 topLeft: Radius.circular(16),
305 topRight: Radius.circular(16),
306 ),
307 ),
308 child: Stack(
309 children: [
310 // Blob image
311 Positioned.fill(
312 child: ClipRRect(
313 borderRadius: const BorderRadius.only(
314 topLeft: Radius.circular(16),
315 topRight: Radius.circular(16),
316 ),
317 child: Image.asset(
318 blobImage,
319 fit: BoxFit.cover,
320 errorBuilder:
321 (context, error, stackTrace) =>
322 Container(color: const Color(0xFF27272A)),
323 ),
324 ),
325 ),
326 // Gradient overlay
327 Positioned.fill(
328 child: Container(
329 decoration: BoxDecoration(
330 gradient: LinearGradient(
331 begin: Alignment.centerLeft,
332 end: Alignment.centerRight,
333 colors: [
334 Colors.white.withOpacity(0.6),
335 const Color(0xFF111EB6).withOpacity(0.82),
336 ],
337 ),
338 backgroundBlendMode: BlendMode.hue,
339 ),
340 ),
341 ),
342 ],
343 ),
344 ),
345 // Icon and label section
346 Padding(
347 padding: const EdgeInsets.all(12),
348 child: Column(
349 crossAxisAlignment: CrossAxisAlignment.start,
350 children: [
351 // Icon in circular background
352 Container(
353 padding: const EdgeInsets.all(8),
354 decoration: BoxDecoration(
355 color: const Color(0xFFE0E7FF),
356 borderRadius: BorderRadius.circular(46),
357 ),
358 child: SvgPicture.asset(
359 iconPath,
360 width: 16,
361 height: 16,
362 colorFilter: const ColorFilter.mode(
363 Color(0xFF7C86FF),
364 BlendMode.srcIn,
365 ),
366 ),
367 ),
368 const SizedBox(height: 8),
369 // Label
370 Text(
371 label,
372 style: Variables.bodyStyle.copyWith(
373 fontWeight: FontWeight.w500,
374 fontSize: 12,
375 ),
376 maxLines: 1,
377 overflow: TextOverflow.ellipsis,
378 ),
379 ],
380 ),
381 ),
382 ],
383 ),
384 ),
385 );
386 }
387
388 Widget _buildEventCard(ProjectModel event) {
389 final eventImages = _eventImages[event.id] ?? [];
390 return GestureDetector(
391 onTap: () {
392 if (event.id != null) _navigateToBoard(event.id!);
393 },
394 child: Container(
395 width: 328,
396 decoration: BoxDecoration(
397 borderRadius: BorderRadius.circular(16),
398 border: Border.all(color: Variables.borderSubtle, width: 1),
399 ),
400 clipBehavior: Clip.antiAlias,
401 child: Column(
402 crossAxisAlignment: CrossAxisAlignment.start,
403 children: [
404 // 3 images side by side (or fewer if not available)
405 Container(
406 height: 115,
407 padding: const EdgeInsets.only(bottom: 4),
408 child: Row(
409 children: [
410 for (int i = 0; i < 3; i++) ...[
411 Expanded(
412 child: ClipRRect(
413 borderRadius: BorderRadius.circular(4),
414 child:
415 i < eventImages.length
416 ? Image.file(
417 File(eventImages[i].filePath),
418 fit: BoxFit.cover,
419 errorBuilder:
420 (_, __, ___) =>
421 Container(color: Colors.grey[300]),
422 )
423 : Container(color: Colors.grey[200]),
424 ),
425 ),
426 if (i < 2) const SizedBox(width: 4),
427 ],
428 ],
429 ),
430 ),
431 // Event title
432 Padding(
433 padding: const EdgeInsets.all(16),
434 child: Text(
435 event.title,
436 style: Variables.bodyStyle.copyWith(
437 fontWeight: FontWeight.w500,
438 ),
439 ),
440 ),
441 ],
442 ),
443 ),
444 );
445 }
446}