diff --git a/electron/ipc-handler.ts b/electron/ipc-handler.ts index 6d2e93598..5d0fe4433 100644 --- a/electron/ipc-handler.ts +++ b/electron/ipc-handler.ts @@ -1,28 +1,11 @@ -// FRONTEND EVENTS -// --------------- -import { - app, - dialog, - globalShortcut, - ipcMain, - IpcMainEvent, - ProgressBarOptions, - shell, -} from 'electron'; -import { IPC } from './shared-with-frontend/ipc-events.const'; -import { lockscreen } from './lockscreen'; -import { errorHandlerWithFrontendInform } from './error-handler-with-frontend-inform'; -import { JiraCfg } from '../src/app/features/issue/providers/jira/jira.model'; -import { sendJiraRequest, setupRequestHeadersForImages } from './jira'; -import { KeyboardConfig } from '../src/app/features/config/keyboard-config.model'; import { log } from 'electron-log/main'; -import { exec } from 'child_process'; -import { getWin } from './main-window'; -import { quitApp, showOrFocus } from './various-shared'; -import { loadSimpleStoreAll, saveSimpleStore } from './simple-store'; -import { getIsLocked } from './shared-state'; -import { BACKUP_DIR, BACKUP_DIR_WINSTORE } from './backup'; import { pluginNodeExecutor } from './plugin-node-executor'; +import { initAppDataIpc } from './ipc-handlers/app-data'; +import { initAppControlIpc } from './ipc-handlers/app-control'; +import { initSystemIpc } from './ipc-handlers/system'; +import { initJiraIpc } from './ipc-handlers/jira'; +import { initGlobalShortcutsIpc } from './ipc-handlers/global-shortcuts'; +import { initExecIpc } from './ipc-handlers/exec'; export const initIpcInterfaces = (): void => { // Initialize plugin node executor (registers IPC handlers) @@ -32,240 +15,11 @@ export const initIpcInterfaces = (): void => { if (!pluginNodeExecutor) { log('Warning: Plugin node executor failed to initialize'); } - // HANDLER - // ------- - ipcMain.handle(IPC.GET_PATH, (ev, name: string) => { - return app.getPath(name as Parameters[0]); - }); - ipcMain.handle(IPC.GET_BACKUP_PATH, () => { - if (process?.windowsStore) { - return BACKUP_DIR_WINSTORE; - } else { - return BACKUP_DIR; - } - }); - // BACKEND EVENTS - // -------------- - // ... - - // ON EVENTS - // --------- - ipcMain.on(IPC.SHUTDOWN_NOW, quitApp); - ipcMain.on(IPC.EXIT, (ev, exitCode: number) => app.exit(exitCode)); - ipcMain.on(IPC.RELAUNCH, () => app.relaunch()); - ipcMain.on(IPC.OPEN_DEV_TOOLS, () => getWin().webContents.openDevTools()); - ipcMain.on( - IPC.SHOW_EMOJI_PANEL, - () => app.isEmojiPanelSupported() && app.showEmojiPanel(), - ); - ipcMain.on(IPC.RELOAD_MAIN_WIN, () => getWin().reload()); - ipcMain.on(IPC.OPEN_PATH, (ev, path: string) => shell.openPath(path)); - ipcMain.on(IPC.OPEN_EXTERNAL, (ev, url: string) => shell.openExternal(url)); - - ipcMain.handle(IPC.SAVE_FILE_DIALOG, async (ev, { filename, data }) => { - const result = await dialog.showSaveDialog(getWin(), { - defaultPath: filename, - filters: [ - { name: 'JSON Files', extensions: ['json'] }, - { name: 'All Files', extensions: ['*'] }, - ], - }); - - if (!result.canceled && result.filePath) { - const fs = await import('fs'); - await fs.promises.writeFile(result.filePath, data, 'utf-8'); - return { success: true, path: result.filePath }; - } - return { success: false }; - }); - - ipcMain.handle(IPC.SHARE_NATIVE, async () => { - // Desktop platforms use the share dialog instead of native share - // This allows for more flexibility and better UX with social media options - return { success: false, error: 'Native share not available on desktop' }; - }); - - ipcMain.on(IPC.LOCK_SCREEN, () => { - if (getIsLocked()) { - return; - } - - try { - lockscreen(); - } catch (e) { - errorHandlerWithFrontendInform(e); - } - }); - - ipcMain.on(IPC.SET_PROGRESS_BAR, (ev, { progress, progressBarMode }) => { - const mainWin = getWin(); - if (mainWin) { - if (progressBarMode === 'none') { - mainWin.setProgressBar(-1); - } else { - mainWin.setProgressBar(Math.min(Math.max(progress, 0), 1), { - mode: progressBarMode as ProgressBarOptions['mode'], - }); - } - } - }); - - ipcMain.on(IPC.FLASH_FRAME, (ev) => { - const mainWin = getWin(); - if (mainWin) { - mainWin.flashFrame(false); - mainWin.flashFrame(true); - - mainWin.once('focus', () => { - mainWin.flashFrame(false); - }); - } - }); - - ipcMain.on(IPC.REGISTER_GLOBAL_SHORTCUTS_EVENT, (ev, cfg) => { - registerShowAppShortCuts(cfg); - }); - - ipcMain.on(IPC.JIRA_SETUP_IMG_HEADERS, (ev, { jiraCfg }: { jiraCfg: JiraCfg }) => { - setupRequestHeadersForImages(jiraCfg); - }); - - ipcMain.on(IPC.JIRA_MAKE_REQUEST_EVENT, (ev, request) => { - sendJiraRequest(request); - }); - - ipcMain.on(IPC.SHOW_OR_FOCUS, () => { - const mainWin = getWin(); - showOrFocus(mainWin); - }); - - ipcMain.on(IPC.EXEC, execWithFrontendErrorHandlerInform); - - // eslint-disable-next-line prefer-arrow/prefer-arrow-functions - function registerShowAppShortCuts(cfg: KeyboardConfig): void { - // unregister all previous - globalShortcut.unregisterAll(); - const GLOBAL_KEY_CFG_KEYS: (keyof KeyboardConfig)[] = [ - 'globalShowHide', - 'globalToggleTaskStart', - 'globalAddNote', - 'globalAddTask', - ]; - - if (cfg) { - const mainWin = getWin(); - Object.keys(cfg) - .filter((key: string) => - GLOBAL_KEY_CFG_KEYS.includes(key as keyof KeyboardConfig), - ) - .forEach((key: string) => { - let actionFn: () => void; - const shortcut = cfg[key as keyof KeyboardConfig]; - - switch (key) { - case 'globalShowHide': - actionFn = () => { - if (mainWin.isFocused()) { - // we need to blur the window for windows - mainWin.blur(); - mainWin.hide(); - } else { - showOrFocus(mainWin); - } - }; - break; - - case 'globalToggleTaskStart': - actionFn = () => { - mainWin.webContents.send(IPC.TASK_TOGGLE_START); - }; - break; - - case 'globalAddNote': - actionFn = () => { - showOrFocus(mainWin); - mainWin.webContents.send(IPC.ADD_NOTE); - }; - break; - - case 'globalAddTask': - actionFn = () => { - showOrFocus(mainWin); - // NOTE: delay slightly to make sure app is ready - mainWin.webContents.send(IPC.SHOW_ADD_TASK_BAR); - }; - break; - - default: - actionFn = () => undefined; - } - - if (shortcut && shortcut.length > 0) { - try { - const ret = globalShortcut.register(shortcut, actionFn) as unknown; - if (!ret) { - errorHandlerWithFrontendInform( - 'Global Shortcut registration failed: ' + shortcut, - shortcut, - ); - } - } catch (e) { - errorHandlerWithFrontendInform( - 'Global Shortcut registration failed: ' + shortcut, - { e, shortcut }, - ); - } - } - }); - } - } + initAppDataIpc(); + initAppControlIpc(); + initSystemIpc(); + initJiraIpc(); + initGlobalShortcutsIpc(); + initExecIpc(); }; - -const COMMAND_MAP_PROP = 'allowedCommands'; - -// eslint-disable-next-line prefer-arrow/prefer-arrow-functions -async function execWithFrontendErrorHandlerInform( - ev: IpcMainEvent, - command: string, -): Promise { - log('trying to run command ' + command); - const existingData = await loadSimpleStoreAll(); - const allowedCommands: string[] = (existingData[COMMAND_MAP_PROP] as string[]) || []; - - if (!Array.isArray(allowedCommands)) { - throw new Error('allowedCommands is no array ???'); - } - if (allowedCommands.includes(command)) { - exec(command, (err) => { - if (err) { - errorHandlerWithFrontendInform(err); - } - }); - } else { - const mainWin = getWin(); - const res = await dialog.showMessageBox(mainWin, { - type: 'question', - buttons: ['Cancel', 'Yes, execute!'], - defaultId: 2, - title: 'Super Productivity – Exec', - message: - 'Do you want to execute this command? ONLY confirm if you are sure you know what you are doing!!', - detail: command, - checkboxLabel: 'Remember my answer', - checkboxChecked: true, - }); - const { response, checkboxChecked } = res; - - if (response === 1) { - if (checkboxChecked) { - await saveSimpleStore(COMMAND_MAP_PROP, [...allowedCommands, command]); - } - exec(command, (err) => { - if (err) { - errorHandlerWithFrontendInform(err); - } - }); - } - } -} diff --git a/electron/ipc-handlers/app-control.ts b/electron/ipc-handlers/app-control.ts new file mode 100644 index 000000000..31be62de9 --- /dev/null +++ b/electron/ipc-handlers/app-control.ts @@ -0,0 +1,57 @@ +import { app, ipcMain, ProgressBarOptions } from 'electron'; +import { IPC } from '../shared-with-frontend/ipc-events.const'; +import { getWin } from '../main-window'; +import { quitApp, showOrFocus } from '../various-shared'; +import { getIsLocked } from '../shared-state'; +import { lockscreen } from '../lockscreen'; +import { errorHandlerWithFrontendInform } from '../error-handler-with-frontend-inform'; + +export const initAppControlIpc = (): void => { + ipcMain.on(IPC.SHUTDOWN_NOW, quitApp); + ipcMain.on(IPC.EXIT, (ev, exitCode: number) => app.exit(exitCode)); + ipcMain.on(IPC.RELAUNCH, () => app.relaunch()); + ipcMain.on(IPC.OPEN_DEV_TOOLS, () => getWin().webContents.openDevTools()); + ipcMain.on(IPC.RELOAD_MAIN_WIN, () => getWin().reload()); + + ipcMain.on(IPC.SHOW_OR_FOCUS, () => { + const mainWin = getWin(); + showOrFocus(mainWin); + }); + + ipcMain.on(IPC.LOCK_SCREEN, () => { + if (getIsLocked()) { + return; + } + + try { + lockscreen(); + } catch (e) { + errorHandlerWithFrontendInform(e); + } + }); + + ipcMain.on(IPC.SET_PROGRESS_BAR, (ev, { progress, progressBarMode }) => { + const mainWin = getWin(); + if (mainWin) { + if (progressBarMode === 'none') { + mainWin.setProgressBar(-1); + } else { + mainWin.setProgressBar(Math.min(Math.max(progress, 0), 1), { + mode: progressBarMode as ProgressBarOptions['mode'], + }); + } + } + }); + + ipcMain.on(IPC.FLASH_FRAME, (ev) => { + const mainWin = getWin(); + if (mainWin) { + mainWin.flashFrame(false); + mainWin.flashFrame(true); + + mainWin.once('focus', () => { + mainWin.flashFrame(false); + }); + } + }); +}; diff --git a/electron/ipc-handlers/app-data.ts b/electron/ipc-handlers/app-data.ts new file mode 100644 index 000000000..a415dca89 --- /dev/null +++ b/electron/ipc-handlers/app-data.ts @@ -0,0 +1,17 @@ +import { app, ipcMain } from 'electron'; +import { IPC } from '../shared-with-frontend/ipc-events.const'; +import { BACKUP_DIR, BACKUP_DIR_WINSTORE } from '../backup'; + +export const initAppDataIpc = (): void => { + ipcMain.handle(IPC.GET_PATH, (ev, name: string) => { + return app.getPath(name as Parameters[0]); + }); + + ipcMain.handle(IPC.GET_BACKUP_PATH, () => { + if (process?.windowsStore) { + return BACKUP_DIR_WINSTORE; + } else { + return BACKUP_DIR; + } + }); +}; diff --git a/electron/ipc-handlers/exec.ts b/electron/ipc-handlers/exec.ts new file mode 100644 index 000000000..30e23d0ba --- /dev/null +++ b/electron/ipc-handlers/exec.ts @@ -0,0 +1,58 @@ +import { dialog, ipcMain, IpcMainEvent } from 'electron'; +import { IPC } from '../shared-with-frontend/ipc-events.const'; +import { exec } from 'child_process'; +import { log } from 'electron-log/main'; +import { loadSimpleStoreAll, saveSimpleStore } from '../simple-store'; +import { getWin } from '../main-window'; +import { errorHandlerWithFrontendInform } from '../error-handler-with-frontend-inform'; + +const COMMAND_MAP_PROP = 'allowedCommands'; + +export const initExecIpc = (): void => { + ipcMain.on(IPC.EXEC, execWithFrontendErrorHandlerInform); +}; + +const execWithFrontendErrorHandlerInform = async ( + ev: IpcMainEvent, + command: string, +): Promise => { + log('trying to run command ' + command); + const existingData = await loadSimpleStoreAll(); + const allowedCommands: string[] = (existingData[COMMAND_MAP_PROP] as string[]) || []; + + if (!Array.isArray(allowedCommands)) { + throw new Error('allowedCommands is no array ???'); + } + if (allowedCommands.includes(command)) { + exec(command, (err) => { + if (err) { + errorHandlerWithFrontendInform(err); + } + }); + } else { + const mainWin = getWin(); + const res = await dialog.showMessageBox(mainWin, { + type: 'question', + buttons: ['Cancel', 'Yes, execute!'], + defaultId: 2, + title: 'Super Productivity – Exec', + message: + 'Do you want to execute this command? ONLY confirm if you are sure you know what you are doing!!', + detail: command, + checkboxLabel: 'Remember my answer', + checkboxChecked: true, + }); + const { response, checkboxChecked } = res; + + if (response === 1) { + if (checkboxChecked) { + await saveSimpleStore(COMMAND_MAP_PROP, [...allowedCommands, command]); + } + exec(command, (err) => { + if (err) { + errorHandlerWithFrontendInform(err); + } + }); + } + } +}; diff --git a/electron/ipc-handlers/global-shortcuts.ts b/electron/ipc-handlers/global-shortcuts.ts new file mode 100644 index 000000000..386206c92 --- /dev/null +++ b/electron/ipc-handlers/global-shortcuts.ts @@ -0,0 +1,88 @@ +import { globalShortcut, ipcMain } from 'electron'; +import { IPC } from '../shared-with-frontend/ipc-events.const'; +import { KeyboardConfig } from '../../src/app/features/config/keyboard-config.model'; +import { getWin } from '../main-window'; +import { showOrFocus } from '../various-shared'; +import { errorHandlerWithFrontendInform } from '../error-handler-with-frontend-inform'; + +export const initGlobalShortcutsIpc = (): void => { + ipcMain.on(IPC.REGISTER_GLOBAL_SHORTCUTS_EVENT, (ev, cfg) => { + registerShowAppShortCuts(cfg); + }); +}; + +const registerShowAppShortCuts = (cfg: KeyboardConfig): void => { + // unregister all previous + globalShortcut.unregisterAll(); + const GLOBAL_KEY_CFG_KEYS: (keyof KeyboardConfig)[] = [ + 'globalShowHide', + 'globalToggleTaskStart', + 'globalAddNote', + 'globalAddTask', + ]; + + if (cfg) { + const mainWin = getWin(); + Object.keys(cfg) + .filter((key: string) => GLOBAL_KEY_CFG_KEYS.includes(key as keyof KeyboardConfig)) + .forEach((key: string) => { + let actionFn: () => void; + const shortcut = cfg[key as keyof KeyboardConfig]; + + switch (key) { + case 'globalShowHide': + actionFn = () => { + if (mainWin.isFocused()) { + // we need to blur the window for windows + mainWin.blur(); + mainWin.hide(); + } else { + showOrFocus(mainWin); + } + }; + break; + + case 'globalToggleTaskStart': + actionFn = () => { + mainWin.webContents.send(IPC.TASK_TOGGLE_START); + }; + break; + + case 'globalAddNote': + actionFn = () => { + showOrFocus(mainWin); + mainWin.webContents.send(IPC.ADD_NOTE); + }; + break; + + case 'globalAddTask': + actionFn = () => { + showOrFocus(mainWin); + // NOTE: delay slightly to make sure app is ready + mainWin.webContents.send(IPC.SHOW_ADD_TASK_BAR); + }; + break; + + default: + actionFn = () => undefined; + } + + if (shortcut && shortcut.length > 0) { + try { + const ret = globalShortcut.register(shortcut, actionFn) as unknown; + if (!ret) { + errorHandlerWithFrontendInform( + 'Global Shortcut registration failed: ' + shortcut, + shortcut, + ); + } + } catch (e) { + errorHandlerWithFrontendInform( + 'Global Shortcut registration failed: ' + shortcut, + { e, shortcut }, + ); + } + } + }); + } +}; diff --git a/electron/ipc-handlers/jira.ts b/electron/ipc-handlers/jira.ts new file mode 100644 index 000000000..ac8bb527a --- /dev/null +++ b/electron/ipc-handlers/jira.ts @@ -0,0 +1,14 @@ +import { ipcMain } from 'electron'; +import { IPC } from '../shared-with-frontend/ipc-events.const'; +import { JiraCfg } from '../../src/app/features/issue/providers/jira/jira.model'; +import { sendJiraRequest, setupRequestHeadersForImages } from '../jira'; + +export const initJiraIpc = (): void => { + ipcMain.on(IPC.JIRA_SETUP_IMG_HEADERS, (ev, { jiraCfg }: { jiraCfg: JiraCfg }) => { + setupRequestHeadersForImages(jiraCfg); + }); + + ipcMain.on(IPC.JIRA_MAKE_REQUEST_EVENT, (ev, request) => { + sendJiraRequest(request); + }); +}; diff --git a/electron/ipc-handlers/system.ts b/electron/ipc-handlers/system.ts new file mode 100644 index 000000000..9984f73e3 --- /dev/null +++ b/electron/ipc-handlers/system.ts @@ -0,0 +1,36 @@ +import { app, dialog, ipcMain, shell } from 'electron'; +import { IPC } from '../shared-with-frontend/ipc-events.const'; +import { getWin } from '../main-window'; + +export const initSystemIpc = (): void => { + ipcMain.on(IPC.OPEN_PATH, (ev, path: string) => shell.openPath(path)); + ipcMain.on(IPC.OPEN_EXTERNAL, (ev, url: string) => shell.openExternal(url)); + + ipcMain.on( + IPC.SHOW_EMOJI_PANEL, + () => app.isEmojiPanelSupported() && app.showEmojiPanel(), + ); + + ipcMain.handle(IPC.SAVE_FILE_DIALOG, async (ev, { filename, data }) => { + const result = await dialog.showSaveDialog(getWin(), { + defaultPath: filename, + filters: [ + { name: 'JSON Files', extensions: ['json'] }, + { name: 'All Files', extensions: ['*'] }, + ], + }); + + if (!result.canceled && result.filePath) { + const fs = await import('fs'); + await fs.promises.writeFile(result.filePath, data, 'utf-8'); + return { success: true, path: result.filePath }; + } + return { success: false }; + }); + + ipcMain.handle(IPC.SHARE_NATIVE, async () => { + // Desktop platforms use the share dialog instead of native share + // This allows for more flexibility and better UX with social media options + return { success: false, error: 'Native share not available on desktop' }; + }); +};