diff --git a/lib/player_screen.dart b/lib/player_screen.dart index dcebf72..b0cd061 100644 --- a/lib/player_screen.dart +++ b/lib/player_screen.dart @@ -107,6 +107,8 @@ class _PlayerScreenState extends State bool _fullscreen = false; bool _automaticFullscreenSuppressed = false; bool _panelOpen = false; + bool _panelBackGuard = false; + Timer? _panelBackGuardTimer; int _mobileTab = 0; bool _autoAdvance = true; bool? _systemFullscreen; @@ -182,15 +184,13 @@ class _PlayerScreenState extends State widget.store.addListener(_accessChanged); _player = widget.playerFactory?.call() ?? - (Platform.isAndroid - ? LunaExoPlayer() - : Player( - configuration: const PlayerConfiguration( - bufferSize: 32 * 1024 * 1024, - logLevel: MPVLogLevel.error, - ), - )); - _video = widget.videoBuilder == null && !Platform.isAndroid + Player( + configuration: const PlayerConfiguration( + bufferSize: 32 * 1024 * 1024, + logLevel: MPVLogLevel.error, + ), + ); + _video = widget.videoBuilder == null ? VideoController( _player, configuration: VideoControllerConfiguration( @@ -1014,8 +1014,14 @@ class _PlayerScreenState extends State await platform.setProperty('vd-lavc-skiploopfilter', 'all'); await platform.setProperty('vd-lavc-skipidct', 'all'); await platform.setProperty('vd-lavc-threads', '2'); - await platform.setProperty('demuxer-max-bytes', '${4 * 1024 * 1024}'); - await platform.setProperty('demuxer-max-back-bytes', '${1 * 1024 * 1024}'); + await platform.setProperty( + 'demuxer-max-bytes', + '${4 * 1024 * 1024}', + ); + await platform.setProperty( + 'demuxer-max-back-bytes', + '${1 * 1024 * 1024}', + ); await platform.setProperty('demuxer-readahead-secs', '5'); } else { await platform.setProperty('hwdec', 'auto-safe'); @@ -1045,7 +1051,9 @@ class _PlayerScreenState extends State _plan = plan; installed = true; _acceptErrors = true; - DiaryService.add('[Play] 调用 _player.open: url=${plan.url}, headers=${plan.headers.keys.toList()}'); + DiaryService.add( + '[Play] 调用 _player.open: url=${plan.url}, headers=${plan.headers.keys.toList()}', + ); await _player.open( Media( plan.url, @@ -1379,6 +1387,7 @@ class _PlayerScreenState extends State ), ); } finally { + _guardPanelBack(); if (mounted && !_closed) setState(() => _panelOpen = false); } if (index != null && mounted && !_closed && index != _index) { @@ -1420,6 +1429,7 @@ class _PlayerScreenState extends State ), ); } finally { + _guardPanelBack(); if (mounted && !_closed) setState(() => _panelOpen = false); } if (selection == null || !mounted || _closed) return; @@ -1439,17 +1449,35 @@ class _PlayerScreenState extends State } } + void _guardPanelBack() { + _panelBackGuard = true; + _panelBackGuardTimer?.cancel(); + _panelBackGuardTimer = Timer(const Duration(milliseconds: 500), () { + _panelBackGuard = false; + }); + } + void _back() { + if (_panelBackGuard) return; + if (_panelOpen) { + _guardPanelBack(); + if (!(ModalRoute.of(context)?.isCurrent ?? true)) { + Navigator.of(context).pop(); + } + return; + } + if (!(ModalRoute.of(context)?.isCurrent ?? true)) return; if (_showFullscreen && !_television) { _rotate(); } else { - Navigator.of(context).maybePop(); + Navigator.of(context).pop(); } } @override void dispose() { _closed = true; + _panelBackGuardTimer?.cancel(); final enhancementClosed = _enhancement.close(); LanController.current?.detachPlayback(_lanIdentity); widget.handoff?.fail('接收端已退出播放'); @@ -1525,11 +1553,9 @@ class _PlayerScreenState extends State final fullscreen = _showFullscreen; final pictureInPicture = _pictureInPictureVisible; return PopScope( - canPop: _television || !fullscreen, + canPop: !_television && !fullscreen, onPopInvokedWithResult: (didPop, result) { - if (!didPop && fullscreen && !_television) { - _rotate(); - } + if (!didPop) _back(); }, child: CallbackShortcuts( bindings: { @@ -1737,8 +1763,14 @@ class _PlayerScreenState extends State Video( controller: _video!, fit: BoxFit.contain, - controls: (_) => layeredControls, + controls: _television + ? NoVideoControls + : (_) => layeredControls, ), + if (_television && + widget.videoBuilder == null && + _player is! LunaExoPlayer) + layeredControls, if (_loading && !hideOverlayForPictureInPicture) ColoredBox( color: Colors.black.withValues(alpha: .78), @@ -1768,7 +1800,8 @@ class _PlayerScreenState extends State crossAxisAlignment: WrapCrossAlignment.center, children: [ FilledButton.tonalIcon( - onPressed: () => DiaryService.showDiaryDialog(context), + onPressed: () => + DiaryService.showDiaryDialog(context), icon: const Icon(Icons.receipt_long_rounded), label: const Text('查看播放日记'), ), @@ -1779,8 +1812,8 @@ class _PlayerScreenState extends State label: const Text('改为在线播放'), ) else if (!_localFailure && - !widget.localOnly && - widget.repository.supportsSourceManagement) + !widget.localOnly && + widget.repository.supportsSourceManagement) SourceDiagnosticsButton( repository: widget.repository, store: widget.store, diff --git a/lib/remote_widgets.dart b/lib/remote_widgets.dart index 161c181..b1705ae 100644 --- a/lib/remote_widgets.dart +++ b/lib/remote_widgets.dart @@ -369,13 +369,25 @@ class _RemoteGridState extends State { crossAxisSpacing: widget.spacing, mainAxisSpacing: widget.spacing, ), - delegate: SliverChildBuilderDelegate( - (context, index) => - widget.itemBuilder(context, index, _node(index), () { - _focused = widget.itemKeys[index]; - }), - childCount: widget.itemKeys.length, - ), + delegate: SliverChildBuilderDelegate((context, index) { + final node = _node(index); + final key = widget.itemKeys[index]; + if (_target == key) { + final generation = _generation; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && + generation == _generation && + _target == key && + node.context != null) { + node.requestFocus(); + _target = null; + } + }); + } + return widget.itemBuilder(context, index, node, () { + _focused = key; + }); + }, childCount: widget.itemKeys.length), ), ), if (widget.footer != null) SliverToBoxAdapter(child: widget.footer), diff --git a/lib/television_controls.dart b/lib/television_controls.dart index 5988542..8b6878c 100644 --- a/lib/television_controls.dart +++ b/lib/television_controls.dart @@ -310,6 +310,7 @@ class _TelevisionControlsState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( + color: Colors.white, fontSize: 22, fontWeight: FontWeight.w600, ), diff --git a/pubspec.lock b/pubspec.lock index fc9fe71..b6175b1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -89,6 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" dbus: dependency: transitive description: @@ -247,6 +255,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + html: + dependency: transitive + description: + name: html + sha256: "43b67b8f43321ab066817dfac5619596c98bb1b61624e77203bb4351785f9699" + url: "https://pub.dev" + source: hosted + version: "0.15.7" http: dependency: transitive description: @@ -864,6 +880,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.3" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: "07ae1e03f2d4c8e2e9a75ee678f90acbc4506d4458a9c628512d119a1eee5754" + url: "https://pub.dev" + source: hosted + version: "2.14.1" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: d27054dea34d748a44f06d433a57005d2aac69944485b0abbaed3303dbcfa3f1 + url: "https://pub.dev" + source: hosted + version: "2.12.2" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: "2fc56e537324a19eb649fca991aa308ac961b5406292876fa657b062de461fd8" + url: "https://pub.dev" + source: hosted + version: "2.12.0" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: "92c0fbabe20c788e71fd10d26cea998d0d253282e65d145aed0818731cf593ce" + url: "https://pub.dev" + source: hosted + version: "6.9.0" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" + url: "https://pub.dev" + source: hosted + version: "2.4.0" vm_service: dependency: transitive description: diff --git a/test/television_player_test.dart b/test/television_player_test.dart index 69025a2..b7b8fbe 100644 --- a/test/television_player_test.dart +++ b/test/television_player_test.dart @@ -123,6 +123,30 @@ void main() { }, ); + testWidgets('TV panel back does not also exit playback', (tester) async { + final repository = RouteRepository(); + final player = ScriptedPlayer(); + await mount(tester, repository, player); + for (final panel in ['tv-episodes', 'tv-settings']) { + await tester.tap(find.byKey(ValueKey(panel))); + await settle(tester); + expect(find.byType(AlertDialog), findsOneWidget); + await tester.sendKeyEvent( + LogicalKeyboardKey.goBack, + physicalKey: PhysicalKeyboardKey.escape, + ); + await settle(tester); + await tester.binding.handlePopRoute(); + await settle(tester); + expect(find.byType(PlayerScreen), findsOneWidget); + expect(player.disposed, isFalse); + await tester.pump(const Duration(milliseconds: 600)); + await settle(tester); + expect(find.byType(AlertDialog), findsNothing); + } + await leave(tester, repository, player); + }); + testWidgets( 'TV selection restores the current episode and panels keep focus during playback', (tester) async { @@ -149,6 +173,7 @@ void main() { ); expect(repository.active.length, 1); await press(tester, LogicalKeyboardKey.arrowRight); + await press(tester, LogicalKeyboardKey.arrowRight); await press(tester, LogicalKeyboardKey.select); expect(find.text('倍速'), findsOneWidget); await press(tester, LogicalKeyboardKey.arrowRight); @@ -174,6 +199,8 @@ void main() { FocusManager.instance.primaryFocus?.debugLabel, 'tv-player-settings', ); + await tester.pump(const Duration(milliseconds: 600)); + await settle(tester); await leave(tester, repository, player); }, );