Merge remote-tracking branch 'origin/master'

* origin/master:
  feat(i18n): add export task list string to localization files
  feat(ipc): improve error handling and streamline IPC initialization
  feat(startup): refactor startup logic and move initialization to StartupService
  feat(ipc): implement IPC handlers for app control, data, exec, global shortcuts, system, and Jira
  feat: add placeholder text support to daily summary and inline markdown components
  feat: don't install api test plugin
  chore(deps): bump the npm_and_yarn group across 4 directories with 1 update

# Conflicts:
#	electron/ipc-handler.ts
#	src/app/app.component.ts
This commit is contained in:
Johannes Millan 2025-11-28 20:28:46 +01:00
commit 5bd59e2760
20 changed files with 809 additions and 688 deletions

View file

@ -1,34 +1,13 @@
// 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,
setIsMinimizeToTray,
setIsTrayShowCurrentTask,
setIsTrayShowCurrentCountdown,
} from './shared-state';
import { BACKUP_DIR, BACKUP_DIR_WINSTORE } from './backup';
import { pluginNodeExecutor } from './plugin-node-executor';
import { GlobalConfigState } from '../src/app/features/config/global-config.model';
import {
initAppControlIpc,
initAppDataIpc,
initExecIpc,
initGlobalShortcutsIpc,
initJiraIpc,
initSystemIpc,
} from './ipc-handlers';
export const initIpcInterfaces = (): void => {
// Initialize plugin node executor (registers IPC handlers)
@ -38,245 +17,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<typeof app.getPath>[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.on(IPC.TRANSFER_SETTINGS_TO_ELECTRON, (ev, cfg: GlobalConfigState) => {
setIsMinimizeToTray(cfg.misc.isMinimizeToTray);
setIsTrayShowCurrentTask(cfg.misc.isTrayShowCurrentTask);
setIsTrayShowCurrentCountdown(cfg.misc.isTrayShowCurrentCountdown);
});
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<void> {
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);
}
});
}
}
}

View file

@ -0,0 +1,69 @@
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,
setIsMinimizeToTray,
setIsTrayShowCurrentTask,
setIsTrayShowCurrentCountdown,
} from '../shared-state';
import { lockscreen } from '../lockscreen';
import { errorHandlerWithFrontendInform } from '../error-handler-with-frontend-inform';
import { GlobalConfigState } from '../../src/app/features/config/global-config.model';
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.TRANSFER_SETTINGS_TO_ELECTRON, (ev, cfg: GlobalConfigState) => {
setIsMinimizeToTray(cfg.misc.isMinimizeToTray);
setIsTrayShowCurrentTask(cfg.misc.isTrayShowCurrentTask);
setIsTrayShowCurrentCountdown(cfg.misc.isTrayShowCurrentCountdown);
});
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);
});
}
});
};

View file

@ -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<typeof app.getPath>[0]);
});
ipcMain.handle(IPC.GET_BACKUP_PATH, () => {
if (process?.windowsStore) {
return BACKUP_DIR_WINSTORE;
} else {
return BACKUP_DIR;
}
});
};

View file

@ -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<void> => {
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('Invalid configuration: allowedCommands must be an 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);
}
});
}
}
};

View file

@ -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 },
);
}
}
});
}
};

View file

@ -0,0 +1,6 @@
export { initAppControlIpc } from './app-control';
export { initAppDataIpc } from './app-data';
export { initExecIpc } from './exec';
export { initGlobalShortcutsIpc } from './global-shortcuts';
export { initJiraIpc } from './jira';
export { initSystemIpc } from './system';

View file

@ -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);
});
};

View file

@ -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' };
});
};