mirror of
https://github.com/kuaifan/dootask.git
synced 2026-01-21 08:28:12 +00:00
将已废弃的 webContents 导航方法迁移到新的 navigationHistory API: - canGoBack() → navigationHistory.canGoBack() - canGoForward() → navigationHistory.canGoForward() - goBack() → navigationHistory.goBack() - goForward() → navigationHistory.goForward()
1834 lines
55 KiB
JavaScript
Vendored
1834 lines
55 KiB
JavaScript
Vendored
/**
|
||
* WebTab 窗口管理模块
|
||
*
|
||
* 负责管理多标签浏览器窗口,支持:
|
||
* - tab 模式:标签页模式(有导航栏)
|
||
* - window 模式:独立窗口模式(无导航栏)
|
||
*/
|
||
|
||
const path = require('path')
|
||
const os = require('os')
|
||
const {
|
||
ipcMain,
|
||
clipboard,
|
||
nativeTheme,
|
||
screen,
|
||
Menu,
|
||
WebContentsView,
|
||
BrowserWindow
|
||
} = require('electron')
|
||
|
||
const utils = require('./utils')
|
||
const navigation = require('./navigation')
|
||
const { allowedCalls, isMac } = require('./other')
|
||
const faviconCache = require('./favicon-cache')
|
||
const { renderer } = require('./renderer')
|
||
|
||
// ============================================================
|
||
// 状态变量
|
||
// ============================================================
|
||
|
||
// Map<windowId, {window, views: [{id, view, name, favicon}], activeTabId, mode: 'tab'|'window'}>
|
||
let webTabWindows = new Map()
|
||
let webTabWindowIdCounter = 1
|
||
|
||
// 标签名称到标签位置的映射,用于复用已存在的标签
|
||
// Map<name, {windowId, tabId}>
|
||
let webTabNameMap = new Map()
|
||
|
||
// 标签栏高度
|
||
const webTabHeight = 40
|
||
|
||
// 快捷键关闭状态 Map<windowId, boolean>
|
||
let webTabClosedByShortcut = new Map()
|
||
|
||
// ============================================================
|
||
// 预加载池
|
||
// ============================================================
|
||
|
||
// 预加载 view 池
|
||
let preloadViewPool = []
|
||
|
||
// 预加载配置
|
||
const PRELOAD_CONFIG = {
|
||
poolSize: 1, // 池大小
|
||
warmupDelay: 1000, // 启动后延迟创建时间(ms)
|
||
refillDelay: 500, // 取走后补充延迟时间(ms)
|
||
}
|
||
|
||
// 预加载定时器(用于防抖补充)
|
||
let preloadRefillTimer = null
|
||
|
||
// ============================================================
|
||
// 依赖注入
|
||
// ============================================================
|
||
|
||
let _context = null
|
||
|
||
/**
|
||
* 初始化模块
|
||
* @param {Object} context
|
||
* @param {Function} context.getServerUrl - 获取服务器URL
|
||
* @param {Function} context.getUserConf - 获取用户配置
|
||
* @param {Function} context.isWillQuitApp - 是否即将退出应用
|
||
* @param {Object} context.electronMenu - 菜单模块
|
||
*/
|
||
function init(context) {
|
||
_context = context
|
||
}
|
||
|
||
/**
|
||
* 获取服务器URL
|
||
*/
|
||
function getServerUrl() {
|
||
return _context?.getServerUrl?.() || ''
|
||
}
|
||
|
||
/**
|
||
* 获取用户配置
|
||
*/
|
||
function getUserConf() {
|
||
return _context?.getUserConf?.()
|
||
}
|
||
|
||
/**
|
||
* 是否即将退出应用
|
||
*/
|
||
function isWillQuitApp() {
|
||
return _context?.isWillQuitApp?.() || false
|
||
}
|
||
|
||
/**
|
||
* 获取菜单模块
|
||
*/
|
||
function getElectronMenu() {
|
||
return _context?.electronMenu
|
||
}
|
||
|
||
// ============================================================
|
||
// 预加载函数
|
||
// ============================================================
|
||
|
||
/**
|
||
* 创建预加载 view
|
||
* 预加载 /preload 路由,完成基础 JS 文件加载
|
||
* @returns {WebContentsView}
|
||
*/
|
||
function createPreloadView() {
|
||
const serverUrl = getServerUrl()
|
||
if (!serverUrl) {
|
||
return null
|
||
}
|
||
|
||
const browserView = new WebContentsView({
|
||
webPreferences: {
|
||
preload: path.join(__dirname, '..', 'electron-preload.js'),
|
||
nodeIntegration: true,
|
||
contextIsolation: true,
|
||
}
|
||
})
|
||
|
||
const originalUA = browserView.webContents.session.getUserAgent() || browserView.webContents.getUserAgent()
|
||
browserView.webContents.setUserAgent(originalUA + " SubTaskWindow/" + process.platform + "/" + os.arch() + "/1.0")
|
||
|
||
utils.loadUrl(browserView.webContents, serverUrl, '/preload')
|
||
|
||
browserView._isPreloaded = true
|
||
browserView._preloadReady = false
|
||
browserView.webContents.on('did-finish-load', () => {
|
||
browserView._preloadReady = true
|
||
})
|
||
|
||
return browserView
|
||
}
|
||
|
||
/**
|
||
* 预热预加载池(延迟创建,避免影响主窗口加载)
|
||
*/
|
||
function warmupPreloadPool() {
|
||
if (!getServerUrl()) return
|
||
|
||
setTimeout(() => {
|
||
while (preloadViewPool.length < PRELOAD_CONFIG.poolSize) {
|
||
const view = createPreloadView()
|
||
if (view) {
|
||
preloadViewPool.push(view)
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
}, PRELOAD_CONFIG.warmupDelay)
|
||
}
|
||
|
||
/**
|
||
* 从池中获取预加载 view(优先取已就绪的)
|
||
* @returns {WebContentsView|null}
|
||
*/
|
||
function getPreloadedView() {
|
||
// 优先取已就绪的
|
||
const readyIndex = preloadViewPool.findIndex(v => v._preloadReady && !v.webContents.isDestroyed())
|
||
if (readyIndex >= 0) {
|
||
const view = preloadViewPool.splice(readyIndex, 1)[0]
|
||
scheduleRefillPool()
|
||
return view
|
||
}
|
||
// 次选任意可用(可能还在加载中)
|
||
const availableIndex = preloadViewPool.findIndex(v => !v.webContents.isDestroyed())
|
||
if (availableIndex >= 0) {
|
||
const view = preloadViewPool.splice(availableIndex, 1)[0]
|
||
scheduleRefillPool()
|
||
return view
|
||
}
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* 延迟补充预加载池
|
||
*/
|
||
function scheduleRefillPool() {
|
||
if (preloadRefillTimer) clearTimeout(preloadRefillTimer)
|
||
preloadRefillTimer = setTimeout(() => {
|
||
preloadRefillTimer = null
|
||
while (preloadViewPool.length < PRELOAD_CONFIG.poolSize) {
|
||
const view = createPreloadView()
|
||
if (view) {
|
||
preloadViewPool.push(view)
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
}, PRELOAD_CONFIG.refillDelay)
|
||
}
|
||
|
||
/**
|
||
* 清理预加载池
|
||
*/
|
||
function clearPreloadPool() {
|
||
if (preloadRefillTimer) {
|
||
clearTimeout(preloadRefillTimer)
|
||
preloadRefillTimer = null
|
||
}
|
||
preloadViewPool.forEach(view => {
|
||
try {
|
||
if (!view.webContents.isDestroyed()) {
|
||
view.webContents.close()
|
||
}
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
})
|
||
preloadViewPool = []
|
||
}
|
||
|
||
// ============================================================
|
||
// 核心函数
|
||
// ============================================================
|
||
|
||
/**
|
||
* 创建内置浏览器窗口(支持多窗口)
|
||
* @param args {url, windowId, position, afterId, insertIndex, name, force, userAgent, title, titleFixed, webPreferences, mode, ...}
|
||
* - mode: 'tab' | 'window'
|
||
* - 'window': 独立窗口模式(无导航栏)
|
||
* - 'tab': 标签页模式(默认,有导航栏)
|
||
* @returns {number} 窗口ID
|
||
*/
|
||
function createWebTabWindow(args) {
|
||
if (!args) {
|
||
return
|
||
}
|
||
|
||
if (!utils.isJson(args)) {
|
||
args = { url: args }
|
||
}
|
||
|
||
const mode = args.mode || 'tab'
|
||
const isWindowMode = mode === 'window'
|
||
|
||
// 查找同名标签/窗口
|
||
if (args.name) {
|
||
const existing = webTabNameMap.get(args.name)
|
||
if (existing) {
|
||
const existingWindowData = webTabWindows.get(existing.windowId)
|
||
if (existingWindowData && existingWindowData.window && !existingWindowData.window.isDestroyed()) {
|
||
const viewItem = existingWindowData.views.find(v => v.id === existing.tabId)
|
||
if (viewItem && viewItem.view && !viewItem.view.webContents.isDestroyed()) {
|
||
if (existingWindowData.window.isMinimized()) {
|
||
existingWindowData.window.restore()
|
||
}
|
||
existingWindowData.window.focus()
|
||
existingWindowData.window.show()
|
||
activateWebTabInWindow(existing.windowId, existing.tabId)
|
||
if (args.force === true && args.url) {
|
||
utils.loadContentUrl(viewItem.view.webContents, getServerUrl(), args.url)
|
||
}
|
||
return existing.windowId
|
||
}
|
||
}
|
||
webTabNameMap.delete(args.name)
|
||
}
|
||
}
|
||
|
||
let windowId = args.windowId
|
||
let windowData = windowId ? webTabWindows.get(windowId) : null
|
||
let webTabWindow = windowData ? windowData.window : null
|
||
|
||
// window 模式创建新窗口;tab 模式尝试复用已有窗口
|
||
if (!webTabWindow) {
|
||
if (!isWindowMode && !windowId) {
|
||
for (const [id, data] of webTabWindows) {
|
||
if (data.window && !data.window.isDestroyed() && data.mode !== 'window') {
|
||
windowId = id
|
||
windowData = data
|
||
webTabWindow = data.window
|
||
break
|
||
}
|
||
}
|
||
}
|
||
if (!webTabWindow) {
|
||
windowId = webTabWindowIdCounter++
|
||
const position = {
|
||
x: args.x,
|
||
y: args.y,
|
||
width: args.width,
|
||
height: args.height,
|
||
minWidth: args.minWidth,
|
||
minHeight: args.minHeight,
|
||
}
|
||
webTabWindow = createWebTabWindowInstance(windowId, position, mode)
|
||
windowData = {
|
||
window: webTabWindow,
|
||
views: [],
|
||
activeTabId: null,
|
||
mode: mode
|
||
}
|
||
webTabWindows.set(windowId, windowData)
|
||
}
|
||
}
|
||
|
||
if (webTabWindow.isMinimized()) {
|
||
webTabWindow.restore()
|
||
}
|
||
webTabWindow.focus()
|
||
webTabWindow.show()
|
||
|
||
const browserView = createWebTabView(windowId, args)
|
||
|
||
let insertIndex = windowData.views.length
|
||
if (args.afterId) {
|
||
const afterIndex = windowData.views.findIndex(item => item.id === args.afterId)
|
||
if (afterIndex > -1) insertIndex = afterIndex + 1
|
||
}
|
||
if (typeof args.insertIndex === 'number') {
|
||
insertIndex = Math.max(0, Math.min(args.insertIndex, windowData.views.length))
|
||
}
|
||
|
||
windowData.views.splice(insertIndex, 0, {
|
||
id: browserView.webContents.id,
|
||
view: browserView,
|
||
name: args.name || null
|
||
})
|
||
|
||
if (args.name) {
|
||
webTabNameMap.set(args.name, {
|
||
windowId: windowId,
|
||
tabId: browserView.webContents.id
|
||
})
|
||
}
|
||
|
||
if (isWindowMode) {
|
||
if (args.title) webTabWindow.setTitle(args.title)
|
||
} else {
|
||
const domain = faviconCache.extractDomain(args.url)
|
||
const cachedFavicon = domain ? faviconCache.getByDomain(domain) : null
|
||
if (cachedFavicon) {
|
||
const viewItem = windowData.views.find(v => v.id === browserView.webContents.id)
|
||
if (viewItem) viewItem.favicon = cachedFavicon
|
||
}
|
||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||
event: 'create',
|
||
id: browserView.webContents.id,
|
||
url: args.url,
|
||
afterId: args.afterId,
|
||
windowId: windowId,
|
||
title: args.title,
|
||
favicon: cachedFavicon || '',
|
||
}).then(_ => { })
|
||
}
|
||
activateWebTabInWindow(windowId, browserView.webContents.id)
|
||
|
||
return windowId
|
||
}
|
||
|
||
/**
|
||
* 创建 WebTabWindow 实例
|
||
* @param windowId
|
||
* @param position {x, y, width, height}
|
||
* @param mode 'tab' | 'window'
|
||
* @returns {BrowserWindow}
|
||
*/
|
||
function createWebTabWindowInstance(windowId, position, mode = 'tab') {
|
||
const isWindowMode = mode === 'window'
|
||
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize
|
||
const isHighRes = screenWidth >= 2560
|
||
|
||
// 根据屏幕分辨率计算默认尺寸
|
||
const screenDefault = {
|
||
width: isHighRes ? 1440 : 1024,
|
||
height: isHighRes ? 900 : 768,
|
||
minWidth: 400,
|
||
minHeight: 300,
|
||
}
|
||
|
||
// 计算窗口尺寸和位置
|
||
const windowOptions = {
|
||
show: false,
|
||
autoHideMenuBar: true,
|
||
webPreferences: {
|
||
preload: path.join(__dirname, '..', 'electron-preload.js'),
|
||
webSecurity: true,
|
||
nodeIntegration: true,
|
||
contextIsolation: true,
|
||
},
|
||
}
|
||
|
||
const userConf = getUserConf()
|
||
|
||
if (isWindowMode) {
|
||
// window 模式:使用 position 参数或屏幕默认值,永远居中
|
||
Object.assign(windowOptions, {
|
||
width: Math.floor(position?.width ?? screenDefault.width),
|
||
height: Math.floor(position?.height ?? screenDefault.height),
|
||
minWidth: Math.floor(position?.minWidth ?? screenDefault.minWidth),
|
||
minHeight: Math.floor(position?.minHeight ?? screenDefault.minHeight),
|
||
backgroundColor: utils.getDefaultBackgroundColor(),
|
||
center: true,
|
||
})
|
||
} else {
|
||
// tab 模式:使用 savedBounds 或屏幕默认值
|
||
const savedBounds = userConf?.get('webTabWindow') || {}
|
||
const maxX = Math.floor(screenWidth * 0.9)
|
||
const maxY = Math.floor(screenHeight * 0.9)
|
||
Object.assign(windowOptions, {
|
||
width: savedBounds.width ?? screenDefault.width,
|
||
height: savedBounds.height ?? screenDefault.height,
|
||
minWidth: screenDefault.minWidth,
|
||
minHeight: screenDefault.minHeight,
|
||
backgroundColor: nativeTheme.shouldUseDarkColors ? '#575757' : '#FFFFFF',
|
||
center: true,
|
||
})
|
||
// 恢复保存的位置,并限制在屏幕 90% 范围内
|
||
if (savedBounds.x !== undefined && savedBounds.y !== undefined) {
|
||
Object.assign(windowOptions, {
|
||
x: Math.min(savedBounds.x, maxX),
|
||
y: Math.min(savedBounds.y, maxY),
|
||
center: false,
|
||
})
|
||
}
|
||
}
|
||
|
||
// tab 模式使用隐藏标题栏 + titleBarOverlay
|
||
if (!isWindowMode) {
|
||
const titleBarOverlay = {
|
||
height: webTabHeight
|
||
}
|
||
if (nativeTheme.shouldUseDarkColors) {
|
||
Object.assign(titleBarOverlay, {
|
||
color: '#3B3B3D',
|
||
symbolColor: '#C5C5C5',
|
||
})
|
||
}
|
||
Object.assign(windowOptions, {
|
||
titleBarStyle: 'hidden',
|
||
titleBarOverlay: titleBarOverlay,
|
||
})
|
||
}
|
||
|
||
const webTabWindow = new BrowserWindow(windowOptions)
|
||
|
||
// 保存窗口ID到窗口对象
|
||
webTabWindow.webTabWindowId = windowId
|
||
|
||
const originalClose = webTabWindow.close
|
||
webTabWindow.close = function() {
|
||
webTabClosedByShortcut.set(windowId, true)
|
||
return originalClose.apply(this, arguments)
|
||
}
|
||
|
||
webTabWindow.on('resize', () => {
|
||
resizeWebTabInWindow(windowId, 0)
|
||
})
|
||
|
||
webTabWindow.on('enter-full-screen', () => {
|
||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||
event: 'enter-full-screen',
|
||
}).then(_ => { })
|
||
})
|
||
|
||
webTabWindow.on('leave-full-screen', () => {
|
||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||
event: 'leave-full-screen',
|
||
}).then(_ => { })
|
||
})
|
||
|
||
webTabWindow.on('close', event => {
|
||
if (webTabClosedByShortcut.get(windowId)) {
|
||
webTabClosedByShortcut.set(windowId, false)
|
||
if (!isWillQuitApp()) {
|
||
closeWebTabInWindow(windowId, 0)
|
||
event.preventDefault()
|
||
return
|
||
}
|
||
}
|
||
// 只有 tab 模式才保存 bounds
|
||
const windowData = webTabWindows.get(windowId)
|
||
if (windowData && windowData.mode !== 'window') {
|
||
userConf?.set('webTabWindow', webTabWindow.getBounds())
|
||
}
|
||
})
|
||
|
||
webTabWindow.on('closed', () => {
|
||
const windowData = webTabWindows.get(windowId)
|
||
if (windowData) {
|
||
windowData.views.forEach(({ view, name }) => {
|
||
// 清理 name 映射
|
||
if (name) {
|
||
webTabNameMap.delete(name)
|
||
}
|
||
try {
|
||
view.webContents.close()
|
||
} catch (e) {
|
||
//
|
||
}
|
||
})
|
||
webTabWindows.delete(windowId)
|
||
}
|
||
webTabClosedByShortcut.delete(windowId)
|
||
})
|
||
|
||
webTabWindow.once('ready-to-show', () => {
|
||
onShowWindow(webTabWindow)
|
||
})
|
||
|
||
webTabWindow.webContents.once('dom-ready', () => {
|
||
onShowWindow(webTabWindow)
|
||
})
|
||
|
||
webTabWindow.webContents.on('before-input-event', (event, input) => {
|
||
if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'r') {
|
||
reloadWebTabInWindow(windowId, 0)
|
||
event.preventDefault()
|
||
} else if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'w') {
|
||
webTabClosedByShortcut.set(windowId, true)
|
||
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
|
||
devToolsWebTabInWindow(windowId, 0)
|
||
} else {
|
||
const item = currentWebTabInWindow(windowId)
|
||
if (item) {
|
||
navigation.handleInput(event, input, item.view.webContents)
|
||
}
|
||
}
|
||
})
|
||
|
||
// 设置鼠标侧键和触控板手势导航
|
||
navigation.setupWindowEvents(webTabWindow, () => {
|
||
const item = currentWebTabInWindow(windowId)
|
||
return item ? item.view.webContents : null
|
||
})
|
||
|
||
// tab 模式加载标签栏界面,window 模式不需要
|
||
if (!isWindowMode) {
|
||
webTabWindow.loadFile(path.join(__dirname, '..', 'render', 'tabs', 'index.html'), { query: { windowId: String(windowId) } }).then(_ => { }).catch(_ => { })
|
||
}
|
||
|
||
return webTabWindow
|
||
}
|
||
|
||
/**
|
||
* 创建 WebTab 视图
|
||
* @param windowId
|
||
* @param args
|
||
* @returns {WebContentsView}
|
||
*/
|
||
function createWebTabView(windowId, args) {
|
||
const windowData = webTabWindows.get(windowId)
|
||
if (!windowData) return null
|
||
|
||
const webTabWindow = windowData.window
|
||
const isWindowMode = windowData.mode === 'window'
|
||
const effectiveTabHeight = isWindowMode ? 0 : webTabHeight
|
||
const electronMenu = getElectronMenu()
|
||
const serverUrl = getServerUrl()
|
||
|
||
// 尝试复用预加载 view(本地站点且无特殊配置)
|
||
let browserView = null
|
||
let isPreloaded = false
|
||
const isLocalUrl = !args.url || !args.url.startsWith('http') ||
|
||
utils.getDomain(args.url) === utils.getDomain(serverUrl)
|
||
const hasCustomPreferences = args.webPreferences && Object.keys(args.webPreferences).length > 0
|
||
|
||
if (isLocalUrl && !hasCustomPreferences) {
|
||
browserView = getPreloadedView()
|
||
if (browserView) isPreloaded = true
|
||
}
|
||
|
||
if (!browserView) {
|
||
const viewOptions = {
|
||
webPreferences: Object.assign({
|
||
preload: path.join(__dirname, '..', 'electron-preload.js'),
|
||
nodeIntegration: true,
|
||
contextIsolation: true
|
||
}, args.webPreferences || {})
|
||
}
|
||
if (!viewOptions.webPreferences.contextIsolation) {
|
||
delete viewOptions.webPreferences.preload
|
||
}
|
||
browserView = new WebContentsView(viewOptions)
|
||
}
|
||
|
||
if (args.backgroundColor) {
|
||
browserView.setBackgroundColor(args.backgroundColor)
|
||
} else if (isWindowMode) {
|
||
browserView.setBackgroundColor(utils.getDefaultBackgroundColor())
|
||
} else if (nativeTheme.shouldUseDarkColors) {
|
||
browserView.setBackgroundColor('#575757')
|
||
} else {
|
||
browserView.setBackgroundColor('#FFFFFF')
|
||
}
|
||
|
||
browserView.setBounds({
|
||
x: 0,
|
||
y: effectiveTabHeight,
|
||
width: webTabWindow.getContentBounds().width || 1280,
|
||
height: (webTabWindow.getContentBounds().height || 800) - effectiveTabHeight,
|
||
})
|
||
|
||
browserView.webTabWindowId = windowId
|
||
browserView.tabName = args.name || null
|
||
browserView.titleFixed = args.titleFixed || false
|
||
|
||
if (!isPreloaded && args.userAgent) {
|
||
const originalUA = browserView.webContents.getUserAgent()
|
||
browserView.webContents.setUserAgent(
|
||
originalUA + " SubTaskWindow/" + process.platform + "/" + os.arch() + "/1.0 " + args.userAgent
|
||
)
|
||
}
|
||
|
||
browserView.webContents.on('destroyed', () => {
|
||
if (browserView.tabName) {
|
||
webTabNameMap.delete(browserView.tabName)
|
||
}
|
||
if (browserView._loadingChecker) {
|
||
clearInterval(browserView._loadingChecker)
|
||
browserView._loadingChecker = null
|
||
}
|
||
closeWebTabInWindow(windowId, browserView.webContents.id)
|
||
})
|
||
browserView.webContents.setWindowOpenHandler(({ url }) => {
|
||
if (allowedCalls.test(url)) {
|
||
renderer.openExternal(url).catch(() => {})
|
||
} else if (isWindowMode) {
|
||
// window 模式下打开外部浏览器
|
||
utils.onBeforeOpenWindow(browserView.webContents, url).then(() => {
|
||
renderer.openExternal(url).catch(() => {})
|
||
})
|
||
} else if (url && url !== 'about:blank') {
|
||
// tab 模式下创建新标签
|
||
createWebTabWindow({ url, afterId: browserView.webContents.id, windowId })
|
||
}
|
||
return { action: 'deny' }
|
||
})
|
||
browserView.webContents.on('page-title-updated', (_, title) => {
|
||
// titleFixed 时不更新标题
|
||
if (browserView.titleFixed) {
|
||
return
|
||
}
|
||
|
||
// 使用动态窗口ID,支持标签在窗口间转移
|
||
const currentWindowId = browserView.webTabWindowId
|
||
const wd = webTabWindows.get(currentWindowId)
|
||
if (!wd || !wd.window) return
|
||
|
||
// 根据模式更新标题
|
||
if (wd.mode === 'window') {
|
||
// window 模式下直接设置窗口标题
|
||
wd.window.setTitle(title)
|
||
} else {
|
||
// tab 模式下通知标签栏更新标题
|
||
utils.onDispatchEvent(wd.window.webContents, {
|
||
event: 'title',
|
||
id: browserView.webContents.id,
|
||
title: title,
|
||
url: browserView.webContents.getURL(),
|
||
}).then(_ => { })
|
||
}
|
||
})
|
||
browserView.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
||
if (!errorDescription) {
|
||
return
|
||
}
|
||
// 主框架加载失败时,展示内置的错误页面
|
||
if (isMainFrame) {
|
||
const originalUrl = validatedURL || args.url || ''
|
||
const filePath = path.join(__dirname, '..', 'render', 'tabs', 'error.html')
|
||
browserView.webContents.loadFile(filePath, {
|
||
query: {
|
||
id: String(browserView.webContents.id),
|
||
url: originalUrl,
|
||
code: String(errorCode),
|
||
desc: errorDescription,
|
||
}
|
||
}).then(_ => { }).catch(_ => { })
|
||
return
|
||
}
|
||
// 使用动态窗口ID,支持标签在窗口间转移
|
||
const currentWindowId = browserView.webTabWindowId
|
||
const wd = webTabWindows.get(currentWindowId)
|
||
if (!wd || !wd.window) return
|
||
utils.onDispatchEvent(wd.window.webContents, {
|
||
event: 'title',
|
||
id: browserView.webContents.id,
|
||
title: errorDescription,
|
||
url: browserView.webContents.getURL(),
|
||
}).then(_ => { })
|
||
})
|
||
browserView.webContents.on('page-favicon-updated', async (_, favicons) => {
|
||
// 使用动态窗口ID,支持标签在窗口间转移
|
||
const currentWindowId = browserView.webTabWindowId
|
||
const wd = webTabWindows.get(currentWindowId)
|
||
if (!wd || !wd.window) return
|
||
|
||
const tabId = browserView.webContents.id
|
||
const faviconUrl = favicons[favicons.length - 1] || ''
|
||
const pageUrl = browserView.webContents.getURL()
|
||
|
||
// 使用缓存模块获取 favicon(先查缓存,无则下载并缓存)
|
||
const base64Favicon = await faviconCache.fetchAndCache(faviconUrl, pageUrl)
|
||
|
||
// 保存验证后的 favicon 到视图对象
|
||
const viewItem = wd.views.find(v => v.id === tabId)
|
||
if (viewItem) {
|
||
viewItem.favicon = base64Favicon || ''
|
||
}
|
||
|
||
// 发送验证后的 favicon 给前端
|
||
utils.onDispatchEvent(wd.window.webContents, {
|
||
event: 'favicon',
|
||
id: tabId,
|
||
favicon: base64Favicon || ''
|
||
}).then(_ => { })
|
||
})
|
||
// 页面加载状态管理,忽略SPA路由切换(isSameDocument)
|
||
browserView._loadingActive = false
|
||
browserView._loadingChecker = null
|
||
const dispatchLoading = (event) => {
|
||
const wd = webTabWindows.get(browserView.webTabWindowId)
|
||
if (wd && wd.window) {
|
||
utils.onDispatchEvent(wd.window.webContents, {
|
||
event,
|
||
id: browserView.webContents.id,
|
||
}).then(_ => { })
|
||
}
|
||
}
|
||
const startLoading = () => {
|
||
if (browserView._loadingActive) return
|
||
browserView._loadingActive = true
|
||
dispatchLoading('start-loading')
|
||
if (!browserView._loadingChecker) {
|
||
browserView._loadingChecker = setInterval(() => {
|
||
if (browserView.webContents.isDestroyed() || !browserView.webContents.isLoading()) {
|
||
stopLoading()
|
||
}
|
||
}, 3000)
|
||
}
|
||
}
|
||
const stopLoading = () => {
|
||
if (browserView._loadingChecker) {
|
||
clearInterval(browserView._loadingChecker)
|
||
browserView._loadingChecker = null
|
||
}
|
||
if (!browserView._loadingActive) return
|
||
browserView._loadingActive = false
|
||
dispatchLoading('stop-loading')
|
||
}
|
||
browserView.webContents.on('did-start-navigation', (_, _url, _isInPlace, isMainFrame, _frameProcessId, _frameRoutingId, _navigationId, isSameDocument) => {
|
||
if (isMainFrame && !isSameDocument) {
|
||
startLoading()
|
||
}
|
||
})
|
||
browserView.webContents.on('did-stop-loading', _ => {
|
||
stopLoading()
|
||
if (nativeTheme.shouldUseDarkColors) {
|
||
browserView.setBackgroundColor('#FFFFFF')
|
||
}
|
||
})
|
||
browserView.webContents.on('before-input-event', (event, input) => {
|
||
// 使用动态窗口ID,支持标签在窗口间转移
|
||
const currentWindowId = browserView.webTabWindowId
|
||
if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'r') {
|
||
browserView.webContents.reload()
|
||
event.preventDefault()
|
||
} else if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'w') {
|
||
webTabClosedByShortcut.set(currentWindowId, true)
|
||
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
|
||
browserView.webContents.toggleDevTools()
|
||
} else {
|
||
navigation.handleInput(event, input, browserView.webContents)
|
||
}
|
||
})
|
||
|
||
if (!isPreloaded) {
|
||
const originalUA = browserView.webContents.session.getUserAgent() || browserView.webContents.getUserAgent()
|
||
browserView.webContents.setUserAgent(originalUA + " SubTaskWindow/" + process.platform + "/" + os.arch() + "/1.0")
|
||
}
|
||
|
||
electronMenu?.webContentsMenu(browserView.webContents, true)
|
||
|
||
// 加载业务路由(预加载 view 通过 __initializeApp 触发路由切换)
|
||
if (isPreloaded) {
|
||
const targetUrl = args.url || ''
|
||
browserView.webContents.executeJavaScript(
|
||
`window.__initializeApp && window.__initializeApp('${targetUrl.replace(/'/g, "\\'")}')`
|
||
).catch(() => {
|
||
utils.loadContentUrl(browserView.webContents, serverUrl, args.url)
|
||
})
|
||
} else {
|
||
utils.loadContentUrl(browserView.webContents, serverUrl, args.url)
|
||
}
|
||
|
||
browserView.setVisible(true)
|
||
|
||
webTabWindow.contentView.addChildView(browserView)
|
||
|
||
return browserView
|
||
}
|
||
|
||
/**
|
||
* 获取当前内置浏览器标签
|
||
* @returns {object|undefined}
|
||
*/
|
||
function currentWebTab() {
|
||
for (const [windowId, windowData] of webTabWindows) {
|
||
if (windowData.window && !windowData.window.isDestroyed()) {
|
||
return currentWebTabInWindow(windowId)
|
||
}
|
||
}
|
||
return undefined
|
||
}
|
||
|
||
/**
|
||
* 获取指定窗口的当前内置浏览器标签
|
||
* @param windowId
|
||
* @returns {object|undefined}
|
||
*/
|
||
function currentWebTabInWindow(windowId) {
|
||
const windowData = webTabWindows.get(windowId)
|
||
if (!windowData || !windowData.window) return undefined
|
||
|
||
const webTabView = windowData.views
|
||
const webTabWindow = windowData.window
|
||
|
||
// 优先级:可见标签 > 聚焦标签 > 最上层视图
|
||
try {
|
||
const item = webTabView.find(({ view }) => view?.getVisible && view.getVisible())
|
||
if (item) return item
|
||
} catch (e) {}
|
||
try {
|
||
const focused = require('electron').webContents.getFocusedWebContents?.()
|
||
if (focused) {
|
||
const item = webTabView.find(it => it.id === focused.id)
|
||
if (item) return item
|
||
}
|
||
} catch (e) {}
|
||
const children = webTabWindow.contentView?.children || []
|
||
for (let i = children.length - 1; i >= 0; i--) {
|
||
const id = children[i]?.webContents?.id
|
||
const item = webTabView.find(it => it.id === id)
|
||
if (item) {
|
||
return item
|
||
}
|
||
}
|
||
return undefined
|
||
}
|
||
|
||
/**
|
||
* 根据 tabId 查找所属窗口ID
|
||
* @param tabId
|
||
* @returns {number|null}
|
||
*/
|
||
function findWindowIdByTabId(tabId) {
|
||
for (const [windowId, windowData] of webTabWindows) {
|
||
if (windowData.views.some(v => v.id === tabId)) {
|
||
return windowId
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* 重新加载内置浏览器标签
|
||
* @param windowId
|
||
* @param id
|
||
*/
|
||
function reloadWebTabInWindow(windowId, id) {
|
||
const windowData = webTabWindows.get(windowId)
|
||
if (!windowData) return
|
||
|
||
const item = id === 0 ? currentWebTabInWindow(windowId) : windowData.views.find(item => item.id == id)
|
||
if (!item) {
|
||
return
|
||
}
|
||
item.view.webContents.reload()
|
||
}
|
||
|
||
/**
|
||
* 内置浏览器标签打开开发者工具
|
||
* @param windowId
|
||
* @param id
|
||
*/
|
||
function devToolsWebTabInWindow(windowId, id) {
|
||
const windowData = webTabWindows.get(windowId)
|
||
if (!windowData) return
|
||
|
||
const item = id === 0 ? currentWebTabInWindow(windowId) : windowData.views.find(item => item.id == id)
|
||
if (!item) {
|
||
return
|
||
}
|
||
item.view.webContents.toggleDevTools()
|
||
}
|
||
|
||
/**
|
||
* 调整内置浏览器标签尺寸
|
||
* @param windowId
|
||
* @param id
|
||
*/
|
||
function resizeWebTabInWindow(windowId, id) {
|
||
const windowData = webTabWindows.get(windowId)
|
||
if (!windowData || !windowData.window) return
|
||
|
||
const webTabWindow = windowData.window
|
||
const isWindowMode = windowData.mode === 'window'
|
||
const effectiveTabHeight = isWindowMode ? 0 : webTabHeight
|
||
|
||
const item = id === 0 ? currentWebTabInWindow(windowId) : windowData.views.find(item => item.id == id)
|
||
if (!item) {
|
||
return
|
||
}
|
||
item.view.setBounds({
|
||
x: 0,
|
||
y: effectiveTabHeight,
|
||
width: webTabWindow.getContentBounds().width || 1280,
|
||
height: (webTabWindow.getContentBounds().height || 800) - effectiveTabHeight,
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 切换内置浏览器标签
|
||
* @param windowId
|
||
* @param id
|
||
*/
|
||
function activateWebTabInWindow(windowId, id) {
|
||
const windowData = webTabWindows.get(windowId)
|
||
if (!windowData || !windowData.window) return
|
||
|
||
const webTabView = windowData.views
|
||
const webTabWindow = windowData.window
|
||
const item = id === 0 ? currentWebTabInWindow(windowId) : webTabView.find(item => item.id == id)
|
||
if (!item) {
|
||
return
|
||
}
|
||
windowData.activeTabId = item.id
|
||
webTabView.forEach(({ id: vid, view }) => {
|
||
view.setVisible(vid === item.id)
|
||
})
|
||
resizeWebTabInWindow(windowId, item.id)
|
||
item.view.webContents.focus()
|
||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||
event: 'switch',
|
||
id: item.id,
|
||
}).then(_ => { })
|
||
}
|
||
|
||
/**
|
||
* 关闭内置浏览器标签
|
||
* @param windowId
|
||
* @param id
|
||
*/
|
||
function closeWebTabInWindow(windowId, id) {
|
||
const windowData = webTabWindows.get(windowId)
|
||
if (!windowData || !windowData.window) return
|
||
|
||
const webTabView = windowData.views
|
||
const webTabWindow = windowData.window
|
||
const isWindowMode = windowData.mode === 'window'
|
||
const userConf = getUserConf()
|
||
|
||
const item = id === 0 ? currentWebTabInWindow(windowId) : webTabView.find(item => item.id == id)
|
||
if (!item) {
|
||
return
|
||
}
|
||
|
||
// window 模式下直接关闭整个窗口
|
||
if (isWindowMode) {
|
||
webTabView.forEach(({ name }) => {
|
||
if (name) webTabNameMap.delete(name)
|
||
})
|
||
webTabWindow.destroy()
|
||
return
|
||
}
|
||
|
||
if (webTabView.length === 1) {
|
||
webTabWindow.hide()
|
||
}
|
||
webTabWindow.contentView.removeChildView(item.view)
|
||
|
||
// 清理 name 映射
|
||
if (item.name) {
|
||
webTabNameMap.delete(item.name)
|
||
}
|
||
|
||
try {
|
||
item.view.webContents.close()
|
||
} catch (e) {
|
||
//
|
||
}
|
||
|
||
const index = webTabView.findIndex(({ id }) => item.id == id)
|
||
if (index > -1) {
|
||
webTabView.splice(index, 1)
|
||
}
|
||
|
||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||
event: 'close',
|
||
id: item.id,
|
||
}).then(_ => { })
|
||
|
||
if (webTabView.length === 0) {
|
||
userConf?.set('webTabWindow', webTabWindow.getBounds())
|
||
webTabWindow.destroy()
|
||
} else {
|
||
activateWebTabInWindow(windowId, 0)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 分离标签到新窗口
|
||
* @param windowId 源窗口ID
|
||
* @param tabId 标签ID
|
||
* @param screenX 屏幕X坐标
|
||
* @param screenY 屏幕Y坐标
|
||
* @returns {number|null} 新窗口ID
|
||
*/
|
||
function detachWebTab(windowId, tabId, screenX, screenY) {
|
||
const sourceWindowData = webTabWindows.get(windowId)
|
||
if (!sourceWindowData) return null
|
||
|
||
const tabIndex = sourceWindowData.views.findIndex(v => v.id === tabId)
|
||
if (tabIndex === -1) return null
|
||
|
||
const tabItem = sourceWindowData.views[tabIndex]
|
||
const view = tabItem.view
|
||
const favicon = tabItem.favicon || ''
|
||
const tabName = tabItem.name || null
|
||
const sourceWindow = sourceWindowData.window
|
||
const userConf = getUserConf()
|
||
|
||
// 从源窗口移除视图
|
||
sourceWindow.contentView.removeChildView(view)
|
||
sourceWindowData.views.splice(tabIndex, 1)
|
||
|
||
// 通知源窗口标签已关闭
|
||
utils.onDispatchEvent(sourceWindow.webContents, {
|
||
event: 'close',
|
||
id: tabId,
|
||
}).then(_ => { })
|
||
|
||
// 创建新窗口,使用源窗口的尺寸
|
||
const sourceBounds = sourceWindow.getBounds()
|
||
const newWindowId = webTabWindowIdCounter++
|
||
|
||
// 先创建窗口实例
|
||
const newWindow = createWebTabWindowInstance(newWindowId, {
|
||
width: sourceBounds.width,
|
||
height: sourceBounds.height,
|
||
})
|
||
|
||
// 计算窗口位置,让鼠标大致在标签区域
|
||
const navAreaWidth = isMac ? 280 : 200
|
||
const targetX = Math.round(screenX - navAreaWidth)
|
||
const targetY = Math.round(screenY - Math.floor(webTabHeight / 2))
|
||
|
||
// 显示窗口前先设置位置
|
||
newWindow.setPosition(targetX, targetY)
|
||
|
||
const newWindowData = {
|
||
window: newWindow,
|
||
views: [{
|
||
id: tabId,
|
||
name: tabName,
|
||
view,
|
||
favicon
|
||
}],
|
||
activeTabId: tabId,
|
||
mode: 'tab'
|
||
}
|
||
webTabWindows.set(newWindowId, newWindowData)
|
||
|
||
// 更新视图所属窗口
|
||
view.webTabWindowId = newWindowId
|
||
|
||
// 更新 name 映射中的 windowId
|
||
if (tabName) {
|
||
webTabNameMap.set(tabName, {
|
||
windowId: newWindowId,
|
||
tabId: tabId
|
||
})
|
||
}
|
||
|
||
// 添加视图到新窗口
|
||
newWindow.contentView.addChildView(view)
|
||
view.setBounds({
|
||
x: 0,
|
||
y: webTabHeight,
|
||
width: newWindow.getContentBounds().width || 1280,
|
||
height: (newWindow.getContentBounds().height || 800) - webTabHeight,
|
||
})
|
||
view.setVisible(true)
|
||
|
||
// 显示新窗口
|
||
newWindow.show()
|
||
|
||
// 再次确保位置正确
|
||
newWindow.setPosition(targetX, targetY)
|
||
newWindow.focus()
|
||
|
||
// 通知新窗口创建标签(传递完整状态信息)
|
||
newWindow.webContents.once('dom-ready', () => {
|
||
const isLoading = view.webContents.isLoading()
|
||
utils.onDispatchEvent(newWindow.webContents, {
|
||
event: 'create',
|
||
id: tabId,
|
||
url: view.webContents.getURL(),
|
||
windowId: newWindowId,
|
||
title: view.webContents.getTitle(),
|
||
state: isLoading ? 'loading' : 'loaded',
|
||
favicon,
|
||
}).then(_ => { })
|
||
utils.onDispatchEvent(newWindow.webContents, {
|
||
event: 'switch',
|
||
id: tabId,
|
||
}).then(_ => { })
|
||
})
|
||
|
||
// 处理源窗口
|
||
if (sourceWindowData.views.length === 0) {
|
||
// 源窗口没有标签了,关闭它
|
||
userConf?.set('webTabWindow', sourceWindow.getBounds())
|
||
sourceWindow.destroy()
|
||
} else {
|
||
// 激活源窗口的下一个标签
|
||
activateWebTabInWindow(windowId, 0)
|
||
}
|
||
|
||
return newWindowId
|
||
}
|
||
|
||
/**
|
||
* 将标签附加到目标窗口
|
||
* @param sourceWindowId 源窗口ID
|
||
* @param tabId 标签ID
|
||
* @param targetWindowId 目标窗口ID
|
||
* @param insertIndex 插入位置(可选)
|
||
* @returns {boolean} 是否成功
|
||
*/
|
||
function attachWebTab(sourceWindowId, tabId, targetWindowId, insertIndex) {
|
||
if (sourceWindowId === targetWindowId) return false
|
||
|
||
const sourceWindowData = webTabWindows.get(sourceWindowId)
|
||
const targetWindowData = webTabWindows.get(targetWindowId)
|
||
if (!sourceWindowData || !targetWindowData) return false
|
||
|
||
const tabIndex = sourceWindowData.views.findIndex(v => v.id === tabId)
|
||
if (tabIndex === -1) return false
|
||
|
||
const tabItem = sourceWindowData.views[tabIndex]
|
||
const view = tabItem.view
|
||
const favicon = tabItem.favicon || ''
|
||
const tabName = tabItem.name || null
|
||
const sourceWindow = sourceWindowData.window
|
||
const targetWindow = targetWindowData.window
|
||
const userConf = getUserConf()
|
||
|
||
// 从源窗口移除视图
|
||
sourceWindow.contentView.removeChildView(view)
|
||
sourceWindowData.views.splice(tabIndex, 1)
|
||
|
||
// 通知源窗口标签已关闭
|
||
utils.onDispatchEvent(sourceWindow.webContents, {
|
||
event: 'close',
|
||
id: tabId,
|
||
}).then(_ => { })
|
||
|
||
// 更新视图所属窗口
|
||
view.webTabWindowId = targetWindowId
|
||
|
||
// 更新 name 映射中的 windowId
|
||
if (tabName) {
|
||
webTabNameMap.set(tabName, {
|
||
windowId: targetWindowId,
|
||
tabId: tabId
|
||
})
|
||
}
|
||
|
||
// 确定插入位置
|
||
const actualInsertIndex = typeof insertIndex === 'number'
|
||
? Math.max(0, Math.min(insertIndex, targetWindowData.views.length))
|
||
: targetWindowData.views.length
|
||
|
||
// 添加到目标窗口,保留 name 信息
|
||
targetWindowData.views.splice(actualInsertIndex, 0, {
|
||
id: tabId,
|
||
name: tabName,
|
||
view,
|
||
favicon
|
||
})
|
||
targetWindow.contentView.addChildView(view)
|
||
|
||
// 调整视图尺寸
|
||
view.setBounds({
|
||
x: 0,
|
||
y: webTabHeight,
|
||
width: targetWindow.getContentBounds().width || 1280,
|
||
height: (targetWindow.getContentBounds().height || 800) - webTabHeight,
|
||
})
|
||
|
||
// 通知目标窗口创建标签(传递完整状态信息)
|
||
const isLoading = view.webContents.isLoading()
|
||
utils.onDispatchEvent(targetWindow.webContents, {
|
||
event: 'create',
|
||
id: tabId,
|
||
url: view.webContents.getURL(),
|
||
windowId: targetWindowId,
|
||
title: view.webContents.getTitle(),
|
||
insertIndex: actualInsertIndex,
|
||
state: isLoading ? 'loading' : 'loaded',
|
||
favicon,
|
||
}).then(_ => { })
|
||
|
||
// 激活新添加的标签
|
||
activateWebTabInWindow(targetWindowId, tabId)
|
||
|
||
// 聚焦目标窗口
|
||
targetWindow.focus()
|
||
|
||
// 处理源窗口
|
||
if (sourceWindowData.views.length === 0) {
|
||
userConf?.set('webTabWindow', sourceWindow.getBounds())
|
||
sourceWindow.destroy()
|
||
} else {
|
||
activateWebTabInWindow(sourceWindowId, 0)
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* 获取所有 webTab 窗口信息(用于跨窗口拖拽检测)
|
||
* @returns {Array}
|
||
*/
|
||
function getAllWebTabWindowsInfo() {
|
||
const result = []
|
||
for (const [windowId, windowData] of webTabWindows) {
|
||
if (windowData.window && !windowData.window.isDestroyed()) {
|
||
const bounds = windowData.window.getBounds()
|
||
result.push({
|
||
windowId,
|
||
bounds,
|
||
tabBarBounds: {
|
||
x: bounds.x,
|
||
y: bounds.y,
|
||
width: bounds.width,
|
||
height: webTabHeight
|
||
},
|
||
tabCount: windowData.views.length
|
||
})
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
/**
|
||
* 从事件发送者获取窗口ID
|
||
* @param sender
|
||
* @returns {number|null}
|
||
*/
|
||
function getWindowIdFromSender(sender) {
|
||
const win = BrowserWindow.fromWebContents(sender)
|
||
if (win && win.webTabWindowId) {
|
||
return win.webTabWindowId
|
||
}
|
||
return null
|
||
}
|
||
|
||
// ============================================================
|
||
// 辅助函数
|
||
// ============================================================
|
||
|
||
// 窗口显示状态管理
|
||
let showState = {}
|
||
|
||
function onShowWindow(win) {
|
||
try {
|
||
if (typeof showState[win.webContents.id] === 'undefined') {
|
||
showState[win.webContents.id] = true
|
||
win.show()
|
||
}
|
||
} catch (e) {
|
||
// loger.error(e)
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 对外接口
|
||
// ============================================================
|
||
|
||
/**
|
||
* 获取所有 webTab 窗口(用于主题更新等)
|
||
* @returns {Map}
|
||
*/
|
||
function getWebTabWindows() {
|
||
return webTabWindows
|
||
}
|
||
|
||
/**
|
||
* 销毁所有 webTab 窗口
|
||
*/
|
||
function destroyAll() {
|
||
for (const [, windowData] of webTabWindows) {
|
||
if (windowData.window && !windowData.window.isDestroyed()) {
|
||
windowData.window.destroy()
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 关闭所有 webTab 窗口
|
||
* 通过逐个关闭标签实现,当最后一个标签关闭时窗口自动销毁
|
||
*/
|
||
function closeAll() {
|
||
// 复制 windowId 列表,避免遍历时 Map 被修改
|
||
const windowIds = [...webTabWindows.keys()]
|
||
for (const windowId of windowIds) {
|
||
const windowData = webTabWindows.get(windowId)
|
||
if (windowData && windowData.window && !windowData.window.isDestroyed()) {
|
||
// 复制 tabId 列表
|
||
const tabIds = windowData.views.map(v => v.id)
|
||
// 逐个关闭标签,最后一个关闭时窗口自动销毁
|
||
for (const tabId of tabIds) {
|
||
closeWebTabInWindow(windowId, tabId)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 关闭所有 mode='window' 的窗口
|
||
*/
|
||
function closeAllWindowMode() {
|
||
for (const [, data] of webTabWindows) {
|
||
if (data.mode === 'window' && data.window && !data.window.isDestroyed()) {
|
||
data.window.close()
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 销毁所有 mode='window' 的窗口
|
||
*/
|
||
function destroyAllWindowMode() {
|
||
for (const [, data] of webTabWindows) {
|
||
if (data.mode === 'window' && data.window && !data.window.isDestroyed()) {
|
||
data.window.destroy()
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// IPC 注册
|
||
// ============================================================
|
||
|
||
/**
|
||
* 注册所有 webTab 相关的 IPC 事件
|
||
*/
|
||
function registerIPC() {
|
||
const electronMenu = getElectronMenu()
|
||
|
||
/**
|
||
* 获取路由窗口信息(从 webTabWindows 中查找 mode='window' 的窗口)
|
||
*/
|
||
ipcMain.handle('getChildWindow', (event, args) => {
|
||
let windowData, viewItem
|
||
if (!args) {
|
||
// 通过发送者查找
|
||
const sender = event.sender
|
||
for (const [, data] of webTabWindows) {
|
||
if (data.mode === 'window') {
|
||
const found = data.views.find(v => v.view.webContents === sender)
|
||
if (found) {
|
||
windowData = data
|
||
viewItem = found
|
||
break
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// 通过名称查找
|
||
const location = webTabNameMap.get(args)
|
||
if (location) {
|
||
windowData = webTabWindows.get(location.windowId)
|
||
if (windowData && windowData.mode === 'window') {
|
||
viewItem = windowData.views.find(v => v.id === location.tabId)
|
||
}
|
||
}
|
||
}
|
||
if (windowData && viewItem) {
|
||
return {
|
||
name: viewItem.name,
|
||
id: viewItem.view.webContents.id,
|
||
url: viewItem.view.webContents.getURL()
|
||
}
|
||
}
|
||
return null
|
||
})
|
||
|
||
/**
|
||
* 统一窗口打开接口
|
||
*/
|
||
ipcMain.on('openWindow', (event, args) => {
|
||
createWebTabWindow(args)
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 更新当前窗口/标签页的 URL 和名称
|
||
*/
|
||
ipcMain.on('updateWindow', (event, args) => {
|
||
if (!args) {
|
||
event.returnValue = "ok"
|
||
return
|
||
}
|
||
|
||
if (!utils.isJson(args)) {
|
||
args = { path: args }
|
||
}
|
||
|
||
const sender = event.sender
|
||
let windowId, windowData, viewItem
|
||
|
||
// 通过发送者查找窗口和视图
|
||
for (const [id, data] of webTabWindows) {
|
||
const found = data.views.find(v => v.view.webContents === sender)
|
||
if (found) {
|
||
windowId = id
|
||
windowData = data
|
||
viewItem = found
|
||
break
|
||
}
|
||
}
|
||
|
||
if (!windowData || !viewItem) {
|
||
event.returnValue = "ok"
|
||
return
|
||
}
|
||
|
||
// 更新 URL
|
||
if (args.path) {
|
||
utils.loadContentUrl(viewItem.view.webContents, getServerUrl(), args.path)
|
||
}
|
||
|
||
// 更新名称
|
||
if (args.name && args.name !== viewItem.name) {
|
||
const oldName = viewItem.name
|
||
viewItem.name = args.name
|
||
|
||
// 更新 webTabNameMap
|
||
if (oldName) {
|
||
webTabNameMap.delete(oldName)
|
||
}
|
||
webTabNameMap.set(args.name, {
|
||
windowId: windowId,
|
||
tabId: viewItem.id
|
||
})
|
||
}
|
||
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 激活标签
|
||
*/
|
||
ipcMain.on('webTabActivate', (event, args) => {
|
||
let windowId, tabId
|
||
if (typeof args === 'object' && args !== null) {
|
||
windowId = args.windowId
|
||
tabId = args.tabId
|
||
} else {
|
||
tabId = args
|
||
windowId = findWindowIdByTabId(tabId)
|
||
}
|
||
if (windowId) {
|
||
activateWebTabInWindow(windowId, tabId)
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 重排标签顺序
|
||
*/
|
||
ipcMain.on('webTabReorder', (event, args) => {
|
||
let windowId, newOrder
|
||
if (Array.isArray(args)) {
|
||
newOrder = args
|
||
if (newOrder.length > 0) {
|
||
windowId = findWindowIdByTabId(newOrder[0])
|
||
}
|
||
} else if (typeof args === 'object' && args !== null) {
|
||
windowId = args.windowId
|
||
newOrder = args.newOrder
|
||
}
|
||
|
||
if (!windowId || !Array.isArray(newOrder) || newOrder.length === 0) {
|
||
event.returnValue = "ok"
|
||
return
|
||
}
|
||
|
||
const windowData = webTabWindows.get(windowId)
|
||
if (!windowData) {
|
||
event.returnValue = "ok"
|
||
return
|
||
}
|
||
|
||
windowData.views.sort((a, b) => {
|
||
const indexA = newOrder.indexOf(a.id)
|
||
const indexB = newOrder.indexOf(b.id)
|
||
return indexA - indexB
|
||
})
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 关闭标签
|
||
*/
|
||
ipcMain.on('webTabClose', (event, args) => {
|
||
let windowId, tabId
|
||
if (typeof args === 'object' && args !== null) {
|
||
windowId = args.windowId
|
||
tabId = args.tabId
|
||
} else {
|
||
tabId = args
|
||
windowId = findWindowIdByTabId(tabId)
|
||
}
|
||
if (windowId) {
|
||
closeWebTabInWindow(windowId, tabId)
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 在外部浏览器打开
|
||
*/
|
||
ipcMain.on('webTabExternal', (event) => {
|
||
const item = currentWebTab()
|
||
if (!item) {
|
||
return
|
||
}
|
||
renderer.openExternal(item.view.webContents.getURL()).catch(() => {})
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 显示更多菜单
|
||
*/
|
||
ipcMain.on('webTabShowMenu', (event, args) => {
|
||
const windowId = args?.windowId
|
||
const tabId = args?.tabId
|
||
const windowData = windowId ? webTabWindows.get(windowId) : null
|
||
const webTabWindow = windowData?.window
|
||
|
||
if (!webTabWindow || webTabWindow.isDestroyed()) {
|
||
event.returnValue = "ok"
|
||
return
|
||
}
|
||
|
||
const item = currentWebTabInWindow(windowId)
|
||
const webContents = item?.view?.webContents
|
||
const currentUrl = webContents?.getURL() || ''
|
||
const canBrowser = !utils.isLocalHost(currentUrl)
|
||
|
||
const menuTemplate = [
|
||
{
|
||
label: electronMenu?.language?.reload || 'Reload',
|
||
click: () => {
|
||
if (webContents && !webContents.isDestroyed()) {
|
||
webContents.reload()
|
||
}
|
||
}
|
||
},
|
||
{
|
||
label: electronMenu?.language?.copyLinkAddress || 'Copy Link',
|
||
enabled: canBrowser,
|
||
click: () => {
|
||
if (currentUrl) {
|
||
clipboard.writeText(currentUrl)
|
||
}
|
||
}
|
||
},
|
||
{
|
||
label: electronMenu?.language?.openInDefaultBrowser || 'Open in Browser',
|
||
enabled: canBrowser,
|
||
click: () => {
|
||
if (currentUrl) {
|
||
renderer.openExternal(currentUrl).catch(() => {})
|
||
}
|
||
}
|
||
},
|
||
{ type: 'separator' },
|
||
{
|
||
label: electronMenu?.language?.moveToNewWindow || 'Move to New Window',
|
||
enabled: windowData?.views?.length > 1,
|
||
click: () => {
|
||
if (tabId) {
|
||
const bounds = webTabWindow.getBounds()
|
||
detachWebTab(windowId, tabId, bounds.x + 50, bounds.y + 50)
|
||
}
|
||
}
|
||
},
|
||
{ type: 'separator' },
|
||
{
|
||
label: electronMenu?.language?.print || 'Print',
|
||
click: () => {
|
||
if (webContents && !webContents.isDestroyed()) {
|
||
webContents.print()
|
||
}
|
||
}
|
||
}
|
||
]
|
||
|
||
const menu = Menu.buildFromTemplate(menuTemplate)
|
||
menu.popup({
|
||
window: webTabWindow,
|
||
x: args?.x,
|
||
y: args?.y
|
||
})
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 打开开发者工具
|
||
*/
|
||
ipcMain.on('webTabOpenDevTools', (event) => {
|
||
const item = currentWebTab()
|
||
if (!item) {
|
||
return
|
||
}
|
||
item.view.webContents.openDevTools()
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 销毁所有标签及窗口
|
||
*/
|
||
ipcMain.on('webTabDestroyAll', (event) => {
|
||
destroyAll()
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 延迟发送导航状态
|
||
*/
|
||
function notifyNavigationState(item) {
|
||
setTimeout(() => {
|
||
const wd = webTabWindows.get(item.view.webTabWindowId)
|
||
if (wd && wd.window) {
|
||
utils.onDispatchEvent(wd.window.webContents, {
|
||
event: 'navigation-state',
|
||
id: item.id,
|
||
canGoBack: item.view.webContents.navigationHistory.canGoBack(),
|
||
canGoForward: item.view.webContents.navigationHistory.canGoForward()
|
||
}).then(_ => { })
|
||
}
|
||
}, 100)
|
||
}
|
||
|
||
/**
|
||
* 内置浏览器 - 后退
|
||
*/
|
||
ipcMain.on('webTabGoBack', (event, args) => {
|
||
const windowId = args?.windowId || getWindowIdFromSender(event.sender)
|
||
const item = windowId ? currentWebTabInWindow(windowId) : currentWebTab()
|
||
if (!item) {
|
||
event.returnValue = "ok"
|
||
return
|
||
}
|
||
if (item.view.webContents.navigationHistory.canGoBack()) {
|
||
item.view.webContents.navigationHistory.goBack()
|
||
notifyNavigationState(item)
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 前进
|
||
*/
|
||
ipcMain.on('webTabGoForward', (event, args) => {
|
||
const windowId = args?.windowId || getWindowIdFromSender(event.sender)
|
||
const item = windowId ? currentWebTabInWindow(windowId) : currentWebTab()
|
||
if (!item) {
|
||
event.returnValue = "ok"
|
||
return
|
||
}
|
||
if (item.view.webContents.navigationHistory.canGoForward()) {
|
||
item.view.webContents.navigationHistory.goForward()
|
||
notifyNavigationState(item)
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 刷新
|
||
*/
|
||
ipcMain.on('webTabReload', (event, args) => {
|
||
const windowId = args?.windowId || getWindowIdFromSender(event.sender)
|
||
const item = windowId ? currentWebTabInWindow(windowId) : currentWebTab()
|
||
if (!item) {
|
||
event.returnValue = "ok"
|
||
return
|
||
}
|
||
item.view.webContents.reload()
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 停止加载
|
||
*/
|
||
ipcMain.on('webTabStop', (event, args) => {
|
||
const windowId = args?.windowId || getWindowIdFromSender(event.sender)
|
||
const item = windowId ? currentWebTabInWindow(windowId) : currentWebTab()
|
||
if (!item) {
|
||
event.returnValue = "ok"
|
||
return
|
||
}
|
||
item.view.webContents.stop()
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 获取导航状态
|
||
*/
|
||
ipcMain.on('webTabGetNavigationState', (event, args) => {
|
||
const windowId = args?.windowId || getWindowIdFromSender(event.sender)
|
||
const item = windowId ? currentWebTabInWindow(windowId) : currentWebTab()
|
||
if (!item) {
|
||
event.returnValue = "ok"
|
||
return
|
||
}
|
||
|
||
const canGoBack = item.view.webContents.navigationHistory.canGoBack()
|
||
const canGoForward = item.view.webContents.navigationHistory.canGoForward()
|
||
|
||
const wd = webTabWindows.get(item.view.webTabWindowId)
|
||
if (wd && wd.window) {
|
||
utils.onDispatchEvent(wd.window.webContents, {
|
||
event: 'navigation-state',
|
||
id: item.id,
|
||
canGoBack,
|
||
canGoForward
|
||
}).then(_ => { })
|
||
}
|
||
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 分离标签到新窗口
|
||
*/
|
||
ipcMain.on('webTabDetach', (event, args) => {
|
||
const { windowId, tabId, screenX, screenY } = args
|
||
detachWebTab(windowId, tabId, screenX, screenY)
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 将标签附加到目标窗口
|
||
*/
|
||
ipcMain.on('webTabAttach', (event, args) => {
|
||
const { sourceWindowId, tabId, targetWindowId, insertIndex } = args
|
||
attachWebTab(sourceWindowId, tabId, targetWindowId, insertIndex)
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 获取所有窗口信息
|
||
*/
|
||
ipcMain.handle('webTabGetAllWindows', () => {
|
||
return getAllWebTabWindowsInfo()
|
||
})
|
||
|
||
/**
|
||
* 关闭窗口(或关闭 tab)
|
||
*/
|
||
ipcMain.on('windowClose', (event) => {
|
||
const tabId = event.sender.id
|
||
const windowId = findWindowIdByTabId(tabId)
|
||
if (windowId !== null) {
|
||
closeWebTabInWindow(windowId, tabId)
|
||
} else {
|
||
const win = BrowserWindow.fromWebContents(event.sender)
|
||
win?.close()
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 销毁窗口(或销毁 tab)
|
||
*/
|
||
ipcMain.on('windowDestroy', (event) => {
|
||
const tabId = event.sender.id
|
||
const windowId = findWindowIdByTabId(tabId)
|
||
if (windowId !== null) {
|
||
closeWebTabInWindow(windowId, tabId)
|
||
} else {
|
||
const win = BrowserWindow.fromWebContents(event.sender)
|
||
win?.destroy()
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
}
|
||
|
||
// ============================================================
|
||
// 导出
|
||
// ============================================================
|
||
|
||
module.exports = {
|
||
// 初始化
|
||
init,
|
||
registerIPC,
|
||
|
||
// 核心功能
|
||
createWebTabWindow,
|
||
closeWebTabInWindow,
|
||
activateWebTabInWindow,
|
||
findWindowIdByTabId,
|
||
|
||
// 预加载
|
||
warmupPreloadPool,
|
||
clearPreloadPool,
|
||
|
||
// 对外接口
|
||
getWebTabWindows,
|
||
closeAll,
|
||
destroyAll,
|
||
closeAllWindowMode,
|
||
destroyAllWindowMode,
|
||
}
|