diff --git a/opennow-stable/src/renderer/src/App.tsx b/opennow-stable/src/renderer/src/App.tsx index 77a78403b..ac093ce6d 100644 --- a/opennow-stable/src/renderer/src/App.tsx +++ b/opennow-stable/src/renderer/src/App.tsx @@ -4606,6 +4606,7 @@ export function App(): JSX.Element { gameTitle={streamingGame?.title ?? t("app.labels.game")} gameCover={streamingGame?.imageUrl} platformStore={streamingStore ?? undefined} + controllerMode={effectiveControllerMode} status={loadingStatus} queuePosition={queuePosition} adState={effectiveAdState} @@ -4750,17 +4751,19 @@ export function App(): JSX.Element { onExitComplete={handleSettingsExitComplete} > {settingsMounted && ( - + )} {logoutConfirmModal} diff --git a/opennow-stable/src/renderer/src/components/HomePage.tsx b/opennow-stable/src/renderer/src/components/HomePage.tsx index f52ed9871..a31ab3786 100644 --- a/opennow-stable/src/renderer/src/components/HomePage.tsx +++ b/opennow-stable/src/renderer/src/components/HomePage.tsx @@ -461,7 +461,8 @@ export const HomePage = memo(function HomePage({ if (pressed & controllerButton.west) setControllerSearchOpen(true); if (pressed & controllerButton.leftShoulder) onPreviousControllerPage?.(); if (pressed & controllerButton.rightShoulder) onNextControllerPage?.(); - if (pressed & controllerButton.menu) cycleControllerVariant(); + if (pressed & controllerButton.menu) onNextControllerPage?.(); + if (pressed & controllerButton.north) cycleControllerVariant(); if (pressed & controllerButton.up) focusControllerTile(rowIndex - 1, columnIndex); if (pressed & controllerButton.down) focusControllerTile(rowIndex + 1, columnIndex); if (pressed & controllerButton.left) focusControllerTile(rowIndex, columnIndex - 1); @@ -650,11 +651,27 @@ export const HomePage = memo(function HomePage({ ))} - ))} +
+ {t("settings.backTo")} + + {returnPageLabel} +
{/* ── Content ───────────────────────────────────────── */} @@ -4680,7 +4737,8 @@ export function SettingsPage({ settings, regions, onSettingChange, codecResults, )} - + + {nativeStreamerEnablePromptVisible && (
{ + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + event.preventDefault(); + onCancel(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [onCancel]); + + useEffect(() => { + if (!controllerMode) return; + + let frameId: number | null = null; + let previousButtons = 0; + + const readButtons = (): number => { + const pad = navigator.getGamepads?.().find((gamepad): gamepad is Gamepad => Boolean(gamepad)); + return readControllerGamepadButtons(pad); + }; + + const handleFrame = () => { + const buttons = readButtons(); + const pressed = buttons & ~previousButtons; + if (pressed & controllerButton.west) { + onCancel(); + } + previousButtons = buttons; + frameId = window.requestAnimationFrame(handleFrame); + }; + + frameId = window.requestAnimationFrame(handleFrame); + return () => { + if (frameId !== null) { + window.cancelAnimationFrame(frameId); + } + }; + }, [controllerMode, onCancel]); + return (
@@ -238,7 +285,11 @@ export function StreamLoading({ )}
diff --git a/opennow-stable/src/renderer/src/styles.css b/opennow-stable/src/renderer/src/styles.css index f8b27e059..c3627f5fd 100644 --- a/opennow-stable/src/renderer/src/styles.css +++ b/opennow-stable/src/renderer/src/styles.css @@ -2069,8 +2069,7 @@ body, .controller-store-hero, .controller-store-sections, -.controller-store-empty, -.controller-store-page .controller-bottom-hints { +.controller-store-empty { position: relative; z-index: 1; } @@ -2131,6 +2130,7 @@ body, flex-direction: column; gap: clamp(24px, 3.1vh, 46px); margin-top: clamp(28px, 3.4vh, 58px); + margin-bottom: clamp(48px, 9vh, 114px); } .controller-store-section { @@ -3573,9 +3573,8 @@ body, display: flex; align-items: center; gap: clamp(18px, 2.4vw, 74px); - pointer-events: none; + pointer-events: auto; } - .controller-hint { display: inline-flex; align-items: center; @@ -3584,6 +3583,20 @@ body, font-size: clamp(0.76rem, 0.68vw, 1.05rem); font-weight: 850; white-space: nowrap; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 999px; + padding: 6px 10px; + cursor: pointer; + transition: transform 0.12s ease, background 0.12s ease, border-color 0.12s ease; +} +.controller-hint:hover { + background: rgba(255, 255, 255, 0.12); + border-color: rgba(255, 255, 255, 0.24); +} +.controller-hint:active { + transform: scale(0.97); + background: rgba(255, 255, 255, 0.18); } .controller-hint--more { @@ -3621,7 +3634,7 @@ body, .controller-search-overlay { position: fixed; inset: 0; - z-index: 2190; + z-index: 1010; display: flex; align-items: center; justify-content: center; @@ -3683,6 +3696,16 @@ body, box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.2); } +.controller-search-actions { + display: flex; + justify-content: flex-end; + margin-top: 18px; +} + +.controller-search-actions .controller-secondary-action { + min-width: 132px; +} + .controller-store-filter-options { display: flex; flex-direction: column; @@ -4558,6 +4581,30 @@ button.game-card-store-chip.owned.active:hover { gap: 14px; } +.settings-sidebar-footer { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: auto; + padding-top: 14px; + border-top: 1px solid var(--panel-border); +} + +.settings-sidebar-footer .settings-footer-kicker { + color: var(--ink-muted); + font-size: 0.64rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.settings-footer-destination { + color: var(--ink-soft); + font-size: 0.78rem; + font-weight: 650; + line-height: 1.3; +} + .settings-nav-group { display: flex; flex-direction: column; @@ -6513,7 +6560,66 @@ button.game-card-store-chip.owned.active:hover { /* Footer */ .settings-footer { display: flex; - justify-content: flex-end; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 16px 22px 20px; + border-top: 1px solid var(--panel-border); + flex-shrink: 0; +} + +.settings-footer-context { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.settings-footer-kicker { + color: var(--ink-muted); + font-size: 0.66rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.settings-footer-context strong { + color: var(--ink); + font-size: 0.95rem; + font-weight: 700; +} + +.settings-back-btn { + width: 100%; + display: inline-flex; + align-items: center; + justify-content: flex-start; + gap: 8px; + min-height: 38px; + padding: 0 14px; + background: color-mix(in srgb, var(--accent) 8%, var(--bg-c)); + border: 1px solid color-mix(in srgb, var(--accent) 18%, var(--panel-border-solid)); + border-radius: var(--r-sm); + color: var(--ink); + font-size: 0.92rem; + font-weight: 700; + font-family: inherit; + cursor: pointer; + transition: transform var(--t-normal), background var(--t-normal), border-color var(--t-normal), color var(--t-normal); +} + +.settings-back-btn svg { + flex-shrink: 0; +} + +.settings-back-btn:hover { + transform: translateY(-1px); + background: color-mix(in srgb, var(--accent) 12%, var(--card-hover)); + border-color: color-mix(in srgb, var(--accent) 24%, var(--panel-border-solid)); +} + +.settings-back-btn:active { + transform: translateY(0); } .settings-save-btn { diff --git a/opennow-stable/src/renderer/src/utils/controllerGamepad.ts b/opennow-stable/src/renderer/src/utils/controllerGamepad.ts index f7ac376b6..44c91cb59 100644 --- a/opennow-stable/src/renderer/src/utils/controllerGamepad.ts +++ b/opennow-stable/src/renderer/src/utils/controllerGamepad.ts @@ -14,17 +14,19 @@ export const controllerButton = { export function readControllerGamepadButtons(pad: Gamepad | undefined): number { if (!pad) return 0; - let buttons = 0; - if (pad.buttons[0]?.pressed) buttons |= controllerButton.south; - if (pad.buttons[1]?.pressed) buttons |= controllerButton.east; - if (pad.buttons[2]?.pressed) buttons |= controllerButton.west; - if (pad.buttons[3]?.pressed) buttons |= controllerButton.north; - if (pad.buttons[4]?.pressed) buttons |= controllerButton.leftShoulder; - if (pad.buttons[5]?.pressed) buttons |= controllerButton.rightShoulder; - if (pad.buttons[12]?.pressed || (pad.axes[1] ?? 0) < -0.65) buttons |= controllerButton.up; - if (pad.buttons[13]?.pressed || (pad.axes[1] ?? 0) > 0.65) buttons |= controllerButton.down; - if (pad.buttons[14]?.pressed || (pad.axes[0] ?? 0) < -0.65) buttons |= controllerButton.left; - if (pad.buttons[15]?.pressed || (pad.axes[0] ?? 0) > 0.65) buttons |= controllerButton.right; - if (pad.buttons[9]?.pressed || pad.buttons[16]?.pressed) buttons |= controllerButton.menu; - return buttons; + const buttons = pad.buttons ?? []; + const axes = pad.axes ?? []; + let result = 0; + if (buttons[0]?.pressed) result |= controllerButton.south; + if (buttons[1]?.pressed) result |= controllerButton.east; + if (buttons[2]?.pressed) result |= controllerButton.west; + if (buttons[3]?.pressed) result |= controllerButton.north; + if (buttons[4]?.pressed) result |= controllerButton.leftShoulder; + if (buttons[5]?.pressed) result |= controllerButton.rightShoulder; + if (buttons[12]?.pressed || (axes[1] ?? 0) < -0.65) result |= controllerButton.up; + if (buttons[13]?.pressed || (axes[1] ?? 0) > 0.65) result |= controllerButton.down; + if (buttons[14]?.pressed || (axes[0] ?? 0) < -0.65) result |= controllerButton.left; + if (buttons[15]?.pressed || (axes[0] ?? 0) > 0.65) result |= controllerButton.right; + if (buttons[9]?.pressed || buttons[16]?.pressed) result |= controllerButton.menu; + return result; }