diff --git a/README.md b/README.md index c944151..401e20a 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,11 @@ A customizable and privacy-focused web browser built with Electron. ## Features -* **Custom Theming:** Personalize your browsing experience with custom themes. * **Privacy Control:** Easily clear your browsing data, including history, cookies, and cache. * **Tab Management:** Open new tabs, and manage them efficiently. -* **History:** Keeps track of your browsing and search history. * **Bookmarks:** Save your favorite sites. -* **Custom Start Page:** Set your own home page. -* **Zoom Controls:** Adjust the zoom level of pages. + + ## Getting Started diff --git a/main.js b/main.js index c72df3f..baa0677 100644 --- a/main.js +++ b/main.js @@ -82,20 +82,55 @@ function createWindow(startUrl) { // ensure all embedded tags also use the same window win.webContents.on('did-attach-webview', (event, webContents) => { + // Set up webview with preload script to provide electronAPI + webContents.on('dom-ready', () => { + webContents.executeJavaScript(` + window.electronAPI = { + invoke: (channel, ...args) => { + return new Promise((resolve, reject) => { + const { ipcRenderer } = require('electron'); + ipcRenderer.invoke(channel, ...args).then(resolve).catch(reject); + }); + } + }; + `); + }); + // intercept window.open() inside webview webContents.setWindowOpenHandler(({ url }) => { webContents.loadURL(url); + // record history for webview navigations + recordHistory('site-history.json', url); + const m = /[?&](?:q|query)=([^&]+)/.exec(url); + if (m && m[1]) { + const query = decodeURIComponent(m[1].replace(/\+/g, ' ')); + recordHistory('search-history.json', query); + } return { action: 'deny' }; }); // intercept legacy new-window on webview webContents.on('new-window', (e, url) => { e.preventDefault(); webContents.loadURL(url); + // record history for webview navigations + recordHistory('site-history.json', url); + const m = /[?&](?:q|query)=([^&]+)/.exec(url); + if (m && m[1]) { + const query = decodeURIComponent(m[1].replace(/\+/g, ' ')); + recordHistory('search-history.json', query); + } }); // intercept navigation on webview (e.g. user clicks link) webContents.on('will-navigate', (e, url) => { e.preventDefault(); webContents.loadURL(url); + // record history for webview navigations + recordHistory('site-history.json', url); + const m = /[?&](?:q|query)=([^&]+)/.exec(url); + if (m && m[1]) { + const query = decodeURIComponent(m[1].replace(/\+/g, ' ')); + recordHistory('search-history.json', query); + } }); }); @@ -116,13 +151,28 @@ function createWindow(startUrl) { // record site and search history on every navigation const recordHistory = async (fileName, entry) => { - const filePath = path.join(__dirname, fileName); - let data = []; - try { data = JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch {} - if (data[0] !== entry) { - data.unshift(entry); - if (data.length > 100) data.pop(); - fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); + if (fileName === 'site-history.json') { + // Save to both file and send to renderer + const filePath = path.join(__dirname, fileName); + let data = []; + try { data = JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch {} + if (data[0] !== entry) { + data.unshift(entry); + if (data.length > 100) data.pop(); + fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); + } + // Also send to renderer for localStorage + win.webContents.send('record-site-history', entry); + } else { + // Keep search history in JSON file for now + const filePath = path.join(__dirname, fileName); + let data = []; + try { data = JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch {} + if (data[0] !== entry) { + data.unshift(entry); + if (data.length > 100) data.pop(); + fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); + } } }; @@ -168,7 +218,10 @@ ipcMain.handle('window-close', event => { }); // Add site and search history IPC handlers +// Site history is now handled via localStorage in the renderer +// But keep these handlers for compatibility and potential future use ipcMain.handle('load-site-history', async () => { + // Read from the site history file for settings page const filePath = path.join(__dirname, 'site-history.json'); try { const data = fs.readFileSync(filePath, 'utf-8'); @@ -179,6 +232,7 @@ ipcMain.handle('load-site-history', async () => { }); ipcMain.handle('save-site-history', async (event, history) => { + // Save to both file and localStorage const filePath = path.join(__dirname, 'site-history.json'); try { fs.writeFileSync(filePath, JSON.stringify(history, null, 2)); @@ -188,6 +242,16 @@ ipcMain.handle('save-site-history', async (event, history) => { } }); +ipcMain.handle('clear-site-history', async () => { + const filePath = path.join(__dirname, 'site-history.json'); + try { + fs.writeFileSync(filePath, JSON.stringify([], null, 2)); + return true; + } catch (err) { + return false; + } +}); + ipcMain.handle('load-search-history', async () => { const filePath = path.join(__dirname, 'search-history.json'); try { @@ -269,3 +333,29 @@ ipcMain.handle('zoom-out', event => { ipcMain.handle('open-tab-in-new-window', (event, url) => { createWindow(url); }); + +ipcMain.handle('save-site-history-entry', async (event, url) => { + const filePath = path.join(__dirname, 'site-history.json'); + try { + let data = []; + try { + data = JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch {} + + // Remove if already exists to avoid duplicates + data = data.filter(item => item !== url); + // Add to beginning + data.unshift(url); + // Keep only last 100 entries + if (data.length > 100) { + data = data.slice(0, 100); + } + + fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); + console.log('[MAIN] Saved site history entry:', url); + return true; + } catch (err) { + console.error('[MAIN] Error saving site history entry:', err); + return false; + } +}); diff --git a/renderer/script.js b/renderer/script.js index 39cc621..9de41e7 100644 --- a/renderer/script.js +++ b/renderer/script.js @@ -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); diff --git a/renderer/settings.html b/renderer/settings.html index c634784..0a2e3a4 100644 --- a/renderer/settings.html +++ b/renderer/settings.html @@ -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; } @@ -23,6 +24,21 @@

Settings are stored locally on this device.

+ +
Loading debug info...
+ + +
+

Search History

+ + +
+
+

Site History

+ + + +
@@ -31,44 +47,185 @@ - - diff --git a/renderer/settings.js b/renderer/settings.js index 5af8503..5b4b870 100644 --- a/renderer/settings.js +++ b/renderer/settings.js @@ -1,3 +1,4 @@ +// Use require('electron') since webviews have nodeIntegrationInSubFrames: true const { ipcRenderer } = require('electron'); const clearBtn = document.getElementById('clear-data-btn'); diff --git a/search-history.json b/search-history.json new file mode 100644 index 0000000..31839b5 --- /dev/null +++ b/search-history.json @@ -0,0 +1,3 @@ +[ + "Awatapu College" +] \ No newline at end of file diff --git a/site-history.json b/site-history.json index 313cae7..a0305ef 100644 --- a/site-history.json +++ b/site-history.json @@ -1,4 +1,13 @@ [ "file:///X:/Projects/Code/NebulaBrowser/renderer/index.html", - "file:///X:/Projects/Code/SteamOS_Browser/renderer/index.html" + "https://www.google.com/search?q=Awatapu%20College", + "https://andrewzambazos.com/", + "https://www.youtube.com/", + "file:///X:/Projects/Code/NebulaBrowser/renderer/index.html", + "file:///X:/Projects/Code/NebulaBrowser/renderer/index.html", + "https://youtube.com/", + "file:///X:/Projects/Code/NebulaBrowser/renderer/index.html", + "https://www.google.com/", + "https://youtube.com/", + "file:///X:/Projects/Code/NebulaBrowser/renderer/index.html" ] \ No newline at end of file