import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; import '../models/ink_stroke.dart'; import '../models/pen_tool.dart'; import '../models/pressure_curve.dart'; import '../services/database_service.dart'; import '../services/undo_manager.dart'; import '../utils/stroke_stabilizer.dart'; import '../widgets/annotation_toolbar.dart'; import '../widgets/ink_canvas.dart'; /// Split-view derivation mode: left pane = reference PDF, right pane = infinite /// scratchpad for formula derivation. Scratchpad strokes are persisted per /// document via [DatabaseService.saveScratchpad] / [DatabaseService.loadScratchpad]. class SplitViewScreen extends StatefulWidget { final String filePath; final String documentId; const SplitViewScreen({ super.key, required this.filePath, required this.documentId, }); @override State createState() => _SplitViewState(); } class _SplitViewState extends State { // -- PDF (left pane) -- final PdfViewerController _pdfController = PdfViewerController(); int _currentPage = 0; int _pageCount = 0; String _fileName = ''; // -- Split divider -- double _leftPaneFraction = 0.5; bool _isDraggingDivider = false; // -- Scratchpad (right pane) -- final UndoManager _undoManager = UndoManager(); List _strokes = []; double _canvasWidth = 4000; double _canvasHeight = 4000; // -- Tool state -- PenTool _currentTool = PenTool.pen; Color _currentColor = Colors.black; double _currentStrokeWidth = 2.0; bool _filled = false; PressureCurveType _pressureCurveType = PressureCurveType.linear; StabilizationLevel _stabilizationLevel = StabilizationLevel.none; // -- Auto-save debounce -- Timer? _saveTimer; bool _dirty = false; // -- Page link markers (optional feature) -- final List<_PageLink> _pageLinks = []; static const double _edgeThreshold = 200.0; static const double _expandAmount = 1000.0; @override void initState() { super.initState(); _loadScratchpad(); } @override void dispose() { _saveTimer?.cancel(); _saveImmediate(); _pdfController.dispose(); super.dispose(); } // -- Persistence -- Future _loadScratchpad() async { final db = await DatabaseService.getInstance(); final strokes = await db.loadScratchpad(widget.documentId); if (mounted) { setState(() { _strokes = strokes; for (final s in strokes) { _undoManager.addStroke(s); } }); } } void _scheduleSave() { _dirty = true; _saveTimer?.cancel(); _saveTimer = Timer(const Duration(seconds: 3), _saveImmediate); } Future _saveImmediate() async { if (!_dirty) return; _dirty = false; final db = await DatabaseService.getInstance(); final json = jsonEncode(_strokes.map((s) => s.toJson()).toList()); await db.saveScratchpad(widget.documentId, json); } // -- Scratchpad stroke callbacks -- void _onStrokeComplete(InkStroke stroke) { setState(() { _strokes.add(stroke); _undoManager.addStroke(stroke); _checkCanvasExpansion(stroke); }); _scheduleSave(); } void _onErase(String strokeId, List replacements) { setState(() { final original = _strokes.where((s) => s.id == strokeId).firstOrNull; if (original != null) { _undoManager.removeStroke(original, replacements: replacements); _strokes = List.from(_undoManager.currentStrokes); } }); _scheduleSave(); } void _undo() { setState(() { _undoManager.undo(); _strokes = List.from(_undoManager.currentStrokes); }); _scheduleSave(); } void _redo() { setState(() { _undoManager.redo(); _strokes = List.from(_undoManager.currentStrokes); }); _scheduleSave(); } // -- Auto-expand canvas -- void _checkCanvasExpansion(InkStroke stroke) { double maxRight = 0; double maxBottom = 0; for (final p in stroke.points) { if (p.x > maxRight) maxRight = p.x; if (p.y > maxBottom) maxBottom = p.y; } bool expanded = false; if (maxRight > _canvasWidth - _edgeThreshold) { _canvasWidth += _expandAmount; expanded = true; } if (maxBottom > _canvasHeight - _edgeThreshold) { _canvasHeight += _expandAmount; expanded = true; } if (expanded) setState(() {}); } // -- Divider drag -- void _onDividerDragStart(DragStartDetails details) { setState(() => _isDraggingDivider = true); } void _onDividerDragUpdate( DragUpdateDetails details, BoxConstraints constraints, ) { final renderWidth = constraints.maxWidth; if (renderWidth <= 0) return; final delta = details.delta.dx / renderWidth; setState(() { _leftPaneFraction = (_leftPaneFraction + delta).clamp(0.2, 0.8); }); } void _onDividerDragEnd(DragEndDetails details) { setState(() => _isDraggingDivider = false); } // -- PDF page navigation -- void _prevPage() { if (_currentPage > 0) { _pdfController.previousPage(); } } void _nextPage() { if (_currentPage < _pageCount - 1) { _pdfController.nextPage(); } } // -- Page link creation (long-press on left pane) -- void _onPdfLongPress(int pageNumber) { // Place a page link marker at the current scratchpad viewport center. // We approximate the viewport center as (0, 0) since InteractiveViewer // manages its own transform — the user can reposition by panning. setState(() { _pageLinks.add( _PageLink( pageNumber: pageNumber, position: const Offset(100, 100), // default top-left area ), ); }); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Page link marker added for page $pageNumber')), ); } void _onPageLinkTap(_PageLink link) { _pdfController.jumpToPage(link.pageNumber); setState(() { _currentPage = link.pageNumber - 1; }); } void _deletePageLink(_PageLink link) { setState(() { _pageLinks.remove(link); }); } // -- Build -- @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( _fileName.isEmpty ? 'Split View' : _fileName, style: const TextStyle(fontSize: 16), ), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { _saveImmediate(); Navigator.of(context).pop(); }, ), actions: [ // Left pane page navigation IconButton( icon: const Icon(Icons.navigate_before), tooltip: 'Previous page (PDF)', onPressed: _currentPage > 0 ? _prevPage : null, ), Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Center( child: Text( '${_currentPage + 1} / $_pageCount', style: const TextStyle(fontSize: 13), ), ), ), IconButton( icon: const Icon(Icons.navigate_next), tooltip: 'Next page (PDF)', onPressed: _currentPage < _pageCount - 1 ? _nextPage : null, ), const SizedBox(width: 8), // Canvas info Tooltip( message: 'Scratchpad size: ${_canvasWidth.round()} x ${_canvasHeight.round()}', child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Center( child: Text( '${_canvasWidth.round()}x${_canvasHeight.round()}', style: const TextStyle(fontSize: 11, color: Colors.grey), ), ), ), ), ], ), body: Column( children: [ // Label clarifying that the toolbar controls the scratchpad pane. Padding( padding: const EdgeInsets.only(left: 12, top: 4), child: Align( alignment: Alignment.centerLeft, child: Text( 'Scratchpad tools', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ), ), // Toolbar (applies to scratchpad only) 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, ), // Split view body Expanded( child: LayoutBuilder( builder: (context, constraints) { final totalWidth = constraints.maxWidth; final leftWidth = totalWidth * _leftPaneFraction; final rightWidth = totalWidth - leftWidth - 12; // 12px divider hit area return Row( children: [ // Left pane: PDF reference (read-only) SizedBox(width: leftWidth, child: _buildPdfPane()), // Draggable divider: 12px hit area, 4px visual strip. GestureDetector( onHorizontalDragStart: _onDividerDragStart, onHorizontalDragUpdate: (d) => _onDividerDragUpdate(d, constraints), onHorizontalDragEnd: _onDividerDragEnd, child: MouseRegion( cursor: SystemMouseCursors.resizeColumn, child: SizedBox( width: 12, child: Center( child: Container( width: 4, color: _isDraggingDivider ? Theme.of(context).colorScheme.primary : Theme.of(context).dividerColor, ), ), ), ), ), // Right pane: Infinite scratchpad SizedBox(width: rightWidth, child: _buildScratchpadPane()), ], ); }, ), ), ], ), ); } Widget _buildPdfPane() { return Stack( children: [ GestureDetector( onLongPress: () { // Long-press on PDF to create page link marker _onPdfLongPress(_currentPage + 1); }, child: SfPdfViewer.file( File(widget.filePath), controller: _pdfController, canShowScrollHead: true, canShowScrollStatus: true, onPageChanged: (PdfPageChangedDetails details) { setState(() { _currentPage = details.newPageNumber - 1; }); }, onDocumentLoaded: (PdfDocumentLoadedDetails details) { setState(() { _pageCount = details.document.pages.count; _fileName = widget.filePath.split(Platform.pathSeparator).last; }); }, ), ), // Page link markers overlay (on PDF pane, showing linked pages) if (_pageLinks.isNotEmpty) Positioned(bottom: 8, left: 8, child: _buildPageLinkChips()), ], ); } Widget _buildPageLinkChips() { return Wrap( spacing: 4, runSpacing: 4, children: _pageLinks.map((link) { return GestureDetector( onTap: () => _onPageLinkTap(link), onLongPress: () => _deletePageLink(link), child: Chip( avatar: const Icon(Icons.link, size: 14, color: Colors.white), label: Text( 'p${link.pageNumber}', style: const TextStyle(fontSize: 11, color: Colors.white), ), backgroundColor: Colors.blue.shade600, padding: EdgeInsets.zero, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.compact, ), ); }).toList(), ); } Widget _buildScratchpadPane() { return Container( color: Theme.of(context).scaffoldBackgroundColor, child: InteractiveViewer( constrained: false, minScale: 0.25, maxScale: 8.0, boundaryMargin: const EdgeInsets.all(double.infinity), child: SizedBox( width: _canvasWidth, height: _canvasHeight, child: InkCanvas( strokes: _strokes, onStrokeComplete: _onStrokeComplete, onErase: _onErase, tool: _currentTool, color: _currentColor, strokeWidth: _currentStrokeWidth, pressureCurve: PressureCurve(type: _pressureCurveType), stabilizationLevel: _stabilizationLevel, filled: _filled, interactionMode: InteractionMode.draw, ), ), ), ); } } /// A marker linking a scratchpad position to a specific PDF page. class _PageLink { final int pageNumber; final Offset position; const _PageLink({required this.pageNumber, required this.position}); }