diff --git a/renderer/bigpicture.css b/renderer/bigpicture.css index 1248d78..c3e3b45 100644 --- a/renderer/bigpicture.css +++ b/renderer/bigpicture.css @@ -413,6 +413,51 @@ body.mouse-active { color: var(--bp-text-muted); } +/* Section action buttons */ +.section-actions { + display: flex; + gap: var(--bp-spacing-md); + margin-bottom: var(--bp-spacing-lg); +} + +.action-btn { + display: flex; + align-items: center; + gap: var(--bp-spacing-sm); + padding: var(--bp-spacing-sm) var(--bp-spacing-md); + background: var(--bp-surface); + border: 2px solid var(--bp-border); + border-radius: var(--bp-radius-md); + color: var(--bp-text-muted); + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all var(--bp-transition-fast); +} + +.action-btn:hover { + background: var(--bp-surface-hover); + color: var(--bp-text); + border-color: var(--bp-text-dim); +} + +.action-btn:focus { + outline: none; + border-color: var(--bp-primary); + box-shadow: var(--bp-focus-ring); + color: var(--bp-text); +} + +.action-btn .material-symbols-outlined { + font-size: 20px; +} + +.action-btn.danger:hover, +.action-btn.danger:focus { + border-color: var(--bp-danger); + color: var(--bp-danger); +} + .subsection-title { font-size: 1.1rem; font-weight: 600; @@ -569,7 +614,8 @@ body.mouse-active { overflow: hidden; } -.tile-icon img { +.tile-icon img, +.tile-favicon { width: 40px; height: 40px; object-fit: contain; @@ -580,6 +626,11 @@ body.mouse-active { color: var(--bp-accent); } +/* Bookmark tile specific styles */ +.bookmark-tile .tile-icon { + background: linear-gradient(135deg, var(--bp-surface-active) 0%, var(--bp-surface-hover) 100%); +} + .tile-title { font-size: 1rem; font-weight: 600; @@ -677,6 +728,9 @@ body.mouse-active { border-radius: var(--bp-radius-sm); margin-bottom: var(--bp-spacing-sm); overflow: hidden; + display: flex; + align-items: center; + justify-content: center; } .scroll-card-preview img { @@ -685,6 +739,17 @@ body.mouse-active { object-fit: cover; } +.scroll-card-favicon { + width: 64px; + height: 64px; + object-fit: contain; +} + +.scroll-card-icon { + width: 100%; + height: 100%; +} + .scroll-card-title { font-size: 1rem; font-weight: 600; @@ -737,14 +802,26 @@ body.mouse-active { align-items: center; justify-content: center; overflow: hidden; + flex-shrink: 0; } -.list-item-icon img { +.list-item-icon img, +.list-item-favicon { width: 32px; height: 32px; object-fit: contain; } +.list-item-icon .material-symbols-outlined { + font-size: 24px; + color: var(--bp-text-muted); +} + +/* History item specific styles */ +.history-item:hover .list-item-icon { + background: var(--bp-surface-active); +} + .list-item-content { flex: 1; min-width: 0; @@ -780,6 +857,10 @@ body.mouse-active { color: var(--bp-text-dim); } +.empty-state.compact { + padding: var(--bp-spacing-lg); +} + .empty-state .material-symbols-outlined { font-size: 64px; margin-bottom: var(--bp-spacing-md); @@ -790,6 +871,12 @@ body.mouse-active { font-size: 1.1rem; } +.empty-state .empty-hint { + font-size: 0.9rem; + margin-top: var(--bp-spacing-xs); + opacity: 0.7; +} + /* NeBot section */ .nebot-launch { display: flex; diff --git a/renderer/bigpicture.html b/renderer/bigpicture.html index d99aea8..551bc9e 100644 --- a/renderer/bigpicture.html +++ b/renderer/bigpicture.html @@ -154,6 +154,16 @@

History

Recently visited sites

+
+ + +
diff --git a/renderer/bigpicture.js b/renderer/bigpicture.js index 3355e4e..8c26c2f 100644 --- a/renderer/bigpicture.js +++ b/renderer/bigpicture.js @@ -185,6 +185,20 @@ function initNavigation() { 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', () => { @@ -468,6 +482,15 @@ function goBack() { } } +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 // ============================================================================= @@ -600,20 +623,24 @@ function handleGamepadInput(gamepad) { state.lastInput.y = false; } - // LB button (usually index 4) - Move cursor left / clear all + // 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) - Submit when OSK open + // 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) { @@ -1079,8 +1106,13 @@ async function loadBookmarks() { async function loadHistory() { try { - const stored = localStorage.getItem('siteHistory'); - state.history = stored ? JSON.parse(stored) : []; + 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) { @@ -1089,6 +1121,48 @@ async function loadHistory() { } } +// 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 // ============================================================================= @@ -1127,23 +1201,53 @@ function renderBookmarks() {
bookmark_border

No bookmarks yet

+

Add bookmarks in desktop mode to see them here

`; return; } state.bookmarks.forEach(bookmark => { - const tile = createTile( - bookmark.title || bookmark.name || getDomainFromUrl(bookmark.url), - bookmark.url, - 'bookmark' - ); + const tile = createBookmarkTile(bookmark); grid.appendChild(tile); }); updateFocusableElements(); } +function createBookmarkTile(bookmark) { + const tile = document.createElement('div'); + tile.className = 'tile bookmark-tile'; + tile.dataset.focusable = ''; + tile.tabIndex = 0; + tile.dataset.url = bookmark.url; + + const title = bookmark.title || bookmark.name || getDomainFromUrl(bookmark.url); + const icon = bookmark.icon || 'bookmark'; + + // Check if icon is a URL (favicon) or a material icon name + const isIconUrl = typeof icon === 'string' && /^(https?:|data:)/.test(icon); + + let iconHtml; + if (isIconUrl) { + iconHtml = ``; + } else { + iconHtml = `${escapeHtml(icon)}`; + } + + tile.innerHTML = ` +
+ ${iconHtml} +
+
${escapeHtml(title)}
+
${getDomainFromUrl(bookmark.url)}
+ `; + + tile.addEventListener('click', () => navigateTo(bookmark.url)); + + return tile; +} + function renderHistory() { const list = document.getElementById('historyList'); if (!list) return; @@ -1155,20 +1259,50 @@ function renderHistory() {
history

No browsing history

+

Sites you visit will appear here

`; return; } - // Show last 20 items - state.history.slice(0, 20).forEach(url => { - const item = createListItem(getDomainFromUrl(url), url); + // Show last 30 items + state.history.slice(0, 30).forEach(url => { + const item = createHistoryItem(url); list.appendChild(item); }); updateFocusableElements(); } +function createHistoryItem(url) { + const item = document.createElement('div'); + item.className = 'list-item history-item'; + item.dataset.focusable = ''; + item.tabIndex = 0; + item.dataset.url = url; + + const domain = getDomainFromUrl(url); + const faviconUrl = getFaviconUrl(url); + + item.innerHTML = ` +
+ + +
+
+
${escapeHtml(domain)}
+
${escapeHtml(url)}
+
+
+ A +
+ `; + + item.addEventListener('click', () => navigateTo(url)); + + return item; +} + function renderRecentSites() { const container = document.getElementById('recentSitesScroll'); if (!container) return; @@ -1177,7 +1311,7 @@ function renderRecentSites() { if (state.history.length === 0) { container.innerHTML = ` -
+
web

Start browsing to see recent sites

@@ -1206,16 +1340,26 @@ function renderRecentSites() { updateFocusableElements(); } -function createTile(title, url, icon) { +function createTile(title, url, icon, useFavicon = false) { const tile = document.createElement('div'); tile.className = 'tile'; tile.dataset.focusable = ''; tile.tabIndex = 0; tile.dataset.url = url; + let iconHtml; + const isIconUrl = typeof icon === 'string' && /^(https?:|data:)/.test(icon); + + if (isIconUrl || useFavicon) { + const faviconUrl = isIconUrl ? icon : getFaviconUrl(url); + iconHtml = ``; + } else { + iconHtml = `${escapeHtml(icon)}`; + } + tile.innerHTML = `
- ${icon} + ${iconHtml}
${escapeHtml(title)}
${getDomainFromUrl(url)}
@@ -1226,6 +1370,15 @@ function createTile(title, url, icon) { return tile; } +function getFaviconUrl(url) { + try { + const urlObj = new URL(url); + return `https://www.google.com/s2/favicons?domain=${urlObj.hostname}&sz=64`; + } catch { + return ''; + } +} + function createListItem(title, url) { const item = document.createElement('div'); item.className = 'list-item'; @@ -1258,9 +1411,12 @@ function createScrollCard(title, url) { card.tabIndex = 0; card.dataset.url = url; + const faviconUrl = getFaviconUrl(url); + card.innerHTML = `
- public + +
${escapeHtml(title)}
Recently visited
@@ -1323,6 +1479,9 @@ function navigateTo(url) { state.currentWebview = webview; state.webviewContentsId = null; // Will be set when webview is ready + // Save initial URL to history + saveToHistory(url); + // Get webContentsId when webview is ready for native input events webview.addEventListener('dom-ready', () => { try { @@ -1337,6 +1496,24 @@ function navigateTo(url) { } }); + // Save navigation to history + webview.addEventListener('did-navigate', (event) => { + const newUrl = event.url; + if (newUrl && !newUrl.startsWith('about:')) { + saveToHistory(newUrl); + } + }); + + // Also save history on in-page navigations (e.g., SPA navigations) + webview.addEventListener('did-navigate-in-page', (event) => { + if (event.isMainFrame) { + const newUrl = event.url; + if (newUrl && !newUrl.startsWith('about:')) { + saveToHistory(newUrl); + } + } + }); + // Listen for IPC messages from webview (for OSK requests) webview.addEventListener('ipc-message', (event) => { if (event.channel === 'bigpicture-input-focused') {