Add local file open support and improve URL handling

Introduces a dialog to open local HTML files via Ctrl+O/Cmd+O, converting file paths to file:// URLs for navigation. Also improves input handling by stripping quotes and better detecting local file paths. Updates bookmarks and clears site history.
This commit is contained in:
2025-08-15 15:43:05 +12:00
parent 47834020b2
commit 4b2f70b53d
5 changed files with 84 additions and 10 deletions
+3 -3
View File
@@ -1,8 +1,8 @@
[ [
{ {
"title": "Apple", "title": "Discord",
"url": "apple.com", "url": "discord.gg",
"icon": "data:image/svg+xml;utf8,%3Csvg%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20style%3D%22color%3A%20white%3B%22%3E%3Ctitle%3EApple%3C%2Ftitle%3E%3Cpath%20d%3D%22M12.152%206.896c-.948%200-2.415-1.078-3.96-1.04-2.04.027-3.91%201.183-4.961%203.014-2.117%203.675-.546%209.103%201.519%2012.09%201.013%201.454%202.208%203.09%203.792%203.039%201.52-.065%202.09-.987%203.935-.987%201.831%200%202.35.987%203.96.948%201.637-.026%202.676-1.48%203.676-2.948%201.156-1.688%201.636-3.325%201.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04%202.48-4.494%202.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675%201.09-4.61%201.09zM15.53%203.83c.843-1.012%201.4-2.427%201.245-3.83-1.207.052-2.662.805-3.532%201.818-.78.896-1.454%202.338-1.273%203.714%201.338.104%202.715-.688%203.559-1.701%22%2F%3E%3C%2Fsvg%3E", "icon": "data:image/svg+xml;utf8,%3Csvg%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EDiscord%3C%2Ftitle%3E%3Cpath%20d%3D%22M20.317%204.3698a19.7913%2019.7913%200%2000-4.8851-1.5152.0741.0741%200%2000-.0785.0371c-.211.3753-.4447.8648-.6083%201.2495-1.8447-.2762-3.68-.2762-5.4868%200-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077%200%2000-.0785-.037%2019.7363%2019.7363%200%2000-4.8852%201.515.0699.0699%200%2000-.0321.0277C.5334%209.0458-.319%2013.5799.0992%2018.0578a.0824.0824%200%2000.0312.0561c2.0528%201.5076%204.0413%202.4228%205.9929%203.0294a.0777.0777%200%2000.0842-.0276c.4616-.6304.8731-1.2952%201.226-1.9942a.076.076%200%2000-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077%200%2001-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743%200%2001.0776-.0105c3.9278%201.7933%208.18%201.7933%2012.0614%200a.0739.0739%200%2001.0785.0095c.1202.099.246.1981.3728.2924a.077.077%200%2001-.0066.1276%2012.2986%2012.2986%200%2001-1.873.8914.0766.0766%200%2000-.0407.1067c.3604.698.7719%201.3628%201.225%201.9932a.076.076%200%2000.0842.0286c1.961-.6067%203.9495-1.5219%206.0023-3.0294a.077.077%200%2000.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061%200%2000-.0312-.0286zM8.02%2015.3312c-1.1825%200-2.1569-1.0857-2.1569-2.419%200-1.3332.9555-2.4189%202.157-2.4189%201.2108%200%202.1757%201.0952%202.1568%202.419%200%201.3332-.9555%202.4189-2.1569%202.4189zm7.9748%200c-1.1825%200-2.1569-1.0857-2.1569-2.419%200-1.3332.9554-2.4189%202.1569-2.4189%201.2108%200%202.1757%201.0952%202.1568%202.419%200%201.3332-.946%202.4189-2.1568%202.4189Z%22%2F%3E%3C%2Fsvg%3E",
"iconSet": "simple" "iconSet": "simple"
} }
] ]
+28 -1
View File
@@ -1,4 +1,5 @@
const { app, BrowserWindow, ipcMain, session, screen, shell } = require('electron'); const { app, BrowserWindow, ipcMain, session, screen, shell, dialog } = require('electron');
const { pathToFileURL } = require('url');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const os = require('os'); const os = require('os');
@@ -548,3 +549,29 @@ ipcMain.handle('open-devtools', (event) => {
} }
return contents.isDevToolsOpened(); return contents.isDevToolsOpened();
}); });
// Open local file dialog -> returns file:// URL (or null if cancelled)
ipcMain.handle('show-open-file-dialog', async () => {
try {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [
{ name: 'HTML Files', extensions: ['html', 'htm', 'xhtml'] },
{ name: 'All Files', extensions: ['*'] }
]
});
if (result.canceled || !result.filePaths || !result.filePaths.length) return null;
const filePath = result.filePaths[0];
try {
return pathToFileURL(filePath).href;
} catch {
// Fallback manual conversion
let p = filePath.replace(/\\/g, '/');
if (!p.startsWith('/')) p = '/' + p; // ensure leading slash for drive letters
return 'file://' + (p.startsWith('/') ? '/' : '') + p; // double slash safety
}
} catch (err) {
console.error('open-file dialog failed:', err);
return null;
}
});
+8
View File
@@ -55,6 +55,14 @@ const electronAPI = {
console.error('IPC open-devtools error:', err); console.error('IPC open-devtools error:', err);
return Promise.reject(err); return Promise.reject(err);
} }
},
openLocalFile: async () => {
try {
return await ipcRenderer.invoke('show-open-file-dialog');
} catch (err) {
console.error('IPC openLocalFile error:', err);
return null;
}
} }
}; };
+44 -2
View File
@@ -295,16 +295,43 @@ function updateTabMetadata(id, key, value) {
} }
function navigate() { function navigate() {
const input = urlBox.value.trim(); const rawInput = urlBox.value.trim();
// Strip surrounding single or double quotes (common when copying paths)
let input = rawInput;
if ((input.startsWith('"') && input.endsWith('"')) || (input.startsWith("'") && input.endsWith("'"))) {
input = input.slice(1, -1);
}
// If we modified input (removed quotes), reflect it back in the UI for clarity
if (input !== rawInput) {
urlBox.value = input;
}
const tab = tabs.find(t => t.id === activeTabId); const tab = tabs.find(t => t.id === activeTabId);
if (!tab) return; if (!tab) return;
// decide if this is a search query or a URL/internal page // decide if this is a search query or a URL/internal page
const hasProtocol = /^https?:\/\//i.test(input); const hasProtocol = /^https?:\/\//i.test(input);
const isFileProtocol = /^file:\/\//i.test(input);
const looksLikeLocalPath = /^(?:[A-Za-z]:\\|\\\\|\/?)[^?]*\.(?:x?html?)$/i.test(input);
const isInternal = input.startsWith('browser://'); const isInternal = input.startsWith('browser://');
const isLikelyUrl = hasProtocol || input.includes('.'); const isLikelyUrl = hasProtocol || input.includes('.');
let resolved; let resolved;
if (!isInternal && !isLikelyUrl) { if (isFileProtocol) {
resolved = input; // Electron will load file:// directly in <webview>
} else if (looksLikeLocalPath) {
// Convert Windows or *nix style path to file:// URL
let p = input;
// Expand backslashes
p = p.replace(/\\/g, '/');
// If it starts with a drive letter like C:/ ensure single leading slash
if (/^[A-Za-z]:\//.test(p)) {
resolved = 'file:///' + encodeURI(p);
} else if (p.startsWith('/')) {
resolved = 'file://' + encodeURI(p); // already absolute
} else {
// relative path relative to app root (renderer directory)
resolved = 'file://' + encodeURI(p); // fallback; treat as relative from working dir
}
} else if (!isInternal && !isLikelyUrl) {
resolved = `https://www.google.com/search?q=${encodeURIComponent(input)}`; resolved = `https://www.google.com/search?q=${encodeURIComponent(input)}`;
} else { } else {
resolved = resolveInternalUrl(input); resolved = resolveInternalUrl(input);
@@ -334,6 +361,21 @@ function navigate() {
scheduleUpdateNavButtons(); scheduleUpdateNavButtons();
} }
// Keyboard shortcut: Ctrl+O (Cmd+O on mac) to open a local file
document.addEventListener('keydown', async (e) => {
const isAccel = (navigator.platform.includes('Mac') ? e.metaKey : e.ctrlKey);
if (isAccel && e.key.toLowerCase() === 'o') {
e.preventDefault();
if (window.electronAPI && window.electronAPI.openLocalFile) {
const fileUrl = await window.electronAPI.openLocalFile();
if (fileUrl) {
urlBox.value = fileUrl;
navigate();
}
}
}
});
function convertHomeTabToWebview(tabId, inputUrl, resolvedUrl) { function convertHomeTabToWebview(tabId, inputUrl, resolvedUrl) {
const tab = tabs.find(t => t.id === tabId); const tab = tabs.find(t => t.id === tabId);
if (!tab) return; if (!tab) return;
+1 -4
View File
@@ -1,4 +1 @@
[ []
"https://discord.com/",
"https://nebula.zambazosmedia.group/"
]