mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
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:
commit
5bd59e2760
20 changed files with 809 additions and 688 deletions
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
69
electron/ipc-handlers/app-control.ts
Normal file
69
electron/ipc-handlers/app-control.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
17
electron/ipc-handlers/app-data.ts
Normal file
17
electron/ipc-handlers/app-data.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
};
|
||||
58
electron/ipc-handlers/exec.ts
Normal file
58
electron/ipc-handlers/exec.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
88
electron/ipc-handlers/global-shortcuts.ts
Normal file
88
electron/ipc-handlers/global-shortcuts.ts
Normal 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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
6
electron/ipc-handlers/index.ts
Normal file
6
electron/ipc-handlers/index.ts
Normal 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';
|
||||
14
electron/ipc-handlers/jira.ts
Normal file
14
electron/ipc-handlers/jira.ts
Normal 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);
|
||||
});
|
||||
};
|
||||
36
electron/ipc-handlers/system.ts
Normal file
36
electron/ipc-handlers/system.ts
Normal 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' };
|
||||
});
|
||||
};
|
||||
|
|
@ -72,6 +72,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
|
||||
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
|
|
@ -1083,6 +1084,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.33.tgz",
|
||||
"integrity": "sha512-wzoocdnnpSxZ+6CjW4ADCK1jVmd1S/J3ArNWfn8FDDQtRm8dkDg7TA+mvek2wNrfCgwuZxqEOiB9B1XCJ6+dbw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
|
|
@ -1273,6 +1275,7 @@
|
|||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001718",
|
||||
"electron-to-chromium": "^1.5.160",
|
||||
|
|
@ -1608,10 +1611,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
|
|
@ -1960,6 +1964,7 @@
|
|||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -2120,6 +2125,7 @@
|
|||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz",
|
||||
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
|
|
@ -2174,6 +2180,7 @@
|
|||
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.7.tgz",
|
||||
"integrity": "sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.0",
|
||||
"seroval": "~1.3.0",
|
||||
|
|
@ -2425,6 +2432,7 @@
|
|||
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
|
||||
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
|
|
@ -477,6 +478,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
|
|
@ -520,6 +522,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -1575,6 +1578,7 @@
|
|||
"integrity": "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"solid-js": "^1.8.6"
|
||||
}
|
||||
|
|
@ -1715,17 +1719,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.15.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.33.tgz",
|
||||
"integrity": "sha512-wzoocdnnpSxZ+6CjW4ADCK1jVmd1S/J3ArNWfn8FDDQtRm8dkDg7TA+mvek2wNrfCgwuZxqEOiB9B1XCJ6+dbw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz",
|
||||
|
|
@ -1762,6 +1755,7 @@
|
|||
"integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.48.0",
|
||||
"@typescript-eslint/types": "8.48.0",
|
||||
|
|
@ -2102,6 +2096,7 @@
|
|||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -2358,6 +2353,7 @@
|
|||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001726",
|
||||
"electron-to-chromium": "^1.5.173",
|
||||
|
|
@ -2773,6 +2769,7 @@
|
|||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
|
|
@ -2833,6 +2830,7 @@
|
|||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
|
|
@ -3231,10 +3229,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
|
|
@ -4031,6 +4030,7 @@
|
|||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -4082,6 +4082,7 @@
|
|||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
|
|
@ -4325,6 +4326,7 @@
|
|||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz",
|
||||
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
|
|
@ -4387,6 +4389,7 @@
|
|||
"integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.0",
|
||||
"seroval": "~1.3.0",
|
||||
|
|
@ -4746,6 +4749,7 @@
|
|||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -4759,8 +4763,7 @@
|
|||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.3",
|
||||
|
|
@ -4820,6 +4823,7 @@
|
|||
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
|
|||
|
|
@ -12,11 +12,12 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@solidjs/router": "^0.14.10",
|
||||
"@super-productivity/vite-plugin": "file:../../vite-plugin",
|
||||
"@types/node": "^22.15.33",
|
||||
"archiver": "^7.0.1",
|
||||
"solid-js": "^1.9.7",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^7.1.12",
|
||||
"vite": "^7.2.0",
|
||||
"vite-plugin-solid": "^2.11.10"
|
||||
}
|
||||
},
|
||||
|
|
@ -31,6 +32,19 @@
|
|||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"../../vite-plugin": {
|
||||
"name": "@super-productivity/vite-plugin",
|
||||
"version": "1.0.0",
|
||||
"dev": true,
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
|
|
@ -62,6 +76,7 @@
|
|||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
|
|
@ -1150,6 +1165,10 @@
|
|||
"resolved": "../../plugin-api",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@super-productivity/vite-plugin": {
|
||||
"resolved": "../../vite-plugin",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
|
|
@ -1208,6 +1227,7 @@
|
|||
"integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
|
|
@ -1443,6 +1463,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.19",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
|
|
@ -1815,9 +1836,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
|
@ -2181,6 +2202,7 @@
|
|||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -2353,6 +2375,7 @@
|
|||
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
|
|
@ -2412,6 +2435,7 @@
|
|||
"integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.0",
|
||||
"seroval": "~1.3.0",
|
||||
|
|
@ -2668,9 +2692,12 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.1.12",
|
||||
"version": "7.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz",
|
||||
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
|
|||
14
packages/plugin-dev/sync-md/package-lock.json
generated
14
packages/plugin-dev/sync-md/package-lock.json
generated
|
|
@ -96,6 +96,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
|
||||
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
|
|
@ -604,6 +605,7 @@
|
|||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
|
|
@ -626,6 +628,7 @@
|
|||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -2302,6 +2305,7 @@
|
|||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001718",
|
||||
"electron-to-chromium": "^1.5.160",
|
||||
|
|
@ -3104,10 +3108,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
|
|
@ -3478,6 +3483,7 @@
|
|||
"resolved": "https://registry.npmjs.org/jest/-/jest-30.0.4.tgz",
|
||||
"integrity": "sha512-9QE0RS4WwTj/TtTC4h/eFVmFAhGNVerSB9XpJh8sqaXlP73ILcPcZ7JWjjEtJJe2m8QyBLKKfPQuK+3F+Xij/g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "30.0.4",
|
||||
"@jest/types": "30.0.1",
|
||||
|
|
@ -4087,6 +4093,7 @@
|
|||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
|
||||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
|
|
@ -5344,6 +5351,7 @@
|
|||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
|
|||
|
|
@ -14,11 +14,9 @@ import {
|
|||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { ChromeExtensionInterfaceService } from './core/chrome-extension-interface/chrome-extension-interface.service';
|
||||
import { ShortcutService } from './core-ui/shortcut/shortcut.service';
|
||||
import { GlobalConfigService } from './features/config/global-config.service';
|
||||
import { LayoutService } from './core-ui/layout/layout.service';
|
||||
import { IPC } from '../../electron/shared-with-frontend/ipc-events.const';
|
||||
import { SnackService } from './core/snack/snack.service';
|
||||
import { IS_ELECTRON } from './app.constants';
|
||||
import { expandAnimation } from './ui/animations/expand.ani';
|
||||
|
|
@ -30,17 +28,11 @@ import { LS } from './core/persistence/storage-keys.const';
|
|||
import { BannerId } from './core/banner/banner.model';
|
||||
import { T } from './t.const';
|
||||
import { GlobalThemeService } from './core/theme/global-theme.service';
|
||||
import { UiHelperService } from './features/ui-helper/ui-helper.service';
|
||||
import { LanguageService } from './core/language/language.service';
|
||||
import { WorkContextService } from './features/work-context/work-context.service';
|
||||
import { ImexViewService } from './imex/imex-meta/imex-view.service';
|
||||
import { IS_ANDROID_WEB_VIEW } from './util/is-android-web-view';
|
||||
import { isOnline$ } from './util/is-online';
|
||||
import { SyncTriggerService } from './imex/sync/sync-trigger.service';
|
||||
import { SyncWrapperService } from './imex/sync/sync-wrapper.service';
|
||||
import { environment } from '../environments/environment';
|
||||
import { ActivatedRoute, RouterOutlet } from '@angular/router';
|
||||
import { TrackingReminderService } from './features/tracking-reminder/tracking-reminder.service';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { IS_MOBILE } from './util/is-mobile';
|
||||
import { warpAnimation, warpInAnimation } from './ui/animations/warp.ani';
|
||||
|
|
@ -56,24 +48,10 @@ import { AsyncPipe, DOCUMENT } from '@angular/common';
|
|||
import { RightPanelComponent } from './features/right-panel/right-panel.component';
|
||||
import { selectIsOverlayShown } from './features/focus-mode/store/focus-mode.selectors';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { PfapiService } from './pfapi/pfapi.service';
|
||||
import { PersistenceLegacyService } from './core/persistence/persistence-legacy.service';
|
||||
import { download } from './util/download';
|
||||
import { PersistenceLocalService } from './core/persistence/persistence-local.service';
|
||||
import { SyncStatus } from './pfapi/api';
|
||||
import { LocalBackupService } from './imex/local-backup/local-backup.service';
|
||||
import { DEFAULT_META_MODEL } from './pfapi/api/model-ctrl/meta-model-ctrl';
|
||||
import { AppDataCompleteNew } from './pfapi/pfapi-config';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { DialogPleaseRateComponent } from './features/dialog-please-rate/dialog-please-rate.component';
|
||||
import { getDbDateStr } from './util/get-db-date-str';
|
||||
import { PluginService } from './plugins/plugin.service';
|
||||
import { MarkdownPasteService } from './features/tasks/markdown-paste.service';
|
||||
import { TaskService } from './features/tasks/task.service';
|
||||
import { IpcRendererEvent } from 'electron';
|
||||
import { SyncSafetyBackupService } from './imex/sync/sync-safety-backup.service';
|
||||
import { Log } from './core/log';
|
||||
import { MatMenuItem } from '@angular/material/menu';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { DialogUnsplashPickerComponent } from './ui/dialog-unsplash-picker/dialog-unsplash-picker.component';
|
||||
|
|
@ -84,11 +62,7 @@ import { ContextMenuComponent } from './ui/context-menu/context-menu.component';
|
|||
import { WorkContextThemeCfg } from './features/work-context/work-context.model';
|
||||
import { isInputElement } from './util/dom-element';
|
||||
import { MobileBottomNavComponent } from './core-ui/mobile-bottom-nav/mobile-bottom-nav.component';
|
||||
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
prompt(): Promise<void>;
|
||||
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
|
||||
}
|
||||
import { StartupService } from './core/startup/startup.service';
|
||||
|
||||
const w = window as Window & { productivityTips?: string[][]; randomIndex?: number };
|
||||
const productivityTip: string[] | undefined =
|
||||
|
|
@ -96,6 +70,11 @@ const productivityTip: string[] | undefined =
|
|||
? w.productivityTips[w.randomIndex]
|
||||
: undefined;
|
||||
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
prompt(): Promise<void>;
|
||||
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
|
|
@ -128,36 +107,23 @@ const productivityTip: string[] | undefined =
|
|||
],
|
||||
})
|
||||
export class AppComponent implements OnDestroy, AfterViewInit {
|
||||
private _translateService = inject(TranslateService);
|
||||
|
||||
private _globalConfigService = inject(GlobalConfigService);
|
||||
private _shortcutService = inject(ShortcutService);
|
||||
private _bannerService = inject(BannerService);
|
||||
private _snackService = inject(SnackService);
|
||||
private _chromeExtensionInterfaceService = inject(ChromeExtensionInterfaceService);
|
||||
private _globalThemeService = inject(GlobalThemeService);
|
||||
private _uiHelperService = inject(UiHelperService);
|
||||
private _languageService = inject(LanguageService);
|
||||
private _startTrackingReminderService = inject(TrackingReminderService);
|
||||
private _activatedRoute = inject(ActivatedRoute);
|
||||
private _pfapiService = inject(PfapiService);
|
||||
private _persistenceLegacyService = inject(PersistenceLegacyService);
|
||||
private _persistenceLocalService = inject(PersistenceLocalService);
|
||||
private _localBackupService = inject(LocalBackupService);
|
||||
private _matDialog = inject(MatDialog);
|
||||
private _markdownPasteService = inject(MarkdownPasteService);
|
||||
private _taskService = inject(TaskService);
|
||||
private _pluginService = inject(PluginService);
|
||||
private _syncWrapperService = inject(SyncWrapperService);
|
||||
private _projectService = inject(ProjectService);
|
||||
private _tagService = inject(TagService);
|
||||
private _destroyRef = inject(DestroyRef);
|
||||
private _noteStartupBannerService = inject(NoteStartupBannerService);
|
||||
private _ngZone = inject(NgZone);
|
||||
private _document = inject(DOCUMENT, { optional: true });
|
||||
|
||||
// needs to be imported for initialization
|
||||
private _syncSafetyBackupService = inject(SyncSafetyBackupService);
|
||||
private _startupService = inject(StartupService);
|
||||
|
||||
readonly syncTriggerService = inject(SyncTriggerService);
|
||||
readonly imexMetaService = inject(ImexViewService);
|
||||
|
|
@ -207,7 +173,7 @@ export class AppComponent implements OnDestroy, AfterViewInit {
|
|||
private _intervalTimer?: NodeJS.Timeout;
|
||||
|
||||
constructor() {
|
||||
this._checkMigrationAndInitBackups();
|
||||
this._startupService.init();
|
||||
|
||||
// Use effect to react to language RTL changes
|
||||
effect(() => {
|
||||
|
|
@ -227,118 +193,11 @@ export class AppComponent implements OnDestroy, AfterViewInit {
|
|||
// init theme and body class handlers
|
||||
this._globalThemeService.init();
|
||||
|
||||
// basically init
|
||||
this._requestPersistence();
|
||||
|
||||
this.syncTriggerService.afterInitialSyncDoneAndDataLoadedInitially$
|
||||
.pipe(take(1))
|
||||
.subscribe(() => {
|
||||
void this._noteStartupBannerService.showLastNoteIfNeeded();
|
||||
});
|
||||
|
||||
// deferred init
|
||||
window.setTimeout(async () => {
|
||||
this._startTrackingReminderService.init();
|
||||
this._checkAvailableStorage();
|
||||
// init offline banner in lack of a better place for it
|
||||
this._initOfflineBanner();
|
||||
|
||||
const miscCfg = this._globalConfigService.misc();
|
||||
if (miscCfg?.isShowProductivityTipLonger && !this._isTourLikelyToBeShown()) {
|
||||
this._snackService.open({
|
||||
ico: 'lightbulb',
|
||||
config: {
|
||||
duration: 16000,
|
||||
},
|
||||
msg:
|
||||
'<strong>' +
|
||||
w.productivityTips![w.randomIndex!][0] +
|
||||
':</strong> ' +
|
||||
w.productivityTips![w.randomIndex!][1],
|
||||
});
|
||||
}
|
||||
|
||||
const appStarts = +(localStorage.getItem(LS.APP_START_COUNT) || 0);
|
||||
const lastStartDay = localStorage.getItem(LS.APP_START_COUNT_LAST_START_DAY);
|
||||
const todayStr = getDbDateStr();
|
||||
if (appStarts === 32 || appStarts === 96) {
|
||||
this._matDialog.open(DialogPleaseRateComponent);
|
||||
localStorage.setItem(LS.APP_START_COUNT, (appStarts + 1).toString());
|
||||
}
|
||||
if (lastStartDay !== todayStr) {
|
||||
localStorage.setItem(LS.APP_START_COUNT, (appStarts + 1).toString());
|
||||
localStorage.setItem(LS.APP_START_COUNT_LAST_START_DAY, todayStr);
|
||||
}
|
||||
|
||||
// Initialize plugin system
|
||||
try {
|
||||
// Wait for sync to complete before initializing plugins to avoid DB lock conflicts
|
||||
await this._syncWrapperService.afterCurrentSyncDoneOrSyncDisabled$
|
||||
.pipe(take(1))
|
||||
.toPromise();
|
||||
await this._pluginService.initializePlugins();
|
||||
Log.log('Plugin system initialized after sync completed');
|
||||
} catch (error) {
|
||||
Log.err('Failed to initialize plugin system:', error);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
if (IS_ELECTRON) {
|
||||
window.ea.informAboutAppReady();
|
||||
|
||||
// Push initial settings to Electron immediately to avoid tray/title races
|
||||
const initialCfg = this._globalConfigService.cfg();
|
||||
if (initialCfg) {
|
||||
window.ea.sendAppSettingsToElectron(initialCfg);
|
||||
}
|
||||
|
||||
// Initialize electron error handler in an effect
|
||||
effect(() => {
|
||||
window.ea.on(IPC.ERROR, (ev: IpcRendererEvent, ...args: unknown[]) => {
|
||||
const data = args[0] as {
|
||||
error: unknown;
|
||||
stack: unknown;
|
||||
errorStr: string | unknown;
|
||||
};
|
||||
const errMsg =
|
||||
typeof data.errorStr === 'string' ? data.errorStr : ' INVALID ERROR MSG :( ';
|
||||
|
||||
this._snackService.open({
|
||||
msg: errMsg,
|
||||
type: 'ERROR',
|
||||
isSkipTranslate: true,
|
||||
});
|
||||
Log.err(data);
|
||||
});
|
||||
});
|
||||
|
||||
// Sync settings to electron on change
|
||||
effect(() => {
|
||||
const cfg = this._globalConfigService.cfg();
|
||||
if (cfg) {
|
||||
window.ea.sendAppSettingsToElectron(cfg);
|
||||
}
|
||||
});
|
||||
|
||||
this._uiHelperService.initElectron();
|
||||
} else {
|
||||
// WEB VERSION
|
||||
window.addEventListener('beforeunload', (e) => {
|
||||
const gCfg = this._globalConfigService.cfg();
|
||||
if (!gCfg) {
|
||||
throw new Error();
|
||||
}
|
||||
if (gCfg.misc.isConfirmBeforeExit) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
}
|
||||
});
|
||||
|
||||
if (!IS_ANDROID_WEB_VIEW) {
|
||||
this._chromeExtensionInterfaceService.init();
|
||||
this._initMultiInstanceWarning();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('document:paste', ['$event']) onPaste(ev: ClipboardEvent): void {
|
||||
|
|
@ -526,212 +385,6 @@ export class AppComponent implements OnDestroy, AfterViewInit {
|
|||
if (this._intervalTimer) clearInterval(this._intervalTimer);
|
||||
}
|
||||
|
||||
private async _checkMigrationAndInitBackups(): Promise<void> {
|
||||
const MIGRATED_VAL = 42;
|
||||
const lastLocalSyncModelChange =
|
||||
await this._persistenceLocalService.loadLastSyncModelChange();
|
||||
// CHECK AND DO MIGRATION
|
||||
// ---------------------
|
||||
if (
|
||||
typeof lastLocalSyncModelChange === 'number' &&
|
||||
lastLocalSyncModelChange > MIGRATED_VAL
|
||||
) {
|
||||
// disable sync until reload
|
||||
this._pfapiService.pf.sync = () => Promise.resolve({ status: SyncStatus.InSync });
|
||||
this.imexMetaService.setDataImportInProgress(true);
|
||||
|
||||
const legacyData = await this._persistenceLegacyService.loadComplete();
|
||||
Log.log({ legacyData: legacyData });
|
||||
|
||||
alert(this._translateService.instant(T.MIGRATE.DETECTED_LEGACY));
|
||||
|
||||
if (
|
||||
!IS_ANDROID_WEB_VIEW &&
|
||||
confirm(this._translateService.instant(T.MIGRATE.C_DOWNLOAD_BACKUP))
|
||||
) {
|
||||
download('sp-legacy-backup.json', JSON.stringify(legacyData));
|
||||
}
|
||||
try {
|
||||
await this._pfapiService.importCompleteBackup(
|
||||
legacyData as unknown as AppDataCompleteNew,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
this.imexMetaService.setDataImportInProgress(true);
|
||||
await this._persistenceLocalService.updateLastSyncModelChange(MIGRATED_VAL);
|
||||
|
||||
alert(this._translateService.instant(T.MIGRATE.SUCCESS));
|
||||
|
||||
if (IS_ELECTRON) {
|
||||
window.ea.relaunch();
|
||||
// if relaunch fails we hard close the app
|
||||
window.setTimeout(() => window.ea.exit(1234), 1000);
|
||||
}
|
||||
window.location.reload();
|
||||
// fallback
|
||||
window.setTimeout(
|
||||
() => alert(this._translateService.instant(T.MIGRATE.E_RESTART_FAILED)),
|
||||
2000,
|
||||
);
|
||||
} catch (error) {
|
||||
// prevent any interaction with the app on after failure
|
||||
this.imexMetaService.setDataImportInProgress(true);
|
||||
Log.err(error);
|
||||
|
||||
try {
|
||||
alert(
|
||||
this._translateService.instant(T.MIGRATE.E_MIGRATION_FAILED) +
|
||||
'\n\n' +
|
||||
JSON.stringify(
|
||||
(error as { additionalLog?: Array<{ errors: unknown }> })
|
||||
.additionalLog?.[0]?.errors,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
alert(
|
||||
this._translateService.instant(T.MIGRATE.E_MIGRATION_FAILED) +
|
||||
'\n\n' +
|
||||
error?.toString(),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// if everything is normal, check for TMP stray backup
|
||||
await this._pfapiService.isCheckForStrayLocalTmpDBBackupAndImport();
|
||||
|
||||
// if completely fresh instance check for local backups
|
||||
if (IS_ELECTRON || IS_ANDROID_WEB_VIEW) {
|
||||
const meta = await this._pfapiService.pf.metaModel.load();
|
||||
if (!meta || meta.lastUpdate === DEFAULT_META_MODEL.lastUpdate) {
|
||||
await this._localBackupService.askForFileStoreBackupIfAvailable();
|
||||
}
|
||||
// trigger backup init after
|
||||
this._localBackupService.init();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _initMultiInstanceWarning(): void {
|
||||
const channel = new BroadcastChannel('superProductivityTab');
|
||||
let isOriginal = true;
|
||||
|
||||
enum Msg {
|
||||
newTabOpened = 'newTabOpened',
|
||||
alreadyOpenElsewhere = 'alreadyOpenElsewhere',
|
||||
}
|
||||
|
||||
channel.postMessage(Msg.newTabOpened);
|
||||
// note that listener is added after posting the message
|
||||
|
||||
channel.addEventListener('message', (msg) => {
|
||||
if (msg.data === Msg.newTabOpened && isOriginal) {
|
||||
// message received from 2nd tab
|
||||
// reply to all new tabs that the website is already open
|
||||
channel.postMessage(Msg.alreadyOpenElsewhere);
|
||||
}
|
||||
if (msg.data === Msg.alreadyOpenElsewhere) {
|
||||
isOriginal = false;
|
||||
// message received from original tab
|
||||
// replace this with whatever logic you need
|
||||
// NOTE: translations not ready yet
|
||||
const t =
|
||||
'You are running multiple instances of Super Productivity (possibly over multiple tabs). This is not recommended and might lead to data loss!!';
|
||||
const t2 = 'Please close all other instances, before you continue!';
|
||||
// show in two dialogs to be sure the user didn't miss it
|
||||
alert(t);
|
||||
alert(t2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _isTourLikelyToBeShown(): boolean {
|
||||
if (localStorage.getItem(LS.IS_SKIP_TOUR)) {
|
||||
return false;
|
||||
}
|
||||
const ua = navigator.userAgent;
|
||||
if (ua === 'NIGHTWATCH' || ua.includes('PLAYWRIGHT')) {
|
||||
return false;
|
||||
}
|
||||
const projectList = this._projectService.list();
|
||||
return !projectList || projectList.length <= 2;
|
||||
}
|
||||
|
||||
private _initOfflineBanner(): void {
|
||||
isOnline$.subscribe((isOnlineIn) => {
|
||||
if (!isOnlineIn) {
|
||||
this._bannerService.open({
|
||||
id: BannerId.Offline,
|
||||
ico: 'cloud_off',
|
||||
msg: T.APP.B_OFFLINE,
|
||||
});
|
||||
} else {
|
||||
this._bannerService.dismissAll(BannerId.Offline);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _requestPersistence(): void {
|
||||
if (navigator.storage) {
|
||||
// try to avoid data-loss
|
||||
Promise.all([navigator.storage.persisted()])
|
||||
.then(([persisted]) => {
|
||||
if (!persisted) {
|
||||
return navigator.storage.persist().then((granted) => {
|
||||
if (granted) {
|
||||
Log.log('Persistent store granted');
|
||||
}
|
||||
// NOTE: we never execute for android web view, because it is always true
|
||||
else if (!IS_ANDROID_WEB_VIEW) {
|
||||
const msg = T.GLOBAL_SNACK.PERSISTENCE_DISALLOWED;
|
||||
Log.warn('Persistence not allowed');
|
||||
this._snackService.open({ msg });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Log.log('Persistence already allowed');
|
||||
return;
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
Log.log(e);
|
||||
const err = e && e.toString ? e.toString() : 'UNKNOWN';
|
||||
const msg = T.GLOBAL_SNACK.PERSISTENCE_ERROR;
|
||||
this._snackService.open({
|
||||
type: 'ERROR',
|
||||
msg,
|
||||
translateParams: {
|
||||
err,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _checkAvailableStorage(): void {
|
||||
if (environment.production) {
|
||||
if ('storage' in navigator && 'estimate' in navigator.storage) {
|
||||
navigator.storage.estimate().then(({ usage, quota }) => {
|
||||
const u = usage || 0;
|
||||
const q = quota || 0;
|
||||
|
||||
const percentUsed = Math.round((u / q) * 100);
|
||||
const usageInMib = Math.round(u / (1024 * 1024));
|
||||
const quotaInMib = Math.round(q / (1024 * 1024));
|
||||
const details = `${usageInMib} out of ${quotaInMib} MiB used (${percentUsed}%)`;
|
||||
Log.log(details);
|
||||
if (quotaInMib - usageInMib <= 333) {
|
||||
alert(
|
||||
`There is only very little disk space available (${
|
||||
quotaInMib - usageInMib
|
||||
}mb). This might affect how the app is running.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* since page load and animation time are not always equal
|
||||
* an interval seemed to feel the most responsive
|
||||
|
|
|
|||
384
src/app/core/startup/startup.service.ts
Normal file
384
src/app/core/startup/startup.service.ts
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
import { effect, inject, Injectable } from '@angular/core';
|
||||
import { PersistenceLocalService } from '../persistence/persistence-local.service';
|
||||
import { PersistenceLegacyService } from '../persistence/persistence-legacy.service';
|
||||
import { PfapiService } from '../../pfapi/pfapi.service';
|
||||
import { ImexViewService } from '../../imex/imex-meta/imex-view.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { LocalBackupService } from '../../imex/local-backup/local-backup.service';
|
||||
import { GlobalConfigService } from '../../features/config/global-config.service';
|
||||
import { SnackService } from '../snack/snack.service';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { PluginService } from '../../plugins/plugin.service';
|
||||
import { SyncWrapperService } from '../../imex/sync/sync-wrapper.service';
|
||||
import { BannerService } from '../banner/banner.service';
|
||||
import { UiHelperService } from '../../features/ui-helper/ui-helper.service';
|
||||
import { ChromeExtensionInterfaceService } from '../chrome-extension-interface/chrome-extension-interface.service';
|
||||
import { ProjectService } from '../../features/project/project.service';
|
||||
import { IS_ANDROID_WEB_VIEW } from '../../util/is-android-web-view';
|
||||
import { IS_ELECTRON } from '../../app.constants';
|
||||
import { SyncStatus } from '../../pfapi/api';
|
||||
import { Log } from '../log';
|
||||
import { download } from '../../util/download';
|
||||
import { AppDataCompleteNew } from '../../pfapi/pfapi-config';
|
||||
import { T } from '../../t.const';
|
||||
import { DEFAULT_META_MODEL } from '../../pfapi/api/model-ctrl/meta-model-ctrl';
|
||||
import { BannerId } from '../banner/banner.model';
|
||||
import { isOnline$ } from '../../util/is-online';
|
||||
import { LS } from '../persistence/storage-keys.const';
|
||||
import { getDbDateStr } from '../../util/get-db-date-str';
|
||||
import { DialogPleaseRateComponent } from '../../features/dialog-please-rate/dialog-please-rate.component';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { GlobalConfigState } from '../../features/config/global-config.model';
|
||||
import { IPC } from '../../../../electron/shared-with-frontend/ipc-events.const';
|
||||
import { IpcRendererEvent } from 'electron';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { TrackingReminderService } from '../../features/tracking-reminder/tracking-reminder.service';
|
||||
import { SyncSafetyBackupService } from '../../imex/sync/sync-safety-backup.service';
|
||||
|
||||
const w = window as Window & { productivityTips?: string[][]; randomIndex?: number };
|
||||
|
||||
/** Delay before running deferred initialization tasks (plugins, storage checks, etc.) */
|
||||
const DEFERRED_INIT_DELAY_MS = 1000;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class StartupService {
|
||||
private _persistenceLocalService = inject(PersistenceLocalService);
|
||||
private _persistenceLegacyService = inject(PersistenceLegacyService);
|
||||
private _pfapiService = inject(PfapiService);
|
||||
private _imexMetaService = inject(ImexViewService);
|
||||
private _translateService = inject(TranslateService);
|
||||
private _localBackupService = inject(LocalBackupService);
|
||||
private _globalConfigService = inject(GlobalConfigService);
|
||||
private _snackService = inject(SnackService);
|
||||
private _matDialog = inject(MatDialog);
|
||||
private _pluginService = inject(PluginService);
|
||||
private _syncWrapperService = inject(SyncWrapperService);
|
||||
private _bannerService = inject(BannerService);
|
||||
private _uiHelperService = inject(UiHelperService);
|
||||
private _chromeExtensionInterfaceService = inject(ChromeExtensionInterfaceService);
|
||||
private _projectService = inject(ProjectService);
|
||||
private _trackingReminderService = inject(TrackingReminderService);
|
||||
|
||||
constructor() {
|
||||
// needs to be injected somewhere to initialize
|
||||
inject(SyncSafetyBackupService);
|
||||
|
||||
// Initialize electron error handler in an effect
|
||||
if (IS_ELECTRON) {
|
||||
effect(() => {
|
||||
window.ea.on(IPC.ERROR, (ev: IpcRendererEvent, ...args: unknown[]) => {
|
||||
const data = args[0] as {
|
||||
error: unknown;
|
||||
stack: unknown;
|
||||
errorStr: string | unknown;
|
||||
};
|
||||
const errMsg =
|
||||
typeof data.errorStr === 'string' ? data.errorStr : ' INVALID ERROR MSG :( ';
|
||||
|
||||
this._snackService.open({
|
||||
msg: errMsg,
|
||||
type: 'ERROR',
|
||||
isSkipTranslate: true,
|
||||
});
|
||||
Log.err(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this._checkMigrationAndInitBackups();
|
||||
this._requestPersistence();
|
||||
|
||||
// deferred init
|
||||
window.setTimeout(async () => {
|
||||
this._trackingReminderService.init();
|
||||
this._checkAvailableStorage();
|
||||
this._initOfflineBanner();
|
||||
|
||||
const miscCfg = this._globalConfigService.misc();
|
||||
if (miscCfg?.isShowProductivityTipLonger && !this._isTourLikelyToBeShown()) {
|
||||
if (w.productivityTips && w.randomIndex !== undefined) {
|
||||
this._snackService.open({
|
||||
ico: 'lightbulb',
|
||||
config: {
|
||||
duration: 16000,
|
||||
},
|
||||
msg:
|
||||
'<strong>' +
|
||||
w.productivityTips[w.randomIndex][0] +
|
||||
':</strong> ' +
|
||||
w.productivityTips[w.randomIndex][1],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._handleAppStartRating();
|
||||
await this._initPlugins();
|
||||
}, DEFERRED_INIT_DELAY_MS);
|
||||
|
||||
if (IS_ELECTRON) {
|
||||
window.ea.informAboutAppReady();
|
||||
this._uiHelperService.initElectron();
|
||||
|
||||
window.ea.on(IPC.TRANSFER_SETTINGS_REQUESTED, () => {
|
||||
window.ea.sendAppSettingsToElectron(
|
||||
this._globalConfigService.cfg() as GlobalConfigState,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// WEB VERSION
|
||||
window.addEventListener('beforeunload', (e) => {
|
||||
const gCfg = this._globalConfigService.cfg();
|
||||
if (!gCfg) {
|
||||
throw new Error();
|
||||
}
|
||||
if (gCfg.misc.isConfirmBeforeExit) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
}
|
||||
});
|
||||
|
||||
if (!IS_ANDROID_WEB_VIEW) {
|
||||
this._chromeExtensionInterfaceService.init();
|
||||
this._initMultiInstanceWarning();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _checkMigrationAndInitBackups(): Promise<void> {
|
||||
const MIGRATED_VAL = 42;
|
||||
const lastLocalSyncModelChange =
|
||||
await this._persistenceLocalService.loadLastSyncModelChange();
|
||||
// CHECK AND DO MIGRATION
|
||||
// ---------------------
|
||||
if (
|
||||
typeof lastLocalSyncModelChange === 'number' &&
|
||||
lastLocalSyncModelChange > MIGRATED_VAL
|
||||
) {
|
||||
// disable sync until reload
|
||||
this._pfapiService.pf.sync = () => Promise.resolve({ status: SyncStatus.InSync });
|
||||
this._imexMetaService.setDataImportInProgress(true);
|
||||
|
||||
const legacyData = await this._persistenceLegacyService.loadComplete();
|
||||
Log.log({ legacyData: legacyData });
|
||||
|
||||
alert(this._translateService.instant(T.MIGRATE.DETECTED_LEGACY));
|
||||
|
||||
if (
|
||||
!IS_ANDROID_WEB_VIEW &&
|
||||
confirm(this._translateService.instant(T.MIGRATE.C_DOWNLOAD_BACKUP))
|
||||
) {
|
||||
download('sp-legacy-backup.json', JSON.stringify(legacyData));
|
||||
}
|
||||
try {
|
||||
await this._pfapiService.importCompleteBackup(
|
||||
legacyData as unknown as AppDataCompleteNew,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
this._imexMetaService.setDataImportInProgress(true);
|
||||
await this._persistenceLocalService.updateLastSyncModelChange(MIGRATED_VAL);
|
||||
|
||||
alert(this._translateService.instant(T.MIGRATE.SUCCESS));
|
||||
|
||||
if (IS_ELECTRON) {
|
||||
window.ea.relaunch();
|
||||
// if relaunch fails we hard close the app
|
||||
window.setTimeout(() => window.ea.exit(1234), 1000);
|
||||
}
|
||||
window.location.reload();
|
||||
// fallback
|
||||
window.setTimeout(
|
||||
() => alert(this._translateService.instant(T.MIGRATE.E_RESTART_FAILED)),
|
||||
2000,
|
||||
);
|
||||
} catch (error) {
|
||||
// prevent any interaction with the app on after failure
|
||||
this._imexMetaService.setDataImportInProgress(true);
|
||||
Log.err(error);
|
||||
|
||||
try {
|
||||
alert(
|
||||
this._translateService.instant(T.MIGRATE.E_MIGRATION_FAILED) +
|
||||
'\n\n' +
|
||||
JSON.stringify(
|
||||
(error as { additionalLog?: Array<{ errors: unknown }> })
|
||||
.additionalLog?.[0]?.errors,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
alert(
|
||||
this._translateService.instant(T.MIGRATE.E_MIGRATION_FAILED) +
|
||||
'\n\n' +
|
||||
error?.toString(),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// if everything is normal, check for TMP stray backup
|
||||
await this._pfapiService.isCheckForStrayLocalTmpDBBackupAndImport();
|
||||
|
||||
// if completely fresh instance check for local backups
|
||||
if (IS_ELECTRON || IS_ANDROID_WEB_VIEW) {
|
||||
const meta = await this._pfapiService.pf.metaModel.load();
|
||||
if (!meta || meta.lastUpdate === DEFAULT_META_MODEL.lastUpdate) {
|
||||
await this._localBackupService.askForFileStoreBackupIfAvailable();
|
||||
}
|
||||
// trigger backup init after
|
||||
this._localBackupService.init();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _initMultiInstanceWarning(): void {
|
||||
const channel = new BroadcastChannel('superProductivityTab');
|
||||
let isOriginal = true;
|
||||
|
||||
enum Msg {
|
||||
newTabOpened = 'newTabOpened',
|
||||
alreadyOpenElsewhere = 'alreadyOpenElsewhere',
|
||||
}
|
||||
|
||||
channel.postMessage(Msg.newTabOpened);
|
||||
// note that listener is added after posting the message
|
||||
|
||||
channel.addEventListener('message', (msg) => {
|
||||
if (msg.data === Msg.newTabOpened && isOriginal) {
|
||||
// message received from 2nd tab
|
||||
// reply to all new tabs that the website is already open
|
||||
channel.postMessage(Msg.alreadyOpenElsewhere);
|
||||
}
|
||||
if (msg.data === Msg.alreadyOpenElsewhere) {
|
||||
isOriginal = false;
|
||||
// message received from original tab
|
||||
// replace this with whatever logic you need
|
||||
// NOTE: translations not ready yet
|
||||
const t =
|
||||
'You are running multiple instances of Super Productivity (possibly over multiple tabs). This is not recommended and might lead to data loss!!';
|
||||
const t2 = 'Please close all other instances, before you continue!';
|
||||
// show in two dialogs to be sure the user didn't miss it
|
||||
alert(t);
|
||||
alert(t2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _isTourLikelyToBeShown(): boolean {
|
||||
if (localStorage.getItem(LS.IS_SKIP_TOUR)) {
|
||||
return false;
|
||||
}
|
||||
const ua = navigator.userAgent;
|
||||
if (ua === 'NIGHTWATCH' || ua.includes('PLAYWRIGHT')) {
|
||||
return false;
|
||||
}
|
||||
const projectList = this._projectService.list();
|
||||
return !projectList || projectList.length <= 2;
|
||||
}
|
||||
|
||||
private _initOfflineBanner(): void {
|
||||
isOnline$.subscribe((isOnlineIn) => {
|
||||
if (!isOnlineIn) {
|
||||
this._bannerService.open({
|
||||
id: BannerId.Offline,
|
||||
ico: 'cloud_off',
|
||||
msg: T.APP.B_OFFLINE,
|
||||
});
|
||||
} else {
|
||||
this._bannerService.dismissAll(BannerId.Offline);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _requestPersistence(): void {
|
||||
if (navigator.storage) {
|
||||
// try to avoid data-loss
|
||||
Promise.all([navigator.storage.persisted()])
|
||||
.then(([persisted]) => {
|
||||
if (!persisted) {
|
||||
return navigator.storage.persist().then((granted) => {
|
||||
if (granted) {
|
||||
Log.log('Persistent store granted');
|
||||
}
|
||||
// NOTE: we never execute for android web view, because it is always true
|
||||
else if (!IS_ANDROID_WEB_VIEW) {
|
||||
const msg = T.GLOBAL_SNACK.PERSISTENCE_DISALLOWED;
|
||||
Log.warn('Persistence not allowed');
|
||||
this._snackService.open({ msg });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Log.log('Persistence already allowed');
|
||||
return;
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
Log.log(e);
|
||||
const err = e && e.toString ? e.toString() : 'UNKNOWN';
|
||||
const msg = T.GLOBAL_SNACK.PERSISTENCE_ERROR;
|
||||
this._snackService.open({
|
||||
type: 'ERROR',
|
||||
msg,
|
||||
translateParams: {
|
||||
err,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _checkAvailableStorage(): void {
|
||||
if (environment.production) {
|
||||
if ('storage' in navigator && 'estimate' in navigator.storage) {
|
||||
navigator.storage.estimate().then(({ usage, quota }) => {
|
||||
const u = usage || 0;
|
||||
const q = quota || 0;
|
||||
|
||||
const percentUsed = Math.round((u / q) * 100);
|
||||
const usageInMib = Math.round(u / (1024 * 1024));
|
||||
const quotaInMib = Math.round(q / (1024 * 1024));
|
||||
const details = `${usageInMib} out of ${quotaInMib} MiB used (${percentUsed}%)`;
|
||||
Log.log(details);
|
||||
if (quotaInMib - usageInMib <= 333) {
|
||||
alert(
|
||||
`There is only very little disk space available (${
|
||||
quotaInMib - usageInMib
|
||||
}mb). This might affect how the app is running.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAppStartRating(): void {
|
||||
const appStarts = +(localStorage.getItem(LS.APP_START_COUNT) || 0);
|
||||
const lastStartDay = localStorage.getItem(LS.APP_START_COUNT_LAST_START_DAY);
|
||||
const todayStr = getDbDateStr();
|
||||
if (appStarts === 32 || appStarts === 96) {
|
||||
this._matDialog.open(DialogPleaseRateComponent);
|
||||
localStorage.setItem(LS.APP_START_COUNT, (appStarts + 1).toString());
|
||||
}
|
||||
if (lastStartDay !== todayStr) {
|
||||
localStorage.setItem(LS.APP_START_COUNT, (appStarts + 1).toString());
|
||||
localStorage.setItem(LS.APP_START_COUNT_LAST_START_DAY, todayStr);
|
||||
}
|
||||
}
|
||||
|
||||
private async _initPlugins(): Promise<void> {
|
||||
// Initialize plugin system
|
||||
try {
|
||||
// Wait for sync to complete before initializing plugins to avoid DB lock conflicts
|
||||
await this._syncWrapperService.afterCurrentSyncDoneOrSyncDisabled$
|
||||
.pipe(take(1))
|
||||
.toPromise();
|
||||
await this._pluginService.initializePlugins();
|
||||
Log.log('Plugin system initialized after sync completed');
|
||||
} catch (error) {
|
||||
Log.err('Failed to initialize plugin system:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -113,6 +113,7 @@
|
|||
[isShowChecklistToggle]="true"
|
||||
(changed)="updateDailySummaryTxt($event)"
|
||||
[model]="dailySummaryNoteTxt()"
|
||||
[placeholderTxt]="T.PDS.END_OF_DAYS_RITUALS_PLACEHOLDER | translate"
|
||||
>
|
||||
<button
|
||||
[matTooltip]="'Remove daily summary note'"
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export class PluginService implements OnDestroy {
|
|||
const pluginPaths = [
|
||||
'assets/bundled-plugins/yesterday-tasks-plugin',
|
||||
'assets/bundled-plugins/sync-md',
|
||||
'assets/bundled-plugins/api-test-plugin',
|
||||
...(environment.production ? [] : ['assets/bundled-plugins/api-test-plugin']),
|
||||
'assets/bundled-plugins/procrastination-buster',
|
||||
'assets/bundled-plugins/ai-productivity-prompts',
|
||||
];
|
||||
|
|
@ -157,7 +157,7 @@ export class PluginService implements OnDestroy {
|
|||
const pluginPaths = [
|
||||
'assets/bundled-plugins/yesterday-tasks-plugin',
|
||||
'assets/bundled-plugins/sync-md',
|
||||
'assets/bundled-plugins/api-test-plugin',
|
||||
...(environment.production ? [] : ['assets/bundled-plugins/api-test-plugin']),
|
||||
'assets/bundled-plugins/procrastination-buster',
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1802,22 +1802,6 @@ const T = {
|
|||
MIN_IDLE_TIME: 'GCF.IDLE.MIN_IDLE_TIME',
|
||||
TITLE: 'GCF.IDLE.TITLE',
|
||||
},
|
||||
APP_FEATURES: {
|
||||
HELP: 'GCF.APP_FEATURES.HELP',
|
||||
TITLE: 'GCF.APP_FEATURES.TITLE',
|
||||
TIME_TRACKING: 'GCF.APP_FEATURES.TIME_TRACKING',
|
||||
FOCUS_MODE: 'GCF.APP_FEATURES.FOCUS_MODE',
|
||||
SCHEDULE: 'GCF.APP_FEATURES.SCHEDULE',
|
||||
PLANNER: 'GCF.APP_FEATURES.PLANNER',
|
||||
BOARDS: 'GCF.APP_FEATURES.BOARDS',
|
||||
SCHEDULE_DAY_PANEL: 'GCF.APP_FEATURES.SCHEDULE_DAY_PANEL',
|
||||
ISSUES_PANEL: 'GCF.APP_FEATURES.ISSUES_PANEL',
|
||||
PROJECT_NOTES: 'GCF.APP_FEATURES.PROJECT_NOTES',
|
||||
SYNC_BUTTON: 'GCF.APP_FEATURES.SYNC_BUTTON',
|
||||
DONATE_PAGE: 'GCF.APP_FEATURES.DONATE_PAGE',
|
||||
USER_PROFILES: 'GCF.APP_FEATURES.USER_PROFILES',
|
||||
USER_PROFILES_HINT: 'GCF.APP_FEATURES.USER_PROFILES_HINT',
|
||||
},
|
||||
IMEX: {
|
||||
HELP: 'GCF.IMEX.HELP',
|
||||
TITLE: 'GCF.IMEX.TITLE',
|
||||
|
|
@ -1899,26 +1883,26 @@ const T = {
|
|||
PT: 'GCF.LANG.PT',
|
||||
RU: 'GCF.LANG.RU',
|
||||
SK: 'GCF.LANG.SK',
|
||||
TIME_LOCALE: 'GCF.LANG.TIME_LOCALE',
|
||||
TIME_LOCALE_AUTO: 'GCF.LANG.TIME_LOCALE_AUTO',
|
||||
TIME_LOCALE_DE_DE: 'GCF.LANG.TIME_LOCALE_DE_DE',
|
||||
TIME_LOCALE_DESCRIPTION: 'GCF.LANG.TIME_LOCALE_DESCRIPTION',
|
||||
TIME_LOCALE_EN_GB: 'GCF.LANG.TIME_LOCALE_EN_GB',
|
||||
TIME_LOCALE_EN_US: 'GCF.LANG.TIME_LOCALE_EN_US',
|
||||
TIME_LOCALE_ES_ES: 'GCF.LANG.TIME_LOCALE_ES_ES',
|
||||
TIME_LOCALE_FR_FR: 'GCF.LANG.TIME_LOCALE_FR_FR',
|
||||
TIME_LOCALE_IT_IT: 'GCF.LANG.TIME_LOCALE_IT_IT',
|
||||
TIME_LOCALE_JA_JP: 'GCF.LANG.TIME_LOCALE_JA_JP',
|
||||
TIME_LOCALE_KO_KR: 'GCF.LANG.TIME_LOCALE_KO_KR',
|
||||
TIME_LOCALE_PT_BR: 'GCF.LANG.TIME_LOCALE_PT_BR',
|
||||
TIME_LOCALE_RU_RU: 'GCF.LANG.TIME_LOCALE_RU_RU',
|
||||
TIME_LOCALE_TR_TR: 'GCF.LANG.TIME_LOCALE_TR_TR',
|
||||
TIME_LOCALE_ZH_CN: 'GCF.LANG.TIME_LOCALE_ZH_CN',
|
||||
TITLE: 'GCF.LANG.TITLE',
|
||||
TR: 'GCF.LANG.TR',
|
||||
UK: 'GCF.LANG.UK',
|
||||
ZH: 'GCF.LANG.ZH',
|
||||
ZH_TW: 'GCF.LANG.ZH_TW',
|
||||
TIME_LOCALE: 'GCF.LANG.TIME_LOCALE',
|
||||
TIME_LOCALE_DESCRIPTION: 'GCF.LANG.TIME_LOCALE_DESCRIPTION',
|
||||
TIME_LOCALE_AUTO: 'GCF.LANG.TIME_LOCALE_AUTO',
|
||||
TIME_LOCALE_EN_US: 'GCF.LANG.TIME_LOCALE_EN_US',
|
||||
TIME_LOCALE_EN_GB: 'GCF.LANG.TIME_LOCALE_EN_GB',
|
||||
TIME_LOCALE_TR_TR: 'GCF.LANG.TIME_LOCALE_TR_TR',
|
||||
TIME_LOCALE_DE_DE: 'GCF.LANG.TIME_LOCALE_DE_DE',
|
||||
TIME_LOCALE_FR_FR: 'GCF.LANG.TIME_LOCALE_FR_FR',
|
||||
TIME_LOCALE_ES_ES: 'GCF.LANG.TIME_LOCALE_ES_ES',
|
||||
TIME_LOCALE_IT_IT: 'GCF.LANG.TIME_LOCALE_IT_IT',
|
||||
TIME_LOCALE_PT_BR: 'GCF.LANG.TIME_LOCALE_PT_BR',
|
||||
TIME_LOCALE_RU_RU: 'GCF.LANG.TIME_LOCALE_RU_RU',
|
||||
TIME_LOCALE_ZH_CN: 'GCF.LANG.TIME_LOCALE_ZH_CN',
|
||||
TIME_LOCALE_JA_JP: 'GCF.LANG.TIME_LOCALE_JA_JP',
|
||||
TIME_LOCALE_KO_KR: 'GCF.LANG.TIME_LOCALE_KO_KR',
|
||||
},
|
||||
MISC: {
|
||||
DARK_MODE: 'GCF.MISC.DARK_MODE',
|
||||
|
|
@ -1938,7 +1922,6 @@ const T = {
|
|||
IS_DARK_MODE: 'GCF.MISC.IS_DARK_MODE',
|
||||
IS_DISABLE_ANIMATIONS: 'GCF.MISC.IS_DISABLE_ANIMATIONS',
|
||||
IS_DISABLE_CELEBRATION: 'GCF.MISC.IS_DISABLE_CELEBRATION',
|
||||
|
||||
IS_HIDE_NAV: 'GCF.MISC.IS_HIDE_NAV',
|
||||
IS_MINIMIZE_TO_TRAY: 'GCF.MISC.IS_MINIMIZE_TO_TRAY',
|
||||
IS_OVERLAY_INDICATOR_ENABLED: 'GCF.MISC.IS_OVERLAY_INDICATOR_ENABLED',
|
||||
|
|
@ -2037,6 +2020,22 @@ const T = {
|
|||
L_TRACKING_REMINDER_MIN_TIME: 'GCF.TIME_TRACKING.L_TRACKING_REMINDER_MIN_TIME',
|
||||
TITLE: 'GCF.TIME_TRACKING.TITLE',
|
||||
},
|
||||
APP_FEATURES: {
|
||||
HELP: 'GCF.APP_FEATURES.HELP',
|
||||
TITLE: 'GCF.APP_FEATURES.TITLE',
|
||||
TIME_TRACKING: 'GCF.APP_FEATURES.TIME_TRACKING',
|
||||
FOCUS_MODE: 'GCF.APP_FEATURES.FOCUS_MODE',
|
||||
SCHEDULE: 'GCF.APP_FEATURES.SCHEDULE',
|
||||
PLANNER: 'GCF.APP_FEATURES.PLANNER',
|
||||
BOARDS: 'GCF.APP_FEATURES.BOARDS',
|
||||
SCHEDULE_DAY_PANEL: 'GCF.APP_FEATURES.SCHEDULE_DAY_PANEL',
|
||||
ISSUES_PANEL: 'GCF.APP_FEATURES.ISSUES_PANEL',
|
||||
PROJECT_NOTES: 'GCF.APP_FEATURES.PROJECT_NOTES',
|
||||
SYNC_BUTTON: 'GCF.APP_FEATURES.SYNC_BUTTON',
|
||||
DONATE_PAGE: 'GCF.APP_FEATURES.DONATE_PAGE',
|
||||
USER_PROFILES: 'GCF.APP_FEATURES.USER_PROFILES',
|
||||
USER_PROFILES_HINT: 'GCF.APP_FEATURES.USER_PROFILES_HINT',
|
||||
},
|
||||
},
|
||||
GLOBAL_RELATIVE_TIME: {
|
||||
FUTURE: {
|
||||
|
|
@ -2172,6 +2171,7 @@ const T = {
|
|||
ESTIMATE_TOTAL: 'PDS.ESTIMATE_TOTAL',
|
||||
EVALUATE_DAY: 'PDS.EVALUATE_DAY',
|
||||
EXPORT_TASK_LIST: 'PDS.EXPORT_TASK_LIST',
|
||||
END_OF_DAYS_RITUALS_PLACEHOLDER: 'PDS.END_OF_DAYS_RITUALS_PLACEHOLDER',
|
||||
FOCUS_SUMMARY: 'PDS.FOCUS_SUMMARY',
|
||||
NO_TASKS: 'PDS.NO_TASKS',
|
||||
PLAN_TOMORROW: 'PDS.PLAN_TOMORROW',
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
(keypress)="keypressHandler($event)"
|
||||
[@fadeIn]
|
||||
[ngModel]="modelCopy()"
|
||||
[placeholder]="placeholderTxt()"
|
||||
class="mat-body-2 markdown-unparsed"
|
||||
rows="1"
|
||||
></textarea>
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export class InlineMarkdownComponent implements OnInit, OnDestroy {
|
|||
readonly isShowControls = input<boolean>(false);
|
||||
readonly isShowChecklistToggle = input<boolean>(false);
|
||||
readonly isDefaultText = input<boolean>(false);
|
||||
readonly placeholderTxt = input<string | undefined>(undefined);
|
||||
|
||||
readonly changed = output<string>();
|
||||
readonly focused = output<Event>();
|
||||
|
|
|
|||
|
|
@ -2136,9 +2136,10 @@
|
|||
"OK": "Exit"
|
||||
},
|
||||
"ESTIMATE_TOTAL": "Total estimate:",
|
||||
"EVALUATE_DAY": "Evaluate",
|
||||
"EVALUATE_DAY": "2. Evaluate Day",
|
||||
"EXPORT_TASK_LIST": "Export Task List",
|
||||
"FOCUS_SUMMARY": "Focus (nr / time)",
|
||||
"END_OF_DAYS_RITUALS_PLACEHOLDER": "You can use this space to write down your own end of days rituals, you want to be reminded of.",
|
||||
"FOCUS_SUMMARY": "Focus Sessions",
|
||||
"NO_TASKS": "There are no tasks for this day",
|
||||
"PLAN_TOMORROW": "Plan",
|
||||
"REVIEW_TASKS": "Review",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue