import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' hide UndoManager; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; import 'package:uuid/uuid.dart'; import '../models/bookmark.dart'; import '../models/document.dart'; import '../models/ink_stroke.dart'; import '../models/pen_tool.dart'; import '../models/pressure_curve.dart'; import '../services/camera_service.dart'; import '../services/database_service.dart'; import '../services/pdf_service.dart'; import '../services/thumbnail_service.dart'; import '../services/undo_manager.dart'; import '../utils/stroke_stabilizer.dart'; import '../widgets/annotation_toolbar.dart'; import '../widgets/ink_canvas.dart'; import '../widgets/page_thumbnail_sidebar.dart'; import '../widgets/pdf_annotation_layer.dart'; import 'pdf_text_search.dart'; import 'split_view_screen.dart'; const _uuid = Uuid(); /// Actions available in the AppBar overflow menu. enum _OverflowAction { pageManagement, cameraInsert, export } /// Full-screen PDF viewer with ink annotation overlay. /// /// Displays a PDF page-by-page with a transparent [PdfAnnotationLayer] /// on top for pen/marker/eraser annotations. Annotations are stored /// per page in normalized [0, 1] coordinates and exported via [PdfService]. /// Annotations and bookmarks are persisted to the database. class PdfAnnotatorScreen extends StatefulWidget { final String filePath; final int initialPage; const PdfAnnotatorScreen({ super.key, required this.filePath, this.initialPage = 0, }); @override State createState() => _PdfAnnotatorScreenState(); } class _PdfAnnotatorScreenState extends State { final PdfService _pdfService = PdfService(); final CameraService _cameraService = CameraService(); final PdfViewerController _viewerController = PdfViewerController(); final GlobalKey _scaffoldKey = GlobalKey(); int _currentPage = 0; int _pageCount = 0; int _pdfMutationVersion = 0; String _fileName = ''; PenTool _currentTool = PenTool.pen; Color _currentColor = Colors.black; double _currentStrokeWidth = 2.0; bool _filled = false; PressureCurveType _pressureCurveType = PressureCurveType.linear; StabilizationLevel _stabilizationLevel = StabilizationLevel.none; InteractionMode _interactionMode = InteractionMode.draw; double _zoomLevel = 1.0; bool _showThumbnails = false; String? _currentDocumentId; final Map _undoManagers = {}; final Map> _annotations = {}; List _bookmarks = []; @override void initState() { super.initState(); _currentPage = widget.initialPage; _loadPdfInfo(); } Future _loadPdfInfo() async { final info = await _pdfService.getPdfInfo(widget.filePath); final count = await _pdfService.getPageCount(widget.filePath); if (mounted) { setState(() { _fileName = info['fileName'] as String; _pageCount = count; }); await _ensureDocumentExists(); await _loadAllAnnotations(); await _loadBookmarks(); } } Future _ensureDocumentExists() async { final db = await DatabaseService.getInstance(); final existing = await db.getDocumentByPath(widget.filePath); if (existing == null) { final now = DateTime.now(); final newDoc = Document( id: _uuid.v4(), filename: _fileName, docType: 'pdf', filePath: widget.filePath, pageCount: _pageCount, createdAt: now, updatedAt: now, ); await db.insertDocument(newDoc); _currentDocumentId = newDoc.id; } else { _currentDocumentId = existing.id; } } Future _loadAllAnnotations() async { if (_currentDocumentId == null) return; final db = await DatabaseService.getInstance(); for (int i = 0; i < _pageCount; i++) { final json = await db.getAnnotations(_currentDocumentId!, i); if (json != null && json.isNotEmpty) { final List list = jsonDecode(json) as List; _annotations[i] = list .map((s) => InkStroke.fromJson(s as Map)) .toList(); _undoManagers[i] = UndoManager(); for (final stroke in _annotations[i]!) { _undoManagers[i]!.addStroke(stroke); } } } if (mounted) setState(() {}); } void _saveCurrentPageAnnotations({int? page}) async { if (_currentDocumentId == null) return; // Capture the page index and serialize its strokes SYNCHRONOUSLY, before // any await. Otherwise a concurrent navigation could change _currentPage // while this is suspended, causing the wrong page's data to be saved. final targetPage = page ?? _currentPage; final documentId = _currentDocumentId!; final strokesJson = jsonEncode( _annotations[targetPage]?.map((s) => s.toJson()).toList() ?? [], ); final db = await DatabaseService.getInstance(); await db.saveAnnotations(documentId, targetPage, strokesJson); } void _onPageChanged(int page) { // Save the page we are leaving, not the one we are navigating to. _saveCurrentPageAnnotations(page: _currentPage); setState(() { _currentPage = page; }); } UndoManager _getUndoManager(int page) { return _undoManagers.putIfAbsent(page, UndoManager.new); } List _getCurrentStrokes() { return _annotations[_currentPage] ?? []; } void _onStrokeComplete(InkStroke stroke) { setState(() { _annotations.putIfAbsent(_currentPage, () => []); _annotations[_currentPage]!.add(stroke); _getUndoManager(_currentPage).addStroke(stroke); }); _saveCurrentPageAnnotations(); } void _onErase(String strokeId, List replacements) { setState(() { final pageStrokes = _annotations[_currentPage]; if (pageStrokes == null) return; final original = pageStrokes.where((s) => s.id == strokeId).firstOrNull; if (original != null) { _getUndoManager( _currentPage, ).removeStroke(original, replacements: replacements); _annotations[_currentPage] = _getUndoManager( _currentPage, ).currentStrokes.toList(); } }); _saveCurrentPageAnnotations(); } void _undo() { setState(() { _getUndoManager(_currentPage).undo(); _annotations[_currentPage] = _getUndoManager( _currentPage, ).currentStrokes.toList(); }); _saveCurrentPageAnnotations(); } void _redo() { setState(() { _getUndoManager(_currentPage).redo(); _annotations[_currentPage] = _getUndoManager( _currentPage, ).currentStrokes.toList(); }); _saveCurrentPageAnnotations(); } // -- Bookmarks -- bool get _isCurrentPageBookmarked => _bookmarks.any((b) => b.pageNumber == _currentPage); Future _loadBookmarks() async { if (_currentDocumentId == null) return; final db = await DatabaseService.getInstance(); final bookmarks = await db.getBookmarks(_currentDocumentId!); if (mounted) { setState(() { _bookmarks = bookmarks; }); } } Future _toggleBookmark() async { if (_currentDocumentId == null) return; final db = await DatabaseService.getInstance(); if (_isCurrentPageBookmarked) { final existing = _bookmarks.firstWhere( (b) => b.pageNumber == _currentPage, ); await db.deleteBookmark(existing.id); setState(() { _bookmarks.removeWhere((b) => b.id == existing.id); }); } else { final label = await _showBookmarkDialog(); if (label == null) return; final bookmark = Bookmark( id: _uuid.v4(), documentId: _currentDocumentId!, pageNumber: _currentPage, label: label, createdAt: DateTime.now(), ); await db.insertBookmark(bookmark); setState(() { _bookmarks.add(bookmark); _bookmarks.sort((a, b) => a.pageNumber.compareTo(b.pageNumber)); }); } } Future _showBookmarkDialog() async { final controller = TextEditingController(); return showDialog( context: context, builder: (context) { return AlertDialog( title: const Text('Add Bookmark'), content: TextField( controller: controller, decoration: const InputDecoration( hintText: 'Label (optional)', labelText: 'Bookmark label', ), autofocus: true, ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), TextButton( onPressed: () => Navigator.of(context).pop(controller.text), child: const Text('Add'), ), ], ); }, ); } Future _deleteBookmark(Bookmark bookmark) async { final db = await DatabaseService.getInstance(); await db.deleteBookmark(bookmark.id); setState(() { _bookmarks.removeWhere((b) => b.id == bookmark.id); }); } void _jumpToPage(int page) { _saveCurrentPageAnnotations(); _viewerController.jumpToPage(page + 1); } // -- Zoom -- void _zoomIn() { final newLevel = (_zoomLevel + 0.25).clamp(0.5, 5.0); _viewerController.zoomLevel = newLevel; setState(() => _zoomLevel = newLevel); } void _zoomOut() { final newLevel = (_zoomLevel - 0.25).clamp(0.5, 5.0); _viewerController.zoomLevel = newLevel; setState(() => _zoomLevel = newLevel); } void _zoomFitWidth() { _viewerController.zoomLevel = 1.0; setState(() => _zoomLevel = 1.0); } // -- Search -- void _openSearch() { showDialog( context: context, builder: (_) => PdfTextSearchDialog(viewerController: _viewerController), ); } // -- Export -- Future _exportPdf() async { try { final outputPath = await _pdfService.exportAnnotatedPdf( widget.filePath, _annotations, ); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Exported to: $outputPath'))); } } catch (e) { if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Export failed: $e'))); } } } // -- Page Management -- void _showPageManagementSheet() { showModalBottomSheet( context: context, builder: (context) { final canDelete = _pageCount > 1; return SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.rotate_right), title: const Text('Rotate Page 90\u00B0'), subtitle: Text('Page ${_currentPage + 1}'), onTap: () { Navigator.of(context).pop(); _rotateCurrentPage(); }, ), ListTile( leading: Icon( Icons.delete_outline, color: canDelete ? null : Colors.grey, ), title: Text( 'Delete Page', style: TextStyle(color: canDelete ? null : Colors.grey), ), subtitle: Text( canDelete ? 'Page ${_currentPage + 1}' : 'Cannot delete the only page', ), enabled: canDelete, onTap: canDelete ? () { Navigator.of(context).pop(); _deleteCurrentPage(); } : null, ), ListTile( leading: const Icon(Icons.note_add_outlined), title: const Text('Insert Blank Page After Current'), subtitle: Text('After page ${_currentPage + 1}'), onTap: () { Navigator.of(context).pop(); _insertBlankPageAfterCurrent(); }, ), ], ), ); }, ); } /// Transform a stroke's normalized [0,1] points to match a 90° clockwise /// page rotation: a point at (x, y) maps to (1 - y, x). Used to keep /// existing annotations glued to the page content after the page itself is /// physically rotated (PDF /Rotate). InkStroke _rotateStroke90CW(InkStroke stroke) { return stroke.copyWith( points: stroke.points .map((p) => p.copyWith(x: 1.0 - p.y, y: p.x)) .toList(), ); } Future _rotateCurrentPage() async { final rotatedPage = _currentPage; final success = await _pdfService.rotatePage(widget.filePath, rotatedPage); if (!success || !mounted) return; // Invalidate thumbnail for the rotated page. if (_currentDocumentId != null) { await ThumbnailService.invalidatePage(_currentDocumentId!, rotatedPage); } setState(() { _pdfMutationVersion++; // The page is physically rotated 90° CW, so transform existing stored // annotations the same way to keep them aligned with the page content. // New strokes drawn afterwards are already captured in the rotated frame. final existing = _annotations[rotatedPage]; if (existing != null && existing.isNotEmpty) { _annotations[rotatedPage] = existing.map(_rotateStroke90CW).toList(); // Undo history holds pre-rotation coordinates; reset it for this page // so undo/redo cannot reintroduce misaligned strokes. _undoManagers.remove(rotatedPage); } }); // Persist the transformed annotations for the rotated page. _saveCurrentPageAnnotations(page: rotatedPage); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Rotated page ${_currentPage + 1}')), ); } } Future _deleteCurrentPage() async { // Confirm. final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Delete Page'), content: Text( 'Delete page ${_currentPage + 1}? This cannot be undone.', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancel'), ), TextButton( onPressed: () => Navigator.of(context).pop(true), child: const Text('Delete', style: TextStyle(color: Colors.red)), ), ], ), ); if (confirmed != true) return; final success = await _pdfService.deletePage(widget.filePath, _currentPage); if (!success || !mounted) return; if (_currentDocumentId != null) { final db = await DatabaseService.getInstance(); // Delete annotation/bookmark/ocr data for the removed page. await db.deletePageData(_currentDocumentId!, _currentPage); // Remap higher-indexed data down by 1. await db.remapAnnotationsAfterDelete(_currentDocumentId!, _currentPage); await db.remapBookmarksAfterDelete(_currentDocumentId!, _currentPage); // Update stored page count. final newCount = _pageCount - 1; await db.updateDocumentPageCount(_currentDocumentId!, newCount); // Invalidate all thumbnails (page indices shifted). await ThumbnailService.invalidateAll(_currentDocumentId!); } // Shift in-memory annotations down. final newAnnotations = >{}; for (final entry in _annotations.entries) { if (entry.key < _currentPage) { newAnnotations[entry.key] = entry.value; } else if (entry.key > _currentPage) { newAnnotations[entry.key - 1] = entry.value; } // entry.key == _currentPage is dropped. } _annotations ..clear() ..addAll(newAnnotations); // Shift undo managers. final newUndoManagers = {}; for (final entry in _undoManagers.entries) { if (entry.key < _currentPage) { newUndoManagers[entry.key] = entry.value; } else if (entry.key > _currentPage) { newUndoManagers[entry.key - 1] = entry.value; } } _undoManagers ..clear() ..addAll(newUndoManagers); // Shift bookmarks in memory. _bookmarks.removeWhere((b) => b.pageNumber == _currentPage); for (int i = 0; i < _bookmarks.length; i++) { if (_bookmarks[i].pageNumber > _currentPage) { _bookmarks[i] = Bookmark( id: _bookmarks[i].id, documentId: _bookmarks[i].documentId, pageNumber: _bookmarks[i].pageNumber - 1, label: _bookmarks[i].label, color: _bookmarks[i].color, createdAt: _bookmarks[i].createdAt, ); } } setState(() { _pageCount = _pageCount - 1; if (_currentPage >= _pageCount) { _currentPage = _pageCount - 1; } _pdfMutationVersion++; }); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('Page deleted'))); } } Future _insertBlankPageAfterCurrent() async { final success = await _pdfService.insertBlankPage( widget.filePath, _currentPage, ); if (!success || !mounted) return; final insertedIndex = _currentPage + 1; if (_currentDocumentId != null) { final db = await DatabaseService.getInstance(); await db.remapAnnotationsAfterInsert(_currentDocumentId!, insertedIndex); await db.remapBookmarksAfterInsert(_currentDocumentId!, insertedIndex); final newCount = _pageCount + 1; await db.updateDocumentPageCount(_currentDocumentId!, newCount); await ThumbnailService.invalidateAll(_currentDocumentId!); } // Shift in-memory annotations up by 1 for pages >= insertedIndex. final newAnnotations = >{}; for (final entry in _annotations.entries) { if (entry.key < insertedIndex) { newAnnotations[entry.key] = entry.value; } else { newAnnotations[entry.key + 1] = entry.value; } } _annotations ..clear() ..addAll(newAnnotations); final newUndoManagers = {}; for (final entry in _undoManagers.entries) { if (entry.key < insertedIndex) { newUndoManagers[entry.key] = entry.value; } else { newUndoManagers[entry.key + 1] = entry.value; } } _undoManagers ..clear() ..addAll(newUndoManagers); // Shift bookmarks in memory. for (int i = 0; i < _bookmarks.length; i++) { if (_bookmarks[i].pageNumber >= insertedIndex) { _bookmarks[i] = Bookmark( id: _bookmarks[i].id, documentId: _bookmarks[i].documentId, pageNumber: _bookmarks[i].pageNumber + 1, label: _bookmarks[i].label, color: _bookmarks[i].color, createdAt: _bookmarks[i].createdAt, ); } } setState(() { _pageCount = _pageCount + 1; _pdfMutationVersion++; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Blank page inserted after page ${_currentPage + 1}'), ), ); } } // -- Camera Insert -- Future _showCameraInsertDialog() async { final source = await showDialog( context: context, builder: (context) => SimpleDialog( title: const Text('Insert Image'), children: [ SimpleDialogOption( onPressed: () => Navigator.of(context).pop('camera'), child: const ListTile( leading: Icon(Icons.camera_alt), title: Text('Camera'), ), ), SimpleDialogOption( onPressed: () => Navigator.of(context).pop('gallery'), child: const ListTile( leading: Icon(Icons.photo_library), title: Text('Gallery'), ), ), ], ), ); if (source == null || !mounted) return; final String? imagePath; if (source == 'camera') { imagePath = await _cameraService.capturePhoto(); } else { imagePath = await _cameraService.pickFromGallery(); } if (imagePath == null || !mounted) return; final result = await _pdfService.insertImageOnPage( widget.filePath, _currentPage, imagePath, ); if (result != null && mounted) { if (_currentDocumentId != null) { await ThumbnailService.invalidatePage( _currentDocumentId!, _currentPage, ); } setState(() { _pdfMutationVersion++; }); // Save current annotations so they overlay the image. _saveCurrentPageAnnotations(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Image inserted on page ${_currentPage + 1}')), ); } } else if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('Failed to insert image'))); } } // -- Bookmark drawer -- Widget _buildBookmarkDrawer() { return Drawer( child: Column( children: [ Padding( padding: const EdgeInsets.all(16), child: Text( 'Bookmarks', style: Theme.of(context).textTheme.headlineSmall, ), ), Expanded( child: _bookmarks.isEmpty ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.bookmark_border, size: 48, color: Theme.of(context).colorScheme.onSurfaceVariant, ), const SizedBox(height: 12), const Text( 'No bookmarks yet', style: TextStyle(fontWeight: FontWeight.w600), ), const SizedBox(height: 4), const Text( 'Tap the bookmark icon in the toolbar\nto bookmark the current page.', textAlign: TextAlign.center, ), ], ), ) : ListView.builder( itemCount: _bookmarks.length, itemBuilder: (context, index) { final bookmark = _bookmarks[index]; return ListTile( leading: CircleAvatar( backgroundColor: Color(bookmark.color), radius: 6, ), title: Text( bookmark.label.isEmpty ? 'Page ${bookmark.pageNumber + 1}' : bookmark.label, ), subtitle: Text('Page ${bookmark.pageNumber + 1}'), trailing: IconButton( icon: const Icon(Icons.delete_outline), tooltip: 'Delete bookmark', onPressed: () => _deleteBookmark(bookmark), ), onTap: () { Navigator.of(context).pop(); _jumpToPage(bookmark.pageNumber); }, onLongPress: () => _deleteBookmark(bookmark), ); }, ), ), ], ), ); } // -- UI -- @override Widget build(BuildContext context) { final undoManager = _getUndoManager(_currentPage); return Scaffold( key: _scaffoldKey, appBar: AppBar( title: Text(_fileName, style: const TextStyle(fontSize: 16)), actions: [ IconButton( icon: const Icon(Icons.vertical_split), tooltip: 'Open in Split View', onPressed: () { if (_currentDocumentId == null) return; _saveCurrentPageAnnotations(); Navigator.of(context).push( MaterialPageRoute( builder: (_) => SplitViewScreen( filePath: widget.filePath, documentId: _currentDocumentId!, ), ), ); }, ), IconButton( icon: const Icon(Icons.search), tooltip: 'Search in PDF (Ctrl+F)', onPressed: _openSearch, ), IconButton( icon: Icon( _isCurrentPageBookmarked ? Icons.bookmark : Icons.bookmark_border, ), tooltip: 'Toggle bookmark', onPressed: _toggleBookmark, ), IconButton( icon: const Icon(Icons.menu_book), tooltip: 'Bookmarks', onPressed: () => _scaffoldKey.currentState?.openEndDrawer(), ), IconButton( icon: Icon( _showThumbnails ? Icons.view_sidebar : Icons.view_sidebar_outlined, ), tooltip: 'Toggle page thumbnails', onPressed: () => setState(() => _showThumbnails = !_showThumbnails), ), PopupMenuButton<_OverflowAction>( icon: const Icon(Icons.more_vert), tooltip: 'More actions', onSelected: (action) { switch (action) { case _OverflowAction.pageManagement: _showPageManagementSheet(); case _OverflowAction.cameraInsert: _showCameraInsertDialog(); case _OverflowAction.export: _exportPdf(); } }, itemBuilder: (context) => const [ PopupMenuItem( value: _OverflowAction.pageManagement, child: ListTile( leading: Icon(Icons.pages), title: Text('Page Management'), contentPadding: EdgeInsets.zero, ), ), PopupMenuItem( value: _OverflowAction.cameraInsert, child: ListTile( leading: Icon(Icons.camera_alt), title: Text('Insert Image'), contentPadding: EdgeInsets.zero, ), ), PopupMenuItem( value: _OverflowAction.export, child: ListTile( leading: Icon(Icons.save_alt), title: Text('Export PDF'), contentPadding: EdgeInsets.zero, ), ), ], ), ], ), endDrawer: _buildBookmarkDrawer(), body: CallbackShortcuts( bindings: { const SingleActivator(LogicalKeyboardKey.keyZ, control: true): _undo, const SingleActivator(LogicalKeyboardKey.keyY, control: true): _redo, const SingleActivator( LogicalKeyboardKey.keyZ, control: true, shift: true, ): _redo, const SingleActivator(LogicalKeyboardKey.keyF, control: true): _openSearch, const SingleActivator(LogicalKeyboardKey.keyS, control: true): _saveCurrentPageAnnotations, const SingleActivator(LogicalKeyboardKey.escape): () { if (_interactionMode != InteractionMode.navigate) { setState(() => _interactionMode = InteractionMode.navigate); } }, }, child: Focus( autofocus: true, child: Column( children: [ AnnotationToolbar( currentTool: _currentTool, currentColor: _currentColor, currentStrokeWidth: _currentStrokeWidth, filled: _filled, pressureCurveType: _pressureCurveType, stabilizationLevel: _stabilizationLevel, canUndo: undoManager.canUndo, canRedo: undoManager.canRedo, onToolChanged: (tool) => setState(() => _currentTool = tool), onColorChanged: (color) => setState(() => _currentColor = color), onStrokeWidthChanged: (w) => setState(() => _currentStrokeWidth = w), onFilledChanged: (f) => setState(() => _filled = f), onPressureCurveChanged: (v) => setState(() => _pressureCurveType = v), onStabilizationChanged: (v) => setState(() => _stabilizationLevel = v), onUndo: _undo, onRedo: _redo, onPreviousPage: _currentPage > 0 ? () => _viewerController.previousPage() : null, onNextPage: _currentPage < _pageCount - 1 ? () => _viewerController.nextPage() : null, pageInfo: '${_currentPage + 1} / $_pageCount', interactionMode: _interactionMode, onInteractionModeChanged: (mode) => setState(() => _interactionMode = mode), onZoomIn: _zoomIn, onZoomOut: _zoomOut, onZoomFitWidth: _zoomFitWidth, zoomLabel: '${(_zoomLevel * 100).round()}%', ), Expanded( child: Row( children: [ if (_showThumbnails && _currentDocumentId != null) PageThumbnailSidebar( documentId: _currentDocumentId!, filePath: widget.filePath, pageCount: _pageCount, currentPage: _currentPage, onPageTap: _jumpToPage, bookmarkedPages: _bookmarks .map((b) => b.pageNumber) .toSet(), ), Expanded( child: Stack( children: [ SfPdfViewer.file( File(widget.filePath), key: ValueKey('pdf-$_pdfMutationVersion'), controller: _viewerController, initialPageNumber: _currentPage + 1, onPageChanged: (PdfPageChangedDetails details) { _onPageChanged(details.newPageNumber - 1); }, ), Positioned.fill( child: _interactionMode == InteractionMode.navigate ? IgnorePointer( child: PdfAnnotationLayer( strokes: _getCurrentStrokes(), onStrokeComplete: _onStrokeComplete, onErase: _onErase, tool: _currentTool, color: _currentColor, strokeWidth: _currentStrokeWidth, filled: _filled, interactionMode: _interactionMode, ), ) : PdfAnnotationLayer( strokes: _getCurrentStrokes(), onStrokeComplete: _onStrokeComplete, onErase: _onErase, tool: _currentTool, color: _currentColor, strokeWidth: _currentStrokeWidth, filled: _filled, interactionMode: _interactionMode, ), ), ], ), ), ], ), ), ], ), ), ), ); } @override void dispose() { _saveCurrentPageAnnotations(); _viewerController.dispose(); super.dispose(); } }