import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import '../../models/reader_book.dart'; import '../../service/book_server.dart'; import '../../utils/reader/book_file_helper.dart'; import '../../widgets/reader/reading_notes_panel.dart'; import 'epub_player.dart'; import 'toc_drawer.dart'; import 'progress_panel.dart'; /// 书籍阅读页面 class ReadingPage extends StatefulWidget { final ReaderBook book; const ReadingPage({super.key, required this.book}); @override State createState() => _ReadingPageState(); } class _ReadingPageState extends State { final GlobalKey _epubPlayerKey = GlobalKey(); final GlobalKey _scaffoldKey = GlobalKey(); final FocusNode _readerFocusNode = FocusNode(); static const _empty = SizedBox.shrink(); bool _toolbarOffstage = true; Widget _currentPage = const SizedBox.shrink(); bool _serverReady = false; List _toc = []; @override void initState() { super.initState(); _initServer(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); WidgetsBinding.instance.addPostFrameCallback((_) { _readerFocusNode.requestFocus(); }); } Future _initServer() async { String _ = await BookFileHelper.instance.bookFileRoot; await Server().start(preferredPort: 0); if (mounted) setState(() => _serverReady = true); } @override void dispose() { _epubPlayerKey.currentState?.saveReadingProgress(); Server().stop(); _readerFocusNode.dispose(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); super.dispose(); } void _showToolbar() => setState(() => _toolbarOffstage = false); void _hideToolbar() { setState(() { _currentPage = _empty; _toolbarOffstage = true; }); } void _toggleToolbar() => _toolbarOffstage ? _showToolbar() : _hideToolbar(); void _onTocReady(List toc) { if (mounted) setState(() => _toc = toc); } void _openTocDrawer() { _hideToolbar(); _scaffoldKey.currentState?.openDrawer(); } void _setPanel(Widget panel) { setState(() => _currentPage = panel); } // ─── 键盘快捷键 ────────────────────────────────── KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) { if (event is! KeyDownEvent) return KeyEventResult.ignored; final key = event.logicalKey; if (key == LogicalKeyboardKey.arrowRight || key == LogicalKeyboardKey.arrowDown || key == LogicalKeyboardKey.pageDown || key == LogicalKeyboardKey.space) { _epubPlayerKey.currentState?.nextPage(); return KeyEventResult.handled; } if (key == LogicalKeyboardKey.arrowLeft || key == LogicalKeyboardKey.arrowUp || key == LogicalKeyboardKey.pageUp) { _epubPlayerKey.currentState?.prevPage(); return KeyEventResult.handled; } if (key == LogicalKeyboardKey.enter) { _toggleToolbar(); return KeyEventResult.handled; } if (key == LogicalKeyboardKey.escape && !_toolbarOffstage) { _hideToolbar(); return KeyEventResult.handled; } return KeyEventResult.ignored; } // ─── 构建 ────────────────────────────────────────── @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final toolbar = Offstage( offstage: _toolbarOffstage, child: PointerInterceptor( child: Stack( children: [ Positioned.fill( child: GestureDetector( onTap: _hideToolbar, behavior: HitTestBehavior.opaque, child: Container(color: Colors.black.withAlpha(38)), ), ), Column( children: [ AppBar( backgroundColor: colors.surface.withAlpha(240), title: Text(widget.book.title, overflow: TextOverflow.ellipsis), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.pop(context), ), ), const Spacer(), BottomSheet( onClosing: () {}, enableDrag: false, builder: (context) => SafeArea( top: false, child: Container( constraints: const BoxConstraints(maxWidth: 600), child: StatefulBuilder( builder: (BuildContext context, StateSetter modalSetState) { final hasContent = !identical(_currentPage, _empty); return IntrinsicHeight( child: Column( mainAxisSize: MainAxisSize.min, children: [ if (hasContent) Expanded( child: SingleChildScrollView(child: _currentPage), ), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ IconButton( icon: const Icon(Icons.toc), tooltip: '目录', onPressed: _openTocDrawer, ), IconButton( icon: const Icon(Icons.edit_note), tooltip: '笔记', onPressed: () { modalSetState(() { _setPanel(_buildNotesPanel()); }); }, ), IconButton( icon: const Icon(Icons.data_usage), tooltip: '进度', onPressed: () { modalSetState(() { _setPanel(ProgressPanel(epubPlayerKey: _epubPlayerKey)); }); }, ), IconButton( icon: const Icon(Icons.skip_previous), tooltip: '上一章', onPressed: () { _epubPlayerKey.currentState?.prevChapter(); }, ), IconButton( icon: const Icon(Icons.skip_next), tooltip: '下一章', onPressed: () { _epubPlayerKey.currentState?.nextChapter(); }, ), ], ), ], ), ); }, ), ), ), ), ], ), ], ), ), ); return Scaffold( key: _scaffoldKey, backgroundColor: colors.surface, drawer: PointerInterceptor( child: Drawer( width: MediaQuery.of(context).size.width * 0.75, child: TocDrawer( toc: _toc, epubPlayerKey: _epubPlayerKey, ), ), ), body: _serverReady ? Focus( focusNode: _readerFocusNode, onKeyEvent: _handleKeyEvent, autofocus: true, child: Stack( children: [ EpubPlayer( key: _epubPlayerKey, book: widget.book, showOrHideToolbar: _toggleToolbar, onTocReady: _onTocReady, ), toolbar, ], ), ) : const Center(child: CircularProgressIndicator()), ); } Widget _buildNotesPanel() { return ReadingNotesPanel( bookId: widget.book.id, onNavigate: (cfi) { _hideToolbar(); _epubPlayerKey.currentState?.goToCfi(cfi); }, ); } }