creek

The AI Image Editor of 2030

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}