creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
lib/ui/pages/image_details_page.dart (41724B)
1import 'dart:io';
2import 'package:flutter/material.dart';
3import 'package:flutter_svg/flutter_svg.dart';
4import 'package:creekui/services/image_service.dart';
5import 'package:creekui/services/note_service.dart';
6import 'package:creekui/data/models/note_model.dart';
7import 'package:creekui/data/models/image_model.dart';
8import 'package:creekui/data/models/canvas_models.dart';
9import 'package:creekui/utils/image_actions_helper.dart';
10import 'package:creekui/utils/image_utils.dart';
11import 'package:creekui/ui/styles/variables.dart';
12import 'package:creekui/ui/widgets/primary_button.dart';
13import 'package:creekui/ui/widgets/empty_state.dart';
14import 'package:creekui/ui/widgets/selection_overlay_painter.dart';
15import 'package:creekui/ui/widgets/note_input_sheet.dart';
16import 'package:creekui/ui/widgets/app_bar.dart';
17import 'package:creekui/ui/widgets/dialog.dart';
18import 'package:creekui/ui/widgets/tag_chip.dart';
19
20class ImageDetailsPage extends StatefulWidget {
21 final String imagePath;
22 final String imageId;
23 final int projectId;
24
25 const ImageDetailsPage({
26 super.key,
27 required this.imagePath,
28 required this.imageId,
29 required this.projectId,
30 });
31
32 @override
33 State<ImageDetailsPage> createState() => _ImageDetailsPageState();
34}
35
36class _ImageDetailsPageState extends State<ImageDetailsPage> {
37 final NoteService _noteService = NoteService();
38 final ImageService _imageService = ImageService();
39
40 ImageModel? _imageModel;
41 List<NoteModel> _notes = [];
42 List<String> _currentTags = [];
43 bool _isLoading = true;
44 int? _activeNoteId;
45
46 // Drawing/Resizing
47 bool _isDrawMode = false;
48 bool _isResizing = false;
49 final GlobalKey _imageKey = GlobalKey();
50
51 Offset? _startPos;
52 Offset? _currentPos;
53 Rect? _finalSelectionRect;
54 Size? _imageRenderSize;
55
56 DragHandle _activeHandle = DragHandle.none;
57 Offset? _startDragLocalOffset;
58 final double _handleSize = 25.0;
59
60 final List<String> _allAvailableTags = [
61 'Compositions',
62 'Subject',
63 'Fonts',
64 'Background',
65 'Texture',
66 'Colours',
67 'Material Look',
68 'Lighting',
69 'Style',
70 'Era',
71 'Emotion',
72 ];
73
74 @override
75 void initState() {
76 super.initState();
77 _loadData();
78 }
79
80 Future<void> _loadData() async {
81 try {
82 final image = await _imageService.getImage(widget.imageId);
83 final notes = await _noteService.getNotesForImage(widget.imageId);
84 final tags = await _imageService.getTags(widget.imageId);
85
86 if (mounted) {
87 setState(() {
88 _imageModel = image;
89 _notes = notes;
90 _currentTags = tags;
91 _isLoading = false;
92 });
93 }
94 } catch (e) {
95 debugPrint("Error loading details: $e");
96 if (mounted) setState(() => _isLoading = false);
97 }
98 }
99
100 // Actions
101 void _resetSelectionMode() {
102 setState(() {
103 _isDrawMode = false;
104 _isResizing = false;
105 _finalSelectionRect = null;
106 _startPos = null;
107 _currentPos = null;
108 _activeHandle = DragHandle.none;
109 _startDragLocalOffset = null;
110 });
111 }
112
113 void _activateDrawMode() {
114 _resetSelectionMode();
115 setState(() => _isDrawMode = true);
116 }
117
118 void _confirmSelectionAndShowModal() {
119 if (_finalSelectionRect != null) {
120 setState(() {
121 _isResizing = false;
122 _activeHandle = DragHandle.none;
123 });
124 _showAddNoteInputDialog();
125 }
126 }
127
128 void _openNotesSheet({int? highlightId}) {
129 setState(() => _activeNoteId = highlightId);
130 showModalBottomSheet(
131 context: context,
132 isScrollControlled: true,
133 backgroundColor: Colors.transparent,
134 builder:
135 (context) => _NotesListSheet(
136 notes: _notes,
137 highlightId: highlightId,
138 onAddNotePressed: () {
139 Navigator.pop(context);
140 _activateDrawMode();
141 },
142 ),
143 ).whenComplete(() => setState(() => _activeNoteId = null));
144 }
145
146 // Gestures
147 Offset? _getLocalPosition(Offset globalPosition) {
148 final RenderBox? box =
149 _imageKey.currentContext?.findRenderObject() as RenderBox?;
150 if (box == null) return null;
151
152 // Store the render size
153 _imageRenderSize = box.size;
154
155 // Convert global to local
156 final local = box.globalToLocal(globalPosition);
157
158 // Clamp coordinates to ensure we don't draw/drag outside the image
159 final dx = local.dx.clamp(0.0, box.size.width);
160 final dy = local.dy.clamp(0.0, box.size.height);
161 return Offset(dx, dy);
162 }
163
164 // Resizing Handlers
165 DragHandle _getDragHandle(Offset pos) {
166 if (_finalSelectionRect == null) return DragHandle.none;
167 final rect = _finalSelectionRect!;
168
169 // Check corners first
170 if (Rect.fromCircle(
171 center: rect.topLeft,
172 radius: _handleSize,
173 ).contains(pos))
174 return DragHandle.topLeft;
175 if (Rect.fromCircle(
176 center: rect.topRight,
177 radius: _handleSize,
178 ).contains(pos))
179 return DragHandle.topRight;
180 if (Rect.fromCircle(
181 center: rect.bottomLeft,
182 radius: _handleSize,
183 ).contains(pos))
184 return DragHandle.bottomLeft;
185 if (Rect.fromCircle(
186 center: rect.bottomRight,
187 radius: _handleSize,
188 ).contains(pos))
189 return DragHandle.bottomRight;
190 if (rect.contains(pos)) return DragHandle.center;
191 return DragHandle.none;
192 }
193
194 void _onPanStart(DragStartDetails details) {
195 if (_isDrawMode) {
196 final pos = _getLocalPosition(details.globalPosition);
197 if (pos == null) return;
198 setState(() {
199 _startPos = pos;
200 _currentPos = pos;
201 });
202 } else if (_isResizing && _finalSelectionRect != null) {
203 final pos = _getLocalPosition(details.globalPosition);
204 if (pos == null) return;
205 final handle = _getDragHandle(pos);
206 if (handle != DragHandle.none) {
207 setState(() {
208 _activeHandle = handle;
209 if (handle == DragHandle.center) {
210 _startDragLocalOffset = pos - _finalSelectionRect!.topLeft;
211 }
212 });
213 }
214 }
215 }
216
217 void _onPanUpdate(DragUpdateDetails details) {
218 if (_isDrawMode) {
219 final pos = _getLocalPosition(details.globalPosition);
220 if (pos == null) return;
221 setState(() => _currentPos = pos);
222 } else if (_isResizing && _finalSelectionRect != null) {
223 final pos = _getLocalPosition(details.globalPosition);
224 if (pos == null || _activeHandle == DragHandle.none) return;
225
226 setState(() {
227 Rect newRect = _finalSelectionRect!;
228 final newPoint = pos;
229
230 switch (_activeHandle) {
231 case DragHandle.topLeft:
232 newRect = Rect.fromLTRB(
233 newPoint.dx,
234 newPoint.dy,
235 newRect.right,
236 newRect.bottom,
237 );
238 break;
239 case DragHandle.topRight:
240 newRect = Rect.fromLTRB(
241 newRect.left,
242 newPoint.dy,
243 newPoint.dx,
244 newRect.bottom,
245 );
246 break;
247 case DragHandle.bottomLeft:
248 newRect = Rect.fromLTRB(
249 newPoint.dx,
250 newRect.top,
251 newRect.right,
252 newPoint.dy,
253 );
254 break;
255 case DragHandle.bottomRight:
256 newRect = Rect.fromLTRB(
257 newRect.left,
258 newRect.top,
259 newPoint.dx,
260 newPoint.dy,
261 );
262 break;
263 case DragHandle.center:
264 if (_startDragLocalOffset != null) {
265 final newTopLeft = newPoint - _startDragLocalOffset!;
266 newRect = Rect.fromLTWH(
267 newTopLeft.dx,
268 newTopLeft.dy,
269 newRect.width,
270 newRect.height,
271 );
272 }
273 break;
274 case DragHandle.none:
275 return;
276 }
277
278 if (_imageRenderSize != null) {
279 final cl = newRect.left.clamp(0.0, _imageRenderSize!.width);
280 final ct = newRect.top.clamp(0.0, _imageRenderSize!.height);
281 final cr = newRect.right.clamp(0.0, _imageRenderSize!.width);
282 final cb = newRect.bottom.clamp(0.0, _imageRenderSize!.height);
283 newRect = Rect.fromLTRB(cl, ct, cr, cb).normalize();
284 } else {
285 newRect = newRect.normalize();
286 }
287
288 if (newRect.width > 10 && newRect.height > 10)
289 _finalSelectionRect = newRect;
290 });
291 }
292 }
293
294 void _onPanEnd(DragEndDetails details) {
295 if (_isDrawMode && _startPos != null && _currentPos != null) {
296 final rect = Rect.fromPoints(_startPos!, _currentPos!).normalize();
297 if (rect.width < 10 || rect.height < 10) {
298 _resetSelectionMode();
299 return;
300 }
301 setState(() {
302 _isDrawMode = false;
303 _isResizing = true;
304 _finalSelectionRect = rect;
305 _startPos = null;
306 _currentPos = null;
307 });
308 } else if (_isResizing) {
309 setState(() {
310 _activeHandle = DragHandle.none;
311 _startDragLocalOffset = null;
312 });
313 }
314 }
315
316 // Show modal
317 void _showAddNoteInputDialog() {
318 String initialCategory =
319 _allAvailableTags.contains('Compositions')
320 ? 'Compositions'
321 : (_allAvailableTags.firstOrNull ?? 'General');
322
323 showModalBottomSheet(
324 context: context,
325 isScrollControlled: true,
326 backgroundColor: Colors.transparent,
327 builder: (context) {
328 return NoteModalOverlay(
329 screenSize: MediaQuery.of(context).size,
330 modalContent: NoteInputSheet(
331 categories: _allAvailableTags,
332 initialCategory: initialCategory,
333 onSubmit: (content, category) async {
334 if (_finalSelectionRect != null && _imageRenderSize != null) {
335 final normRect = _finalSelectionRect!.normalize();
336 await _noteService.addNote(
337 widget.imageId,
338 content,
339 category,
340 normX: normRect.center.dx / _imageRenderSize!.width,
341 normY: normRect.center.dy / _imageRenderSize!.height,
342 normWidth: normRect.width / _imageRenderSize!.width,
343 normHeight: normRect.height / _imageRenderSize!.height,
344 );
345 final updatedNotes = await _noteService.getNotesForImage(
346 widget.imageId,
347 );
348 if (mounted)
349 setState(() {
350 _notes = updatedNotes;
351 _finalSelectionRect = null;
352 });
353 if (context.mounted) Navigator.pop(context);
354 }
355 },
356 ),
357 );
358 },
359 );
360 }
361
362 // Edit Tags Dialog
363 void _openEditTagsDialog() {
364 List<String> tempTags = List.from(_currentTags);
365 showDialog(
366 context: context,
367 builder:
368 (ctx) => StatefulBuilder(
369 builder: (context, setLocalState) {
370 return Dialog(
371 backgroundColor: Colors.white,
372 shape: RoundedRectangleBorder(
373 borderRadius: BorderRadius.circular(Variables.radiusLarge),
374 ),
375 child: ShowDialog(
376 title: "Edit Tags",
377 primaryButtonText: "Save",
378 onPrimaryPressed: () async {
379 setState(() => _currentTags = tempTags);
380 await _imageService.updateTags(widget.imageId, tempTags);
381 if (mounted) Navigator.pop(context);
382 },
383 content: SingleChildScrollView(
384 child: Wrap(
385 spacing: 8,
386 runSpacing: 8,
387 children:
388 _allAvailableTags.map((tag) {
389 final isSelected = tempTags.contains(tag);
390 return GestureDetector(
391 onTap: () {
392 setLocalState(() {
393 isSelected
394 ? tempTags.remove(tag)
395 : tempTags.add(tag);
396 });
397 },
398 child: TagChip(
399 label: tag,
400 icon:
401 isSelected
402 ? const Icon(
403 Icons.check,
404 size: 14,
405 color: Variables.chipText,
406 )
407 : null,
408 ),
409 );
410 }).toList(),
411 ),
412 ),
413 ),
414 );
415 },
416 ),
417 );
418 }
419
420 @override
421 Widget build(BuildContext context) {
422 // Determine if user can pan/zoom the image carousel
423 final isSelectionModeActive = _isDrawMode || _isResizing;
424
425 return Scaffold(
426 // Prevents the main screen from pushing up when the keyboard opens
427 resizeToAvoidBottomInset: false,
428 backgroundColor: Variables.surfaceBackground,
429 appBar: CommonAppBar(
430 showBack: true,
431 leading: IconButton(
432 icon: const Icon(
433 Icons.arrow_back_ios_new,
434 size: 20,
435 color: Variables.textPrimary,
436 ),
437 onPressed: () => Navigator.pop(context),
438 ),
439 centerTitle: true,
440 titleWidget:
441 (!isSelectionModeActive && _imageModel != null)
442 ? Container(
443 height: 40,
444 decoration: BoxDecoration(
445 color: Variables.surfaceSubtle,
446 borderRadius: BorderRadius.circular(30),
447 ),
448 child: InkWell(
449 borderRadius: BorderRadius.circular(30),
450 onTap:
451 () => ImageActionsHelper.sendToFiles(
452 context,
453 _imageModel!.filePath,
454 ),
455 child: Padding(
456 padding: const EdgeInsets.symmetric(horizontal: 16.0),
457 child: Row(
458 mainAxisSize: MainAxisSize.min,
459 children: [
460 SvgPicture.asset(
461 'assets/icons/files_icon.svg',
462 width: 18,
463 height: 18,
464 colorFilter: const ColorFilter.mode(
465 Variables.textPrimary,
466 BlendMode.srcIn,
467 ),
468 ),
469 const SizedBox(width: 8),
470 Text(
471 "Send to File",
472 style: Variables.headerStyle.copyWith(
473 fontSize: 14,
474 fontWeight: FontWeight.w600,
475 ),
476 ),
477 ],
478 ),
479 ),
480 ),
481 )
482 : null,
483 actions: [
484 // 1. Confirm Selection (Resizing mode)
485 if (_isResizing && _finalSelectionRect != null)
486 IconButton(
487 icon: const Icon(
488 Icons.check_circle_outline,
489 color: Color(0xFF7C4DFF),
490 ),
491 onPressed: _confirmSelectionAndShowModal,
492 ),
493
494 // 2. Cancel Selection (Any selection mode)
495 if (isSelectionModeActive)
496 IconButton(
497 icon: const Icon(Icons.close, color: Variables.textPrimary),
498 onPressed: _resetSelectionMode,
499 ),
500
501 // 3. Standard Actions (when NOT selecting)
502 if (!isSelectionModeActive && _imageModel != null) ...[
503 // Share Button
504 IconButton(
505 onPressed:
506 () => ImageActionsHelper.shareImage(
507 context,
508 _imageModel!.filePath,
509 ),
510 icon: SvgPicture.asset(
511 'assets/icons/share_icon.svg',
512 width: 20,
513 height: 20,
514 colorFilter: const ColorFilter.mode(
515 Variables.textPrimary,
516 BlendMode.srcIn,
517 ),
518 ),
519 ),
520
521 // Rename Button
522 IconButton(
523 onPressed:
524 () => ImageActionsHelper.renameImage(
525 context,
526 _imageModel!,
527 () => _loadData(),
528 ),
529 icon: const Icon(
530 Icons.drive_file_rename_outline,
531 color: Variables.textPrimary,
532 ),
533 ),
534 ],
535 ],
536 ),
537 body:
538 _isLoading
539 ? const Center(
540 child: CircularProgressIndicator(color: Variables.textPrimary),
541 )
542 : Column(
543 children: [
544 Expanded(
545 child: SingleChildScrollView(
546 child: Column(
547 crossAxisAlignment: CrossAxisAlignment.start,
548 children: [
549 // 1. Image Container
550 Container(
551 margin: const EdgeInsets.symmetric(horizontal: 16),
552 constraints: const BoxConstraints(maxHeight: 500),
553 decoration: BoxDecoration(
554 color: Colors.black,
555 borderRadius: BorderRadius.circular(20),
556 ),
557 child: ClipRRect(
558 borderRadius: BorderRadius.circular(20),
559 child: LayoutBuilder(
560 builder: (context, constraints) {
561 return Stack(
562 fit: StackFit.passthrough,
563 children: [
564 InteractiveViewer(
565 // Disable pan/scale if selection or resizing is active
566 panEnabled: !isSelectionModeActive,
567 scaleEnabled: !isSelectionModeActive,
568 child: GestureDetector(
569 onPanStart: _onPanStart,
570 onPanUpdate: _onPanUpdate,
571 onPanEnd: _onPanEnd,
572 child: Stack(
573 children: [
574 Image.file(
575 File(widget.imagePath),
576 key: _imageKey,
577 fit: BoxFit.contain,
578 width: double.infinity,
579 ),
580
581 // Drawing Overlay (if in drawing mode)
582 if (_isDrawMode &&
583 _startPos != null &&
584 _currentPos != null)
585 Positioned.fill(
586 child: CustomPaint(
587 painter:
588 SelectionOverlayPainter(
589 rect:
590 Rect.fromPoints(
591 _startPos!,
592 _currentPos!,
593 ).normalize(),
594 isResizing: false,
595 ),
596 ),
597 ),
598
599 // Final Selection Rect (if in resizing mode)
600 if (_isResizing &&
601 _finalSelectionRect != null)
602 Positioned.fill(
603 child: CustomPaint(
604 painter:
605 SelectionOverlayPainter(
606 rect:
607 _finalSelectionRect!,
608 isResizing: true,
609 activeHandle:
610 _activeHandle,
611 ),
612 ),
613 ),
614 ],
615 ),
616 ),
617 ),
618
619 // Existing Note Indicators
620 ..._notes.map((note) {
621 final x =
622 note.normX * constraints.maxWidth;
623 final y =
624 note.normY * constraints.maxHeight;
625 return Positioned(
626 left: x - 10,
627 top: y - 10,
628 child: GestureDetector(
629 onTap:
630 () => _openNotesSheet(
631 highlightId: note.id,
632 ),
633 child: Container(
634 width: 20,
635 height: 20,
636 decoration: BoxDecoration(
637 color: Colors.white,
638 shape: BoxShape.circle,
639 boxShadow: [
640 BoxShadow(
641 color: Colors.black
642 .withOpacity(0.3),
643 blurRadius: 4,
644 offset: const Offset(0, 1),
645 ),
646 ],
647 border:
648 note.id == _activeNoteId
649 ? Border.all(
650 color: const Color(
651 0xFF7C4DFF,
652 ),
653 width: 3,
654 )
655 : null,
656 ),
657 ),
658 ),
659 );
660 }),
661
662 // Notes Button (Show only when not in selection mode)
663 if (!isSelectionModeActive)
664 Positioned(
665 bottom: 12,
666 right: 12,
667 child: ElevatedButton(
668 onPressed: () => _openNotesSheet(),
669 style: ElevatedButton.styleFrom(
670 backgroundColor: Colors.white,
671 foregroundColor: Colors.black,
672 elevation: 4,
673 padding:
674 const EdgeInsets.symmetric(
675 horizontal: 16,
676 vertical: 12,
677 ),
678 shape: RoundedRectangleBorder(
679 borderRadius:
680 BorderRadius.circular(12),
681 ),
682 ),
683 child: Row(
684 children: const [
685 Text(
686 "Notes",
687 style: TextStyle(
688 fontWeight: FontWeight.w600,
689 fontSize: 14,
690 ),
691 ),
692 SizedBox(width: 8),
693 Icon(
694 Icons.assignment_outlined,
695 size: 18,
696 ),
697 ],
698 ),
699 ),
700 ),
701
702 // Instruction Overlays
703 if (_isDrawMode && _startPos == null)
704 Positioned(
705 top: 20,
706 left: 0,
707 right: 0,
708 child: Center(
709 child: Container(
710 padding:
711 const EdgeInsets.symmetric(
712 horizontal: 16,
713 vertical: 8,
714 ),
715 decoration: BoxDecoration(
716 color: Colors.black87,
717 borderRadius:
718 BorderRadius.circular(20),
719 ),
720 child: const Text(
721 "Drag on image to select area",
722 style: TextStyle(
723 color: Colors.white,
724 fontSize: 12,
725 ),
726 ),
727 ),
728 ),
729 ),
730 if (_isResizing &&
731 _finalSelectionRect != null &&
732 _activeHandle == DragHandle.none)
733 Positioned(
734 top: 20,
735 left: 0,
736 right: 0,
737 child: Center(
738 child: Container(
739 padding:
740 const EdgeInsets.symmetric(
741 horizontal: 16,
742 vertical: 8,
743 ),
744 decoration: BoxDecoration(
745 color: Colors.black87,
746 borderRadius:
747 BorderRadius.circular(20),
748 ),
749 child: const Text(
750 "Adjust area or tap checkmark to confirm",
751 style: TextStyle(
752 color: Colors.white,
753 fontSize: 12,
754 ),
755 ),
756 ),
757 ),
758 ),
759 ],
760 );
761 },
762 ),
763 ),
764 ),
765
766 const SizedBox(height: 20),
767
768 // 2. Info Section
769 Padding(
770 padding: const EdgeInsets.symmetric(horizontal: 20),
771 child: Column(
772 crossAxisAlignment: CrossAxisAlignment.start,
773 children: [
774 Text(
775 _imageModel?.name ?? "Untitled Image",
776 style: Variables.bodyStyle.copyWith(
777 fontSize: 16,
778 fontWeight: FontWeight.bold,
779 ),
780 ),
781
782 const SizedBox(height: 24),
783
784 // Tags Box
785 Container(
786 padding: const EdgeInsets.all(16),
787 decoration: BoxDecoration(
788 color: const Color(0xFFF9F9F9),
789 borderRadius: BorderRadius.circular(16),
790 border: Border.all(
791 color: Variables.borderSubtle,
792 ),
793 ),
794 child: Column(
795 children: [
796 Row(
797 mainAxisAlignment:
798 MainAxisAlignment.spaceBetween,
799 children: [
800 const Text(
801 "Tags",
802 style: TextStyle(
803 fontWeight: FontWeight.bold,
804 fontSize: 15,
805 ),
806 ),
807 GestureDetector(
808 onTap: _openEditTagsDialog,
809 child: const Icon(
810 Icons.edit_outlined,
811 size: 18,
812 color: Variables.textSecondary,
813 ),
814 ),
815 ],
816 ),
817 const SizedBox(height: 12),
818 const Divider(
819 height: 1,
820 color: Variables.borderSubtle,
821 ),
822 const SizedBox(height: 16),
823 SizedBox(
824 width: double.infinity,
825 child:
826 _currentTags.isEmpty
827 ? const Text(
828 "No tags yet.",
829 style: TextStyle(
830 color: Colors.grey,
831 ),
832 )
833 : Wrap(
834 spacing: 8,
835 runSpacing: 8,
836 children:
837 _currentTags
838 .map(
839 (tag) => TagChip(
840 label: tag,
841 icon: null,
842 ),
843 )
844 .toList(),
845 ),
846 ),
847 ],
848 ),
849 ),
850 ],
851 ),
852 ),
853 const SizedBox(height: 40),
854 ],
855 ),
856 ),
857 ),
858 ],
859 ),
860 );
861 }
862}
863
864// Notes List Sheet
865class _NotesListSheet extends StatefulWidget {
866 final List<NoteModel> notes;
867 final int? highlightId;
868 final VoidCallback onAddNotePressed;
869 const _NotesListSheet({
870 required this.notes,
871 this.highlightId,
872 required this.onAddNotePressed,
873 });
874 @override
875 State<_NotesListSheet> createState() => __NotesListSheetState();
876}
877
878class __NotesListSheetState extends State<_NotesListSheet> {
879 final ScrollController _scrollController = ScrollController();
880 @override
881 void initState() {
882 super.initState();
883 if (widget.highlightId != null) {
884 WidgetsBinding.instance.addPostFrameCallback((_) {
885 final index = widget.notes.indexWhere(
886 (n) => n.id == widget.highlightId,
887 );
888 if (index != -1 && _scrollController.hasClients) {
889 _scrollController.animateTo(
890 index * 80.0,
891 duration: const Duration(milliseconds: 300),
892 curve: Curves.easeOut,
893 );
894 }
895 });
896 }
897 }
898
899 @override
900 Widget build(BuildContext context) {
901 return DraggableScrollableSheet(
902 initialChildSize: 0.5,
903 minChildSize: 0.3,
904 maxChildSize: 0.85,
905 builder: (_, controller) {
906 return Container(
907 decoration: const BoxDecoration(
908 color: Colors.white,
909 borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
910 ),
911 padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
912 child: Column(
913 children: [
914 Center(
915 child: Container(
916 width: 40,
917 height: 4,
918 margin: const EdgeInsets.only(bottom: 20),
919 decoration: BoxDecoration(
920 color: Colors.grey[300],
921 borderRadius: BorderRadius.circular(2),
922 ),
923 ),
924 ),
925 Row(
926 mainAxisAlignment: MainAxisAlignment.spaceBetween,
927 children: [
928 Text(
929 "Notes (${widget.notes.length})",
930 style: const TextStyle(
931 fontWeight: FontWeight.bold,
932 fontSize: 18,
933 ),
934 ),
935 IconButton(
936 icon: const Icon(Icons.close, color: Colors.grey),
937 onPressed: () => Navigator.pop(context),
938 ),
939 ],
940 ),
941 const Divider(),
942 Expanded(
943 child:
944 widget.notes.isEmpty
945 ? const EmptyState(
946 icon: Icons.comment,
947 title: "No notes yet",
948 subtitle: "Tap 'Add Note' to start",
949 )
950 : ListView.builder(
951 controller: controller,
952 itemCount: widget.notes.length,
953 itemBuilder: (context, index) {
954 final note = widget.notes[index];
955 final isHighlighted = note.id == widget.highlightId;
956 return Container(
957 margin: const EdgeInsets.only(bottom: 12),
958 padding: const EdgeInsets.all(16),
959 decoration: BoxDecoration(
960 color:
961 isHighlighted
962 ? const Color(0xFFF3F0FF)
963 : Colors.white,
964 borderRadius: BorderRadius.circular(12),
965 border: Border.all(
966 color:
967 isHighlighted
968 ? const Color(0xFF7C4DFF)
969 : Colors.grey[200]!,
970 width: isHighlighted ? 1.5 : 1,
971 ),
972 ),
973 child: Column(
974 crossAxisAlignment: CrossAxisAlignment.start,
975 children: [
976 Container(
977 padding: const EdgeInsets.symmetric(
978 horizontal: 10,
979 vertical: 4,
980 ),
981 decoration: BoxDecoration(
982 color: const Color(0xFFEEF0FF),
983 borderRadius: BorderRadius.circular(6),
984 ),
985 child: Text(
986 note.category,
987 style: const TextStyle(
988 fontSize: 11,
989 fontWeight: FontWeight.w700,
990 color: Color(0xFF7C4DFF),
991 ),
992 ),
993 ),
994 const SizedBox(height: 8),
995 Text(
996 note.content,
997 style: const TextStyle(
998 fontSize: 15,
999 color: Colors.black87,
1000 height: 1.4,
1001 ),
1002 ),
1003 ],
1004 ),
1005 );
1006 },
1007 ),
1008 ),
1009 // Add Note Button
1010 const SizedBox(height: 10),
1011 PrimaryButton(
1012 text: "Add Note",
1013 onPressed: widget.onAddNotePressed,
1014 iconPath: "assets/icons/add-line.svg",
1015 ),
1016 const SizedBox(height: 10),
1017 ],
1018 ),
1019 );
1020 },
1021 );
1022 }
1023}