Refactor history and settings UI; improve tab rendering

Moved site and search history management from the main process to the renderer for better performance and reliability. Updated settings UI to use a sidebar tab layout with improved accessibility and responsive design. Refactored tab rendering in the browser to use efficient scheduling and added a robust tab label function. Cleaned up context menu code and improved async file operations for bookmarks and history.
This commit is contained in:
2025-08-09 21:51:31 +12:00
parent 4cf0634ef5
commit 8deeccb32e
6 changed files with 356 additions and 321 deletions
+37 -81
View File
@@ -53,6 +53,30 @@ let activeTabId = null;
const allowedInternalPages = ['settings', 'home'];
let bookmarks = [];
// Efficient render scheduling to avoid redundant DOM work
let tabsRenderPending = false;
function scheduleRenderTabs() {
if (tabsRenderPending) return;
tabsRenderPending = true;
requestAnimationFrame(() => {
tabsRenderPending = false;
renderTabs();
});
}
// Derive a stable, safe label for a tab without throwing on non-URLs
function getTabLabel(tab) {
if (tab.title && tab.title !== 'New Tab') return tab.title;
const u = tab.url || '';
try {
if (u.startsWith('http')) return new URL(u).hostname;
if (u.startsWith('browser://')) return u.replace('browser://', '');
return u || 'New Tab';
} catch {
return u || 'New Tab';
}
}
// Load bookmarks on startup
async function loadBookmarks() {
try {
@@ -107,9 +131,9 @@ function createTab(inputUrl) {
isHome: true
};
tabs.push(tab);
setActiveTab(id);
// Render the tab bar so the new home tab appears
renderTabs();
setActiveTab(id);
// Render the tab bar so the new home tab appears
scheduleRenderTabs();
return id;
}
@@ -208,7 +232,7 @@ function createTab(inputUrl) {
});
setActiveTab(id);
renderTabs();
scheduleRenderTabs();
}
@@ -236,7 +260,7 @@ function updateTabMetadata(id, key, value) {
const tab = tabs.find(t => t.id === id);
if (tab) {
tab[key] = value;
renderTabs();
scheduleRenderTabs();
}
}
@@ -276,7 +300,7 @@ function navigate() {
tab.url = input;
webview.src = resolved;
renderTabs();
scheduleRenderTabs();
updateNavButtons();
}
@@ -363,7 +387,7 @@ function convertHomeTabToWebview(tabId, inputUrl, resolvedUrl) {
updateNavButtons();
// Activate converted webview tab and update UI
setActiveTab(tabId);
renderTabs();
scheduleRenderTabs();
}
function handleNavigation(tabId, newUrl) {
@@ -406,7 +430,7 @@ function handleNavigation(tabId, newUrl) {
urlBox.value = displayUrl === 'browser://home' ? '' : displayUrl;
}
renderTabs();
scheduleRenderTabs();
updateNavButtons();
}
@@ -439,7 +463,7 @@ function setActiveTab(id) {
if (tab) {
// If the tab URL represents the home page, keep the URL bar blank.
urlBox.value = tab.url === 'browser://home' ? '' : tab.url;
renderTabs();
scheduleRenderTabs();
updateNavButtons();
updateZoomUI(); // ← update zoom display for new active tab
}
@@ -455,7 +479,7 @@ function closeTab(id) {
if (tabs.length > 0) setActiveTab(tabs[0].id);
}
renderTabs();
scheduleRenderTabs();
updateNavButtons();
}
@@ -475,7 +499,7 @@ function renderTabs() {
el.appendChild(icon);
}
el.appendChild(document.createTextNode(tab.title || new URL(tab.url).hostname));
el.appendChild(document.createTextNode(getTabLabel(tab)));
const closeBtn = document.createElement('button');
closeBtn.textContent = '×';
@@ -572,17 +596,7 @@ menuBtn.addEventListener('click', () => {
});
window.addEventListener('DOMContentLoaded', () => {
// 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());
}
// Initial boot
createTab();
// Handle IPC messages from the static home webview (bookmarks navigation)
const staticHome = document.getElementById('home-webview');
@@ -651,65 +665,7 @@ window.addEventListener('DOMContentLoaded', () => {
document.getElementById('zoom-percent').textContent = `${Math.round(z * 100)}%`;
});
// menurelated code (moved here so #context-menu exists)
const items = contextMenu ? contextMenu.querySelectorAll('li') : [];
function showContextMenu(x, y) {
if (!menu) return;
menu.style.top = `${y}px`;
menu.style.left = `${x}px`;
menu.classList.add('visible');
}
document.addEventListener('contextmenu', e => {
if (e.target.tagName === 'WEBVIEW' ||
e.composedPath().some(el => el.id === 'webviews')) {
e.preventDefault();
showContextMenu(e.clientX, e.clientY);
}
});
document.addEventListener('click', () => {
if (menu) menu.classList.remove('visible');
});
items.forEach(item => {
item.addEventListener('click', async () => {
const action = item.dataset.action;
const win = remote.getCurrentWindow();
switch (action) {
case 'save-page': {
const { canceled, filePath } = await remote.dialog.showSaveDialog(win, { defaultPath: 'page.html' });
if (!canceled && filePath) win.webContents.savePage(filePath, 'HTMLComplete');
break;
}
case 'select-all':
document.execCommand('selectAll');
break;
case 'screenshot': {
const image = await win.webContents.capturePage();
const { canceled, filePath } = await remote.dialog.showSaveDialog(win, { defaultPath: 'screenshot.png' });
if (!canceled && filePath) fs.writeFileSync(filePath, image.toPNG());
break;
}
case 'view-source': {
const html = document.documentElement.outerHTML;
const { canceled, filePath } = await remote.dialog.showSaveDialog(win, { defaultPath: 'source.html' });
if (!canceled && filePath) fs.writeFileSync(filePath, html);
break;
}
case 'inspect-accessibility':
win.webContents.inspectAccessibilityNode(e.clientX, e.clientY);
break;
case 'inspect-element':
win.webContents.inspectElement(e.clientX, e.clientY);
break;
}
menu.classList.remove('visible');
});
});
// (Removed broken duplicate context menu wiring)
// Migrate existing site history from JSON file to localStorage (one-time migration)
const migrateSiteHistory = async () => {
+77 -5
View File
@@ -29,11 +29,73 @@ body {
.container {
background-color: var(--dark-purple);
padding: 2rem;
padding: 0;
border-radius: 16px;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
max-width: 500px;
max-width: 1100px;
width: 100%;
display: flex;
overflow: hidden;
}
/* Sidebar + content layout */
.sidebar {
width: 260px;
background: rgba(0,0,0,0.2);
border-right: 1px solid rgba(255,255,255,0.08);
padding: 1.25rem 1rem;
}
.sidebar h1 {
font-size: 1.2rem;
margin: 0 0 1rem 0;
color: var(--primary);
}
.tabs {
display: flex;
flex-direction: column;
gap: 4px;
}
.tab-link {
text-align: left;
background: transparent;
color: var(--text);
border: none;
border-radius: 8px;
padding: 10px 12px;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
font-size: 14px;
font-family: inherit;
width: 100%;
position: relative;
z-index: 1;
}
.tab-link:hover {
background: rgba(255,255,255,0.06);
}
.tab-link.active {
background: rgba(123, 46, 255, 0.18);
color: #fff;
border: 1px solid rgba(123,46,255,0.35);
}
.content {
flex: 1;
padding: 1.25rem 1.5rem 2rem 1.5rem;
overflow: auto;
}
.tab-panel {
display: none;
}
.tab-panel.active {
display: block;
}
h1 {
@@ -132,10 +194,20 @@ button:hover {
/* small-screen adjustments */
@media (max-width: 480px) {
.container {
padding: 1rem;
border-radius: 0;
box-shadow: none;
padding: 0;
border-radius: 12px;
box-shadow: 0 0 8px rgba(0,0,0,0.35);
flex-direction: column;
max-width: 100%;
}
.sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid rgba(255,255,255,0.08);
padding-bottom: 0.5rem;
}
.tabs { flex-direction: row; flex-wrap: wrap; gap: 6px; }
.tab-link { flex: 1 1 auto; }
h1 {
font-size: 1.25rem;
}
+74 -51
View File
@@ -240,25 +240,35 @@
</style>
</head>
<body>
<div class="container">
<h1>⚙️ Browser Settings</h1>
<div class="container" role="application">
<aside class="sidebar" aria-label="Settings categories">
<h1>⚙️ Settings</h1>
<nav class="tabs" role="tablist">
<button class="tab-link active" role="tab" aria-selected="true" aria-controls="panel-general" id="tab-general" data-tab="general">General</button>
<button class="tab-link" role="tab" aria-selected="false" aria-controls="panel-appearance" id="tab-appearance" data-tab="appearance">Appearance</button>
<button class="tab-link" role="tab" aria-selected="false" aria-controls="panel-history" id="tab-history" data-tab="history">History</button>
<button class="tab-link" role="tab" aria-selected="false" aria-controls="panel-about" id="tab-about" data-tab="about">About</button>
</nav>
</aside>
<div class="setting-group">
<label for="clear-data-btn">Clear All Cookies &amp; Data</label>
<button id="clear-data-btn">Clear Data</button>
</div>
<main class="content">
<!-- General Panel -->
<section class="tab-panel active" id="panel-general" role="tabpanel" aria-labelledby="tab-general">
<h2>General</h2>
<div class="setting-group">
<label for="clear-data-btn">Clear All Cookies &amp; Data</label>
<button id="clear-data-btn">Clear Data</button>
</div>
<p class="note">Settings are stored locally on this device.</p>
<div class="debug-info" id="debug-info">Loading debug info...</div>
</section>
<p class="note">Settings are stored locally on this device.</p>
<div class="debug-info" id="debug-info">Loading debug info...</div>
<!-- Customization Section -->
<section>
<h2>🎨 Browser Customization</h2>
<!-- Theme Selection -->
<div class="customization-group">
<h3>Theme Presets</h3>
<!-- Appearance Panel -->
<section class="tab-panel" id="panel-appearance" role="tabpanel" aria-labelledby="tab-appearance">
<h2>🎨 Appearance</h2>
<!-- Theme Selection -->
<div class="customization-group">
<h3>Theme Presets</h3>
<div class="theme-selector">
<button id="theme-default" class="theme-btn" data-theme="default">
<div class="theme-preview" style="background: linear-gradient(145deg, #121418, #1B1035);"></div>
@@ -313,11 +323,11 @@
<span>Custom</span>
</button>
</div>
</div>
</div>
<!-- Color Customization -->
<div class="customization-group">
<h3>Custom Colors</h3>
<!-- Color Customization -->
<div class="customization-group">
<h3>Custom Colors</h3>
<div class="color-controls">
<div class="color-group">
<label for="bg-color">Background:</label>
@@ -340,11 +350,11 @@
<input type="color" id="text-color" value="#E0E0E0">
</div>
</div>
</div>
</div>
<!-- Home Page Layout -->
<div class="customization-group">
<h3>Home Page Layout</h3>
<!-- Home Page Layout -->
<div class="customization-group">
<h3>Home Page Layout</h3>
<div class="layout-options">
<label>
<input type="radio" name="layout" value="centered" checked>
@@ -359,11 +369,11 @@
<span>Compact View</span>
</label>
</div>
</div>
</div>
<!-- Logo Customization -->
<div class="customization-group">
<h3>Logo & Branding</h3>
<!-- Logo Customization -->
<div class="customization-group">
<h3>Logo & Branding</h3>
<div class="logo-options">
<label for="show-logo">
<input type="checkbox" id="show-logo" checked>
@@ -374,11 +384,11 @@
<input type="text" id="custom-title" placeholder="Nebula Browser" maxlength="50">
</label>
</div>
</div>
</div>
<!-- Theme Management -->
<div class="customization-group">
<h3>Theme Management</h3>
<!-- Theme Management -->
<div class="customization-group">
<h3>Theme Management</h3>
<div class="theme-management">
<button id="save-custom-theme">Save Current as Custom Theme</button>
<button id="export-theme">Export Theme</button>
@@ -386,11 +396,11 @@
<input type="file" id="theme-file-input" accept=".json" style="display: none;">
<button id="reset-to-default">Reset to Default</button>
</div>
</div>
</div>
<!-- Live Preview -->
<div class="customization-group">
<h3>Preview</h3>
<!-- Live Preview -->
<div class="customization-group">
<h3>Preview</h3>
<div class="preview-container" id="preview-container">
<div class="preview-home">
<div class="preview-logo" id="preview-logo">../assets/images/Logos/Nebula-Logo.svg</div>
@@ -403,21 +413,34 @@
</div>
</div>
</div>
</div>
</section>
</div>
</section>
<!-- 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>
<!-- History Panel -->
<section class="tab-panel" id="panel-history" role="tabpanel" aria-labelledby="tab-history">
<h2>History</h2>
<div class="customization-group">
<h3>Search History</h3>
<ul id="search-history-list"></ul>
<button id="clear-search-history-btn" style="margin-top: 10px;">Clear Search History</button>
</div>
<div class="customization-group">
<h3>Site History</h3>
<ul id="site-history-list"></ul>
<div style="display:flex; gap:10px; margin-top:10px; flex-wrap:wrap;">
<button id="clear-site-history-btn">Clear Site History</button>
<button id="add-test-history-btn">Add Test History</button>
</div>
</div>
</section>
<!-- About Panel -->
<section class="tab-panel" id="panel-about" role="tabpanel" aria-labelledby="tab-about">
<h2>About</h2>
<p class="note">Nebula Browser Experimental Settings</p>
<p class="note">Version info and links can go here.</p>
</section>
</main>
</div>
<!-- status overlay moved outside of .container -->
+137 -22
View File
@@ -1,10 +1,30 @@
// Use require('electron') since webviews have nodeIntegrationInSubFrames: true
// In settings webview we use the same preload API as main windows
const { ipcRenderer } = require('electron');
// Try to get ipcRenderer, but don't fail if it's not available
let ipcRenderer = null;
try {
if (typeof require !== 'undefined') {
const electron = require('electron');
ipcRenderer = electron.ipcRenderer;
}
} catch (e) {
console.log('[SETTINGS] Electron IPC not available, some features may be limited');
}
const clearBtn = document.getElementById('clear-data-btn');
let clearBtn = document.getElementById('clear-data-btn');
const statusDiv = document.getElementById('status');
const statusText = document.getElementById('status-text');
const TAB_STORAGE_KEY = 'nebula-settings-active-tab';
function showStatus(message) {
if (statusText && statusDiv) {
statusText.textContent = message;
statusDiv.classList.remove('hidden');
setTimeout(() => {
statusDiv.classList.add('hidden');
}, 2000);
} else {
console.log('[STATUS]', message);
}
}
function showStatus(message) {
statusText.textContent = message;
@@ -14,23 +34,118 @@ function showStatus(message) {
}, 2000);
}
clearBtn.onclick = async () => {
statusDiv.classList.remove('hidden');
statusText.textContent = 'Clearing all browser data...';
try {
const ok = await ipcRenderer.invoke('clear-browser-data');
showStatus(ok
? 'All browser data and bookmarks cleared!'
: 'Failed to clear browser data.');
} catch (error) {
console.error('Error clearing browser data:', error);
showStatus('An error occurred while clearing data.');
} finally {
// Send theme update to host after clearing
const currentTheme = window.browserCustomizer ? window.browserCustomizer.currentTheme : null;
if (currentTheme && window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
window.electronAPI.sendToHost('theme-update', currentTheme);
function attachClearHandler(btn) {
if (!btn) return;
btn.onclick = async () => {
if (statusDiv && statusText) {
statusDiv.classList.remove('hidden');
statusText.textContent = 'Clearing all browser data...';
}
try {
if (ipcRenderer) {
const ok = await ipcRenderer.invoke('clear-browser-data');
showStatus(ok
? 'All browser data and bookmarks cleared!'
: 'Failed to clear browser data.');
} else {
showStatus('Clear data feature not available in this context.');
}
} catch (error) {
console.error('Error clearing browser data:', error);
showStatus('An error occurred while clearing data.');
} finally {
const currentTheme = window.browserCustomizer ? window.browserCustomizer.currentTheme : null;
if (currentTheme && window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
window.electronAPI.sendToHost('theme-update', currentTheme);
}
}
};
}
// Try attaching immediately, and again on DOMContentLoaded
attachClearHandler(clearBtn);
window.addEventListener('DOMContentLoaded', () => {
if (!clearBtn) {
clearBtn = document.getElementById('clear-data-btn');
attachClearHandler(clearBtn);
}
};
});
// Tabs: simple controller
function activateTab(tabName) {
console.log('[TABS] Activating tab:', tabName);
const links = document.querySelectorAll('.tab-link');
const panels = document.querySelectorAll('.tab-panel');
console.log('[TABS] Found', links.length, 'tab links and', panels.length, 'panels');
links.forEach(l => {
const isActive = l.dataset.tab === tabName;
l.classList.toggle('active', isActive);
l.setAttribute('aria-selected', isActive ? 'true' : 'false');
if (isActive) l.focus({ preventScroll: true });
});
panels.forEach(p => {
const isActive = p.id === `panel-${tabName}`;
p.classList.toggle('active', isActive);
p.hidden = !isActive;
console.log('[TABS] Panel', p.id, 'active:', isActive);
});
try { localStorage.setItem(TAB_STORAGE_KEY, tabName); } catch {}
}
function initTabs() {
console.log('[TABS] Initializing tabs...');
const links = document.querySelectorAll('.tab-link');
console.log('[TABS] Found tab links:', links.length);
// Direct listeners (for accessibility focus handling)
links.forEach((link, index) => {
console.log('[TABS] Setting up listener for tab', index, 'with data-tab:', link.dataset.tab);
link.addEventListener('click', (e) => {
console.log('[TABS] Tab clicked:', link.dataset.tab);
e.preventDefault();
e.stopPropagation();
const name = link.dataset.tab;
if (!name) return;
if (location.hash !== `#${name}`) {
history.replaceState(null, '', `#${name}`);
}
activateTab(name);
});
});
// Delegation as a fallback if elements are re-rendered
const tabContainer = document.querySelector('.tabs');
if (tabContainer) {
console.log('[TABS] Setting up delegation on tabs container');
tabContainer.addEventListener('click', (e) => {
console.log('[TABS] Container click detected');
const btn = e.target && e.target.closest ? e.target.closest('.tab-link') : null;
if (!btn || !tabContainer.contains(btn)) return;
console.log('[TABS] Delegated click for tab:', btn.dataset.tab);
const name = btn.dataset.tab;
if (!name) return;
if (location.hash !== `#${name}`) {
history.replaceState(null, '', `#${name}`);
}
activateTab(name);
});
}
// Resolve initial tab: hash > storage > default 'general'
let initial = (location.hash || '').replace('#', '') || null;
if (!initial) {
try { initial = localStorage.getItem(TAB_STORAGE_KEY) || null; } catch {}
}
if (!initial) initial = 'general';
console.log('[TABS] Initial tab:', initial);
activateTab(initial);
}
// Initialize tabs after DOM is ready but before customization init uses the DOM
window.addEventListener('DOMContentLoaded', () => {
console.log('[TABS] DOM loaded, initializing tabs...');
initTabs();
console.log('[TABS] Tabs initialized');
});