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:
@@ -99,74 +99,13 @@ function createWindow(startUrl) {
|
|||||||
win.loadURL(url);
|
win.loadURL(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ensure all embedded <webview> tags also use the same window
|
// ensure all embedded <webview> tags behave predictably without heavy injections
|
||||||
win.webContents.on('did-attach-webview', (event, webContents) => {
|
win.webContents.on('did-attach-webview', (event, webContents) => {
|
||||||
// Set up webview with preload script to provide electronAPI - fixed injection
|
// Let the renderer/webview handle navigation; avoid extra JS injection that can stall
|
||||||
webContents.on('dom-ready', () => {
|
|
||||||
// Simpler, more reliable API injection that doesn't require cloning
|
|
||||||
webContents.executeJavaScript(`
|
|
||||||
if (!window.electronAPI) {
|
|
||||||
// Create a simple bridge without complex objects
|
|
||||||
window.electronAPI = {
|
|
||||||
invoke: function(channel) {
|
|
||||||
const args = Array.prototype.slice.call(arguments, 1);
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
try {
|
|
||||||
const ipcRenderer = require('electron').ipcRenderer;
|
|
||||||
ipcRenderer.invoke(channel, ...args).then(resolve).catch(reject);
|
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
console.log('electronAPI injected successfully');
|
|
||||||
}
|
|
||||||
`).catch(err => {
|
|
||||||
console.error('Failed to inject electronAPI:', err);
|
|
||||||
// Fallback: inject minimal API
|
|
||||||
webContents.executeJavaScript(`
|
|
||||||
window.electronAPI = { invoke: function() { return Promise.resolve(); } };
|
|
||||||
`).catch(() => {});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// intercept window.open() inside webview
|
|
||||||
webContents.setWindowOpenHandler(({ url }) => {
|
webContents.setWindowOpenHandler(({ url }) => {
|
||||||
webContents.loadURL(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' };
|
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
win.loadFile('renderer/index.html');
|
win.loadFile('renderer/index.html');
|
||||||
@@ -189,69 +128,7 @@ function createWindow(startUrl) {
|
|||||||
perfMonitor.trackLoadTime(win.webContents.getURL(), loadTime);
|
perfMonitor.trackLoadTime(win.webContents.getURL(), loadTime);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Debounced history recording to prevent excessive I/O
|
// Renderer manages history; no main-process recording here
|
||||||
let historyTimeout;
|
|
||||||
const recordHistory = async (fileName, entry) => {
|
|
||||||
// Clear existing timeout
|
|
||||||
if (historyTimeout) {
|
|
||||||
clearTimeout(historyTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounce history recording by 500ms
|
|
||||||
historyTimeout = setTimeout(async () => {
|
|
||||||
if (fileName === 'site-history.json') {
|
|
||||||
// Save to both file and send to renderer
|
|
||||||
const filePath = path.join(__dirname, fileName);
|
|
||||||
let data = [];
|
|
||||||
try {
|
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
||||||
data = JSON.parse(fileContent);
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
if (data[0] !== entry) {
|
|
||||||
data.unshift(entry);
|
|
||||||
if (data.length > 100) data.pop();
|
|
||||||
|
|
||||||
// Use async file operations to prevent blocking
|
|
||||||
try {
|
|
||||||
await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2));
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error writing site history:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 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 {
|
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
||||||
data = JSON.parse(fileContent);
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
if (data[0] !== entry) {
|
|
||||||
data.unshift(entry);
|
|
||||||
if (data.length > 100) data.pop();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2));
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error writing search history:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
win.webContents.on('did-navigate', (event, url) => {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method will be called when Electron has finished initialization
|
// This method will be called when Electron has finished initialization
|
||||||
@@ -350,10 +227,9 @@ ipcMain.handle('window-close', event => {
|
|||||||
// Site history is now handled via localStorage in the renderer
|
// Site history is now handled via localStorage in the renderer
|
||||||
// But keep these handlers for compatibility and potential future use
|
// But keep these handlers for compatibility and potential future use
|
||||||
ipcMain.handle('load-site-history', async () => {
|
ipcMain.handle('load-site-history', async () => {
|
||||||
// Read from the site history file for settings page
|
|
||||||
const filePath = path.join(__dirname, 'site-history.json');
|
const filePath = path.join(__dirname, 'site-history.json');
|
||||||
try {
|
try {
|
||||||
const data = fs.readFileSync(filePath, 'utf-8');
|
const data = await fs.promises.readFile(filePath, 'utf-8');
|
||||||
return JSON.parse(data);
|
return JSON.parse(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return [];
|
return [];
|
||||||
@@ -361,10 +237,9 @@ ipcMain.handle('load-site-history', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('save-site-history', async (event, history) => {
|
ipcMain.handle('save-site-history', async (event, history) => {
|
||||||
// Save to both file and localStorage
|
|
||||||
const filePath = path.join(__dirname, 'site-history.json');
|
const filePath = path.join(__dirname, 'site-history.json');
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(filePath, JSON.stringify(history, null, 2));
|
await fs.promises.writeFile(filePath, JSON.stringify(history, null, 2));
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false;
|
return false;
|
||||||
@@ -374,7 +249,7 @@ ipcMain.handle('save-site-history', async (event, history) => {
|
|||||||
ipcMain.handle('clear-site-history', async () => {
|
ipcMain.handle('clear-site-history', async () => {
|
||||||
const filePath = path.join(__dirname, 'site-history.json');
|
const filePath = path.join(__dirname, 'site-history.json');
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(filePath, JSON.stringify([], null, 2));
|
await fs.promises.writeFile(filePath, JSON.stringify([], null, 2));
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false;
|
return false;
|
||||||
@@ -384,7 +259,7 @@ ipcMain.handle('clear-site-history', async () => {
|
|||||||
ipcMain.handle('load-search-history', async () => {
|
ipcMain.handle('load-search-history', async () => {
|
||||||
const filePath = path.join(__dirname, 'search-history.json');
|
const filePath = path.join(__dirname, 'search-history.json');
|
||||||
try {
|
try {
|
||||||
const data = fs.readFileSync(filePath, 'utf-8');
|
const data = await fs.promises.readFile(filePath, 'utf-8');
|
||||||
return JSON.parse(data);
|
return JSON.parse(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return [];
|
return [];
|
||||||
@@ -394,7 +269,7 @@ ipcMain.handle('load-search-history', async () => {
|
|||||||
ipcMain.handle('save-search-history', async (event, history) => {
|
ipcMain.handle('save-search-history', async (event, history) => {
|
||||||
const filePath = path.join(__dirname, 'search-history.json');
|
const filePath = path.join(__dirname, 'search-history.json');
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(filePath, JSON.stringify(history, null, 2));
|
await fs.promises.writeFile(filePath, JSON.stringify(history, null, 2));
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false;
|
return false;
|
||||||
@@ -410,24 +285,24 @@ ipcMain.on('homepage-changed', (event, url) => {
|
|||||||
ipcMain.handle('load-bookmarks', async () => {
|
ipcMain.handle('load-bookmarks', async () => {
|
||||||
try {
|
try {
|
||||||
const bookmarksPath = path.join(__dirname, 'bookmarks.json');
|
const bookmarksPath = path.join(__dirname, 'bookmarks.json');
|
||||||
if (fs.existsSync(bookmarksPath)) {
|
try {
|
||||||
const data = fs.readFileSync(bookmarksPath, 'utf8');
|
await fs.promises.access(bookmarksPath);
|
||||||
|
} catch {
|
||||||
|
console.log('No bookmarks file found, starting with empty array');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const data = await fs.promises.readFile(bookmarksPath, 'utf8');
|
||||||
const bookmarks = JSON.parse(data);
|
const bookmarks = JSON.parse(data);
|
||||||
console.log(`Loaded ${bookmarks.length} bookmarks from file`);
|
console.log(`Loaded ${bookmarks.length} bookmarks from file`);
|
||||||
return bookmarks;
|
return bookmarks;
|
||||||
}
|
|
||||||
console.log('No bookmarks file found, starting with empty array');
|
|
||||||
return [];
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading bookmarks:', error);
|
console.error('Error loading bookmarks:', error);
|
||||||
// Try to create a backup if the file is corrupted
|
// Try to create a backup if the file is corrupted
|
||||||
const bookmarksPath = path.join(__dirname, 'bookmarks.json');
|
const bookmarksPath = path.join(__dirname, 'bookmarks.json');
|
||||||
const backupPath = path.join(__dirname, `bookmarks.backup.${Date.now()}.json`);
|
const backupPath = path.join(__dirname, `bookmarks.backup.${Date.now()}.json`);
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(bookmarksPath)) {
|
await fs.promises.copyFile(bookmarksPath, backupPath);
|
||||||
fs.copyFileSync(bookmarksPath, backupPath);
|
|
||||||
console.log(`Corrupted bookmarks file backed up to: ${backupPath}`);
|
console.log(`Corrupted bookmarks file backed up to: ${backupPath}`);
|
||||||
}
|
|
||||||
} catch (backupError) {
|
} catch (backupError) {
|
||||||
console.error('Failed to create backup:', backupError);
|
console.error('Failed to create backup:', backupError);
|
||||||
}
|
}
|
||||||
@@ -438,14 +313,12 @@ ipcMain.handle('load-bookmarks', async () => {
|
|||||||
ipcMain.handle('save-bookmarks', async (event, bookmarks) => {
|
ipcMain.handle('save-bookmarks', async (event, bookmarks) => {
|
||||||
try {
|
try {
|
||||||
const bookmarksPath = path.join(__dirname, 'bookmarks.json');
|
const bookmarksPath = path.join(__dirname, 'bookmarks.json');
|
||||||
|
try {
|
||||||
// Create a backup before saving
|
await fs.promises.access(bookmarksPath);
|
||||||
if (fs.existsSync(bookmarksPath)) {
|
|
||||||
const backupPath = path.join(__dirname, 'bookmarks.backup.json');
|
const backupPath = path.join(__dirname, 'bookmarks.backup.json');
|
||||||
fs.copyFileSync(bookmarksPath, backupPath);
|
await fs.promises.copyFile(bookmarksPath, backupPath);
|
||||||
}
|
} catch {}
|
||||||
|
await fs.promises.writeFile(bookmarksPath, JSON.stringify(bookmarks, null, 2));
|
||||||
fs.writeFileSync(bookmarksPath, JSON.stringify(bookmarks, null, 2));
|
|
||||||
console.log(`Saved ${bookmarks.length} bookmarks to file`);
|
console.log(`Saved ${bookmarks.length} bookmarks to file`);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -515,20 +388,15 @@ ipcMain.handle('save-site-history-entry', async (event, url) => {
|
|||||||
try {
|
try {
|
||||||
let data = [];
|
let data = [];
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
const raw = await fs.promises.readFile(filePath, 'utf8');
|
||||||
|
data = JSON.parse(raw);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
// Remove if already exists to avoid duplicates
|
// Remove if already exists to avoid duplicates
|
||||||
data = data.filter(item => item !== url);
|
data = data.filter(item => item !== url);
|
||||||
// Add to beginning
|
// Add to beginning and clamp size
|
||||||
data.unshift(url);
|
data.unshift(url);
|
||||||
// Keep only last 100 entries
|
if (data.length > 100) data = data.slice(0, 100);
|
||||||
if (data.length > 100) {
|
await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2));
|
||||||
data = data.slice(0, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
||||||
console.log('[MAIN] Saved site history entry:', url);
|
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[MAIN] Error saving site history entry:', err);
|
console.error('[MAIN] Error saving site history entry:', err);
|
||||||
|
|||||||
+35
-79
@@ -53,6 +53,30 @@ let activeTabId = null;
|
|||||||
const allowedInternalPages = ['settings', 'home'];
|
const allowedInternalPages = ['settings', 'home'];
|
||||||
let bookmarks = [];
|
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
|
// Load bookmarks on startup
|
||||||
async function loadBookmarks() {
|
async function loadBookmarks() {
|
||||||
try {
|
try {
|
||||||
@@ -109,7 +133,7 @@ function createTab(inputUrl) {
|
|||||||
tabs.push(tab);
|
tabs.push(tab);
|
||||||
setActiveTab(id);
|
setActiveTab(id);
|
||||||
// Render the tab bar so the new home tab appears
|
// Render the tab bar so the new home tab appears
|
||||||
renderTabs();
|
scheduleRenderTabs();
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +232,7 @@ function createTab(inputUrl) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setActiveTab(id);
|
setActiveTab(id);
|
||||||
renderTabs();
|
scheduleRenderTabs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -236,7 +260,7 @@ function updateTabMetadata(id, key, value) {
|
|||||||
const tab = tabs.find(t => t.id === id);
|
const tab = tabs.find(t => t.id === id);
|
||||||
if (tab) {
|
if (tab) {
|
||||||
tab[key] = value;
|
tab[key] = value;
|
||||||
renderTabs();
|
scheduleRenderTabs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +300,7 @@ function navigate() {
|
|||||||
tab.url = input;
|
tab.url = input;
|
||||||
webview.src = resolved;
|
webview.src = resolved;
|
||||||
|
|
||||||
renderTabs();
|
scheduleRenderTabs();
|
||||||
updateNavButtons();
|
updateNavButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,7 +387,7 @@ function convertHomeTabToWebview(tabId, inputUrl, resolvedUrl) {
|
|||||||
updateNavButtons();
|
updateNavButtons();
|
||||||
// Activate converted webview tab and update UI
|
// Activate converted webview tab and update UI
|
||||||
setActiveTab(tabId);
|
setActiveTab(tabId);
|
||||||
renderTabs();
|
scheduleRenderTabs();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNavigation(tabId, newUrl) {
|
function handleNavigation(tabId, newUrl) {
|
||||||
@@ -406,7 +430,7 @@ function handleNavigation(tabId, newUrl) {
|
|||||||
urlBox.value = displayUrl === 'browser://home' ? '' : displayUrl;
|
urlBox.value = displayUrl === 'browser://home' ? '' : displayUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTabs();
|
scheduleRenderTabs();
|
||||||
updateNavButtons();
|
updateNavButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,7 +463,7 @@ function setActiveTab(id) {
|
|||||||
if (tab) {
|
if (tab) {
|
||||||
// If the tab URL represents the home page, keep the URL bar blank.
|
// If the tab URL represents the home page, keep the URL bar blank.
|
||||||
urlBox.value = tab.url === 'browser://home' ? '' : tab.url;
|
urlBox.value = tab.url === 'browser://home' ? '' : tab.url;
|
||||||
renderTabs();
|
scheduleRenderTabs();
|
||||||
updateNavButtons();
|
updateNavButtons();
|
||||||
updateZoomUI(); // ← update zoom display for new active tab
|
updateZoomUI(); // ← update zoom display for new active tab
|
||||||
}
|
}
|
||||||
@@ -455,7 +479,7 @@ function closeTab(id) {
|
|||||||
if (tabs.length > 0) setActiveTab(tabs[0].id);
|
if (tabs.length > 0) setActiveTab(tabs[0].id);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTabs();
|
scheduleRenderTabs();
|
||||||
updateNavButtons();
|
updateNavButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +499,7 @@ function renderTabs() {
|
|||||||
el.appendChild(icon);
|
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');
|
const closeBtn = document.createElement('button');
|
||||||
closeBtn.textContent = '×';
|
closeBtn.textContent = '×';
|
||||||
@@ -572,17 +596,7 @@ menuBtn.addEventListener('click', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
// Add some debug info
|
// Initial boot
|
||||||
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();
|
createTab();
|
||||||
// Handle IPC messages from the static home webview (bookmarks navigation)
|
// Handle IPC messages from the static home webview (bookmarks navigation)
|
||||||
const staticHome = document.getElementById('home-webview');
|
const staticHome = document.getElementById('home-webview');
|
||||||
@@ -651,65 +665,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
document.getElementById('zoom-percent').textContent = `${Math.round(z * 100)}%`;
|
document.getElementById('zoom-percent').textContent = `${Math.round(z * 100)}%`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// menu‐related code (moved here so #context-menu exists)
|
// (Removed broken duplicate context menu wiring)
|
||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Migrate existing site history from JSON file to localStorage (one-time migration)
|
// Migrate existing site history from JSON file to localStorage (one-time migration)
|
||||||
const migrateSiteHistory = async () => {
|
const migrateSiteHistory = async () => {
|
||||||
|
|||||||
+77
-5
@@ -29,11 +29,73 @@ body {
|
|||||||
|
|
||||||
.container {
|
.container {
|
||||||
background-color: var(--dark-purple);
|
background-color: var(--dark-purple);
|
||||||
padding: 2rem;
|
padding: 0;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 0 10px rgba(0,0,0,0.5);
|
box-shadow: 0 0 10px rgba(0,0,0,0.5);
|
||||||
max-width: 500px;
|
max-width: 1100px;
|
||||||
width: 100%;
|
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 {
|
h1 {
|
||||||
@@ -132,10 +194,20 @@ button:hover {
|
|||||||
/* small-screen adjustments */
|
/* small-screen adjustments */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.container {
|
.container {
|
||||||
padding: 1rem;
|
padding: 0;
|
||||||
border-radius: 0;
|
border-radius: 12px;
|
||||||
box-shadow: none;
|
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 {
|
h1 {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-16
@@ -240,22 +240,32 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container" role="application">
|
||||||
<h1>⚙️ Browser Settings</h1>
|
<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>
|
||||||
|
|
||||||
|
<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">
|
<div class="setting-group">
|
||||||
<label for="clear-data-btn">Clear All Cookies & Data</label>
|
<label for="clear-data-btn">Clear All Cookies & Data</label>
|
||||||
<button id="clear-data-btn">Clear Data</button>
|
<button id="clear-data-btn">Clear Data</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="note">Settings are stored locally on this device.</p>
|
<p class="note">Settings are stored locally on this device.</p>
|
||||||
|
|
||||||
<div class="debug-info" id="debug-info">Loading debug info...</div>
|
<div class="debug-info" id="debug-info">Loading debug info...</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Customization Section -->
|
<!-- Appearance Panel -->
|
||||||
<section>
|
<section class="tab-panel" id="panel-appearance" role="tabpanel" aria-labelledby="tab-appearance">
|
||||||
<h2>🎨 Browser Customization</h2>
|
<h2>🎨 Appearance</h2>
|
||||||
|
|
||||||
<!-- Theme Selection -->
|
<!-- Theme Selection -->
|
||||||
<div class="customization-group">
|
<div class="customization-group">
|
||||||
<h3>Theme Presets</h3>
|
<h3>Theme Presets</h3>
|
||||||
@@ -406,18 +416,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- add history views -->
|
<!-- History Panel -->
|
||||||
<section>
|
<section class="tab-panel" id="panel-history" role="tabpanel" aria-labelledby="tab-history">
|
||||||
<h2>Search History</h2>
|
<h2>History</h2>
|
||||||
|
<div class="customization-group">
|
||||||
|
<h3>Search History</h3>
|
||||||
<ul id="search-history-list"></ul>
|
<ul id="search-history-list"></ul>
|
||||||
<button id="clear-search-history-btn" style="margin-top: 10px;">Clear Search History</button>
|
<button id="clear-search-history-btn" style="margin-top: 10px;">Clear Search History</button>
|
||||||
</section>
|
</div>
|
||||||
<section>
|
<div class="customization-group">
|
||||||
<h2>Site History</h2>
|
<h3>Site History</h3>
|
||||||
<ul id="site-history-list"></ul>
|
<ul id="site-history-list"></ul>
|
||||||
<button id="clear-site-history-btn" style="margin-top: 10px;">Clear Site History</button>
|
<div style="display:flex; gap:10px; margin-top:10px; flex-wrap:wrap;">
|
||||||
<button id="add-test-history-btn" style="margin-top: 10px; margin-left: 10px;">Add Test History</button>
|
<button id="clear-site-history-btn">Clear Site History</button>
|
||||||
|
<button id="add-test-history-btn">Add Test History</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- status overlay moved outside of .container -->
|
<!-- status overlay moved outside of .container -->
|
||||||
|
|||||||
+122
-7
@@ -1,10 +1,30 @@
|
|||||||
// Use require('electron') since webviews have nodeIntegrationInSubFrames: true
|
// Try to get ipcRenderer, but don't fail if it's not available
|
||||||
// In settings webview we use the same preload API as main windows
|
let ipcRenderer = null;
|
||||||
const { ipcRenderer } = require('electron');
|
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 statusDiv = document.getElementById('status');
|
||||||
const statusText = document.getElementById('status-text');
|
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) {
|
function showStatus(message) {
|
||||||
statusText.textContent = message;
|
statusText.textContent = message;
|
||||||
@@ -14,23 +34,118 @@ function showStatus(message) {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearBtn.onclick = async () => {
|
function attachClearHandler(btn) {
|
||||||
|
if (!btn) return;
|
||||||
|
btn.onclick = async () => {
|
||||||
|
if (statusDiv && statusText) {
|
||||||
statusDiv.classList.remove('hidden');
|
statusDiv.classList.remove('hidden');
|
||||||
statusText.textContent = 'Clearing all browser data...';
|
statusText.textContent = 'Clearing all browser data...';
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (ipcRenderer) {
|
||||||
const ok = await ipcRenderer.invoke('clear-browser-data');
|
const ok = await ipcRenderer.invoke('clear-browser-data');
|
||||||
showStatus(ok
|
showStatus(ok
|
||||||
? 'All browser data and bookmarks cleared!'
|
? 'All browser data and bookmarks cleared!'
|
||||||
: 'Failed to clear browser data.');
|
: 'Failed to clear browser data.');
|
||||||
|
} else {
|
||||||
|
showStatus('Clear data feature not available in this context.');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error clearing browser data:', error);
|
console.error('Error clearing browser data:', error);
|
||||||
showStatus('An error occurred while clearing data.');
|
showStatus('An error occurred while clearing data.');
|
||||||
} finally {
|
} finally {
|
||||||
// Send theme update to host after clearing
|
|
||||||
const currentTheme = window.browserCustomizer ? window.browserCustomizer.currentTheme : null;
|
const currentTheme = window.browserCustomizer ? window.browserCustomizer.currentTheme : null;
|
||||||
if (currentTheme && window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
|
if (currentTheme && window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
|
||||||
window.electronAPI.sendToHost('theme-update', currentTheme);
|
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');
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
[
|
[
|
||||||
|
"https://www.youtube.com/"
|
||||||
]
|
]
|
||||||
Reference in New Issue
Block a user