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.
This commit is contained in:
2025-12-28 10:47:04 +13:00
parent 8a2b7ee5e9
commit 37345b267b
3 changed files with 292 additions and 18 deletions
+89 -2
View File
@@ -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;
+10
View File
@@ -154,6 +154,16 @@
<h1 class="section-title">History</h1>
<p class="section-subtitle">Recently visited sites</p>
</div>
<div class="section-actions">
<button class="action-btn" id="clearHistoryBtn" data-focusable tabindex="0">
<span class="material-symbols-outlined">delete_sweep</span>
<span>Clear History</span>
</button>
<button class="action-btn" id="refreshHistoryBtn" data-focusable tabindex="0">
<span class="material-symbols-outlined">refresh</span>
<span>Refresh</span>
</button>
</div>
<div class="list-container" id="historyList">
<!-- History will be populated dynamically -->
</div>
+191 -14
View File
@@ -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 {
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() {
<div class="empty-state">
<span class="material-symbols-outlined">bookmark_border</span>
<p>No bookmarks yet</p>
<p class="empty-hint">Add bookmarks in desktop mode to see them here</p>
</div>
`;
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 = `<img src="${escapeHtml(icon)}" alt="" class="tile-favicon" onerror="this.style.display='none';this.parentElement.innerHTML='<span class=\\'material-symbols-outlined\\'>bookmark</span>'">`;
} else {
iconHtml = `<span class="material-symbols-outlined">${escapeHtml(icon)}</span>`;
}
tile.innerHTML = `
<div class="tile-icon">
${iconHtml}
</div>
<div class="tile-title">${escapeHtml(title)}</div>
<div class="tile-url">${getDomainFromUrl(bookmark.url)}</div>
`;
tile.addEventListener('click', () => navigateTo(bookmark.url));
return tile;
}
function renderHistory() {
const list = document.getElementById('historyList');
if (!list) return;
@@ -1155,20 +1259,50 @@ function renderHistory() {
<div class="empty-state">
<span class="material-symbols-outlined">history</span>
<p>No browsing history</p>
<p class="empty-hint">Sites you visit will appear here</p>
</div>
`;
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 = `
<div class="list-item-icon">
<img src="${escapeHtml(faviconUrl)}" alt="" class="list-item-favicon" onerror="this.style.display='none';this.nextElementSibling.style.display='inline'">
<span class="material-symbols-outlined" style="display:none">public</span>
</div>
<div class="list-item-content">
<div class="list-item-title">${escapeHtml(domain)}</div>
<div class="list-item-meta">${escapeHtml(url)}</div>
</div>
<div class="list-item-action">
<span class="key-hint">A</span>
</div>
`;
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 = `
<div class="empty-state">
<div class="empty-state compact">
<span class="material-symbols-outlined">web</span>
<p>Start browsing to see recent sites</p>
</div>
@@ -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 = `<img src="${escapeHtml(faviconUrl)}" alt="" class="tile-favicon" onerror="this.style.display='none';this.parentElement.innerHTML='<span class=\\'material-symbols-outlined\\'>public</span>'">`;
} else {
iconHtml = `<span class="material-symbols-outlined">${escapeHtml(icon)}</span>`;
}
tile.innerHTML = `
<div class="tile-icon">
<span class="material-symbols-outlined">${icon}</span>
${iconHtml}
</div>
<div class="tile-title">${escapeHtml(title)}</div>
<div class="tile-url">${getDomainFromUrl(url)}</div>
@@ -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 = `
<div class="scroll-card-preview">
<span class="material-symbols-outlined" style="font-size: 48px; color: var(--bp-text-dim); display: flex; align-items: center; justify-content: center; height: 100%;">public</span>
<img src="${escapeHtml(faviconUrl)}" alt="" class="scroll-card-favicon" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
<span class="material-symbols-outlined scroll-card-icon" style="display:none;font-size: 48px; color: var(--bp-text-dim); align-items: center; justify-content: center; height: 100%;">public</span>
</div>
<div class="scroll-card-title">${escapeHtml(title)}</div>
<div class="scroll-card-meta">Recently visited</div>
@@ -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') {