Style Holesail tunnel browser with peardock titlebar and pear-ctrl
Release rolling / release (push) Successful in 7m37s
Release rolling / release (push) Successful in 7m37s
Open local tunnel URLs in a framed Electron window that matches main app chrome (hiddenInset/frameless, pear-ctrl, brand) instead of a bare popup.
This commit is contained in:
+300
-24
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
'use strict'
|
||||
|
||||
const { app, BrowserWindow, ipcMain, session, Menu } = require('electron')
|
||||
const { app, BrowserWindow, BrowserView, ipcMain, session, Menu } = require('electron')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
@@ -223,12 +223,10 @@ function resolveAppIcon() {
|
||||
return undefined
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
const icon = resolveAppIcon()
|
||||
/** Shared window chrome: hiddenInset (macOS) / frameless (win/linux) + pear-ctrl. */
|
||||
function windowChromeOpts() {
|
||||
const isDarwin = process.platform === 'darwin'
|
||||
// Match Pear GUI chrome: no separate native title strip — traffic lights /
|
||||
// window buttons live inside #titlebar (see pear-ctrl in preload + CSS).
|
||||
const chromeOpts = isDarwin
|
||||
return isDarwin
|
||||
? {
|
||||
titleBarStyle: 'hiddenInset',
|
||||
// Align OS traffic lights with 42px #titlebar (see --titlebar-h)
|
||||
@@ -238,7 +236,216 @@ function createWindow() {
|
||||
// Windows / Linux: frameless; pear-ctrl renders min/max/close
|
||||
frame: false,
|
||||
}
|
||||
}
|
||||
|
||||
function defaultWebPreferences() {
|
||||
return {
|
||||
preload: buildPreloadPath(),
|
||||
// Full peardock UI imports HyperDHT etc. as ESM node packages — same as pear UI.
|
||||
// Tunnel shell also needs nodeIntegration so the pear-ctrl polyfill / IPC works.
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
sandbox: false,
|
||||
spellcheck: false,
|
||||
webSecurity: true,
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Map<string, import('electron').BrowserWindow>} */
|
||||
const tunnelWindows = new Map()
|
||||
|
||||
function isLocalTunnelUrl(href) {
|
||||
try {
|
||||
const u = new URL(String(href || ''))
|
||||
if (u.protocol !== 'http:' && u.protocol !== 'https:') return false
|
||||
const host = (u.hostname || '').toLowerCase()
|
||||
return host === '127.0.0.1' || host === 'localhost' || host === '::1' || host === '[::1]'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Titlebar (42) + tunnel toolbar (6+28+6=40) — keep in sync with tunnel-browser.html */
|
||||
const TUNNEL_CHROME_H = 42 + 40
|
||||
|
||||
/**
|
||||
* Open (or focus) a peardock-styled BrowserWindow for a local Holesail tunnel URL.
|
||||
* Shell HTML matches main #titlebar + pear-ctrl; tunnel content loads in a
|
||||
* BrowserView (avoids X-Frame-Options blocking iframes).
|
||||
* @param {string} targetUrl
|
||||
* @param {{ title?: string }} [opts]
|
||||
*/
|
||||
function createTunnelWindow(targetUrl, opts = {}) {
|
||||
const href = String(targetUrl || '').trim()
|
||||
if (!href || !isLocalTunnelUrl(href)) {
|
||||
console.warn('[peardock] refused non-local tunnel URL:', href)
|
||||
return null
|
||||
}
|
||||
if (!staticPort) {
|
||||
console.warn('[peardock] static server not ready; cannot open tunnel browser')
|
||||
return null
|
||||
}
|
||||
|
||||
const existing = tunnelWindows.get(href)
|
||||
if (existing && !existing.isDestroyed()) {
|
||||
if (existing.isMinimized()) existing.restore()
|
||||
existing.focus()
|
||||
return existing
|
||||
}
|
||||
|
||||
const icon = resolveAppIcon()
|
||||
const titleHint = String(opts.title || '').trim()
|
||||
const win = new BrowserWindow({
|
||||
width: 1100,
|
||||
height: 800,
|
||||
minWidth: 640,
|
||||
minHeight: 420,
|
||||
backgroundColor: pkg.pear?.gui?.backgroundColor || '#0f1117',
|
||||
title: titleHint ? `${titleHint} — peardock tunnel` : 'Holesail tunnel — peardock',
|
||||
...(icon ? { icon } : {}),
|
||||
autoHideMenuBar: process.platform !== 'darwin',
|
||||
...windowChromeOpts(),
|
||||
webPreferences: defaultWebPreferences(),
|
||||
show: false,
|
||||
})
|
||||
|
||||
// Content view: real navigation (no iframe / X-Frame-Options issues)
|
||||
const view = new BrowserView({
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
sandbox: true,
|
||||
spellcheck: false,
|
||||
webSecurity: true,
|
||||
},
|
||||
})
|
||||
win.setBrowserView(view)
|
||||
/** @type {{ view: import('electron').BrowserView, url: string }} */
|
||||
win.__pdTunnel = { view, url: href }
|
||||
|
||||
const layoutView = () => {
|
||||
if (win.isDestroyed()) return
|
||||
const [width, height] = win.getContentSize()
|
||||
view.setBounds({
|
||||
x: 0,
|
||||
y: TUNNEL_CHROME_H,
|
||||
width,
|
||||
height: Math.max(0, height - TUNNEL_CHROME_H),
|
||||
})
|
||||
}
|
||||
layoutView()
|
||||
win.on('resize', layoutView)
|
||||
|
||||
const pushNavState = () => {
|
||||
if (win.isDestroyed()) return
|
||||
const wc = view.webContents
|
||||
const state = {
|
||||
url: wc.getURL() || href,
|
||||
title: wc.getTitle() || titleHint || 'Holesail',
|
||||
canGoBack: wc.canGoBack(),
|
||||
canGoForward: wc.canGoForward(),
|
||||
loading: wc.isLoading(),
|
||||
}
|
||||
try {
|
||||
win.webContents.send('peardock:tunnel-nav-state', state)
|
||||
} catch {
|
||||
// shell not ready
|
||||
}
|
||||
try {
|
||||
if (state.title) win.setTitle(`${state.title} — peardock tunnel`)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
view.webContents.on('did-start-loading', pushNavState)
|
||||
view.webContents.on('did-stop-loading', pushNavState)
|
||||
view.webContents.on('did-navigate', pushNavState)
|
||||
view.webContents.on('did-navigate-in-page', pushNavState)
|
||||
view.webContents.on('page-title-updated', pushNavState)
|
||||
view.webContents.on('did-fail-load', (_e, code, desc, validatedURL, isMainFrame) => {
|
||||
if (!isMainFrame) return
|
||||
try {
|
||||
win.webContents.send('peardock:tunnel-nav-state', {
|
||||
url: validatedURL || href,
|
||||
title: 'Failed to load',
|
||||
canGoBack: view.webContents.canGoBack(),
|
||||
canGoForward: view.webContents.canGoForward(),
|
||||
loading: false,
|
||||
error: desc || `Error ${code}`,
|
||||
})
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
})
|
||||
|
||||
view.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (isLocalTunnelUrl(url)) {
|
||||
createTunnelWindow(url)
|
||||
return { action: 'deny' }
|
||||
}
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
tunnelWindows.set(href, win)
|
||||
win.on('closed', () => {
|
||||
if (tunnelWindows.get(href) === win) tunnelWindows.delete(href)
|
||||
try {
|
||||
win.setBrowserView(null)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
})
|
||||
|
||||
win.once('ready-to-show', () => {
|
||||
win.show()
|
||||
layoutView()
|
||||
})
|
||||
|
||||
const shell = new URL(`http://127.0.0.1:${staticPort}/tunnel-browser.html`)
|
||||
shell.searchParams.set('url', href)
|
||||
shell.searchParams.set('mode', 'view')
|
||||
if (titleHint) shell.searchParams.set('title', titleHint)
|
||||
|
||||
win
|
||||
.loadURL(shell.href)
|
||||
.then(() => {
|
||||
layoutView()
|
||||
return view.webContents.loadURL(href)
|
||||
})
|
||||
.then(() => pushNavState())
|
||||
.catch((err) => {
|
||||
console.error('[peardock] failed to load tunnel browser:', err)
|
||||
try {
|
||||
win.webContents.send('peardock:tunnel-nav-state', {
|
||||
url: href,
|
||||
title: 'Failed to load',
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
loading: false,
|
||||
error: String(err?.message || err),
|
||||
})
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
})
|
||||
|
||||
// Nested window.open from shell chrome only
|
||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (isLocalTunnelUrl(url)) {
|
||||
createTunnelWindow(url)
|
||||
return { action: 'deny' }
|
||||
}
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
return win
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
const icon = resolveAppIcon()
|
||||
// Match Pear GUI chrome: no separate native title strip — traffic lights /
|
||||
// window buttons live inside #titlebar (see pear-ctrl in preload + CSS).
|
||||
const win = new BrowserWindow({
|
||||
width: pkg.pear?.gui?.width || 1280,
|
||||
height: pkg.pear?.gui?.height || 800,
|
||||
@@ -248,16 +455,8 @@ function createWindow() {
|
||||
// Hide the default Electron menu bar (File/Edit/View/…) on Linux/Windows.
|
||||
// macOS keeps the system menu bar; non-darwin has no app menu (set in whenReady).
|
||||
autoHideMenuBar: process.platform !== 'darwin',
|
||||
...chromeOpts,
|
||||
webPreferences: {
|
||||
preload: buildPreloadPath(),
|
||||
// Full peardock UI imports HyperDHT etc. as ESM node packages — same as pear UI.
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
sandbox: false,
|
||||
spellcheck: false,
|
||||
webSecurity: true,
|
||||
},
|
||||
...windowChromeOpts(),
|
||||
webPreferences: defaultWebPreferences(),
|
||||
show: false,
|
||||
})
|
||||
|
||||
@@ -273,6 +472,40 @@ function createWindow() {
|
||||
console.error('did-fail-load', code, desc)
|
||||
})
|
||||
|
||||
// Holesail tryOpenBrowser / window.open → styled tunnel shell (not bare Chromium popup)
|
||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||
try {
|
||||
const u = new URL(url)
|
||||
// Direct open of our shell page: allow with matching chrome
|
||||
if (
|
||||
u.hostname === '127.0.0.1' &&
|
||||
u.port === String(staticPort) &&
|
||||
u.pathname.endsWith('/tunnel-browser.html')
|
||||
) {
|
||||
return {
|
||||
action: 'allow',
|
||||
overrideBrowserWindowOptions: {
|
||||
width: 1100,
|
||||
height: 800,
|
||||
minWidth: 640,
|
||||
minHeight: 420,
|
||||
backgroundColor: pkg.pear?.gui?.backgroundColor || '#0f1117',
|
||||
autoHideMenuBar: process.platform !== 'darwin',
|
||||
...windowChromeOpts(),
|
||||
webPreferences: defaultWebPreferences(),
|
||||
},
|
||||
}
|
||||
}
|
||||
if (isLocalTunnelUrl(url)) {
|
||||
createTunnelWindow(url)
|
||||
return { action: 'deny' }
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
return win
|
||||
}
|
||||
|
||||
@@ -291,27 +524,70 @@ ipcMain.handle('peardock:exit', () => {
|
||||
})
|
||||
|
||||
/** Window chrome controls for frameless (win/linux) and general shell actions */
|
||||
function windowFromEvent(evt) {
|
||||
try {
|
||||
return BrowserWindow.fromWebContents(evt.sender)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function focusedWindow() {
|
||||
return BrowserWindow.getFocusedWindow() || BrowserWindow.getAllWindows()[0] || null
|
||||
}
|
||||
|
||||
ipcMain.handle('peardock:window-minimize', () => {
|
||||
focusedWindow()?.minimize()
|
||||
ipcMain.handle('peardock:window-minimize', (evt) => {
|
||||
;(windowFromEvent(evt) || focusedWindow())?.minimize()
|
||||
})
|
||||
|
||||
ipcMain.handle('peardock:window-maximize', () => {
|
||||
const win = focusedWindow()
|
||||
ipcMain.handle('peardock:window-maximize', (evt) => {
|
||||
const win = windowFromEvent(evt) || focusedWindow()
|
||||
if (!win) return
|
||||
if (win.isMaximized()) win.unmaximize()
|
||||
else win.maximize()
|
||||
})
|
||||
|
||||
ipcMain.handle('peardock:window-close', () => {
|
||||
focusedWindow()?.close()
|
||||
ipcMain.handle('peardock:window-close', (evt) => {
|
||||
;(windowFromEvent(evt) || focusedWindow())?.close()
|
||||
})
|
||||
|
||||
ipcMain.handle('peardock:window-is-maximized', () => {
|
||||
return Boolean(focusedWindow()?.isMaximized())
|
||||
ipcMain.handle('peardock:window-is-maximized', (evt) => {
|
||||
return Boolean((windowFromEvent(evt) || focusedWindow())?.isMaximized())
|
||||
})
|
||||
|
||||
/**
|
||||
* Open a Holesail tunnel in a peardock-styled BrowserWindow
|
||||
* (matching main #titlebar + pear-ctrl). Used by client/holesailLocal.js.
|
||||
*/
|
||||
ipcMain.handle('peardock:open-tunnel-browser', (_evt, payload) => {
|
||||
try {
|
||||
const url = typeof payload === 'string' ? payload : payload?.url
|
||||
const title = typeof payload === 'object' && payload ? payload.title : undefined
|
||||
const win = createTunnelWindow(url, { title })
|
||||
return Boolean(win)
|
||||
} catch (err) {
|
||||
console.error('[peardock] open-tunnel-browser failed:', err)
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
/** Toolbar actions from tunnel-browser.html (BrowserView mode) */
|
||||
ipcMain.handle('peardock:tunnel-nav', (evt, action) => {
|
||||
try {
|
||||
const win = BrowserWindow.fromWebContents(evt.sender)
|
||||
const view = win?.__pdTunnel?.view
|
||||
if (!win || !view || view.webContents.isDestroyed()) return false
|
||||
const act = String(action || '')
|
||||
if (act === 'back' && view.webContents.canGoBack()) view.webContents.goBack()
|
||||
else if (act === 'forward' && view.webContents.canGoForward()) view.webContents.goForward()
|
||||
else if (act === 'reload') view.webContents.reload()
|
||||
else if (act === 'stop') view.webContents.stop()
|
||||
else return false
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('[peardock] tunnel-nav failed:', err)
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user