From 49c5a9f6217396b454009191bbdf646d6711d3e5 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Tue, 22 Feb 2022 21:02:27 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=96=E6=B6=88=20electron-renderer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main.js | 832 +++++++++++++++++- electron/package.json | 3 +- electron/preload.js | 79 +- package.json | 5 +- resources/assets/js/App.vue | 9 +- resources/assets/js/app.js | 10 +- .../assets/js/components/RightBottom.vue | 8 +- resources/assets/js/functions/web.js | 2 +- resources/assets/js/pages/manage.vue | 2 +- .../js/pages/manage/components/DialogView.vue | 2 +- .../js/pages/manage/components/TaskDetail.vue | 6 +- resources/assets/js/pages/manage/file.vue | 2 +- webpack.mix.js | 16 +- 13 files changed, 908 insertions(+), 68 deletions(-) diff --git a/electron/main.js b/electron/main.js index 8c080628..895725e6 100644 --- a/electron/main.js +++ b/electron/main.js @@ -2,11 +2,14 @@ const fs = require('fs') const fse = require('fs-extra') const os = require("os"); const path = require('path') -const XLSX = require('xlsx'); -const {app, BrowserWindow, ipcMain, dialog} = require('electron') +const {app, BrowserWindow, ipcMain, dialog, clipboard, nativeImage, shell} = require('electron') +const log = require("electron-log"); +const fsProm = require('fs/promises'); +const PDFDocument = require('pdf-lib').PDFDocument; +const crc = require('crc'); +const zlib = require('zlib'); const utils = require('./utils'); const config = require('./package.json'); -const log = require("electron-log"); let mainWindow = null, subWindow = [], @@ -68,8 +71,9 @@ function createMainWindow() { autoHideMenuBar: true, webPreferences: { preload: path.join(__dirname, 'preload.js'), + webSecurity: true, nodeIntegration: true, - contextIsolation: false + contextIsolation: true } }) mainWindow.webContents.setUserAgent(mainWindow.webContents.getUserAgent() + " MainTaskWindow/" + process.platform + "/" + os.arch() + "/1.0"); @@ -150,9 +154,10 @@ function createSubWindow(args) { webPreferences: { preload: path.join(__dirname, 'preload.js'), devTools: args.devTools !== false, + webSecurity: true, nodeIntegration: true, - contextIsolation: false - } + contextIsolation: true + }, }, config)) browser.on('page-title-updated', (event, title) => { if (title == "index.html" || args.titleFixed === true) { @@ -289,6 +294,15 @@ ipcMain.on('windowClose', (event) => { event.returnValue = "ok" }) +/** + * 关闭窗口(强制) + */ +ipcMain.on('windowDestroy', (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + win.destroy() + event.returnValue = "ok" +}) + /** * 设置窗口尺寸 * @param args {width, height, autoZoom, minWidth, minHeight, maxWidth, maxHeight} @@ -404,20 +418,794 @@ ipcMain.on('setDockBadge', (event, args) => { event.returnValue = "ok" }) -/** - * 保存sheets - */ -ipcMain.on('saveSheet', (event, data, filename, opts) => { - const EXTENSIONS = "xls|xlsx|xlsm|xlsb|xml|csv|txt|dif|sylk|slk|prn|ods|fods|htm|html".split("|"); - dialog.showSaveDialog({ - title: 'Save file as', - defaultPath: filename, - filters: [{ - name: "Spreadsheets", - extensions: EXTENSIONS - }] - }).then(o => { - XLSX.writeFile(data, o.filePath, opts); + +//Pdf export +const MICRON_TO_PIXEL = 264.58 //264.58 micron = 1 pixel +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) { + log.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) { + log.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, + center: true, + show: false, + autoHideMenuBar: true, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + devTools: args.devTools !== false, + 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('diagrams.net'); + + if (xml != null) { + //Embed diagram XML as file attachment + await pdfDoc.attach(Buffer.from(xml).toString('base64'), 'diagram.xml', { + mimeType: 'application/vnd.jgraph.mxfile', + description: 'Diagram 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: `${__dirname}/electron-preload.js`, + backgroundThrottling: false, + contextIsolation: true, + nativeWindowOpen: true + }, + show: false, + frame: false, + enableLargerThanScreen: true, + transparent: args.format == 'png' && (args.bg == null || args.bg == 'none'), + }); + + browser.loadURL(`file://${__dirname}/export3.html`); + + const contents = browser.webContents; + let pageByPage = (args.format == 'pdf' && !args.print), from, 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 { + //Chrome generates Pdf files larger than requested pixels size and requires scaling + let fixingScale = 0.959; + + let w = Math.ceil(bounds.width * fixingScale); + + // +0.1 fixes cases where adding 1px below is not enough + // Increase this if more cropped PDFs have extra empty pages + let h = Math.ceil(bounds.height * fixingScale + 0.1); + + pdfOptions = { + printBackground: true, + pageSize: { + width: w * MICRON_TO_PIXEL, + height: (h + 2) * MICRON_TO_PIXEL //the extra 2 pixels to prevent adding an extra empty page + }, + marginsType: 1 // 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 {COPYFILE_EXCL} = fs.constants; +const DRAFT_PREFEX = '~$'; +const DRAFT_EXT = '.dtmp'; +const BKP_PREFEX = '~$'; +const BKP_EXT = '.bkp'; + +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 + + 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 (data == null || data.length == 0) { + throw new Error('empty data'); + } else { + let draftFileName = fileObject.draftFileName || getDraftFileName(fileObject); + await fsProm.writeFile(draftFileName, data, 'utf8'); + return draftFileName; + } +} + +async function saveFile(fileObject, data, origStat, overwrite, defEnc) { + let retryCount = 0; + let backupCreated = false; + let bkpPath = path.join(path.dirname(fileObject.path), BKP_PREFEX + path.basename(fileObject.path) + BKP_EXT); + + let writeFile = async function () { + if (data == null || data.length == 0) { + throw new Error('empty data'); + } else { + let writeEnc = defEnc || fileObject.encoding; + + await fsProm.writeFile(fileObject.path, data, writeEnc); + let stat2 = await fsProm.stat(fileObject.path); + 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 { + if (backupCreated) { + fs.unlink(bkpPath, (err) => { + }); //Ignore errors! + } + + return stat2; + } + } + }; + + async function doSaveFile() { + try { + await fsProm.copyFile(fileObject.path, bkpPath, COPYFILE_EXCL); + backupCreated = true; + } catch (e) { + } //Ignore + + return await writeFile(); + } + + if (overwrite) { + return await doSaveFile(); + } else { + let stat = fs.existsSync(fileObject.path) ? + await fsProm.stat(fileObject.path) : null; + + if (stat && isConflict(origStat, stat)) { + new Error('conflict'); + } else { + return await doSaveFile(); + } + } +} + +async function writeFile(path, data, enc) { + return await fsProm.writeFile(path, data, enc); +} + +function getAppDataFolder() { + try { + let appDataDir = app.getPath('appData'); + let drawioDir = appDataDir + '/dootask.com'; + + if (!fs.existsSync(drawioDir)) //Usually this dir already exists + { + fs.mkdirSync(drawioDir); + } + + return drawioDir; + } catch (e) { + } + + return '.'; +} + +function getDocumentsFolder() { + 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) { + return dialog.showOpenDialogSync({ + defaultPath: defaultPath, + filters: filters, + properties: properties }); - event.returnValue = "ok" -}) +} + +async function showSaveDialog(defaultPath, filters) { + return dialog.showSaveDialogSync({ + defaultPath: defaultPath, + filters: filters + }); +} + +async function installPlugin(filePath) { + 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 uninstallPlugin(plugin) { + let pluginsFile = path.join(getAppDataFolder(), '/plugins', plugin); + + if (fs.existsSync(pluginsFile)) { + fs.unlinkSync(pluginsFile); + } + return null +} + +function dirname(path_p) { + return path.dirname(path_p); +} + +async function readFile(filename, encoding) { + return await fsProm.readFile(filename, encoding); +} + +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: '' + }); + } +} + +async function deleteFile(file) { + await fsProm.unlink(file); +} + +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(); + } + } +} + +function openExternal(url) { + shell.openExternal(url); + return null +} + +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 + }); + } + return null +} + +function unwatchFile(path) { + fs.unwatchFile(path); + return null +} + +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 'getAppDataFolder': + ret = getAppDataFolder(); + break; + case 'getDocumentsFolder': + ret = await getDocumentsFolder(); + break; + case 'checkFileExists': + ret = checkFileExists(args.pathParts); + break; + case 'showOpenDialog': + ret = await showOpenDialog(args.defaultPath, args.filters, args.properties); + break; + case 'showSaveDialog': + ret = await showSaveDialog(args.defaultPath, args.filters); + break; + case 'installPlugin': + ret = await installPlugin(args.filePath); + break; + case 'uninstallPlugin': + ret = await uninstallPlugin(args.plugin); + break; + case 'dirname': + ret = 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 = windowAction(args.method); + break; + case 'openExternal': + ret = await openExternal(args.url); + break; + case 'watchFile': + ret = await watchFile(args.path); + break; + case 'unwatchFile': + ret = await unwatchFile(args.path); + 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}); + } +}); diff --git a/electron/package.json b/electron/package.json index bf0800c5..d393fc86 100644 --- a/electron/package.json +++ b/electron/package.json @@ -40,10 +40,11 @@ }, "dependencies": { "axios": "^0.26.0", + "crc": "^3.8.0", "electron-squirrel-startup": "^1.0.0", "electron-log": "^4.4.6", "fs-extra": "^10.0.0", - "xlsx": "^0.18.1" + "pdf-lib": "^1.16.0" }, "build": { "appId": "com.dootask.task", diff --git a/electron/preload.js b/electron/preload.js index 601ef65b..50fcfc14 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -1,14 +1,73 @@ -// preload.js +const { + shell, + contextBridge, + ipcRenderer +} = require("electron"); -// All of the Node.js APIs are available in the preload process. -// It has the same sandbox as a Chrome extension. -window.addEventListener('DOMContentLoaded', () => { - const replaceText = (selector, text) => { - const element = document.getElementById(selector) - if (element) element.innerText = text +let reqId = 1; +let reqInfo = {}; +let fileChangedListeners = {}; + +ipcRenderer.on('mainResp', (event, resp) => { + let callbacks = reqInfo[resp.reqId]; + + if (resp.error) { + callbacks.error(resp.msg, resp.e); + } else { + callbacks.callback(resp.data); } - for (const dependency of ['chrome', 'node', 'electron']) { - replaceText(`${dependency}-version`, process.versions[dependency]) + delete reqInfo[resp.reqId]; +}); + +ipcRenderer.on('fileChanged', (event, resp) => { + let listener = fileChangedListeners[resp.path]; + + if (listener) { + listener(resp.curr, resp.prev); } -}) +}); + +contextBridge.exposeInMainWorld( + 'electron', { + request: (msg, callback, error) => { + msg.reqId = reqId++; + reqInfo[msg.reqId] = {callback: callback, error: error}; + + if (msg.action == 'watchFile') { + fileChangedListeners[msg.path] = msg.listener; + delete msg.listener; + } + + ipcRenderer.send('rendererReq', msg); + }, + registerMsgListener: (action, callback) => { + ipcRenderer.on(action, (event, args) => { + callback(args); + }); + }, + listenOnce: (action, callback) => { + ipcRenderer.once(action, (event, args) => { + callback(args); + }); + }, + sendMessage: (action, args) => { + ipcRenderer.send(action, args); + }, + sendSyncMessage: (action, args) => { + ipcRenderer.sendSync(action, args); + }, + openExternal: (url) => { + return new Promise((resolve, reject) => { + shell.openExternal(url).then(resolve).catch(reject) + }); + } + } +); + +contextBridge.exposeInMainWorld( + 'process', { + type: process.type, + versions: process.versions + } +); diff --git a/package.json b/package.json index 53cd3252..96352eca 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "cross-env": "^7.0.3", "css-loader": "^6.5.1", "echarts": "^5.2.2", - "electron": "^17.0.1", "element-ui": "^2.15.6", "file-loader": "^6.2.0", "inquirer": "^8.2.0", @@ -75,8 +74,8 @@ "vue-template-compiler": "^2.6.14", "vuedraggable": "^2.24.3", "vuex": "^3.6.2", - "webpack": "^5.65.0", - "webpack-cli": "^4.9.1", + "webpack": "^5.69.1", + "webpack-cli": "^4.9.2", "xlsx": "^0.17.4" } } diff --git a/resources/assets/js/App.vue b/resources/assets/js/App.vue index 45e1127e..f634fed6 100755 --- a/resources/assets/js/App.vue +++ b/resources/assets/js/App.vue @@ -179,9 +179,8 @@ export default { if (!this.$Electron) { return; } - const {ipcRenderer} = this.$Electron; - ipcRenderer.send('inheritClose'); - ipcRenderer.on('windowClose', () => { + this.$Electron.sendMessage('inheritClose'); + this.$Electron.registerMsgListener('windowClose', () => { if (this.$Modal.removeLast()) { return; } @@ -189,9 +188,9 @@ export default { this.cacheDrawerOverlay[this.cacheDrawerOverlay.length - 1].close(); return; } - ipcRenderer.send('windowHidden'); + this.$Electron.sendMessage('windowHidden'); }) - ipcRenderer.on('dispatch', (event, args) => { + this.$Electron.registerMsgListener('dispatch', (event, args) => { if (!$A.isJson(args)) { return; } diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js index 9d077c47..6aea7f97 100644 --- a/resources/assets/js/app.js +++ b/resources/assets/js/app.js @@ -1,3 +1,5 @@ +const isElectron = window && window.process && window.process.type; + import './functions/common' import './functions/web' @@ -56,7 +58,7 @@ VueRouter.prototype.push = function push(location) { } const router = new VueRouter({ - mode: !!__IS_ELECTRON ? 'hash' : 'history', + mode: isElectron ? 'hash' : 'history', routes }); @@ -98,8 +100,8 @@ Vue.prototype.$Electron = null; Vue.prototype.$Platform = "web"; Vue.prototype.$isMainElectron = false; Vue.prototype.$isSubElectron = false; -if (!!__IS_ELECTRON) { - Vue.prototype.$Electron = require('electron'); +if (isElectron) { + Vue.prototype.$Electron = electron; Vue.prototype.$Platform = /macintosh|mac os x/i.test(navigator.userAgent) ? "mac" : "win"; Vue.prototype.$isMainElectron = /\s+MainTaskWindow\//.test(window.navigator.userAgent); Vue.prototype.$isSubElectron = /\s+SubTaskWindow\//.test(window.navigator.userAgent); @@ -131,7 +133,7 @@ $A.isMainElectron = app.$isMainElectron; $A.isSubElectron = app.$isSubElectron; $A.execMainDispatch = (action, data) => { if ($A.isSubElectron) { - $A.Electron.ipcRenderer.send('sendForwardMain', { + $A.Electron.sendMessage('sendForwardMain', { channel: 'dispatch', data: {action, data}, }); diff --git a/resources/assets/js/components/RightBottom.vue b/resources/assets/js/components/RightBottom.vue index cf2f1365..f1d9243d 100644 --- a/resources/assets/js/components/RightBottom.vue +++ b/resources/assets/js/components/RightBottom.vue @@ -44,7 +44,7 @@ export default { this.getReleases(); // if (this.$Electron) { - this.$Electron.ipcRenderer.on('downloadDone', (event, {result}) => { + this.$Electron.registerMsgListener('downloadDone', (event, {result}) => { if (result.name == this.repoData.name) { this.downloadResult = result; this.releasesNotification() @@ -186,7 +186,7 @@ export default { if (this.compareVersion(latestVersion, currentVersion) === 1) { // 有新版本 console.log("New version: " + latestVersion); - this.$Electron.ipcRenderer.send('downloadFile', { + this.$Electron.sendMessage('downloadFile', { url: this.repoData.browser_download_url }); } @@ -241,10 +241,10 @@ export default { if (!this.$Electron) { return; } - this.$Electron.ipcRenderer.send('openFile', { + this.$Electron.sendMessage('openFile', { path: this.downloadResult.savePath }); - this.$Electron.ipcRenderer.send('windowQuit'); + this.$Electron.sendMessage('windowQuit'); }, useSSOLogin() { diff --git a/resources/assets/js/functions/web.js b/resources/assets/js/functions/web.js index d15ff778..399cf87c 100755 --- a/resources/assets/js/functions/web.js +++ b/resources/assets/js/functions/web.js @@ -366,7 +366,7 @@ return } if ($A.Electron) { - $A.Electron.shell.openExternal(url).catch(() => { + $A.Electron.openExternal(url).catch(() => { $A.modalError("下载失败"); }); } else { diff --git a/resources/assets/js/pages/manage.vue b/resources/assets/js/pages/manage.vue index 9117e41b..9e4a2b71 100644 --- a/resources/assets/js/pages/manage.vue +++ b/resources/assets/js/pages/manage.vue @@ -475,7 +475,7 @@ export default { unreadTotal: { handler(num) { if (this.$Electron) { - this.$Electron.ipcRenderer.send('setDockBadge', num); + this.$Electron.sendMessage('setDockBadge', num); } }, immediate: true diff --git a/resources/assets/js/pages/manage/components/DialogView.vue b/resources/assets/js/pages/manage/components/DialogView.vue index 53444d4f..093d7bf8 100644 --- a/resources/assets/js/pages/manage/components/DialogView.vue +++ b/resources/assets/js/pages/manage/components/DialogView.vue @@ -213,7 +213,7 @@ export default { viewFile() { if (this.$Electron) { - this.$Electron.ipcRenderer.send('windowRouter', { + this.$Electron.sendMessage('windowRouter', { title: `${this.msgData.msg.name} (${$A.bytesToSize(this.msgData.msg.size)})`, titleFixed: true, name: 'file-msg-' + this.msgData.id, diff --git a/resources/assets/js/pages/manage/components/TaskDetail.vue b/resources/assets/js/pages/manage/components/TaskDetail.vue index bb48818a..1c798d3b 100644 --- a/resources/assets/js/pages/manage/components/TaskDetail.vue +++ b/resources/assets/js/pages/manage/components/TaskDetail.vue @@ -1126,7 +1126,7 @@ export default { config.minWidth = 800; config.minHeight = 600; } - this.$Electron.ipcRenderer.send('windowRouter', { + this.$Electron.sendMessage('windowRouter', { title: this.taskDetail.name, titleFixed: true, name: 'task-' + this.taskDetail.id, @@ -1139,7 +1139,7 @@ export default { resizeDialog() { return new Promise(resolve => { - this.$Electron.ipcRenderer.sendSync('windowSize', { + this.$Electron.sendSyncMessage('windowSize', { width: Math.max(1100, window.innerWidth), height: Math.max(720, window.innerHeight), minWidth: 800, @@ -1161,7 +1161,7 @@ export default { viewFile(file) { if (this.$Electron) { - this.$Electron.ipcRenderer.send('windowRouter', { + this.$Electron.sendMessage('windowRouter', { title: `${file.name} (${$A.bytesToSize(file.size)})`, titleFixed: true, name: 'file-task-' + file.id, diff --git a/resources/assets/js/pages/manage/file.vue b/resources/assets/js/pages/manage/file.vue index 5cb13a3e..cab0a3ba 100644 --- a/resources/assets/js/pages/manage/file.vue +++ b/resources/assets/js/pages/manage/file.vue @@ -854,7 +854,7 @@ export default { }, openSingle(item) { - this.$Electron.ipcRenderer.send('windowRouter', { + this.$Electron.sendMessage('windowRouter', { title: this.formatName(item), titleFixed: true, userAgent: "/hideenOfficeTitle/", diff --git a/webpack.mix.js b/webpack.mix.js index 44a73bcf..123b8d9b 100644 --- a/webpack.mix.js +++ b/webpack.mix.js @@ -21,24 +21,16 @@ mix .js('resources/assets/js/app.js', 'js') .sass('resources/assets/sass/app.scss', 'css') .setPublicPath(publicPath) - .webpackConfig(webpack => { + .webpackConfig(() => { let config = { output: { chunkFilename: ({chunk}) => { return `js/build/${mixBuildName(chunk.id)}.js` } - }, - plugins: [ - new webpack.DefinePlugin({ - '__IS_ELECTRON': isElectron, - }) - ] - }; - if (isElectron) { - config.target = 'electron-renderer' - if (!isHot) { - config.output.publicPath = './' } + }; + if (isElectron && !isHot) { + config.output.publicPath = './' } return config })