/** * Big Picture Mode - Controller-friendly UI for Steam Deck / Console * Supports gamepad navigation, on-screen keyboard, and touch input */ const ipcRenderer = window.electronAPI; // ============================================================================= // CONFIGURATION // ============================================================================= const CONFIG = { // Navigation NAV_SOUND_ENABLED: true, HAPTIC_FEEDBACK: true, // Controller deadzone STICK_DEADZONE: 0.3, TRIGGER_DEADZONE: 0.1, // Timing REPEAT_DELAY: 500, // Initial delay before key repeat REPEAT_RATE: 100, // Rate of key repeat // Quick access sites DEFAULT_QUICK_ACCESS: [ { title: 'Google', url: 'https://www.google.com', icon: 'search' }, { title: 'YouTube', url: 'https://www.youtube.com', icon: 'play_circle' }, { title: 'Reddit', url: 'https://www.reddit.com', icon: 'forum' }, { title: 'Twitter', url: 'https://twitter.com', icon: 'tag' }, { title: 'Wikipedia', url: 'https://www.wikipedia.org', icon: 'school' }, { title: 'Netflix', url: 'https://www.netflix.com', icon: 'movie' }, ] }; // ============================================================================= // STATE // ============================================================================= const state = { currentSection: 'home', focusedElement: null, focusableElements: [], focusIndex: 0, // Gamepad gamepadConnected: false, gamepadIndex: null, lastInput: { x: 0, y: 0 }, inputRepeatTimer: null, // Virtual cursor for webview cursorEnabled: false, cursorX: 0, cursorY: 0, cursorSpeed: 15, cursorElement: null, // Sidebar visibility (for fullscreen webview) sidebarHidden: false, // OSK (On-Screen Keyboard) oskVisible: false, oskCallback: null, oskFocusIndex: 0, // Data bookmarks: [], history: [], // Mouse tracking mouseTimeout: null, // Webview for browsing currentWebview: null, webviewContentsId: null, // For native input event injection webviewStack: [] // Stack of webview instances for navigation history }; // ============================================================================= // INITIALIZATION // ============================================================================= document.addEventListener('DOMContentLoaded', () => { console.log('[BigPicture] Initializing Big Picture Mode'); initClock(); initNavigation(); initGamepadSupport(); initMouseTracking(); initKeyboardShortcuts(); initOSK(); loadData(); // Set initial focus setTimeout(() => { updateFocusableElements(); focusFirstElement(); }, 100); }); // ============================================================================= // CLOCK & DATE // ============================================================================= function initClock() { updateClock(); setInterval(updateClock, 1000); } function updateClock() { const now = new Date(); const timeEl = document.getElementById('bp-time'); const dateEl = document.getElementById('bp-date'); if (timeEl) { timeEl.textContent = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }); } if (dateEl) { dateEl.textContent = now.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' }); } // Update greeting based on time const greetingEl = document.getElementById('greeting-text'); if (greetingEl) { const hour = now.getHours(); let greeting = 'Welcome back'; if (hour < 12) greeting = 'Good morning'; else if (hour < 17) greeting = 'Good afternoon'; else greeting = 'Good evening'; greetingEl.textContent = greeting; } } // ============================================================================= // NAVIGATION // ============================================================================= function initNavigation() { // Sidebar navigation document.querySelectorAll('.nav-item').forEach(item => { item.addEventListener('click', () => { const section = item.dataset.section; if (section) { switchSection(section); } }); }); // Exit button const exitBtn = document.getElementById('exitBigPicture'); if (exitBtn) { exitBtn.addEventListener('click', exitBigPictureMode); } // Search card click const searchCard = document.querySelector('.search-card'); if (searchCard) { searchCard.addEventListener('click', () => openOSK('search')); } // Search input const searchInput = document.getElementById('bp-search'); if (searchInput) { searchInput.addEventListener('focus', () => openOSK('search')); searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { performSearch(searchInput.value); } }); } // NeBot launch const launchNebot = document.getElementById('launchNebot'); if (launchNebot) { launchNebot.addEventListener('click', () => navigateTo('browser://nebot')); } // History section buttons const clearHistoryBtn = document.getElementById('clearHistoryBtn'); if (clearHistoryBtn) { clearHistoryBtn.addEventListener('click', clearHistory); } const refreshHistoryBtn = document.getElementById('refreshHistoryBtn'); if (refreshHistoryBtn) { refreshHistoryBtn.addEventListener('click', async () => { await loadHistory(); showToast('History refreshed'); }); } // Settings cards document.querySelectorAll('.settings-card').forEach(card => { card.addEventListener('click', () => { const action = card.dataset.action; handleSettingsAction(action); }); }); } // ============================================================================= // 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) { if (sectionId === 'browse' && state.currentWebview) { // Show the preserved webview when going back to browse webviewContainer.classList.remove('hidden'); // Re-enable cursor when returning to browse enableCursor(); } else if (sectionId !== 'browse') { // Just hide the webview, don't destroy it webviewContainer.classList.add('hidden'); // Disable cursor when leaving browse disableCursor(); } } // Update nav items document.querySelectorAll('.nav-item').forEach(item => { item.classList.toggle('active', item.dataset.section === sectionId); }); // Update sections document.querySelectorAll('.bp-section').forEach(section => { section.classList.toggle('active', section.id === `section-${sectionId}`); }); state.currentSection = sectionId; // Update focusable elements for new section setTimeout(() => { updateFocusableElements(); focusFirstInContent(); }, 50); playNavSound(); } function updateFocusableElements() { // If OSK is visible, only include OSK elements if (state.oskVisible) { const oskOverlay = document.getElementById('osk-overlay'); if (oskOverlay) { state.focusableElements = [...oskOverlay.querySelectorAll('[data-focusable]')]; console.log('[BigPicture] OSK focusable elements:', state.focusableElements.length); return; } } // 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; // Get all focusable elements in sidebar and active section state.focusableElements = [ ...document.querySelectorAll('.bp-sidebar [data-focusable]'), ...activeSection.querySelectorAll('[data-focusable]'), ...document.querySelectorAll('.bp-header [data-focusable]') ]; console.log('[BigPicture] Focusable elements:', state.focusableElements.length); } function focusFirstElement() { if (state.focusableElements.length > 0) { focusElement(state.focusableElements[0]); state.focusIndex = 0; } } function focusFirstInContent() { const activeSection = document.querySelector('.bp-section.active'); if (!activeSection) return; const firstFocusable = activeSection.querySelector('[data-focusable]'); if (firstFocusable) { const index = state.focusableElements.indexOf(firstFocusable); if (index !== -1) { focusElement(firstFocusable); state.focusIndex = index; } } } function focusElement(element) { if (!element) return; // Remove focus from previous if (state.focusedElement) { state.focusedElement.classList.remove('focused'); } // Add focus to new element element.classList.add('focused'); element.focus(); state.focusedElement = element; // Scroll into view if needed element.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } function navigateFocus(direction) { if (state.focusableElements.length === 0) return; let newIndex = state.focusIndex; switch (direction) { case 'up': newIndex = findElementInDirection('up'); break; case 'down': newIndex = findElementInDirection('down'); break; case 'left': newIndex = findElementInDirection('left'); break; case 'right': newIndex = findElementInDirection('right'); break; } if (newIndex !== state.focusIndex && newIndex >= 0 && newIndex < state.focusableElements.length) { state.focusIndex = newIndex; focusElement(state.focusableElements[newIndex]); playNavSound(); } } function findElementInDirection(direction) { const current = state.focusedElement; if (!current) return 0; const currentRect = current.getBoundingClientRect(); const currentCenter = { x: currentRect.left + currentRect.width / 2, y: currentRect.top + currentRect.height / 2 }; let bestIndex = state.focusIndex; let bestDistance = Infinity; state.focusableElements.forEach((element, index) => { if (element === current) return; const rect = element.getBoundingClientRect(); const center = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; // Check if element is in the correct direction let isValid = false; switch (direction) { case 'up': isValid = center.y < currentCenter.y - 10; break; case 'down': isValid = center.y > currentCenter.y + 10; break; case 'left': isValid = center.x < currentCenter.x - 10; break; case 'right': isValid = center.x > currentCenter.x + 10; break; } if (isValid) { const distance = Math.sqrt( Math.pow(center.x - currentCenter.x, 2) + Math.pow(center.y - currentCenter.y, 2) ); if (distance < bestDistance) { bestDistance = distance; bestIndex = index; } } }); return bestIndex; } function activateFocused() { if (state.focusedElement) { state.focusedElement.click(); playSelectSound(); } } function goBack() { // If OSK is open, close it if (state.oskVisible) { closeOSK(); return; } // If viewing a website, go back in browsing history if (state.currentSection === 'browse' && state.currentWebview) { if (state.currentWebview.canGoBack()) { state.currentWebview.goBack(); return; } } // If not on home, go to home if (state.currentSection !== 'home') { switchSection('home'); // Cleanup webview const container = document.getElementById('webview-container'); if (container) { const webview = container.querySelector('webview'); if (webview) webview.remove(); container.classList.add('hidden'); } state.currentWebview = null; // Focus the home nav item const homeNav = document.querySelector('.nav-item[data-section="home"]'); if (homeNav) { const index = state.focusableElements.indexOf(homeNav); if (index !== -1) { state.focusIndex = index; focusElement(homeNav); } } } } function goForward() { // If viewing a website, go forward in browsing history if (state.currentSection === 'browse' && state.currentWebview) { if (state.currentWebview.canGoForward()) { state.currentWebview.goForward(); } } } // ============================================================================= // GAMEPAD SUPPORT // ============================================================================= function initGamepadSupport() { window.addEventListener('gamepadconnected', (e) => { console.log('[BigPicture] Gamepad connected:', e.gamepad.id); state.gamepadConnected = true; state.gamepadIndex = e.gamepad.index; showToast('Controller connected'); }); window.addEventListener('gamepaddisconnected', (e) => { console.log('[BigPicture] Gamepad disconnected'); state.gamepadConnected = false; state.gamepadIndex = null; showToast('Controller disconnected'); }); // Start polling for gamepad input requestAnimationFrame(pollGamepad); } function pollGamepad() { if (state.gamepadConnected && state.gamepadIndex !== null) { const gamepads = navigator.getGamepads(); const gamepad = gamepads[state.gamepadIndex]; if (gamepad) { handleGamepadInput(gamepad); } } requestAnimationFrame(pollGamepad); } function handleGamepadInput(gamepad) { // D-pad and left stick for navigation const leftX = gamepad.axes[0]; const leftY = gamepad.axes[1]; // D-pad buttons (indices may vary by controller) const dpadUp = gamepad.buttons[12]?.pressed; const dpadDown = gamepad.buttons[13]?.pressed; const dpadLeft = gamepad.buttons[14]?.pressed; const dpadRight = gamepad.buttons[15]?.pressed; // Analog stick with deadzone const stickUp = leftY < -CONFIG.STICK_DEADZONE; const stickDown = leftY > CONFIG.STICK_DEADZONE; const stickLeft = leftX < -CONFIG.STICK_DEADZONE; const stickRight = leftX > CONFIG.STICK_DEADZONE; // 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(); if (up && !state.lastInput.up) { navigateFocus('up'); state.lastInput.up = now; } else if (!up) { state.lastInput.up = 0; } if (down && !state.lastInput.down) { navigateFocus('down'); state.lastInput.down = now; } else if (!down) { state.lastInput.down = 0; } if (left && !state.lastInput.left) { navigateFocus('left'); state.lastInput.left = now; } else if (!left) { state.lastInput.left = 0; } if (right && !state.lastInput.right) { navigateFocus('right'); state.lastInput.right = now; } else if (!right) { state.lastInput.right = 0; } // A button (usually index 0) - Always select/activate focused menu item if (gamepad.buttons[0]?.pressed && !state.lastInput.a) { activateFocused(); state.lastInput.a = true; } else if (!gamepad.buttons[0]?.pressed) { state.lastInput.a = false; } // B button (usually index 1) - Back/Close OSK if (gamepad.buttons[1]?.pressed && !state.lastInput.b) { goBack(); state.lastInput.b = true; } else if (!gamepad.buttons[1]?.pressed) { state.lastInput.b = false; } // X button (usually index 2) - Backspace when OSK is open if (gamepad.buttons[2]?.pressed && !state.lastInput.x) { if (state.oskVisible) { backspaceOSK(); } state.lastInput.x = true; } else if (!gamepad.buttons[2]?.pressed) { state.lastInput.x = false; } // Y button (usually index 3) - Space when OSK open, otherwise open search if (gamepad.buttons[3]?.pressed && !state.lastInput.y) { if (state.oskVisible) { appendToOSK(' '); } else { openOSK('search'); } state.lastInput.y = true; } else if (!gamepad.buttons[3]?.pressed) { state.lastInput.y = false; } // LB button (usually index 4) - Go back in webview / clear OSK if (gamepad.buttons[4]?.pressed && !state.lastInput.lb) { if (state.oskVisible) { clearOSK(); } else if (state.currentSection === 'browse' && state.currentWebview) { goBack(); } state.lastInput.lb = true; } else if (!gamepad.buttons[4]?.pressed) { state.lastInput.lb = false; } // RB button (usually index 5) - Go forward in webview / submit OSK if (gamepad.buttons[5]?.pressed && !state.lastInput.rb) { if (state.oskVisible) { submitOSK(); } else if (state.currentSection === 'browse' && state.currentWebview) { goForward(); } state.lastInput.rb = true; } else if (!gamepad.buttons[5]?.pressed) { state.lastInput.rb = false; } // 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) { // 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'); } state.lastInput.start = true; } else if (!gamepad.buttons[9]?.pressed) { state.lastInput.start = false; } // Virtual cursor handling when webview is active if (state.cursorEnabled && state.currentWebview) { // Right stick for cursor movement const rightX = gamepad.axes[2] || 0; const rightY = gamepad.axes[3] || 0; // Apply deadzone const deadzone = 0.15; const moveX = Math.abs(rightX) > deadzone ? rightX : 0; const moveY = Math.abs(rightY) > deadzone ? rightY : 0; if (moveX !== 0 || moveY !== 0) { 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(); state.lastInput.rt = true; } else if (!gamepad.buttons[7]?.pressed) { state.lastInput.rt = false; } // Left trigger (index 6) - Right click if (gamepad.buttons[6]?.pressed && !state.lastInput.lt) { virtualClick(true); state.lastInput.lt = true; } else if (!gamepad.buttons[6]?.pressed) { state.lastInput.lt = false; } // Right stick click (index 11) - Toggle cursor speed if (gamepad.buttons[11]?.pressed && !state.lastInput.rs) { state.cursorSpeed = state.cursorSpeed === 15 ? 8 : (state.cursorSpeed === 8 ? 25 : 15); showToast(`Cursor speed: ${state.cursorSpeed === 8 ? 'Slow' : state.cursorSpeed === 15 ? 'Normal' : 'Fast'}`); state.lastInput.rs = true; } else if (!gamepad.buttons[11]?.pressed) { state.lastInput.rs = false; } } } // ============================================================================= // KEYBOARD SHORTCUTS // ============================================================================= function initKeyboardShortcuts() { document.addEventListener('keydown', (e) => { // Don't handle if OSK is visible and we're typing if (state.oskVisible) { handleOSKKeyboard(e); return; } switch (e.key) { case 'ArrowUp': e.preventDefault(); navigateFocus('up'); break; case 'ArrowDown': e.preventDefault(); navigateFocus('down'); break; case 'ArrowLeft': e.preventDefault(); navigateFocus('left'); break; case 'ArrowRight': e.preventDefault(); navigateFocus('right'); break; case 'Enter': case ' ': e.preventDefault(); activateFocused(); break; case 'Escape': case 'Backspace': e.preventDefault(); goBack(); break; case 'Tab': // Allow tab navigation break; } }); } // ============================================================================= // MOUSE TRACKING // ============================================================================= function initMouseTracking() { document.addEventListener('mousemove', () => { document.body.classList.add('mouse-active'); clearTimeout(state.mouseTimeout); state.mouseTimeout = setTimeout(() => { document.body.classList.remove('mouse-active'); }, 3000); }); // Add hover focus for mouse document.addEventListener('mouseover', (e) => { const focusable = e.target.closest('[data-focusable]'); if (focusable && state.focusableElements.includes(focusable)) { const index = state.focusableElements.indexOf(focusable); state.focusIndex = index; focusElement(focusable); } }); } // ============================================================================= // ON-SCREEN KEYBOARD // ============================================================================= function initOSK() { const keyboard = document.getElementById('osk-keyboard'); if (!keyboard) return; const rows = [ '1234567890', 'qwertyuiop', 'asdfghjkl', 'zxcvbnm', ]; rows.forEach(row => { const rowEl = document.createElement('div'); rowEl.className = 'osk-row'; [...row].forEach(char => { const key = document.createElement('button'); key.className = 'osk-key'; key.textContent = char; key.dataset.focusable = ''; key.tabIndex = 0; key.addEventListener('click', () => appendToOSK(char)); rowEl.appendChild(key); }); keyboard.appendChild(rowEl); }); // Special keys const specialRow = document.createElement('div'); specialRow.className = 'osk-row'; ['.', '-', '_', '@', '/', ':', '.com'].forEach(char => { const key = document.createElement('button'); key.className = 'osk-key' + (char === '.com' ? ' wide' : ''); key.textContent = char; key.dataset.focusable = ''; key.tabIndex = 0; key.addEventListener('click', () => appendToOSK(char)); specialRow.appendChild(key); }); keyboard.appendChild(specialRow); // Action buttons document.getElementById('osk-space')?.addEventListener('click', () => appendToOSK(' ')); document.getElementById('osk-backspace')?.addEventListener('click', () => backspaceOSK()); document.getElementById('osk-clear')?.addEventListener('click', () => clearOSK()); document.getElementById('osk-submit')?.addEventListener('click', () => submitOSK()); // Close button document.querySelector('.osk-close')?.addEventListener('click', () => closeOSK()); } function openOSK(mode = 'search') { const overlay = document.getElementById('osk-overlay'); const input = document.getElementById('osk-input'); const label = document.getElementById('osk-label'); if (!overlay || !input) return; state.oskVisible = true; state.oskMode = mode; overlay.classList.remove('hidden'); // Clear input input.value = ''; // Reset cursor position updateOSKCursorPosition(); // Update label based on mode if (label) { label.textContent = mode === 'search' ? 'Search or enter URL' : 'Enter text'; } // Update focusable elements to only include OSK keys updateFocusableElements(); // Focus first key setTimeout(() => { const firstKey = overlay.querySelector('.osk-key'); if (firstKey) { const index = state.focusableElements.indexOf(firstKey); if (index !== -1) { state.focusIndex = index; focusElement(firstKey); } else { firstKey.focus(); } } }, 100); } /** * Open OSK for typing into a focused input field in the webview */ function openOSKForWebview() { const overlay = document.getElementById('osk-overlay'); const input = document.getElementById('osk-input'); const label = document.getElementById('osk-label'); if (!overlay || !input) return; state.oskVisible = true; state.oskMode = 'webview'; // Special mode for webview input overlay.classList.remove('hidden'); // Clear input (could optionally preserve current input value) input.value = ''; // Reset cursor position updateOSKCursorPosition(); // Update the label to indicate webview mode if (label) { label.textContent = 'Type your text'; } // Update focusable elements to only include OSK keys updateFocusableElements(); // Focus first key setTimeout(() => { const firstKey = overlay.querySelector('.osk-key'); if (firstKey) { const index = state.focusableElements.indexOf(firstKey); if (index !== -1) { state.focusIndex = index; focusElement(firstKey); } else { firstKey.focus(); } } }, 100); showToast('📝 Type and press Submit to enter text'); } function closeOSK() { const overlay = document.getElementById('osk-overlay'); if (!overlay) return; state.oskVisible = false; overlay.classList.add('hidden'); // Return focus to main content setTimeout(() => { updateFocusableElements(); focusFirstInContent(); }, 100); } function appendToOSK(char) { const input = document.getElementById('osk-input'); if (input) { input.value += char; updateOSKCursorPosition(); } } function backspaceOSK() { const input = document.getElementById('osk-input'); if (input && input.value.length > 0) { input.value = input.value.slice(0, -1); updateOSKCursorPosition(); playNavSound(); } } function clearOSK() { const input = document.getElementById('osk-input'); if (input) { input.value = ''; updateOSKCursorPosition(); playNavSound(); } } /** * Update the blinking cursor position to follow the text */ function updateOSKCursorPosition() { const input = document.getElementById('osk-input'); const cursor = document.getElementById('osk-cursor'); const measure = document.getElementById('osk-text-measure'); if (!input || !cursor || !measure) return; // Copy the input text to the measure element measure.textContent = input.value || ''; // Get the text width + padding offset const textWidth = measure.offsetWidth; const paddingLeft = 32; // var(--bp-spacing-lg) = 32px // Position cursor right after the text cursor.style.left = `${paddingLeft + textWidth}px`; } function submitOSK() { const input = document.getElementById('osk-input'); if (!input) return; const value = input.value; if (state.oskMode === 'search') { if (!value.trim()) return; performSearch(value.trim()); } else if (state.oskMode === 'webview' && state.currentWebview) { // Send the typed text to the webview's focused input sendTextToWebview(value, true); // true = submit after setting } closeOSK(); } /** * Send typed text from OSK to the focused input field in webview */ function sendTextToWebview(text, submit = false) { if (!state.currentWebview) return; try { // Send the text value to the webview const script = submit ? ` (function() { const activeEl = document.activeElement; if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.isContentEditable)) { activeEl.value = ${JSON.stringify(text)}; activeEl.dispatchEvent(new Event('input', { bubbles: true })); activeEl.dispatchEvent(new Event('change', { bubbles: true })); // Trigger Enter key to submit setTimeout(() => { activeEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', keyCode: 13, bubbles: true })); activeEl.dispatchEvent(new KeyboardEvent('keypress', { key: 'Enter', keyCode: 13, bubbles: true })); activeEl.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', keyCode: 13, bubbles: true })); // Also try form submission const form = activeEl.closest('form'); if (form) { const submitBtn = form.querySelector('button[type="submit"], input[type="submit"], button:not([type])'); if (submitBtn) submitBtn.click(); } }, 50); } })(); ` : ` (function() { const activeEl = document.activeElement; if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.isContentEditable)) { activeEl.value = ${JSON.stringify(text)}; activeEl.dispatchEvent(new Event('input', { bubbles: true })); } })(); `; state.currentWebview.executeJavaScript(script).catch(err => { console.log('[BigPicture] Send text error:', err); }); } catch (err) { console.log('[BigPicture] sendTextToWebview error:', err); } } function handleOSKKeyboard(e) { if (e.key === 'Escape') { e.preventDefault(); closeOSK(); } else if (e.key === 'Enter') { e.preventDefault(); submitOSK(); } else if (e.key === 'Backspace') { backspaceOSK(); } else if (e.key.length === 1) { appendToOSK(e.key); } } // ============================================================================= // DATA LOADING // ============================================================================= async function loadData() { await loadBookmarks(); await loadHistory(); renderQuickAccess(); } async function loadBookmarks() { try { if (ipcRenderer && ipcRenderer.invoke) { state.bookmarks = await ipcRenderer.invoke('load-bookmarks') || []; } else { // Fallback to localStorage const stored = localStorage.getItem('bookmarks'); state.bookmarks = stored ? JSON.parse(stored) : []; } renderBookmarks(); } catch (err) { console.error('[BigPicture] Failed to load bookmarks:', err); state.bookmarks = []; } } async function loadHistory() { try { if (ipcRenderer && ipcRenderer.invoke) { state.history = await ipcRenderer.invoke('load-site-history') || []; } else { // Fallback to localStorage const stored = localStorage.getItem('siteHistory'); state.history = stored ? JSON.parse(stored) : []; } renderHistory(); renderRecentSites(); } catch (err) { console.error('[BigPicture] Failed to load history:', err); state.history = []; } } // Save a site to history async function saveToHistory(url) { if (!url || url.startsWith('browser://')) return; try { if (ipcRenderer && ipcRenderer.invoke) { await ipcRenderer.invoke('save-site-history-entry', url); // Refresh history after saving await loadHistory(); } else { // Fallback to localStorage let history = state.history; history = history.filter(item => item !== url); history.unshift(url); if (history.length > 100) history = history.slice(0, 100); localStorage.setItem('siteHistory', JSON.stringify(history)); state.history = history; renderHistory(); renderRecentSites(); } } catch (err) { console.error('[BigPicture] Failed to save history:', err); } } // Clear all browsing history async function clearHistory() { try { if (ipcRenderer && ipcRenderer.invoke) { await ipcRenderer.invoke('clear-site-history'); } else { localStorage.removeItem('siteHistory'); } state.history = []; renderHistory(); renderRecentSites(); showToast('History cleared'); } catch (err) { console.error('[BigPicture] Failed to clear history:', err); showToast('Failed to clear history'); } } // ============================================================================= // RENDERING // ============================================================================= function renderQuickAccess() { const grid = document.getElementById('quickAccessGrid'); if (!grid) return; grid.innerHTML = ''; CONFIG.DEFAULT_QUICK_ACCESS.forEach(site => { const tile = createTile(site.title, site.url, site.icon); grid.appendChild(tile); }); // Add "Add" tile const addTile = document.createElement('div'); addTile.className = 'tile add-tile'; addTile.dataset.focusable = ''; addTile.tabIndex = 0; addTile.innerHTML = `add`; addTile.addEventListener('click', () => showToast('Add bookmark coming soon')); grid.appendChild(addTile); updateFocusableElements(); } function renderBookmarks() { const grid = document.getElementById('bookmarksGrid'); if (!grid) return; grid.innerHTML = ''; if (state.bookmarks.length === 0) { grid.innerHTML = `
No bookmarks yet
Add bookmarks in desktop mode to see them here
No browsing history
Sites you visit will appear here
Start browsing to see recent sites