creek

The AI Image Editor of 2030

lib/ui/pages/canvas_page.dart (62124B)


   1import 'dart:io';
   2import 'dart:convert';
   3import 'dart:async';
   4import 'dart:math' as math;
   5import 'dart:ui' as ui;
   6import 'dart:typed_data';
   7import 'package:flutter/material.dart';
   8import 'package:flutter/foundation.dart';
   9import 'package:flutter/rendering.dart';
  10import 'package:path_provider/path_provider.dart';
  11import 'package:flutter_svg/flutter_svg.dart';
  12import 'package:image_picker/image_picker.dart';
  13import 'package:undo/undo.dart';
  14import 'package:share_plus/share_plus.dart';
  15import 'package:image/image.dart' as img;
  16
  17import 'package:creekui/data/repos/project_repo.dart';
  18import 'package:creekui/data/models/file_model.dart';
  19import 'package:creekui/services/stylesheet_service.dart';
  20import 'package:creekui/services/file_service.dart';
  21import 'package:creekui/services/flask_service.dart';
  22import 'package:creekui/ui/styles/variables.dart';
  23import 'package:creekui/data/models/canvas_models.dart';
  24import 'package:creekui/ui/painters/canvas_painter.dart';
  25import 'package:creekui/utils/image_utils.dart';
  26
  27import 'package:creekui/ui/widgets/canvas/manipulating_box.dart';
  28import 'package:creekui/ui/widgets/canvas/canvas_bottom_bar.dart';
  29import 'package:creekui/ui/widgets/canvas/asset_picker_sheet.dart';
  30import 'package:creekui/ui/widgets/canvas/layers_panel.dart';
  31import 'package:creekui/ui/widgets/app_bar.dart';
  32import 'package:creekui/ui/widgets/dialog.dart';
  33import 'package:creekui/ui/widgets/text_field.dart';
  34import './canvas_toolbar/magic_draw_overlay.dart';
  35import './canvas_toolbar/text_tools_overlay.dart';
  36import 'project_file_page.dart';
  37
  38class CanvasPage extends StatefulWidget {
  39  final int projectId;
  40  final double width;
  41  final double height;
  42  final File? initialImage;
  43  final FileModel? existingFile;
  44  final File? injectedMedia;
  45
  46  const CanvasPage({
  47    super.key,
  48    required this.projectId,
  49    required this.width,
  50    required this.height,
  51    this.initialImage,
  52    this.existingFile,
  53    this.injectedMedia,
  54  });
  55
  56  @override
  57  State<CanvasPage> createState() => _CanvasPageState();
  58}
  59
  60class _CanvasPageState extends State<CanvasPage> {
  61  final FileService _fileService = FileService();
  62  final ChangeStack _changeStack = ChangeStack();
  63  final ImagePicker _picker = ImagePicker();
  64
  65  // Stack for magic draw actions
  66  final ChangeStack _magicDrawChangeStack = ChangeStack();
  67
  68  bool _hasUnsavedChanges = false;
  69  bool _canPop = false;
  70  String? _currentFileId;
  71
  72  // Layer state
  73  // Bottom-most layer is at index 0
  74  List<CanvasLayer> _layers = [];
  75  String? _activeLayerId;
  76  bool _showLayersPanel = false;
  77
  78  // Magic Draw & Flashing
  79  String? _magicDrawLayerId;
  80  String? _flashingLayerId;
  81  Timer? _flashTimer;
  82
  83  // Snapshots for undo grouping
  84  CanvasState? _gestureStartSnapshot;
  85
  86  // AI & Inpainting State
  87  Timer? _inactivityTimer;
  88  DateTime _lastAnalysisTime = DateTime.now();
  89  final GlobalKey _canvasGlobalKey = GlobalKey();
  90  String? _aiDescription;
  91  bool _isAnalyzing = false;
  92  bool _isDescriptionExpanded = false;
  93  File? _tempBaseImage;
  94  bool _isInpainting = false;
  95  bool _isCapturingBase = false; // Hide strokes during capture
  96
  97  // Background Removal Banner State
  98  bool _showBgRemovalBanner = false;
  99  String? _bgRemovalTargetId;
 100  String? _bgRemovalTargetPath;
 101  Timer? _bgRemovalBannerTimer;
 102  bool _isRemovingBg = false;
 103
 104  late Size _canvasSize;
 105  List<Color> _brandColors = [];
 106
 107  // Tools
 108  bool _isMagicDrawActive = false;
 109  bool _isTextToolsActive = false;
 110  bool _isMagicPanelDisabled = false;
 111  bool _isViewMode = false;
 112
 113  // Text Editing
 114  bool _isEditingText = false;
 115  final TextEditingController _textEditingController = TextEditingController();
 116  final FocusNode _textFocusNode = FocusNode();
 117
 118  // Drawing
 119  List<DrawingPoint> _currentPoints = [];
 120  Color _selectedColor = Variables.defaultBrush;
 121  double _strokeWidth = 10.0;
 122  bool _isEraser = false;
 123  final GlobalKey _drawingKey = GlobalKey();
 124
 125  // Viewport
 126  final TransformationController _transformationController =
 127      TransformationController();
 128  bool _hasInitializedView = false;
 129
 130  @override
 131  void initState() {
 132    super.initState();
 133    _canvasSize = Size(widget.width, widget.height);
 134    _currentFileId = widget.existingFile?.id;
 135
 136    _fetchBrandColors();
 137
 138    if (widget.existingFile != null) {
 139      _loadCanvasFromFile();
 140    } else {
 141      _initializeNewCanvas();
 142    }
 143  }
 144
 145  void _initializeNewCanvas() {
 146    // Add initial image layer if present
 147    if (widget.initialImage != null) {
 148      final double imageWidth = _canvasSize.width * 0.4;
 149      final double imageHeight = _canvasSize.height * 0.4;
 150      final Offset centeredPosition = Offset(
 151        (_canvasSize.width - imageWidth) / 2,
 152        (_canvasSize.height - imageHeight) / 2,
 153      );
 154
 155      final bgId = 'bg_${DateTime.now().millisecondsSinceEpoch}';
 156      _layers.add(
 157        ImageLayer(
 158          id: bgId,
 159          data: {
 160            'id': bgId,
 161            'type': 'file_image',
 162            'content': widget.initialImage!.path,
 163            'position': centeredPosition,
 164            'size': Size(imageWidth, imageHeight),
 165            'rotation': 0.0,
 166          },
 167        ),
 168      );
 169      _activeLayerId = bgId;
 170    }
 171
 172    if (widget.injectedMedia != null) {
 173      _addAssetsToCanvas([widget.injectedMedia!.path]);
 174    }
 175    _hasUnsavedChanges = true;
 176  }
 177
 178  @override
 179  void dispose() {
 180    _inactivityTimer?.cancel();
 181    _bgRemovalBannerTimer?.cancel();
 182    _flashTimer?.cancel();
 183    _textEditingController.dispose();
 184    _textFocusNode.dispose();
 185    _transformationController.dispose();
 186    super.dispose();
 187  }
 188
 189  // Handle inactivity timer logic
 190  void _resetInactivityTimer() {
 191    _inactivityTimer?.cancel();
 192    // Only analyze if changes happened
 193    _inactivityTimer = Timer(const Duration(seconds: 3), () {
 194      if (_hasUnsavedChanges) _analyzeCanvas();
 195    });
 196  }
 197
 198  // LAYER MANAGEMENT
 199  void _handleLayerReorder(int oldUiIndex, int newUiIndex) {
 200    setState(() {
 201      // The UI shows layers Top -> Bottom (reversed order of stack)
 202      List<CanvasLayer> uiList = _layers.reversed.toList();
 203
 204      if (oldUiIndex < newUiIndex) {
 205        newUiIndex -= 1;
 206      }
 207      final CanvasLayer item = uiList.removeAt(oldUiIndex);
 208      uiList.insert(newUiIndex, item);
 209
 210      // Restore stack order (Bottom -> Top)
 211      _layers = uiList.reversed.toList();
 212      _hasUnsavedChanges = true;
 213    });
 214  }
 215
 216  void _handleLayerDelete(String id) {
 217    final oldState = _getCurrentState();
 218    setState(() {
 219      _layers.removeWhere((l) => l.id == id);
 220
 221      if (_activeLayerId == id) {
 222        // If active layer is deleted, select the new top-most layer
 223        _activeLayerId = _layers.isNotEmpty ? _layers.last.id : null;
 224      }
 225
 226      _hasUnsavedChanges = true;
 227    });
 228    _recordChange(oldState);
 229  }
 230
 231  void _handleLayerVisibility(String id) {
 232    final index = _layers.indexWhere((l) => l.id == id);
 233    if (index != -1) {
 234      setState(() {
 235        _layers[index].isVisible = !_layers[index].isVisible;
 236      });
 237    }
 238  }
 239
 240  void _setActiveLayer(String id) {
 241    setState(() {
 242      _activeLayerId = id;
 243      _flashLayer(id); // Visual feedback
 244
 245      final layer = _layers.firstWhere((l) => l.id == id);
 246
 247      // Auto-enable text tools if selecting a text layer
 248      if (layer is ImageLayer && layer.data['type'] == 'text') {
 249        _isTextToolsActive = true;
 250      } else {
 251        _isTextToolsActive = false;
 252        _exitEditMode();
 253      }
 254    });
 255  }
 256
 257  void _flashLayer(String id) {
 258    _flashTimer?.cancel();
 259    setState(() => _flashingLayerId = id);
 260    _flashTimer = Timer(const Duration(milliseconds: 600), () {
 261      if (mounted) setState(() => _flashingLayerId = null);
 262    });
 263  }
 264
 265  // DRAWING LOGIC
 266  void _startMagicDraw() {
 267    // Capture the base canvas (everything currently visible)
 268    _ensureBaseImageCaptured().then((_) {
 269      setState(() {
 270        _isMagicDrawActive = true;
 271        _isTextToolsActive = false;
 272        _exitEditMode();
 273
 274        // Add an initial sketch layer
 275        _addNewMagicLayer();
 276        _magicDrawChangeStack.clear();
 277      });
 278    });
 279  }
 280
 281  void _addNewMagicLayer() {
 282    final id = 'magic_${DateTime.now().millisecondsSinceEpoch}';
 283    final magicLayer = SketchLayer(id: id, isMagicDraw: true, paths: []);
 284    setState(() {
 285      _layers.add(magicLayer);
 286      _activeLayerId = id;
 287      _magicDrawLayerId = id;
 288    });
 289  }
 290
 291  Future<void> _handleMagicDrawExit() async {
 292    // Check if there is any content in any magic draw layer
 293    bool hasDrawing = _layers.any(
 294      (l) => l is SketchLayer && l.isMagicDraw && l.paths.isNotEmpty,
 295    );
 296
 297    if (hasDrawing && !_isInpainting) {
 298      final confirm = await _confirmDiscardMagicDraw();
 299      if (confirm) {
 300        _cleanupMagicDraw();
 301      }
 302    } else {
 303      _cleanupMagicDraw();
 304    }
 305  }
 306
 307  Future<bool> _confirmDiscardMagicDraw() async {
 308    return await ShowDialog.show<bool>(
 309          context,
 310          title: "Discard Magic Draw?",
 311          description:
 312              "Leaving Magic Draw will remove your sketches. Continue?",
 313          primaryButtonText: "Discard",
 314          isDestructive: true,
 315          onPrimaryPressed: () => Navigator.pop(context, true),
 316          secondaryButtonText: "Stay",
 317          onSecondaryPressed: () => Navigator.pop(context, false),
 318        ) ??
 319        false;
 320  }
 321
 322  void _cleanupMagicDraw() {
 323    setState(() {
 324      // Remove all magic draw layers
 325      _layers.removeWhere((l) => l is SketchLayer && l.isMagicDraw);
 326      _isMagicDrawActive = false;
 327      _activeLayerId = _layers.isNotEmpty ? _layers.last.id : null;
 328      _magicDrawChangeStack.clear();
 329      _tempBaseImage = null;
 330    });
 331  }
 332
 333  void _onPanUpdate(DragUpdateDetails details) {
 334    if (!_isMagicDrawActive) return;
 335
 336    setState(() {
 337      _currentPoints.add(
 338        DrawingPoint(offset: details.localPosition, paint: Paint()),
 339      );
 340    });
 341
 342    // Throttle AI analysis
 343    if (DateTime.now().difference(_lastAnalysisTime).inMilliseconds > 2500) {
 344      _lastAnalysisTime = DateTime.now();
 345      _analyzeCanvas();
 346    }
 347  }
 348
 349  Future<void> _onPanEnd(DragEndDetails details) async {
 350    if (!_isMagicDrawActive) return;
 351
 352    setState(() {
 353      if (_currentPoints.isNotEmpty) {
 354        CanvasLayer? targetLayer;
 355
 356        // 1. Try active layer
 357        if (_activeLayerId != null) {
 358          final index = _layers.indexWhere((l) => l.id == _activeLayerId);
 359          if (index != -1 &&
 360              _layers[index] is SketchLayer &&
 361              (_layers[index] as SketchLayer).isMagicDraw) {
 362            targetLayer = _layers[index];
 363          }
 364        }
 365
 366        // 2. If not valid, find any magic layer or create one
 367        if (targetLayer == null) {
 368          try {
 369            targetLayer = _layers.lastWhere(
 370              (l) => l is SketchLayer && (l as SketchLayer).isMagicDraw,
 371            );
 372            _activeLayerId = targetLayer.id;
 373          } catch (_) {
 374            _addNewMagicLayer();
 375            targetLayer = _layers.last;
 376          }
 377        }
 378
 379        if (targetLayer is SketchLayer) {
 380          final oldPaths = List<DrawingPath>.from(targetLayer.paths);
 381          targetLayer.paths.add(
 382            DrawingPath(
 383              points: List.from(_currentPoints),
 384              color: _selectedColor,
 385              strokeWidth: _strokeWidth,
 386              isEraser: _isEraser,
 387            ),
 388          );
 389          _recordMagicChange(oldPaths, targetLayer.id);
 390        }
 391        _currentPoints = [];
 392      }
 393    });
 394  }
 395
 396  // GENERATION & INPAINTING
 397  Future<void> _processInpainting(String prompt, String modelId) async {
 398    FocusScope.of(context).unfocus();
 399    setState(() => _isInpainting = true);
 400    _resetInactivityTimer();
 401
 402    try {
 403      // Check if there are image layers (which dictates the context)
 404      bool hasImageLayers = _layers.any((l) => l is ImageLayer);
 405
 406      if (_tempBaseImage == null) {
 407        _tempBaseImage = await _captureCanvasToFile();
 408      }
 409      if (_tempBaseImage == null) return;
 410
 411      // Consolidate paths from all active magic layers for mask
 412      List<DrawingPath> combinedPaths = [];
 413      for (var layer in _layers) {
 414        if (layer is SketchLayer && layer.isMagicDraw) {
 415          combinedPaths.addAll(layer.paths);
 416        }
 417      }
 418
 419      File? maskFile = await _generateMaskImageFromPaths(
 420        combinedPaths,
 421        _canvasSize,
 422        _tempBaseImage,
 423      );
 424      if (maskFile == null) throw Exception("Failed to generate mask");
 425
 426      String? newImageUrl;
 427
 428      // Case 1: Inpainting
 429      if (hasImageLayers) {
 430        // API Inpainting
 431        if (modelId == 'inpaint_api') {
 432          newImageUrl = await FlaskService().inpaintApiImage(
 433            imagePath: _tempBaseImage!.path,
 434            maskPath: maskFile.path,
 435            prompt: prompt,
 436          );
 437        } else {
 438          // Local inpainting
 439          newImageUrl = await FlaskService().inpaintImage(
 440            imagePath: _tempBaseImage!.path,
 441            maskPath: maskFile.path,
 442            prompt: prompt,
 443          );
 444        }
 445      } else
 446      // Case 2: Sketch to Image
 447      {
 448        if (modelId == 'sketch_fusion') {
 449          newImageUrl = await FlaskService().sketchToImage(
 450            projectId: widget.projectId,
 451            sketchPath: _tempBaseImage!.path,
 452            userPrompt: prompt,
 453            imageDescription: _aiDescription,
 454          );
 455        } else {
 456          newImageUrl = await FlaskService().sketchToImage(
 457            projectId: widget.projectId,
 458            sketchPath: _tempBaseImage!.path,
 459            userPrompt: prompt,
 460            imageDescription: _aiDescription,
 461          );
 462        }
 463      }
 464
 465      if (newImageUrl != null) {
 466        _addGeneratedImage(newImageUrl);
 467        if (!hasImageLayers && _layers.isNotEmpty) {
 468          _triggerBgRemovalBanner(_layers.last.id, newImageUrl);
 469        }
 470      }
 471    } catch (e) {
 472      debugPrint("Gen Error: $e");
 473      ScaffoldMessenger.of(
 474        context,
 475      ).showSnackBar(const SnackBar(content: Text("Generation failed")));
 476    } finally {
 477      setState(() {
 478        _isInpainting = false;
 479        _layers.removeWhere((l) => l is SketchLayer && l.isMagicDraw);
 480        _isMagicDrawActive = false;
 481        _tempBaseImage = null;
 482        // Select newly generated image
 483        if (_layers.isNotEmpty) _activeLayerId = _layers.last.id;
 484      });
 485    }
 486  }
 487
 488  void _addGeneratedImage(String newImageUrl) {
 489    final id = 'gen_${DateTime.now().millisecondsSinceEpoch}';
 490    setState(() {
 491      _layers.add(
 492        ImageLayer(
 493          id: id,
 494          data: {
 495            'id': id,
 496            'type': 'file_image',
 497            'content': newImageUrl,
 498            'position': const Offset(0, 0),
 499            'size': _canvasSize,
 500            'rotation': 0.0,
 501          },
 502        ),
 503      );
 504    });
 505  }
 506
 507  // UI BUILDER
 508  @override
 509  Widget build(BuildContext context) {
 510    Map<String, dynamic>? selectedEl;
 511    if (_activeLayerId != null) {
 512      try {
 513        final layer = _layers.firstWhere((l) => l.id == _activeLayerId);
 514        if (layer is ImageLayer) selectedEl = layer.data;
 515      } catch (_) {}
 516    }
 517
 518    final bool isTextSelected =
 519        selectedEl != null && selectedEl['type'] == 'text';
 520    final bool showTextOverlay = _isTextToolsActive || isTextSelected;
 521
 522    return PopScope(
 523      canPop: _canPop,
 524      onPopInvoked: (didPop) {
 525        if (didPop) return;
 526        _handleBackNavigation();
 527      },
 528      child: Scaffold(
 529        backgroundColor: Variables.canvasBackground,
 530        appBar: _buildAppBar(),
 531        body: LayoutBuilder(
 532          builder: (context, constraints) {
 533            if (!_hasInitializedView) {
 534              _hasInitializedView = true;
 535              final double scaleX =
 536                  (constraints.maxWidth - 40) / _canvasSize.width;
 537              final double scaleY =
 538                  (constraints.maxHeight - 40) / _canvasSize.height;
 539              final double initialScale = math
 540                  .min(scaleX, scaleY)
 541                  .clamp(0.01, 1.0);
 542              final double transX =
 543                  (constraints.maxWidth - (_canvasSize.width * initialScale)) /
 544                  2;
 545              final double transY =
 546                  (constraints.maxHeight -
 547                      (_canvasSize.height * initialScale)) /
 548                  2;
 549
 550              _transformationController.value =
 551                  Matrix4.identity()
 552                    ..translate(transX, transY)
 553                    ..scale(initialScale);
 554            }
 555
 556            return Stack(
 557              children: [
 558                GestureDetector(
 559                  onTap: () {
 560                    // Tapping background clears selection (unless inside magic draw)
 561                    if (!_isMagicDrawActive || _isMagicPanelDisabled) {
 562                      _exitEditMode();
 563                      setState(() => _activeLayerId = null);
 564                    }
 565                  },
 566                  behavior: HitTestBehavior.translucent,
 567                  child: InteractiveViewer(
 568                    transformationController: _transformationController,
 569                    constrained: false,
 570                    boundaryMargin: const EdgeInsets.all(double.infinity),
 571                    minScale: 0.01,
 572                    maxScale: 10.0,
 573                    scaleEnabled: !_isMagicDrawActive || _isMagicPanelDisabled,
 574                    panEnabled: !_isMagicDrawActive || _isMagicPanelDisabled,
 575                    child: RepaintBoundary(
 576                      key: _canvasGlobalKey,
 577                      child: SizedBox(
 578                        width: _canvasSize.width,
 579                        height: _canvasSize.height,
 580                        child: Stack(
 581                          children: [
 582                            Container(
 583                              width: double.infinity,
 584                              height: double.infinity,
 585                              decoration: BoxDecoration(
 586                                color: Variables.background,
 587                                boxShadow: [
 588                                  BoxShadow(
 589                                    color: Colors.black.withOpacity(0.15),
 590                                    blurRadius: 40,
 591                                    offset: const Offset(0, 10),
 592                                  ),
 593                                ],
 594                              ),
 595                            ),
 596
 597                            // Render Layers
 598                            ..._layers.map((layer) {
 599                              if (!layer.isVisible)
 600                                return const SizedBox.shrink();
 601
 602                              bool isFlashing = layer.id == _flashingLayerId;
 603
 604                              if (layer is ImageLayer) {
 605                                final e = layer.data;
 606                                final bool isSelected =
 607                                    _activeLayerId == layer.id;
 608
 609                                return ManipulatingBox(
 610                                  key: ValueKey(layer.id),
 611                                  id: e['id'],
 612                                  position: e['position'],
 613                                  size: e['size'],
 614                                  rotation: e['rotation'],
 615                                  type: e['type'],
 616                                  content: e['content'],
 617                                  styleData: e,
 618                                  isSelected: isSelected && !_isMagicDrawActive,
 619                                  isEditing: isSelected && _isEditingText,
 620                                  isFlashing: isFlashing,
 621                                  viewScale:
 622                                      _transformationController.value
 623                                          .getMaxScaleOnAxis(),
 624                                  transformationController:
 625                                      _transformationController,
 626                                  onTap: () {
 627                                    if (!_isMagicDrawActive)
 628                                      _setActiveLayer(layer.id);
 629                                  },
 630                                  onDoubleTap: () {
 631                                    if (e['type'] == 'text') _enterEditMode(e);
 632                                  },
 633                                  onDragStart: _handleGestureStart,
 634                                  onUpdate:
 635                                      (newPos, newSize, newRot) =>
 636                                          _handleElementUpdate(
 637                                            layer.id,
 638                                            newPos,
 639                                            newSize,
 640                                            newRot,
 641                                          ),
 642                                  onDragEnd: (newPos, newSize, newRot) {
 643                                    _handleElementUpdate(
 644                                      layer.id,
 645                                      newPos,
 646                                      newSize,
 647                                      newRot,
 648                                    );
 649                                    _handleGestureEnd();
 650                                  },
 651                                  textController:
 652                                      isSelected
 653                                          ? _textEditingController
 654                                          : null,
 655                                  focusNode: isSelected ? _textFocusNode : null,
 656                                );
 657                              } else if (layer is SketchLayer) {
 658                                return IgnorePointer(
 659                                  ignoring: true,
 660                                  child: RepaintBoundary(
 661                                    child: CustomPaint(
 662                                      size: Size.infinite,
 663                                      painter: CanvasPainter(
 664                                        paths: layer.paths,
 665                                        currentPoints: [],
 666                                        currentColor: Colors.transparent,
 667                                        currentWidth: 0,
 668                                        isEraser: false,
 669                                        overrideColor:
 670                                            isFlashing
 671                                                ? Colors.greenAccent
 672                                                : null,
 673                                      ),
 674                                    ),
 675                                  ),
 676                                );
 677                              }
 678                              return const SizedBox.shrink();
 679                            }).toList(),
 680
 681                            // Active Drawing Surface
 682                            IgnorePointer(
 683                              ignoring:
 684                                  !(_isMagicDrawActive &&
 685                                      !_isMagicPanelDisabled),
 686                              child: GestureDetector(
 687                                onPanStart: (d) {},
 688                                onPanUpdate: _onPanUpdate,
 689                                onPanEnd: _onPanEnd,
 690                                child: CustomPaint(
 691                                  size: Size.infinite,
 692                                  painter: CanvasPainter(
 693                                    paths: [],
 694                                    currentPoints:
 695                                        _isCapturingBase ? [] : _currentPoints,
 696                                    currentColor:
 697                                        _isEraser
 698                                            ? Colors.transparent
 699                                            : _selectedColor,
 700                                    currentWidth: _strokeWidth,
 701                                    isEraser: _isEraser,
 702                                  ),
 703                                ),
 704                              ),
 705                            ),
 706                          ],
 707                        ),
 708                      ),
 709                    ),
 710                  ),
 711                ),
 712
 713                // AI Description Overlay
 714                if (_aiDescription != null && _isMagicDrawActive)
 715                  Positioned(
 716                    top: 10,
 717                    left: 16,
 718                    right: 16,
 719                    child: GestureDetector(
 720                      onTap: () {
 721                        setState(() {
 722                          _isDescriptionExpanded = !_isDescriptionExpanded;
 723                        });
 724                      },
 725                      child: AnimatedContainer(
 726                        duration: const Duration(milliseconds: 200),
 727                        padding: const EdgeInsets.symmetric(
 728                          horizontal: 16,
 729                          vertical: 10,
 730                        ),
 731                        decoration: BoxDecoration(
 732                          color: Colors.white.withOpacity(0.95),
 733                          borderRadius: BorderRadius.circular(12),
 734                          boxShadow: [
 735                            BoxShadow(
 736                              color: Colors.black.withOpacity(0.1),
 737                              blurRadius: 10,
 738                              offset: const Offset(0, 4),
 739                            ),
 740                          ],
 741                        ),
 742                        child: Row(
 743                          crossAxisAlignment: CrossAxisAlignment.start,
 744                          children: [
 745                            Padding(
 746                              padding: const EdgeInsets.only(top: 2.0),
 747                              child: Icon(
 748                                Icons.auto_awesome,
 749                                size: 20,
 750                                color: Colors.indigo.shade400,
 751                              ),
 752                            ),
 753                            const SizedBox(width: 12),
 754                            Expanded(
 755                              child: Text(
 756                                _aiDescription!,
 757                                style: const TextStyle(
 758                                  fontSize: 14,
 759                                  fontWeight: FontWeight.w500,
 760                                  color: Colors.black87,
 761                                ),
 762                                maxLines: _isDescriptionExpanded ? null : 1,
 763                                overflow:
 764                                    _isDescriptionExpanded
 765                                        ? TextOverflow.visible
 766                                        : TextOverflow.ellipsis,
 767                              ),
 768                            ),
 769                            const SizedBox(width: 8),
 770                            Icon(
 771                              _isDescriptionExpanded
 772                                  ? Icons.keyboard_arrow_up
 773                                  : Icons.keyboard_arrow_down,
 774                              size: 20,
 775                              color: Colors.grey[600],
 776                            ),
 777                          ],
 778                        ),
 779                      ),
 780                    ),
 781                  ),
 782
 783                MagicDrawTools(
 784                  isActive: _isMagicDrawActive,
 785                  selectedColor: _selectedColor,
 786                  strokeWidth: _strokeWidth,
 787                  isEraser: _isEraser,
 788                  brandColors: _brandColors,
 789                  onClose: _handleMagicDrawExit,
 790                  onColorChanged: (c) => setState(() => _selectedColor = c),
 791                  onWidthChanged: (w) => setState(() => _strokeWidth = w),
 792                  onEraserToggle: (e) => setState(() => _isEraser = e),
 793                  onPromptSubmit: _processInpainting,
 794                  isProcessing: _isInpainting,
 795                  onMagicPanelActivityToggle:
 796                      (d) => setState(() => _isMagicPanelDisabled = d),
 797                  isMagicPanelDisabled: _isMagicPanelDisabled,
 798                  onViewModeToggle: (e) => setState(() => _isViewMode = e),
 799                  hasImageLayers: _layers.any((l) => l is ImageLayer),
 800                ),
 801
 802                TextToolsOverlay(
 803                  isActive: showTextOverlay && !_isMagicDrawActive,
 804                  isTextSelected: isTextSelected,
 805                  currentColor: Color(
 806                    (selectedEl?['style_color'] ?? Colors.black.value),
 807                  ),
 808                  currentFontSize:
 809                      (selectedEl?['style_fontSize'] ?? 24.0) as double,
 810                  onClose: () {
 811                    _exitEditMode();
 812                    setState(() => _activeLayerId = null);
 813                  },
 814                  onAddText: _addTextElement,
 815                  onColorChanged:
 816                      (c) =>
 817                          _updateSelectedTextProperty('style_color', c.value),
 818                  onFontSizeChanged:
 819                      (s) => _updateSelectedTextProperty('style_fontSize', s),
 820                ),
 821
 822                // Background Removal Banner
 823                if (_showBgRemovalBanner)
 824                  Positioned(
 825                    top: SafeArea(child: Container()).minimum.top + 10,
 826                    left: 0,
 827                    right: 0,
 828                    child: Center(
 829                      child: Container(
 830                        padding: const EdgeInsets.symmetric(
 831                          horizontal: 16,
 832                          vertical: 8,
 833                        ),
 834                        decoration: BoxDecoration(
 835                          color: Variables.surfaceDark,
 836                          borderRadius: BorderRadius.circular(30),
 837                        ),
 838                        child: Row(
 839                          mainAxisSize: MainAxisSize.min,
 840                          children: [
 841                            if (_isRemovingBg) ...[
 842                              const SizedBox(
 843                                width: 16,
 844                                height: 16,
 845                                child: CircularProgressIndicator(
 846                                  strokeWidth: 2,
 847                                  color: Colors.white,
 848                                ),
 849                              ),
 850                              const SizedBox(width: 8),
 851                              const Text(
 852                                "Removing...",
 853                                style: TextStyle(
 854                                  color: Colors.white,
 855                                  fontSize: 14,
 856                                ),
 857                              ),
 858                            ] else ...[
 859                              const Text(
 860                                "Remove background?",
 861                                style: TextStyle(
 862                                  color: Colors.white,
 863                                  fontSize: 14,
 864                                ),
 865                              ),
 866                              const SizedBox(width: 8),
 867                              InkWell(
 868                                onTap: _confirmRemoveBackground,
 869                                child: const Icon(
 870                                  Icons.check,
 871                                  color: Colors.white,
 872                                ),
 873                              ),
 874                              const SizedBox(width: 8),
 875                              InkWell(
 876                                onTap:
 877                                    () => setState(
 878                                      () => _showBgRemovalBanner = false,
 879                                    ),
 880                                child: const Icon(
 881                                  Icons.close,
 882                                  color: Colors.white54,
 883                                ),
 884                              ),
 885                            ],
 886                          ],
 887                        ),
 888                      ),
 889                    ),
 890                  ),
 891
 892                // Layers Panel
 893                if (!(_isMagicDrawActive && _isDescriptionExpanded))
 894                  Positioned(
 895                    right: 16,
 896                    // Prevent overlap with description overlay
 897                    top:
 898                        SafeArea(child: Container()).minimum.top +
 899                        ((_isMagicDrawActive && _aiDescription != null)
 900                            ? 80
 901                            : 10),
 902                    child: LayersPanel(
 903                      layers: _layers,
 904                      activeLayerId: _activeLayerId,
 905                      isOpen: _showLayersPanel,
 906                      onToggle:
 907                          () => setState(
 908                            () => _showLayersPanel = !_showLayersPanel,
 909                          ),
 910                      onReorder: _handleLayerReorder,
 911                      onDelete: _handleLayerDelete,
 912                      onToggleVisibility: _handleLayerVisibility,
 913                      onLayerTap: _setActiveLayer,
 914                      onAddLayer: _isMagicDrawActive ? _addNewMagicLayer : null,
 915                    ),
 916                  ),
 917
 918                Positioned(
 919                  bottom: 0,
 920                  left: 0,
 921                  right: 0,
 922                  child: CanvasBottomBar(
 923                    activeItem:
 924                        _isMagicDrawActive
 925                            ? "Magic Draw"
 926                            : (_isTextToolsActive ? "Text" : null),
 927                    onMagicDraw: () async {
 928                      if (_isMagicDrawActive) {
 929                        _handleMagicDrawExit();
 930                      } else {
 931                        _startMagicDraw();
 932                      }
 933                    },
 934                    onMedia: () async {
 935                      if (_isMagicDrawActive) {
 936                        if (!await _confirmDiscardMagicDraw()) return;
 937                        _cleanupMagicDraw();
 938                      }
 939                      _pickImageFromGallery();
 940                    },
 941                    onStylesheet: () async {
 942                      if (_isMagicDrawActive) {
 943                        if (!await _confirmDiscardMagicDraw()) return;
 944                        _cleanupMagicDraw();
 945                      }
 946                      _openStylesheet();
 947                    },
 948                    onTools: () async {
 949                      if (_isMagicDrawActive) {
 950                        if (!await _confirmDiscardMagicDraw()) return;
 951                        _cleanupMagicDraw();
 952                      }
 953                      _showComingSoon('Tools');
 954                    },
 955                    onText: () async {
 956                      if (_isMagicDrawActive) {
 957                        if (!await _confirmDiscardMagicDraw()) return;
 958                        _cleanupMagicDraw();
 959                      }
 960                      _toggleTextTools();
 961                    },
 962                    onSelect: () async {
 963                      if (_isMagicDrawActive) {
 964                        if (!await _confirmDiscardMagicDraw()) return;
 965                        _cleanupMagicDraw();
 966                      }
 967                      _showComingSoon('Select');
 968                    },
 969                    onPlugins: () async {
 970                      if (_isMagicDrawActive) {
 971                        if (!await _confirmDiscardMagicDraw()) return;
 972                        _cleanupMagicDraw();
 973                      }
 974                      _showComingSoon('Plugins');
 975                    },
 976                  ),
 977                ),
 978              ],
 979            );
 980          },
 981        ),
 982      ),
 983    );
 984  }
 985
 986  // Helpers
 987  Future<void> _ensureBaseImageCaptured() async {
 988    if (_tempBaseImage != null) return;
 989    setState(() => _isCapturingBase = true);
 990    await Future.delayed(const Duration(milliseconds: 50));
 991    try {
 992      _tempBaseImage = await _captureCanvasToFile();
 993    } catch (e) {
 994      debugPrint("Failed to capture base: $e");
 995    } finally {
 996      if (mounted) setState(() => _isCapturingBase = false);
 997    }
 998  }
 999
1000  Future<File?> _captureCanvasToFile() async {
1001    try {
1002      RenderRepaintBoundary? boundary =
1003          _canvasGlobalKey.currentContext?.findRenderObject()
1004              as RenderRepaintBoundary?;
1005      if (boundary == null) return null;
1006      ui.Image image = await boundary.toImage(pixelRatio: 1.0);
1007      ByteData? byteData = await image.toByteData(
1008        format: ui.ImageByteFormat.png,
1009      );
1010      if (byteData == null) return null;
1011      final tempDir = await getTemporaryDirectory();
1012      final tempFile = File(
1013        '${tempDir.path}/canvas_capture_${DateTime.now().millisecondsSinceEpoch}.png',
1014      );
1015      await tempFile.writeAsBytes(byteData.buffer.asUint8List());
1016      return tempFile;
1017    } catch (e) {
1018      return null;
1019    }
1020  }
1021
1022  Future<void> _analyzeCanvas() async {
1023    if (_isAnalyzing || _isInpainting) return;
1024    setState(() => _isAnalyzing = true);
1025    try {
1026      File? imageFile = await _captureCanvasToFile();
1027      if (imageFile == null) return;
1028      final description = await FlaskService().describeImage(
1029        imagePath: imageFile.path,
1030      );
1031      if (description != null && mounted) {
1032        setState(() {
1033          _aiDescription = description;
1034          _isDescriptionExpanded = false;
1035        });
1036      }
1037    } catch (e) {
1038      debugPrint("AI Error: $e");
1039    } finally {
1040      if (mounted) setState(() => _isAnalyzing = false);
1041    }
1042  }
1043
1044  Future<void> _fetchBrandColors() async {
1045    try {
1046      final int? pId = widget.projectId;
1047      if (pId == null) return;
1048      final project = await ProjectRepo().getProjectById(pId);
1049      if (project != null && project.globalStylesheet != null) {
1050        final styleData = StylesheetService().parse(project.globalStylesheet);
1051        if (mounted && styleData.colors.isNotEmpty)
1052          setState(() => _brandColors = styleData.colors);
1053      }
1054    } catch (e) {
1055      debugPrint("Error loading brand colors: $e");
1056    }
1057  }
1058
1059  void _recordMagicChange(List<DrawingPath> oldPaths, String layerId) {
1060    final newPaths = List<DrawingPath>.from(
1061      (_layers.firstWhere((l) => l.id == layerId) as SketchLayer).paths,
1062    );
1063    _magicDrawChangeStack.add(
1064      Change(
1065        oldPaths,
1066        () {
1067          // Redo
1068          final index = _layers.indexWhere((l) => l.id == layerId);
1069          if (index != -1) {
1070            setState(
1071              () =>
1072                  _layers[index] = SketchLayer(
1073                    id: _layers[index].id,
1074                    isMagicDraw: true,
1075                    paths: newPaths,
1076                    isVisible: _layers[index].isVisible,
1077                  ),
1078            );
1079          }
1080        },
1081        (val) {
1082          // Undo
1083          final index = _layers.indexWhere((l) => l.id == layerId);
1084          if (index != -1) {
1085            setState(
1086              () =>
1087                  _layers[index] = SketchLayer(
1088                    id: _layers[index].id,
1089                    isMagicDraw: true,
1090                    paths: List.from(val),
1091                    isVisible: _layers[index].isVisible,
1092                  ),
1093            );
1094          }
1095        },
1096      ),
1097    );
1098  }
1099
1100  void _recordChange(CanvasState oldState) {
1101    _resetInactivityTimer();
1102    final newState = CanvasState(_deepCopyLayers(_layers));
1103    _changeStack.add(
1104      Change(
1105        oldState,
1106        () => setState(() {
1107          _layers = _deepCopyLayers(newState.layers);
1108          _hasUnsavedChanges = true;
1109        }),
1110        (val) => setState(() {
1111          _layers = _deepCopyLayers(val.layers);
1112          _hasUnsavedChanges = true;
1113        }),
1114      ),
1115    );
1116    setState(() => _hasUnsavedChanges = true);
1117  }
1118
1119  CanvasState _getCurrentState() => CanvasState(_deepCopyLayers(_layers));
1120
1121  List<CanvasLayer> _deepCopyLayers(List<CanvasLayer> layers) {
1122    return layers.map((l) {
1123      if (l is SketchLayer) {
1124        return SketchLayer(
1125          id: l.id,
1126          paths: List<DrawingPath>.from(l.paths),
1127          isMagicDraw: l.isMagicDraw,
1128          isVisible: l.isVisible,
1129        );
1130      } else if (l is ImageLayer) {
1131        return ImageLayer(
1132          id: l.id,
1133          data: Map<String, dynamic>.from(l.data),
1134          isVisible: l.isVisible,
1135        );
1136      }
1137      return l;
1138    }).toList();
1139  }
1140
1141  void _toggleTextTools() {
1142    setState(() {
1143      _isTextToolsActive = !_isTextToolsActive;
1144      _isMagicDrawActive = false;
1145      if (!_isTextToolsActive) {
1146        _exitEditMode();
1147        _activeLayerId = null;
1148      }
1149    });
1150  }
1151
1152  void _addTextElement() {
1153    final oldState = _getCurrentState();
1154    final id = 'text_${DateTime.now().millisecondsSinceEpoch}';
1155    final initialPos = Offset(
1156      _canvasSize.width / 2 - 150,
1157      _canvasSize.height / 2 - 50,
1158    );
1159    final newElementData = {
1160      'id': id,
1161      'type': 'text',
1162      'content': 'Double tap to edit',
1163      'position': initialPos,
1164      'size': Size(300, 60),
1165      'rotation': 0.0,
1166      'style_color': Colors.black.value,
1167      'style_fontSize': 30.0,
1168    };
1169    setState(() {
1170      _layers.add(ImageLayer(id: id, data: newElementData));
1171      _activeLayerId = id;
1172      _isTextToolsActive = true;
1173    });
1174    _recordChange(oldState);
1175    _enterEditMode(newElementData);
1176  }
1177
1178  void _enterEditMode(Map<String, dynamic> element) {
1179    setState(() {
1180      _activeLayerId = element['id'];
1181      _isEditingText = true;
1182      _textEditingController.text = element['content'];
1183      _textEditingController.selection = TextSelection.fromPosition(
1184        TextPosition(offset: _textEditingController.text.length),
1185      );
1186    });
1187    WidgetsBinding.instance.addPostFrameCallback((_) {
1188      if (_textFocusNode.canRequestFocus) _textFocusNode.requestFocus();
1189    });
1190  }
1191
1192  void _exitEditMode() {
1193    if (_isEditingText && _activeLayerId != null) {
1194      final index = _layers.indexWhere((l) => l.id == _activeLayerId);
1195      if (index != -1 && _layers[index] is ImageLayer) {
1196        final layer = _layers[index] as ImageLayer;
1197        if (layer.data['content'] != _textEditingController.text) {
1198          final oldState = _getCurrentState();
1199          setState(() {
1200            layer.data['content'] = _textEditingController.text;
1201            _isEditingText = false;
1202          });
1203          _recordChange(oldState);
1204        } else {
1205          setState(() => _isEditingText = false);
1206        }
1207      } else {
1208        setState(() => _isEditingText = false);
1209      }
1210      _textFocusNode.unfocus();
1211    }
1212  }
1213
1214  void _updateSelectedTextProperty(String key, dynamic value) {
1215    if (_activeLayerId == null) return;
1216    final index = _layers.indexWhere((l) => l.id == _activeLayerId);
1217    if (index != -1 && _layers[index] is ImageLayer) {
1218      final oldState = _getCurrentState();
1219      setState(() => (_layers[index] as ImageLayer).data[key] = value);
1220      _recordChange(oldState);
1221    }
1222  }
1223
1224  void _deleteSelectedElement() {
1225    if (_activeLayerId != null) {
1226      _handleLayerDelete(_activeLayerId!);
1227      setState(() {
1228        _isEditingText = false;
1229        _isTextToolsActive = false;
1230      });
1231      _textFocusNode.unfocus();
1232    }
1233  }
1234
1235  void _handleElementUpdate(
1236    String id,
1237    Offset newPos,
1238    Size newSize,
1239    double newRot,
1240  ) {
1241    final index = _layers.indexWhere((l) => l.id == id);
1242    if (index != -1 && _layers[index] is ImageLayer) {
1243      setState(() {
1244        (_layers[index] as ImageLayer).data['position'] = newPos;
1245        (_layers[index] as ImageLayer).data['size'] = newSize;
1246        (_layers[index] as ImageLayer).data['rotation'] = newRot;
1247        _hasUnsavedChanges = true;
1248      });
1249    }
1250  }
1251
1252  void _handleGestureEnd() {
1253    if (_gestureStartSnapshot != null) {
1254      _recordChange(_gestureStartSnapshot!);
1255      _gestureStartSnapshot = null;
1256    }
1257  }
1258
1259  void _handleGestureStart() {
1260    if (_isMagicDrawActive) return;
1261    _gestureStartSnapshot = _getCurrentState();
1262  }
1263
1264  void _triggerBgRemovalBanner(String elementId, String imagePath) {
1265    _bgRemovalBannerTimer?.cancel();
1266    setState(() {
1267      _showBgRemovalBanner = true;
1268      _bgRemovalTargetId = elementId;
1269      _bgRemovalTargetPath = imagePath;
1270    });
1271
1272    // Automatically hide after 10 seconds
1273    _bgRemovalBannerTimer = Timer(const Duration(seconds: 10), () {
1274      if (mounted) setState(() => _showBgRemovalBanner = false);
1275    });
1276  }
1277
1278  Future<void> _confirmRemoveBackground() async {
1279    if (_bgRemovalTargetId == null || _bgRemovalTargetPath == null) return;
1280    _bgRemovalBannerTimer?.cancel();
1281    setState(() => _isRemovingBg = true);
1282    try {
1283      final newPath = await FlaskService().generateAsset(
1284        imagePath: _bgRemovalTargetPath!,
1285      );
1286      if (newPath != null && mounted) {
1287        final index = _layers.indexWhere((e) => e.id == _bgRemovalTargetId);
1288        if (index != -1 && _layers[index] is ImageLayer) {
1289          final oldState = _getCurrentState();
1290          setState(() {
1291            (_layers[index] as ImageLayer).data['content'] = newPath;
1292            _showBgRemovalBanner = false;
1293            _isRemovingBg = false;
1294            _hasUnsavedChanges = true;
1295          });
1296          _recordChange(oldState);
1297        }
1298      }
1299    } catch (e) {
1300      debugPrint("BG Removal Failed: $e");
1301    } finally {
1302      if (mounted)
1303        setState(() {
1304          _isRemovingBg = false;
1305          _showBgRemovalBanner = false;
1306        });
1307    }
1308  }
1309
1310  // Generate mask: Base Image + Drawing Strokes
1311  Future<File?> _generateMaskImageFromPaths(
1312    List<DrawingPath> paths,
1313    Size size,
1314    File? baseImageFile,
1315  ) async {
1316    try {
1317      final recorder = ui.PictureRecorder();
1318      final canvas = Canvas(
1319        recorder,
1320        Rect.fromLTWH(0, 0, size.width, size.height),
1321      );
1322
1323      // 1. Draw Base Image First (if available)
1324      if (baseImageFile != null) {
1325        final data = await baseImageFile.readAsBytes();
1326        final codec = await ui.instantiateImageCodec(data);
1327        final frameInfo = await codec.getNextFrame();
1328        paintImage(
1329          canvas: canvas,
1330          rect: Rect.fromLTWH(0, 0, size.width, size.height),
1331          image: frameInfo.image,
1332          fit: BoxFit.cover,
1333        );
1334      } else {
1335        // Fallback to black if no base image (shouldn't happen based on logic)
1336        canvas.drawRect(
1337          Rect.fromLTWH(0, 0, size.width, size.height),
1338          Paint()..color = Colors.black,
1339        );
1340      }
1341
1342      // 2. Draw Strokes on top of base image
1343      for (final path in paths) {
1344        final paint =
1345            Paint()
1346              ..color = path.color
1347              ..strokeWidth = path.strokeWidth
1348              ..strokeCap = StrokeCap.round
1349              ..strokeJoin = StrokeJoin.round
1350              ..style = PaintingStyle.stroke;
1351        if (path.points.length > 1) {
1352          final Path p = Path();
1353          p.moveTo(path.points.first.offset.dx, path.points.first.offset.dy);
1354          for (int i = 1; i < path.points.length; i++)
1355            p.lineTo(path.points[i].offset.dx, path.points[i].offset.dy);
1356          canvas.drawPath(p, paint);
1357        } else if (path.points.isNotEmpty) {
1358          canvas.drawPoints(ui.PointMode.points, [
1359            path.points.first.offset,
1360          ], paint);
1361        }
1362      }
1363      final picture = recorder.endRecording();
1364      final img = await picture.toImage(
1365        size.width.toInt(),
1366        size.height.toInt(),
1367      );
1368      final byteData = await img.toByteData(format: ui.ImageByteFormat.png);
1369      if (byteData == null) return null;
1370      final tempDir = await getTemporaryDirectory();
1371      final file = File(
1372        '${tempDir.path}/mask_${DateTime.now().millisecondsSinceEpoch}.png',
1373      );
1374      await file.writeAsBytes(byteData.buffer.asUint8List());
1375      return file;
1376    } catch (e) {
1377      return null;
1378    }
1379  }
1380
1381  void _showComingSoon(String feature) => ScaffoldMessenger.of(
1382    context,
1383  ).showSnackBar(SnackBar(content: Text('$feature Coming soon')));
1384  void _openSettings() => _showComingSoon('Settings');
1385
1386  void _openStylesheet() {
1387    showModalBottomSheet(
1388      context: context,
1389      isScrollControlled: true,
1390      backgroundColor: Colors.transparent,
1391      builder:
1392          (context) => DraggableScrollableSheet(
1393            initialChildSize: 0.7,
1394            minChildSize: 0.5,
1395            maxChildSize: 0.9,
1396            builder:
1397                (_, controller) => AssetPickerSheet(
1398                  projectId: widget.projectId,
1399                  scrollController: controller,
1400                  onAddAssets: _addAssetsToCanvas,
1401                ),
1402          ),
1403    );
1404  }
1405
1406  void _addAssetsToCanvas(List<String> paths) {
1407    if (paths.isEmpty) return;
1408    final oldState = _getCurrentState();
1409    setState(() {
1410      for (var path in paths) {
1411        final double imageWidth = _canvasSize.width * 0.4;
1412        final double imageHeight = _canvasSize.height * 0.4;
1413        final Offset centeredPosition = Offset(
1414          (_canvasSize.width - imageWidth) / 2,
1415          (_canvasSize.height - imageHeight) / 2,
1416        );
1417        final id =
1418            'asset_${DateTime.now().millisecondsSinceEpoch}_${math.Random().nextInt(1000)}';
1419        _layers.add(
1420          ImageLayer(
1421            id: id,
1422            data: {
1423              'id': id,
1424              'type': 'file_image',
1425              'content': path,
1426              'position': centeredPosition,
1427              'size': Size(imageWidth, imageHeight),
1428              'rotation': 0.0,
1429            },
1430          ),
1431        );
1432        _activeLayerId = id;
1433      }
1434      _hasUnsavedChanges = true;
1435    });
1436    _recordChange(oldState);
1437  }
1438
1439  Future<void> _handleBackNavigation() async {
1440    if (_isMagicDrawActive) {
1441      _handleMagicDrawExit();
1442      return;
1443    }
1444    // If no changes, allow pop
1445    if (!_hasUnsavedChanges && _currentFileId != null) {
1446      setState(() => _canPop = true);
1447      WidgetsBinding.instance.addPostFrameCallback((_) {
1448        if (mounted) Navigator.pop(context);
1449      });
1450      return;
1451    }
1452    ShowDialog.show(
1453      context,
1454      title: "Save Changes?",
1455      description: "Do you want to save your canvas before leaving?",
1456      primaryButtonText: "Save",
1457      onPrimaryPressed: () async {
1458        Navigator.pop(context);
1459        await _saveCanvas();
1460        setState(() => _canPop = true);
1461        WidgetsBinding.instance.addPostFrameCallback((_) {
1462          if (mounted) Navigator.pop(context);
1463        });
1464      },
1465      secondaryButtonText: "Discard",
1466      onSecondaryPressed: () {
1467        Navigator.pop(context);
1468        setState(() => _canPop = true);
1469        WidgetsBinding.instance.addPostFrameCallback((_) {
1470          if (mounted) Navigator.pop(context);
1471        });
1472      },
1473    );
1474  }
1475
1476  Future<String?> _showNameDialog() async {
1477    final nameController = TextEditingController(text: "Untitled Canvas");
1478    return await showDialog<String>(
1479      context: context,
1480      builder:
1481          (ctx) => Dialog(
1482            backgroundColor: Colors.white,
1483            shape: RoundedRectangleBorder(
1484              borderRadius: BorderRadius.circular(Variables.radiusLarge),
1485            ),
1486            child: ShowDialog(
1487              title: "Save Canvas",
1488              content: CommonTextField(
1489                hintText: "Enter a name for your file",
1490                controller: nameController,
1491                autoFocus: true,
1492              ),
1493              primaryButtonText: "Save",
1494              onPrimaryPressed:
1495                  () => Navigator.pop(ctx, nameController.text.trim()),
1496              secondaryButtonText: "Cancel",
1497              onSecondaryPressed: () => Navigator.pop(ctx),
1498            ),
1499          ),
1500    );
1501  }
1502
1503  Future<String?> _generatePreviewImage() async {
1504    try {
1505      final boundary =
1506          _canvasGlobalKey.currentContext?.findRenderObject()
1507              as RenderRepaintBoundary?;
1508      if (boundary == null) return null;
1509      final ui.Image image = await boundary.toImage(pixelRatio: 1.0);
1510      final ByteData? byteData = await image.toByteData(
1511        format: ui.ImageByteFormat.png,
1512      );
1513      if (byteData == null) return null;
1514      final directory = await getApplicationDocumentsDirectory();
1515      final previewDir = Directory('${directory.path}/previews');
1516      if (!await previewDir.exists()) await previewDir.create(recursive: true);
1517      final String filePath =
1518          '${previewDir.path}/preview_${DateTime.now().millisecondsSinceEpoch}.png';
1519      await File(filePath).writeAsBytes(byteData.buffer.asUint8List());
1520      return filePath;
1521    } catch (e) {
1522      return null;
1523    }
1524  }
1525
1526  Future<void> _saveCanvas() async {
1527    try {
1528      String fileName = "Canvas ${DateTime.now().toString().split(' ')[0]}";
1529      // If new file, ask for name
1530      if (_currentFileId == null) {
1531        final userFileName = await _showNameDialog();
1532        if (userFileName == null || userFileName.isEmpty) return;
1533        fileName = userFileName;
1534      }
1535      final String? previewPath = await _generatePreviewImage();
1536      final layersJson =
1537          _layers
1538              .where((l) => !(l is SketchLayer && l.isMagicDraw))
1539              .map((l) => l.toMap())
1540              .toList();
1541      final canvasData = {
1542        'version': 2,
1543        'layers': layersJson,
1544        'width': _canvasSize.width,
1545        'height': _canvasSize.height,
1546        'preview_path': previewPath,
1547      };
1548      // Pass current ID and capture returned ID
1549      final savedId = await _fileService.saveCanvasFile(
1550        projectId: widget.projectId,
1551        canvasData: canvasData,
1552        fileId: _currentFileId,
1553        fileName: _currentFileId == null ? fileName : null,
1554      );
1555      if (mounted) {
1556        setState(() {
1557          _currentFileId = savedId;
1558          _hasUnsavedChanges = false;
1559        });
1560        ScaffoldMessenger.of(context).showSnackBar(
1561          const SnackBar(content: Text("Canvas Saved Successfully")),
1562        );
1563      }
1564    } catch (e) {
1565      debugPrint("Save Error: $e");
1566      if (mounted) {
1567        ScaffoldMessenger.of(context).showSnackBar(
1568          SnackBar(
1569            content: Text("Failed to save: $e"),
1570            backgroundColor: Colors.red,
1571          ),
1572        );
1573      }
1574    }
1575  }
1576
1577  Future<void> _loadCanvasFromFile() async {
1578    try {
1579      final meta = await _fileService.getFileMetadata(
1580        widget.existingFile!.filePath,
1581      );
1582      if (meta.width > 0 && meta.height > 0)
1583        setState(() {
1584          _canvasSize = Size(meta.width, meta.height);
1585          _hasInitializedView = false;
1586        });
1587      final file = File(widget.existingFile!.filePath);
1588      if (await file.exists()) {
1589        final jsonString = await file.readAsString();
1590        final dynamic decoded = jsonDecode(jsonString);
1591        setState(() {
1592          _layers.clear();
1593          if (decoded is Map && decoded.containsKey('layers')) {
1594            for (var item in decoded['layers'])
1595              _layers.add(CanvasLayer.fromMap(item));
1596          } else if (decoded is Map && decoded.containsKey('elements')) {
1597            for (var e in _jsonToElements(decoded['elements']))
1598              _layers.add(ImageLayer(id: e['id'], data: e));
1599            if (decoded['paths'] != null) {
1600              final paths =
1601                  (decoded['paths'] as List)
1602                      .map((p) => DrawingPath.fromMap(p))
1603                      .toList();
1604              if (paths.isNotEmpty)
1605                _layers.add(SketchLayer(id: 'legacy_sketch', paths: paths));
1606            }
1607          }
1608          _hasUnsavedChanges = false;
1609          _activeLayerId = _layers.isNotEmpty ? _layers.last.id : null;
1610        });
1611        if (widget.injectedMedia != null)
1612          _addAssetsToCanvas([widget.injectedMedia!.path]);
1613      }
1614    } catch (e) {
1615      debugPrint("Error loading: $e");
1616      _initializeNewCanvas();
1617    }
1618  }
1619
1620  Future<void> _pickImageFromGallery() async {
1621    final List<XFile> images = await _picker.pickMultiImage();
1622    if (images.isNotEmpty) {
1623      final oldState = _getCurrentState();
1624      setState(() {
1625        for (int i = 0; i < images.length; i++) {
1626          final double imageWidth = _canvasSize.width * 0.4;
1627          final double imageHeight = _canvasSize.height * 0.4;
1628          final Offset centeredPosition = Offset(
1629            (_canvasSize.width - imageWidth) / 2,
1630            (_canvasSize.height - imageHeight) / 2,
1631          );
1632          final id = '${DateTime.now().millisecondsSinceEpoch}_$i';
1633          _layers.add(
1634            ImageLayer(
1635              id: id,
1636              data: {
1637                'id': id,
1638                'type': 'file_image',
1639                'content': images[i].path,
1640                'position': centeredPosition,
1641                'size': Size(imageWidth, imageHeight),
1642                'rotation': 0.0,
1643              },
1644            ),
1645          );
1646          _activeLayerId = id;
1647        }
1648        _hasUnsavedChanges = true;
1649      });
1650      _recordChange(oldState);
1651    }
1652  }
1653
1654  List<Map<String, dynamic>> _jsonToElements(List<dynamic> jsonList) {
1655    return jsonList.map((item) {
1656      final e = Map<String, dynamic>.from(item);
1657      if (e['position'] is Map)
1658        e['position'] = Offset(e['position']['dx'], e['position']['dy']);
1659      if (e['size'] is Map)
1660        e['size'] = Size(e['size']['width'], e['size']['height']);
1661      e['rotation'] = (e['rotation'] as num).toDouble();
1662      return e;
1663    }).toList();
1664  }
1665
1666  Future<void> _exportProject() async {
1667    try {
1668      ScaffoldMessenger.of(
1669        context,
1670      ).showSnackBar(const SnackBar(content: Text("Generating image...")));
1671      final boundary =
1672          _canvasGlobalKey.currentContext?.findRenderObject()
1673              as RenderRepaintBoundary?;
1674      if (boundary == null) return;
1675      final ui.Image image = await boundary.toImage(pixelRatio: 3.0);
1676      final ByteData? byteData = await image.toByteData(
1677        format: ui.ImageByteFormat.png,
1678      );
1679      if (byteData == null) return;
1680      final img.Image? decodedImage = img.decodePng(
1681        byteData.buffer.asUint8List(),
1682      );
1683      if (decodedImage == null) return;
1684      final Uint8List jpgBytes = img.encodeJpg(decodedImage, quality: 90);
1685      final directory = await getTemporaryDirectory();
1686      final String filePath =
1687          '${directory.path}/export_${DateTime.now().millisecondsSinceEpoch}.jpg';
1688      await File(filePath).writeAsBytes(jpgBytes);
1689      await Share.shareXFiles([
1690        XFile(filePath, mimeType: 'image/jpeg'),
1691      ], text: 'Check out my design created with CreekUI!');
1692    } catch (e) {
1693      debugPrint("Export Error: $e");
1694    }
1695  }
1696
1697  PreferredSizeWidget _buildAppBar() {
1698    final bool canUndo =
1699        _isMagicDrawActive
1700            ? _magicDrawChangeStack.canUndo
1701            : _changeStack.canUndo;
1702    final bool canRedo =
1703        _isMagicDrawActive
1704            ? _magicDrawChangeStack.canRedo
1705            : _changeStack.canRedo;
1706
1707    return AppBar(
1708      leadingWidth: 160,
1709      leading: SafeArea(
1710        child: Row(
1711          children: [
1712            IconButton(
1713              icon: SvgPicture.asset(
1714                'assets/icons/arrow-left-s-line.svg',
1715                width: 22,
1716                colorFilter: const ColorFilter.mode(
1717                  Colors.black,
1718                  BlendMode.srcIn,
1719                ),
1720              ),
1721              onPressed: _handleBackNavigation,
1722            ),
1723            IconButton(
1724              icon: SvgPicture.asset(
1725                'assets/icons/arrow-go-back-line.svg',
1726                width: 22,
1727                colorFilter: ColorFilter.mode(
1728                  canUndo ? Colors.black : Colors.grey[400]!,
1729                  BlendMode.srcIn,
1730                ),
1731              ),
1732              onPressed:
1733                  canUndo
1734                      ? () => setState(
1735                        () =>
1736                            _isMagicDrawActive
1737                                ? _magicDrawChangeStack.undo()
1738                                : _changeStack.undo(),
1739                      )
1740                      : null,
1741            ),
1742            IconButton(
1743              icon: SvgPicture.asset(
1744                'assets/icons/arrow-go-forward-line.svg',
1745                width: 22,
1746                colorFilter: ColorFilter.mode(
1747                  canRedo ? Colors.black : Colors.grey[400]!,
1748                  BlendMode.srcIn,
1749                ),
1750              ),
1751              onPressed:
1752                  canRedo
1753                      ? () => setState(
1754                        () =>
1755                            _isMagicDrawActive
1756                                ? _magicDrawChangeStack.redo()
1757                                : _changeStack.redo(),
1758                      )
1759                      : null,
1760            ),
1761          ],
1762        ),
1763      ),
1764      actions: [
1765        SafeArea(
1766          child: Row(
1767            children: [
1768              if (_activeLayerId != null)
1769                IconButton(
1770                  icon: const Icon(Icons.delete_outline, color: Colors.red),
1771                  onPressed: _deleteSelectedElement,
1772                  tooltip: "Delete Selected",
1773                ),
1774              IconButton(
1775                icon: SvgPicture.asset(
1776                  'assets/icons/save-3-line.svg',
1777                  width: 22,
1778                ),
1779                onPressed: _saveCanvas,
1780              ),
1781              IconButton(
1782                icon: SvgPicture.asset(
1783                  'assets/icons/upload-2-line.svg',
1784                  width: 22,
1785                ),
1786                onPressed: _exportProject,
1787              ),
1788              IconButton(
1789                icon: SvgPicture.asset(
1790                  'assets/icons/settings-line.svg',
1791                  width: 22,
1792                ),
1793                onPressed: _openSettings,
1794              ),
1795              const SizedBox(width: 8),
1796            ],
1797          ),
1798        ),
1799      ],
1800    );
1801  }
1802}