From d7545b69241f7fbc4056b8a380ec981e78c5b226 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Tue, 9 Jun 2026 00:40:17 +0530 Subject: [PATCH] feat: add keyboard controls for playback and seeking actions --- .../nuvio/app/features/player/PlayerEngine.kt | 3 ++ .../PlayerScreenRuntimeGestureActions.kt | 22 +++++++++---- .../features/player/PlayerScreenRuntimeUi.kt | 14 ++++++++- .../player/desktop/NativePlayerController.kt | 12 +++++-- .../resources/player-ui/controls.html | 31 ++++++++++++++----- 5 files changed, 65 insertions(+), 17 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerEngine.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerEngine.kt index 439e11fb4..a21f26696 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerEngine.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerEngine.kt @@ -28,8 +28,11 @@ enum class PlayerControlsAction { RevealLockedOverlay, Back, TogglePlayback, + KeyboardTogglePlayback, SeekBack, + KeyboardSeekBack, SeekForward, + KeyboardSeekForward, ResizeMode, Speed, Subtitles, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeGestureActions.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeGestureActions.kt index d7e422097..afabfa862 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeGestureActions.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeGestureActions.kt @@ -162,9 +162,11 @@ internal fun PlayerScreenRuntime.togglePlayback() { controlsVisible = true } -internal fun PlayerScreenRuntime.prepareTogglePlaybackForNativeFallback() { +internal fun PlayerScreenRuntime.prepareTogglePlaybackForNativeFallback(revealControls: Boolean = true) { shouldPlay = !playbackSnapshot.isPlaying - controlsVisible = true + if (revealControls) { + controlsVisible = true + } } internal fun PlayerScreenRuntime.seekBy(offsetMs: Long) { @@ -172,13 +174,21 @@ internal fun PlayerScreenRuntime.seekBy(offsetMs: Long) { applySeekByControlFeedback(offsetMs) } -internal fun PlayerScreenRuntime.prepareSeekByForNativeFallback(offsetMs: Long) { - applySeekByControlFeedback(offsetMs) +internal fun PlayerScreenRuntime.prepareSeekByForNativeFallback( + offsetMs: Long, + revealControls: Boolean = true, +) { + applySeekByControlFeedback(offsetMs, revealControls) } -private fun PlayerScreenRuntime.applySeekByControlFeedback(offsetMs: Long) { +private fun PlayerScreenRuntime.applySeekByControlFeedback( + offsetMs: Long, + revealControls: Boolean = true, +) { scheduleProgressSyncAfterSeek() - controlsVisible = true + if (revealControls) { + controlsVisible = true + } when { offsetMs > 0L -> showSeekFeedback(PlayerSeekDirection.Forward, offsetMs) offsetMs < 0L -> showSeekFeedback(PlayerSeekDirection.Backward, abs(offsetMs)) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeUi.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeUi.kt index 81fa38563..751c7d7b0 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeUi.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeUi.kt @@ -226,7 +226,7 @@ internal fun PlayerScreenRuntime.RenderPlayerRuntimeUi() { isLoading = playbackSnapshot.isLoading, isLocked = playerControlsLocked, lockedOverlayVisible = lockedOverlayVisible, - controlsVisible = (controlsVisible || showParentalGuide) && !playerControlsLocked, + controlsVisible = controlsVisible && !playerControlsLocked, parentalWarnings = parentalWarnings, showParentalGuide = showParentalGuide, showSubmitIntro = isSeries && @@ -508,14 +508,26 @@ private fun PlayerScreenRuntime.handlePlayerControlsAction(action: PlayerControl prepareTogglePlaybackForNativeFallback() return false } + PlayerControlsAction.KeyboardTogglePlayback -> { + prepareTogglePlaybackForNativeFallback(revealControls = false) + return false + } PlayerControlsAction.SeekBack -> { prepareSeekByForNativeFallback(-10_000L) return false } + PlayerControlsAction.KeyboardSeekBack -> { + prepareSeekByForNativeFallback(-10_000L, revealControls = false) + return false + } PlayerControlsAction.SeekForward -> { prepareSeekByForNativeFallback(10_000L) return false } + PlayerControlsAction.KeyboardSeekForward -> { + prepareSeekByForNativeFallback(10_000L, revealControls = false) + return false + } PlayerControlsAction.ResizeMode -> cycleResizeMode() PlayerControlsAction.Speed -> cyclePlaybackSpeed() PlayerControlsAction.Subtitles -> { diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/desktop/NativePlayerController.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/desktop/NativePlayerController.kt index ef672b406..c27344e36 100644 --- a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/desktop/NativePlayerController.kt +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/desktop/NativePlayerController.kt @@ -164,7 +164,8 @@ internal class NativePlayerController( private fun handleFallbackAction(action: PlayerControlsAction) { when (action) { - PlayerControlsAction.TogglePlayback -> { + PlayerControlsAction.TogglePlayback, + PlayerControlsAction.KeyboardTogglePlayback -> { val current = handle if (current == 0L) return val isEnded = NativePlayerBridge.isEnded(current) @@ -176,8 +177,10 @@ internal class NativePlayerController( NativePlayerBridge.setPaused(current, !isPaused) } } - PlayerControlsAction.SeekBack -> fallbackSeekBy(-10_000L) - PlayerControlsAction.SeekForward -> fallbackSeekBy(10_000L) + PlayerControlsAction.SeekBack, + PlayerControlsAction.KeyboardSeekBack -> fallbackSeekBy(-10_000L) + PlayerControlsAction.SeekForward, + PlayerControlsAction.KeyboardSeekForward -> fallbackSeekBy(10_000L) PlayerControlsAction.DoubleTapSeekBack -> fallbackSeekBy(-10_000L) PlayerControlsAction.DoubleTapSeekForward -> fallbackSeekBy(10_000L) PlayerControlsAction.Speed -> cycleFallbackSpeed() @@ -432,8 +435,11 @@ private fun String.toPlayerControlsAction(): PlayerControlsAction? = "revealLockedOverlay" -> PlayerControlsAction.RevealLockedOverlay "back" -> PlayerControlsAction.Back "toggle" -> PlayerControlsAction.TogglePlayback + "keyboardToggle" -> PlayerControlsAction.KeyboardTogglePlayback "seekBack" -> PlayerControlsAction.SeekBack + "keyboardSeekBack" -> PlayerControlsAction.KeyboardSeekBack "seekForward" -> PlayerControlsAction.SeekForward + "keyboardSeekForward" -> PlayerControlsAction.KeyboardSeekForward "resize" -> PlayerControlsAction.ResizeMode "speed" -> PlayerControlsAction.Speed "subtitles" -> PlayerControlsAction.Subtitles diff --git a/composeApp/src/desktopMain/resources/player-ui/controls.html b/composeApp/src/desktopMain/resources/player-ui/controls.html index 1e511be28..3c82fb865 100644 --- a/composeApp/src/desktopMain/resources/player-ui/controls.html +++ b/composeApp/src/desktopMain/resources/player-ui/controls.html @@ -166,7 +166,6 @@ .player.chrome-hidden .top-gradient, .player.chrome-hidden .bottom-gradient, .player.chrome-hidden .metadata, - .player.chrome-hidden .parental-guide, .player.chrome-hidden .header-actions, .player.chrome-hidden .center-controls, .player.chrome-hidden .progress, @@ -226,7 +225,7 @@ .header { position: absolute; - top: calc(var(--vp) / 4 + env(safe-area-inset-top)); + top: calc(var(--slider-bottom) + env(safe-area-inset-top)); left: var(--hp); right: var(--hp); display: flex; @@ -310,7 +309,7 @@ .parental-guide { position: absolute; - top: calc(var(--vp) / 4 + env(safe-area-inset-top)); + top: calc(var(--slider-bottom) + env(safe-area-inset-top)); left: var(--hp); z-index: 5; display: flex; @@ -3090,18 +3089,28 @@ switch (event.code) { case "Space": case "KeyK": - return "toggle"; + return "keyboardToggle"; case "ArrowLeft": case "KeyJ": - return "seekBack"; + return "keyboardSeekBack"; case "ArrowRight": case "KeyL": - return "seekForward"; + return "keyboardSeekForward"; default: return ""; } }; + const toggleChrome = () => { + if (state.isLocked) { + send("revealLockedOverlay", 0); + return; + } + state = { ...state, controlsVisible: !state.controlsVisible }; + renderChrome(); + send("toggleChrome", 0); + }; + const clearPressedButton = () => { if (!pressedButton) return; pressedButton.classList.remove("is-pressed"); @@ -3158,6 +3167,9 @@ openingOverlay.addEventListener("click", event => { event.stopPropagation(); + if (!event.target.closest("button,input")) { + toggleChrome(); + } }); modalElements.forEach(modal => { @@ -3420,7 +3432,7 @@ if (event.target.closest("button,input")) return; window.clearTimeout(tapTimer); tapTimer = window.setTimeout(() => { - send(state.isLocked ? "revealLockedOverlay" : "toggleChrome"); + toggleChrome(); }, 220); }); @@ -3439,6 +3451,11 @@ focusShortcutRoot(); return; } + if (event.key === "Escape") { + event.preventDefault(); + send("back", 0); + return; + } if (activeModal || isTextEntryTarget(event.target)) { return; }