diff --git a/electron/ipc-handler.ts b/electron/ipc-handler.ts index a7b856909..603c42207 100644 --- a/electron/ipc-handler.ts +++ b/electron/ipc-handler.ts @@ -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[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 { - log('trying to run command ' + command); - const existingData = await loadSimpleStoreAll(); - const allowedCommands: string[] = (existingData[COMMAND_MAP_PROP] as string[]) || []; - - if (!Array.isArray(allowedCommands)) { - throw new Error('allowedCommands is no array ???'); - } - if (allowedCommands.includes(command)) { - exec(command, (err) => { - if (err) { - errorHandlerWithFrontendInform(err); - } - }); - } else { - const mainWin = getWin(); - const res = await dialog.showMessageBox(mainWin, { - type: 'question', - buttons: ['Cancel', 'Yes, execute!'], - defaultId: 2, - title: 'Super Productivity – Exec', - message: - 'Do you want to execute this command? ONLY confirm if you are sure you know what you are doing!!', - detail: command, - checkboxLabel: 'Remember my answer', - checkboxChecked: true, - }); - const { response, checkboxChecked } = res; - - if (response === 1) { - if (checkboxChecked) { - await saveSimpleStore(COMMAND_MAP_PROP, [...allowedCommands, command]); - } - exec(command, (err) => { - if (err) { - errorHandlerWithFrontendInform(err); - } - }); - } - } -} diff --git a/electron/ipc-handlers/app-control.ts b/electron/ipc-handlers/app-control.ts new file mode 100644 index 000000000..7c5c70221 --- /dev/null +++ b/electron/ipc-handlers/app-control.ts @@ -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); + }); + } + }); +}; diff --git a/electron/ipc-handlers/app-data.ts b/electron/ipc-handlers/app-data.ts new file mode 100644 index 000000000..a415dca89 --- /dev/null +++ b/electron/ipc-handlers/app-data.ts @@ -0,0 +1,17 @@ +import { app, ipcMain } from 'electron'; +import { IPC } from '../shared-with-frontend/ipc-events.const'; +import { BACKUP_DIR, BACKUP_DIR_WINSTORE } from '../backup'; + +export const initAppDataIpc = (): void => { + ipcMain.handle(IPC.GET_PATH, (ev, name: string) => { + return app.getPath(name as Parameters[0]); + }); + + ipcMain.handle(IPC.GET_BACKUP_PATH, () => { + if (process?.windowsStore) { + return BACKUP_DIR_WINSTORE; + } else { + return BACKUP_DIR; + } + }); +}; diff --git a/electron/ipc-handlers/exec.ts b/electron/ipc-handlers/exec.ts new file mode 100644 index 000000000..6864462fc --- /dev/null +++ b/electron/ipc-handlers/exec.ts @@ -0,0 +1,58 @@ +import { dialog, ipcMain, IpcMainEvent } from 'electron'; +import { IPC } from '../shared-with-frontend/ipc-events.const'; +import { exec } from 'child_process'; +import { log } from 'electron-log/main'; +import { loadSimpleStoreAll, saveSimpleStore } from '../simple-store'; +import { getWin } from '../main-window'; +import { errorHandlerWithFrontendInform } from '../error-handler-with-frontend-inform'; + +const COMMAND_MAP_PROP = 'allowedCommands'; + +export const initExecIpc = (): void => { + ipcMain.on(IPC.EXEC, execWithFrontendErrorHandlerInform); +}; + +const execWithFrontendErrorHandlerInform = async ( + ev: IpcMainEvent, + command: string, +): Promise => { + log('trying to run command ' + command); + const existingData = await loadSimpleStoreAll(); + const allowedCommands: string[] = (existingData[COMMAND_MAP_PROP] as string[]) || []; + + if (!Array.isArray(allowedCommands)) { + throw new Error('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); + } + }); + } + } +}; diff --git a/electron/ipc-handlers/global-shortcuts.ts b/electron/ipc-handlers/global-shortcuts.ts new file mode 100644 index 000000000..386206c92 --- /dev/null +++ b/electron/ipc-handlers/global-shortcuts.ts @@ -0,0 +1,88 @@ +import { globalShortcut, ipcMain } from 'electron'; +import { IPC } from '../shared-with-frontend/ipc-events.const'; +import { KeyboardConfig } from '../../src/app/features/config/keyboard-config.model'; +import { getWin } from '../main-window'; +import { showOrFocus } from '../various-shared'; +import { errorHandlerWithFrontendInform } from '../error-handler-with-frontend-inform'; + +export const initGlobalShortcutsIpc = (): void => { + ipcMain.on(IPC.REGISTER_GLOBAL_SHORTCUTS_EVENT, (ev, cfg) => { + registerShowAppShortCuts(cfg); + }); +}; + +const registerShowAppShortCuts = (cfg: KeyboardConfig): void => { + // unregister all previous + globalShortcut.unregisterAll(); + const GLOBAL_KEY_CFG_KEYS: (keyof KeyboardConfig)[] = [ + 'globalShowHide', + 'globalToggleTaskStart', + 'globalAddNote', + 'globalAddTask', + ]; + + if (cfg) { + const mainWin = getWin(); + Object.keys(cfg) + .filter((key: string) => GLOBAL_KEY_CFG_KEYS.includes(key as keyof KeyboardConfig)) + .forEach((key: string) => { + let actionFn: () => void; + const shortcut = cfg[key as keyof KeyboardConfig]; + + switch (key) { + case 'globalShowHide': + actionFn = () => { + if (mainWin.isFocused()) { + // we need to blur the window for windows + mainWin.blur(); + mainWin.hide(); + } else { + showOrFocus(mainWin); + } + }; + break; + + case 'globalToggleTaskStart': + actionFn = () => { + mainWin.webContents.send(IPC.TASK_TOGGLE_START); + }; + break; + + case 'globalAddNote': + actionFn = () => { + showOrFocus(mainWin); + mainWin.webContents.send(IPC.ADD_NOTE); + }; + break; + + case 'globalAddTask': + actionFn = () => { + showOrFocus(mainWin); + // NOTE: delay slightly to make sure app is ready + mainWin.webContents.send(IPC.SHOW_ADD_TASK_BAR); + }; + break; + + default: + actionFn = () => undefined; + } + + if (shortcut && shortcut.length > 0) { + try { + const ret = globalShortcut.register(shortcut, actionFn) as unknown; + if (!ret) { + errorHandlerWithFrontendInform( + 'Global Shortcut registration failed: ' + shortcut, + shortcut, + ); + } + } catch (e) { + errorHandlerWithFrontendInform( + 'Global Shortcut registration failed: ' + shortcut, + { e, shortcut }, + ); + } + } + }); + } +}; diff --git a/electron/ipc-handlers/index.ts b/electron/ipc-handlers/index.ts new file mode 100644 index 000000000..b23c88b4c --- /dev/null +++ b/electron/ipc-handlers/index.ts @@ -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'; diff --git a/electron/ipc-handlers/jira.ts b/electron/ipc-handlers/jira.ts new file mode 100644 index 000000000..ac8bb527a --- /dev/null +++ b/electron/ipc-handlers/jira.ts @@ -0,0 +1,14 @@ +import { ipcMain } from 'electron'; +import { IPC } from '../shared-with-frontend/ipc-events.const'; +import { JiraCfg } from '../../src/app/features/issue/providers/jira/jira.model'; +import { sendJiraRequest, setupRequestHeadersForImages } from '../jira'; + +export const initJiraIpc = (): void => { + ipcMain.on(IPC.JIRA_SETUP_IMG_HEADERS, (ev, { jiraCfg }: { jiraCfg: JiraCfg }) => { + setupRequestHeadersForImages(jiraCfg); + }); + + ipcMain.on(IPC.JIRA_MAKE_REQUEST_EVENT, (ev, request) => { + sendJiraRequest(request); + }); +}; diff --git a/electron/ipc-handlers/system.ts b/electron/ipc-handlers/system.ts new file mode 100644 index 000000000..9984f73e3 --- /dev/null +++ b/electron/ipc-handlers/system.ts @@ -0,0 +1,36 @@ +import { app, dialog, ipcMain, shell } from 'electron'; +import { IPC } from '../shared-with-frontend/ipc-events.const'; +import { getWin } from '../main-window'; + +export const initSystemIpc = (): void => { + ipcMain.on(IPC.OPEN_PATH, (ev, path: string) => shell.openPath(path)); + ipcMain.on(IPC.OPEN_EXTERNAL, (ev, url: string) => shell.openExternal(url)); + + ipcMain.on( + IPC.SHOW_EMOJI_PANEL, + () => app.isEmojiPanelSupported() && app.showEmojiPanel(), + ); + + ipcMain.handle(IPC.SAVE_FILE_DIALOG, async (ev, { filename, data }) => { + const result = await dialog.showSaveDialog(getWin(), { + defaultPath: filename, + filters: [ + { name: 'JSON Files', extensions: ['json'] }, + { name: 'All Files', extensions: ['*'] }, + ], + }); + + if (!result.canceled && result.filePath) { + const fs = await import('fs'); + await fs.promises.writeFile(result.filePath, data, 'utf-8'); + return { success: true, path: result.filePath }; + } + return { success: false }; + }); + + ipcMain.handle(IPC.SHARE_NATIVE, async () => { + // Desktop platforms use the share dialog instead of native share + // This allows for more flexibility and better UX with social media options + return { success: false, error: 'Native share not available on desktop' }; + }); +}; diff --git a/packages/plugin-dev/ai-productivity-prompts/package-lock.json b/packages/plugin-dev/ai-productivity-prompts/package-lock.json index 7dec53849..2b729b3d8 100644 --- a/packages/plugin-dev/ai-productivity-prompts/package-lock.json +++ b/packages/plugin-dev/ai-productivity-prompts/package-lock.json @@ -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", diff --git a/packages/plugin-dev/boilerplate-solid-js/package-lock.json b/packages/plugin-dev/boilerplate-solid-js/package-lock.json index d7b1f5d77..6b70e7f83 100644 --- a/packages/plugin-dev/boilerplate-solid-js/package-lock.json +++ b/packages/plugin-dev/boilerplate-solid-js/package-lock.json @@ -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", diff --git a/packages/plugin-dev/procrastination-buster/package-lock.json b/packages/plugin-dev/procrastination-buster/package-lock.json index 56f2f11e0..c70e42418 100644 --- a/packages/plugin-dev/procrastination-buster/package-lock.json +++ b/packages/plugin-dev/procrastination-buster/package-lock.json @@ -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", diff --git a/packages/plugin-dev/sync-md/package-lock.json b/packages/plugin-dev/sync-md/package-lock.json index 9658c605f..f2893fce3 100644 --- a/packages/plugin-dev/sync-md/package-lock.json +++ b/packages/plugin-dev/sync-md/package-lock.json @@ -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" diff --git a/src/app/app.component.ts b/src/app/app.component.ts index a733a63f8..3864123ff 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -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; - 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; + 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: - '' + - w.productivityTips![w.randomIndex!][0] + - ': ' + - 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 { - 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 diff --git a/src/app/core/startup/startup.service.ts b/src/app/core/startup/startup.service.ts new file mode 100644 index 000000000..470e418ee --- /dev/null +++ b/src/app/core/startup/startup.service.ts @@ -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: + '' + + w.productivityTips[w.randomIndex][0] + + ': ' + + 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 { + 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 { + // 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); + } + } +} diff --git a/src/app/pages/daily-summary/daily-summary.component.html b/src/app/pages/daily-summary/daily-summary.component.html index d6cf8dcf6..6d116e60d 100644 --- a/src/app/pages/daily-summary/daily-summary.component.html +++ b/src/app/pages/daily-summary/daily-summary.component.html @@ -113,6 +113,7 @@ [isShowChecklistToggle]="true" (changed)="updateDailySummaryTxt($event)" [model]="dailySummaryNoteTxt()" + [placeholderTxt]="T.PDS.END_OF_DAYS_RITUALS_PLACEHOLDER | translate" >