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'; import 'package:sheetless/core/models/config.dart'; import 'package:sheetless/core/models/sheet.dart'; 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 'widgets/pdf_page_display.dart'; import 'widgets/touch_navigation_layer.dart'; /// Page for viewing and annotating PDF sheet music. class SheetViewerPage extends StatefulWidget { final Sheet sheet; final ApiClient apiClient; final Config config; const SheetViewerPage({ super.key, required this.sheet, required this.apiClient, required this.config, }); @override State createState() => _SheetViewerPageState(); } class _SheetViewerPageState extends State with FullScreenListener { final _log = Logger('SheetViewerPage'); final _storageService = StorageService(); PdfDocument? _document; late Future _documentLoaded; int _currentPage = 1; int _totalPages = 1; bool _isPaintMode = false; late DrawingController _drawingController; @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) ); FullScreen.addListener(this); FullScreen.setFullScreen(widget.config.fullscreen); _documentLoaded = _loadPdf(); } @override void dispose() { _drawingController.dispose(); FullScreen.removeListener(this); _document?.dispose(); super.dispose(); } // --------------------------------------------------------------------------- // PDF Loading // --------------------------------------------------------------------------- Future _loadPdf() async { if (kIsWeb) { // Web: load directly into memory final data = await widget.apiClient.fetchPdfData(widget.sheet.uuid); _document = await PdfDocument.openData(data); } else { // Native: use file cache final file = await widget.apiClient.getPdfFileCached(widget.sheet.uuid); _document = await PdfDocument.openFile(file.path); } setState(() { _totalPages = _document!.pages.length; }); return true; } // --------------------------------------------------------------------------- // Fullscreen // --------------------------------------------------------------------------- @override void onFullScreenChanged(bool enabled, SystemUiMode? systemUiMode) { setState(() { widget.config.fullscreen = enabled; _storageService.writeConfig(widget.config); }); } void _toggleFullscreen() { if (_isPaintMode) { _showSnackBar('Cannot enter fullscreen while in paint mode'); return; } FullScreen.setFullScreen(!widget.config.fullscreen); } // --------------------------------------------------------------------------- // Navigation // --------------------------------------------------------------------------- void _turnPage(int delta) { setState(() { _currentPage = (_currentPage + delta).clamp(1, _totalPages); }); } // --------------------------------------------------------------------------- // Mode Switching // --------------------------------------------------------------------------- void _togglePaintMode() { if (widget.config.twoPageMode) { _showSnackBar('Paint mode is only available in single page mode'); return; } setState(() => _isPaintMode = !_isPaintMode); } void _toggleTwoPageMode() { if (_isPaintMode) { _showSnackBar('Cannot enter two-page mode while painting'); return; } setState(() { widget.config.twoPageMode = !widget.config.twoPageMode; _storageService.writeConfig(widget.config); }); } // --------------------------------------------------------------------------- // UI // --------------------------------------------------------------------------- @override Widget build(BuildContext context) { return PedalShortcuts( onPageForward: () => _turnPage(1), onPageBackward: () => _turnPage(-1), child: Scaffold(appBar: _buildAppBar(), body: _buildBody()), ); } PreferredSizeWidget? _buildAppBar() { // Hide app bar in fullscreen when document is loaded if (widget.config.fullscreen && _document != null) { return null; } return AppBar( title: Text(widget.sheet.name), actions: [ IconButton( icon: Icon( widget.config.fullscreen ? Icons.fullscreen_exit : Icons.fullscreen, ), tooltip: widget.config.fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen', onPressed: _toggleFullscreen, ), IconButton( icon: Icon(_isPaintMode ? Icons.brush : Icons.brush_outlined), tooltip: 'Toggle 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', onPressed: _toggleTwoPageMode, ), ], ); } Widget _buildBody() { return FutureBuilder( future: _documentLoaded, builder: (context, snapshot) { if (snapshot.hasError) { _log.warning('Error loading PDF', snapshot.error); return _buildError(snapshot.error.toString()); } if (snapshot.hasData && _document != null) { return _buildPdfViewer(); } return const Center(child: CircularProgressIndicator()); }, ); } Widget _buildPdfViewer() { final pageDisplay = PdfPageDisplay( document: _document!, numPages: _totalPages, currentPageNumber: _currentPage, config: widget.config, ); 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, ), ), // Show paint mode layer when active Visibility( visible: _isPaintMode, child: PaintModeLayer( pageDisplay: pageDisplay, drawingController: _drawingController, ), ), ], ); } Widget _buildError(String message) { return Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Text( message, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(color: Colors.red), textAlign: TextAlign.center, ), ), ); } void _showSnackBar(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(message), duration: Duration(seconds: 2)), ); } }