From cbaed8cdfb788cebeec7f843b57ce641167d42a7 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:32:19 +0200 Subject: [PATCH] fix(livetv): tune current guide programs on tv close #1298 --- lib/screens/livetv/tabs/guide_tab.dart | 96 +++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/lib/screens/livetv/tabs/guide_tab.dart b/lib/screens/livetv/tabs/guide_tab.dart index 30bb188e..b6c83cd0 100644 --- a/lib/screens/livetv/tabs/guide_tab.dart +++ b/lib/screens/livetv/tabs/guide_tab.dart @@ -23,6 +23,7 @@ import '../../../utils/live_tv_grouping.dart'; import '../../../utils/live_tv_matching.dart'; import '../../../utils/media_image_helper.dart'; import '../../../utils/live_tv_player_navigation.dart'; +import '../../../utils/platform_detector.dart'; import '../../../widgets/app_icon.dart'; import '../../../widgets/app_menu.dart'; import '../../../widgets/clickable_cursor.dart'; @@ -76,6 +77,7 @@ class GuideTabState extends State with MountedSetStateMixin { static const _sourceHeaderRowHeight = 40.0; static const _timeHeaderHeight = 40.0; static const _minutesPerSlot = 30; + static const _longPressDuration = Duration(milliseconds: 500); List _programs = []; Set _scheduledRecordingKeys = const {}; @@ -91,6 +93,7 @@ class GuideTabState extends State with MountedSetStateMixin { bool _syncingScroll = false; Timer? _timeIndicatorTimer; + Timer? _programSelectLongPressTimer; final _dayPickerKey = GlobalKey(); // Focus state @@ -103,6 +106,7 @@ class GuideTabState extends State with MountedSetStateMixin { final ValueNotifier _hasFocusNotifier = ValueNotifier(false); LiveTvProgram? _focusedProgram; bool _pendingFocus = false; + bool _isProgramSelectKeyDown = false; /// Focus into the guide content (called from tab bar navigation or initial load). void focusContent() { @@ -163,6 +167,7 @@ class GuideTabState extends State with MountedSetStateMixin { @override void dispose() { + _programSelectLongPressTimer?.cancel(); _guideFocusNode.dispose(); _gridVerticalController.dispose(); _gridHorizontalController.removeListener(_syncGridToHeader); @@ -177,10 +182,16 @@ class GuideTabState extends State with MountedSetStateMixin { void _handleGuideFocusChange(bool hasFocus) { if (_hasFocus == hasFocus) return; + if (!hasFocus) _resetProgramSelectLongPressState(); _hasFocus = hasFocus; _hasFocusNotifier.value = hasFocus; } + void _resetProgramSelectLongPressState() { + _programSelectLongPressTimer?.cancel(); + _isProgramSelectKeyDown = false; + } + void _syncGridToHeader() { if (_syncingScroll) return; _syncingScroll = true; @@ -466,9 +477,80 @@ class GuideTabState extends State with MountedSetStateMixin { await tuneAndNavigateToLiveTv(context, multiServer: multiServer, channel: channel, channels: widget.channels); } + void _activateProgram(LiveTvChannel channel, LiveTvProgram program) { + if (PlatformDetector.isTV() && program.isCurrentlyAiring) { + _tuneChannel(channel); + return; + } + + _showProgramDetails(channel, program); + } + + ({LiveTvChannel channel, LiveTvProgram program})? _focusedProgramTarget() { + if (_focusZone != _GuideZone.grid || _gridColumn != 1) return null; + final program = _focusedProgram; + if (program == null) return null; + if (_gridChannelIndex < 0 || _gridChannelIndex >= widget.channels.length) return null; + + return (channel: widget.channels[_gridChannelIndex], program: program); + } + + KeyEventResult _handleFocusedProgramSelectKey(KeyEvent event) { + if (!event.logicalKey.isSelectKey) return KeyEventResult.ignored; + final target = _focusedProgramTarget(); + if (target == null) return KeyEventResult.ignored; + + if (event is KeyDownEvent) { + if (!_isProgramSelectKeyDown) { + _isProgramSelectKeyDown = true; + _programSelectLongPressTimer?.cancel(); + _programSelectLongPressTimer = Timer(_longPressDuration, () { + if (!mounted || !_isProgramSelectKeyDown) return; + SelectKeyUpSuppressor.suppressSelectUntilKeyUp(); + _resetProgramSelectLongPressState(); + _showProgramDetails(target.channel, target.program); + }); + } + return KeyEventResult.handled; + } + + if (event is KeyRepeatEvent) { + return KeyEventResult.handled; + } + + if (event is KeyUpEvent) { + final timerWasActive = _programSelectLongPressTimer?.isActive ?? false; + _programSelectLongPressTimer?.cancel(); + if (timerWasActive && _isProgramSelectKeyDown) { + _activateProgram(target.channel, target.program); + } + _isProgramSelectKeyDown = false; + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + } + + KeyEventResult _handleFocusedProgramContextMenuKey(KeyEvent event) { + if (!event.logicalKey.isContextMenuKey || !event.isActionable) return KeyEventResult.ignored; + final target = _focusedProgramTarget(); + if (target == null) return KeyEventResult.ignored; + + _resetProgramSelectLongPressState(); + _showProgramDetails(target.channel, target.program); + return KeyEventResult.handled; + } + KeyEventResult _handleKeyEvent(FocusNode _, KeyEvent event) { final key = event.logicalKey; + if (SelectKeyUpSuppressor.consumeIfSuppressed(event)) { + if (event is KeyUpEvent && key.isSelectKey) { + _resetProgramSelectLongPressState(); + } + return KeyEventResult.handled; + } + // Back key if (key.isBackKey) { if (BackKeyUpSuppressor.consumeIfSuppressed(event)) { @@ -486,6 +568,14 @@ class GuideTabState extends State with MountedSetStateMixin { return handleBackKeyAction(event, () => widget.onBack?.call()); } + if (PlatformDetector.isTV()) { + final selectResult = _handleFocusedProgramSelectKey(event); + if (selectResult != KeyEventResult.ignored) return selectResult; + } + + final contextMenuResult = _handleFocusedProgramContextMenuKey(event); + if (contextMenuResult != KeyEventResult.ignored) return contextMenuResult; + if (!event.isActionable) return KeyEventResult.ignored; return _focusZone == _GuideZone.timeNav ? _handleTimeNavKey(key) : _handleGridKey(key); @@ -595,7 +685,7 @@ class GuideTabState extends State with MountedSetStateMixin { if (_gridColumn == 0) { _tuneChannel(channel); } else if (_focusedProgram != null) { - _showProgramDetails(channel, _focusedProgram!); + _activateProgram(channel, _focusedProgram!); } } return KeyEventResult.handled; @@ -1272,7 +1362,9 @@ class GuideTabState extends State with MountedSetStateMixin { child: InkWell( mouseCursor: SystemMouseCursors.click, canRequestFocus: false, - onTap: () => _showProgramDetails(channel, program), + onTap: () => _activateProgram(channel, program), + onLongPress: () => _showProgramDetails(channel, program), + onSecondaryTap: () => _showProgramDetails(channel, program), child: Container( decoration: BoxDecoration( border: Border(