From 37345b267bc983faedbec7be4c19f44416ca9dea Mon Sep 17 00:00:00 2001 From: Andrew Zambazos Date: Sun, 28 Dec 2025 10:47:04 +1300 Subject: [PATCH] Add enhanced history management and favicons Introduces clear and refresh buttons for browsing history, enables favicon display for history, bookmarks, and recent sites, and improves history storage with IPC support and localStorage fallback. Also updates styles for action buttons and favicons, and adds gamepad navigation for browser history. --- renderer/bigpicture.css | 91 ++++++++++++++++- renderer/bigpicture.html | 10 ++ renderer/bigpicture.js | 209 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 292 insertions(+), 18 deletions(-) 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') {