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 = `
+
+
})
+
public
+
+
+
${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 = `
${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') {