creek

The AI Image Editor of 2030

lib/ui/pages/image_save_page.dart (33896B)


  1import 'dart:io';
  2import 'package:flutter/material.dart';
  3import 'package:flutter/services.dart';
  4import 'package:creekui/data/models/canvas_models.dart';
  5import 'package:creekui/services/image_service.dart';
  6import 'package:creekui/services/note_service.dart';
  7import 'package:creekui/utils/image_utils.dart';
  8import 'package:creekui/ui/styles/variables.dart';
  9import 'package:creekui/ui/widgets/primary_button.dart';
 10import 'package:creekui/ui/widgets/selection_overlay_painter.dart';
 11import 'package:creekui/ui/widgets/note_input_sheet.dart';
 12import 'package:creekui/ui/widgets/app_bar.dart';
 13import 'project_board_page.dart';
 14
 15// Temporary note model
 16class TempNote {
 17  final double normX, normY, normWidth, normHeight;
 18  final String content, category;
 19  TempNote({
 20    required this.normX,
 21    required this.normY,
 22    required this.normWidth,
 23    required this.normHeight,
 24    required this.content,
 25    required this.category,
 26  });
 27}
 28
 29class ImageSavePage extends StatefulWidget {
 30  final List<String> imagePaths;
 31  final int projectId;
 32  final String projectName;
 33  final bool isFromShare;
 34  final String? parentProjectName;
 35
 36  const ImageSavePage({
 37    super.key,
 38    required this.imagePaths,
 39    required this.projectId,
 40    required this.projectName,
 41    this.isFromShare = true,
 42    this.parentProjectName,
 43  });
 44
 45  @override
 46  State<ImageSavePage> createState() => _ImageSavePageState();
 47}
 48
 49class _ImageSavePageState extends State<ImageSavePage> {
 50  final ImageService _imageService = ImageService();
 51  final NoteService _noteService = NoteService();
 52
 53  late PageController _pageController;
 54  int _currentImageIndex = 0;
 55
 56  final Map<int, Set<String>> _tagsPerImage = {};
 57  final Map<int, List<TempNote>> _notesPerImage = {};
 58
 59  // A unique key for each image to calculate drawing coordinates correctly
 60  late List<GlobalKey> _imageKeys;
 61
 62  bool _isSaving = false;
 63
 64  // Drawing/Resizing state
 65  bool _isDrawMode =
 66      false; // True when initial drag is happening (to create box)
 67  bool _isResizing =
 68      false; // True when a selection box is visible and resizable
 69  Offset? _startPos;
 70  Offset? _currentPos;
 71  Rect? _finalSelectionRect;
 72
 73  // Resizing state
 74  DragHandle _activeHandle = DragHandle.none;
 75  Offset? _startDragLocalOffset; // Used for moving the entire rect
 76  final Map<int, Size> _imageRenderSizes = {};
 77  final double _handleSize = 25.0;
 78
 79  final List<String> _availableTags = [
 80    'Compositions',
 81    'Subject',
 82    'Fonts',
 83    'Background',
 84    'Texture',
 85    'Colours',
 86    'Material Look',
 87    'Lighting',
 88    'Style',
 89    'Era',
 90    'Emotion',
 91  ];
 92
 93  @override
 94  void initState() {
 95    super.initState();
 96    _pageController = PageController();
 97
 98    // Generate a unique key for every image path
 99    _imageKeys = List.generate(widget.imagePaths.length, (_) => GlobalKey());
100    for (int i = 0; i < widget.imagePaths.length; i++) {
101      _tagsPerImage[i] = {};
102      _notesPerImage[i] = [];
103    }
104  }
105
106  @override
107  void dispose() {
108    _pageController.dispose();
109    super.dispose();
110  }
111
112  // Actions
113  void _activateSelectionMode() {
114    setState(() {
115      _isDrawMode = true; // Start initial drawing mode
116      _isResizing = false;
117      _finalSelectionRect = null;
118      _startPos = null;
119      _currentPos = null;
120    });
121  }
122
123  void _resetSelectionMode() {
124    setState(() {
125      _isDrawMode = false;
126      _isResizing = false;
127      _finalSelectionRect = null;
128      _startPos = null;
129      _currentPos = null;
130      _activeHandle = DragHandle.none;
131    });
132  }
133
134  void _confirmSelectionAndShowModal() {
135    if (_finalSelectionRect != null) {
136      // Exit resizing mode before showing the modal to prevent visual conflict
137      setState(() => _isResizing = false);
138      _showNoteModal();
139    }
140  }
141
142  void _toggleTag(String tag) {
143    setState(() {
144      final tags = _tagsPerImage[_currentImageIndex]!;
145      if (tags.contains(tag))
146        tags.remove(tag);
147      else
148        tags.add(tag);
149    });
150  }
151
152  // Gestures
153  Offset? _getLocalPosition(Offset globalPosition) {
154    final currentKey = _imageKeys[_currentImageIndex];
155    final RenderBox? box =
156        currentKey.currentContext?.findRenderObject() as RenderBox?;
157    if (box == null) return null;
158    _imageRenderSizes[_currentImageIndex] = box.size;
159    final local = box.globalToLocal(globalPosition);
160
161    // Clamp coordinates to ensure we don't draw/drag outside the image
162    final dx = local.dx.clamp(0.0, box.size.width);
163    final dy = local.dy.clamp(0.0, box.size.height);
164    return Offset(dx, dy);
165  }
166
167  DragHandle _getDragHandle(Offset pos) {
168    if (_finalSelectionRect == null) return DragHandle.none;
169    final rect = _finalSelectionRect!;
170
171    // Check corners first
172    if (Rect.fromCircle(
173      center: rect.topLeft,
174      radius: _handleSize,
175    ).contains(pos))
176      return DragHandle.topLeft;
177    if (Rect.fromCircle(
178      center: rect.topRight,
179      radius: _handleSize,
180    ).contains(pos))
181      return DragHandle.topRight;
182    if (Rect.fromCircle(
183      center: rect.bottomLeft,
184      radius: _handleSize,
185    ).contains(pos))
186      return DragHandle.bottomLeft;
187    if (Rect.fromCircle(
188      center: rect.bottomRight,
189      radius: _handleSize,
190    ).contains(pos))
191      return DragHandle.bottomRight;
192    if (rect.contains(pos)) return DragHandle.center;
193    return DragHandle.none;
194  }
195
196  void _onPanStart(DragStartDetails details) {
197    if (_isDrawMode) {
198      final pos = _getLocalPosition(details.globalPosition);
199      if (pos == null) return;
200      setState(() {
201        _startPos = pos;
202        _currentPos = pos;
203      });
204    } else if (_isResizing && _finalSelectionRect != null) {
205      final pos = _getLocalPosition(details.globalPosition);
206      if (pos == null) return;
207      final handle = _getDragHandle(pos);
208      if (handle != DragHandle.none) {
209        setState(() {
210          _activeHandle = handle;
211          if (handle == DragHandle.center) {
212            _startDragLocalOffset = pos - _finalSelectionRect!.topLeft;
213          }
214        });
215      }
216    }
217  }
218
219  void _onPanUpdate(DragUpdateDetails details) {
220    if (_isDrawMode) {
221      final pos = _getLocalPosition(details.globalPosition);
222      if (pos == null) return;
223      setState(() => _currentPos = pos);
224    } else if (_isResizing && _finalSelectionRect != null) {
225      final pos = _getLocalPosition(details.globalPosition);
226      if (pos == null || _activeHandle == DragHandle.none) return;
227
228      setState(() {
229        Rect newRect = _finalSelectionRect!;
230        final newPoint = pos;
231
232        switch (_activeHandle) {
233          case DragHandle.topLeft:
234            newRect = Rect.fromLTRB(
235              newPoint.dx,
236              newPoint.dy,
237              newRect.right,
238              newRect.bottom,
239            );
240            break;
241          case DragHandle.topRight:
242            newRect = Rect.fromLTRB(
243              newRect.left,
244              newPoint.dy,
245              newPoint.dx,
246              newRect.bottom,
247            );
248            break;
249          case DragHandle.bottomLeft:
250            newRect = Rect.fromLTRB(
251              newPoint.dx,
252              newRect.top,
253              newRect.right,
254              newPoint.dy,
255            );
256            break;
257          case DragHandle.bottomRight:
258            newRect = Rect.fromLTRB(
259              newRect.left,
260              newRect.top,
261              newPoint.dx,
262              newPoint.dy,
263            );
264            break;
265          case DragHandle.center:
266            if (_startDragLocalOffset != null) {
267              final newTopLeft = newPoint - _startDragLocalOffset!;
268              newRect = Rect.fromLTWH(
269                newTopLeft.dx,
270                newTopLeft.dy,
271                newRect.width,
272                newRect.height,
273              );
274            }
275            break;
276          case DragHandle.none:
277            return;
278        }
279
280        final imageSize = _imageRenderSizes[_currentImageIndex];
281        if (imageSize != null) {
282          final cl = newRect.left.clamp(0.0, imageSize.width);
283          final ct = newRect.top.clamp(0.0, imageSize.height);
284          final cr = newRect.right.clamp(0.0, imageSize.width);
285          final cb = newRect.bottom.clamp(0.0, imageSize.height);
286          newRect = Rect.fromLTRB(cl, ct, cr, cb).normalize();
287        } else {
288          newRect = newRect.normalize();
289        }
290
291        if (newRect.width > 10 && newRect.height > 10)
292          _finalSelectionRect = newRect;
293      });
294    }
295  }
296
297  void _onPanEnd(DragEndDetails details) {
298    if (_isDrawMode && _startPos != null && _currentPos != null) {
299      final rect = Rect.fromPoints(_startPos!, _currentPos!).normalize();
300      if (rect.width < 10 || rect.height < 10) {
301        _resetSelectionMode();
302        return;
303      }
304      setState(() {
305        _isDrawMode = false;
306        _isResizing = true;
307        _finalSelectionRect = rect;
308        _startPos = null;
309        _currentPos = null;
310      });
311    } else if (_isResizing) {
312      setState(() {
313        _activeHandle = DragHandle.none;
314        _startDragLocalOffset = null;
315      });
316    }
317  }
318
319  // Modal
320  void _showNoteModal() {
321    String initialCategory = _availableTags.firstOrNull ?? 'General';
322    showModalBottomSheet(
323      context: context,
324      isScrollControlled: true,
325      backgroundColor: Colors.transparent,
326      builder:
327          (context) => NoteModalOverlay(
328            screenSize: MediaQuery.of(context).size,
329            modalContent: NoteInputSheet(
330              categories: _availableTags,
331              initialCategory: initialCategory,
332              onSubmit: (content, category) {
333                final imageSize = _imageRenderSizes[_currentImageIndex];
334                if (_finalSelectionRect != null && imageSize != null) {
335                  final nX = _finalSelectionRect!.center.dx / imageSize.width;
336                  final nY = _finalSelectionRect!.center.dy / imageSize.height;
337                  final nW = _finalSelectionRect!.width / imageSize.width;
338                  final nH = _finalSelectionRect!.height / imageSize.height;
339
340                  final newNote = TempNote(
341                    normX: nX,
342                    normY: nY,
343                    normWidth: nW,
344                    normHeight: nH,
345                    content: content,
346                    category: category,
347                  );
348                  setState(() {
349                    _notesPerImage[_currentImageIndex]?.add(newNote);
350                    _finalSelectionRect = null;
351                  });
352                  Navigator.pop(context);
353                }
354              },
355            ),
356          ),
357    );
358  }
359
360  // Final Save
361  Future<void> _onSaveToMoodboard() async {
362    setState(() => _isSaving = true);
363    try {
364      for (int i = 0; i < widget.imagePaths.length; i++) {
365        String path = widget.imagePaths[i];
366        final file = File(path);
367        if (!file.existsSync()) continue;
368
369        final tags = _tagsPerImage[i] ?? {};
370        final imageId = await _imageService.saveOrUpdateImage(
371          file,
372          widget.projectId,
373          tags: tags.toList(),
374        );
375
376        final notes = _notesPerImage[i] ?? [];
377        for (var note in notes) {
378          await _noteService.addNote(
379            imageId,
380            note.content,
381            note.category,
382            normX: note.normX,
383            normY: note.normY,
384            normWidth: note.normWidth,
385            normHeight: note.normHeight,
386          );
387        }
388      }
389
390      if (mounted) {
391        ScaffoldMessenger.of(context).showSnackBar(
392          const SnackBar(
393            content: Text('All images saved successfully!'),
394            backgroundColor: Colors.green,
395          ),
396        );
397        if (widget.isFromShare)
398          SystemNavigator.pop();
399        else
400          Navigator.pushAndRemoveUntil(
401            context,
402            MaterialPageRoute(
403              builder:
404                  (_) => ProjectBoardPage(
405                    projectId: widget.projectId,
406                    initialShowAlternateView: true,
407                  ),
408            ),
409            (route) => false,
410          );
411      }
412    } catch (e) {
413      if (mounted) {
414        ScaffoldMessenger.of(
415          context,
416        ).showSnackBar(SnackBar(content: Text('Error: $e')));
417        setState(() => _isSaving = false);
418      }
419    }
420  }
421
422  @override
423  Widget build(BuildContext context) {
424    final currentTags = _tagsPerImage[_currentImageIndex] ?? {};
425    final String parentName = widget.parentProjectName?.trim() ?? '';
426    final String titleText =
427        parentName.isNotEmpty
428            ? '$parentName / ${widget.projectName}'
429            : widget.projectName;
430
431    // Determine if user can pan/zoom the image carousel
432    final isPageLocked = _isDrawMode || _isResizing;
433
434    return Scaffold(
435      resizeToAvoidBottomInset: false,
436      backgroundColor: Variables.surfaceBackground,
437      appBar: CommonAppBar(
438        title: titleText,
439        leading: IconButton(
440          icon: const Icon(
441            Icons.arrow_back_ios_new,
442            size: 20,
443            color: Variables.textPrimary,
444          ),
445          onPressed: () => Navigator.pop(context),
446        ),
447        actions: [
448          // Confirm Selection Button (Visible only in resizing mode)
449          if (_isResizing && _finalSelectionRect != null)
450            IconButton(
451              icon: const Icon(Icons.check, color: Color(0xFF7C86FF), size: 24),
452              onPressed: _confirmSelectionAndShowModal,
453            ),
454          // Cancel Selection Button (Visible only in drawing/resizing mode)
455          if (_isDrawMode || _isResizing)
456            IconButton(
457              icon: const Icon(Icons.close, color: Colors.black87, size: 24),
458              onPressed: _resetSelectionMode,
459            ),
460        ],
461      ),
462      body: Column(
463        children: [
464          // Horizontal Image Carousel
465          Expanded(
466            child: Container(
467              width: double.infinity,
468              margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
469              decoration: BoxDecoration(
470                color: Colors.black,
471                borderRadius: BorderRadius.circular(16),
472              ),
473              child: ClipRRect(
474                borderRadius: BorderRadius.circular(16),
475                child: Stack(
476                  fit: StackFit.expand,
477                  children: [
478                    PageView.builder(
479                      controller: _pageController,
480                      // Disable page view scrolling if a selection process is active
481                      physics:
482                          isPageLocked
483                              ? const NeverScrollableScrollPhysics()
484                              : const PageScrollPhysics(),
485                      itemCount: widget.imagePaths.length,
486                      onPageChanged:
487                          (index) => setState(() {
488                            _currentImageIndex = index;
489                            _resetSelectionMode();
490                          }),
491                      itemBuilder: (context, index) {
492                        return LayoutBuilder(
493                          builder: (context, constraints) {
494                            // Determine the gesture handler based on the mode
495                            final onPanStartHandler =
496                                _isDrawMode
497                                    ? _onPanStart
498                                    : (_isResizing &&
499                                            index == _currentImageIndex
500                                        ? _onPanStart
501                                        : null);
502                            final onPanUpdateHandler =
503                                _isDrawMode
504                                    ? _onPanUpdate
505                                    : (_isResizing &&
506                                            index == _currentImageIndex
507                                        ? _onPanUpdate
508                                        : null);
509                            final onPanEndHandler =
510                                _isDrawMode
511                                    ? _onPanEnd
512                                    : (_isResizing &&
513                                            index == _currentImageIndex
514                                        ? _onPanEnd
515                                        : null);
516
517                            return Stack(
518                              fit: StackFit.expand,
519                              children: [
520                                InteractiveViewer(
521                                  // Disable pan/scale if selection or resizing is active
522                                  panEnabled: !isPageLocked,
523                                  scaleEnabled: !isPageLocked,
524                                  child: Center(
525                                    child: GestureDetector(
526                                      onPanStart: onPanStartHandler,
527                                      onPanUpdate: onPanUpdateHandler,
528                                      onPanEnd: onPanEndHandler,
529                                      child: Stack(
530                                        children: [
531                                          Image.file(
532                                            File(widget.imagePaths[index]),
533                                            key: _imageKeys[index],
534                                            fit: BoxFit.contain,
535                                            width: double.infinity,
536                                          ),
537                                          // Drawing Overlay (if in drawing mode)
538                                          if (_isDrawMode &&
539                                              index == _currentImageIndex &&
540                                              _startPos != null &&
541                                              _currentPos != null)
542                                            Positioned.fill(
543                                              child: CustomPaint(
544                                                painter:
545                                                    SelectionOverlayPainter(
546                                                      rect:
547                                                          Rect.fromPoints(
548                                                            _startPos!,
549                                                            _currentPos!,
550                                                          ).normalize(),
551                                                      isResizing: false,
552                                                    ),
553                                              ),
554                                            ),
555                                          // Final Selection Rect (if in resizing mode)
556                                          if (_isResizing &&
557                                              index == _currentImageIndex &&
558                                              _finalSelectionRect != null)
559                                            Positioned.fill(
560                                              child: CustomPaint(
561                                                painter:
562                                                    SelectionOverlayPainter(
563                                                      rect:
564                                                          _finalSelectionRect!,
565                                                      isResizing: true,
566                                                      activeHandle:
567                                                          _activeHandle,
568                                                    ),
569                                              ),
570                                            ),
571                                        ],
572                                      ),
573                                    ),
574                                  ),
575                                ),
576                                // Existing Note Indicators (Dots) - visible only if no selection is active
577                                if (!isPageLocked)
578                                  ...(_notesPerImage[index] ?? []).map((note) {
579                                    return Positioned(
580                                      left:
581                                          (note.normX * constraints.maxWidth) -
582                                          10,
583                                      top:
584                                          (note.normY * constraints.maxHeight) -
585                                          10,
586                                      child: Container(
587                                        width: 20,
588                                        height: 20,
589                                        decoration: BoxDecoration(
590                                          color: Colors.white,
591                                          shape: BoxShape.circle,
592                                          boxShadow: [
593                                            BoxShadow(
594                                              color: Colors.black.withOpacity(
595                                                0.3,
596                                              ),
597                                              blurRadius: 4,
598                                              offset: const Offset(0, 1),
599                                            ),
600                                          ],
601                                          border: Border.all(
602                                            color: const Color(0xFF7C4DFF),
603                                            width: 2,
604                                          ),
605                                        ),
606                                      ),
607                                    );
608                                  }),
609                                // Page Dots
610                                if (widget.imagePaths.length > 1 &&
611                                    !isPageLocked)
612                                  Positioned(
613                                    bottom: 12,
614                                    left: 0,
615                                    right: 0,
616                                    child: Row(
617                                      mainAxisAlignment:
618                                          MainAxisAlignment.center,
619                                      children: List.generate(
620                                        widget.imagePaths.length,
621                                        (i) => Container(
622                                          margin: const EdgeInsets.symmetric(
623                                            horizontal: 4,
624                                          ),
625                                          width: 8,
626                                          height: 8,
627                                          decoration: BoxDecoration(
628                                            shape: BoxShape.circle,
629                                            color:
630                                                _currentImageIndex == i
631                                                    ? Colors.blue
632                                                    : Colors.white.withOpacity(
633                                                      0.5,
634                                                    ),
635                                          ),
636                                        ),
637                                      ),
638                                    ),
639                                  ),
640                                // Notes Button (Visible only when not drawing/resizing)
641                                if (!isPageLocked)
642                                  Positioned(
643                                    bottom: 24,
644                                    right: 12,
645                                    child: ElevatedButton.icon(
646                                      onPressed: _activateSelectionMode,
647                                      style: ElevatedButton.styleFrom(
648                                        backgroundColor: Colors.white,
649                                        foregroundColor: Colors.black,
650                                        padding: const EdgeInsets.symmetric(
651                                          horizontal: 16,
652                                          vertical: 12,
653                                        ),
654                                        shape: RoundedRectangleBorder(
655                                          borderRadius: BorderRadius.circular(
656                                            12,
657                                          ),
658                                        ),
659                                      ),
660                                      icon: const Icon(
661                                        Icons.assignment_outlined,
662                                        size: 18,
663                                      ),
664                                      label: const Text(
665                                        "Notes",
666                                        style: TextStyle(
667                                          fontFamily: 'GeneralSans',
668                                          fontSize: 14,
669                                          fontWeight: FontWeight.w500,
670                                        ),
671                                      ),
672                                    ),
673                                  ),
674                                // Instruction overlay (for initial drawing)
675                                if (_isDrawMode && _startPos == null)
676                                  Positioned(
677                                    top: 20,
678                                    left: 0,
679                                    right: 0,
680                                    child: Center(
681                                      child: Container(
682                                        padding: const EdgeInsets.symmetric(
683                                          horizontal: 16,
684                                          vertical: 8,
685                                        ),
686                                        decoration: BoxDecoration(
687                                          color: Colors.black87,
688                                          borderRadius: BorderRadius.circular(
689                                            20,
690                                          ),
691                                        ),
692                                        child: const Text(
693                                          "Drag on image to select area",
694                                          style: TextStyle(
695                                            color: Colors.white,
696                                            fontSize: 12,
697                                          ),
698                                        ),
699                                      ),
700                                    ),
701                                  ),
702                                // Instruction overlay (for resizing)
703                                if (_isResizing &&
704                                    _finalSelectionRect != null &&
705                                    _activeHandle == DragHandle.none)
706                                  Positioned(
707                                    top: 20,
708                                    left: 0,
709                                    right: 0,
710                                    child: Center(
711                                      child: Container(
712                                        padding: const EdgeInsets.symmetric(
713                                          horizontal: 16,
714                                          vertical: 8,
715                                        ),
716                                        decoration: BoxDecoration(
717                                          color: Colors.black87,
718                                          borderRadius: BorderRadius.circular(
719                                            20,
720                                          ),
721                                        ),
722                                        child: const Text(
723                                          "Adjust area or tap checkmark to confirm",
724                                          style: TextStyle(
725                                            color: Colors.white,
726                                            fontSize: 12,
727                                          ),
728                                        ),
729                                      ),
730                                    ),
731                                  ),
732                              ],
733                            );
734                          },
735                        );
736                      },
737                    ),
738                  ],
739                ),
740              ),
741            ),
742          ),
743          // Bottom Form (Tags & Save)
744          Padding(
745            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
746            child: Column(
747              crossAxisAlignment: CrossAxisAlignment.start,
748              children: [
749                // Tags box
750                Container(
751                  width: double.infinity,
752                  decoration: BoxDecoration(
753                    color: const Color(0xFFFEFEFE),
754                    border: Border.all(color: Variables.borderSubtle),
755                    borderRadius: BorderRadius.circular(16),
756                  ),
757                  child: Column(
758                    crossAxisAlignment: CrossAxisAlignment.start,
759                    children: [
760                      // Header section
761                      Padding(
762                        padding: const EdgeInsets.all(16),
763                        child: Row(
764                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
765                          children: [
766                            const Text(
767                              'What did you like about this image?',
768                              style: TextStyle(
769                                fontFamily: 'GeneralSans',
770                                fontSize: 14,
771                                fontWeight: FontWeight.w500,
772                              ),
773                            ),
774                            if (widget.imagePaths.length > 1)
775                              Text(
776                                "Image ${_currentImageIndex + 1}/${widget.imagePaths.length}",
777                                style: Variables.captionStyle,
778                              ),
779                          ],
780                        ),
781                      ),
782                      const Divider(
783                        height: 0,
784                        thickness: 1,
785                        color: Color(0xFFE4E4E7),
786                      ),
787                      // Content section with tags
788                      Padding(
789                        padding: const EdgeInsets.symmetric(
790                          horizontal: 16,
791                          vertical: 12,
792                        ),
793                        child: Wrap(
794                          spacing: 8,
795                          runSpacing: 8,
796                          children:
797                              _availableTags.map((tag) {
798                                final isSelected = currentTags.contains(tag);
799                                return GestureDetector(
800                                  onTap: () => _toggleTag(tag),
801                                  child: Container(
802                                    padding: const EdgeInsets.symmetric(
803                                      horizontal: 14,
804                                      vertical: 8,
805                                    ),
806                                    decoration: BoxDecoration(
807                                      color:
808                                          isSelected
809                                              ? const Color(0xFFE0E7FF)
810                                              : Colors.white,
811                                      borderRadius: BorderRadius.circular(20),
812                                      border: Border.all(
813                                        color:
814                                            isSelected
815                                                ? const Color(0xFF7C86FF)
816                                                : Colors.grey[300]!,
817                                      ),
818                                    ),
819                                    child: Text(
820                                      tag,
821                                      style: TextStyle(
822                                        fontFamily: 'GeneralSans',
823                                        fontSize: 14,
824                                        color:
825                                            isSelected
826                                                ? const Color(0xFF27272A)
827                                                : Colors.black87,
828                                      ),
829                                    ),
830                                  ),
831                                );
832                              }).toList(),
833                        ),
834                      ),
835                    ],
836                  ),
837                ),
838                const SizedBox(height: 24),
839                PrimaryButton(
840                  text:
841                      'Save ${widget.imagePaths.length > 1 ? "All" : ""} to Moodboard',
842                  isLoading: _isSaving,
843                  onPressed: _onSaveToMoodboard,
844                ),
845              ],
846            ),
847          ),
848        ],
849      ),
850    );
851  }
852}