From 3d538a09f9d5667f985faaf7a39ad57d33fe2bfa Mon Sep 17 00:00:00 2001 From: Andrew Zambazos Date: Sat, 27 Dec 2025 23:27:03 +1300 Subject: [PATCH] Add sidebar toggle and fullscreen webview support Implements a sidebar hidden state and toggle functionality for fullscreen webview mode, including new CSS for sidebar transitions and a menu hint. Updates gamepad input handling to allow toggling the sidebar and scrolling in webview mode, and restricts focusable elements to the sidebar and header when browsing. Improves user experience when navigating and interacting with web content in fullscreen. --- renderer/bigpicture.css | 51 ++++++++++++++++++ renderer/bigpicture.js | 112 ++++++++++++++++++++++++++++++++-------- 2 files changed, 142 insertions(+), 21 deletions(-) diff --git a/renderer/bigpicture.css b/renderer/bigpicture.css index 3d7ee62..9099553 100644 --- a/renderer/bigpicture.css +++ b/renderer/bigpicture.css @@ -1426,3 +1426,54 @@ body.fullscreen .bp-main { font-size: 0.7rem; color: var(--bp-text); } + +/* ============================================================================= + SIDEBAR HIDDEN STATE (Fullscreen webview mode) + ============================================================================= */ + +.bp-sidebar.sidebar-hidden { + transform: translateX(-100%); + opacity: 0; + pointer-events: none; + width: 0; + min-width: 0; + padding: 0; + border: none; + transition: all 0.3s ease-out; +} + +.bp-sidebar { + transition: all 0.3s ease-out; +} + +.bp-content.fullscreen { + margin-left: 0; + width: 100%; +} + +.bp-header.sidebar-hidden .header-left { + opacity: 0.5; + transform: scale(0.9); + transition: all 0.3s ease; +} + +/* Show sidebar toggle hint when in fullscreen */ +.bp-content.fullscreen::before { + content: '☰ Menu'; + position: fixed; + bottom: 20px; + left: 20px; + background: rgba(20, 20, 31, 0.8); + border: 1px solid var(--bp-border); + border-radius: var(--bp-radius-sm); + padding: 8px 12px; + font-size: 0.75rem; + color: var(--bp-text-muted); + z-index: 100; + opacity: 0.6; + transition: opacity 0.2s ease; +} + +.bp-content.fullscreen:hover::before { + opacity: 1; +} diff --git a/renderer/bigpicture.js b/renderer/bigpicture.js index 4a7ae7e..7af8891 100644 --- a/renderer/bigpicture.js +++ b/renderer/bigpicture.js @@ -56,6 +56,9 @@ const state = { cursorSpeed: 15, cursorElement: null, + // Sidebar visibility (for fullscreen webview) + sidebarHidden: false, + // OSK (On-Screen Keyboard) oskVisible: false, oskCallback: null, @@ -190,9 +193,44 @@ function initNavigation() { }); } +// ============================================================================= +// SIDEBAR TOGGLE (for fullscreen webview) +// ============================================================================= + +function toggleSidebar() { + state.sidebarHidden = !state.sidebarHidden; + + const sidebar = document.querySelector('.bp-sidebar'); + const content = document.querySelector('.bp-content'); + const header = document.querySelector('.bp-header'); + + if (state.sidebarHidden) { + sidebar?.classList.add('sidebar-hidden'); + content?.classList.add('fullscreen'); + header?.classList.add('sidebar-hidden'); + showToast('📺 Fullscreen mode | Press ☰ to show sidebar'); + } else { + sidebar?.classList.remove('sidebar-hidden'); + content?.classList.remove('fullscreen'); + header?.classList.remove('sidebar-hidden'); + showToast('Sidebar restored'); + } +} + +function showSidebar() { + if (state.sidebarHidden) { + toggleSidebar(); + } +} + function switchSection(sectionId) { console.log('[BigPicture] Switching to section:', sectionId); + // Restore sidebar when leaving browse section + if (sectionId !== 'browse' && state.sidebarHidden) { + showSidebar(); + } + // Handle webview container visibility (preserve state instead of destroying) const webviewContainer = document.getElementById('webview-container'); if (webviewContainer) { @@ -241,6 +279,16 @@ function updateFocusableElements() { } } + // When in webview mode, only sidebar navigation is available + if (state.cursorEnabled && state.currentWebview) { + state.focusableElements = [ + ...document.querySelectorAll('.bp-sidebar [data-focusable]'), + ...document.querySelectorAll('.bp-header [data-focusable]') + ]; + console.log('[BigPicture] Webview mode - sidebar focusable elements:', state.focusableElements.length); + return; + } + const activeSection = document.querySelector('.bp-section.active'); if (!activeSection) return; @@ -472,11 +520,15 @@ function handleGamepadInput(gamepad) { const stickLeft = leftX < -CONFIG.STICK_DEADZONE; const stickRight = leftX > CONFIG.STICK_DEADZONE; - // Combine inputs - const up = dpadUp || stickUp; - const down = dpadDown || stickDown; - const left = dpadLeft || stickLeft; - const right = dpadRight || stickRight; + // When cursor is enabled (viewing a webpage), only D-Pad navigates sidebar + // Left stick is ignored for UI navigation in webview mode + const inWebviewMode = state.cursorEnabled && state.currentWebview; + + // Combine inputs - but only use D-Pad when in webview mode + const up = inWebviewMode ? dpadUp : (dpadUp || stickUp); + const down = inWebviewMode ? dpadDown : (dpadDown || stickDown); + const left = inWebviewMode ? dpadLeft : (dpadLeft || stickLeft); + const right = inWebviewMode ? dpadRight : (dpadRight || stickRight); // Navigation with repeat prevention const now = Date.now(); @@ -509,7 +561,7 @@ function handleGamepadInput(gamepad) { state.lastInput.right = 0; } - // A button (usually index 0) - Select/Type letter + // A button (usually index 0) - Always select/activate focused menu item if (gamepad.buttons[0]?.pressed && !state.lastInput.a) { activateFocused(); state.lastInput.a = true; @@ -567,10 +619,22 @@ function handleGamepadInput(gamepad) { state.lastInput.rb = false; } - // Start button (usually index 9) - Menu + // Back/Select button (usually index 8) - Toggle sidebar when in webview + if (gamepad.buttons[8]?.pressed && !state.lastInput.select) { + if (state.currentSection === 'browse' && state.currentWebview) { + toggleSidebar(); + } + state.lastInput.select = true; + } else if (!gamepad.buttons[8]?.pressed) { + state.lastInput.select = false; + } + + // Start button (usually index 9) - Menu / Toggle sidebar when viewing webpage if (gamepad.buttons[9]?.pressed && !state.lastInput.start) { - // Toggle to settings - if (state.currentSection !== 'settings') { + // If viewing a webpage, toggle sidebar instead of going to settings + if (state.currentSection === 'browse' && state.currentWebview) { + toggleSidebar(); + } else if (state.currentSection !== 'settings') { switchSection('settings'); } else { switchSection('home'); @@ -595,6 +659,15 @@ function handleGamepadInput(gamepad) { moveCursor(moveX * state.cursorSpeed, moveY * state.cursorSpeed); } + // Left stick for scrolling in webview mode + const scrollDeadzone = 0.25; + const scrollX = Math.abs(leftX) > scrollDeadzone ? leftX : 0; + const scrollY = Math.abs(leftY) > scrollDeadzone ? leftY : 0; + + if (scrollX !== 0 || scrollY !== 0) { + scrollWebview(scrollY * 20, scrollX * 20); + } + // Right trigger (index 7) - Left click if (gamepad.buttons[7]?.pressed && !state.lastInput.rt) { virtualClick(); @@ -619,15 +692,6 @@ function handleGamepadInput(gamepad) { } else if (!gamepad.buttons[11]?.pressed) { state.lastInput.rs = false; } - - // Left stick click (index 10) - Scroll mode toggle could go here - if (gamepad.buttons[10]?.pressed && !state.lastInput.ls) { - // Scroll the page - scrollWebview(leftY * 100); - state.lastInput.ls = true; - } else if (!gamepad.buttons[10]?.pressed) { - state.lastInput.ls = false; - } } } @@ -1222,8 +1286,11 @@ function enableCursor() { updateCursorPosition(); state.cursorElement.classList.add('active'); + // Update focusable elements to only include sidebar when in webview mode + updateFocusableElements(); + // Show cursor hint - showToast('🎮 Right stick: Move cursor | RT: Click | LT: Right-click | B: Back'); + showToast('🎮 Right stick: Move cursor | RT: Click | Left stick: Scroll | B: Back'); } function disableCursor() { @@ -1231,6 +1298,9 @@ function disableCursor() { if (state.cursorElement) { state.cursorElement.classList.remove('active'); } + + // Restore full focusable elements + updateFocusableElements(); } function moveCursor(dx, dy) { @@ -1328,11 +1398,11 @@ function virtualClick(rightClick = false) { } } -function scrollWebview(amount) { +function scrollWebview(amountY, amountX = 0) { if (!state.currentWebview) return; try { - state.currentWebview.executeJavaScript(`window.scrollBy(0, ${amount})`); + state.currentWebview.executeJavaScript(`window.scrollBy(${amountX}, ${amountY})`); } catch (err) { console.log('[BigPicture] Scroll error:', err); }