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:
+89
-2
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
+193
-16
@@ -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() {
|
||||
<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') {
|
||||
|
||||
Reference in New Issue
Block a user