diff --git a/main.js b/main.js index 2958f6c..38b1256 100644 --- a/main.js +++ b/main.js @@ -643,6 +643,7 @@ function buildAndShowContextMenu(sender, params = {}) { template.push( { label: 'Open Image in New Tab', click: () => embedder.send('context-menu-command', { cmd: 'open-image-new-tab', url: imageURL }) }, { label: 'Copy Image Address', click: () => clipboard.writeText(imageURL) }, + { label: 'Save Image As...', click: () => embedder.send('context-menu-command', { cmd: 'save-image', url: imageURL, mime: params.mediaType === 'image' ? params.mimeType : undefined }) }, { type: 'separator' } ); } @@ -688,3 +689,59 @@ app.on('web-contents-created', (event, contents) => { buildAndShowContextMenu(contents, params); }); }); + +// --- Image save handlers --- +ipcMain.handle('save-image-from-dataurl', async (event, { suggestedName = 'image', dataUrl }) => { + try { + if (!dataUrl || !dataUrl.startsWith('data:')) return false; + const match = /^data:(.*?);base64,(.*)$/.exec(dataUrl); + if (!match) return false; + const mime = match[1] || 'application/octet-stream'; + const ext = (mime.split('/')[1] || 'png').split(';')[0]; + const buf = Buffer.from(match[2], 'base64'); + const win = BrowserWindow.fromWebContents(event.sender.hostWebContents || event.sender); + const { canceled, filePath } = await dialog.showSaveDialog(win, { defaultPath: `${suggestedName}.${ext}` }); + if (canceled || !filePath) return false; + await fs.promises.writeFile(filePath, buf); + return true; + } catch (err) { + console.error('save-image-from-dataurl failed:', err); + return false; + } +}); + +ipcMain.handle('save-image-from-url', async (event, { url }) => { + if (!url) return false; + const win = BrowserWindow.fromWebContents(event.sender.hostWebContents || event.sender); + try { + let dataBuf; + if (url.startsWith('http')) { + const res = await fetch(url); + if (!res.ok) throw new Error('HTTP '+res.status); + const arrayBuf = await res.arrayBuffer(); + dataBuf = Buffer.from(arrayBuf); + const ctype = res.headers.get('content-type') || 'application/octet-stream'; + const ext = (ctype.split('/')[1] || 'png').split(';')[0]; + const { canceled, filePath } = await dialog.showSaveDialog(win, { defaultPath: `image.${ext}` }); + if (canceled || !filePath) return false; + await fs.promises.writeFile(filePath, dataBuf); + return true; + } else if (url.startsWith('data:')) { + // Forward to dataURL handler path – easier to keep logic single + return ipcMain.emit('save-image-from-dataurl', event, { dataUrl: url }); + } else if (url.startsWith('file:')) { + // Copy file to chosen destination + const filePathSrc = new URL(url).pathname.replace(/^\//, ''); + const base = path.basename(filePathSrc); + const { canceled, filePath } = await dialog.showSaveDialog(win, { defaultPath: base }); + if (canceled || !filePath) return false; + await fs.promises.copyFile(filePathSrc, filePath); + return true; + } else { + return false; + } + } catch (err) { + console.error('save-image-from-url failed:', err); + return false; + } +}); diff --git a/preload.js b/preload.js index 91d7b49..fafa572 100644 --- a/preload.js +++ b/preload.js @@ -70,7 +70,9 @@ const electronAPI = { } catch (err) { console.error('IPC showContextMenu error:', err); } - } + }, + saveImageToDisk: async (suggestedName, dataUrl) => ipcRenderer.invoke('save-image-from-dataurl', { suggestedName, dataUrl }), + saveImageFromNet: async (url) => ipcRenderer.invoke('save-image-from-url', { url }) }; // Cache for bookmarks to reduce IPC calls diff --git a/renderer/script.js b/renderer/script.js index 31e6f9b..4787289 100644 --- a/renderer/script.js +++ b/renderer/script.js @@ -87,6 +87,9 @@ function getTabLabel(tab) { if (tab.title && tab.title !== 'New Tab') return tab.title; const u = tab.url || ''; try { + if (u.startsWith('data:image')) return 'Image'; + if (u.startsWith('data:')) return 'Data'; + if (u.startsWith('blob:')) return 'Resource'; if (u.startsWith('http')) return new URL(u).hostname; if (u.startsWith('browser://')) return u.replace('browser://', ''); return u || 'New Tab'; @@ -156,7 +159,17 @@ function createTab(inputUrl) { } // For all other URLs, use webview - const resolvedUrl = resolveInternalUrl(inputUrl); + let resolvedUrl = resolveInternalUrl(inputUrl); + // If it's a raw data: URL (image) keep as is; blob: will only resolve within its origin context (may fail) + // For very long data URLs we could embed them in a minimal viewer page for cleaner rendering. + if (resolvedUrl.startsWith('data:') && resolvedUrl.length > 4096) { + // Create a simple object URL page to avoid huge URL in the address bar (cannot easily persist across restarts). + const html = `
`+ + `