import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.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; @override void initState() { super.initState(); FullScreen.addListener(this); FullScreen.setFullScreen(widget.config.fullscreen); _documentLoaded = _loadPdf(); } @override void 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() { 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) { // Paint mode only works in single page mode ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Paint mode is only available in single page mode'), duration: Duration(seconds: 2), ), ); return; } setState(() => _isPaintMode = !_isPaintMode); } void _toggleTwoPageMode() { setState(() { widget.config.twoPageMode = !widget.config.twoPageMode; _storageService.writeConfig(widget.config); if (widget.config.twoPageMode && _isPaintMode) { _isPaintMode = false; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Paint mode disabled in two-page mode'), duration: Duration(seconds: 2), ), ); } }); } // --------------------------------------------------------------------------- // 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), ), ], ); } 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, ), ), ); } }