diff --git a/src/background.ts b/src/background.ts index 908fe60..0384667 100644 --- a/src/background.ts +++ b/src/background.ts @@ -33,10 +33,7 @@ import bus from '~/main/utils/eventBus' import { updateShortKeyFromVersion212 } from '~/main/migrate/shortKeyUpdateHelper' -import { - shortKeyUpdater, - initShortKeyRegister -} from '~/main/utils/shortKeyHandler' +import shortKeyHandler from '~/main/utils/shortKeyHandler' import logger from '~/main/utils/logger' const isDevelopment = process.env.NODE_ENV !== 'production' @@ -462,13 +459,39 @@ ipcMain.on('uploadChoosedFiles', async (evt: IpcMainEvent, files: IFileWithPath[ return uploadChoosedFiles(evt.sender, files) }) -ipcMain.on('updateShortKey', (evt: IpcMainEvent, item: IShortKeyConfig, oldKey: string) => { - shortKeyUpdater(globalShortcut, item, oldKey) - const notification = new Notification({ - title: '操作成功', - body: '你的快捷键已经修改成功' - }) - notification.show() +ipcMain.on('updateShortKey', (evt: IpcMainEvent, item: IShortKeyConfig, oldKey: string, from: string) => { + const result = shortKeyHandler.updateShortKey(item, oldKey, from) + evt.sender.send('updateShortKeyResponse', result) + if (result) { + const notification = new Notification({ + title: '操作成功', + body: '你的快捷键已经修改成功' + }) + notification.show() + } else { + const notification = new Notification({ + title: '操作失败', + body: '快捷键冲突,请重新设置' + }) + notification.show() + } +}) + +ipcMain.on('bindOrUnbindShortKey', (evt: IpcMainEvent, item: IShortKeyConfig, from: string) => { + const result = shortKeyHandler.bindOrUnbindShortKey(item, from) + if (result) { + const notification = new Notification({ + title: '操作成功', + body: '你的快捷键已经修改成功' + }) + notification.show() + } else { + const notification = new Notification({ + title: '操作失败', + body: '快捷键冲突,请重新设置' + }) + notification.show() + } }) ipcMain.on('updateCustomLink', () => { @@ -585,7 +608,7 @@ app.on('ready', async () => { // 不需要阻塞 process.nextTick(() => { updateShortKeyFromVersion212(db, db.get('settings.shortKey')) - initShortKeyRegister(globalShortcut, db.get('settings.shortKey')) + shortKeyHandler.init() }) if (process.env.NODE_ENV !== 'development') { @@ -638,13 +661,26 @@ app.setLoginItemSettings({ function initEventCenter () { const eventList: any = { - 'picgo:upload': uploadClipboardFiles + 'picgo:upload': uploadClipboardFiles, + 'createSettingWindow': shortKeyRequestSettingWindow, + hideMiniWindow } for (let i in eventList) { bus.on(i, eventList[i]) } } +function shortKeyRequestSettingWindow (command: string) { + if (!settingWindow) createSettingWindow() + bus.emit('createSettingWindowDone', command, settingWindow!.id) +} + +function hideMiniWindow () { + if (miniWindow && miniWindow.isVisible()) { + miniWindow.hide() + } +} + // Exit cleanly on request from parent process in development mode. if (isDevelopment) { if (process.platform === 'win32') { diff --git a/src/main/migrate/shortKeyUpdateHelper.ts b/src/main/migrate/shortKeyUpdateHelper.ts index 5786ce7..0a1f86a 100644 --- a/src/main/migrate/shortKeyUpdateHelper.ts +++ b/src/main/migrate/shortKeyUpdateHelper.ts @@ -8,7 +8,7 @@ const updateShortKeyFromVersion212 = (db: typeof DB, shortKeyConfig: IShortKeyCo shortKeyConfig['picgo:upload'] = { enable: true, key: shortKeyConfig.upload, - name: 'picgo:upload', + name: 'upload', label: '快捷上传' } delete shortKeyConfig.upload diff --git a/src/main/utils/guiApi.ts b/src/main/utils/guiApi.ts index 3ed4094..d91f74c 100644 --- a/src/main/utils/guiApi.ts +++ b/src/main/utils/guiApi.ts @@ -3,68 +3,56 @@ import { BrowserWindow, clipboard, Notification, - IpcMain, - WebContents + WebContents, + ipcMain } from 'electron' import db from '#/datastore' import Uploader from './uploader' import pasteTemplate from '#/utils/pasteTemplate' -import PicGoCore from '~/universal/types/picgo' const WEBCONTENTS = Symbol('WEBCONTENTS') -const IPCMAIN = Symbol('IPCMAIN') -const PICGO = Symbol('PICGO') -class GuiApi { +class GuiApi implements IGuiApi { private [WEBCONTENTS]: WebContents - private [IPCMAIN]: IpcMain - private [PICGO]: PicGoCore - constructor (ipcMain: IpcMain, webcontents: WebContents, picgo: PicGoCore) { + constructor (webcontents: WebContents) { this[WEBCONTENTS] = webcontents - this[IPCMAIN] = ipcMain - this[PICGO] = picgo } - /** - * for plugin showInputBox - * @param {object} options - * return type is string or '' - */ - showInputBox (options: IShowInputBoxOption) { - if (options === undefined) { - options = { - title: '', - placeholder: '' - } + private async showSettingWindow () { + const settingWindow = BrowserWindow.fromWebContents(this[WEBCONTENTS]) + if (settingWindow.isVisible()) { + return true } - this[WEBCONTENTS].send('showInputBox', options) + settingWindow.show() return new Promise((resolve, reject) => { - this[IPCMAIN].once('showInputBox', (event: Event, value: string) => { + setTimeout(() => { + resolve() + }, 1000) // TODO: a better way to wait page loaded. + }) + } + + async showInputBox (options: IShowInputBoxOption = { + title: '', + placeholder: '' + }) { + await this.showSettingWindow() + this[WEBCONTENTS].send('showInputBox', options) + return new Promise((resolve, reject) => { + ipcMain.once('showInputBox', (event: Event, value: string) => { resolve(value) }) }) } - /** - * for plugin show file explorer - * @param {object} options - */ - showFileExplorer (options: {}) { - if (options === undefined) { - options = {} - } - return new Promise((resolve, reject) => { + showFileExplorer (options: IShowFileExplorerOption = {}) { + return new Promise((resolve, reject) => { dialog.showOpenDialog(BrowserWindow.fromWebContents(this[WEBCONTENTS]), options, (filename: string) => { resolve(filename) }) }) } - /** - * for plugin to upload file - * @param {array} input - */ - async upload (input: []) { - const imgs = await new Uploader(input, this[WEBCONTENTS], this[PICGO]).upload() + async upload (input: IUploadOption) { + const imgs = await new Uploader(input, this[WEBCONTENTS]).upload() if (imgs !== false) { const pasteStyle = db.get('settings.pasteStyle') || 'markdown' let pasteText = '' @@ -88,11 +76,7 @@ class GuiApi { return [] } - /** - * For notification - * @param {Object} options - */ - showNotification (options = { + showNotification (options: IShowNotificationOption = { title: '', body: '' }) { @@ -103,17 +87,13 @@ class GuiApi { notification.show() } - /** - * - * @param {Object} options - */ - showMessageBox (options = { + showMessageBox (options: IShowMessageBoxOption = { title: '', message: '', type: 'info', buttons: ['Yes', 'No'] }) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { dialog.showMessageBox( BrowserWindow.fromWebContents(this[WEBCONTENTS]), options diff --git a/src/main/utils/logger.ts b/src/main/utils/logger.ts index 6832a6e..7572a01 100644 --- a/src/main/utils/logger.ts +++ b/src/main/utils/logger.ts @@ -15,7 +15,7 @@ class Logger { warn: IChalkType.warn, error: IChalkType.error } - protected handleLog (type: ILogType, msg: string | Error): string | Error | undefined { + protected handleLog (type: ILogType, msg: ILoggerType): ILoggerType { // if configPath is invalid then this.ctx.config === undefined // if not then check config.silent const log = chalk[this.level[type]](`[PicGo ${type.toUpperCase()}]:`) @@ -26,7 +26,7 @@ class Logger { return msg } - protected handleWriteLog (type: string, msg: string | Error): void { + protected handleWriteLog (type: string, msg: ILoggerType): void { try { const logLevel = db.get('settings.logLevel') const logPath = db.get('settings.logPath') || path.join(baseDir, './picgo.log') @@ -56,19 +56,19 @@ class Logger { } } - success (msg: string | Error): string | Error | undefined { + success (msg: ILoggerType): ILoggerType { return this.handleLog('success', msg) } - info (msg: string | Error): string | Error | undefined { + info (msg: ILoggerType): ILoggerType { return this.handleLog('info', msg) } - error (msg: string | Error): string | Error | undefined { + error (msg: ILoggerType): ILoggerType { return this.handleLog('error', msg) } - warn (msg: string | Error): string | Error | undefined { + warn (msg: ILoggerType): ILoggerType { return this.handleLog('warn', msg) } } diff --git a/src/main/utils/picgoCoreIPC.ts b/src/main/utils/picgoCoreIPC.ts index b5e53cd..4d8eeb1 100644 --- a/src/main/utils/picgoCoreIPC.ts +++ b/src/main/utils/picgoCoreIPC.ts @@ -178,7 +178,7 @@ const handlePluginActions = (ipcMain: IpcMain, CONFIG_PATH: string) => { ipcMain.on('pluginActions', (event: IpcMainEvent, name: string, label: string) => { const picgo = new PicGo(CONFIG_PATH) const plugin = picgo.pluginLoader.getPlugin(`picgo-plugin-${name}`) - const guiApi = new GuiApi(ipcMain, event.sender, picgo) + const guiApi = new GuiApi(event.sender) if (plugin.guiMenu && plugin.guiMenu(picgo).length > 0) { const menu: GuiMenuItem[] = plugin.guiMenu(picgo) menu.forEach(item => { @@ -193,7 +193,7 @@ const handlePluginActions = (ipcMain: IpcMain, CONFIG_PATH: string) => { const handleRemoveFiles = (ipcMain: IpcMain, CONFIG_PATH: string) => { ipcMain.on('removeFiles', (event: IpcMainEvent, files: ImgInfo[]) => { const picgo = new PicGo(CONFIG_PATH) - const guiApi = new GuiApi(ipcMain, event.sender, picgo) + const guiApi = new GuiApi(event.sender) setTimeout(() => { picgo.emit('remove', files, guiApi) }, 500) diff --git a/src/main/utils/shortKeyHandler.ts b/src/main/utils/shortKeyHandler.ts index ef120f2..cf1315b 100644 --- a/src/main/utils/shortKeyHandler.ts +++ b/src/main/utils/shortKeyHandler.ts @@ -1,60 +1,178 @@ import bus from './eventBus' +import PicGoCore from '~/universal/types/picgo' +import path from 'path' import { - GlobalShortcut + app, + globalShortcut, + BrowserWindow } from 'electron' -let isInModifiedMode = false // 修改快捷键模式 -bus.on('toggleShortKeyModifiedMode', flag => { - isInModifiedMode = flag -}) -/** - * - * @param {string} name - */ -const shortKeyHandler = (name: string) => { - if (isInModifiedMode) { - return - } - if (name.includes('picgo:')) { - bus.emit(name) - } else if (name.includes('picgo-plugin-')) { - // TODO: 处理插件快捷键 - } -} +import logger from './logger' +import GuiApi from './guiApi' +import db from '#/datastore' +import shortKeyService from './shortKeyService' +// eslint-disable-next-line +const requireFunc = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require -/** - * 用于更新快捷键绑定 - */ -const shortKeyUpdater = (globalShortcut: GlobalShortcut, item: IShortKeyConfig, oldKey: string) => { - // 如果提供了旧key,则解绑 - if (oldKey) { - globalShortcut.unregister(oldKey) +const PicGo = requireFunc('picgo') as typeof PicGoCore +const STORE_PATH = app.getPath('userData') +const CONFIG_PATH = path.join(STORE_PATH, '/data.json') + +class ShortKeyHandler { + private isInModifiedMode: boolean = false + constructor () { + bus.on('toggleShortKeyModifiedMode', flag => { + this.isInModifiedMode = flag + }) } - if (item.enable === false) { - globalShortcut.unregister(item.key) - } else { + init () { + this.initBuiltInShortKey() + this.initPluginsShortKey() + } + private initBuiltInShortKey () { + const commands = db.get('settings.shortKey') as IShortKeyConfigs + Object.keys(commands) + .filter(item => item.includes('picgo:')) + .map(command => { + const config = commands[command] + globalShortcut.register(config.key, () => { + this.handler(command) + }) + }) + } + private initPluginsShortKey () { + const picgo = new PicGo(CONFIG_PATH) + const pluginList = picgo.pluginLoader.getList() + for (let item of pluginList) { + const plugin = picgo.pluginLoader.getPlugin(item) + // if a plugin has commands + if (plugin && plugin.commands) { + if (typeof plugin.commands !== 'function') { + logger.warn(`${item}'s commands is not a function`) + continue + } + const commands = plugin.commands(picgo) as IPluginShortKeyConfig[] + for (let cmd of commands) { + const command = `${item}:${cmd.name}` + if (db.has(`settings.shortKey[${command}]`)) { + const commandConfig = db.get(`settings.shortKey.${command}`) as IShortKeyConfig + this.registerShortKey(commandConfig, command, cmd.handle, false) + } else { + this.registerShortKey(cmd, command, cmd.handle, true) + } + } + } else { + continue + } + } + } + private registerShortKey (config: IShortKeyConfig | IPluginShortKeyConfig, command: string, handler: IShortKeyHandler, writeFlag: boolean) { + shortKeyService.registerCommand(command, handler) + if (config.key) { + globalShortcut.register(config.key, () => { + this.handler(command) + }) + } else { + logger.warn(`${command} do not provide a key to bind`) + } + if (writeFlag) { + db.set(`settings.shortKey.${command}`, { + enable: true, + name: config.name, + label: config.label, + key: config.key + } as IShortKeyConfig) + } + } + // enable or disable shortKey + bindOrUnbindShortKey (item: IShortKeyConfig, from: string): boolean { + const command = `${from}:${item.name}` + if (item.enable === false) { + globalShortcut.unregister(item.key) + db.set(`settings.shortKey.${command}.enable`, false) + return true + } else { + if (globalShortcut.isRegistered(item.key)) { + return false + } else { + db.set(`settings.shortKey.${command}.enable`, true) + globalShortcut.register(item.key, () => { + this.handler(command) + }) + return true + } + } + } + // update shortKey bindings + updateShortKey (item: IShortKeyConfig, oldKey: string, from: string): boolean { + const command = `${from}:${item.name}` + if (globalShortcut.isRegistered(item.key)) return false + globalShortcut.unregister(oldKey) + db.set(`settings.shortKey.${command}.key`, item.key) globalShortcut.register(item.key, () => { - shortKeyHandler(item.name) + this.handler(`${from}:${item.name}`) + }) + return true + } + private async handler (command: string) { + if (this.isInModifiedMode) { + return + } + if (command.includes('picgo:')) { + bus.emit(command) + } else if (command.includes('picgo-plugin-')) { + const handler = shortKeyService.getShortKeyHandler(command) + if (handler) { + const picgo = new PicGo(CONFIG_PATH) + // make sure settingWindow is created + bus.once('createSettingWindowDone', (cmd: string, settingWindowId: number) => { + if (cmd === command) { + const webContents = BrowserWindow.fromId(settingWindowId).webContents + const guiApi = new GuiApi(webContents) + return handler(picgo, guiApi) + } + }) + bus.emit('createSettingWindow', command) + } + } else { + logger.warn(`can not find command: ${command}`) + } + } + registerPluginShortKey (pluginName: string) { + const picgo = new PicGo(CONFIG_PATH) + const plugin = picgo.pluginLoader.getPlugin(pluginName) + if (plugin && plugin.commands) { + if (typeof plugin.commands !== 'function') { + logger.warn(`${pluginName}'s commands is not a function`) + return + } + const commands = plugin.commands(picgo) as IPluginShortKeyConfig[] + for (let cmd of commands) { + const command = `${pluginName}:${cmd.name}` + if (db.has(`settings.shortKey[${command}]`)) { + const commandConfig = db.get(`settings.shortKey[${command}]`) as IShortKeyConfig + this.registerShortKey(commandConfig, command, cmd.handle, false) + } else { + this.registerShortKey(cmd, command, cmd.handle, true) + } + } + } + } + unregisterPluginShortKey (pluginName: string) { + const commands = db.get('settings.shortKey') as IShortKeyConfigs + const keyList = Object.keys(commands) + .filter(command => command.includes(pluginName)) + .map(command => { + return { + command, + key: commands[command].key + } + }) as IKeyCommandType[] + keyList.forEach(item => { + globalShortcut.unregister(item.key) + shortKeyService.unregisterCommand(item.command) + db.unset('settings.shortKey', item.command) }) } } -// 初始化阶段的注册 -const initShortKeyRegister = (globalShortcut: GlobalShortcut, shortKeys: IShortKeyConfig[]) => { - let errorList = [] - for (let i in shortKeys) { - try { - if (shortKeys[i].enable) { - globalShortcut.register(shortKeys[i].key, () => { - shortKeyHandler(shortKeys[i].name) - }) - } - } catch (e) { - errorList.push(shortKeys[i]) - } - } -} - -export { - shortKeyUpdater, - initShortKeyRegister -} +export default new ShortKeyHandler() diff --git a/src/main/utils/shortkeyService.ts b/src/main/utils/shortkeyService.ts new file mode 100644 index 0000000..4e1c66c --- /dev/null +++ b/src/main/utils/shortkeyService.ts @@ -0,0 +1,21 @@ +import logger from './logger' +class ShortKeyService { + private commandList: Map = new Map() + registerCommand (command: string, handler: IShortKeyHandler) { + this.commandList.set(command, handler) + } + unregisterCommand (command: string) { + this.commandList.delete(command) + } + getShortKeyHandler (command: string): IShortKeyHandler | null { + const handler = this.commandList.get(command) + if (handler) return handler + logger.warn(`cannot find command: ${command}`) + return null + } + getCommandList () { + return [...this.commandList.keys()] + } +} + +export default new ShortKeyService() diff --git a/src/main/utils/uploader.ts b/src/main/utils/uploader.ts index c7d9019..7244959 100644 --- a/src/main/utils/uploader.ts +++ b/src/main/utils/uploader.ts @@ -88,7 +88,7 @@ const waitForRename = (window: BrowserWindow, id: number): Promise return new Promise((resolve, reject) => { ipcMain.once(`rename${id}`, (evt: Event, newName: string) => { resolve(newName) - window.hide() + window.close() }) window.on('close', () => { resolve(null) @@ -101,7 +101,7 @@ class Uploader { private picgo: PicGoCore private webContents: WebContents private img: undefined | string[] - constructor (img: undefined | string[], webContents: WebContents, picgo: PicGoCore | undefined = undefined) { + constructor (img: IUploadOption, webContents: WebContents, picgo: PicGoCore | undefined = undefined) { this.img = img this.webContents = webContents this.picgo = picgo || new PicGo(CONFIG_PATH) diff --git a/src/renderer/layouts/SettingPage.vue b/src/renderer/layouts/SettingPage.vue index dbc0fe1..e9c25fc 100644 --- a/src/renderer/layouts/SettingPage.vue +++ b/src/renderer/layouts/SettingPage.vue @@ -171,7 +171,7 @@ export default class extends Vue { ] } os = '' - shortKey: ShortKeyMap = { + shortKey: IShortKeyMap = { upload: db.get('shortKey.upload') } picBed: IPicBedType[] = [] diff --git a/src/renderer/pages/Gallery.vue b/src/renderer/pages/Gallery.vue index 7131444..ec5e0e8 100644 --- a/src/renderer/pages/Gallery.vue +++ b/src/renderer/pages/Gallery.vue @@ -131,7 +131,7 @@ export default class extends Vue { id: null, imgUrl: '' } - choosedList: ObjT = {} + choosedList: IObjT = {} choosedPicBed: string[] = [] searchText = '' handleBarActive = false @@ -173,7 +173,7 @@ export default class extends Vue { if (this.choosedPicBed.length > 0) { let arr: ImgInfo[] = [] this.choosedPicBed.forEach(item => { - let obj: Obj = { + let obj: IObj = { type: item } if (this.searchText) { @@ -288,7 +288,7 @@ export default class extends Vue { cleanSearch () { this.searchText = '' } - isMultiple (obj: Obj) { + isMultiple (obj: IObj) { return Object.values(obj).some(item => item) } multiRemove () { diff --git a/src/renderer/pages/PicGoSetting.vue b/src/renderer/pages/PicGoSetting.vue index ee0966e..dd1f443 100644 --- a/src/renderer/pages/PicGoSetting.vue +++ b/src/renderer/pages/PicGoSetting.vue @@ -292,7 +292,7 @@ export default class extends Vue { customLink = { value: db.get('settings.customLink') || '$url' } - shortKey: ShortKeyMap = { + shortKey: IShortKeyMap = { upload: db.get('settings.shortKey.upload') } proxy = db.get('picBed.proxy') || '' @@ -492,7 +492,7 @@ export default class extends Vue { remote.shell.openExternal('https://picgo.github.io/PicGo-Doc/zh/guide/config.html#picgo设置') } goShortCutPage () { - this.$router.push('shortcut') + this.$router.push('shortKey') } beforeDestroy () { ipcRenderer.removeListener('getPicBeds', this.getPicBeds) diff --git a/src/renderer/pages/ShortCutPage.vue b/src/renderer/pages/ShortCutPage.vue deleted file mode 100644 index c43a8d5..0000000 --- a/src/renderer/pages/ShortCutPage.vue +++ /dev/null @@ -1,183 +0,0 @@ - - - diff --git a/src/renderer/pages/ShortCut.vue b/src/renderer/pages/ShortKey.vue similarity index 73% rename from src/renderer/pages/ShortCut.vue rename to src/renderer/pages/ShortKey.vue index 8ad4c79..d687c25 100644 --- a/src/renderer/pages/ShortCut.vue +++ b/src/renderer/pages/ShortKey.vue @@ -38,7 +38,7 @@ label="来源" > 编辑 @@ -96,20 +96,25 @@