853 lines
23 KiB
TypeScript
853 lines
23 KiB
TypeScript
import { join } from 'node:path'
|
|
|
|
import type { Session, WebContents } from 'electron/main'
|
|
import { BrowserWindow, WebContentsView, ipcMain, nativeTheme } from 'electron/main'
|
|
|
|
import { STANDARD_USER_AGENT } from './user-agent'
|
|
|
|
interface WorkbenchWindowOptions {
|
|
title: string
|
|
targetURL: string
|
|
session: Session
|
|
}
|
|
|
|
interface WorkbenchNavigationState {
|
|
canGoBack: boolean
|
|
canGoForward: boolean
|
|
loading: boolean
|
|
url: string
|
|
title: string
|
|
host: string
|
|
secure: boolean
|
|
}
|
|
|
|
interface WorkbenchWindowRecord {
|
|
shellContentsId: number
|
|
window: BrowserWindow
|
|
view: WebContentsView
|
|
title: string
|
|
initialURL: string
|
|
activeTargetURL: string
|
|
pageTitle: string
|
|
loading: boolean
|
|
}
|
|
|
|
const TOOLBAR_HEIGHT = 46
|
|
const workbenchWindows = new Map<number, WorkbenchWindowRecord>()
|
|
let handlersRegistered = false
|
|
|
|
function preloadPath(): string {
|
|
return join(__dirname, '../preload/bridge.cjs')
|
|
}
|
|
|
|
function titleBarStyle(): 'hidden' | 'hiddenInset' {
|
|
return process.platform === 'darwin' ? 'hiddenInset' : 'hidden'
|
|
}
|
|
|
|
function titleBarOverlay(): { color: string; symbolColor: string; height: number } | undefined {
|
|
if (process.platform === 'darwin') {
|
|
return undefined
|
|
}
|
|
|
|
return {
|
|
color: nativeTheme.shouldUseDarkColors ? '#15191a' : '#fbfcfd',
|
|
symbolColor: nativeTheme.shouldUseDarkColors ? '#f8fafc' : '#111827',
|
|
height: TOOLBAR_HEIGHT,
|
|
}
|
|
}
|
|
|
|
function escapeHtml(value: string): string {
|
|
return value
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''')
|
|
}
|
|
|
|
function hostFromURL(rawURL: string): string {
|
|
try {
|
|
return new URL(rawURL).host
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
function isRemoteHTTPURL(rawURL: string): boolean {
|
|
return /^https?:\/\//i.test(rawURL)
|
|
}
|
|
|
|
function isLocalWorkbenchPage(rawURL: string): boolean {
|
|
return rawURL.startsWith('data:text/html')
|
|
}
|
|
|
|
function workbenchShellHTML(title: string, targetURL: string): string {
|
|
const initialHost = hostFromURL(targetURL)
|
|
const platformClass = `platform-${process.platform}`
|
|
|
|
return `<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>${escapeHtml(title)}</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: light dark;
|
|
--toolbar-height: ${TOOLBAR_HEIGHT}px;
|
|
--surface: #fbfcfd;
|
|
--surface-elevated: rgba(255, 255, 255, 0.82);
|
|
--surface-control: #eef2f6;
|
|
--surface-control-hover: #e3e9f1;
|
|
--border: #dfe5ed;
|
|
--border-soft: rgba(148, 163, 184, 0.28);
|
|
--text: #1f2933;
|
|
--muted: #64717f;
|
|
--muted-soft: #94a3b8;
|
|
--accent: #2f6fed;
|
|
font-family:
|
|
"SF Pro Text",
|
|
"SF Pro Display",
|
|
-apple-system,
|
|
BlinkMacSystemFont,
|
|
"Segoe UI",
|
|
sans-serif;
|
|
background: var(--surface);
|
|
color: var(--text);
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html,
|
|
body {
|
|
width: 100%;
|
|
height: 100%;
|
|
margin: 0;
|
|
overflow: hidden;
|
|
background: var(--surface);
|
|
}
|
|
|
|
.workbench-toolbar {
|
|
position: fixed;
|
|
inset: 0 0 auto 0;
|
|
height: var(--toolbar-height);
|
|
display: grid;
|
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 8px 14px;
|
|
border-bottom: 1px solid var(--border);
|
|
background:
|
|
linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(248, 250, 252, 0.9)),
|
|
var(--surface);
|
|
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.9) inset;
|
|
-webkit-app-region: drag;
|
|
user-select: none;
|
|
}
|
|
|
|
.platform-darwin .workbench-toolbar {
|
|
padding-left: 82px;
|
|
}
|
|
|
|
.platform-win32 .workbench-toolbar {
|
|
padding-right: 148px;
|
|
}
|
|
|
|
.nav-cluster {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 3px;
|
|
height: 30px;
|
|
padding: 2px;
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: 8px;
|
|
background: rgba(241, 245, 249, 0.76);
|
|
box-shadow:
|
|
0 1px 2px rgba(15, 23, 42, 0.05),
|
|
0 1px 0 rgba(255, 255, 255, 0.78) inset;
|
|
-webkit-app-region: no-drag;
|
|
}
|
|
|
|
.nav-button {
|
|
width: 25px;
|
|
height: 24px;
|
|
display: inline-grid;
|
|
place-items: center;
|
|
padding: 0;
|
|
border: 0;
|
|
border-radius: 6px;
|
|
background: transparent;
|
|
color: #334155;
|
|
cursor: pointer;
|
|
transition:
|
|
background 150ms ease,
|
|
color 150ms ease,
|
|
transform 120ms ease,
|
|
opacity 150ms ease;
|
|
}
|
|
|
|
.nav-button:hover:not(:disabled) {
|
|
background: var(--surface-control-hover);
|
|
color: #111827;
|
|
}
|
|
|
|
.nav-button:active:not(:disabled) {
|
|
transform: translateY(1px) scale(0.98);
|
|
}
|
|
|
|
.nav-button:disabled {
|
|
cursor: default;
|
|
color: var(--muted-soft);
|
|
opacity: 0.58;
|
|
}
|
|
|
|
.nav-button svg {
|
|
width: 15px;
|
|
height: 15px;
|
|
display: block;
|
|
stroke: currentColor;
|
|
stroke-width: 2;
|
|
stroke-linecap: round;
|
|
stroke-linejoin: round;
|
|
fill: none;
|
|
}
|
|
|
|
.title-strip {
|
|
min-width: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
height: 30px;
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
line-height: 1;
|
|
}
|
|
|
|
.page-title {
|
|
min-width: 0;
|
|
max-width: min(46vw, 620px);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
font-weight: 650;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
.host-pill {
|
|
min-width: 0;
|
|
max-width: 260px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 0 10px;
|
|
height: 24px;
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: 999px;
|
|
background: var(--surface-elevated);
|
|
color: var(--muted);
|
|
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.72) inset;
|
|
}
|
|
|
|
.host-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
flex: 0 0 auto;
|
|
border-radius: 999px;
|
|
background: #22a06b;
|
|
box-shadow: 0 0 0 3px rgba(34, 160, 107, 0.12);
|
|
}
|
|
|
|
.host {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.toolbar-spacer {
|
|
width: 30px;
|
|
height: 30px;
|
|
}
|
|
|
|
.loading-line {
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: -1px;
|
|
height: 2px;
|
|
overflow: hidden;
|
|
opacity: 0;
|
|
transition: opacity 140ms ease;
|
|
}
|
|
|
|
.loading-line.is-loading {
|
|
opacity: 1;
|
|
}
|
|
|
|
.loading-line::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0 auto 0 0;
|
|
width: 38%;
|
|
background: linear-gradient(90deg, transparent, var(--accent), transparent);
|
|
transform: translateX(-100%);
|
|
animation: loading-sweep 1.05s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes loading-sweep {
|
|
to {
|
|
transform: translateX(270%);
|
|
}
|
|
}
|
|
|
|
@media (max-width: 760px) {
|
|
.workbench-toolbar {
|
|
gap: 8px;
|
|
}
|
|
|
|
.platform-darwin .workbench-toolbar {
|
|
padding-left: 78px;
|
|
}
|
|
|
|
.host-pill {
|
|
display: none;
|
|
}
|
|
|
|
.page-title {
|
|
max-width: 48vw;
|
|
}
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--surface: #15191a;
|
|
--surface-elevated: rgba(31, 37, 41, 0.82);
|
|
--surface-control: #20272c;
|
|
--surface-control-hover: #293139;
|
|
--border: #293138;
|
|
--border-soft: rgba(148, 163, 184, 0.18);
|
|
--text: #edf2f7;
|
|
--muted: #a6b0bb;
|
|
--muted-soft: #66717e;
|
|
--accent: #6aa2ff;
|
|
}
|
|
|
|
.workbench-toolbar {
|
|
background:
|
|
linear-gradient(180deg, rgba(24, 29, 32, 0.94), rgba(20, 24, 27, 0.92)),
|
|
var(--surface);
|
|
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05) inset;
|
|
}
|
|
|
|
.nav-cluster {
|
|
background: rgba(30, 37, 42, 0.76);
|
|
box-shadow:
|
|
0 1px 2px rgba(0, 0, 0, 0.2),
|
|
0 1px 0 rgba(255, 255, 255, 0.04) inset;
|
|
}
|
|
|
|
.nav-button {
|
|
color: #cbd5e1;
|
|
}
|
|
|
|
.nav-button:hover:not(:disabled) {
|
|
background: var(--surface-control-hover);
|
|
color: #f8fafc;
|
|
}
|
|
|
|
.host-pill {
|
|
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04) inset;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="${platformClass}">
|
|
<header class="workbench-toolbar">
|
|
<nav class="nav-cluster" aria-label="工作台导航">
|
|
<button class="nav-button" id="backButton" type="button" title="后退" aria-label="后退" disabled>
|
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 18 9 12l6-6" /></svg>
|
|
</button>
|
|
<button class="nav-button" id="forwardButton" type="button" title="前进" aria-label="前进" disabled>
|
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m9 18 6-6-6-6" /></svg>
|
|
</button>
|
|
<button class="nav-button" id="reloadButton" type="button" title="刷新" aria-label="刷新">
|
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6v5h-5" /><path d="M4 18v-5h5" /><path d="M18.4 9A7 7 0 0 0 6.8 6.4L4 9" /><path d="M5.6 15a7 7 0 0 0 11.6 2.6L20 15" /></svg>
|
|
</button>
|
|
</nav>
|
|
<div class="title-strip">
|
|
<strong class="page-title" id="pageTitle">${escapeHtml(title)}</strong>
|
|
<span class="host-pill" title="${escapeHtml(initialHost)}">
|
|
<span class="host-dot" aria-hidden="true"></span>
|
|
<span class="host" id="hostLabel">${escapeHtml(initialHost)}</span>
|
|
</span>
|
|
</div>
|
|
<div class="toolbar-spacer" aria-hidden="true"></div>
|
|
<div class="loading-line" id="loadingLine" aria-hidden="true"></div>
|
|
</header>
|
|
<script>
|
|
(() => {
|
|
const bridge = window.desktopBridge && window.desktopBridge.workbenchNavigation;
|
|
const backButton = document.getElementById('backButton');
|
|
const forwardButton = document.getElementById('forwardButton');
|
|
const reloadButton = document.getElementById('reloadButton');
|
|
const pageTitle = document.getElementById('pageTitle');
|
|
const hostLabel = document.getElementById('hostLabel');
|
|
const loadingLine = document.getElementById('loadingLine');
|
|
|
|
function applyState(state) {
|
|
if (!state) {
|
|
return;
|
|
}
|
|
backButton.disabled = !state.canGoBack;
|
|
forwardButton.disabled = !state.canGoForward;
|
|
pageTitle.textContent = state.title || ${JSON.stringify(title)};
|
|
hostLabel.textContent = state.host || '';
|
|
hostLabel.parentElement.title = state.url || state.host || '';
|
|
loadingLine.classList.toggle('is-loading', Boolean(state.loading));
|
|
}
|
|
|
|
if (!bridge) {
|
|
reloadButton.disabled = true;
|
|
return;
|
|
}
|
|
|
|
backButton.addEventListener('click', () => {
|
|
void bridge.goBack();
|
|
});
|
|
forwardButton.addEventListener('click', () => {
|
|
void bridge.goForward();
|
|
});
|
|
reloadButton.addEventListener('click', () => {
|
|
void bridge.reload();
|
|
});
|
|
|
|
bridge.onStateChanged(applyState);
|
|
void bridge.getState().then(applyState).catch(() => undefined);
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>`
|
|
}
|
|
|
|
function workbenchShellDataURL(title: string, targetURL: string): string {
|
|
return `data:text/html;charset=UTF-8,${encodeURIComponent(workbenchShellHTML(title, targetURL))}`
|
|
}
|
|
|
|
function workbenchErrorDataURL(title: string, targetURL: string, message: string): string {
|
|
const host = hostFromURL(targetURL)
|
|
const html = `<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>${escapeHtml(title)} - 加载失败</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: light dark;
|
|
font-family:
|
|
"SF Pro Text",
|
|
-apple-system,
|
|
BlinkMacSystemFont,
|
|
"Segoe UI",
|
|
sans-serif;
|
|
background: #f6f8fb;
|
|
color: #17202a;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
display: grid;
|
|
place-items: center;
|
|
padding: 28px;
|
|
background: #f6f8fb;
|
|
}
|
|
main {
|
|
width: min(620px, 100%);
|
|
padding: 26px;
|
|
border: 1px solid #dbe3ec;
|
|
border-radius: 8px;
|
|
background: #ffffff;
|
|
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.06);
|
|
}
|
|
h1 {
|
|
margin: 0;
|
|
font-size: 22px;
|
|
line-height: 1.35;
|
|
letter-spacing: 0;
|
|
}
|
|
p {
|
|
margin: 12px 0 0;
|
|
color: #536170;
|
|
line-height: 1.7;
|
|
}
|
|
code {
|
|
display: block;
|
|
margin-top: 16px;
|
|
padding: 11px 12px;
|
|
border-radius: 6px;
|
|
background: #f1f5f9;
|
|
color: #334155;
|
|
font: 13px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
word-break: break-all;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root,
|
|
body {
|
|
background: #111827;
|
|
color: #f8fafc;
|
|
}
|
|
main {
|
|
border-color: #2f3948;
|
|
background: #182130;
|
|
box-shadow: none;
|
|
}
|
|
p {
|
|
color: #b6bfcb;
|
|
}
|
|
code {
|
|
background: #111827;
|
|
color: #d8e0ea;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>${escapeHtml(title)}加载失败</h1>
|
|
<p>远程页面暂时无法打开。可以使用窗口左上方的刷新按钮重试。</p>
|
|
<code>${escapeHtml(host || targetURL)}
|
|
|
|
${escapeHtml(message)}</code>
|
|
</main>
|
|
</body>
|
|
</html>`
|
|
|
|
return `data:text/html;charset=UTF-8,${encodeURIComponent(html)}`
|
|
}
|
|
|
|
function stateForRecord(record: WorkbenchWindowRecord): WorkbenchNavigationState {
|
|
const contents = record.view.webContents
|
|
const currentURL = contents.isDestroyed() ? '' : contents.getURL()
|
|
const displayURL = isLocalWorkbenchPage(currentURL) ? record.activeTargetURL : currentURL
|
|
return {
|
|
canGoBack: !contents.isDestroyed() && contents.canGoBack(),
|
|
canGoForward: !contents.isDestroyed() && contents.canGoForward(),
|
|
loading: record.loading || (!contents.isDestroyed() && contents.isLoading()),
|
|
url: displayURL,
|
|
title: record.pageTitle || record.title,
|
|
host: hostFromURL(displayURL),
|
|
secure: displayURL.startsWith('https://'),
|
|
}
|
|
}
|
|
|
|
function sendNavigationState(record: WorkbenchWindowRecord): void {
|
|
if (record.window.isDestroyed()) {
|
|
return
|
|
}
|
|
|
|
record.window.webContents.send('desktop:workbench-navigation-state', stateForRecord(record))
|
|
}
|
|
|
|
function getRecordForSender(sender: WebContents): WorkbenchWindowRecord | null {
|
|
const record = workbenchWindows.get(sender.id)
|
|
if (!record || record.window.isDestroyed() || record.view.webContents.isDestroyed()) {
|
|
return null
|
|
}
|
|
return record
|
|
}
|
|
|
|
function getRecordForWindow(window: BrowserWindow): WorkbenchWindowRecord | null {
|
|
const record = workbenchWindows.get(window.webContents.id)
|
|
if (
|
|
!record ||
|
|
record.window !== window ||
|
|
record.window.isDestroyed() ||
|
|
record.view.webContents.isDestroyed()
|
|
) {
|
|
return null
|
|
}
|
|
return record
|
|
}
|
|
|
|
function defaultNavigationState(): WorkbenchNavigationState {
|
|
return {
|
|
canGoBack: false,
|
|
canGoForward: false,
|
|
loading: false,
|
|
url: '',
|
|
title: '',
|
|
host: '',
|
|
secure: false,
|
|
}
|
|
}
|
|
|
|
function registerWorkbenchNavigationHandlers(): void {
|
|
if (handlersRegistered) {
|
|
return
|
|
}
|
|
handlersRegistered = true
|
|
|
|
ipcMain.handle('desktop:workbench-navigation:get-state', (event) => {
|
|
const record = getRecordForSender(event.sender)
|
|
return record ? stateForRecord(record) : defaultNavigationState()
|
|
})
|
|
|
|
ipcMain.handle('desktop:workbench-navigation:go-back', (event) => {
|
|
const record = getRecordForSender(event.sender)
|
|
if (record?.view.webContents.canGoBack()) {
|
|
record.view.webContents.goBack()
|
|
}
|
|
return null
|
|
})
|
|
|
|
ipcMain.handle('desktop:workbench-navigation:go-forward', (event) => {
|
|
const record = getRecordForSender(event.sender)
|
|
if (record?.view.webContents.canGoForward()) {
|
|
record.view.webContents.goForward()
|
|
}
|
|
return null
|
|
})
|
|
|
|
ipcMain.handle('desktop:workbench-navigation:reload', (event) => {
|
|
const record = getRecordForSender(event.sender)
|
|
if (!record) {
|
|
return null
|
|
}
|
|
|
|
const currentURL = record.view.webContents.getURL()
|
|
if (isLocalWorkbenchPage(currentURL)) {
|
|
void navigateWorkbenchRemote(record, record.activeTargetURL || record.initialURL)
|
|
} else {
|
|
record.view.webContents.reload()
|
|
}
|
|
return null
|
|
})
|
|
}
|
|
|
|
function layoutWorkbenchView(record: WorkbenchWindowRecord): void {
|
|
if (record.window.isDestroyed() || record.view.webContents.isDestroyed()) {
|
|
return
|
|
}
|
|
|
|
const [width, height] = record.window.getContentSize()
|
|
record.view.setBounds({
|
|
x: 0,
|
|
y: TOOLBAR_HEIGHT,
|
|
width: Math.max(0, width),
|
|
height: Math.max(0, height - TOOLBAR_HEIGHT),
|
|
})
|
|
}
|
|
|
|
function attachWorkbenchViewEvents(record: WorkbenchWindowRecord): void {
|
|
const { webContents } = record.view
|
|
|
|
webContents.on('did-start-loading', () => {
|
|
record.loading = true
|
|
sendNavigationState(record)
|
|
})
|
|
|
|
webContents.on('did-stop-loading', () => {
|
|
record.loading = false
|
|
sendNavigationState(record)
|
|
})
|
|
|
|
webContents.on('did-start-navigation', (_event, url, isInPlace, isMainFrame) => {
|
|
if (isMainFrame && isRemoteHTTPURL(url)) {
|
|
record.activeTargetURL = url
|
|
record.loading = !isInPlace
|
|
sendNavigationState(record)
|
|
}
|
|
})
|
|
|
|
webContents.on('did-navigate', (_event, url) => {
|
|
if (isRemoteHTTPURL(url)) {
|
|
record.activeTargetURL = url
|
|
}
|
|
sendNavigationState(record)
|
|
})
|
|
|
|
webContents.on('did-navigate-in-page', (_event, url) => {
|
|
if (isRemoteHTTPURL(url)) {
|
|
record.activeTargetURL = url
|
|
}
|
|
sendNavigationState(record)
|
|
})
|
|
|
|
webContents.on('page-title-updated', (_event, pageTitle) => {
|
|
record.pageTitle = pageTitle || record.title
|
|
sendNavigationState(record)
|
|
})
|
|
|
|
webContents.on(
|
|
'did-fail-load',
|
|
(_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
|
if (!isMainFrame || errorCode === -3 || errorCode === -20) {
|
|
return
|
|
}
|
|
|
|
const targetURL = validatedURL || record.activeTargetURL || record.initialURL
|
|
console.warn('[desktop-workbench] workbench page load failed', {
|
|
title: record.title,
|
|
targetURL,
|
|
errorCode,
|
|
errorDescription,
|
|
})
|
|
void showWorkbenchError(record, targetURL, `${errorCode}: ${errorDescription}`)
|
|
},
|
|
)
|
|
|
|
webContents.setWindowOpenHandler(({ url }) => {
|
|
if (isRemoteHTTPURL(url)) {
|
|
setImmediate(() => {
|
|
void navigateWorkbenchRemote(record, url)
|
|
})
|
|
}
|
|
return { action: 'deny' }
|
|
})
|
|
}
|
|
|
|
async function showWorkbenchError(
|
|
record: WorkbenchWindowRecord,
|
|
targetURL: string,
|
|
message: string,
|
|
): Promise<void> {
|
|
if (record.window.isDestroyed() || record.view.webContents.isDestroyed()) {
|
|
return
|
|
}
|
|
|
|
record.loading = false
|
|
record.activeTargetURL = targetURL
|
|
record.pageTitle = `${record.title}加载失败`
|
|
await record.view.webContents.loadURL(workbenchErrorDataURL(record.title, targetURL, message))
|
|
sendNavigationState(record)
|
|
}
|
|
|
|
async function navigateWorkbenchRemote(
|
|
record: WorkbenchWindowRecord,
|
|
targetURL: string,
|
|
): Promise<void> {
|
|
if (record.window.isDestroyed() || record.view.webContents.isDestroyed()) {
|
|
return
|
|
}
|
|
|
|
record.activeTargetURL = targetURL
|
|
record.loading = true
|
|
sendNavigationState(record)
|
|
|
|
try {
|
|
await record.view.webContents.loadURL(targetURL, { userAgent: STANDARD_USER_AGENT })
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
if (/ERR_ABORTED|ERR_BLOCKED_BY_CLIENT|\((-3|-20)\) loading/i.test(message)) {
|
|
return
|
|
}
|
|
|
|
await showWorkbenchError(record, targetURL, message)
|
|
}
|
|
}
|
|
|
|
export function getWorkbenchWindowWebContents(window: BrowserWindow): WebContents | null {
|
|
return getRecordForWindow(window)?.view.webContents ?? null
|
|
}
|
|
|
|
export function isWorkbenchWindowReadyForDetection(window: BrowserWindow): boolean {
|
|
const record = getRecordForWindow(window)
|
|
if (!record) {
|
|
return false
|
|
}
|
|
|
|
const currentURL = record.view.webContents.getURL()
|
|
return (
|
|
isRemoteHTTPURL(currentURL) &&
|
|
!isLocalWorkbenchPage(currentURL) &&
|
|
!record.loading &&
|
|
!record.view.webContents.isLoading()
|
|
)
|
|
}
|
|
|
|
export async function loadWorkbenchWindowURL(
|
|
window: BrowserWindow,
|
|
targetURL: string,
|
|
): Promise<boolean> {
|
|
const record = getRecordForWindow(window)
|
|
if (!record) {
|
|
return false
|
|
}
|
|
|
|
await navigateWorkbenchRemote(record, targetURL)
|
|
return true
|
|
}
|
|
|
|
export async function openWorkbenchWindow(options: WorkbenchWindowOptions): Promise<BrowserWindow> {
|
|
registerWorkbenchNavigationHandlers()
|
|
|
|
const window = new BrowserWindow({
|
|
show: false,
|
|
width: 1320,
|
|
height: 900,
|
|
minWidth: 1100,
|
|
minHeight: 760,
|
|
title: options.title,
|
|
autoHideMenuBar: true,
|
|
titleBarStyle: titleBarStyle(),
|
|
titleBarOverlay: titleBarOverlay(),
|
|
trafficLightPosition: process.platform === 'darwin' ? { x: 14, y: 16 } : undefined,
|
|
backgroundColor: nativeTheme.shouldUseDarkColors ? '#15191a' : '#fbfcfd',
|
|
webPreferences: {
|
|
preload: preloadPath(),
|
|
sandbox: true,
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
},
|
|
})
|
|
const view = new WebContentsView({
|
|
webPreferences: {
|
|
session: options.session,
|
|
sandbox: true,
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
},
|
|
})
|
|
|
|
view.webContents.setUserAgent(STANDARD_USER_AGENT)
|
|
window.contentView.addChildView(view)
|
|
|
|
const record: WorkbenchWindowRecord = {
|
|
shellContentsId: window.webContents.id,
|
|
window,
|
|
view,
|
|
title: options.title,
|
|
initialURL: options.targetURL,
|
|
activeTargetURL: options.targetURL,
|
|
pageTitle: options.title,
|
|
loading: false,
|
|
}
|
|
|
|
workbenchWindows.set(record.shellContentsId, record)
|
|
attachWorkbenchViewEvents(record)
|
|
|
|
window.on('resize', () => {
|
|
layoutWorkbenchView(record)
|
|
})
|
|
window.on('closed', () => {
|
|
workbenchWindows.delete(record.shellContentsId)
|
|
if (!record.view.webContents.isDestroyed()) {
|
|
record.view.webContents.close()
|
|
}
|
|
})
|
|
|
|
await window.webContents.loadURL(workbenchShellDataURL(options.title, options.targetURL))
|
|
layoutWorkbenchView(record)
|
|
sendNavigationState(record)
|
|
void navigateWorkbenchRemote(record, options.targetURL)
|
|
|
|
return window
|
|
}
|