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

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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"

View file

@ -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

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

View file

@ -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'"

View file

@ -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',
];

View file

@ -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',

View file

@ -13,6 +13,7 @@
(keypress)="keypressHandler($event)"
[@fadeIn]
[ngModel]="modelCopy()"
[placeholder]="placeholderTxt()"
class="mat-body-2 markdown-unparsed"
rows="1"
></textarea>

View file

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

View file

@ -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",