diff --git a/documentation/BIG_PICTURE_MODE.md b/documentation/BIG_PICTURE_MODE.md
new file mode 100644
index 0000000..6367469
--- /dev/null
+++ b/documentation/BIG_PICTURE_MODE.md
@@ -0,0 +1,120 @@
+# Big Picture Mode - Steam Deck & Controller UI
+
+Nebula Browser includes a **Big Picture Mode** - a controller-friendly, console-style interface designed for Steam Deck, handheld devices, and living room setups.
+
+## Features
+
+### 🎮 Controller Support
+- **Full gamepad navigation** - Use D-pad or left stick to navigate
+- **Button mapping**:
+ - **A / Cross** - Select/Activate
+ - **B / Circle** - Go Back
+ - **Y / Triangle** - Quick Search
+ - **Start** - Toggle Settings
+- **Audio feedback** for navigation
+
+### 📱 Optimized for Steam Deck
+- **1280x800 native resolution** support
+- Automatic detection of Steam Deck screens
+- Large touch-friendly UI elements
+- Fullscreen immersive experience
+
+### 🎨 Modern Console-Style UI
+- Inspired by Steam OS Big Picture and Xbox Dashboard
+- Smooth animations and transitions
+- Glowing focus indicators
+- Dark theme optimized for OLED displays
+
+### ⌨️ On-Screen Keyboard
+- Built-in virtual keyboard for controller input
+- URL and search input support
+- Special keys for common domains (.com, .org, etc.)
+
+## How to Access
+
+### From Desktop Mode
+1. **Menu Button (☰)** → Click **"🎮 Big Picture Mode"**
+2. **Settings** → **General** → Click **"Launch Big Picture Mode"**
+
+### Keyboard Shortcut
+- Press `F11` while in Big Picture Mode to toggle fullscreen
+
+### Automatic Detection
+If Nebula detects a Steam Deck-sized display (1280x800), it will suggest Big Picture Mode in settings.
+
+## Navigation Sections
+
+| Section | Description |
+|---------|-------------|
+| **Home** | Quick access sites, search, and recent browsing |
+| **Bookmarks** | Your saved websites in a tile grid |
+| **History** | Recently visited sites |
+| **Downloads** | Downloaded files |
+| **NeBot AI** | Launch the AI assistant |
+| **Settings** | Theme, privacy, and display options |
+
+## Controller Button Reference
+
+| Button | Action |
+|--------|--------|
+| D-Pad / Left Stick | Navigate between elements |
+| A / Cross | Select focused element |
+| B / Circle | Go back / Close menu |
+| Y / Triangle | Open search (on-screen keyboard) |
+| Start | Open/Close settings |
+| LB/RB | Scroll horizontally |
+
+## Exiting Big Picture Mode
+
+- Press the **Exit** button in the top-right corner
+- Go to **Settings** → **Desktop Mode**
+- Press `Escape` key multiple times
+
+## Technical Details
+
+### Files
+- `renderer/bigpicture.html` - Main HTML structure
+- `renderer/bigpicture.css` - Console-optimized styles
+- `renderer/bigpicture.js` - Controller handling and navigation
+
+### Screen Detection
+Big Picture Mode is suggested for displays matching:
+- Steam Deck resolution: 1280×800
+- Screens smaller than 1366px width
+- 16:10 or 16:9 aspect ratios
+
+### API
+```javascript
+// Check if Big Picture Mode is recommended
+const suggested = await window.bigPictureAPI.isSuggested();
+
+// Get screen information
+const info = await window.bigPictureAPI.getScreenInfo();
+
+// Launch Big Picture Mode
+await window.bigPictureAPI.launch();
+
+// Exit Big Picture Mode
+await window.bigPictureAPI.exit();
+```
+
+## Customization
+
+The Big Picture Mode respects your theme settings. Colors are applied from your selected theme:
+- Background colors
+- Accent and primary colors
+- Text colors
+
+## Known Limitations
+
+- Some complex web forms may be difficult to navigate with controller only
+- Video players use native controls
+- Right-click context menus require mouse/touch
+
+## Future Improvements
+
+- [ ] Rumble/haptic feedback for compatible controllers
+- [ ] Voice search integration with NeBot
+- [ ] Picture-in-picture mode for videos
+- [ ] Game overlay mode
+- [ ] Custom controller mappings
diff --git a/main.js b/main.js
index 739d0b5..54841ee 100644
--- a/main.js
+++ b/main.js
@@ -68,6 +68,143 @@ ipcMain.removeHandler('window-minimize');
ipcMain.removeHandler('window-maximize');
ipcMain.removeHandler('window-close');
+// =============================================================================
+// BIG PICTURE MODE - Steam Deck / Console UI
+// =============================================================================
+
+// Steam Deck screen dimensions: 1280x800
+const STEAM_DECK_WIDTH = 1280;
+const STEAM_DECK_HEIGHT = 800;
+const HANDHELD_THRESHOLD = 1366; // Consider screens smaller than this as "handheld"
+
+let bigPictureWindow = null;
+
+/**
+ * Check if the current display is likely a Steam Deck or similar handheld
+ */
+function isSteamDeckDisplay() {
+ const primaryDisplay = screen.getPrimaryDisplay();
+ const { width, height } = primaryDisplay.size;
+
+ // Check for Steam Deck resolution or similar small screens
+ const isSteamDeckRes = width === STEAM_DECK_WIDTH && height === STEAM_DECK_HEIGHT;
+ const isSmallScreen = width <= HANDHELD_THRESHOLD;
+
+ // Also check for certain aspect ratios common in handhelds (16:10, 16:9)
+ const aspectRatio = width / height;
+ const isHandheldAspect = aspectRatio >= 1.5 && aspectRatio <= 1.8;
+
+ return isSteamDeckRes || (isSmallScreen && isHandheldAspect);
+}
+
+/**
+ * Get screen info for UI decisions
+ */
+function getScreenInfo() {
+ const primaryDisplay = screen.getPrimaryDisplay();
+ const { width, height } = primaryDisplay.size;
+ const { scaleFactor } = primaryDisplay;
+
+ return {
+ width,
+ height,
+ scaleFactor,
+ isSteamDeck: width === STEAM_DECK_WIDTH && height === STEAM_DECK_HEIGHT,
+ isSmallScreen: width <= HANDHELD_THRESHOLD,
+ aspectRatio: width / height,
+ suggestBigPicture: isSteamDeckDisplay()
+ };
+}
+
+/**
+ * Create Big Picture Mode window
+ */
+function createBigPictureWindow() {
+ if (bigPictureWindow && !bigPictureWindow.isDestroyed()) {
+ bigPictureWindow.focus();
+ return bigPictureWindow;
+ }
+
+ const primaryDisplay = screen.getPrimaryDisplay();
+ const { width, height } = primaryDisplay.workAreaSize;
+
+ bigPictureWindow = new BrowserWindow({
+ width: width,
+ height: height,
+ fullscreen: true,
+ frame: false,
+ show: false,
+ backgroundColor: '#0a0a0f',
+ webPreferences: {
+ preload: path.join(__dirname, 'preload.js'),
+ nodeIntegration: false,
+ contextIsolation: true,
+ webviewTag: true,
+ spellcheck: false,
+ webSecurity: true,
+ },
+ icon: process.platform === 'darwin'
+ ? path.join(__dirname, 'assets/images/Logos/Nebula-Favicon.icns')
+ : path.join(__dirname, 'assets/images/Logos/Nebula-favicon.png'),
+ title: 'Nebula - Big Picture Mode'
+ });
+
+ bigPictureWindow.loadFile('renderer/bigpicture.html');
+
+ bigPictureWindow.once('ready-to-show', () => {
+ bigPictureWindow.show();
+ console.log('[BigPicture] Window ready');
+ });
+
+ bigPictureWindow.on('closed', () => {
+ bigPictureWindow = null;
+ console.log('[BigPicture] Window closed');
+ });
+
+ return bigPictureWindow;
+}
+
+/**
+ * Exit Big Picture Mode and return to desktop UI
+ */
+function exitBigPictureMode() {
+ if (bigPictureWindow && !bigPictureWindow.isDestroyed()) {
+ bigPictureWindow.close();
+ bigPictureWindow = null;
+ }
+
+ // Ensure main window exists and is focused
+ const windows = BrowserWindow.getAllWindows();
+ const mainWindow = windows.find(w => w !== bigPictureWindow);
+ if (mainWindow) {
+ mainWindow.focus();
+ } else if (windows.length === 0) {
+ createWindow();
+ }
+}
+
+// IPC handlers for Big Picture Mode
+ipcMain.handle('get-screen-info', () => getScreenInfo());
+
+ipcMain.handle('launch-bigpicture', () => {
+ createBigPictureWindow();
+ return { success: true };
+});
+
+ipcMain.handle('exit-bigpicture', () => {
+ exitBigPictureMode();
+ return { success: true };
+});
+
+ipcMain.handle('is-bigpicture-suggested', () => {
+ return isSteamDeckDisplay();
+});
+
+ipcMain.on('exit-bigpicture', () => {
+ exitBigPictureMode();
+});
+
+// =============================================================================
function createWindow(startUrl) {
diff --git a/preload.js b/preload.js
index d717f1e..39b7e78 100644
--- a/preload.js
+++ b/preload.js
@@ -116,6 +116,20 @@ contextBridge.exposeInMainWorld('aboutAPI', {
getInfo: () => ipcRenderer.invoke('get-about-info')
});
+// Big Picture Mode API - Steam Deck / Console UI
+contextBridge.exposeInMainWorld('bigPictureAPI', {
+ // Get screen info to determine if Big Picture Mode is recommended
+ getScreenInfo: () => ipcRenderer.invoke('get-screen-info'),
+ // Check if device is likely a Steam Deck or handheld
+ isSuggested: () => ipcRenderer.invoke('is-bigpicture-suggested'),
+ // Launch Big Picture Mode
+ launch: () => ipcRenderer.invoke('launch-bigpicture'),
+ // Exit Big Picture Mode
+ exit: () => ipcRenderer.invoke('exit-bigpicture'),
+ // Navigate to URL (from Big Picture Mode)
+ navigate: (url) => ipcRenderer.send('bigpicture-navigate', url)
+});
+
// Relay context-menu commands from main to active renderer context (open new tabs etc.)
ipcRenderer.on('context-menu-command', (event, payload) => {
window.dispatchEvent(new CustomEvent('nebula-context-command', { detail: payload }));
diff --git a/renderer/bigpicture.css b/renderer/bigpicture.css
new file mode 100644
index 0000000..3d7ee62
--- /dev/null
+++ b/renderer/bigpicture.css
@@ -0,0 +1,1428 @@
+/* Big Picture Mode - Steam Deck / Console-style UI */
+/* Optimized for 1280x800 (Steam Deck) and controller navigation */
+
+@font-face {
+ font-family: 'InterVariable';
+ src: url('../assets/images/fonts/InterVariable.ttf') format('truetype');
+ font-weight: 100 900;
+ font-display: swap;
+}
+
+/* CSS Variables for theming */
+:root {
+ --bp-bg: #0a0a0f;
+ --bp-surface: #14141f;
+ --bp-surface-hover: #1e1e2d;
+ --bp-surface-active: #28283d;
+ --bp-primary: #7B2EFF;
+ --bp-primary-glow: rgba(123, 46, 255, 0.4);
+ --bp-accent: #00C6FF;
+ --bp-accent-glow: rgba(0, 198, 255, 0.3);
+ --bp-text: #ffffff;
+ --bp-text-muted: #8888a0;
+ --bp-text-dim: #555570;
+ --bp-border: #2a2a40;
+ --bp-success: #4ade80;
+ --bp-warning: #fbbf24;
+ --bp-danger: #ef4444;
+
+ /* Focus ring for controller navigation */
+ --bp-focus-ring: 0 0 0 3px var(--bp-primary), 0 0 30px var(--bp-primary-glow);
+ --bp-focus-ring-accent: 0 0 0 3px var(--bp-accent), 0 0 30px var(--bp-accent-glow);
+
+ /* Spacing scaled for touch/controller */
+ --bp-spacing-xs: 8px;
+ --bp-spacing-sm: 12px;
+ --bp-spacing-md: 20px;
+ --bp-spacing-lg: 32px;
+ --bp-spacing-xl: 48px;
+
+ /* Border radius */
+ --bp-radius-sm: 8px;
+ --bp-radius-md: 12px;
+ --bp-radius-lg: 16px;
+ --bp-radius-xl: 24px;
+
+ /* Animation timing */
+ --bp-transition-fast: 150ms ease;
+ --bp-transition-normal: 250ms ease;
+ --bp-transition-slow: 400ms ease;
+}
+
+/* Base reset */
+*, *::before, *::after {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+html, body {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ font-family: 'InterVariable', 'Segoe UI', system-ui, -apple-system, sans-serif;
+ font-size: 18px; /* Larger base for readability on TV/handheld */
+ line-height: 1.5;
+ color: var(--bp-text);
+ background: var(--bp-bg);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ cursor: none; /* Hide cursor for controller-only mode */
+}
+
+/* Show cursor when mouse moves */
+body.mouse-active {
+ cursor: auto;
+}
+
+/* Main container */
+.bp-container {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ overflow: hidden;
+}
+
+/* Animated background */
+.bp-background {
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ pointer-events: none;
+ overflow: hidden;
+}
+
+.bg-gradient {
+ position: absolute;
+ inset: 0;
+ background:
+ radial-gradient(ellipse 120% 80% at 20% 10%, rgba(123, 46, 255, 0.15) 0%, transparent 50%),
+ radial-gradient(ellipse 100% 60% at 80% 90%, rgba(0, 198, 255, 0.1) 0%, transparent 40%),
+ linear-gradient(180deg, var(--bp-bg) 0%, #0d0d15 100%);
+}
+
+.bg-particles {
+ position: absolute;
+ inset: 0;
+ background-image:
+ radial-gradient(2px 2px at 20% 30%, rgba(255,255,255,0.15), transparent),
+ radial-gradient(2px 2px at 40% 70%, rgba(255,255,255,0.1), transparent),
+ radial-gradient(1px 1px at 60% 20%, rgba(255,255,255,0.12), transparent),
+ radial-gradient(2px 2px at 80% 50%, rgba(255,255,255,0.08), transparent);
+ animation: particles-drift 60s linear infinite;
+}
+
+@keyframes particles-drift {
+ 0% { transform: translateY(0); }
+ 100% { transform: translateY(-100px); }
+}
+
+.bg-glow {
+ position: absolute;
+ width: 600px;
+ height: 600px;
+ border-radius: 50%;
+ background: radial-gradient(circle, var(--bp-primary-glow) 0%, transparent 70%);
+ filter: blur(80px);
+ opacity: 0.5;
+ animation: glow-pulse 8s ease-in-out infinite alternate;
+ top: -200px;
+ left: -100px;
+}
+
+@keyframes glow-pulse {
+ 0% { transform: scale(1); opacity: 0.3; }
+ 100% { transform: scale(1.2); opacity: 0.5; }
+}
+
+/* Header */
+.bp-header {
+ position: relative;
+ z-index: 100;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: var(--bp-spacing-sm) var(--bp-spacing-lg);
+ background: linear-gradient(180deg, rgba(10,10,15,0.95) 0%, rgba(10,10,15,0.8) 100%);
+ backdrop-filter: blur(20px);
+ border-bottom: 1px solid var(--bp-border);
+}
+
+.header-left {
+ display: flex;
+ align-items: center;
+ gap: var(--bp-spacing-sm);
+}
+
+.bp-logo {
+ width: 40px;
+ height: 40px;
+ filter: drop-shadow(0 0 10px var(--bp-primary-glow));
+}
+
+.bp-title {
+ font-size: 1.4rem;
+ font-weight: 700;
+ background: linear-gradient(135deg, var(--bp-text) 0%, var(--bp-accent) 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.header-center {
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.clock-widget {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2px;
+}
+
+.clock-widget .time {
+ font-size: 1.5rem;
+ font-weight: 600;
+ letter-spacing: 1px;
+}
+
+.clock-widget .date {
+ font-size: 0.8rem;
+ color: var(--bp-text-muted);
+ text-transform: uppercase;
+ letter-spacing: 2px;
+}
+
+.header-right {
+ display: flex;
+ align-items: center;
+ gap: var(--bp-spacing-md);
+}
+
+.status-icons {
+ display: flex;
+ align-items: center;
+ gap: var(--bp-spacing-sm);
+}
+
+.status-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ border-radius: var(--bp-radius-sm);
+ background: var(--bp-surface);
+ color: var(--bp-text-muted);
+}
+
+.status-icon .material-symbols-outlined {
+ font-size: 20px;
+}
+
+.bp-exit-btn {
+ display: flex;
+ align-items: center;
+ gap: var(--bp-spacing-xs);
+ padding: var(--bp-spacing-xs) var(--bp-spacing-md);
+ background: var(--bp-surface);
+ border: 1px solid var(--bp-border);
+ border-radius: var(--bp-radius-md);
+ color: var(--bp-text);
+ font-size: 0.9rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all var(--bp-transition-fast);
+}
+
+.bp-exit-btn:hover,
+.bp-exit-btn:focus {
+ background: var(--bp-surface-hover);
+ border-color: var(--bp-primary);
+ box-shadow: var(--bp-focus-ring);
+ outline: none;
+}
+
+.bp-exit-btn .material-symbols-outlined {
+ font-size: 18px;
+}
+
+/* Main layout */
+.bp-main {
+ flex: 1;
+ display: flex;
+ overflow: hidden;
+ position: relative;
+ z-index: 1;
+}
+
+/* Sidebar navigation */
+.bp-sidebar {
+ width: 220px;
+ min-width: 220px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: var(--bp-spacing-md);
+ background: rgba(20, 20, 31, 0.6);
+ backdrop-filter: blur(10px);
+ border-right: 1px solid var(--bp-border);
+}
+
+.nav-items {
+ display: flex;
+ flex-direction: column;
+ gap: var(--bp-spacing-xs);
+}
+
+.nav-item {
+ display: flex;
+ align-items: center;
+ gap: var(--bp-spacing-md);
+ padding: var(--bp-spacing-md) var(--bp-spacing-md);
+ background: transparent;
+ border: 2px solid transparent;
+ border-radius: var(--bp-radius-lg);
+ color: var(--bp-text-muted);
+ font-size: 1rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all var(--bp-transition-fast);
+ text-align: left;
+ width: 100%;
+}
+
+.nav-item .material-symbols-outlined {
+ font-size: 28px;
+ transition: transform var(--bp-transition-fast);
+}
+
+.nav-item:hover {
+ background: var(--bp-surface);
+ color: var(--bp-text);
+}
+
+.nav-item:focus {
+ outline: none;
+ background: var(--bp-surface-hover);
+ border-color: var(--bp-primary);
+ box-shadow: var(--bp-focus-ring);
+ color: var(--bp-text);
+}
+
+.nav-item:focus .material-symbols-outlined {
+ transform: scale(1.1);
+}
+
+.nav-item.active {
+ background: linear-gradient(135deg, var(--bp-primary) 0%, #5a1fd4 100%);
+ border-color: var(--bp-primary);
+ color: var(--bp-text);
+ box-shadow: 0 4px 20px var(--bp-primary-glow);
+}
+
+.nav-item.active .material-symbols-outlined {
+ color: var(--bp-text);
+}
+
+/* Webview container for browsing */
+.webview-container {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ background: var(--bp-bg);
+ z-index: 2;
+}
+
+.webview-container.hidden {
+ display: none;
+ pointer-events: none;
+}
+
+.webview-container webview {
+ width: 100%;
+ height: 100%;
+ border: none;
+}
+
+/* Content area */
+.bp-content {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ padding: var(--bp-spacing-lg);
+ scroll-behavior: smooth;
+ position: relative;
+}
+
+/* Custom scrollbar */
+.bp-content::-webkit-scrollbar {
+ width: 8px;
+}
+
+.bp-content::-webkit-scrollbar-track {
+ background: var(--bp-surface);
+ border-radius: 4px;
+}
+
+.bp-content::-webkit-scrollbar-thumb {
+ background: var(--bp-border);
+ border-radius: 4px;
+}
+
+.bp-content::-webkit-scrollbar-thumb:hover {
+ background: var(--bp-primary);
+}
+
+/* Sections */
+.bp-section {
+ display: none;
+ animation: fadeIn 0.3s ease;
+}
+
+.bp-section.active {
+ display: block;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.section-header {
+ margin-bottom: var(--bp-spacing-lg);
+}
+
+.section-title {
+ font-size: 2rem;
+ font-weight: 700;
+ margin-bottom: var(--bp-spacing-xs);
+ background: linear-gradient(135deg, var(--bp-text) 0%, var(--bp-accent) 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.section-subtitle {
+ font-size: 1rem;
+ color: var(--bp-text-muted);
+}
+
+.subsection-title {
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: var(--bp-text-muted);
+ margin-bottom: var(--bp-spacing-md);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+/* Search card */
+.search-card {
+ display: flex;
+ align-items: center;
+ gap: var(--bp-spacing-md);
+ padding: var(--bp-spacing-md) var(--bp-spacing-lg);
+ background: var(--bp-surface);
+ border: 2px solid var(--bp-border);
+ border-radius: var(--bp-radius-xl);
+ margin-bottom: var(--bp-spacing-xl);
+ transition: all var(--bp-transition-fast);
+ cursor: pointer;
+}
+
+.search-card:hover {
+ background: var(--bp-surface-hover);
+ border-color: var(--bp-text-dim);
+}
+
+.search-card:focus,
+.search-card:focus-within {
+ outline: none;
+ border-color: var(--bp-accent);
+ box-shadow: var(--bp-focus-ring-accent);
+}
+
+.search-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 48px;
+ height: 48px;
+ border-radius: var(--bp-radius-md);
+ background: linear-gradient(135deg, var(--bp-accent) 0%, var(--bp-primary) 100%);
+}
+
+.search-icon .material-symbols-outlined {
+ font-size: 28px;
+ color: var(--bp-text);
+}
+
+.search-input {
+ flex: 1;
+ background: transparent;
+ border: none;
+ outline: none;
+ font-size: 1.2rem;
+ color: var(--bp-text);
+ caret-color: var(--bp-accent);
+}
+
+.search-input::placeholder {
+ color: var(--bp-text-dim);
+}
+
+.search-hint {
+ display: flex;
+ align-items: center;
+ gap: var(--bp-spacing-xs);
+ color: var(--bp-text-muted);
+ font-size: 0.9rem;
+}
+
+/* Key hints */
+.key-hint {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 28px;
+ height: 28px;
+ padding: 0 8px;
+ background: var(--bp-primary);
+ border-radius: var(--bp-radius-sm);
+ font-size: 0.85rem;
+ font-weight: 700;
+ color: var(--bp-text);
+}
+
+/* Tile grid */
+.tile-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: var(--bp-spacing-md);
+ margin-bottom: var(--bp-spacing-xl);
+}
+
+.tile-grid.large {
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+}
+
+.tile {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ aspect-ratio: 16 / 10;
+ padding: var(--bp-spacing-md);
+ background: var(--bp-surface);
+ border: 2px solid var(--bp-border);
+ border-radius: var(--bp-radius-lg);
+ cursor: pointer;
+ transition: all var(--bp-transition-fast);
+ overflow: hidden;
+ position: relative;
+}
+
+.tile::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(135deg, transparent 0%, rgba(123, 46, 255, 0.1) 100%);
+ opacity: 0;
+ transition: opacity var(--bp-transition-fast);
+}
+
+.tile:hover {
+ background: var(--bp-surface-hover);
+ transform: scale(1.02);
+}
+
+.tile:hover::before {
+ opacity: 1;
+}
+
+.tile:focus {
+ outline: none;
+ border-color: var(--bp-primary);
+ box-shadow: var(--bp-focus-ring);
+ transform: scale(1.02);
+}
+
+.tile:focus::before {
+ opacity: 1;
+}
+
+.tile-icon {
+ width: 64px;
+ height: 64px;
+ border-radius: var(--bp-radius-md);
+ background: var(--bp-surface-active);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: var(--bp-spacing-sm);
+ overflow: hidden;
+}
+
+.tile-icon img {
+ width: 40px;
+ height: 40px;
+ object-fit: contain;
+}
+
+.tile-icon .material-symbols-outlined {
+ font-size: 36px;
+ color: var(--bp-accent);
+}
+
+.tile-title {
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--bp-text);
+ text-align: center;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100%;
+}
+
+.tile-url {
+ font-size: 0.8rem;
+ color: var(--bp-text-dim);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100%;
+}
+
+/* Add tile button */
+.tile.add-tile {
+ border-style: dashed;
+ border-color: var(--bp-text-dim);
+}
+
+.tile.add-tile .material-symbols-outlined {
+ font-size: 48px;
+ color: var(--bp-text-dim);
+}
+
+.tile.add-tile:hover,
+.tile.add-tile:focus {
+ border-color: var(--bp-accent);
+ border-style: solid;
+}
+
+.tile.add-tile:hover .material-symbols-outlined,
+.tile.add-tile:focus .material-symbols-outlined {
+ color: var(--bp-accent);
+}
+
+/* Horizontal scroll */
+.horizontal-scroll {
+ display: flex;
+ gap: var(--bp-spacing-md);
+ overflow-x: auto;
+ padding-bottom: var(--bp-spacing-md);
+ scroll-snap-type: x mandatory;
+}
+
+.horizontal-scroll::-webkit-scrollbar {
+ height: 6px;
+}
+
+.horizontal-scroll::-webkit-scrollbar-track {
+ background: var(--bp-surface);
+ border-radius: 3px;
+}
+
+.horizontal-scroll::-webkit-scrollbar-thumb {
+ background: var(--bp-border);
+ border-radius: 3px;
+}
+
+.scroll-card {
+ flex: 0 0 280px;
+ scroll-snap-align: start;
+ display: flex;
+ flex-direction: column;
+ padding: var(--bp-spacing-md);
+ background: var(--bp-surface);
+ border: 2px solid var(--bp-border);
+ border-radius: var(--bp-radius-lg);
+ cursor: pointer;
+ transition: all var(--bp-transition-fast);
+}
+
+.scroll-card:hover {
+ background: var(--bp-surface-hover);
+ transform: translateY(-4px);
+}
+
+.scroll-card:focus {
+ outline: none;
+ border-color: var(--bp-primary);
+ box-shadow: var(--bp-focus-ring);
+ transform: translateY(-4px);
+}
+
+.scroll-card-preview {
+ width: 100%;
+ aspect-ratio: 16 / 9;
+ background: var(--bp-surface-active);
+ border-radius: var(--bp-radius-sm);
+ margin-bottom: var(--bp-spacing-sm);
+ overflow: hidden;
+}
+
+.scroll-card-preview img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.scroll-card-title {
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--bp-text);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.scroll-card-meta {
+ font-size: 0.85rem;
+ color: var(--bp-text-muted);
+}
+
+/* List container */
+.list-container {
+ display: flex;
+ flex-direction: column;
+ gap: var(--bp-spacing-sm);
+}
+
+.list-item {
+ display: flex;
+ align-items: center;
+ gap: var(--bp-spacing-md);
+ padding: var(--bp-spacing-md) var(--bp-spacing-lg);
+ background: var(--bp-surface);
+ border: 2px solid var(--bp-border);
+ border-radius: var(--bp-radius-md);
+ cursor: pointer;
+ transition: all var(--bp-transition-fast);
+}
+
+.list-item:hover {
+ background: var(--bp-surface-hover);
+}
+
+.list-item:focus {
+ outline: none;
+ border-color: var(--bp-primary);
+ box-shadow: var(--bp-focus-ring);
+}
+
+.list-item-icon {
+ width: 48px;
+ height: 48px;
+ border-radius: var(--bp-radius-sm);
+ background: var(--bp-surface-active);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+.list-item-icon img {
+ width: 32px;
+ height: 32px;
+ object-fit: contain;
+}
+
+.list-item-content {
+ flex: 1;
+ min-width: 0;
+}
+
+.list-item-title {
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--bp-text);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.list-item-meta {
+ font-size: 0.85rem;
+ color: var(--bp-text-muted);
+}
+
+.list-item-action {
+ display: flex;
+ align-items: center;
+ gap: var(--bp-spacing-xs);
+}
+
+/* Empty state */
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: var(--bp-spacing-xl);
+ color: var(--bp-text-dim);
+}
+
+.empty-state .material-symbols-outlined {
+ font-size: 64px;
+ margin-bottom: var(--bp-spacing-md);
+ opacity: 0.5;
+}
+
+.empty-state p {
+ font-size: 1.1rem;
+}
+
+/* NeBot section */
+.nebot-launch {
+ display: flex;
+ justify-content: center;
+}
+
+.nebot-card {
+ display: flex;
+ align-items: center;
+ gap: var(--bp-spacing-lg);
+ padding: var(--bp-spacing-lg) var(--bp-spacing-xl);
+ background: linear-gradient(135deg, var(--bp-surface) 0%, var(--bp-surface-hover) 100%);
+ border: 2px solid var(--bp-border);
+ border-radius: var(--bp-radius-xl);
+ cursor: pointer;
+ transition: all var(--bp-transition-fast);
+ max-width: 500px;
+ width: 100%;
+}
+
+.nebot-card:hover {
+ border-color: var(--bp-accent);
+ transform: scale(1.02);
+}
+
+.nebot-card:focus {
+ outline: none;
+ border-color: var(--bp-accent);
+ box-shadow: var(--bp-focus-ring-accent);
+ transform: scale(1.02);
+}
+
+.nebot-icon {
+ width: 72px;
+ height: 72px;
+ border-radius: var(--bp-radius-lg);
+ background: linear-gradient(135deg, var(--bp-accent) 0%, var(--bp-primary) 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.nebot-icon .material-symbols-outlined {
+ font-size: 40px;
+ color: var(--bp-text);
+}
+
+.nebot-info h3 {
+ font-size: 1.3rem;
+ font-weight: 600;
+ color: var(--bp-text);
+ margin-bottom: 4px;
+}
+
+.nebot-info p {
+ font-size: 0.95rem;
+ color: var(--bp-text-muted);
+}
+
+.nebot-action {
+ margin-left: auto;
+}
+
+/* Settings grid */
+.settings-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
+ gap: var(--bp-spacing-md);
+}
+
+.settings-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: var(--bp-spacing-sm);
+ padding: var(--bp-spacing-lg);
+ background: var(--bp-surface);
+ border: 2px solid var(--bp-border);
+ border-radius: var(--bp-radius-lg);
+ cursor: pointer;
+ transition: all var(--bp-transition-fast);
+}
+
+.settings-card:hover {
+ background: var(--bp-surface-hover);
+ transform: scale(1.02);
+}
+
+.settings-card:focus {
+ outline: none;
+ border-color: var(--bp-primary);
+ box-shadow: var(--bp-focus-ring);
+ transform: scale(1.02);
+}
+
+.settings-card .material-symbols-outlined {
+ font-size: 40px;
+ color: var(--bp-accent);
+}
+
+.settings-label {
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--bp-text);
+}
+
+/* Footer controller hints */
+.bp-footer {
+ position: relative;
+ z-index: 100;
+ padding: var(--bp-spacing-sm) var(--bp-spacing-lg);
+ background: linear-gradient(0deg, rgba(10,10,15,0.95) 0%, rgba(10,10,15,0.8) 100%);
+ backdrop-filter: blur(20px);
+ border-top: 1px solid var(--bp-border);
+}
+
+.controller-hints {
+ display: flex;
+ justify-content: center;
+ gap: var(--bp-spacing-xl);
+}
+
+.hint {
+ display: flex;
+ align-items: center;
+ gap: var(--bp-spacing-sm);
+ color: var(--bp-text-muted);
+ font-size: 0.9rem;
+}
+
+.controller-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 32px;
+ height: 32px;
+ padding: 0 8px;
+ background: var(--bp-surface);
+ border: 1px solid var(--bp-border);
+ border-radius: var(--bp-radius-sm);
+ font-size: 0.85rem;
+ font-weight: 700;
+ color: var(--bp-text);
+}
+
+.controller-btn.a-btn {
+ background: #107c10;
+ border-color: #107c10;
+}
+
+.controller-btn.b-btn {
+ background: #e81123;
+ border-color: #e81123;
+}
+
+.controller-btn.y-btn {
+ background: #ffb900;
+ border-color: #ffb900;
+ color: #000;
+}
+
+.controller-btn .material-symbols-outlined {
+ font-size: 18px;
+}
+
+/* On-screen keyboard */
+.osk-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 1000;
+ background: rgba(0, 0, 0, 0.8);
+ backdrop-filter: blur(10px);
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ padding-bottom: var(--bp-spacing-xl);
+}
+
+.osk-overlay.hidden {
+ display: none;
+}
+
+.osk-container {
+ width: 100%;
+ max-width: 900px;
+ background: var(--bp-surface);
+ border: 1px solid var(--bp-border);
+ border-radius: var(--bp-radius-xl) var(--bp-radius-xl) 0 0;
+ padding: var(--bp-spacing-lg);
+}
+
+.osk-header {
+ display: flex;
+ align-items: center;
+ gap: var(--bp-spacing-md);
+ margin-bottom: var(--bp-spacing-md);
+}
+
+.osk-text-input {
+ flex: 1;
+ padding: var(--bp-spacing-md) var(--bp-spacing-lg);
+ background: var(--bp-bg);
+ border: 2px solid var(--bp-border);
+ border-radius: var(--bp-radius-md);
+ font-size: 1.2rem;
+ color: var(--bp-text);
+}
+
+.osk-text-input:focus {
+ outline: none;
+ border-color: var(--bp-accent);
+}
+
+.osk-close {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 48px;
+ height: 48px;
+ background: var(--bp-surface-hover);
+ border: 1px solid var(--bp-border);
+ border-radius: var(--bp-radius-md);
+ color: var(--bp-text);
+ cursor: pointer;
+ transition: all var(--bp-transition-fast);
+}
+
+.osk-close:hover,
+.osk-close:focus {
+ background: var(--bp-danger);
+ border-color: var(--bp-danger);
+ outline: none;
+}
+
+.osk-keyboard {
+ display: flex;
+ flex-direction: column;
+ gap: var(--bp-spacing-sm);
+ margin-bottom: var(--bp-spacing-md);
+}
+
+.osk-row {
+ display: flex;
+ justify-content: center;
+ gap: var(--bp-spacing-sm);
+}
+
+.osk-key {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 64px;
+ height: 64px;
+ padding: 0 var(--bp-spacing-md);
+ background: var(--bp-surface-hover);
+ border: 3px solid var(--bp-border);
+ border-radius: var(--bp-radius-md);
+ font-size: 1.3rem;
+ font-weight: 700;
+ color: var(--bp-text);
+ cursor: pointer;
+ transition: all var(--bp-transition-fast);
+ text-transform: uppercase;
+}
+
+.osk-key:hover {
+ background: var(--bp-surface-active);
+ transform: scale(1.05);
+}
+
+.osk-key:focus {
+ outline: none;
+ border-color: var(--bp-accent);
+ box-shadow: 0 0 0 4px var(--bp-accent-glow), 0 0 20px var(--bp-accent-glow);
+ background: var(--bp-surface-active);
+ transform: scale(1.1);
+ z-index: 1;
+}
+
+.osk-key.wide {
+ min-width: 120px;
+}
+
+.osk-actions {
+ display: flex;
+ justify-content: center;
+ gap: var(--bp-spacing-md);
+ flex-wrap: wrap;
+}
+
+.osk-action-btn {
+ display: flex;
+ align-items: center;
+ gap: var(--bp-spacing-sm);
+ padding: var(--bp-spacing-md) var(--bp-spacing-xl);
+ background: var(--bp-surface-hover);
+ border: 2px solid var(--bp-border);
+ border-radius: var(--bp-radius-md);
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--bp-text);
+ cursor: pointer;
+ transition: all var(--bp-transition-fast);
+}
+
+.osk-action-btn .btn-hint {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 28px;
+ height: 28px;
+ padding: 0 6px;
+ background: var(--bp-primary);
+ border-radius: 6px;
+ font-size: 0.75rem;
+ font-weight: 700;
+ color: white;
+}
+
+.osk-action-btn:hover,
+.osk-action-btn:focus {
+ background: var(--bp-surface-active);
+ outline: none;
+ border-color: var(--bp-accent);
+}
+
+.osk-action-btn.primary {
+ background: linear-gradient(135deg, var(--bp-primary) 0%, #5a1fd4 100%);
+ border-color: var(--bp-primary);
+}
+
+.osk-action-btn.primary:hover,
+.osk-action-btn.primary:focus {
+ box-shadow: var(--bp-focus-ring);
+}
+
+.osk-action-btn.primary .btn-hint {
+ background: rgba(255, 255, 255, 0.2);
+}
+
+/* OSK hints bar */
+.osk-hints {
+ display: flex;
+ justify-content: center;
+ gap: var(--bp-spacing-lg);
+ margin-top: var(--bp-spacing-md);
+ padding-top: var(--bp-spacing-md);
+ border-top: 1px solid var(--bp-border);
+ color: var(--bp-text-muted);
+ font-size: 0.9rem;
+}
+
+.osk-hints b {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 24px;
+ height: 24px;
+ padding: 0 6px;
+ margin-right: 4px;
+ background: var(--bp-surface-active);
+ border: 1px solid var(--bp-border);
+ border-radius: 4px;
+ font-size: 0.75rem;
+ font-weight: 700;
+ color: var(--bp-text);
+}
+
+/* Context menu */
+.context-menu {
+ position: fixed;
+ z-index: 1001;
+ min-width: 200px;
+ background: var(--bp-surface);
+ border: 1px solid var(--bp-border);
+ border-radius: var(--bp-radius-md);
+ padding: var(--bp-spacing-xs);
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
+}
+
+.context-menu.hidden {
+ display: none;
+}
+
+.context-item {
+ display: flex;
+ align-items: center;
+ gap: var(--bp-spacing-md);
+ width: 100%;
+ padding: var(--bp-spacing-md);
+ background: transparent;
+ border: 2px solid transparent;
+ border-radius: var(--bp-radius-sm);
+ font-size: 1rem;
+ color: var(--bp-text);
+ cursor: pointer;
+ transition: all var(--bp-transition-fast);
+ text-align: left;
+}
+
+.context-item:hover {
+ background: var(--bp-surface-hover);
+}
+
+.context-item:focus {
+ outline: none;
+ border-color: var(--bp-primary);
+ background: var(--bp-surface-hover);
+}
+
+.context-item .material-symbols-outlined {
+ font-size: 20px;
+ color: var(--bp-accent);
+}
+
+/* Focus indicators for controller navigation */
+[data-focusable]:focus {
+ outline: none;
+}
+
+/* Quick access specific styles */
+.quick-access {
+ margin-bottom: var(--bp-spacing-xl);
+}
+
+/* Recent sites specific styles */
+.recent-sites {
+ margin-bottom: var(--bp-spacing-lg);
+}
+
+/* Responsive adjustments for Steam Deck (1280x800) */
+@media screen and (max-width: 1280px) and (max-height: 800px) {
+ html, body {
+ font-size: 16px;
+ }
+
+ .bp-sidebar {
+ width: 180px;
+ min-width: 180px;
+ }
+
+ .nav-item {
+ padding: var(--bp-spacing-sm) var(--bp-spacing-md);
+ }
+
+ .nav-item .material-symbols-outlined {
+ font-size: 24px;
+ }
+
+ .section-title {
+ font-size: 1.6rem;
+ }
+
+ .tile-grid {
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+ }
+
+ .tile-icon {
+ width: 48px;
+ height: 48px;
+ }
+}
+
+/* Even smaller screens */
+@media screen and (max-width: 960px) {
+ .bp-sidebar {
+ width: 80px;
+ min-width: 80px;
+ }
+
+ .nav-label {
+ display: none;
+ }
+
+ .nav-item {
+ justify-content: center;
+ }
+}
+
+/* Fullscreen mode */
+body.fullscreen .bp-header,
+body.fullscreen .bp-footer {
+ display: none;
+}
+
+body.fullscreen .bp-main {
+ height: 100vh;
+}
+
+/* Loading state */
+.loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--bp-spacing-xl);
+}
+
+.loading::after {
+ content: '';
+ width: 40px;
+ height: 40px;
+ border: 3px solid var(--bp-border);
+ border-top-color: var(--bp-primary);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* Notification toast */
+.toast {
+ position: fixed;
+ bottom: 100px;
+ left: 50%;
+ transform: translateX(-50%);
+ padding: var(--bp-spacing-md) var(--bp-spacing-lg);
+ background: var(--bp-surface);
+ border: 1px solid var(--bp-border);
+ border-radius: var(--bp-radius-md);
+ color: var(--bp-text);
+ font-size: 1rem;
+ z-index: 1002;
+ animation: toastIn 0.3s ease, toastOut 0.3s ease 2.7s forwards;
+}
+
+@keyframes toastIn {
+ from { opacity: 0; transform: translateX(-50%) translateY(20px); }
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
+}
+
+@keyframes toastOut {
+ from { opacity: 1; transform: translateX(-50%) translateY(0); }
+ to { opacity: 0; transform: translateX(-50%) translateY(20px); }
+}
+
+/* Virtual Cursor for controller-based web browsing */
+.virtual-cursor {
+ position: fixed;
+ z-index: 10000;
+ pointer-events: none;
+ transform: translate(-2px, -2px);
+ opacity: 0;
+ transition: opacity 0.2s ease;
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
+}
+
+.virtual-cursor.active {
+ opacity: 1;
+}
+
+.virtual-cursor svg {
+ width: 28px;
+ height: 28px;
+ transition: transform 0.1s ease;
+}
+
+.virtual-cursor.clicking svg {
+ transform: scale(0.85);
+}
+
+.cursor-click-indicator {
+ position: absolute;
+ top: 4px;
+ left: 4px;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: var(--bp-primary);
+ opacity: 0;
+ transform: scale(0);
+ transition: all 0.15s ease;
+}
+
+.virtual-cursor.clicking .cursor-click-indicator {
+ opacity: 0.5;
+ transform: scale(1.5);
+}
+
+/* Cursor trail effect (optional visual enhancement) */
+.virtual-cursor::after {
+ content: '';
+ position: absolute;
+ top: 6px;
+ left: 6px;
+ width: 8px;
+ height: 8px;
+ background: var(--bp-accent);
+ border-radius: 50%;
+ opacity: 0.6;
+ animation: cursorPulse 1.5s ease-in-out infinite;
+}
+
+@keyframes cursorPulse {
+ 0%, 100% { transform: scale(1); opacity: 0.6; }
+ 50% { transform: scale(1.3); opacity: 0.3; }
+}
+
+/* Cursor hint overlay when in webview */
+.cursor-controls-hint {
+ position: fixed;
+ bottom: 80px;
+ right: 20px;
+ background: rgba(20, 20, 31, 0.9);
+ border: 1px solid var(--bp-border);
+ border-radius: var(--bp-radius-md);
+ padding: var(--bp-spacing-sm) var(--bp-spacing-md);
+ font-size: 0.8rem;
+ color: var(--bp-text-muted);
+ z-index: 9999;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.cursor-controls-hint .hint-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.cursor-controls-hint .hint-key {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 24px;
+ padding: 2px 6px;
+ background: var(--bp-surface-active);
+ border: 1px solid var(--bp-border);
+ border-radius: 4px;
+ font-weight: 600;
+ font-size: 0.7rem;
+ color: var(--bp-text);
+}
diff --git a/renderer/bigpicture.html b/renderer/bigpicture.html
new file mode 100644
index 0000000..75a8f87
--- /dev/null
+++ b/renderer/bigpicture.html
@@ -0,0 +1,311 @@
+
+
+
+
+
+ Nebula - Big Picture Mode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ search
+
+
+
+ A Search
+
+
+
+
+
+
+
+
+
Continue Browsing
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
folder_open
+
No recent downloads
+
+
+
+
+
+
+
+
+
+
+ smart_toy
+
+
+
Start Conversation
+
Ask questions, get summaries, and more
+
+
+ A
+
+
+
+
+
+
+
+
+
+
+ palette
+ Themes
+
+
+ shield
+ Privacy
+
+
+ display_settings
+ Display
+
+
+ desktop_windows
+ Desktop Mode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ A Type
+ X Backspace
+ Y Space
+ B Close
+ LB Clear All
+ RB Submit
+
+
+
+
+
+
+
+
+
+
+
diff --git a/renderer/bigpicture.js b/renderer/bigpicture.js
new file mode 100644
index 0000000..4a7ae7e
--- /dev/null
+++ b/renderer/bigpicture.js
@@ -0,0 +1,1452 @@
+/**
+ * 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,
+
+ // OSK (On-Screen Keyboard)
+ oskVisible: false,
+ oskCallback: null,
+ oskFocusIndex: 0,
+
+ // Data
+ bookmarks: [],
+ history: [],
+
+ // Mouse tracking
+ mouseTimeout: null,
+
+ // Webview for browsing
+ currentWebview: null,
+ 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'));
+ }
+
+ // Settings cards
+ document.querySelectorAll('.settings-card').forEach(card => {
+ card.addEventListener('click', () => {
+ const action = card.dataset.action;
+ handleSettingsAction(action);
+ });
+ });
+}
+
+function switchSection(sectionId) {
+ console.log('[BigPicture] Switching to section:', sectionId);
+
+ // 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;
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+}
+
+// =============================================================================
+// 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;
+
+ // Combine inputs
+ const up = dpadUp || stickUp;
+ const down = dpadDown || stickDown;
+ const left = dpadLeft || stickLeft;
+ const right = 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) - Select/Type letter
+ 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) - Move cursor left / clear all
+ if (gamepad.buttons[4]?.pressed && !state.lastInput.lb) {
+ if (state.oskVisible) {
+ clearOSK();
+ }
+ state.lastInput.lb = true;
+ } else if (!gamepad.buttons[4]?.pressed) {
+ state.lastInput.lb = false;
+ }
+
+ // RB button (usually index 5) - Submit when OSK open
+ if (gamepad.buttons[5]?.pressed && !state.lastInput.rb) {
+ if (state.oskVisible) {
+ submitOSK();
+ }
+ state.lastInput.rb = true;
+ } else if (!gamepad.buttons[5]?.pressed) {
+ state.lastInput.rb = false;
+ }
+
+ // Start button (usually index 9) - Menu
+ if (gamepad.buttons[9]?.pressed && !state.lastInput.start) {
+ // Toggle to settings
+ 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);
+ }
+
+ // 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;
+ }
+
+ // 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;
+ }
+ }
+}
+
+// =============================================================================
+// 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');
+
+ if (!overlay || !input) return;
+
+ state.oskVisible = true;
+ state.oskMode = mode;
+ overlay.classList.remove('hidden');
+
+ // Clear input
+ input.value = '';
+
+ // 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);
+}
+
+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;
+ }
+}
+
+function backspaceOSK() {
+ const input = document.getElementById('osk-input');
+ if (input && input.value.length > 0) {
+ input.value = input.value.slice(0, -1);
+ playNavSound();
+ }
+}
+
+function clearOSK() {
+ const input = document.getElementById('osk-input');
+ if (input) {
+ input.value = '';
+ playNavSound();
+ }
+}
+
+function submitOSK() {
+ const input = document.getElementById('osk-input');
+ if (!input || !input.value.trim()) return;
+
+ const value = input.value.trim();
+
+ if (state.oskMode === 'search') {
+ performSearch(value);
+ }
+
+ closeOSK();
+}
+
+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 {
+ 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 = [];
+ }
+}
+
+// =============================================================================
+// 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 = `
+
+
bookmark_border
+
No bookmarks yet
+
+ `;
+ return;
+ }
+
+ state.bookmarks.forEach(bookmark => {
+ const tile = createTile(
+ bookmark.title || bookmark.name || getDomainFromUrl(bookmark.url),
+ bookmark.url,
+ 'bookmark'
+ );
+ grid.appendChild(tile);
+ });
+
+ updateFocusableElements();
+}
+
+function renderHistory() {
+ const list = document.getElementById('historyList');
+ if (!list) return;
+
+ list.innerHTML = '';
+
+ if (state.history.length === 0) {
+ list.innerHTML = `
+
+
history
+
No browsing history
+
+ `;
+ return;
+ }
+
+ // Show last 20 items
+ state.history.slice(0, 20).forEach(url => {
+ const item = createListItem(getDomainFromUrl(url), url);
+ list.appendChild(item);
+ });
+
+ updateFocusableElements();
+}
+
+function renderRecentSites() {
+ const container = document.getElementById('recentSitesScroll');
+ if (!container) return;
+
+ container.innerHTML = '';
+
+ if (state.history.length === 0) {
+ container.innerHTML = `
+
+
web
+
Start browsing to see recent sites
+
+ `;
+ return;
+ }
+
+ // Show last 10 unique domains
+ const seenDomains = new Set();
+ const uniqueSites = [];
+
+ for (const url of state.history) {
+ const domain = getDomainFromUrl(url);
+ if (!seenDomains.has(domain)) {
+ seenDomains.add(domain);
+ uniqueSites.push({ url, domain });
+ if (uniqueSites.length >= 10) break;
+ }
+ }
+
+ uniqueSites.forEach(site => {
+ const card = createScrollCard(site.domain, site.url);
+ container.appendChild(card);
+ });
+
+ updateFocusableElements();
+}
+
+function createTile(title, url, icon) {
+ const tile = document.createElement('div');
+ tile.className = 'tile';
+ tile.dataset.focusable = '';
+ tile.tabIndex = 0;
+ tile.dataset.url = url;
+
+ tile.innerHTML = `
+
+ ${icon}
+
+ ${escapeHtml(title)}
+ ${getDomainFromUrl(url)}
+ `;
+
+ tile.addEventListener('click', () => navigateTo(url));
+
+ return tile;
+}
+
+function createListItem(title, url) {
+ const item = document.createElement('div');
+ item.className = 'list-item';
+ item.dataset.focusable = '';
+ item.tabIndex = 0;
+ item.dataset.url = url;
+
+ item.innerHTML = `
+
+ public
+
+
+
${escapeHtml(title)}
+
${escapeHtml(url)}
+
+
+ A
+
+ `;
+
+ item.addEventListener('click', () => navigateTo(url));
+
+ return item;
+}
+
+function createScrollCard(title, url) {
+ const card = document.createElement('div');
+ card.className = 'scroll-card';
+ card.dataset.focusable = '';
+ card.tabIndex = 0;
+ card.dataset.url = url;
+
+ card.innerHTML = `
+
+ public
+
+ ${escapeHtml(title)}
+ Recently visited
+ `;
+
+ card.addEventListener('click', () => navigateTo(url));
+
+ return card;
+}
+
+// =============================================================================
+// ACTIONS
+// =============================================================================
+
+function performSearch(query) {
+ if (!query.trim()) return;
+
+ // Check if it's a URL
+ let url = query.trim();
+ if (isUrl(url)) {
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
+ url = 'https://' + url;
+ }
+ navigateTo(url);
+ } else {
+ // Search with default engine (Google)
+ navigateTo(`https://www.google.com/search?q=${encodeURIComponent(query)}`);
+ }
+}
+
+function navigateTo(url) {
+ console.log('[BigPicture] Navigating to:', url);
+
+ // Create or reuse webview for browsing
+ const container = document.getElementById('webview-container');
+ if (!container) return;
+
+ // Hide content and show webview
+ document.querySelectorAll('.bp-section').forEach(s => s.classList.remove('active'));
+ container.classList.remove('hidden');
+
+ // Remove existing webview if any
+ const existingWebview = container.querySelector('webview');
+ if (existingWebview) {
+ existingWebview.remove();
+ }
+
+ // Create new webview
+ const webview = document.createElement('webview');
+ webview.src = url;
+ webview.style.width = '100%';
+ webview.style.height = '100%';
+ webview.style.border = 'none';
+ webview.preload = '../preload.js';
+ webview.partition = 'persist:main';
+ webview.allowpopups = true;
+ webview.webpreferences = 'allowRunningInsecureContent=false,javascript=true,webSecurity=true';
+
+ container.appendChild(webview);
+ state.currentWebview = webview;
+
+ // Enable virtual cursor for webview interaction
+ enableCursor();
+
+ // Switch section to browse
+ switchSection('browse');
+
+ // Update focusable elements to include webview controls
+ setTimeout(() => {
+ updateFocusableElements();
+ }, 100);
+}
+
+function exitBigPictureMode() {
+ console.log('[BigPicture] Exiting Big Picture Mode');
+
+ if (ipcRenderer) {
+ ipcRenderer.send('exit-bigpicture');
+ } else if (window.opener) {
+ window.opener.postMessage({ type: 'exit-bigpicture' }, '*');
+ window.close();
+ }
+}
+
+function handleSettingsAction(action) {
+ switch (action) {
+ case 'theme':
+ showToast('Theme settings coming soon');
+ break;
+ case 'privacy':
+ showToast('Privacy settings coming soon');
+ break;
+ case 'display':
+ showToast('Display settings coming soon');
+ break;
+ case 'exit-bigpicture':
+ exitBigPictureMode();
+ break;
+ default:
+ console.log('[BigPicture] Unknown settings action:', action);
+ }
+}
+
+// =============================================================================
+// UTILITIES
+// =============================================================================
+
+function isUrl(str) {
+ // Simple URL detection
+ return /^(https?:\/\/)?[\w-]+(\.[\w-]+)+/.test(str) ||
+ str.includes('.com') ||
+ str.includes('.org') ||
+ str.includes('.net') ||
+ str.includes('.io') ||
+ str.startsWith('browser://');
+}
+
+// =============================================================================
+// VIRTUAL CURSOR (for webview interaction)
+// =============================================================================
+
+function createCursorElement() {
+ if (state.cursorElement) return;
+
+ const cursor = document.createElement('div');
+ cursor.id = 'virtual-cursor';
+ cursor.className = 'virtual-cursor';
+ cursor.innerHTML = `
+
+
+ `;
+ document.body.appendChild(cursor);
+ state.cursorElement = cursor;
+}
+
+function enableCursor() {
+ if (!state.cursorElement) {
+ createCursorElement();
+ }
+
+ const container = document.getElementById('webview-container');
+ if (container) {
+ const rect = container.getBoundingClientRect();
+ state.cursorX = rect.left + rect.width / 2;
+ state.cursorY = rect.top + rect.height / 2;
+ } else {
+ state.cursorX = window.innerWidth / 2;
+ state.cursorY = window.innerHeight / 2;
+ }
+
+ state.cursorEnabled = true;
+ updateCursorPosition();
+ state.cursorElement.classList.add('active');
+
+ // Show cursor hint
+ showToast('🎮 Right stick: Move cursor | RT: Click | LT: Right-click | B: Back');
+}
+
+function disableCursor() {
+ state.cursorEnabled = false;
+ if (state.cursorElement) {
+ state.cursorElement.classList.remove('active');
+ }
+}
+
+function moveCursor(dx, dy) {
+ if (!state.cursorEnabled) return;
+
+ const container = document.getElementById('webview-container');
+ if (!container) return;
+
+ const rect = container.getBoundingClientRect();
+
+ // Update cursor position with bounds checking
+ state.cursorX = Math.max(rect.left, Math.min(rect.right - 10, state.cursorX + dx));
+ state.cursorY = Math.max(rect.top, Math.min(rect.bottom - 10, state.cursorY + dy));
+
+ updateCursorPosition();
+}
+
+function updateCursorPosition() {
+ if (!state.cursorElement) return;
+
+ state.cursorElement.style.left = `${state.cursorX}px`;
+ state.cursorElement.style.top = `${state.cursorY}px`;
+}
+
+function virtualClick(rightClick = false) {
+ if (!state.currentWebview || !state.cursorEnabled) return;
+
+ const container = document.getElementById('webview-container');
+ if (!container) return;
+
+ const containerRect = container.getBoundingClientRect();
+
+ // Calculate position relative to webview
+ const x = state.cursorX - containerRect.left;
+ const y = state.cursorY - containerRect.top;
+
+ // Show click animation
+ if (state.cursorElement) {
+ state.cursorElement.classList.add('clicking');
+ setTimeout(() => state.cursorElement.classList.remove('clicking'), 150);
+ }
+
+ // Send mouse event to webview
+ try {
+ const webContents = state.currentWebview;
+
+ // Use executeJavaScript to simulate click at coordinates
+ const clickScript = rightClick ? `
+ (function() {
+ const el = document.elementFromPoint(${x}, ${y});
+ if (el) {
+ const event = new MouseEvent('contextmenu', {
+ bubbles: true,
+ cancelable: true,
+ clientX: ${x},
+ clientY: ${y},
+ button: 2
+ });
+ el.dispatchEvent(event);
+ }
+ })();
+ ` : `
+ (function() {
+ const el = document.elementFromPoint(${x}, ${y});
+ if (el) {
+ // Try to focus if it's an input
+ if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.contentEditable === 'true') {
+ el.focus();
+ }
+ // Simulate full click sequence
+ const rect = el.getBoundingClientRect();
+ const events = ['mousedown', 'mouseup', 'click'];
+ events.forEach(type => {
+ const event = new MouseEvent(type, {
+ bubbles: true,
+ cancelable: true,
+ view: window,
+ clientX: ${x},
+ clientY: ${y},
+ button: 0
+ });
+ el.dispatchEvent(event);
+ });
+ // Also try clicking directly for links and buttons
+ if (el.click) el.click();
+ }
+ })();
+ `;
+
+ webContents.executeJavaScript(clickScript).catch(err => {
+ console.log('[BigPicture] Click injection error:', err);
+ });
+ } catch (err) {
+ console.log('[BigPicture] Virtual click error:', err);
+ }
+}
+
+function scrollWebview(amount) {
+ if (!state.currentWebview) return;
+
+ try {
+ state.currentWebview.executeJavaScript(`window.scrollBy(0, ${amount})`);
+ } catch (err) {
+ console.log('[BigPicture] Scroll error:', err);
+ }
+}
+
+// =============================================================================
+// UTILITIES
+// =============================================================================
+
+function getDomainFromUrl(url) {
+ try {
+ if (url.startsWith('browser://')) {
+ return url.replace('browser://', '').split('/')[0];
+ }
+ const hostname = new URL(url).hostname;
+ return hostname.replace(/^www\./, '');
+ } catch {
+ return url;
+ }
+}
+
+function escapeHtml(str) {
+ const div = document.createElement('div');
+ div.textContent = str;
+ return div.innerHTML;
+}
+
+function showToast(message) {
+ // Remove existing toast
+ const existing = document.querySelector('.toast');
+ if (existing) existing.remove();
+
+ const toast = document.createElement('div');
+ toast.className = 'toast';
+ toast.textContent = message;
+ document.body.appendChild(toast);
+
+ setTimeout(() => toast.remove(), 3000);
+}
+
+function playNavSound() {
+ if (!CONFIG.NAV_SOUND_ENABLED) return;
+
+ // Simple beep using Web Audio API
+ try {
+ const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
+ const oscillator = audioCtx.createOscillator();
+ const gainNode = audioCtx.createGain();
+
+ oscillator.connect(gainNode);
+ gainNode.connect(audioCtx.destination);
+
+ oscillator.frequency.value = 800;
+ oscillator.type = 'sine';
+ gainNode.gain.value = 0.05;
+
+ oscillator.start();
+ oscillator.stop(audioCtx.currentTime + 0.03);
+ } catch (e) {
+ // Audio not available
+ }
+}
+
+function playSelectSound() {
+ if (!CONFIG.NAV_SOUND_ENABLED) return;
+
+ try {
+ const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
+ const oscillator = audioCtx.createOscillator();
+ const gainNode = audioCtx.createGain();
+
+ oscillator.connect(gainNode);
+ gainNode.connect(audioCtx.destination);
+
+ oscillator.frequency.value = 1200;
+ oscillator.type = 'sine';
+ gainNode.gain.value = 0.08;
+
+ oscillator.start();
+ oscillator.stop(audioCtx.currentTime + 0.05);
+ } catch (e) {
+ // Audio not available
+ }
+}
+
+// =============================================================================
+// IPC HANDLERS
+// =============================================================================
+
+if (ipcRenderer) {
+ // Listen for theme changes
+ ipcRenderer.on('theme-changed', (theme) => {
+ if (theme && theme.colors) {
+ applyTheme(theme);
+ }
+ });
+}
+
+function applyTheme(theme) {
+ if (!theme || !theme.colors) return;
+
+ const root = document.documentElement;
+
+ if (theme.colors.bg) root.style.setProperty('--bp-bg', theme.colors.bg);
+ if (theme.colors.darkPurple) root.style.setProperty('--bp-surface', theme.colors.darkPurple);
+ if (theme.colors.primary) {
+ root.style.setProperty('--bp-primary', theme.colors.primary);
+ root.style.setProperty('--bp-primary-glow', `${theme.colors.primary}66`);
+ }
+ if (theme.colors.accent) {
+ root.style.setProperty('--bp-accent', theme.colors.accent);
+ root.style.setProperty('--bp-accent-glow', `${theme.colors.accent}4d`);
+ }
+ if (theme.colors.text) root.style.setProperty('--bp-text', theme.colors.text);
+}
+
+console.log('[BigPicture] Module loaded');
diff --git a/renderer/index.html b/renderer/index.html
index 7af7e81..6e3038a 100644
--- a/renderer/index.html
+++ b/renderer/index.html
@@ -62,6 +62,7 @@