mirror of
https://github.com/kuaifan/dootask.git
synced 2026-01-13 01:28:11 +00:00
2515 lines
75 KiB
JavaScript
Vendored
2515 lines
75 KiB
JavaScript
Vendored
// Node.js 核心模块
|
||
const fs = require('fs')
|
||
const os = require("os");
|
||
const path = require('path')
|
||
const spawn = require("child_process").spawn;
|
||
|
||
// Web 服务相关
|
||
const express = require('express')
|
||
const axios = require('axios');
|
||
|
||
// Electron 核心模块
|
||
const {
|
||
app,
|
||
ipcMain,
|
||
dialog,
|
||
clipboard,
|
||
nativeImage,
|
||
globalShortcut,
|
||
nativeTheme,
|
||
screen,
|
||
Tray,
|
||
Menu,
|
||
WebContentsView,
|
||
BrowserWindow
|
||
} = require('electron')
|
||
|
||
// 禁用渲染器后台化
|
||
app.commandLine.appendSwitch('disable-renderer-backgrounding');
|
||
app.commandLine.appendSwitch('disable-backgrounding-occluded-windows');
|
||
|
||
// Electron 扩展和工具
|
||
const {autoUpdater} = require("electron-updater")
|
||
const Store = require("electron-store");
|
||
const loger = require("electron-log");
|
||
const electronConf = require('electron-config')
|
||
const Screenshots = require("electron-screenshots-tool").Screenshots;
|
||
|
||
// 本地模块和配置
|
||
const utils = require('./lib/utils');
|
||
const navigation = require('./lib/navigation');
|
||
const config = require('./package.json');
|
||
const electronDown = require("./electron-down");
|
||
const electronMenu = require("./electron-menu");
|
||
const { startMCPServer, stopMCPServer } = require("./lib/mcp");
|
||
const {onRenderer, renderer} = require("./lib/renderer");
|
||
const {onExport} = require("./lib/pdf-export");
|
||
const {allowedCalls, isWin, isMac} = require("./lib/other");
|
||
|
||
// 实例初始化
|
||
const userConf = new electronConf()
|
||
const store = new Store();
|
||
|
||
// 路径和缓存配置
|
||
const cacheDir = path.join(os.tmpdir(), 'dootask-cache')
|
||
const updaterLockFile = path.join(cacheDir, '.dootask_updater.lock');
|
||
|
||
// 应用状态标志
|
||
let isReady = false,
|
||
willQuitApp = false,
|
||
isDevelopMode = false;
|
||
|
||
// 服务器配置
|
||
let serverPort = 22223,
|
||
mcpPort = 22224,
|
||
serverPublicDir = path.join(__dirname, 'public'),
|
||
serverUrl = "",
|
||
serverTimer = null;
|
||
|
||
// 截图相关变量
|
||
let screenshotObj = null,
|
||
screenshotKey = null;
|
||
|
||
// 窗口实例变量
|
||
let mainWindow = null,
|
||
mainTray = null,
|
||
mediaWindow = null;
|
||
|
||
// 多窗口 Tab 管理
|
||
// 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();
|
||
|
||
// 窗口配置和状态
|
||
let mediaType = null,
|
||
webTabHeight = 40,
|
||
webTabClosedByShortcut = new Map(); // Map<windowId, boolean>
|
||
|
||
// 开发模式路径
|
||
let devloadPath = path.resolve(__dirname, ".devload");
|
||
|
||
// 窗口显示状态管理
|
||
let showState = {},
|
||
onShowWindow = (win) => {
|
||
try {
|
||
if (typeof showState[win.webContents.id] === 'undefined') {
|
||
showState[win.webContents.id] = true
|
||
win.show();
|
||
}
|
||
} catch (e) {
|
||
// loger.error(e)
|
||
}
|
||
}
|
||
|
||
// 开发模式加载
|
||
if (fs.existsSync(devloadPath)) {
|
||
let devloadContent = fs.readFileSync(devloadPath, 'utf8')
|
||
if (devloadContent.startsWith('http')) {
|
||
serverUrl = devloadContent;
|
||
isDevelopMode = true;
|
||
}
|
||
}
|
||
|
||
// 缓存目录检查
|
||
if (!fs.existsSync(cacheDir)) {
|
||
fs.mkdirSync(cacheDir, { recursive: true });
|
||
}
|
||
|
||
// 初始化下载
|
||
electronDown.initialize(() => {
|
||
if (mainWindow) {
|
||
mainWindow.webContents.send("openDownloadWindow", {})
|
||
}
|
||
})
|
||
|
||
/**
|
||
* 启动web服务
|
||
*/
|
||
async function startWebServer(force = false) {
|
||
if (serverUrl && !force) {
|
||
return Promise.resolve();
|
||
}
|
||
|
||
// 每次启动前清理缓存
|
||
utils.clearServerCache();
|
||
|
||
return new Promise((resolve, reject) => {
|
||
// 创建Express应用
|
||
const expressApp = express();
|
||
|
||
// 健康检查
|
||
expressApp.head('/health', (req, res) => {
|
||
res.status(200).send('OK');
|
||
});
|
||
|
||
// 使用express.static中间件提供静态文件服务
|
||
// Express内置了全面的MIME类型支持,无需手动配置
|
||
expressApp.use(express.static(serverPublicDir, {
|
||
// 设置默认文件
|
||
index: ['index.html', 'index.htm'],
|
||
// 启用etag缓存
|
||
etag: true,
|
||
// 设置缓存时间(开发环境可以设置较短)
|
||
maxAge: '1h',
|
||
// 启用压缩
|
||
dotfiles: 'ignore',
|
||
// 自定义头部
|
||
setHeaders: (res, path, stat) => {
|
||
const ext = path.split('.').pop().toLowerCase();
|
||
// HTML、JS、CSS文件禁用缓存,方便开发调试
|
||
if (['html', 'js', 'css'].includes(ext)){
|
||
res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||
res.set('Pragma', 'no-cache');
|
||
res.set('Expires', '0');
|
||
}
|
||
}
|
||
}));
|
||
|
||
// 404处理中间件
|
||
expressApp.use((req, res) => {
|
||
res.status(404).send('File not found');
|
||
});
|
||
|
||
// 错误处理中间件
|
||
expressApp.use((err, req, res, next) => {
|
||
// 不是ENOENT错误,记录error级别日志
|
||
if (err.code !== 'ENOENT') {
|
||
loger.error('Server error:', err);
|
||
res.status(500).send('Internal Server Error');
|
||
return;
|
||
}
|
||
|
||
// 没有path,说明是404错误
|
||
if (!err.path) {
|
||
loger.warn('File not found:', req.url);
|
||
res.status(404).send('File not found');
|
||
return;
|
||
}
|
||
|
||
// 不是临时文件错误,普通404
|
||
if (!err.path.includes('.com.dootask.task.')) {
|
||
loger.warn('File not found:', err.path);
|
||
res.status(404).send('File not found');
|
||
return;
|
||
}
|
||
|
||
// 防止死循环 - 如果已经是重定向请求,直接返回404
|
||
if (req.query._dt_restored) {
|
||
const redirectTime = parseInt(req.query._dt_restored);
|
||
const timeDiff = Date.now() - redirectTime;
|
||
// 10秒内的重定向认为是死循环,直接返回404
|
||
if (timeDiff < 10000) {
|
||
loger.warn('Recent redirect detected, avoiding loop:', timeDiff + 'ms ago');
|
||
res.status(404).send('File not found');
|
||
return;
|
||
}
|
||
}
|
||
|
||
loger.warn('Temporary file cleaned up by system:', err.path, req.url);
|
||
|
||
// 临时文件被系统清理,尝试从serverPublicDir重新读取并恢复
|
||
const requestedUrl = new URL(req.url, serverUrl);
|
||
const requestedFile = path.join(serverPublicDir, requestedUrl.pathname === '/' ? '/index.html' : requestedUrl.pathname);
|
||
try {
|
||
// 检查文件是否存在于serverPublicDir
|
||
fs.accessSync(requestedFile, fs.constants.F_OK);
|
||
|
||
// 确保目标目录存在
|
||
const targetDir = path.dirname(err.path);
|
||
if (!fs.existsSync(targetDir)) {
|
||
fs.mkdirSync(targetDir, {recursive: true});
|
||
}
|
||
|
||
// 从ASAR文件中读取文件并写入到临时位置
|
||
fs.writeFileSync(err.path, fs.readFileSync(requestedFile));
|
||
|
||
// 文件恢复成功后,301重定向到带__redirect参数的URL
|
||
requestedUrl.searchParams.set('_dt_restored', Date.now());
|
||
res.redirect(301, requestedUrl.toString());
|
||
} catch (accessErr) {
|
||
// 文件不存在于serverPublicDir,返回404
|
||
loger.warn('Source file not found:', requestedFile, 'Error:', accessErr.message);
|
||
res.status(404).send('File not found');
|
||
}
|
||
});
|
||
|
||
// 启动服务器
|
||
const server = expressApp.listen(serverPort, 'localhost', () => {
|
||
loger.info(`Express static file server running at http://localhost:${serverPort}/`);
|
||
loger.info(`Serving files from: ${serverPublicDir}`);
|
||
serverUrl = `http://localhost:${serverPort}/`;
|
||
resolve(server);
|
||
// 启动健康检查定时器
|
||
serverTimeout();
|
||
});
|
||
|
||
// 错误处理
|
||
server.on('error', (err) => {
|
||
loger.error('Server error:', err);
|
||
reject(err);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 健康检查定时器
|
||
*/
|
||
function serverTimeout() {
|
||
clearTimeout(serverTimer)
|
||
serverTimer = setTimeout(async () => {
|
||
if (!serverUrl) {
|
||
return; // 没有服务器URL,直接返回
|
||
}
|
||
try {
|
||
const res = await axios.head(serverUrl + 'health')
|
||
if (res.status === 200) {
|
||
serverTimeout() // 健康检查通过,重新设置定时器
|
||
return;
|
||
}
|
||
loger.error('Server health check failed with status: ' + res.status);
|
||
} catch (err) {
|
||
loger.error('Server health check error:', err);
|
||
}
|
||
// 如果健康检查失败,尝试重新启动服务器
|
||
try {
|
||
await startWebServer(true)
|
||
loger.info('Server restarted successfully');
|
||
} catch (error) {
|
||
loger.error('Failed to restart server:', error);
|
||
}
|
||
}, 10000)
|
||
}
|
||
|
||
/**
|
||
* 创建主窗口
|
||
*/
|
||
function createMainWindow() {
|
||
mainWindow = new BrowserWindow({
|
||
width: 1280,
|
||
height: 800,
|
||
minWidth: 360,
|
||
minHeight: 360,
|
||
center: true,
|
||
autoHideMenuBar: true,
|
||
backgroundColor: utils.getDefaultBackgroundColor(),
|
||
webPreferences: {
|
||
preload: path.join(__dirname, 'electron-preload.js'),
|
||
webSecurity: true,
|
||
nodeIntegration: true,
|
||
contextIsolation: true,
|
||
backgroundThrottling: false,
|
||
}
|
||
})
|
||
|
||
mainWindow.on('page-title-updated', (event, title) => {
|
||
if (title == "index.html") {
|
||
event.preventDefault()
|
||
}
|
||
})
|
||
|
||
mainWindow.on('focus', () => {
|
||
mainWindow.webContents.send("browserWindowFocus", {})
|
||
})
|
||
|
||
mainWindow.on('blur', () => {
|
||
mainWindow.webContents.send("browserWindowBlur", {})
|
||
})
|
||
|
||
mainWindow.on('close', event => {
|
||
if (!willQuitApp) {
|
||
utils.onBeforeUnload(event, mainWindow).then(() => {
|
||
if (['darwin', 'win32'].includes(process.platform)) {
|
||
if (mainWindow.isFullScreen()) {
|
||
mainWindow.once('leave-full-screen', () => {
|
||
mainWindow.hide();
|
||
})
|
||
mainWindow.setFullScreen(false)
|
||
} else {
|
||
mainWindow.hide();
|
||
}
|
||
} else {
|
||
app.quit();
|
||
}
|
||
})
|
||
}
|
||
})
|
||
|
||
// 设置 UA
|
||
const originalUA = mainWindow.webContents.session.getUserAgent() || mainWindow.webContents.getUserAgent()
|
||
mainWindow.webContents.setUserAgent(originalUA + " MainTaskWindow/" + process.platform + "/" + os.arch() + "/1.0");
|
||
|
||
// 新窗口处理
|
||
mainWindow.webContents.setWindowOpenHandler(({url}) => {
|
||
if (allowedCalls.test(url)) {
|
||
renderer.openExternal(url).catch(() => {})
|
||
} else {
|
||
utils.onBeforeOpenWindow(mainWindow.webContents, url).then(() => {
|
||
renderer.openExternal(url).catch(() => {})
|
||
})
|
||
}
|
||
return {action: 'deny'}
|
||
})
|
||
|
||
// 设置右键菜单
|
||
electronMenu.webContentsMenu(mainWindow.webContents)
|
||
|
||
// 设置导航快捷键(返回/前进)
|
||
navigation.setup(mainWindow)
|
||
|
||
// 加载地址
|
||
utils.loadUrl(mainWindow, serverUrl)
|
||
}
|
||
|
||
/**
|
||
* 创建更新程序子进程
|
||
*/
|
||
function createUpdaterWindow(updateTitle) {
|
||
// 检查平台是否支持
|
||
if (!['darwin', 'win32'].includes(process.platform)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 构建updater应用路径
|
||
let updaterPath;
|
||
if (isWin) {
|
||
updaterPath = path.join(process.resourcesPath, 'updater', 'updater.exe');
|
||
} else {
|
||
updaterPath = path.join(process.resourcesPath, 'updater', 'updater');
|
||
}
|
||
|
||
// 检查updater应用是否存在
|
||
if (!fs.existsSync(updaterPath)) {
|
||
loger.error('Updater not found:', updaterPath);
|
||
return;
|
||
}
|
||
|
||
// 检查文件权限
|
||
try {
|
||
fs.accessSync(updaterPath, fs.constants.X_OK);
|
||
} catch (e) {
|
||
if (isWin) {
|
||
try {
|
||
spawn('icacls', [updaterPath, '/grant', 'everyone:F'], { stdio: 'inherit', shell: true });
|
||
} catch (e) {
|
||
loger.error('Failed to set executable permission:', e);
|
||
}
|
||
} else if (process.platform === 'darwin') {
|
||
try {
|
||
spawn('chmod', ['+x', updaterPath], {stdio: 'inherit'});
|
||
} catch (e) {
|
||
loger.error('Failed to set executable permission:', e);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 创建锁文件
|
||
fs.writeFileSync(updaterLockFile, Date.now().toString());
|
||
|
||
// 启动子进程,传入锁文件路径作为第一个参数
|
||
const child = spawn(updaterPath, [updaterLockFile], {
|
||
detached: true,
|
||
stdio: 'ignore',
|
||
shell: isWin,
|
||
env: {
|
||
...process.env,
|
||
ELECTRON_RUN_AS_NODE: '1',
|
||
UPDATER_TITLE: updateTitle || ''
|
||
}
|
||
});
|
||
|
||
child.unref();
|
||
|
||
child.on('error', (err) => {
|
||
loger.error('Updater process error:', err);
|
||
});
|
||
|
||
} catch (e) {
|
||
loger.error('Failed to create updater process:', e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建媒体浏览器窗口
|
||
* @param args
|
||
* @param type
|
||
*/
|
||
function createMediaWindow(args, type = 'image') {
|
||
if (mediaWindow === null) {
|
||
mediaWindow = new BrowserWindow({
|
||
width: Math.floor(args.width) || 970,
|
||
height: Math.floor(args.height) || 700,
|
||
minWidth: 360,
|
||
minHeight: 360,
|
||
autoHideMenuBar: true,
|
||
webPreferences: {
|
||
nodeIntegration: true,
|
||
contextIsolation: false,
|
||
webSecurity: false,
|
||
plugins: true
|
||
},
|
||
show: false
|
||
});
|
||
|
||
// 监听关闭事件
|
||
mediaWindow.addListener('close', event => {
|
||
if (!willQuitApp) {
|
||
event.preventDefault()
|
||
if (mediaWindow.isFullScreen()) {
|
||
mediaWindow.once('leave-full-screen', () => {
|
||
mediaWindow.hide();
|
||
})
|
||
mediaWindow.setFullScreen(false)
|
||
} else {
|
||
mediaWindow.webContents.send('on-close');
|
||
mediaWindow.hide();
|
||
}
|
||
}
|
||
})
|
||
|
||
// 监听关闭事件
|
||
mediaWindow.addListener('closed', () => {
|
||
mediaWindow = null;
|
||
mediaType = null;
|
||
})
|
||
|
||
// 设置右键菜单
|
||
electronMenu.webContentsMenu(mediaWindow.webContents)
|
||
} else {
|
||
// 直接显示
|
||
mediaWindow.show();
|
||
}
|
||
|
||
// 加载图片浏览器的HTML
|
||
if (mediaType === type) {
|
||
// 更新窗口
|
||
mediaWindow.webContents.send('load-media', args);
|
||
} else {
|
||
// 重置窗口
|
||
mediaType = type;
|
||
let filePath = './render/viewer/index.html';
|
||
if (type === 'video') {
|
||
filePath = './render/video/index.html';
|
||
}
|
||
mediaWindow.loadFile(filePath, {}).then(_ => { }).catch(_ => { })
|
||
}
|
||
|
||
// 窗口准备好后事件
|
||
mediaWindow.removeAllListeners("ready-to-show");
|
||
mediaWindow.addListener('ready-to-show', () => {
|
||
mediaWindow.show();
|
||
mediaWindow.webContents.send('load-media', args);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 创建内置浏览器窗口(支持多窗口)
|
||
* @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';
|
||
|
||
// 如果有 name,先查找是否已存在同名标签/窗口
|
||
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);
|
||
|
||
// force=true 时重新加载
|
||
if (args.force === true && args.url) {
|
||
utils.loadContentUrl(viewItem.view.webContents, serverUrl, args.url);
|
||
}
|
||
return existing.windowId;
|
||
}
|
||
}
|
||
// 标签已失效,清理映射
|
||
webTabNameMap.delete(args.name);
|
||
}
|
||
}
|
||
|
||
// 确定目标窗口ID
|
||
let windowId = args.windowId;
|
||
let windowData = windowId ? webTabWindows.get(windowId) : null;
|
||
let webTabWindow = windowData ? windowData.window : null;
|
||
|
||
// 如果没有指定窗口或窗口不存在,查找可用窗口或创建新窗口
|
||
if (!webTabWindow) {
|
||
// window 模式总是创建新窗口;tab 模式尝试使用第一个可用的 tab 窗口
|
||
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++;
|
||
// 从 args 中提取窗口尺寸
|
||
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();
|
||
|
||
// 创建 tab 子视图
|
||
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));
|
||
}
|
||
|
||
// 插入到指定位置,包含 name 信息
|
||
windowData.views.splice(insertIndex, 0, {
|
||
id: browserView.webContents.id,
|
||
view: browserView,
|
||
name: args.name || null
|
||
});
|
||
|
||
// 如果有 name,注册到映射
|
||
if (args.name) {
|
||
webTabNameMap.set(args.name, {
|
||
windowId: windowId,
|
||
tabId: browserView.webContents.id
|
||
});
|
||
}
|
||
|
||
// tab 模式通知标签栏创建标签;window 模式设置窗口标题
|
||
if (isWindowMode) {
|
||
// window 模式下,如果传入了 title 参数,设置窗口标题
|
||
if (args.title) {
|
||
webTabWindow.setTitle(args.title);
|
||
}
|
||
} else {
|
||
// tab 模式下通知标签栏创建新标签
|
||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||
event: 'create',
|
||
id: browserView.webContents.id,
|
||
url: args.url,
|
||
afterId: args.afterId,
|
||
windowId: windowId,
|
||
title: args.title,
|
||
}).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,
|
||
},
|
||
};
|
||
|
||
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 (!willQuitApp) {
|
||
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('./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 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;
|
||
}
|
||
|
||
const 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,
|
||
});
|
||
|
||
// 保存所属窗口ID和元数据
|
||
browserView.webTabWindowId = windowId;
|
||
browserView.tabName = args.name || null;
|
||
browserView.titleFixed = args.titleFixed || false;
|
||
|
||
// 设置自定义 UserAgent
|
||
if (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] || '';
|
||
|
||
// 验证并转换 favicon 为 base64
|
||
const base64Favicon = await utils.fetchFaviconAsBase64(faviconUrl);
|
||
|
||
// 保存验证后的 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);
|
||
}
|
||
});
|
||
|
||
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);
|
||
|
||
// 加载地址
|
||
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) {}
|
||
// 第二:使用当前聚焦的 webContents
|
||
try {
|
||
const focused = require('electron').webContents.getFocusedWebContents?.();
|
||
if (focused) {
|
||
const item = webTabView.find(it => it.id === focused.id);
|
||
if (item) {
|
||
return item;
|
||
}
|
||
}
|
||
} catch (e) {}
|
||
// 兜底:根据 children 顺序选择最上层的可用视图
|
||
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 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;
|
||
|
||
// 从源窗口移除视图
|
||
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
|
||
};
|
||
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;
|
||
|
||
// 从源窗口移除视图
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* 监听主题变化
|
||
*/
|
||
function monitorThemeChanges() {
|
||
let currentTheme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
||
nativeTheme.on('updated', () => {
|
||
const newTheme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
||
if (currentTheme === newTheme) {
|
||
return
|
||
}
|
||
currentTheme = newTheme;
|
||
// 更新背景
|
||
const backgroundColor = utils.getDefaultBackgroundColor()
|
||
mainWindow?.setBackgroundColor(backgroundColor);
|
||
mediaWindow?.setBackgroundColor(backgroundColor);
|
||
// 更新所有 webTab 窗口背景
|
||
for (const [, windowData] of webTabWindows) {
|
||
windowData.window?.setBackgroundColor(nativeTheme.shouldUseDarkColors ? '#575757' : '#FFFFFF');
|
||
}
|
||
// 通知所有窗口
|
||
BrowserWindow.getAllWindows().forEach(window => {
|
||
window.webContents.send('systemThemeChanged', {
|
||
theme: currentTheme,
|
||
});
|
||
});
|
||
})
|
||
}
|
||
|
||
const getTheLock = app.requestSingleInstanceLock()
|
||
if (!getTheLock) {
|
||
app.quit()
|
||
} else {
|
||
app.on('second-instance', () => {
|
||
utils.setShowWindow(mainWindow)
|
||
})
|
||
app.on('ready', async () => {
|
||
isReady = true
|
||
isWin && app.setAppUserModelId(config.appId)
|
||
// 启动 Web 服务器
|
||
try {
|
||
await startWebServer()
|
||
} catch (error) {
|
||
dialog.showErrorBox('启动失败', `Web 服务器启动失败:${error.message}`);
|
||
app.quit();
|
||
return;
|
||
}
|
||
// SameSite
|
||
utils.useCookie()
|
||
// 创建主窗口
|
||
createMainWindow()
|
||
// 监听主题变化
|
||
monitorThemeChanges()
|
||
// 创建托盘
|
||
if (['darwin', 'win32'].includes(process.platform) && utils.isJson(config.trayIcon)) {
|
||
mainTray = new Tray(path.join(__dirname, config.trayIcon[isDevelopMode ? 'dev' : 'prod'][process.platform === 'darwin' ? 'mac' : 'win']));
|
||
mainTray.on('click', () => {
|
||
utils.setShowWindow(mainWindow)
|
||
})
|
||
mainTray.setToolTip(config.name)
|
||
if (process.platform === 'win32') {
|
||
const trayMenu = Menu.buildFromTemplate([{
|
||
label: '显示',
|
||
click: () => {
|
||
utils.setShowWindow(mainWindow)
|
||
}
|
||
}, {
|
||
label: '退出',
|
||
click: () => {
|
||
app.quit()
|
||
}
|
||
}])
|
||
mainTray.setContextMenu(trayMenu)
|
||
}
|
||
}
|
||
// 删除updater锁文件(如果存在)
|
||
if (fs.existsSync(updaterLockFile)) {
|
||
try {
|
||
fs.unlinkSync(updaterLockFile);
|
||
} catch (e) {
|
||
//忽略错误
|
||
}
|
||
}
|
||
// 截图对象
|
||
screenshotObj = new Screenshots({
|
||
singleWindow: true,
|
||
mainWindow: mainWindow
|
||
})
|
||
})
|
||
}
|
||
|
||
app.on('activate', () => {
|
||
if (BrowserWindow.getAllWindows().length === 0) {
|
||
if (isReady) {
|
||
createMainWindow()
|
||
}
|
||
} else if (mainWindow) {
|
||
if (!mainWindow.isVisible()) {
|
||
mainWindow.show()
|
||
}
|
||
}
|
||
})
|
||
|
||
app.on('window-all-closed', () => {
|
||
if (willQuitApp || process.platform !== 'darwin') {
|
||
app.quit()
|
||
}
|
||
})
|
||
|
||
app.on('before-quit', () => {
|
||
willQuitApp = true
|
||
})
|
||
|
||
app.on("will-quit", () => {
|
||
globalShortcut.unregisterAll();
|
||
})
|
||
|
||
/**
|
||
* 设置菜单语言包
|
||
* @param args {path}
|
||
*/
|
||
ipcMain.on('setMenuLanguage', (event, args) => {
|
||
if (utils.isJson(args)) {
|
||
electronMenu.setLanguage(args)
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 打开文件
|
||
* @param args {path}
|
||
*/
|
||
ipcMain.on('openFile', (event, args) => {
|
||
utils.openFile(args.path)
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 退出客户端
|
||
*/
|
||
ipcMain.on('windowQuit', (event) => {
|
||
event.returnValue = "ok"
|
||
app.quit();
|
||
})
|
||
|
||
/**
|
||
* 获取路由窗口信息(从 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('openMediaViewer', (event, args) => {
|
||
createMediaWindow(args, ['image', 'video'].includes(args.type) ? args.type : 'image');
|
||
event.returnValue = "ok"
|
||
});
|
||
|
||
/**
|
||
* 统一窗口打开接口
|
||
* @param args {url, name, mode, force, config, userAgent, webPreferences, ...}
|
||
* - url: 要打开的地址
|
||
* - name: 窗口/标签名称
|
||
* - mode: 'tab' | 'window'
|
||
* - 'window': 独立窗口模式(无导航栏)
|
||
* - 'tab': 标签页模式(默认,有导航栏)
|
||
*/
|
||
ipcMain.on('openWindow', (event, args) => {
|
||
// 统一使用 createWebTabWindow,通过 mode 区分窗口类型
|
||
createWebTabWindow(args)
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 激活标签
|
||
* @param args {windowId, tabId} 或 tabId
|
||
*/
|
||
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"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 重排标签顺序
|
||
* @param args {windowId, newOrder} 或 newOrder数组
|
||
*/
|
||
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
|
||
}
|
||
|
||
// 根据新顺序重排 views 数组
|
||
windowData.views.sort((a, b) => {
|
||
const indexA = newOrder.indexOf(a.id)
|
||
const indexB = newOrder.indexOf(b.id)
|
||
return indexA - indexB
|
||
})
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 关闭标签
|
||
* @param args {windowId, tabId} 或 tabId
|
||
*/
|
||
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,
|
||
click: () => {
|
||
if (webContents && !webContents.isDestroyed()) {
|
||
webContents.reload()
|
||
}
|
||
}
|
||
},
|
||
{
|
||
label: electronMenu.language.copyLinkAddress,
|
||
enabled: canBrowser,
|
||
click: () => {
|
||
if (currentUrl) {
|
||
clipboard.writeText(currentUrl)
|
||
}
|
||
}
|
||
},
|
||
{
|
||
label: electronMenu.language.openInDefaultBrowser,
|
||
enabled: canBrowser,
|
||
click: () => {
|
||
if (currentUrl) {
|
||
renderer.openExternal(currentUrl).catch(() => {})
|
||
}
|
||
}
|
||
},
|
||
{ type: 'separator' },
|
||
{
|
||
label: electronMenu.language.moveToNewWindow,
|
||
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,
|
||
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) => {
|
||
for (const [, windowData] of webTabWindows) {
|
||
if (windowData.window && !windowData.window.isDestroyed()) {
|
||
windowData.window.destroy();
|
||
}
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 后退
|
||
* @param args {windowId} 可选
|
||
*/
|
||
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.canGoBack()) {
|
||
item.view.webContents.goBack()
|
||
// 导航后更新状态
|
||
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.canGoBack(),
|
||
canGoForward: item.view.webContents.canGoForward()
|
||
}).then(_ => { })
|
||
}
|
||
}, 100)
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 前进
|
||
* @param args {windowId} 可选
|
||
*/
|
||
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.canGoForward()) {
|
||
item.view.webContents.goForward()
|
||
// 导航后更新状态
|
||
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.canGoBack(),
|
||
canGoForward: item.view.webContents.canGoForward()
|
||
}).then(_ => { })
|
||
}
|
||
}, 100)
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 刷新
|
||
* @param args {windowId} 可选
|
||
*/
|
||
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()
|
||
// 刷新完成后会触发 did-stop-loading 事件,在那里会更新导航状态
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 停止加载
|
||
* @param args {windowId} 可选
|
||
*/
|
||
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"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 获取导航状态
|
||
* @param args {windowId} 可选
|
||
*/
|
||
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.canGoBack()
|
||
const canGoForward = item.view.webContents.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"
|
||
})
|
||
|
||
/**
|
||
* 从事件发送者获取窗口ID
|
||
* @param sender
|
||
* @returns {number|null}
|
||
*/
|
||
function getWindowIdFromSender(sender) {
|
||
const win = BrowserWindow.fromWebContents(sender);
|
||
if (win && win.webTabWindowId) {
|
||
return win.webTabWindowId;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 内置浏览器 - 分离标签到新窗口
|
||
* @param args {windowId, tabId, screenX, screenY}
|
||
*/
|
||
ipcMain.on('webTabDetach', (event, args) => {
|
||
const {windowId, tabId, screenX, screenY} = args;
|
||
detachWebTab(windowId, tabId, screenX, screenY);
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 将标签附加到目标窗口
|
||
* @param args {sourceWindowId, tabId, targetWindowId, insertIndex}
|
||
*/
|
||
ipcMain.on('webTabAttach', (event, args) => {
|
||
const {sourceWindowId, tabId, targetWindowId, insertIndex} = args;
|
||
attachWebTab(sourceWindowId, tabId, targetWindowId, insertIndex);
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 内置浏览器 - 获取所有窗口信息(用于跨窗口拖拽检测)
|
||
*/
|
||
ipcMain.handle('webTabGetAllWindows', () => {
|
||
return getAllWebTabWindowsInfo();
|
||
})
|
||
|
||
/**
|
||
* 隐藏窗口(mac、win隐藏,其他关闭)
|
||
*/
|
||
ipcMain.on('windowHidden', (event) => {
|
||
if (['darwin', 'win32'].includes(process.platform)) {
|
||
app.hide();
|
||
} else {
|
||
app.quit();
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 关闭窗口(或关闭 tab,如果发送者是 tab 中的页面)
|
||
*/
|
||
ipcMain.on('windowClose', (event) => {
|
||
const tabId = event.sender.id;
|
||
const windowId = findWindowIdByTabId(tabId);
|
||
if (windowId !== null) {
|
||
// 发送者是 tab 中的页面,只关闭这个 tab
|
||
closeWebTabInWindow(windowId, tabId);
|
||
} else {
|
||
// 发送者是独立窗口,关闭整个窗口
|
||
const win = BrowserWindow.fromWebContents(event.sender);
|
||
win?.close()
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 销毁窗口(或销毁 tab,如果发送者是 tab 中的页面)
|
||
*/
|
||
ipcMain.on('windowDestroy', (event) => {
|
||
const tabId = event.sender.id;
|
||
const windowId = findWindowIdByTabId(tabId);
|
||
if (windowId !== null) {
|
||
// 发送者是 tab 中的页面,只关闭这个 tab
|
||
closeWebTabInWindow(windowId, tabId);
|
||
} else {
|
||
// 发送者是独立窗口,销毁整个窗口
|
||
const win = BrowserWindow.fromWebContents(event.sender);
|
||
win?.destroy()
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 关闭所有子窗口(mode='window' 的窗口)
|
||
*/
|
||
ipcMain.on('childWindowCloseAll', (event) => {
|
||
for (const [, data] of webTabWindows) {
|
||
if (data.mode === 'window' && data.window && !data.window.isDestroyed()) {
|
||
data.window.close();
|
||
}
|
||
}
|
||
mediaWindow?.close()
|
||
electronDown.close()
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 销毁所有子窗口(mode='window' 的窗口)
|
||
*/
|
||
ipcMain.on('childWindowDestroyAll', (event) => {
|
||
for (const [, data] of webTabWindows) {
|
||
if (data.mode === 'window' && data.window && !data.window.isDestroyed()) {
|
||
data.window.destroy();
|
||
}
|
||
}
|
||
mediaWindow?.destroy()
|
||
electronDown.destroy()
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
|
||
/**
|
||
* 设置窗口尺寸
|
||
* @param args {width, height, autoZoom, minWidth, minHeight, maxWidth, maxHeight}
|
||
*/
|
||
ipcMain.on('windowSize', (event, args) => {
|
||
const win = BrowserWindow.fromWebContents(event.sender);
|
||
if (win) {
|
||
if (args.width || args.height) {
|
||
let [w, h] = win.getSize()
|
||
const width = args.width || w
|
||
const height = args.height || h
|
||
win.setSize(width, height, args.animate === true)
|
||
//
|
||
if (args.autoZoom === true) {
|
||
let move = false
|
||
let [x, y] = win.getPosition()
|
||
if (Math.abs(width - w) > 10) {
|
||
move = true
|
||
x -= (width - w) / 2
|
||
}
|
||
if (Math.abs(height - h) > 10) {
|
||
move = true
|
||
y -= (height - h) / 2
|
||
}
|
||
if (move) {
|
||
win.setPosition(Math.max(0, Math.floor(x)), Math.max(0, Math.floor(y)))
|
||
}
|
||
}
|
||
}
|
||
if (args.minWidth || args.minHeight) {
|
||
win.setMinimumSize(args.minWidth || win.getMinimumSize()[0], args.minHeight || win.getMinimumSize()[1])
|
||
}
|
||
if (args.maxWidth || args.maxHeight) {
|
||
win.setMaximumSize(args.maxWidth || win.getMaximumSize()[0], args.maxHeight || win.getMaximumSize()[1])
|
||
}
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 设置窗口最小尺寸
|
||
* @param args {minWidth, minHeight}
|
||
*/
|
||
ipcMain.on('windowMinSize', (event, args) => {
|
||
const win = BrowserWindow.fromWebContents(event.sender);
|
||
if (win) {
|
||
win.setMinimumSize(args.minWidth || win.getMinimumSize()[0], args.minHeight || win.getMinimumSize()[1])
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 设置窗口最大尺寸
|
||
* @param args {maxWidth, maxHeight}
|
||
*/
|
||
ipcMain.on('windowMaxSize', (event, args) => {
|
||
const win = BrowserWindow.fromWebContents(event.sender);
|
||
if (win) {
|
||
win.setMaximumSize(args.maxWidth || win.getMaximumSize()[0], args.maxHeight || win.getMaximumSize()[1])
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 窗口居中
|
||
*/
|
||
ipcMain.on('windowCenter', (event) => {
|
||
const win = BrowserWindow.fromWebContents(event.sender);
|
||
if (win) {
|
||
win.center();
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 窗口最大化或恢复
|
||
*/
|
||
ipcMain.on('windowMax', (event) => {
|
||
const win = BrowserWindow.fromWebContents(event.sender);
|
||
if (win.isMaximized()) {
|
||
win.restore();
|
||
} else {
|
||
win.maximize();
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 给所有窗口广播指令(除了本身)
|
||
* @param args {type, payload}
|
||
*/
|
||
ipcMain.on('broadcastCommand', (event, args) => {
|
||
const channel = args.channel || args.command
|
||
const payload = args.payload || args.data
|
||
BrowserWindow.getAllWindows().forEach(window => {
|
||
if (window.webContents.id !== event.sender.id) {
|
||
window.webContents.send(channel, payload)
|
||
}
|
||
})
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 设置Dock标记(window闪烁、macos标记)
|
||
* @param args
|
||
*/
|
||
ipcMain.on('setDockBadge', (event, args) => {
|
||
if (process.platform === 'win32') {
|
||
// Window flash
|
||
if (!mainWindow.isFocused()) {
|
||
mainWindow.once('focus', () => mainWindow.flashFrame(false))
|
||
mainWindow.flashFrame(true)
|
||
}
|
||
return;
|
||
}
|
||
if (process.platform !== 'darwin') {
|
||
// Mac only
|
||
return;
|
||
}
|
||
let num = args;
|
||
let tray = true;
|
||
if (utils.isJson(args)) {
|
||
num = args.num
|
||
tray = !!args.tray
|
||
}
|
||
let text = typeof num === "string" ? num : (utils.runNum(num) > 0 ? String(num) : "")
|
||
app.dock.setBadge(text)
|
||
if (tray && mainTray) {
|
||
mainTray.setTitle(text)
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* MCP 服务器状态切换
|
||
* @param args
|
||
*/
|
||
ipcMain.on('mcpServerToggle', (event, args) => {
|
||
const { running } = args;
|
||
if (running === 'running') {
|
||
startMCPServer(mainWindow, mcpPort)
|
||
} else {
|
||
stopMCPServer()
|
||
}
|
||
})
|
||
|
||
/**
|
||
* 复制Base64图片
|
||
* @param args
|
||
*/
|
||
ipcMain.on('copyBase64Image', (event, args) => {
|
||
const { base64 } = args;
|
||
if (base64) {
|
||
const img = nativeImage.createFromDataURL(base64)
|
||
clipboard.writeImage(img)
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 复制图片根据坐标
|
||
* @param args
|
||
*/
|
||
ipcMain.on('copyImageAt', (event, args) => {
|
||
try {
|
||
event.sender.copyImageAt(args.x, args.y);
|
||
} catch (e) {
|
||
loger.error('copyImageAt error:', e)
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 保存图片
|
||
* @param args
|
||
*/
|
||
ipcMain.on('saveImageAt', async (event, args) => {
|
||
await electronMenu.saveImageAs(args.url, args.params)
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 绑定截图快捷键
|
||
* @param args
|
||
*/
|
||
ipcMain.on('bindScreenshotKey', (event, args) => {
|
||
const { key } = args;
|
||
if (screenshotKey !== key) {
|
||
if (screenshotKey) {
|
||
globalShortcut.unregister(screenshotKey)
|
||
screenshotKey = null
|
||
}
|
||
if (key) {
|
||
screenshotKey = key
|
||
globalShortcut.register(key, () => {
|
||
screenshotObj.startCapture().then(_ => {
|
||
screenshotObj.view.webContents.executeJavaScript(`if(typeof window.__initializeShortcuts==='undefined'){window.__initializeShortcuts=true;document.addEventListener('keydown',function(e){console.log(e);if(e.keyCode===27){window.screenshots.cancel()}})}`, true).catch(() => {});
|
||
screenshotObj.view.webContents.focus()
|
||
})
|
||
})
|
||
}
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 执行截图
|
||
*/
|
||
ipcMain.on('openScreenshot', (event) => {
|
||
if (screenshotObj) {
|
||
screenshotObj.startCapture().then(_ => {})
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 关闭截图
|
||
*/
|
||
ipcMain.on('closeScreenshot', (event) => {
|
||
if (screenshotObj && screenshotObj.window?.isFocused()) {
|
||
screenshotObj.endCapture().then(_ => {});
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 通知
|
||
*/
|
||
ipcMain.on('openNotification', (event, args) => {
|
||
utils.showNotification(args, mainWindow)
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 保存缓存
|
||
*/
|
||
ipcMain.on('setStore', (event, args) => {
|
||
if (utils.isJson(args)) {
|
||
store.set(args.key, args.value)
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 获取缓存
|
||
*/
|
||
ipcMain.handle('getStore', (event, args) => {
|
||
return store.get(args)
|
||
});
|
||
|
||
/**
|
||
* 清理服务器缓存
|
||
*/
|
||
ipcMain.on('clearServerCache', (event) => {
|
||
utils.clearServerCache();
|
||
event.returnValue = "ok";
|
||
});
|
||
|
||
//================================================================
|
||
// Update
|
||
//================================================================
|
||
|
||
let autoUpdating = 0
|
||
if (autoUpdater) {
|
||
autoUpdater.logger = loger
|
||
autoUpdater.autoDownload = false
|
||
autoUpdater.autoInstallOnAppQuit = true
|
||
autoUpdater.on('update-available', info => {
|
||
mainWindow.webContents.send("updateAvailable", info)
|
||
})
|
||
autoUpdater.on('update-downloaded', info => {
|
||
mainWindow.webContents.send("updateDownloaded", info)
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 检查更新
|
||
*/
|
||
ipcMain.on('updateCheckAndDownload', (event, args) => {
|
||
event.returnValue = "ok"
|
||
if (autoUpdating + 3600 > utils.dayjs().unix()) {
|
||
return // 限制1小时仅执行一次
|
||
}
|
||
if (!autoUpdater) {
|
||
return
|
||
}
|
||
if (args.provider) {
|
||
autoUpdater.setFeedURL(args)
|
||
}
|
||
autoUpdater.checkForUpdates().then(info => {
|
||
if (!info) {
|
||
return
|
||
}
|
||
if (utils.compareVersion(config.version, info.updateInfo.version) >= 0) {
|
||
return
|
||
}
|
||
if (args.apiVersion) {
|
||
if (utils.compareVersion(info.updateInfo.version, args.apiVersion) <= 0) {
|
||
// 客户端版本 <= 接口版本
|
||
autoUpdating = utils.dayjs().unix()
|
||
autoUpdater.downloadUpdate().then(_ => {}).catch(_ => {})
|
||
}
|
||
} else {
|
||
autoUpdating = utils.dayjs().unix()
|
||
autoUpdater.downloadUpdate().then(_ => {}).catch(_ => {})
|
||
}
|
||
})
|
||
})
|
||
|
||
/**
|
||
* 将主窗口激活到顶层
|
||
*/
|
||
ipcMain.on('mainWindowTop', (event) => {
|
||
mainWindow.moveTop()
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 将主窗口激活
|
||
*/
|
||
ipcMain.on('mainWindowActive', (event) => {
|
||
if (!mainWindow.isVisible()) {
|
||
mainWindow.show()
|
||
}
|
||
mainWindow.focus()
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 退出并安装更新
|
||
*/
|
||
ipcMain.on('updateQuitAndInstall', (event, args) => {
|
||
if (!utils.isJson(args)) {
|
||
args = {}
|
||
}
|
||
event.returnValue = "ok"
|
||
|
||
// 关闭所有子窗口
|
||
willQuitApp = true
|
||
for (const [, data] of webTabWindows) {
|
||
if (data.mode === 'window' && data.window && !data.window.isDestroyed()) {
|
||
data.window.destroy();
|
||
}
|
||
}
|
||
mediaWindow?.destroy()
|
||
electronDown.destroy()
|
||
|
||
// 启动更新子窗口
|
||
createUpdaterWindow(args.updateTitle)
|
||
|
||
// 退出并安装更新
|
||
setTimeout(_ => {
|
||
mainWindow.hide()
|
||
autoUpdater?.quitAndInstall(true, true)
|
||
}, 600)
|
||
})
|
||
|
||
//================================================================
|
||
//================================================================
|
||
//================================================================
|
||
|
||
onExport()
|
||
onRenderer(mainWindow)
|