From d4d6e41a9d2a4da44a4a7b2d93e7045575a7260d Mon Sep 17 00:00:00 2001 From: Julian Mutter Date: Thu, 5 Feb 2026 17:47:03 +0100 Subject: [PATCH] Custom drawing implementation --- lib/core/services/storage_service.dart | 80 ++++++- .../sheet_viewer/drawing/drawing.dart | 12 + .../sheet_viewer/drawing/drawing_board.dart | 186 +++++++++++++++ .../sheet_viewer/drawing/drawing_canvas.dart | 176 ++++++++++++++ .../drawing/drawing_controller.dart | 189 ++++++++++++++++ .../sheet_viewer/drawing/drawing_line.dart | 93 ++++++++ .../sheet_viewer/drawing/drawing_toolbar.dart | 194 ++++++++++++++++ .../sheet_viewer/drawing/paint_preset.dart | 108 +++++++++ .../sheet_viewer/sheet_viewer_page.dart | 214 +++++++++++++----- .../widgets/paint_mode_layer.dart | 45 ---- .../widgets/pdf_page_display.dart | 126 ++++++++--- .../widgets/touch_navigation_layer.dart | 6 +- pubspec.lock | 8 - pubspec.yaml | 1 - 14 files changed, 1291 insertions(+), 147 deletions(-) create mode 100644 lib/features/sheet_viewer/drawing/drawing.dart create mode 100644 lib/features/sheet_viewer/drawing/drawing_board.dart create mode 100644 lib/features/sheet_viewer/drawing/drawing_canvas.dart create mode 100644 lib/features/sheet_viewer/drawing/drawing_controller.dart create mode 100644 lib/features/sheet_viewer/drawing/drawing_line.dart create mode 100644 lib/features/sheet_viewer/drawing/drawing_toolbar.dart create mode 100644 lib/features/sheet_viewer/drawing/paint_preset.dart delete mode 100644 lib/features/sheet_viewer/widgets/paint_mode_layer.dart diff --git a/lib/core/services/storage_service.dart b/lib/core/services/storage_service.dart index f27f662..ed9b282 100644 --- a/lib/core/services/storage_service.dart +++ b/lib/core/services/storage_service.dart @@ -9,12 +9,14 @@ enum SecureStorageKey { url, jwt, email } /// Service for managing local storage operations. /// /// Uses [FlutterSecureStorage] for sensitive data (credentials, tokens) -/// and [Hive] for general app data (config, sheet access times, change queue). +/// and [Hive] for general app data (config, sheet access times, change queue, +/// and PDF annotations). class StorageService { // Hive box names static const String _sheetAccessTimesBox = 'sheetAccessTimes'; static const String _configBox = 'config'; static const String _changeQueueBox = 'changeQueue'; + static const String _annotationsBox = 'annotations'; late final FlutterSecureStorage _secureStorage; @@ -75,8 +77,9 @@ class StorageService { Future> readSheetAccessTimes() async { final box = await Hive.openBox(_sheetAccessTimesBox); return box.toMap().map( - (key, value) => MapEntry(key as String, DateTime.parse(value as String)), - ); + (key, value) => + MapEntry(key as String, DateTime.parse(value as String)), + ); } /// Records when a sheet was last accessed. @@ -116,4 +119,75 @@ class StorageService { await box.deleteAt(0); } } + + // --------------------------------------------------------------------------- + // Annotations (PDF Drawing Persistence) + // --------------------------------------------------------------------------- + + /// Generates a storage key for a specific page's annotations. + String _annotationKey(String sheetUuid, int pageNumber) { + return '${sheetUuid}_page_$pageNumber'; + } + + /// Reads annotations for a specific sheet page. + /// + /// Returns the JSON string of annotations, or null if none exist. + Future readAnnotations(String sheetUuid, int pageNumber) async { + final box = await Hive.openBox(_annotationsBox); + return box.get(_annotationKey(sheetUuid, pageNumber)); + } + + /// Writes annotations for a specific sheet page. + /// + /// Pass null or empty string to delete annotations for that page. + Future writeAnnotations( + String sheetUuid, + int pageNumber, + String? annotationsJson, + ) async { + final box = await Hive.openBox(_annotationsBox); + final key = _annotationKey(sheetUuid, pageNumber); + + if (annotationsJson == null || annotationsJson.isEmpty) { + await box.delete(key); + } else { + await box.put(key, annotationsJson); + } + } + + /// Reads all annotations for a sheet (all pages). + /// + /// Returns a map of page number to JSON string. + Future> readAllAnnotations(String sheetUuid) async { + final box = await Hive.openBox(_annotationsBox); + final prefix = '${sheetUuid}_page_'; + final result = {}; + + for (final key in box.keys) { + if (key is String && key.startsWith(prefix)) { + final pageStr = key.substring(prefix.length); + final pageNumber = int.tryParse(pageStr); + if (pageNumber != null) { + final value = box.get(key); + if (value != null && value is String && value.isNotEmpty) { + result[pageNumber] = value; + } + } + } + } + + return result; + } + + /// Deletes all annotations for a sheet. + Future deleteAllAnnotations(String sheetUuid) async { + final box = await Hive.openBox(_annotationsBox); + final prefix = '${sheetUuid}_page_'; + final keysToDelete = + box.keys.where((key) => key is String && key.startsWith(prefix)); + + for (final key in keysToDelete.toList()) { + await box.delete(key); + } + } } diff --git a/lib/features/sheet_viewer/drawing/drawing.dart b/lib/features/sheet_viewer/drawing/drawing.dart new file mode 100644 index 0000000..444c041 --- /dev/null +++ b/lib/features/sheet_viewer/drawing/drawing.dart @@ -0,0 +1,12 @@ +/// Custom drawing library for PDF annotations. +/// +/// Provides scalable drawing with normalized coordinates (0-1 range) +/// that work correctly when the canvas size changes. +library; + +export 'drawing_board.dart'; +export 'drawing_canvas.dart'; +export 'drawing_controller.dart'; +export 'drawing_line.dart'; +export 'drawing_toolbar.dart'; +export 'paint_preset.dart'; diff --git a/lib/features/sheet_viewer/drawing/drawing_board.dart b/lib/features/sheet_viewer/drawing/drawing_board.dart new file mode 100644 index 0000000..56a25d8 --- /dev/null +++ b/lib/features/sheet_viewer/drawing/drawing_board.dart @@ -0,0 +1,186 @@ +import 'package:flutter/material.dart'; + +import 'drawing_canvas.dart'; +import 'drawing_controller.dart'; + +/// A drawing board that overlays a child widget with a drawing canvas. +/// +/// Supports: +/// - Drawing with normalized coordinates that scale correctly +/// - Zooming and panning via InteractiveViewer +/// - Toggle between view-only and drawing modes +class DrawingBoard extends StatefulWidget { + /// The widget to display behind the drawing (e.g., PDF page) + final Widget child; + + /// Size of the drawing area (should match child size) + final Size boardSize; + + /// Controller for managing drawing state + final DrawingController controller; + + /// Whether drawing is enabled (false = view only) + final bool drawingEnabled; + + /// Minimum zoom scale + final double minScale; + + /// Maximum zoom scale + final double maxScale; + + /// Alignment of the board within available space + final Alignment alignment; + + const DrawingBoard({ + super.key, + required this.child, + required this.boardSize, + required this.controller, + this.drawingEnabled = true, + this.minScale = 1.0, + this.maxScale = 3.0, + this.alignment = Alignment.topCenter, + }); + + @override + State createState() => _DrawingBoardState(); +} + +class _DrawingBoardState extends State { + final TransformationController _transformationController = + TransformationController(); + + /// Tracks whether we're currently in a drawing gesture + bool _isDrawing = false; + + /// Tracks the number of active pointers for gesture disambiguation + int _pointerCount = 0; + + @override + void dispose() { + _transformationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // The content that will be transformed (zoomed/panned) + final content = SizedBox( + width: widget.boardSize.width, + height: widget.boardSize.height, + child: Stack( + children: [ + // Background child (e.g., PDF) + Positioned.fill(child: widget.child), + + // Drawing overlay + Positioned.fill( + child: widget.drawingEnabled + ? _buildDrawingLayer() + : DrawingOverlay( + controller: widget.controller, + canvasSize: widget.boardSize, + ), + ), + ], + ), + ); + + if (!widget.drawingEnabled) { + // View-only mode: just show the content (no zoom/pan here, + // let parent handle navigation) + return Align( + alignment: widget.alignment, + child: content, + ); + } + + // Drawing mode: wrap with InteractiveViewer for zoom/pan + return Align( + alignment: widget.alignment, + child: InteractiveViewer( + transformationController: _transformationController, + minScale: widget.minScale, + maxScale: widget.maxScale, + boundaryMargin: EdgeInsets.zero, + constrained: true, + panEnabled: !_isDrawing, + scaleEnabled: !_isDrawing, + child: content, + ), + ); + } + + /// Builds the drawing layer with gesture handling. + /// + /// Uses Listener for drawing and distinguishes between: + /// - Single finger: Draw + /// - Two+ fingers: Pan/Zoom (handled by InteractiveViewer) + Widget _buildDrawingLayer() { + return Listener( + onPointerDown: _onPointerDown, + onPointerMove: _onPointerMove, + onPointerUp: _onPointerUp, + onPointerCancel: _onPointerCancel, + behavior: HitTestBehavior.translucent, + child: DrawingOverlay( + controller: widget.controller, + canvasSize: widget.boardSize, + ), + ); + } + + void _onPointerDown(PointerDownEvent event) { + _pointerCount++; + + // Only start drawing with single finger touch + if (_pointerCount == 1) { + _isDrawing = true; + final normalized = _toNormalized(event.localPosition); + widget.controller.startLine(normalized); + setState(() {}); // Update to disable pan/scale + } else { + // Multiple fingers: cancel drawing, enable pan/zoom + if (_isDrawing) { + widget.controller.endLine(); + _isDrawing = false; + setState(() {}); + } + } + } + + void _onPointerMove(PointerMoveEvent event) { + if (_isDrawing && _pointerCount == 1) { + final normalized = _toNormalized(event.localPosition); + widget.controller.addPoint(normalized); + } + } + + void _onPointerUp(PointerUpEvent event) { + _pointerCount = (_pointerCount - 1).clamp(0, 10); + + if (_isDrawing) { + widget.controller.endLine(); + _isDrawing = false; + setState(() {}); // Re-enable pan/scale + } + } + + void _onPointerCancel(PointerCancelEvent event) { + _pointerCount = (_pointerCount - 1).clamp(0, 10); + + if (_isDrawing) { + widget.controller.endLine(); + _isDrawing = false; + setState(() {}); + } + } + + /// Converts local position to normalized coordinates (0-1). + Offset _toNormalized(Offset localPosition) { + return Offset( + (localPosition.dx / widget.boardSize.width).clamp(0.0, 1.0), + (localPosition.dy / widget.boardSize.height).clamp(0.0, 1.0), + ); + } +} diff --git a/lib/features/sheet_viewer/drawing/drawing_canvas.dart b/lib/features/sheet_viewer/drawing/drawing_canvas.dart new file mode 100644 index 0000000..b201517 --- /dev/null +++ b/lib/features/sheet_viewer/drawing/drawing_canvas.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; + +import 'drawing_controller.dart'; +import 'drawing_line.dart'; + +/// Custom painter that renders drawing lines on a canvas. +/// +/// Converts normalized coordinates (0-1) to actual canvas coordinates +/// based on the provided canvas size. +class DrawingPainter extends CustomPainter { + final List lines; + final DrawingLine? currentLine; + final Size canvasSize; + + DrawingPainter({ + required this.lines, + required this.currentLine, + required this.canvasSize, + }); + + @override + void paint(Canvas canvas, Size size) { + // Draw all completed lines + for (final line in lines) { + _drawLine(canvas, line); + } + + // Draw the current line being drawn + if (currentLine != null) { + _drawLine(canvas, currentLine!); + } + } + + void _drawLine(Canvas canvas, DrawingLine line) { + if (line.points.length < 2) return; + + final paint = Paint() + ..color = line.color + ..strokeWidth = line.strokeWidth * canvasSize.width + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round + ..style = PaintingStyle.stroke + ..isAntiAlias = true; + + // Create path from normalized points + final path = Path(); + final firstPoint = _toCanvasPoint(line.points.first); + path.moveTo(firstPoint.dx, firstPoint.dy); + + // Use quadratic bezier curves for smooth lines + if (line.points.length == 2) { + final endPoint = _toCanvasPoint(line.points.last); + path.lineTo(endPoint.dx, endPoint.dy); + } else { + for (int i = 1; i < line.points.length - 1; i++) { + final p0 = _toCanvasPoint(line.points[i]); + final p1 = _toCanvasPoint(line.points[i + 1]); + final midPoint = Offset( + (p0.dx + p1.dx) / 2, + (p0.dy + p1.dy) / 2, + ); + path.quadraticBezierTo(p0.dx, p0.dy, midPoint.dx, midPoint.dy); + } + // Draw to the last point + final lastPoint = _toCanvasPoint(line.points.last); + path.lineTo(lastPoint.dx, lastPoint.dy); + } + + canvas.drawPath(path, paint); + } + + /// Converts a normalized point (0-1) to canvas coordinates. + Offset _toCanvasPoint(Offset normalizedPoint) { + return Offset( + normalizedPoint.dx * canvasSize.width, + normalizedPoint.dy * canvasSize.height, + ); + } + + @override + bool shouldRepaint(covariant DrawingPainter oldDelegate) { + return lines != oldDelegate.lines || + currentLine != oldDelegate.currentLine || + canvasSize != oldDelegate.canvasSize; + } +} + +/// A widget that displays drawing lines on a transparent canvas. +/// +/// This widget only shows the drawings, it doesn't handle input. +/// Use [DrawingCanvas] or [DrawingBoard] for input handling. +class DrawingOverlay extends StatelessWidget { + final DrawingController controller; + final Size canvasSize; + + const DrawingOverlay({ + super.key, + required this.controller, + required this.canvasSize, + }); + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: controller, + builder: (context, _) { + return CustomPaint( + size: canvasSize, + painter: DrawingPainter( + lines: controller.lines, + currentLine: controller.currentLine, + canvasSize: canvasSize, + ), + ); + }, + ); + } +} + +/// A widget that handles drawing input and renders lines. +/// +/// Converts touch/pointer events to normalized coordinates and +/// passes them to the [DrawingController]. +class DrawingCanvas extends StatelessWidget { + final DrawingController controller; + final Size canvasSize; + final bool enabled; + + const DrawingCanvas({ + super.key, + required this.controller, + required this.canvasSize, + this.enabled = true, + }); + + @override + Widget build(BuildContext context) { + return Listener( + onPointerDown: enabled ? _onPointerDown : null, + onPointerMove: enabled ? _onPointerMove : null, + onPointerUp: enabled ? _onPointerUp : null, + onPointerCancel: enabled ? _onPointerCancel : null, + behavior: HitTestBehavior.opaque, + child: DrawingOverlay( + controller: controller, + canvasSize: canvasSize, + ), + ); + } + + void _onPointerDown(PointerDownEvent event) { + final normalized = _toNormalized(event.localPosition); + controller.startLine(normalized); + } + + void _onPointerMove(PointerMoveEvent event) { + final normalized = _toNormalized(event.localPosition); + controller.addPoint(normalized); + } + + void _onPointerUp(PointerUpEvent event) { + controller.endLine(); + } + + void _onPointerCancel(PointerCancelEvent event) { + controller.endLine(); + } + + /// Converts canvas coordinates to normalized coordinates (0-1). + Offset _toNormalized(Offset canvasPoint) { + return Offset( + (canvasPoint.dx / canvasSize.width).clamp(0.0, 1.0), + (canvasPoint.dy / canvasSize.height).clamp(0.0, 1.0), + ); + } +} diff --git a/lib/features/sheet_viewer/drawing/drawing_controller.dart b/lib/features/sheet_viewer/drawing/drawing_controller.dart new file mode 100644 index 0000000..945ba40 --- /dev/null +++ b/lib/features/sheet_viewer/drawing/drawing_controller.dart @@ -0,0 +1,189 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +import 'drawing_line.dart'; +import 'paint_preset.dart'; + +/// Controller for managing drawing state with undo/redo support. +/// +/// Manages a stack of [DrawingLine] objects and provides methods for +/// drawing, undoing, redoing, and serializing the drawing state. +class DrawingController extends ChangeNotifier { + /// All completed lines in the drawing + final List _lines = []; + + /// Lines that have been undone (for redo functionality) + final List _undoneLines = []; + + /// The line currently being drawn (null when not drawing) + DrawingLine? _currentLine; + + /// Current paint preset being used + PaintPreset _currentPreset = PaintPreset.blackPen; + + /// Maximum number of history steps to keep + final int maxHistorySteps; + + DrawingController({this.maxHistorySteps = 50}); + + // --------------------------------------------------------------------------- + // Getters + // --------------------------------------------------------------------------- + + /// All completed lines (read-only) + List get lines => List.unmodifiable(_lines); + + /// The line currently being drawn + DrawingLine? get currentLine => _currentLine; + + /// Current paint preset + PaintPreset get currentPreset => _currentPreset; + + /// Whether undo is available + bool get canUndo => _lines.isNotEmpty; + + /// Whether redo is available + bool get canRedo => _undoneLines.isNotEmpty; + + // --------------------------------------------------------------------------- + // Drawing Operations + // --------------------------------------------------------------------------- + + /// Starts a new line at the given normalized position. + void startLine(Offset normalizedPoint) { + _currentLine = DrawingLine( + points: [normalizedPoint], + color: _currentPreset.color, + strokeWidth: _currentPreset.strokeWidth, + ); + notifyListeners(); + } + + /// Adds a point to the current line. + void addPoint(Offset normalizedPoint) { + if (_currentLine == null) return; + + _currentLine = _currentLine!.addPoint(normalizedPoint); + notifyListeners(); + } + + /// Completes the current line and adds it to the history. + void endLine() { + if (_currentLine == null) return; + + // Only add lines with at least 2 points + if (_currentLine!.points.length >= 2) { + _lines.add(_currentLine!); + // Clear redo stack when new action is performed + _undoneLines.clear(); + _trimHistory(); + } + + _currentLine = null; + notifyListeners(); + } + + /// Trims history to maxHistorySteps to prevent memory growth. + void _trimHistory() { + while (_lines.length > maxHistorySteps) { + _lines.removeAt(0); + } + } + + // --------------------------------------------------------------------------- + // Undo/Redo + // --------------------------------------------------------------------------- + + /// Undoes the last line drawn. + void undo() { + if (!canUndo) return; + + final line = _lines.removeLast(); + _undoneLines.add(line); + notifyListeners(); + } + + /// Redoes the last undone line. + void redo() { + if (!canRedo) return; + + final line = _undoneLines.removeLast(); + _lines.add(line); + notifyListeners(); + } + + /// Clears all lines from the canvas. + void clear() { + _lines.clear(); + _undoneLines.clear(); + _currentLine = null; + notifyListeners(); + } + + // --------------------------------------------------------------------------- + // Paint Preset + // --------------------------------------------------------------------------- + + /// Sets the current paint preset. + void setPreset(PaintPreset preset) { + _currentPreset = preset; + notifyListeners(); + } + + // --------------------------------------------------------------------------- + // Serialization + // --------------------------------------------------------------------------- + + /// Exports all lines to a JSON-serializable list. + List> toJsonList() { + return _lines.map((line) => line.toJson()).toList(); + } + + /// Exports all lines to a JSON string. + String toJsonString() { + return jsonEncode(toJsonList()); + } + + /// Imports lines from a JSON-serializable list. + void fromJsonList(List> jsonList) { + _lines.clear(); + _undoneLines.clear(); + _currentLine = null; + + for (final json in jsonList) { + _lines.add(DrawingLine.fromJson(json)); + } + + notifyListeners(); + } + + /// Imports lines from a JSON string. + void fromJsonString(String jsonString) { + if (jsonString.isEmpty || jsonString == '[]') { + clear(); + return; + } + + final decoded = jsonDecode(jsonString) as List; + if (decoded.isEmpty) { + clear(); + return; + } + fromJsonList(decoded.cast>()); + } + + /// Adds existing lines without clearing (for merging annotations). + void addLines(List newLines) { + _lines.addAll(newLines); + _trimHistory(); + notifyListeners(); + } + + @override + void dispose() { + _lines.clear(); + _undoneLines.clear(); + super.dispose(); + } +} diff --git a/lib/features/sheet_viewer/drawing/drawing_line.dart b/lib/features/sheet_viewer/drawing/drawing_line.dart new file mode 100644 index 0000000..8ebd435 --- /dev/null +++ b/lib/features/sheet_viewer/drawing/drawing_line.dart @@ -0,0 +1,93 @@ +import 'dart:ui'; + +/// Represents a single stroke/line drawn on the canvas. +/// +/// Points are stored in normalized coordinates (0.0 to 1.0) where: +/// - (0, 0) is the top-left corner of the drawing area +/// - (1, 1) is the bottom-right corner of the drawing area +/// +/// This allows drawings to scale correctly when the canvas size changes. +class DrawingLine { + /// Points in normalized coordinates (0.0 to 1.0) + final List points; + + /// Color of the line (stored as ARGB integer for JSON serialization) + final Color color; + + /// Stroke width in normalized units (relative to canvas width) + /// A value of 0.01 means the stroke is 1% of the canvas width + final double strokeWidth; + + const DrawingLine({ + required this.points, + required this.color, + required this.strokeWidth, + }); + + /// Creates a DrawingLine from JSON data. + factory DrawingLine.fromJson(Map json) { + final pointsList = (json['points'] as List) + .map((p) => Offset( + (p['x'] as num).toDouble(), + (p['y'] as num).toDouble(), + )) + .toList(); + + return DrawingLine( + points: pointsList, + color: Color(json['color'] as int), + strokeWidth: (json['strokeWidth'] as num).toDouble(), + ); + } + + /// Converts this line to a JSON-serializable map. + Map toJson() { + return { + 'points': points.map((p) => {'x': p.dx, 'y': p.dy}).toList(), + 'color': color.toARGB32(), + 'strokeWidth': strokeWidth, + }; + } + + /// Creates a copy of this line with an additional point. + DrawingLine addPoint(Offset normalizedPoint) { + return DrawingLine( + points: [...points, normalizedPoint], + color: color, + strokeWidth: strokeWidth, + ); + } + + /// Creates a copy with updated points. + DrawingLine copyWith({ + List? points, + Color? color, + double? strokeWidth, + }) { + return DrawingLine( + points: points ?? this.points, + color: color ?? this.color, + strokeWidth: strokeWidth ?? this.strokeWidth, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! DrawingLine) return false; + if (points.length != other.points.length) return false; + if (color != other.color) return false; + if (strokeWidth != other.strokeWidth) return false; + for (int i = 0; i < points.length; i++) { + if (points[i] != other.points[i]) return false; + } + return true; + } + + @override + int get hashCode => Object.hash( + Object.hashAll(points), + color, + strokeWidth, + ); +} diff --git a/lib/features/sheet_viewer/drawing/drawing_toolbar.dart b/lib/features/sheet_viewer/drawing/drawing_toolbar.dart new file mode 100644 index 0000000..802e05a --- /dev/null +++ b/lib/features/sheet_viewer/drawing/drawing_toolbar.dart @@ -0,0 +1,194 @@ +import 'package:flutter/material.dart'; + +import 'drawing_controller.dart'; +import 'paint_preset.dart'; + +/// A floating toolbar for drawing controls. +/// +/// Provides quick access to: +/// - Paint presets (pens and markers) +/// - Undo/Redo buttons +class DrawingToolbar extends StatelessWidget { + final DrawingController controller; + final VoidCallback? onClose; + + const DrawingToolbar({ + super.key, + required this.controller, + this.onClose, + }); + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: controller, + builder: (context, _) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Paint presets + ...PaintPreset.quickAccess.map((preset) => _buildPresetButton( + context, + preset, + isSelected: controller.currentPreset == preset, + )), + + const SizedBox(width: 8), + _buildDivider(context), + const SizedBox(width: 8), + + // Undo button + _buildActionButton( + context, + icon: Icons.undo, + onPressed: controller.canUndo ? controller.undo : null, + tooltip: 'Undo', + ), + + // Redo button + _buildActionButton( + context, + icon: Icons.redo, + onPressed: controller.canRedo ? controller.redo : null, + tooltip: 'Redo', + ), + + if (onClose != null) ...[ + const SizedBox(width: 8), + _buildDivider(context), + const SizedBox(width: 8), + _buildActionButton( + context, + icon: Icons.close, + onPressed: onClose, + tooltip: 'Exit Drawing Mode', + ), + ], + ], + ), + ); + }, + ); + } + + Widget _buildPresetButton( + BuildContext context, + PaintPreset preset, { + required bool isSelected, + }) { + final isMarker = preset.strokeWidth > 0.01; + final colorScheme = Theme.of(context).colorScheme; + + return Tooltip( + message: preset.name, + child: InkWell( + onTap: () => controller.setPreset(preset), + borderRadius: BorderRadius.circular(20), + child: Container( + width: 36, + height: 36, + margin: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: isSelected + ? Border.all(color: colorScheme.primary, width: 2) + : null, + ), + child: Center( + child: Container( + width: isMarker ? 24 : 18, + height: isMarker ? 12 : 18, + decoration: BoxDecoration( + color: preset.color, + borderRadius: isMarker + ? BorderRadius.circular(2) + : BorderRadius.circular(9), + border: preset.color.a < 1 + ? Border.all(color: Colors.grey.shade400, width: 0.5) + : null, + ), + ), + ), + ), + ), + ); + } + + Widget _buildActionButton( + BuildContext context, { + required IconData icon, + required VoidCallback? onPressed, + required String tooltip, + }) { + final colorScheme = Theme.of(context).colorScheme; + + return Tooltip( + message: tooltip, + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(20), + child: Container( + width: 36, + height: 36, + margin: const EdgeInsets.symmetric(horizontal: 2), + child: Icon( + icon, + size: 20, + color: onPressed != null + ? colorScheme.onSurface + : colorScheme.onSurface.withValues(alpha: 0.3), + ), + ), + ), + ); + } + + Widget _buildDivider(BuildContext context) { + return Container( + width: 1, + height: 24, + color: Theme.of(context).dividerColor, + ); + } +} + +/// A compact floating action button to toggle paint mode. +class DrawingModeButton extends StatelessWidget { + final bool isActive; + final VoidCallback onPressed; + + const DrawingModeButton({ + super.key, + required this.isActive, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return FloatingActionButton.small( + onPressed: onPressed, + backgroundColor: isActive + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.surface, + child: Icon( + isActive ? Icons.brush : Icons.brush_outlined, + color: isActive + ? Theme.of(context).colorScheme.onPrimaryContainer + : Theme.of(context).colorScheme.onSurface, + ), + ); + } +} diff --git a/lib/features/sheet_viewer/drawing/paint_preset.dart b/lib/features/sheet_viewer/drawing/paint_preset.dart new file mode 100644 index 0000000..101ecf7 --- /dev/null +++ b/lib/features/sheet_viewer/drawing/paint_preset.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; + +/// Predefined paint configurations for common annotation styles. +/// +/// Each preset defines a color and stroke width for drawing. +/// Stroke width is normalized (relative to canvas width). +class PaintPreset { + /// Display name for the preset + final String name; + + /// Color of the paint (including opacity) + final Color color; + + /// Stroke width in normalized units (relative to canvas width) + /// A value of 0.005 means the stroke is 0.5% of the canvas width + final double strokeWidth; + + /// Icon to display for this preset + final IconData icon; + + const PaintPreset({ + required this.name, + required this.color, + required this.strokeWidth, + required this.icon, + }); + + // --------------------------------------------------------------------------- + // Default Presets + // --------------------------------------------------------------------------- + + /// Black pen for writing/notes + static const blackPen = PaintPreset( + name: 'Black Pen', + color: Colors.black, + strokeWidth: 0.003, // Thin line for writing + icon: Icons.edit, + ); + + /// Red pen for corrections/markings + static const redPen = PaintPreset( + name: 'Red Pen', + color: Colors.red, + strokeWidth: 0.003, + icon: Icons.edit, + ); + + /// Blue pen for annotations + static const bluePen = PaintPreset( + name: 'Blue Pen', + color: Colors.blue, + strokeWidth: 0.003, + icon: Icons.edit, + ); + + /// Yellow highlighter (semi-transparent, thicker) + static const yellowMarker = PaintPreset( + name: 'Yellow Marker', + color: Color(0x80FFEB3B), // Yellow with 50% opacity + strokeWidth: 0.015, // Thicker for highlighting + icon: Icons.highlight, + ); + + /// Green highlighter + static const greenMarker = PaintPreset( + name: 'Green Marker', + color: Color(0x804CAF50), // Green with 50% opacity + strokeWidth: 0.015, + icon: Icons.highlight, + ); + + /// Pink highlighter + static const pinkMarker = PaintPreset( + name: 'Pink Marker', + color: Color(0x80E91E63), // Pink with 50% opacity + strokeWidth: 0.015, + icon: Icons.highlight, + ); + + /// All available default presets + static const List defaults = [ + blackPen, + redPen, + bluePen, + yellowMarker, + greenMarker, + pinkMarker, + ]; + + /// Quick access presets (shown in main toolbar) + static const List quickAccess = [ + blackPen, + redPen, + yellowMarker, + ]; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is PaintPreset && + other.name == name && + other.color == color && + other.strokeWidth == strokeWidth; + } + + @override + int get hashCode => Object.hash(name, color, strokeWidth); +} diff --git a/lib/features/sheet_viewer/sheet_viewer_page.dart b/lib/features/sheet_viewer/sheet_viewer_page.dart index 5a1f7de..bcabe1c 100644 --- a/lib/features/sheet_viewer/sheet_viewer_page.dart +++ b/lib/features/sheet_viewer/sheet_viewer_page.dart @@ -1,8 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_drawing_board/flutter_drawing_board.dart'; -import 'package:flutter_drawing_board/paint_contents.dart'; import 'package:flutter_fullscreen/flutter_fullscreen.dart'; import 'package:logging/logging.dart'; import 'package:pdfrx/pdfrx.dart'; @@ -12,7 +10,7 @@ import 'package:sheetless/core/services/api_client.dart'; import 'package:sheetless/core/services/storage_service.dart'; import '../../shared/input/pedal_shortcuts.dart'; -import 'widgets/paint_mode_layer.dart'; +import 'drawing/drawing.dart'; import 'widgets/pdf_page_display.dart'; import 'widgets/touch_navigation_layer.dart'; @@ -43,20 +41,21 @@ class _SheetViewerPageState extends State int _currentPage = 1; int _totalPages = 1; bool _isPaintMode = false; - late DrawingController _drawingController; + + /// Drawing controller for the current left page + late DrawingController _leftDrawingController; + + /// Drawing controller for the right page (two-page mode) + late DrawingController _rightDrawingController; @override void initState() { super.initState(); - // Initialize drawing controller with default configuration - _drawingController = DrawingController( - config: DrawConfig( - contentType: SimpleLine, - strokeWidth: 4.0, - color: Colors.black, - ), - maxHistorySteps: 100, // Limit undo/redo history (default: 100) - ); + + // Initialize drawing controllers + _leftDrawingController = DrawingController(maxHistorySteps: 50); + _rightDrawingController = DrawingController(maxHistorySteps: 50); + FullScreen.addListener(this); FullScreen.setFullScreen(widget.config.fullscreen); _documentLoaded = _loadPdf(); @@ -64,12 +63,38 @@ class _SheetViewerPageState extends State @override void dispose() { - _drawingController.dispose(); + // Save current annotations synchronously before disposing + // Note: This is fire-and-forget, but Hive operations are fast enough + _saveCurrentAnnotationsSync(); + + _leftDrawingController.dispose(); + _rightDrawingController.dispose(); FullScreen.removeListener(this); _document?.dispose(); super.dispose(); } + /// Synchronous version that doesn't await - used in dispose + void _saveCurrentAnnotationsSync() { + // Save left page (always, since paint mode is single-page only) + final leftJson = _leftDrawingController.toJsonString(); + _storageService.writeAnnotations( + widget.sheet.uuid, + _currentPage, + leftJson.isEmpty || leftJson == '[]' ? null : leftJson, + ); + + // Save right page if in two-page mode + if (widget.config.twoPageMode && _currentPage < _totalPages) { + final rightJson = _rightDrawingController.toJsonString(); + _storageService.writeAnnotations( + widget.sheet.uuid, + _currentPage + 1, + rightJson.isEmpty || rightJson == '[]' ? null : rightJson, + ); + } + } + // --------------------------------------------------------------------------- // PDF Loading // --------------------------------------------------------------------------- @@ -89,9 +114,64 @@ class _SheetViewerPageState extends State _totalPages = _document!.pages.length; }); + // Load annotations for current page(s) + await _loadAnnotationsForCurrentPages(); + return true; } + // --------------------------------------------------------------------------- + // Annotation Persistence + // --------------------------------------------------------------------------- + + /// Loads annotations for the current page(s) from storage. + Future _loadAnnotationsForCurrentPages() async { + // Load left page annotations + final leftJson = await _storageService.readAnnotations( + widget.sheet.uuid, + _currentPage, + ); + if (leftJson != null && leftJson.isNotEmpty) { + _leftDrawingController.fromJsonString(leftJson); + } else { + _leftDrawingController.clear(); + } + + // Load right page annotations (two-page mode) + if (widget.config.twoPageMode && _currentPage < _totalPages) { + final rightJson = await _storageService.readAnnotations( + widget.sheet.uuid, + _currentPage + 1, + ); + if (rightJson != null && rightJson.isNotEmpty) { + _rightDrawingController.fromJsonString(rightJson); + } else { + _rightDrawingController.clear(); + } + } + } + + /// Saves the current page(s) annotations to storage. + Future _saveCurrentAnnotations() async { + // Save left page + final leftJson = _leftDrawingController.toJsonString(); + await _storageService.writeAnnotations( + widget.sheet.uuid, + _currentPage, + leftJson.isEmpty || leftJson == '[]' ? null : leftJson, + ); + + // Save right page (two-page mode) + if (widget.config.twoPageMode && _currentPage < _totalPages) { + final rightJson = _rightDrawingController.toJsonString(); + await _storageService.writeAnnotations( + widget.sheet.uuid, + _currentPage + 1, + rightJson.isEmpty || rightJson == '[]' ? null : rightJson, + ); + } + } + // --------------------------------------------------------------------------- // Fullscreen // --------------------------------------------------------------------------- @@ -105,10 +185,6 @@ class _SheetViewerPageState extends State } void _toggleFullscreen() { - if (_isPaintMode) { - _showSnackBar('Cannot enter fullscreen while in paint mode'); - return; - } FullScreen.setFullScreen(!widget.config.fullscreen); } @@ -116,10 +192,19 @@ class _SheetViewerPageState extends State // Navigation // --------------------------------------------------------------------------- - void _turnPage(int delta) { - setState(() { - _currentPage = (_currentPage + delta).clamp(1, _totalPages); - }); + Future _turnPage(int delta) async { + // Save current annotations before turning + await _saveCurrentAnnotations(); + + // Calculate new page + final newPage = (_currentPage + delta).clamp(1, _totalPages); + + // Load annotations for new page(s) BEFORE updating state + _currentPage = newPage; + await _loadAnnotationsForCurrentPages(); + + // Now update UI + setState(() {}); } // --------------------------------------------------------------------------- @@ -132,6 +217,11 @@ class _SheetViewerPageState extends State return; } + if (_isPaintMode) { + // Exiting paint mode - save annotations + _saveCurrentAnnotations(); + } + setState(() => _isPaintMode = !_isPaintMode); } @@ -145,6 +235,9 @@ class _SheetViewerPageState extends State widget.config.twoPageMode = !widget.config.twoPageMode; _storageService.writeConfig(widget.config); }); + + // Reload annotations for new mode + _loadAnnotationsForCurrentPages(); } // --------------------------------------------------------------------------- @@ -156,7 +249,12 @@ class _SheetViewerPageState extends State return PedalShortcuts( onPageForward: () => _turnPage(1), onPageBackward: () => _turnPage(-1), - child: Scaffold(appBar: _buildAppBar(), body: _buildBody()), + child: Scaffold( + appBar: _buildAppBar(), + body: _buildBody(), + floatingActionButton: _isPaintMode ? _buildDrawingToolbar() : null, + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + ), ); } @@ -173,23 +271,21 @@ class _SheetViewerPageState extends State icon: Icon( widget.config.fullscreen ? Icons.fullscreen_exit : Icons.fullscreen, ), - tooltip: widget.config.fullscreen - ? 'Exit Fullscreen' - : 'Enter Fullscreen', + tooltip: + widget.config.fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen', onPressed: _toggleFullscreen, ), IconButton( icon: Icon(_isPaintMode ? Icons.brush : Icons.brush_outlined), - tooltip: 'Toggle Paint Mode', + tooltip: _isPaintMode ? 'Exit Paint Mode' : 'Enter Paint Mode', onPressed: _togglePaintMode, ), IconButton( icon: Icon( widget.config.twoPageMode ? Icons.filter_1 : Icons.filter_2, ), - tooltip: widget.config.twoPageMode - ? 'Single Page Mode' - : 'Two Page Mode', + tooltip: + widget.config.twoPageMode ? 'Single Page Mode' : 'Two Page Mode', onPressed: _toggleTwoPageMode, ), ], @@ -220,30 +316,37 @@ class _SheetViewerPageState extends State numPages: _totalPages, currentPageNumber: _currentPage, config: widget.config, + leftDrawingController: _leftDrawingController, + rightDrawingController: + widget.config.twoPageMode ? _rightDrawingController : null, + drawingEnabled: _isPaintMode, ); - return Stack( - children: [ - // Show touch navigation when not in paint mode - Visibility( - visible: !_isPaintMode, - child: TouchNavigationLayer( - pageDisplay: pageDisplay, - config: widget.config, - onToggleFullscreen: _toggleFullscreen, - onExit: () => Navigator.pop(context), - onPageTurn: _turnPage, - ), + // When in paint mode, show the page display directly (DrawingBoard handles zoom/pan) + if (_isPaintMode) { + return pageDisplay; + } + + // When not in paint mode, wrap with touch navigation + return TouchNavigationLayer( + pageDisplay: pageDisplay, + config: widget.config, + onToggleFullscreen: _toggleFullscreen, + onExit: () => Navigator.pop(context), + onPageTurn: _turnPage, + child: pageDisplay, + ); + } + + Widget _buildDrawingToolbar() { + return SafeArea( + child: Padding( + padding: const EdgeInsets.only(bottom: 16), + child: DrawingToolbar( + controller: _leftDrawingController, + onClose: _togglePaintMode, ), - // Show paint mode layer when active - Visibility( - visible: _isPaintMode, - child: PaintModeLayer( - pageDisplay: pageDisplay, - drawingController: _drawingController, - ), - ), - ], + ), ); } @@ -253,9 +356,10 @@ class _SheetViewerPageState extends State padding: const EdgeInsets.all(16.0), child: Text( message, - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(color: Colors.red), + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(color: Colors.red), textAlign: TextAlign.center, ), ), @@ -264,7 +368,7 @@ class _SheetViewerPageState extends State void _showSnackBar(String message) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message), duration: Duration(seconds: 2)), + SnackBar(content: Text(message), duration: const Duration(seconds: 2)), ); } } diff --git a/lib/features/sheet_viewer/widgets/paint_mode_layer.dart b/lib/features/sheet_viewer/widgets/paint_mode_layer.dart deleted file mode 100644 index e3b9b74..0000000 --- a/lib/features/sheet_viewer/widgets/paint_mode_layer.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_drawing_board/flutter_drawing_board.dart'; - -import 'pdf_page_display.dart'; - -/// Drawing overlay for annotating PDF pages. -/// -/// Uses flutter_drawing_board to provide a paint canvas over the PDF. -/// Only working in single-page mode. -class PaintModeLayer extends StatelessWidget { - final PdfPageDisplay pageDisplay; - final DrawingController drawingController; - - const PaintModeLayer({ - super.key, - required this.pageDisplay, - required this.drawingController, - }); - - @override - Widget build(BuildContext context) { - return SizedBox.expand( - child: LayoutBuilder( - builder: (context, constraints) { - final maxSize = Size(constraints.maxWidth, constraints.maxHeight); - final (pageSize, _) = pageDisplay.calculateScaledPageSizes(maxSize); - - return DrawingBoard( - controller: drawingController, - background: SizedBox( - width: pageSize.width, - height: pageSize.height, - child: pageDisplay, - ), - boardConstrained: true, - minScale: 1, - maxScale: 3, - alignment: Alignment.topRight, - boardBoundaryMargin: EdgeInsets.zero, - ); - }, - ), - ); - } -} diff --git a/lib/features/sheet_viewer/widgets/pdf_page_display.dart b/lib/features/sheet_viewer/widgets/pdf_page_display.dart index cf817aa..71a0229 100644 --- a/lib/features/sheet_viewer/widgets/pdf_page_display.dart +++ b/lib/features/sheet_viewer/widgets/pdf_page_display.dart @@ -4,20 +4,33 @@ import 'package:flutter/material.dart'; import 'package:pdfrx/pdfrx.dart'; import '../../../core/models/config.dart'; +import '../drawing/drawing.dart'; -/// Displays PDF pages with optional two-page mode. +/// Displays PDF pages with optional two-page mode and drawing overlay. class PdfPageDisplay extends StatelessWidget { final PdfDocument document; final int numPages; final int currentPageNumber; final Config config; + /// Controller for the left/main page drawing + final DrawingController? leftDrawingController; + + /// Controller for the right page drawing (two-page mode only) + final DrawingController? rightDrawingController; + + /// Whether drawing is enabled + final bool drawingEnabled; + const PdfPageDisplay({ super.key, required this.document, required this.numPages, required this.currentPageNumber, required this.config, + this.leftDrawingController, + this.rightDrawingController, + this.drawingEnabled = false, }); /// Whether two-page mode is active and we have enough pages. @@ -25,55 +38,102 @@ class PdfPageDisplay extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [_buildLeftPage(), if (_showTwoPages) _buildRightPage()], - ); - } + return LayoutBuilder( + builder: (context, constraints) { + final maxSize = Size(constraints.maxWidth, constraints.maxHeight); + final (leftSize, rightSize) = calculateScaledPageSizes(maxSize); - Widget _buildLeftPage() { - return Expanded( - child: Stack( - children: [ - PdfPageView( - key: ValueKey(currentPageNumber), - document: document, + if (_showTwoPages) { + // Two-page mode: pages touch each other and are centered together + return Center( + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildPage( + pageNumber: currentPageNumber, + pageSize: leftSize, + controller: leftDrawingController, + ), + // Only show right page if there is one + if (currentPageNumber < numPages) + _buildPage( + pageNumber: currentPageNumber + 1, + pageSize: rightSize!, + controller: rightDrawingController, + ) + else + // Empty space to keep left page position consistent on last page + SizedBox(width: rightSize!.width, height: rightSize.height), + ], + ), + ); + } + + // Single page mode + return Center( + child: _buildPage( pageNumber: currentPageNumber, - maximumDpi: 300, - alignment: _showTwoPages ? Alignment.centerRight : Alignment.center, + pageSize: leftSize, + controller: leftDrawingController, ), - _buildPageIndicator(currentPageNumber), - ], - ), + ); + }, ); } - Widget _buildRightPage() { - final rightPageNumber = currentPageNumber + 1; - - return Expanded( + Widget _buildPage({ + required int pageNumber, + required Size pageSize, + required DrawingController? controller, + }) { + final pdfPage = SizedBox( + width: pageSize.width, + height: pageSize.height, child: Stack( children: [ PdfPageView( - key: ValueKey(rightPageNumber), + key: ValueKey(pageNumber), document: document, - pageNumber: rightPageNumber, + pageNumber: pageNumber, maximumDpi: 300, - alignment: Alignment.centerLeft, + alignment: Alignment.center, ), - _buildPageIndicator(rightPageNumber), + _buildPageIndicator(pageNumber, pageSize), ], ), ); + + // If no controller, just show the PDF + if (controller == null) { + return pdfPage; + } + + // Wrap with DrawingBoard + return DrawingBoard( + boardSize: pageSize, + controller: controller, + drawingEnabled: drawingEnabled, + minScale: 1.0, + maxScale: 3.0, + alignment: Alignment.center, + child: pdfPage, + ); } - Widget _buildPageIndicator(int pageNumber) { - return Positioned.fill( - child: Container( - alignment: Alignment.bottomCenter, - padding: const EdgeInsets.only(bottom: 5), - child: Text('$pageNumber / $numPages'), + Widget _buildPageIndicator(int pageNumber, Size pageSize) { + return Positioned( + bottom: 5, + left: 0, + right: 0, + child: Center( + child: Text( + '$pageNumber / $numPages', + style: const TextStyle( + fontSize: 12, + color: Colors.black54, + ), + ), ), ); } diff --git a/lib/features/sheet_viewer/widgets/touch_navigation_layer.dart b/lib/features/sheet_viewer/widgets/touch_navigation_layer.dart index 4f08679..9cf0384 100644 --- a/lib/features/sheet_viewer/widgets/touch_navigation_layer.dart +++ b/lib/features/sheet_viewer/widgets/touch_navigation_layer.dart @@ -4,7 +4,7 @@ import '../../../core/models/config.dart'; import 'pdf_page_display.dart'; /// Callback for page turn events. -typedef PageTurnCallback = void Function(int delta); +typedef PageTurnCallback = dynamic Function(int delta); /// Gesture layer for touch-based navigation over PDF pages. /// @@ -14,6 +14,7 @@ typedef PageTurnCallback = void Function(int delta); /// - Right side: Turn page forward (+1 or +2 in two-page mode) class TouchNavigationLayer extends StatelessWidget { final PdfPageDisplay pageDisplay; + final Widget child; final Config config; final VoidCallback onToggleFullscreen; final VoidCallback onExit; @@ -22,6 +23,7 @@ class TouchNavigationLayer extends StatelessWidget { const TouchNavigationLayer({ super.key, required this.pageDisplay, + required this.child, required this.config, required this.onToggleFullscreen, required this.onExit, @@ -33,7 +35,7 @@ class TouchNavigationLayer extends StatelessWidget { return GestureDetector( behavior: HitTestBehavior.opaque, onTapUp: (details) => _handleTap(context, details), - child: pageDisplay, + child: child, ); } diff --git a/pubspec.lock b/pubspec.lock index 5237a2a..b221c02 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -142,14 +142,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.1" - flutter_drawing_board: - dependency: "direct main" - description: - name: flutter_drawing_board - sha256: "0fc6b73ac6a54f23d0357ff3f3a804156315f43212a417406062462fe2e3ca7b" - url: "https://pub.dev" - source: hosted - version: "1.0.1+2" flutter_fullscreen: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7de38c3..2921ae0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,7 +45,6 @@ dependencies: jwt_decoder: ^2.0.1 pdfrx: ^2.0.4 logging: ^1.3.0 - flutter_drawing_board: ^1.0.1+2 flutter_launcher_icons: ^0.14.4 hive: ^2.2.3 flutter_fullscreen: ^1.2.0