dootask/electron/electron.js

2989 lines
89 KiB
JavaScript
Vendored
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Node.js 核心模块
const fs = require('fs')
const os = require("os");
const path = require('path')
const spawn = require("child_process").spawn;
const fsProm = require('fs/promises');
const crc = require('crc');
const zlib = require('zlib');
// Web 服务相关
const express = require('express')
const axios = require('axios');
// Electron 核心模块
const {
app,
ipcMain,
dialog,
clipboard,
nativeImage,
shell,
globalShortcut,
nativeTheme,
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;
// PDF 处理
const PDFDocument = require('pdf-lib').PDFDocument;
// 本地模块和配置
const utils = require('./lib/utils');
const config = require('./package.json');
const electronDown = require("./electron-down");
const electronMenu = require("./electron-menu");
const { startMCPServer, stopMCPServer } = require("./lib/mcp");
// 实例初始化
const userConf = new electronConf()
const store = new Store();
// 平台检测常量
const isMac = process.platform === 'darwin'
const isWin = process.platform === 'win32'
// URL 和调用验证正则
const allowedUrls = /^(?:https?|mailto|tel|callto):/i;
const allowedCalls = /^(?:mailto|tel|callto):/i;
// 路径和缓存配置
const cacheDir = path.join(os.tmpdir(), 'dootask-cache')
const updaterLockFile = path.join(cacheDir, '.dootask_updater.lock');
// 应用状态标志
let enableStoreBkp = true,
dialogOpen = false,
enablePlugins = false,
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,
preloadWindow = null,
mediaWindow = null,
webTabWindow = null;
// 窗口数组和状态
let childWindow = [],
webTabView = [];
// 窗口配置和状态
let mediaType = null,
webTabHeight = 40,
webTabClosedByShortcut = false;
// 开发模式路径
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)) {
openExternal(url).catch(() => {})
} else {
utils.onBeforeOpenWindow(mainWindow.webContents, url).then(() => {
openExternal(url).catch(() => {})
})
}
return {action: 'deny'}
})
// 设置右键菜单
electronMenu.webContentsMenu(mainWindow.webContents)
// 加载地址
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);
}
}
/**
* 创建预窗口
*/
function preCreateChildWindow() {
if (preloadWindow) {
return;
}
const browser = new BrowserWindow({
width: 360,
height: 360,
minWidth: 360,
minHeight: 360,
center: true,
show: false,
autoHideMenuBar: true,
backgroundColor: utils.getDefaultBackgroundColor(),
webPreferences: {
preload: path.join(__dirname, 'electron-preload.js'),
webSecurity: true,
nodeIntegration: true,
contextIsolation: true,
}
});
// 关闭事件
browser.addListener('closed', () => {
preloadWindow = null;
})
// 设置 UA
const originalUA = browser.webContents.session.getUserAgent() || browser.webContents.getUserAgent()
browser.webContents.setUserAgent(originalUA + " SubTaskWindow/" + process.platform + "/" + os.arch() + "/1.0");
utils.loadUrl(browser, serverUrl, '/preload')
preloadWindow = browser;
}
/**
* 创建子窗口
* @param args {name, path, hash, force, userAgent, config, webPreferences}
* - config: {title, titleFixed, ...BrowserWindowConstructorOptions}
*/
function createChildWindow(args) {
if (!args) {
return;
}
if (!utils.isJson(args)) {
args = {path: args, config: {}}
}
const name = args.name || "auto_" + utils.randomString(6);
const wind = childWindow.find(item => item.name == name);
let browser = wind ? wind.browser : null;
let isPreload = false;
if (browser) {
browser.focus();
if (args.force === false) {
return;
}
} else {
const config = args.config || {};
const webPreferences = args.webPreferences || {};
const options = Object.assign({
width: 1280,
height: 800,
minWidth: 360,
minHeight: 360,
center: true,
show: false,
autoHideMenuBar: true,
backgroundColor: utils.getDefaultBackgroundColor(),
webPreferences: Object.assign({
preload: path.join(__dirname, 'electron-preload.js'),
webSecurity: true,
nodeIntegration: true,
contextIsolation: true,
}, webPreferences),
}, config)
options.width = utils.normalizeSize(options.width, 1280)
options.height = utils.normalizeSize(options.height, 800)
options.minWidth = utils.normalizeSize(options.minWidth, 360)
options.minHeight = utils.normalizeSize(options.minHeight, 360)
if (!options.webPreferences.contextIsolation) {
delete options.webPreferences.preload;
}
if (options.parent) {
options.parent = mainWindow
}
if (preloadWindow && Object.keys(webPreferences).length === 0) {
// 使用预加载窗口
browser = preloadWindow;
preloadWindow = null;
isPreload = true;
options.title && browser.setTitle(options.title);
options.parent && browser.setParentWindow(options.parent);
browser.setSize(options.width, options.height);
browser.setMinimumSize(options.minWidth, options.minHeight);
browser.center();
browser.setAutoHideMenuBar(options.autoHideMenuBar);
browser.removeAllListeners("closed");
setTimeout(() => onShowWindow(browser), 300)
process.nextTick(() => setTimeout(() => onShowWindow(browser), 50));
} else {
// 创建新窗口
browser = new BrowserWindow(options)
loger.info("create new window")
}
browser.on('page-title-updated', (event, title) => {
if (title == "index.html" || options.titleFixed === true) {
event.preventDefault()
}
})
browser.on('focus', () => {
browser.webContents.send("browserWindowFocus", {})
})
browser.on('blur', () => {
browser.webContents.send("browserWindowBlur", {})
})
browser.on('close', event => {
if (!willQuitApp) {
utils.onBeforeUnload(event, browser).then(() => {
browser.hide()
setTimeout(() => {
browser.destroy()
}, 100)
})
}
})
browser.on('closed', () => {
const index = childWindow.findIndex(item => item.name == name);
if (index > -1) {
childWindow.splice(index, 1)
}
})
browser.once('ready-to-show', () => {
onShowWindow(browser);
})
browser.webContents.once('dom-ready', () => {
onShowWindow(browser);
})
childWindow.push({ name, browser })
}
// 设置 UA
const originalUA = browser.webContents.session.getUserAgent() || browser.webContents.getUserAgent()
browser.webContents.setUserAgent(originalUA + " SubTaskWindow/" + process.platform + "/" + os.arch() + "/1.0" + (args.userAgent ? (" " + args.userAgent) : ""));
// 新窗口处理
browser.webContents.setWindowOpenHandler(({url}) => {
if (allowedCalls.test(url)) {
openExternal(url).catch(() => {})
} else {
utils.onBeforeOpenWindow(browser.webContents, url).then(() => {
openExternal(url).catch(() => {})
})
}
return {action: 'deny'}
})
// 设置右键菜单
electronMenu.webContentsMenu(browser.webContents)
// 加载地址
const hash = `${args.hash || args.path}`;
if (/^https?:/i.test(hash)) {
browser.loadURL(hash)
.then(_ => { })
.catch(_ => { })
} else if (isPreload) {
browser
.webContents
.executeJavaScript(`if(typeof window.__initializeApp === 'function'){window.__initializeApp('${hash}')}else{throw new Error('no function')}`, true)
.catch(() => {
utils.loadUrl(browser, serverUrl, hash)
});
} else {
utils.loadUrl(browser, serverUrl, hash)
}
// 预创建下一个窗口
preCreateChildWindow();
}
/**
* 更新子窗口
* @param browser
* @param args
*/
function updateChildWindow(browser, args) {
if (!args) {
return;
}
if (!utils.isJson(args)) {
args = {path: args, name: null}
}
const hash = args.hash || args.path;
if (hash) {
utils.loadUrl(browser, serverUrl, hash)
}
if (args.name) {
const er = childWindow.find(item => item.browser == browser);
if (er) {
er.name = args.name;
}
}
}
/**
* 创建媒体浏览器窗口
* @param args
* @param type
*/
function createMediaWindow(args, type = 'image') {
if (mediaWindow === null) {
mediaWindow = new BrowserWindow({
width: args.width || 970,
height: 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, ?}
*/
function createWebTabWindow(args) {
if (!args) {
return;
}
if (!utils.isJson(args)) {
args = {url: args}
}
// 创建父级窗口
if (!webTabWindow) {
const titleBarOverlay = {
height: webTabHeight
}
if (nativeTheme.shouldUseDarkColors) {
titleBarOverlay.color = '#3B3B3D'
titleBarOverlay.symbolColor = '#C5C5C5'
}
webTabWindow = new BrowserWindow(Object.assign({
x: mainWindow.getBounds().x + webTabHeight,
y: mainWindow.getBounds().y + webTabHeight,
width: 1280,
height: 800,
minWidth: 360,
minHeight: 360,
center: true,
show: false,
autoHideMenuBar: true,
titleBarStyle: 'hidden',
titleBarOverlay,
backgroundColor: nativeTheme.shouldUseDarkColors ? '#575757' : '#FFFFFF',
webPreferences: {
preload: path.join(__dirname, 'electron-preload.js'),
webSecurity: true,
nodeIntegration: true,
contextIsolation: true,
},
}, userConf.get('webTabWindow') || {}))
const originalClose = webTabWindow.close;
webTabWindow.close = function() {
webTabClosedByShortcut = true;
return originalClose.apply(this, arguments);
};
webTabWindow.on('resize', () => {
resizeWebTab(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) {
webTabClosedByShortcut = false
if (!willQuitApp) {
closeWebTab(0)
event.preventDefault()
return
}
}
userConf.set('webTabWindow', webTabWindow.getBounds())
})
webTabWindow.on('closed', () => {
webTabView.forEach(({view}) => {
try {
view.webContents.close()
} catch (e) {
//
}
})
webTabView = []
webTabWindow = null
})
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') {
reloadWebTab(0)
event.preventDefault()
} else if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'w') {
webTabClosedByShortcut = true
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
devToolsWebTab(0)
}
})
webTabWindow.loadFile('./render/tabs/index.html', {}).then(_ => { }).catch(_ => { })
}
if (webTabWindow.isMinimized()) {
webTabWindow.restore()
}
webTabWindow.focus();
webTabWindow.show();
// 创建 tab 子窗口
const viewOptions = args.config || {}
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 (nativeTheme.shouldUseDarkColors) {
browserView.setBackgroundColor('#575757')
} else {
browserView.setBackgroundColor('#FFFFFF')
}
browserView.setBounds({
x: 0,
y: webTabHeight,
width: webTabWindow.getContentBounds().width || 1280,
height: (webTabWindow.getContentBounds().height || 800) - webTabHeight,
})
browserView.webContents.on('destroyed', () => {
closeWebTab(browserView.webContents.id)
})
browserView.webContents.setWindowOpenHandler(({url}) => {
if (allowedCalls.test(url)) {
openExternal(url).catch(() => {})
} else {
createWebTabWindow({url})
}
return {action: 'deny'}
})
browserView.webContents.on('page-title-updated', (event, title) => {
utils.onDispatchEvent(webTabWindow.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
}
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'title',
id: browserView.webContents.id,
title: errorDescription,
url: browserView.webContents.getURL(),
}).then(_ => { })
})
browserView.webContents.on('page-favicon-updated', (event, favicons) => {
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'favicon',
id: browserView.webContents.id,
favicons
}).then(_ => { })
})
browserView.webContents.on('did-start-loading', _ => {
webTabView.forEach(({id: vid, view}) => {
view.setVisible(vid === browserView.webContents.id)
})
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'start-loading',
id: browserView.webContents.id,
}).then(_ => { })
})
browserView.webContents.on('did-stop-loading', _ => {
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'stop-loading',
id: browserView.webContents.id,
}).then(_ => { })
// 加载完成暗黑模式下把窗口背景色改成白色,避免透明网站背景色穿透
if (nativeTheme.shouldUseDarkColors) {
browserView.setBackgroundColor('#FFFFFF')
}
})
browserView.webContents.on('before-input-event', (event, input) => {
if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'r') {
browserView.webContents.reload()
event.preventDefault()
} else if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'w') {
webTabClosedByShortcut = true
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
browserView.webContents.toggleDevTools()
}
})
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)
browserView.webContents.loadURL(args.url).then(_ => { }).catch(_ => { })
browserView.setVisible(true)
webTabWindow.contentView.addChildView(browserView)
webTabView.push({
id: browserView.webContents.id,
view: browserView
})
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'create',
id: browserView.webContents.id,
url: args.url,
}).then(_ => { })
activateWebTab(browserView.webContents.id)
}
/**
* 获取当前内置浏览器标签
* @returns {Electron.WebContentsView|undefined}
*/
function currentWebTab() {
// 第一:使用当前可见的标签
try {
const item = webTabView.find(({view}) => view?.getVisible && view.getVisible())
if (item) {
return item
}
} catch (e) {}
// 第二:使用当前聚焦的 webContents
try {
const focused = 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
}
/**
* 重新加载内置浏览器标签
* @param id
*/
function reloadWebTab(id) {
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
if (!item) {
return
}
item.view.webContents.reload()
}
/**
* 内置浏览器标签打开开发者工具
* @param id
*/
function devToolsWebTab(id) {
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
if (!item) {
return
}
item.view.webContents.toggleDevTools()
}
/**
* 调整内置浏览器标签尺寸
* @param id
*/
function resizeWebTab(id) {
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
if (!item) {
return
}
item.view.setBounds({
x: 0,
y: webTabHeight,
width: webTabWindow.getContentBounds().width || 1280,
height: (webTabWindow.getContentBounds().height || 800) - webTabHeight,
})
}
/**
* 切换内置浏览器标签
* @param id
*/
function activateWebTab(id) {
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
if (!item) {
return
}
webTabView.forEach(({id: vid, view}) => {
view.setVisible(vid === item.id)
})
resizeWebTab(item.id)
item.view.webContents.focus()
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'switch',
id: item.id,
}).then(_ => { })
}
/**
* 关闭内置浏览器标签
* @param id
*/
function closeWebTab(id) {
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
if (!item) {
return
}
if (webTabView.length === 1) {
webTabWindow.hide()
}
webTabWindow.contentView.removeChildView(item.view)
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 {
activateWebTab(0)
}
}
/**
* 监听主题变化
*/
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);
preloadWindow?.setBackgroundColor(backgroundColor);
mediaWindow?.setBackgroundColor(backgroundColor);
childWindow.some(({browser}) => browser.setBackgroundColor(backgroundColor))
webTabWindow?.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()
// 预创建子窗口
preCreateChildWindow()
// 监听主题变化
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();
})
/**
* 创建路由窗口
* @param args {path, ?}
*/
ipcMain.on('openChildWindow', (event, args) => {
createChildWindow(args)
event.returnValue = "ok"
})
/**
* 显示预加载窗口(用于调试)
*/
ipcMain.on('showPreloadWindow', (event) => {
if (preloadWindow) {
onShowWindow(preloadWindow)
}
event.returnValue = "ok"
})
/**
* 更新路由窗口
* @param args {?name, ?path} // name: 不是要更改的窗口名,是要把窗口名改成什么, path: 地址
*/
ipcMain.on('updateChildWindow', (event, args) => {
const browser = BrowserWindow.fromWebContents(event.sender);
updateChildWindow(browser, args)
event.returnValue = "ok"
})
/**
* 获取路由窗口信息
*/
ipcMain.handle('getChildWindow', (event, args) => {
let child;
if (!args) {
const browser = BrowserWindow.fromWebContents(event.sender);
child = childWindow.find(({browser: win}) => win === browser)
} else {
child = childWindow.find(({name}) => name === args)
}
if (child) {
return {
name: child.name,
id: child.browser.webContents.id,
url: child.browser.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, ?}
*/
ipcMain.on('openWebTabWindow', (event, args) => {
createWebTabWindow(args)
event.returnValue = "ok"
})
/**
* 内置浏览器 - 激活标签
* @param id
*/
ipcMain.on('webTabActivate', (event, id) => {
activateWebTab(id)
event.returnValue = "ok"
})
/**
* 内置浏览器 - 关闭标签
* @param id
*/
ipcMain.on('webTabClose', (event, id) => {
closeWebTab(id)
event.returnValue = "ok"
})
/**
* 内置浏览器 - 在外部浏览器打开
*/
ipcMain.on('webTabExternal', (event) => {
const item = currentWebTab()
if (!item) {
return
}
openExternal(item.view.webContents.getURL()).catch(() => {})
event.returnValue = "ok"
})
/**
* 内置浏览器 - 打开开发者工具
*/
ipcMain.on('webTabOpenDevTools', (event) => {
const item = currentWebTab()
if (!item) {
return
}
item.view.webContents.openDevTools()
event.returnValue = "ok"
})
/**
* 内置浏览器 - 销毁所有标签及窗口
*/
ipcMain.on('webTabDestroyAll', (event) => {
if (webTabWindow) {
webTabWindow.destroy()
}
event.returnValue = "ok"
})
/**
* 内置浏览器 - 后退
*/
ipcMain.on('webTabGoBack', (event) => {
const item = currentWebTab()
if (!item) {
return
}
if (item.view.webContents.canGoBack()) {
item.view.webContents.goBack()
// 导航后更新状态
setTimeout(() => {
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'navigation-state',
id: item.id,
canGoBack: item.view.webContents.canGoBack(),
canGoForward: item.view.webContents.canGoForward()
}).then(_ => { })
}, 100)
}
event.returnValue = "ok"
})
/**
* 内置浏览器 - 前进
*/
ipcMain.on('webTabGoForward', (event) => {
const item = currentWebTab()
if (!item) {
return
}
if (item.view.webContents.canGoForward()) {
item.view.webContents.goForward()
// 导航后更新状态
setTimeout(() => {
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'navigation-state',
id: item.id,
canGoBack: item.view.webContents.canGoBack(),
canGoForward: item.view.webContents.canGoForward()
}).then(_ => { })
}, 100)
}
event.returnValue = "ok"
})
/**
* 内置浏览器 - 刷新
*/
ipcMain.on('webTabReload', (event) => {
const item = currentWebTab()
if (!item) {
return
}
item.view.webContents.reload()
// 刷新完成后会触发 did-stop-loading 事件,在那里会更新导航状态
event.returnValue = "ok"
})
/**
* 内置浏览器 - 停止加载
*/
ipcMain.on('webTabStop', (event) => {
const item = currentWebTab()
if (!item) {
return
}
item.view.webContents.stop()
event.returnValue = "ok"
})
/**
* 内置浏览器 - 获取导航状态
*/
ipcMain.on('webTabGetNavigationState', (event) => {
const item = currentWebTab()
if (!item) {
return
}
const canGoBack = item.view.webContents.canGoBack()
const canGoForward = item.view.webContents.canGoForward()
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'navigation-state',
id: item.id,
canGoBack,
canGoForward
}).then(_ => { })
event.returnValue = "ok"
})
/**
* 隐藏窗口mac、win隐藏其他关闭
*/
ipcMain.on('windowHidden', (event) => {
if (['darwin', 'win32'].includes(process.platform)) {
app.hide();
} else {
app.quit();
}
event.returnValue = "ok"
})
/**
* 关闭窗口
*/
ipcMain.on('windowClose', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
win.close()
event.returnValue = "ok"
})
/**
* 销毁窗口
*/
ipcMain.on('windowDestroy', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
win.destroy()
event.returnValue = "ok"
})
/**
* 关闭所有子窗口
*/
ipcMain.on('childWindowCloseAll', (event) => {
childWindow.some(({browser}) => {
browser && browser.close()
})
preloadWindow?.close()
mediaWindow?.close()
electronDown.close()
event.returnValue = "ok"
})
/**
* 销毁所有子窗口
*/
ipcMain.on('childWindowDestroyAll', (event) => {
childWindow.some(({browser}) => {
browser && browser.destroy()
})
preloadWindow?.destroy()
mediaWindow?.destroy()
electronDown.destroy()
event.returnValue = "ok"
})
/**
* 刷新预加载窗口(用于更换语言和主题时触发)
*/
ipcMain.on('reloadPreloadWindow', (event) => {
if (preloadWindow) {
preloadWindow.webContents.reload()
}
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
childWindow.some(({browser}) => {
browser && browser.destroy()
})
preloadWindow?.destroy()
mediaWindow?.destroy()
electronDown.destroy()
// 启动更新子窗口
createUpdaterWindow(args.updateTitle)
// 退出并安装更新
setTimeout(_ => {
mainWindow.hide()
autoUpdater?.quitAndInstall(true, true)
}, 600)
})
//================================================================
// Pdf export
//================================================================
const MICRON_TO_PIXEL = 264.58 //264.58 micron = 1 pixel
const PIXELS_PER_INCH = 100.117 // Usually it is 100 pixels per inch but this give better results
const PNG_CHUNK_IDAT = 1229209940;
const LARGE_IMAGE_AREA = 30000000;
//NOTE: Key length must not be longer than 79 bytes (not checked)
function writePngWithText(origBuff, key, text, compressed, base64encoded) {
let isDpi = key == 'dpi';
let inOffset = 0;
let outOffset = 0;
let data = text;
let dataLen = isDpi ? 9 : key.length + data.length + 1; //we add 1 zeros with non-compressed data, for pHYs it's 2 of 4-byte-int + 1 byte
//prepare compressed data to get its size
if (compressed) {
data = zlib.deflateRawSync(encodeURIComponent(text));
dataLen = key.length + data.length + 2; //we add 2 zeros with compressed data
}
let outBuff = Buffer.allocUnsafe(origBuff.length + dataLen + 4); //4 is the header size "zTXt", "tEXt" or "pHYs"
try {
let magic1 = origBuff.readUInt32BE(inOffset);
inOffset += 4;
let magic2 = origBuff.readUInt32BE(inOffset);
inOffset += 4;
if (magic1 != 0x89504e47 && magic2 != 0x0d0a1a0a) {
throw new Error("PNGImageDecoder0");
}
outBuff.writeUInt32BE(magic1, outOffset);
outOffset += 4;
outBuff.writeUInt32BE(magic2, outOffset);
outOffset += 4;
} catch (e) {
loger.error(e.message, {stack: e.stack});
throw new Error("PNGImageDecoder1");
}
try {
while (inOffset < origBuff.length) {
let length = origBuff.readInt32BE(inOffset);
inOffset += 4;
let type = origBuff.readInt32BE(inOffset)
inOffset += 4;
if (type == PNG_CHUNK_IDAT) {
// Insert zTXt chunk before IDAT chunk
outBuff.writeInt32BE(dataLen, outOffset);
outOffset += 4;
let typeSignature = isDpi ? 'pHYs' : (compressed ? "zTXt" : "tEXt");
outBuff.write(typeSignature, outOffset);
outOffset += 4;
if (isDpi) {
let dpm = Math.round(parseInt(text) / 0.0254) || 3937; //One inch is equal to exactly 0.0254 meters. 3937 is 100dpi
outBuff.writeInt32BE(dpm, outOffset);
outBuff.writeInt32BE(dpm, outOffset + 4);
outBuff.writeInt8(1, outOffset + 8);
outOffset += 9;
data = Buffer.allocUnsafe(9);
data.writeInt32BE(dpm, 0);
data.writeInt32BE(dpm, 4);
data.writeInt8(1, 8);
} else {
outBuff.write(key, outOffset);
outOffset += key.length;
outBuff.writeInt8(0, outOffset);
outOffset++;
if (compressed) {
outBuff.writeInt8(0, outOffset);
outOffset++;
data.copy(outBuff, outOffset);
} else {
outBuff.write(data, outOffset);
}
outOffset += data.length;
}
let crcVal = 0xffffffff;
crcVal = crc.crcjam(typeSignature, crcVal);
crcVal = crc.crcjam(data, crcVal);
// CRC
outBuff.writeInt32BE(crcVal ^ 0xffffffff, outOffset);
outOffset += 4;
// Writes the IDAT chunk after the zTXt
outBuff.writeInt32BE(length, outOffset);
outOffset += 4;
outBuff.writeInt32BE(type, outOffset);
outOffset += 4;
origBuff.copy(outBuff, outOffset, inOffset);
// Encodes the buffer using base64 if requested
return base64encoded ? outBuff.toString('base64') : outBuff;
}
outBuff.writeInt32BE(length, outOffset);
outOffset += 4;
outBuff.writeInt32BE(type, outOffset);
outOffset += 4;
origBuff.copy(outBuff, outOffset, inOffset, inOffset + length + 4);// +4 to move past the crc
inOffset += length + 4;
outOffset += length + 4;
}
} catch (e) {
loger.error(e.message, {stack: e.stack});
throw e;
}
}
//TODO Create a lightweight html file similar to export3.html for exporting to vsdx
function exportVsdx(event, args, directFinalize) {
let win = new BrowserWindow({
width: 1280,
height: 800,
show: false,
webPreferences: {
preload: path.join(__dirname, 'electron-preload.js'),
webSecurity: true,
nodeIntegration: true,
contextIsolation: true,
},
})
let loadEvtCount = 0;
function loadFinished() {
loadEvtCount++;
if (loadEvtCount == 2) {
win.webContents.send('export-vsdx', args);
ipcMain.once('export-vsdx-finished', (evt, data) => {
let hasError = false;
if (data == null) {
hasError = true;
}
//Set finalize here since it is call in the reply below
function finalize() {
win.destroy();
}
if (directFinalize === true) {
event.finalize = finalize;
} else {
//Destroy the window after response being received by caller
ipcMain.once('export-finalize', finalize);
}
if (hasError) {
event.reply('export-error');
} else {
event.reply('export-success', data);
}
});
}
}
//Order of these two events is not guaranteed, so wait for them async.
//TOOD There is still a chance we catch another window 'app-load-finished' if user created multiple windows quickly
ipcMain.once('app-load-finished', loadFinished);
win.webContents.on('did-finish-load', loadFinished);
}
async function mergePdfs(pdfFiles, xml) {
//Pass throgh single files
if (pdfFiles.length == 1 && xml == null) {
return pdfFiles[0];
}
try {
const pdfDoc = await PDFDocument.create();
pdfDoc.setCreator(config.name);
if (xml != null) {
//Embed diagram XML as file attachment
await pdfDoc.attach(Buffer.from(xml).toString('base64'), config.name + '.xml', {
mimeType: 'application/vnd.jgraph.mxfile',
description: config.name + ' Content'
});
}
for (let i = 0; i < pdfFiles.length; i++) {
const pdfFile = await PDFDocument.load(pdfFiles[i].buffer);
const pages = await pdfDoc.copyPages(pdfFile, pdfFile.getPageIndices());
pages.forEach(p => pdfDoc.addPage(p));
}
const pdfBytes = await pdfDoc.save();
return Buffer.from(pdfBytes);
} catch (e) {
throw new Error('Error during PDF combination: ' + e.message);
}
}
//TODO Use canvas to export images if math is not used to speedup export (no capturePage). Requires change to export3.html also
function exportDiagram(event, args, directFinalize) {
if (args.format == 'vsdx') {
exportVsdx(event, args, directFinalize);
return;
}
let browser = null;
try {
browser = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'electron-preload.js'),
backgroundThrottling: false,
contextIsolation: true,
disableBlinkFeatures: 'Auxclick' // Is this needed?
},
show: false,
frame: false,
enableLargerThanScreen: true,
transparent: args.format == 'png' && (args.bg == null || args.bg == 'none'),
});
if (serverUrl) {
browser.loadURL(serverUrl + 'drawio/webapp/export3.html').then(_ => { }).catch(_ => { })
} else {
browser.loadFile('./public/drawio/webapp/export3.html').then(_ => { }).catch(_ => { })
}
const contents = browser.webContents;
let pageByPage = (args.format == 'pdf' && !args.print), from, to, pdfs;
if (pageByPage) {
from = args.allPages ? 0 : parseInt(args.from || 0);
to = args.allPages ? 1000 : parseInt(args.to || 1000) + 1; //The 'to' will be corrected later
pdfs = [];
args.from = from;
args.to = from;
args.allPages = false;
}
contents.on('did-finish-load', function () {
//Set finalize here since it is call in the reply below
function finalize() {
browser.destroy();
}
if (directFinalize === true) {
event.finalize = finalize;
} else {
//Destroy the window after response being received by caller
ipcMain.once('export-finalize', finalize);
}
function renderingFinishHandler(evt, renderInfo) {
if (renderInfo == null) {
event.reply('export-error');
return;
}
let pageCount = renderInfo.pageCount, bounds = null;
//For some reason, Electron 9 doesn't send this object as is without stringifying. Usually when variable is external to function own scope
try {
bounds = JSON.parse(renderInfo.bounds);
} catch (e) {
bounds = null;
}
let pdfOptions = {pageSize: 'A4'};
let hasError = false;
if (bounds == null || bounds.width < 5 || bounds.height < 5) //very small page size never return from printToPDF
{
//A workaround to detect errors in the input file or being empty file
hasError = true;
} else {
pdfOptions = {
printBackground: true,
pageSize: {
width: bounds.width / PIXELS_PER_INCH,
height: (bounds.height + 2) / PIXELS_PER_INCH //the extra 2 pixels to prevent adding an extra empty page
},
margins: {
top: 0,
bottom: 0,
left: 0,
right: 0
} // no margin
}
}
let base64encoded = args.base64 == '1';
if (hasError) {
event.reply('export-error');
} else if (args.format == 'png' || args.format == 'jpg' || args.format == 'jpeg') {
//Adds an extra pixel to prevent scrollbars from showing
let newBounds = {
width: Math.ceil(bounds.width + bounds.x) + 1,
height: Math.ceil(bounds.height + bounds.y) + 1
};
browser.setBounds(newBounds);
//TODO The browser takes sometime to show the graph (also after resize it takes some time to render)
// 1 sec is most probably enough (for small images, 5 for large ones) BUT not a stable solution
setTimeout(function () {
browser.capturePage().then(function (img) {
//Image is double the given bounds, so resize is needed!
let tScale = 1;
//If user defined width and/or height, enforce it precisely here. Height override width
if (args.h) {
tScale = args.h / newBounds.height;
} else if (args.w) {
tScale = args.w / newBounds.width;
}
newBounds.width *= tScale;
newBounds.height *= tScale;
img = img.resize(newBounds);
let data = args.format == 'png' ? img.toPNG() : img.toJPEG(args.jpegQuality || 90);
if (args.dpi != null && args.format == 'png') {
data = writePngWithText(data, 'dpi', args.dpi);
}
if (args.embedXml == "1" && args.format == 'png') {
data = writePngWithText(data, "mxGraphModel", args.xml, true,
base64encoded);
} else {
if (base64encoded) {
data = data.toString('base64');
}
}
event.reply('export-success', data);
});
}, bounds.width * bounds.height < LARGE_IMAGE_AREA ? 1000 : 5000);
} else if (args.format == 'pdf') {
if (args.print) {
pdfOptions = {
scaleFactor: args.pageScale,
printBackground: true,
pageSize: {
width: args.pageWidth * MICRON_TO_PIXEL,
//This height adjustment fixes the output. TODO Test more cases
height: (args.pageHeight * 1.025) * MICRON_TO_PIXEL
},
marginsType: 1 // no margin
};
contents.print(pdfOptions, (success, errorType) => {
//Consider all as success
event.reply('export-success', {});
});
} else {
contents.printToPDF(pdfOptions).then(async (data) => {
pdfs.push(data);
to = to > pageCount ? pageCount : to;
from++;
if (from < to) {
args.from = from;
args.to = from;
ipcMain.once('render-finished', renderingFinishHandler);
contents.send('render', args);
} else {
data = await mergePdfs(pdfs, args.embedXml == '1' ? args.xml : null);
event.reply('export-success', data);
}
})
.catch((error) => {
event.reply('export-error', error);
});
}
} else if (args.format == 'svg') {
contents.send('get-svg-data');
ipcMain.once('svg-data', (evt, data) => {
event.reply('export-success', data);
});
} else {
event.reply('export-error', 'Error: Unsupported format');
}
}
ipcMain.once('render-finished', renderingFinishHandler);
if (args.format == 'xml') {
ipcMain.once('xml-data', (evt, data) => {
event.reply('export-success', data);
});
ipcMain.once('xml-data-error', () => {
event.reply('export-error');
});
}
args.border = args.border || 0;
args.scale = args.scale || 1;
contents.send('render', args);
});
} catch (e) {
if (browser != null) {
browser.destroy();
}
event.reply('export-error', e);
console.log('export-error', e);
}
}
ipcMain.on('export', exportDiagram);
//================================================================
// Renderer Helper functions
//================================================================
const {O_SYNC, O_CREAT, O_WRONLY, O_TRUNC, O_RDONLY} = fs.constants;
const DRAFT_PREFEX = '.$';
const OLD_DRAFT_PREFEX = '~$';
const DRAFT_EXT = '.dtmp';
const BKP_PREFEX = '.$';
const OLD_BKP_PREFEX = '~$';
const BKP_EXT = '.bkp';
/**
* Checks the file content type
* Confirm content is xml, pdf, png, jpg, svg, vsdx ...
*/
function checkFileContent(body, enc) {
if (body != null) {
let head, headBinay;
if (typeof body === 'string') {
if (enc == 'base64') {
headBinay = Buffer.from(body.substring(0, 22), 'base64');
head = headBinay.toString();
} else {
head = body.substring(0, 16);
headBinay = Buffer.from(head);
}
} else {
head = new TextDecoder("utf-8").decode(body.subarray(0, 16));
headBinay = body;
}
let c1 = head[0],
c2 = head[1],
c3 = head[2],
c4 = head[3],
c5 = head[4],
c6 = head[5],
c7 = head[6],
c8 = head[7],
c9 = head[8],
c10 = head[9],
c11 = head[10],
c12 = head[11],
c13 = head[12],
c14 = head[13],
c15 = head[14],
c16 = head[15];
let cc1 = headBinay[0],
cc2 = headBinay[1],
cc3 = headBinay[2],
cc4 = headBinay[3],
cc5 = headBinay[4],
cc6 = headBinay[5],
cc7 = headBinay[6],
cc8 = headBinay[7],
cc9 = headBinay[8],
cc10 = headBinay[9],
cc11 = headBinay[10],
cc12 = headBinay[11],
cc13 = headBinay[12],
cc14 = headBinay[13],
cc15 = headBinay[14],
cc16 = headBinay[15];
if (c1 == '<') {
// text/html
if (c2 == '!'
|| ((c2 == 'h'
&& (c3 == 't' && c4 == 'm' && c5 == 'l'
|| c3 == 'e' && c4 == 'a' && c5 == 'd')
|| (c2 == 'b' && c3 == 'o' && c4 == 'd'
&& c5 == 'y')))
|| ((c2 == 'H'
&& (c3 == 'T' && c4 == 'M' && c5 == 'L'
|| c3 == 'E' && c4 == 'A' && c5 == 'D')
|| (c2 == 'B' && c3 == 'O' && c4 == 'D'
&& c5 == 'Y')))) {
return true;
}
// application/xml
if (c2 == '?' && c3 == 'x' && c4 == 'm' && c5 == 'l'
&& c6 == ' ') {
return true;
}
// application/svg+xml
if (c2 == 's' && c3 == 'v' && c4 == 'g' && c5 == ' ') {
return true;
}
}
// big and little (identical) endian UTF-8 encodings, with BOM
// application/xml
if (cc1 == 0xef && cc2 == 0xbb && cc3 == 0xbf) {
if (c4 == '<' && c5 == '?' && c6 == 'x') {
return true;
}
}
// big and little endian UTF-16 encodings, with byte order mark
// application/xml
if (cc1 == 0xfe && cc2 == 0xff) {
if (cc3 == 0 && c4 == '<' && cc5 == 0 && c6 == '?' && cc7 == 0
&& c8 == 'x') {
return true;
}
}
// application/xml
if (cc1 == 0xff && cc2 == 0xfe) {
if (c3 == '<' && cc4 == 0 && c5 == '?' && cc6 == 0 && c7 == 'x'
&& cc8 == 0) {
return true;
}
}
// big and little endian UTF-32 encodings, with BOM
// application/xml
if (cc1 == 0x00 && cc2 == 0x00 && cc3 == 0xfe && cc4 == 0xff) {
if (cc5 == 0 && cc6 == 0 && cc7 == 0 && c8 == '<' && cc9 == 0
&& cc10 == 0 && cc11 == 0 && c12 == '?' && cc13 == 0
&& cc14 == 0 && cc15 == 0 && c16 == 'x') {
return true;
}
}
// application/xml
if (cc1 == 0xff && cc2 == 0xfe && cc3 == 0x00 && cc4 == 0x00) {
if (c5 == '<' && cc6 == 0 && cc7 == 0 && cc8 == 0 && c9 == '?'
&& cc10 == 0 && cc11 == 0 && cc12 == 0 && c13 == 'x'
&& cc14 == 0 && cc15 == 0 && cc16 == 0) {
return true;
}
}
// application/pdf (%PDF-)
if (cc1 == 37 && cc2 == 80 && cc3 == 68 && cc4 == 70 && cc5 == 45) {
return true;
}
// image/png
if ((cc1 == 137 && cc2 == 80 && cc3 == 78 && cc4 == 71 && cc5 == 13
&& cc6 == 10 && cc7 == 26 && cc8 == 10) ||
(cc1 == 194 && cc2 == 137 && cc3 == 80 && cc4 == 78 && cc5 == 71 && cc6 == 13 //Our embedded PNG+XML
&& cc7 == 10 && cc8 == 26 && cc9 == 10)) {
return true;
}
// image/jpeg
if (cc1 == 0xFF && cc2 == 0xD8 && cc3 == 0xFF) {
if (cc4 == 0xE0 || cc4 == 0xEE) {
return true;
}
/**
* File format used by digital cameras to store images.
* Exif Format can be read by any application supporting
* JPEG. Exif Spec can be found at:
* http://www.pima.net/standards/it10/PIMA15740/Exif_2-1.PDF
*/
if ((cc4 == 0xE1) && (c7 == 'E' && c8 == 'x' && c9 == 'i'
&& c10 == 'f' && cc11 == 0)) {
return true;
}
}
// vsdx, vssx (also zip, jar, odt, ods, odp, docx, xlsx, pptx, apk, aar)
if (cc1 == 0x50 && cc2 == 0x4B && cc3 == 0x03 && cc4 == 0x04) {
return true;
} else if (cc1 == 0x50 && cc2 == 0x4B && cc3 == 0x03 && cc4 == 0x06) {
return true;
}
// mxfile, mxlibrary, mxGraphModel
if (c1 == '<' && c2 == 'm' && c3 == 'x') {
return true;
}
}
return false;
}
function isConflict(origStat, stat) {
return stat != null && origStat != null && stat.mtimeMs != origStat.mtimeMs;
}
function getDraftFileName(fileObject) {
let filePath = fileObject.path;
let draftFileName = '', counter = 1, uniquePart = '';
do {
draftFileName = path.join(path.dirname(filePath), DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT);
uniquePart = '_' + counter++;
} while (fs.existsSync(draftFileName));
return draftFileName;
}
async function getFileDrafts(fileObject) {
let filePath = fileObject.path;
let draftsPaths = [], drafts = [], draftFileName, counter = 1, uniquePart = '';
do {
draftsPaths.push(draftFileName);
draftFileName = path.join(path.dirname(filePath), DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT);
uniquePart = '_' + counter++;
} while (fs.existsSync(draftFileName)); //TODO this assume continuous drafts names
//Port old draft files to new prefex
counter = 1;
uniquePart = '';
let draftExists = false;
do {
draftFileName = path.join(path.dirname(filePath), OLD_DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT);
draftExists = fs.existsSync(draftFileName);
if (draftExists) {
const newDraftFileName = path.join(path.dirname(filePath), DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT);
await fsProm.rename(draftFileName, newDraftFileName);
draftsPaths.push(newDraftFileName);
}
uniquePart = '_' + counter++;
} while (draftExists); //TODO this assume continuous drafts names
//Skip the first null element
for (let i = 1; i < draftsPaths.length; i++) {
try {
let stat = await fsProm.lstat(draftsPaths[i]);
drafts.push({
data: await fsProm.readFile(draftsPaths[i], 'utf8'),
created: stat.ctimeMs,
modified: stat.mtimeMs,
path: draftsPaths[i]
});
} catch (e) {
} // Ignore
}
return drafts;
}
async function saveDraft(fileObject, data) {
if (!checkFileContent(data)) {
throw new Error('Invalid file data');
} else {
let draftFileName = fileObject.draftFileName || getDraftFileName(fileObject);
await fsProm.writeFile(draftFileName, data, 'utf8');
if (isWin) {
try {
// Add Hidden attribute:
spawn('attrib', ['+h', draftFileName], {shell: true});
} catch (e) {
}
}
return draftFileName;
}
}
async function saveFile(fileObject, data, origStat, overwrite, defEnc) {
if (!checkFileContent(data)) {
throw new Error('Invalid file data');
}
let retryCount = 0;
let backupCreated = false;
let bkpPath = path.join(path.dirname(fileObject.path), BKP_PREFEX + path.basename(fileObject.path) + BKP_EXT);
const oldBkpPath = path.join(path.dirname(fileObject.path), OLD_BKP_PREFEX + path.basename(fileObject.path) + BKP_EXT);
let writeEnc = defEnc || fileObject.encoding;
let writeFile = async function () {
let fh;
try {
// O_SYNC is for sync I/O and reduce risk of file corruption
fh = await fsProm.open(fileObject.path, O_SYNC | O_CREAT | O_WRONLY | O_TRUNC);
await fsProm.writeFile(fh, data, writeEnc);
} finally {
await fh?.close();
}
let stat2 = await fsProm.stat(fileObject.path);
// Workaround for possible writing errors is to check the written
// contents of the file and retry 3 times before showing an error
let writtenData = await fsProm.readFile(fileObject.path, writeEnc);
if (data != writtenData) {
retryCount++;
if (retryCount < 3) {
return await writeFile();
} else {
throw new Error('all saving trials failed');
}
} else {
//We'll keep the backup file in case the original file is corrupted. TODO When should we delete the backup file?
if (backupCreated) {
//fs.unlink(bkpPath, (err) => {}); //Ignore errors!
//Delete old backup file with old prefix
if (fs.existsSync(oldBkpPath)) {
fs.unlink(oldBkpPath, (err) => {
}); //Ignore errors
}
}
return stat2;
}
};
async function doSaveFile(isNew) {
if (enableStoreBkp && !isNew) {
//Copy file to back up file (after conflict and stat is checked)
let bkpFh;
try {
//Use file read then write to open the backup file direct sync write to reduce the chance of file corruption
let fileContent = await fsProm.readFile(fileObject.path, writeEnc);
bkpFh = await fsProm.open(bkpPath, O_SYNC | O_CREAT | O_WRONLY | O_TRUNC);
await fsProm.writeFile(bkpFh, fileContent, writeEnc);
backupCreated = true;
} catch (e) {
if (__DEV__) {
console.log('Backup file writing failed', e); //Ignore
}
} finally {
await bkpFh?.close();
if (isWin) {
try {
// Add Hidden attribute:
spawn('attrib', ['+h', bkpPath], {shell: true});
} catch (e) {
}
}
}
}
return await writeFile();
}
if (overwrite) {
return await doSaveFile(true);
} else {
let stat = fs.existsSync(fileObject.path) ?
await fsProm.stat(fileObject.path) : null;
if (stat && isConflict(origStat, stat)) {
throw new Error('conflict');
} else {
return await doSaveFile(stat == null);
}
}
}
async function writeFile(path, data, enc) {
if (!checkFileContent(data, enc)) {
throw new Error('Invalid file data');
} else {
return await fsProm.writeFile(path, data, enc);
}
}
function getAppDataFolder() {
try {
let appDataDir = app.getPath('appData');
let drawioDir = appDataDir + '/' + config.name;
if (!fs.existsSync(drawioDir)) //Usually this dir already exists
{
fs.mkdirSync(drawioDir);
}
return drawioDir;
} catch (e) {
}
return '.';
}
function getDocumentsFolder() {
//On windows, misconfigured Documents folder cause an exception
try {
return app.getPath('documents');
} catch (e) {
}
return '.';
}
function checkFileExists(pathParts) {
let filePath = path.join(...pathParts);
return {exists: fs.existsSync(filePath), path: filePath};
}
async function showOpenDialog(defaultPath, filters, properties) {
let win = BrowserWindow.getFocusedWindow();
return dialog.showOpenDialog(win, {
defaultPath: defaultPath,
filters: filters,
properties: properties
});
}
async function showSaveDialog(defaultPath, filters) {
let win = BrowserWindow.getFocusedWindow();
return dialog.showSaveDialog(win, {
defaultPath: defaultPath,
filters: filters
});
}
async function installPlugin(filePath) {
if (!enablePlugins) return {};
let pluginsDir = path.join(getAppDataFolder(), '/plugins');
if (!fs.existsSync(pluginsDir)) {
fs.mkdirSync(pluginsDir);
}
let pluginName = path.basename(filePath);
let dstFile = path.join(pluginsDir, pluginName);
if (fs.existsSync(dstFile)) {
throw new Error('fileExists');
} else {
await fsProm.copyFile(filePath, dstFile);
}
return {pluginName: pluginName, selDir: path.dirname(filePath)};
}
function getPluginFile(plugin) {
if (!enablePlugins) return null;
const prefix = path.join(getAppDataFolder(), '/plugins/');
const pluginFile = path.join(prefix, plugin);
if (pluginFile.startsWith(prefix) && fs.existsSync(pluginFile)) {
return pluginFile;
}
return null;
}
async function uninstallPlugin(plugin) {
const pluginFile = getPluginFile(plugin);
if (pluginFile != null) {
fs.unlinkSync(pluginFile);
}
}
function dirname(path_p) {
return path.dirname(path_p);
}
async function readFile(filename, encoding) {
let data = await fsProm.readFile(filename, encoding);
if (checkFileContent(data, encoding)) {
return data;
}
throw new Error('Invalid file data');
}
async function fileStat(file) {
return await fsProm.stat(file);
}
async function isFileWritable(file) {
try {
await fsProm.access(file, fs.constants.W_OK);
return true;
} catch (e) {
return false;
}
}
function clipboardAction(method, data) {
if (method == 'writeText') {
clipboard.writeText(data);
} else if (method == 'readText') {
return clipboard.readText();
} else if (method == 'writeImage') {
clipboard.write({
image:
nativeImage.createFromDataURL(data.dataUrl), html: '<img src="' +
data.dataUrl + '" width="' + data.w + '" height="' + data.h + '">'
});
}
}
async function deleteFile(file) {
// Reading the header of the file to confirm it is a file we can delete
let fh = await fsProm.open(file, O_RDONLY);
let buffer = Buffer.allocUnsafe(16);
await fh.read(buffer, 0, 16);
await fh.close();
if (checkFileContent(buffer)) {
await fsProm.unlink(file);
}
}
async function windowAction(method) {
let win = BrowserWindow.getFocusedWindow();
if (win) {
if (method == 'minimize') {
win.minimize();
} else if (method == 'maximize') {
win.maximize();
} else if (method == 'unmaximize') {
win.unmaximize();
} else if (method == 'close') {
win.close();
} else if (method == 'isMaximized') {
return win.isMaximized();
} else if (method == 'removeAllListeners') {
win.removeAllListeners();
}
}
}
async function openExternal(url) {
//Only open http(s), mailto, tel, and callto links
if (allowedUrls.test(url)) {
await shell.openExternal(url)
}
}
async function watchFile(path) {
let win = BrowserWindow.getFocusedWindow();
if (win) {
fs.watchFile(path, (curr, prev) => {
try {
win.webContents.send('fileChanged', {
path: path,
curr: curr,
prev: prev
});
} catch (e) {
// Ignore
}
});
}
}
async function unwatchFile(path) {
fs.unwatchFile(path);
}
function getCurDir() {
return __dirname;
}
ipcMain.on("rendererReq", async (event, args) => {
try {
let ret = null;
switch (args.action) {
case 'saveFile':
ret = await saveFile(args.fileObject, args.data, args.origStat, args.overwrite, args.defEnc);
break;
case 'writeFile':
ret = await writeFile(args.path, args.data, args.enc);
break;
case 'saveDraft':
ret = await saveDraft(args.fileObject, args.data);
break;
case 'getFileDrafts':
ret = await getFileDrafts(args.fileObject);
break;
case 'getDocumentsFolder':
ret = await getDocumentsFolder();
break;
case 'checkFileExists':
ret = checkFileExists(args.pathParts);
break;
case 'showOpenDialog':
dialogOpen = true;
ret = await showOpenDialog(args.defaultPath, args.filters, args.properties);
ret = ret.filePaths;
dialogOpen = false;
break;
case 'showSaveDialog':
dialogOpen = true;
ret = await showSaveDialog(args.defaultPath, args.filters);
ret = ret.canceled ? null : ret.filePath;
dialogOpen = false;
break;
case 'installPlugin':
ret = await installPlugin(args.filePath);
break;
case 'uninstallPlugin':
ret = await uninstallPlugin(args.plugin);
break;
case 'getPluginFile':
ret = getPluginFile(args.plugin);
break;
case 'isPluginsEnabled':
ret = enablePlugins;
break;
case 'dirname':
ret = await dirname(args.path);
break;
case 'readFile':
ret = await readFile(args.filename, args.encoding);
break;
case 'clipboardAction':
ret = clipboardAction(args.method, args.data);
break;
case 'deleteFile':
ret = await deleteFile(args.file);
break;
case 'fileStat':
ret = await fileStat(args.file);
break;
case 'isFileWritable':
ret = await isFileWritable(args.file);
break;
case 'windowAction':
ret = await windowAction(args.method);
break;
case 'openExternal':
ret = await openExternal(args.url);
break;
case 'openDownloadWindow':
ret = await electronDown.open(args.language || 'zh', args.theme || 'light');
break;
case 'updateDownloadWindow':
ret = await electronDown.updateWindow(args.language, args.theme);
break;
case 'createDownload':
ret = await electronDown.createDownload(mainWindow, args.url, args.options || {});
break;
case 'watchFile':
ret = await watchFile(args.path);
break;
case 'unwatchFile':
ret = await unwatchFile(args.path);
break;
case 'getCurDir':
ret = getCurDir();
break;
}
event.reply('mainResp', {success: true, data: ret, reqId: args.reqId});
} catch (e) {
event.reply('mainResp', {error: true, msg: e.message, e: e, reqId: args.reqId});
loger.error('Renderer request error', e.message, e.stack);
}
});