fix(livetv): tune current guide programs on tv

close #1298
This commit is contained in:
edde746
2026-06-11 07:15:31 +02:00
parent eac0e26696
commit cbaed8cdfb
+94 -2
View File
@@ -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<GuideTab> with MountedSetStateMixin {
static const _sourceHeaderRowHeight = 40.0;
static const _timeHeaderHeight = 40.0;
static const _minutesPerSlot = 30;
static const _longPressDuration = Duration(milliseconds: 500);
List<LiveTvProgram> _programs = [];
Set<String> _scheduledRecordingKeys = const {};
@@ -91,6 +93,7 @@ class GuideTabState extends State<GuideTab> with MountedSetStateMixin {
bool _syncingScroll = false;
Timer? _timeIndicatorTimer;
Timer? _programSelectLongPressTimer;
final _dayPickerKey = GlobalKey();
// Focus state
@@ -103,6 +106,7 @@ class GuideTabState extends State<GuideTab> with MountedSetStateMixin {
final ValueNotifier<bool> _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<GuideTab> with MountedSetStateMixin {
@override
void dispose() {
_programSelectLongPressTimer?.cancel();
_guideFocusNode.dispose();
_gridVerticalController.dispose();
_gridHorizontalController.removeListener(_syncGridToHeader);
@@ -177,10 +182,16 @@ class GuideTabState extends State<GuideTab> 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<GuideTab> 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<GuideTab> 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<GuideTab> 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<GuideTab> 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(