From 2b88764c7e1e74d64fdf608d963fcce0b4c5add5 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Sun, 6 Aug 2023 17:58:54 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E6=A1=8C=E9=9D=A2?= =?UTF-8?q?=E7=AB=AF=E9=82=AE=E4=BB=B6=E5=9B=BE=E7=89=87=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/electron-menu.js | 154 ++++++++++++++++++ electron/electron.js | 36 ++++ .../PreviewImage/components/view.vue | 1 + .../pages/manage/components/DialogWrapper.vue | 6 +- 4 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 electron/electron-menu.js diff --git a/electron/electron-menu.js b/electron/electron-menu.js new file mode 100644 index 000000000..bfa2a0d56 --- /dev/null +++ b/electron/electron-menu.js @@ -0,0 +1,154 @@ +const { + clipboard, + nativeImage, + Menu, + MenuItem, + dialog, + shell, +} = require('electron') +const fs = require('fs') +const url = require('url') +const {pipeline} = require('stream') + +const MAILTO_PREFIX = "mailto:"; + +const PERMITTED_URL_SCHEMES = ["http:", "https:", MAILTO_PREFIX]; + +const electronMenu = { + language: { + openInBrowser: "在浏览器中打开", + saveImageAs: "图片存储为...", + copyImage: "复制图片", + copyEmailAddress: "复制电子邮件地址", + copyLinkAddress: "复制链接地址", + copyImageAddress: "复制图片地址", + failedToSaveImage: "图片保存失败", + theImageFailedToSave: "图片无法保存", + }, + + setLanguage(language) { + this.language = Object.assign(this.language, language); + }, + + safeOpenURL(target) { + const parsedUrl = url.parse(target); + if (PERMITTED_URL_SCHEMES.includes(parsedUrl.protocol)) { + const newTarget = url.format(parsedUrl); + shell.openExternal(newTarget).then(r => { + }); + } + }, + + async saveImageAs(url, params) { + const targetFileName = params.suggestedFilename || params.altText || "image.png"; + const {filePath} = await dialog.showSaveDialog({ + defaultPath: targetFileName, + }); + + if (!filePath) return; // user cancelled dialog + + try { + if (url.startsWith("data:")) { + await electronMenu.writeNativeImage(filePath, nativeImage.createFromDataURL(url)); + } else { + const resp = await fetch(url); + if (!resp.ok) throw new Error(`unexpected response ${resp.statusText}`); + if (!resp.body) throw new Error(`unexpected response has no body ${resp.statusText}`); + pipeline(resp.body, fs.createWriteStream(filePath)); + } + } catch (err) { + await dialog.showMessageBox({ + type: "error", + title: electronMenu.language.failedToSaveImage, + message: electronMenu.language.theImageFailedToSave, + }); + } + }, + + writeNativeImage(filePath, img) { + switch (filePath.split(".").pop()?.toLowerCase()) { + case "jpg": + case "jpeg": + return fs.promises.writeFile(filePath, img.toJPEG(100)); + case "bmp": + return fs.promises.writeFile(filePath, img.toBitmap()); + case "png": + default: + return fs.promises.writeFile(filePath, img.toPNG()); + } + }, + + webContentsMenu(webContents) { + webContents.on("context-menu", function (e, params) { + if (params.linkURL || params.srcURL) { + const url = params.linkURL || params.srcURL; + const popupMenu = new Menu(); + + if (!url.startsWith("blob:")) { + popupMenu.append( + new MenuItem({ + label: electronMenu.language.openInBrowser, + accelerator: "o", + click() { + electronMenu.safeOpenURL(url); + }, + }), + ); + if (params.hasImageContents) { + popupMenu.append( + new MenuItem({ + label: electronMenu.language.saveImageAs, + accelerator: "s", + click: async function () { + await electronMenu.saveImageAs(url, params); + }, + }), + ); + } + } + + if (params.hasImageContents) { + popupMenu.append( + new MenuItem({ + label: electronMenu.language.copyImage, + accelerator: "c", + click() { + webContents.copyImageAt(params.x, params.y); + }, + }), + ); + } + + if (!url.startsWith("blob:")) { + if (url.startsWith(MAILTO_PREFIX)) { + popupMenu.append( + new MenuItem({ + label: electronMenu.language.copyEmailAddress, + accelerator: "a", + click() { + clipboard.writeText(url.substring(MAILTO_PREFIX.length)); + }, + }), + ); + } else { + popupMenu.append( + new MenuItem({ + label: params.hasImageContents ? electronMenu.language.copyImageAddress : electronMenu.language.copyLinkAddress, + accelerator: "a", + click() { + clipboard.writeText(url); + }, + }), + ); + } + } + + if (popupMenu.items.length > 0) { + popupMenu.popup({}); + e.preventDefault(); + } + } + }) + } +} +module.exports = electronMenu; diff --git a/electron/electron.js b/electron/electron.js index b79306dd4..560f3d2bc 100644 --- a/electron/electron.js +++ b/electron/electron.js @@ -11,6 +11,7 @@ const crc = require('crc'); const zlib = require('zlib'); const utils = require('./utils'); const config = require('./package.json'); +const electronMenu = require("./electron-menu"); const spawn = require("child_process").spawn; const isMac = process.platform === 'darwin' @@ -58,6 +59,7 @@ function createMainWindow() { openExternal(url) return {action: 'deny'} }) + electronMenu.webContentsMenu(mainWindow.webContents) if (devloadUrl) { mainWindow.loadURL(devloadUrl).then(_ => { @@ -158,6 +160,7 @@ function createSubWindow(args) { openExternal(url) return {action: 'deny'} }) + electronMenu.webContentsMenu(browser.webContents) if (devloadUrl) { browser.loadURL(devloadUrl + '#' + (args.hash || args.path)).then(_ => { @@ -257,6 +260,17 @@ app.on('browser-window-focus', () => { } }) +/** + * 设置菜单语言包 + * @param args {path} + */ +ipcMain.on('setMenuLanguage', (event, args) => { + if (utils.isJson(args)) { + electronMenu.setLanguage(args) + } + event.returnValue = "ok" +}) + /** * 打开文件 * @param args {path} @@ -475,6 +489,28 @@ ipcMain.on('copyBase64Image', (event, args) => { event.returnValue = "ok" }) +/** + * 复制图片根据坐标 + * @param args + */ +ipcMain.on('copyImageAt', (event, args) => { + try { + event.sender.copyImageAt(args.x, args.y); + } catch (e) { + // log.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 diff --git a/resources/assets/js/components/PreviewImage/components/view.vue b/resources/assets/js/components/PreviewImage/components/view.vue index 46d259b90..73abd2be7 100644 --- a/resources/assets/js/components/PreviewImage/components/view.vue +++ b/resources/assets/js/components/PreviewImage/components/view.vue @@ -138,6 +138,7 @@ height: 100%; justify-content: center; width: 100%; + user-select: none; } } } diff --git a/resources/assets/js/pages/manage/components/DialogWrapper.vue b/resources/assets/js/pages/manage/components/DialogWrapper.vue index f9c6da7c9..b1f4213f6 100644 --- a/resources/assets/js/pages/manage/components/DialogWrapper.vue +++ b/resources/assets/js/pages/manage/components/DialogWrapper.vue @@ -610,6 +610,7 @@ export default { navStyle: {}, + operateClient: {x: 0, y: 0}, operateVisible: false, operatePreventScroll: 0, operateCopys: [], @@ -2335,6 +2336,7 @@ export default { top: `${projectRect.top + this.windowScrollY}px`, height: projectRect.height + 'px', } + this.operateClient = {x: event.clientX, y: event.clientY}; this.operateVisible = true; }) }, @@ -2440,9 +2442,7 @@ export default { switch (type) { case 'image': if (this.$Electron) { - this.getBase64Image(value).then(base64 => { - this.$Electron.sendMessage('copyBase64Image', {base64}); - }) + this.$Electron.sendMessage('copyImageAt', this.operateClient); } break;