Refactor site history to use localStorage and improve UI

Site history is now managed in localStorage for faster access and better cross-tab experience, with file sync for persistence. Added a dropdown for quick site history access in the address bar, updated settings page to display and manage site history from localStorage, and improved history recording logic in both main and renderer processes. Also added debug info and test data utilities for development.
This commit is contained in:
2025-07-26 14:15:13 +12:00
parent ba9a2aa891
commit 0b61f86dd4
7 changed files with 496 additions and 46 deletions
+195 -3
View File
@@ -1,5 +1,33 @@
const ipcRenderer = window.electronAPI;
// Site history management using localStorage
function getSiteHistory() {
try {
const history = localStorage.getItem('siteHistory');
return history ? JSON.parse(history) : [];
} catch (err) {
console.error('Error reading site history from localStorage:', err);
return [];
}
}
function addToSiteHistory(url) {
try {
let history = getSiteHistory();
// Remove if already exists to avoid duplicates
history = history.filter(item => item !== url);
// Add to beginning
history.unshift(url);
// Keep only last 100 entries
if (history.length > 100) {
history = history.slice(0, 100);
}
localStorage.setItem('siteHistory', JSON.stringify(history));
} catch (err) {
console.error('Error saving site history to localStorage:', err);
}
}
// 1) cache hot DOM references
const urlBox = document.getElementById('url');
const tabBarEl = document.getElementById('tab-bar');
@@ -8,13 +36,93 @@ const menuPopup = document.getElementById('menu-popup');
const contextMenu = document.getElementById('context-menu');
const menuItems = contextMenu ? contextMenu.querySelectorAll('li') : [];
let siteHistoryDropdown = null;
function initializeSiteHistoryDropdown() {
// Create site history dropdown
siteHistoryDropdown = document.createElement('div');
siteHistoryDropdown.id = 'site-history-dropdown';
siteHistoryDropdown.style.cssText = `
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ccc;
border-top: none;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
`;
if (urlBox && urlBox.parentElement) {
urlBox.parentElement.style.position = 'relative';
urlBox.parentElement.appendChild(siteHistoryDropdown);
}
}
function showSiteHistory() {
if (!siteHistoryDropdown) return;
const history = getSiteHistory();
if (history.length === 0) {
siteHistoryDropdown.style.display = 'none';
return;
}
siteHistoryDropdown.innerHTML = '';
history.slice(0, 10).forEach(url => {
const item = document.createElement('div');
item.style.cssText = `
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #eee;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
item.textContent = url;
item.addEventListener('mouseenter', () => item.style.backgroundColor = '#f0f0f0');
item.addEventListener('mouseleave', () => item.style.backgroundColor = 'white');
item.addEventListener('click', () => {
urlBox.value = url;
navigate();
hideSiteHistory();
});
siteHistoryDropdown.appendChild(item);
});
siteHistoryDropdown.style.display = 'block';
}
function hideSiteHistory() {
if (siteHistoryDropdown) {
siteHistoryDropdown.style.display = 'none';
}
}
// Select all text on focus and prevent mouseup from deselecting
urlBox.addEventListener('focus', () => urlBox.select());
urlBox.addEventListener('focus', () => {
urlBox.select();
showSiteHistory();
});
urlBox.addEventListener('mouseup', e => e.preventDefault());
// Add Enter key navigation
urlBox.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
navigate();
hideSiteHistory();
} else if (e.key === 'Escape') {
hideSiteHistory();
}
});
// Hide history when clicking outside
document.addEventListener('click', (e) => {
if (!urlBox.contains(e.target) && (!siteHistoryDropdown || !siteHistoryDropdown.contains(e.target))) {
hideSiteHistory();
}
});
@@ -24,6 +132,12 @@ let activeTabId = null;
const allowedInternalPages = ['settings', 'home'];
let bookmarks = [];
// Listen for site history updates from main process
ipcRenderer.on('record-site-history', (event, url) => {
console.log('[DEBUG] Received site history update:', url);
addToSiteHistory(url);
});
function createTab(inputUrl) {
inputUrl = inputUrl || 'browser://home';
console.log('[DEBUG] createTab() inputUrl =', inputUrl);
@@ -45,8 +159,41 @@ function createTab(inputUrl) {
if (e.favicons.length > 0) updateTabMetadata(id, 'favicon', e.favicons[0]);
});
webview.addEventListener('did-navigate', e => handleNavigation(id, e.url)); // was using inputUrl
webview.addEventListener('did-navigate-in-page', e => handleNavigation(id, e.url)); // was using inputUrl
// Consolidated navigation recording - only use did-navigate to avoid duplicates
webview.addEventListener('did-navigate', e => {
handleNavigation(id, e.url);
// Record ALL HTTP navigations
if (e.url.startsWith('http')) {
console.log('[DEBUG] Recording navigation to:', e.url);
addToSiteHistory(e.url);
// Also save to file for cross-context sharing
ipcRenderer.invoke('save-site-history-entry', e.url).catch(err =>
console.error('Failed to save to file:', err)
);
}
});
webview.addEventListener('did-navigate-in-page', e => {
handleNavigation(id, e.url);
// Record in-page navigations too
if (e.url.startsWith('http')) {
console.log('[DEBUG] Recording in-page navigation to:', e.url);
addToSiteHistory(e.url);
ipcRenderer.invoke('save-site-history-entry', e.url).catch(err =>
console.error('Failed to save to file:', err)
);
}
});
// Also capture when pages finish loading
webview.addEventListener('did-finish-load', () => {
const currentUrl = webview.getURL();
if (currentUrl.startsWith('http') && !currentUrl.includes('browser://')) {
console.log('[DEBUG] Webview did-finish-load, recording:', currentUrl);
addToSiteHistory(currentUrl);
ipcRenderer.invoke('save-site-history-entry', currentUrl);
}
});
// catch any target="_blank" or window.open() calls and open them as new tabs
webview.addEventListener('new-window', e => {
@@ -131,6 +278,8 @@ function handleNavigation(tabId, newUrl) {
const tab = tabs.find(t => t.id === tabId);
if (!tab) return;
console.log('[DEBUG] handleNavigation called with:', newUrl);
// --- record every real navigation into history ---
if (tab.history[tab.historyIndex] !== newUrl) {
tab.history = tab.history.slice(0, tab.historyIndex + 1);
@@ -138,6 +287,18 @@ function handleNavigation(tabId, newUrl) {
tab.historyIndex++;
}
// Record site history in localStorage (skip internal pages and file:// URLs)
if (!newUrl.endsWith('home.html') &&
!newUrl.endsWith('settings.html') &&
!newUrl.startsWith('file://') &&
!newUrl.includes('browser://') &&
newUrl.startsWith('http')) {
console.log('[DEBUG] Adding to site history:', newUrl);
addToSiteHistory(newUrl);
// Also send to main process for file storage
ipcRenderer.invoke('save-site-history-entry', newUrl);
}
// translate local files back to our browser:// scheme
const isHome = newUrl.endsWith('home.html');
const isSettings = newUrl.endsWith('settings.html');
@@ -305,6 +466,20 @@ menuBtn.addEventListener('click', () => {
});
window.addEventListener('DOMContentLoaded', () => {
// Initialize site history dropdown after DOM is ready
initializeSiteHistoryDropdown();
// Add some debug info
console.log('[DEBUG] Site history initialized, current entries:', getSiteHistory().length);
// Add test entries if none exist (for debugging)
if (getSiteHistory().length === 0) {
console.log('[DEBUG] No existing history, adding test entries for debugging');
addToSiteHistory('https://www.google.com');
addToSiteHistory('https://github.com');
console.log('[DEBUG] Test entries added, history now:', getSiteHistory());
}
createTab();
// only now bind the reload button (guaranteed to exist)
const reloadBtn = document.getElementById('reload-btn');
@@ -401,6 +576,23 @@ window.addEventListener('DOMContentLoaded', () => {
});
});
// Migrate existing site history from JSON file to localStorage (one-time migration)
const migrateSiteHistory = async () => {
try {
// Check if we already have data in localStorage
const existingHistory = getSiteHistory();
if (existingHistory.length === 0) {
// Try to load from the old JSON file system
console.log('Attempting to migrate site history from JSON file...');
// Since we can't access the file directly, we'll just start fresh
// The site-history.json file was the old method, localStorage is the new method
}
} catch (err) {
console.log('Site history migration skipped:', err.message);
}
};
migrateSiteHistory();
// ipcRenderer.invoke('load-bookmarks').then(bs => {
// bookmarks = bs;
// console.log('[DEBUG] Loaded bookmarks:', bookmarks);
+188 -31
View File
@@ -11,6 +11,7 @@
h2 { border-bottom: 1px solid #ccc; padding-bottom: 5px; }
ul { list-style: none; padding-left: 0; }
li { padding: 5px 0; border-bottom: 1px solid #eee; }
.debug-info { background: #f0f0f0; padding: 10px; margin: 10px 0; font-family: monospace; font-size: 12px; }
</style>
</head>
<body>
@@ -23,6 +24,21 @@
</div>
<p class="note">Settings are stored locally on this device.</p>
<div class="debug-info" id="debug-info">Loading debug info...</div>
<!-- add history views -->
<section>
<h2>Search History</h2>
<ul id="search-history-list"></ul>
<button id="clear-search-history-btn" style="margin-top: 10px;">Clear Search History</button>
</section>
<section>
<h2>Site History</h2>
<ul id="site-history-list"></ul>
<button id="clear-site-history-btn" style="margin-top: 10px;">Clear Site History</button>
<button id="add-test-history-btn" style="margin-top: 10px; margin-left: 10px;">Add Test History</button>
</section>
</div>
<!-- status overlay moved outside of .container -->
@@ -31,44 +47,185 @@
<span id="status-text"></span>
</div>
<script src="settings.js"></script>
<script>
const { ipcRenderer } = require('electron');
// Update debug info
function updateDebugInfo() {
const debugDiv = document.getElementById('debug-info');
const localStorage = window.localStorage;
const hasElectronAPI = !!window.electronAPI;
const siteHistory = localStorage ? localStorage.getItem('siteHistory') : 'N/A';
debugDiv.innerHTML = `
localStorage available: ${!!localStorage}<br>
electronAPI available: ${hasElectronAPI}<br>
siteHistory in localStorage: ${siteHistory || 'null'}<br>
Current domain: ${window.location.hostname}<br>
Current protocol: ${window.location.protocol}
`;
}
async function loadHistories() {
// Get site history from localStorage in this webview context
function getSiteHistoryFromLocalStorage() {
try {
const searchHistory = await ipcRenderer.invoke('load-search-history');
const siteHistory = await ipcRenderer.invoke('load-site-history');
const searchList = document.getElementById('search-history-list');
const siteList = document.getElementById('site-history-list');
// Clear existing content
searchList.innerHTML = '';
siteList.innerHTML = '';
// Populate search history
searchHistory.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
searchList.appendChild(li);
});
// Populate site history
siteHistory.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
siteList.appendChild(li);
});
} catch(err) {
console.error('Error loading histories:', err);
const history = localStorage.getItem('siteHistory');
console.log('[SETTINGS DEBUG] localStorage siteHistory:', history);
return history ? JSON.parse(history) : [];
} catch (err) {
console.error('[SETTINGS DEBUG] Error reading from localStorage:', err);
return [];
}
}
// Load histories on page load.
window.addEventListener('DOMContentLoaded', loadHistories);
// Sync site history from main browser's localStorage to this webview's localStorage
function syncSiteHistoryFromMain() {
try {
// Try to get the parent window's localStorage
if (window.parent && window.parent !== window) {
const parentHistory = window.parent.localStorage.getItem('siteHistory');
if (parentHistory) {
localStorage.setItem('siteHistory', parentHistory);
console.log('[SETTINGS DEBUG] Synced history from parent window');
return JSON.parse(parentHistory);
}
}
return [];
} catch (err) {
console.log('[SETTINGS DEBUG] Could not sync from parent:', err.message);
return [];
}
}
function addTestHistory() {
const testSites = [
'https://www.google.com',
'https://github.com',
'https://stackoverflow.com',
'https://developer.mozilla.org',
'https://www.wikipedia.org'
];
testSites.forEach(site => {
try {
let history = getSiteHistoryFromLocalStorage();
history = history.filter(item => item !== site);
history.unshift(site);
localStorage.setItem('siteHistory', JSON.stringify(history));
console.log('[SETTINGS DEBUG] Added test site:', site);
} catch (err) {
console.error('Error adding test history:', err);
}
});
loadHistories();
}
async function loadHistories() {
try {
console.log('[SETTINGS DEBUG] Loading histories...');
updateDebugInfo();
// First try to sync from parent window
let siteHistory = syncSiteHistoryFromMain();
// If that didn't work, get from local storage
if (siteHistory.length === 0) {
siteHistory = getSiteHistoryFromLocalStorage();
}
// Try to get search history via message passing to parent or direct file access
let searchHistory = [];
const searchList = document.getElementById('search-history-list');
const siteList = document.getElementById('site-history-list');
// Clear existing content
searchList.innerHTML = '';
siteList.innerHTML = '';
// Populate search history
const searchLi = document.createElement('li');
searchLi.textContent = 'Search history not available in webview context';
searchLi.style.fontStyle = 'italic';
searchLi.style.color = '#666';
searchList.appendChild(searchLi);
// Populate site history
if (siteHistory && siteHistory.length > 0) {
console.log('[SETTINGS DEBUG] Displaying', siteHistory.length, 'site history items');
siteHistory.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
li.style.wordBreak = 'break-all';
siteList.appendChild(li);
});
} else {
console.log('[SETTINGS DEBUG] No site history to display');
const li = document.createElement('li');
li.textContent = 'No browsing history found. Browse some websites first!';
li.style.fontStyle = 'italic';
li.style.color = '#666';
siteList.appendChild(li);
}
} catch(err) {
console.error('[SETTINGS DEBUG] Error loading histories:', err);
const searchList = document.getElementById('search-history-list');
const siteList = document.getElementById('site-history-list');
const errorLi1 = document.createElement('li');
errorLi1.textContent = 'Error loading search history: ' + err.message;
errorLi1.style.color = 'red';
searchList.appendChild(errorLi1);
const errorLi2 = document.createElement('li');
errorLi2.textContent = 'Error loading site history: ' + err.message;
errorLi2.style.color = 'red';
siteList.appendChild(errorLi2);
}
}
async function clearSiteHistory() {
try {
// Clear from localStorage
localStorage.removeItem('siteHistory');
console.log('[SETTINGS DEBUG] Cleared site history from localStorage');
loadHistories(); // Refresh the display
} catch (err) {
console.error('Error clearing site history:', err);
}
}
async function clearSearchHistory() {
console.log('Search history clearing not available in webview context');
}
// Load histories on page load
window.addEventListener('DOMContentLoaded', () => {
console.log('[SETTINGS DEBUG] DOM loaded, window.electronAPI:', !!window.electronAPI);
loadHistories();
// Refresh history every few seconds to pick up new browsing
setInterval(loadHistories, 3000);
// Add test history button functionality
const addTestBtn = document.getElementById('add-test-history-btn');
if (addTestBtn) {
addTestBtn.addEventListener('click', addTestHistory);
}
// Add clear buttons functionality
const clearSiteBtn = document.getElementById('clear-site-history-btn');
const clearSearchBtn = document.getElementById('clear-search-history-btn');
if (clearSiteBtn) {
clearSiteBtn.addEventListener('click', clearSiteHistory);
}
if (clearSearchBtn) {
clearSearchBtn.addEventListener('click', clearSearchHistory);
}
});
</script>
</body>
</html>
+1
View File
@@ -1,3 +1,4 @@
// Use require('electron') since webviews have nodeIntegrationInSubFrames: true
const { ipcRenderer } = require('electron');
const clearBtn = document.getElementById('clear-data-btn');