From f7a0938c3815fd0aabb10c236d9cb09bc6d445bc Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Tue, 24 Jan 2023 19:31:37 +0500 Subject: [PATCH] desktop: improve integration with operating system --- apps/web/desktop/autolaunch.ts | 66 ++++++ apps/web/desktop/config/desktopIntegration.js | 35 +++ apps/web/desktop/electron.js | 206 +++++++++++++++++- apps/web/desktop/events.js | 3 +- apps/web/desktop/ipc/actions/index.js | 4 +- .../ipc/actions/setDesktopIntegration.js | 33 +++ .../ipc/calls/getDesktopIntegration.js | 24 ++ apps/web/desktop/ipc/calls/index.js | 4 +- .../jsonstorage/{index.js => index.ts} | 14 +- apps/web/desktop/package-lock.json | 73 ++----- apps/web/desktop/package.json | 3 +- apps/web/desktop/preload.js | 5 + apps/web/desktop/utils.js | 7 +- apps/web/src/app-effects.js | 18 ++ .../src/commands/set-desktop-integration.ts | 26 +++ apps/web/src/common/index.js | 8 +- apps/web/src/global.d.ts | 9 + apps/web/src/hooks/use-desktop-integration.ts | 50 +++++ apps/web/src/views/settings.js | 59 +++++ 19 files changed, 564 insertions(+), 83 deletions(-) create mode 100644 apps/web/desktop/autolaunch.ts create mode 100644 apps/web/desktop/config/desktopIntegration.js create mode 100644 apps/web/desktop/ipc/actions/setDesktopIntegration.js create mode 100644 apps/web/desktop/ipc/calls/getDesktopIntegration.js rename apps/web/desktop/jsonstorage/{index.js => index.ts} (89%) create mode 100644 apps/web/src/commands/set-desktop-integration.ts create mode 100644 apps/web/src/hooks/use-desktop-integration.ts diff --git a/apps/web/desktop/autolaunch.ts b/apps/web/desktop/autolaunch.ts new file mode 100644 index 000000000..de858fb13 --- /dev/null +++ b/apps/web/desktop/autolaunch.ts @@ -0,0 +1,66 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +import { app } from "electron"; +import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs"; +import path from "path"; + +const LINUX_DESKTOP_ENTRY = (hidden: boolean) => `[Desktop Entry] +Type=Application +Version=${app.getVersion()} +Name=${app.getName()} +Comment=${app.getName()} startup script +Exec=${process.execPath}${hidden ? " --hidden" : ""} +StartupNotify=false +Terminal=false`; + +const LINUX_AUTOSTART_DIRECTORY_PATH = path.join( + app.getPath("home"), + ".config", + "autostart" +); + +export class AutoLaunch { + static enable(hidden: boolean) { + if (process.platform === "linux") { + mkdirSync(LINUX_AUTOSTART_DIRECTORY_PATH, { recursive: true }); + writeFileSync( + path.join( + LINUX_AUTOSTART_DIRECTORY_PATH, + `${app.getName().toLowerCase()}.desktop` + ), + LINUX_DESKTOP_ENTRY(hidden) + ); + } else { + app.setLoginItemSettings({ openAtLogin: true, openAsHidden: hidden }); + } + } + + static disable() { + if (process.platform === "linux") { + const desktopFilePath = path.join( + LINUX_AUTOSTART_DIRECTORY_PATH, + `${app.getName().toLowerCase()}.desktop` + ); + if (!existsSync(desktopFilePath)) return; + rmSync(desktopFilePath); + } else { + app.setLoginItemSettings({ openAtLogin: false, openAsHidden: false }); + } + } +} diff --git a/apps/web/desktop/config/desktopIntegration.js b/apps/web/desktop/config/desktopIntegration.js new file mode 100644 index 000000000..b7c26b49c --- /dev/null +++ b/apps/web/desktop/config/desktopIntegration.js @@ -0,0 +1,35 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { JSONStorage } from "../jsonstorage"; + +function getDesktopIntegration() { + return JSONStorage.get("desktopSettings", { + autoStart: false, + startMinimized: false, + minimizeToSystemTray: false, + closeToSystemTray: false + }); +} + +function setDesktopIntegration(settings) { + return JSONStorage.set("desktopSettings", settings); +} + +export { getDesktopIntegration, setDesktopIntegration }; diff --git a/apps/web/desktop/electron.js b/apps/web/desktop/electron.js index 537c1096a..99a1239b6 100644 --- a/apps/web/desktop/electron.js +++ b/apps/web/desktop/electron.js @@ -19,10 +19,8 @@ along with this program. If not, see . /* global MAC_APP_STORE, RELEASE */ import "isomorphic-fetch"; -import { app, BrowserWindow, nativeTheme, shell } from "electron"; -import { join } from "path"; -import { platform } from "os"; -import { isDevelopment } from "./utils"; +import { app, BrowserWindow, Menu, nativeTheme, shell, Tray } from "electron"; +import { APP_ICON_PATH, isDevelopment } from "./utils"; import { registerProtocol, PROTOCOL_URL } from "./protocol"; import { configureAutoUpdater } from "./autoupdate"; import { getBackgroundColor, getTheme, setTheme } from "./config/theme"; @@ -36,6 +34,10 @@ import "./ipc/index.js"; import getPrivacyMode from "./ipc/calls/getPrivacyMode"; import setPrivacyMode from "./ipc/actions/setPrivacyMode"; import { getIsSpellCheckerEnabled } from "./config/spellChecker"; +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; +import { getDesktopIntegration } from "./config/desktopIntegration"; +import { AutoLaunch } from "./autolaunch"; if (!RELEASE) { require("electron-reloader")(module); @@ -51,20 +53,21 @@ if (process.platform === "win32") { app.setAppUserModelId(app.name); } +var mainWindowState; async function createWindow() { - const mainWindowState = new WindowState({}); + mainWindowState = new WindowState({}); setTheme(getTheme()); + const mainWindow = new BrowserWindow({ x: mainWindowState.x, y: mainWindowState.y, width: mainWindowState.width, height: mainWindowState.height, + fullscreen: mainWindowState.isFullScreen, + backgroundColor: getBackgroundColor(), autoHideMenuBar: true, - icon: join( - __dirname, - platform() === "win32" ? "app.ico" : "favicon-72x72.png" - ), + icon: APP_ICON_PATH, webPreferences: { zoomFactor: getZoomFactor(), devTools: true, // isDev, @@ -76,10 +79,14 @@ async function createWindow() { preload: __dirname + "/preload.js" } }); + mainWindow.setAutoHideMenuBar(true); mainWindowState.manage(mainWindow); globalThis.window = mainWindow; setupMenu(); + setupJumplist(); + setupTray(); + setupDesktopIntegration(); if (isDevelopment()) mainWindow.webContents.openDevTools({ mode: "right", activate: true }); @@ -88,10 +95,9 @@ async function createWindow() { setPrivacyMode({ privacyMode: getPrivacyMode() }); } + const cliOptions = await parseCli(); try { - await mainWindow.loadURL( - isDevelopment() ? "http://localhost:3000" : PROTOCOL_URL - ); + await mainWindow.loadURL(`${createURL(cliOptions)}`); } catch (e) { logger.error(e); } @@ -134,3 +140,179 @@ app.on("activate", () => { createWindow(); } }); + +function createURL(options) { + const url = new URL(isDevelopment() ? "http://localhost:3000" : PROTOCOL_URL); + + if (options.note === true) url.hash = "/notes/create/1"; + else if (options.notebook === true) url.hash = "/notebooks/create"; + else if (options.reminder === true) url.hash = "/reminders/create"; + else if (typeof options.note === "string") + url.hash = `/notes/${options.note}/edit`; + else if (typeof options.notebook === "string") + url.hash = `/notebooks/${options.notebook}`; + + return url; +} + +async function parseCli() { + const result = { + note: false, + notebook: false, + reminder: false + }; + await yargs(hideBin(process.argv)) + .command("new", "Create a new item", (yargs) => { + return yargs + .command("note", "Create a new note", {}, () => (result.note = true)) + .command( + "notebook", + "Create a new notebook", + {}, + () => (result.notebook = true) + ) + .command( + "reminder", + "Add a new reminder", + {}, + () => (result.reminder = true) + ); + }) + .command("open", "Open a specific item", (yargs) => { + return yargs + .command( + "note", + "Open a note", + { id: { string: true, description: "Id of the note" } }, + (args) => (result.note = args.id) + ) + .command( + "notebook", + "Open a notebook", + { id: { string: true, description: "Id of the notebook" } }, + (args) => (result.notebook = args.id) + ) + .command( + "topic", + "Open a topic", + { + id: { string: true, description: "Id of the topic" }, + notebookId: { string: true, description: "Id of the notebook" } + }, + (args) => (result.notebook = `${args.notebookId}/${args.id}`) + ); + }) + .parse(); + return result; +} + +function setupJumplist() { + if (process.platform === "win32") { + app.setJumpList([ + { type: "frequent" }, + { + type: "tasks", + items: [ + { + program: process.execPath, + iconPath: process.execPath, + args: "new note", + description: "Create a new note", + title: "New note", + type: "task" + }, + { + program: process.execPath, + iconPath: process.execPath, + args: "new notebook", + description: "Create a new notebook", + title: "New notebook", + type: "task" + }, + { + program: process.execPath, + iconPath: process.execPath, + args: "new reminder", + description: "Add a new reminder", + title: "New reminder", + type: "task" + } + ] + } + ]); + } +} + +function setupTray() { + const tray = new Tray(APP_ICON_PATH); + + const contextMenu = Menu.buildFromTemplate([ + { + label: "Show Notesnook", + type: "normal", + icon: APP_ICON_PATH, + click: showApp + }, + { type: "separator" }, + { + label: "New note", + type: "normal", + click: () => { + showApp(); + sendMessageToRenderer(EVENTS.createItem, { itemType: "note" }); + } + }, + { + label: "New notebook", + type: "normal", + click: () => { + showApp(); + sendMessageToRenderer(EVENTS.createItem, { itemType: "notebook" }); + } + }, + { type: "separator" }, + { + label: "Quit Notesnook", + type: "normal", + click: () => { + app.exit(0); + } + } + ]); + tray.on("double-click", showApp); + tray.on("click", showApp); + tray.setToolTip("Notesnook"); + tray.setContextMenu(contextMenu); +} + +function showApp() { + if (globalThis.window.isMinimized()) { + if (mainWindowState.isMaximized) { + globalThis.window.maximize(); + } else globalThis.window.restore(); + } + globalThis.window.show(); + globalThis.window.focus(); + globalThis.window.moveTop(); + globalThis.window.webContents.focus(); +} + +function setupDesktopIntegration() { + const desktopIntegration = getDesktopIntegration(); + + if (desktopIntegration.autoStart) { + AutoLaunch.enable(desktopIntegration.startMinimized); + } + + globalThis.window.on("close", (e) => { + if (getDesktopIntegration().closeToSystemTray) { + e.preventDefault(); + globalThis.window.minimize(); + globalThis.window.hide(); + } + }); + + globalThis.window.on("minimize", () => { + if (getDesktopIntegration().minimizeToSystemTray) globalThis.window.hide(); + }); +} diff --git a/apps/web/desktop/events.js b/apps/web/desktop/events.js index b036ee6bc..f8a6d1cb4 100644 --- a/apps/web/desktop/events.js +++ b/apps/web/desktop/events.js @@ -24,5 +24,6 @@ export const EVENTS = { updateDownloadCompleted: "updateDownloadCompleted", updateNotAvailable: "updateNotAvailable", themeChanged: "themeChanged", - notificationClicked: "notificationClicked" + notificationClicked: "notificationClicked", + createItem: "createItem" }; diff --git a/apps/web/desktop/ipc/actions/index.js b/apps/web/desktop/ipc/actions/index.js index 40bb0d4a4..a1fbb220c 100644 --- a/apps/web/desktop/ipc/actions/index.js +++ b/apps/web/desktop/ipc/actions/index.js @@ -29,6 +29,7 @@ import showNotification from "./showNotification"; import bringToFront from "./bringToFront"; import setSpellCheckerLanguages from "./setSpellCheckerLanguages"; import toggleSpellChecker from "./toggleSpellChecker"; +import setDesktopIntegration from "./setDesktopIntegration"; const actions = { changeAppTheme, @@ -42,7 +43,8 @@ const actions = { showNotification, bringToFront, setSpellCheckerLanguages, - toggleSpellChecker + toggleSpellChecker, + setDesktopIntegration, }; export function getAction(actionName) { diff --git a/apps/web/desktop/ipc/actions/setDesktopIntegration.js b/apps/web/desktop/ipc/actions/setDesktopIntegration.js new file mode 100644 index 000000000..e38162921 --- /dev/null +++ b/apps/web/desktop/ipc/actions/setDesktopIntegration.js @@ -0,0 +1,33 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { AutoLaunch } from "../../autolaunch"; +import { setDesktopIntegration } from "../../config/desktopIntegration"; + +export default (args) => { + if (!globalThis.window) return; + + if (args.autoStart) { + AutoLaunch.enable(args.startMinimized); + } else { + AutoLaunch.disable(); + } + + setDesktopIntegration(args); +}; diff --git a/apps/web/desktop/ipc/calls/getDesktopIntegration.js b/apps/web/desktop/ipc/calls/getDesktopIntegration.js new file mode 100644 index 000000000..9d6d9b604 --- /dev/null +++ b/apps/web/desktop/ipc/calls/getDesktopIntegration.js @@ -0,0 +1,24 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { getDesktopIntegration } from "../../config/desktopIntegration"; + +export default function () { + return getDesktopIntegration(); +} diff --git a/apps/web/desktop/ipc/calls/index.js b/apps/web/desktop/ipc/calls/index.js index 02f0bbe24..e43efc46e 100644 --- a/apps/web/desktop/ipc/calls/index.js +++ b/apps/web/desktop/ipc/calls/index.js @@ -22,6 +22,7 @@ import getPrivacyMode from "./getPrivacyMode"; import selectDirectory from "./selectDirectory"; import { gunzip, gzip } from "./gzip"; import getSpellChecker from "./getSpellChecker"; +import getDesktopIntegration from "./getDesktopIntegration"; const calls = { getZoomFactor, @@ -29,7 +30,8 @@ const calls = { selectDirectory, gunzip, gzip, - getSpellChecker + getSpellChecker, + getDesktopIntegration, }; export const getCall = function getAction(callName) { diff --git a/apps/web/desktop/jsonstorage/index.js b/apps/web/desktop/jsonstorage/index.ts similarity index 89% rename from apps/web/desktop/jsonstorage/index.js rename to apps/web/desktop/jsonstorage/index.ts index 511acca62..93dd71561 100644 --- a/apps/web/desktop/jsonstorage/index.js +++ b/apps/web/desktop/jsonstorage/index.ts @@ -25,12 +25,12 @@ const directory = app.getPath("userData"); const filename = "config.json"; const filePath = join(directory, filename); class JSONStorage { - static get(key, def) { + static get(key: string, def?: T): T { const json = this.readJson(); return json[key] === undefined ? def : json[key]; } - static set(key, value) { + static set(key: string, value: unknown) { const json = this.readJson(); json[key] = value; this.writeJson(json); @@ -40,10 +40,7 @@ class JSONStorage { this.writeJson({}); } - /** - * @private - */ - static readJson() { + private static readJson() { try { const json = readFileSync(filePath, "utf-8"); return JSON.parse(json); @@ -53,10 +50,7 @@ class JSONStorage { } } - /** - * @private - */ - static writeJson(json) { + private static writeJson(json: Record) { try { writeFileSync(filePath, JSON.stringify(json)); } catch (e) { diff --git a/apps/web/desktop/package-lock.json b/apps/web/desktop/package-lock.json index 5af09333d..7a731d2a9 100644 --- a/apps/web/desktop/package-lock.json +++ b/apps/web/desktop/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "diary": "^0.3.1", "electron-updater": "^5.3.0", - "isomorphic-fetch": "^3.0.0" + "isomorphic-fetch": "^3.0.0", + "yargs": "^17.6.2" }, "devDependencies": { "@types/node-fetch": "^2.6.2", @@ -514,7 +515,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -523,7 +523,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1073,7 +1072,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -1096,7 +1094,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1107,8 +1104,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/colors": { "version": "1.0.3", @@ -1801,8 +1797,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/encodeurl": { "version": "1.0.2", @@ -2209,7 +2204,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } @@ -2434,7 +2428,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -2846,7 +2839,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -3591,7 +3583,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3835,7 +3826,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -3849,7 +3839,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4140,7 +4129,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -4172,7 +4160,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -4183,10 +4170,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", - "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", - "dev": true, + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -4194,7 +4180,7 @@ "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^21.0.0" + "yargs-parser": "^21.1.1" }, "engines": { "node": ">=12" @@ -4204,7 +4190,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "engines": { "node": ">=12" } @@ -4624,14 +4609,12 @@ "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -5055,7 +5038,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "requires": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -5075,7 +5057,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -5083,8 +5064,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "colors": { "version": "1.0.3", @@ -5642,8 +5622,7 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "encodeurl": { "version": "1.0.2", @@ -5856,8 +5835,7 @@ "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, "escape-string-regexp": { "version": "4.0.0", @@ -6024,8 +6002,7 @@ "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-intrinsic": { "version": "1.1.2", @@ -6328,8 +6305,7 @@ "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-glob": { "version": "4.0.1", @@ -6887,8 +6863,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" }, "resolve": { "version": "1.20.0", @@ -7091,7 +7066,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -7102,7 +7076,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -7342,7 +7315,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -7364,8 +7336,7 @@ "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, "yallist": { "version": "4.0.0", @@ -7373,10 +7344,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", - "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", - "dev": true, + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", "requires": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -7384,14 +7354,13 @@ "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^21.0.0" + "yargs-parser": "^21.1.1" } }, "yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" }, "yauzl": { "version": "2.10.0", diff --git a/apps/web/desktop/package.json b/apps/web/desktop/package.json index 1c80c9de3..c799ec716 100644 --- a/apps/web/desktop/package.json +++ b/apps/web/desktop/package.json @@ -11,7 +11,8 @@ "dependencies": { "diary": "^0.3.1", "electron-updater": "^5.3.0", - "isomorphic-fetch": "^3.0.0" + "isomorphic-fetch": "^3.0.0", + "yargs": "^17.6.2" }, "devDependencies": { "@types/node-fetch": "^2.6.2", diff --git a/apps/web/desktop/preload.js b/apps/web/desktop/preload.js index d9cd5b7d8..7341702ef 100644 --- a/apps/web/desktop/preload.js +++ b/apps/web/desktop/preload.js @@ -61,6 +61,11 @@ contextBridge.exposeInMainWorld("config", { return ipcRenderer.invoke("fromRenderer", { type: "getSpellChecker" }); + }, + desktopIntegration: () => { + return ipcRenderer.invoke("fromRenderer", { + type: "getDesktopIntegration" + }); } }); diff --git a/apps/web/desktop/utils.js b/apps/web/desktop/utils.js index 0d22fa244..1c8135354 100644 --- a/apps/web/desktop/utils.js +++ b/apps/web/desktop/utils.js @@ -21,6 +21,11 @@ import { app } from "electron"; import { join } from "path"; import { statSync } from "fs"; +const APP_ICON_PATH = join( + __dirname, + process.platform === "win32" ? "app.ico" : "favicon-72x72.png" +); + function isDevelopment() { if (typeof electron === "string") { throw new TypeError("Not running in an Electron environment!"); @@ -47,4 +52,4 @@ function getPath(filePath) { } } -export { getPath, isDevelopment }; +export { getPath, isDevelopment, APP_ICON_PATH }; diff --git a/apps/web/src/app-effects.js b/apps/web/src/app-effects.js index ecc29fff2..4441e39bb 100644 --- a/apps/web/src/app-effects.js +++ b/apps/web/src/app-effects.js @@ -29,6 +29,7 @@ import { introduceFeatures, showUpgradeReminderDialogs } from "./common"; import { AppEventManager, AppEvents } from "./common/app-events"; import { db } from "./common/db"; import { EV, EVENTS } from "@notesnook/core/common"; +import { EVENTS as DESKTOP_APP_EVENTS } from "@notesnook/desktop/events"; import { registerKeyMap } from "./common/key-map"; import { isUserPremium } from "./hooks/use-is-user-premium"; import useAnnouncements from "./hooks/use-announcements"; @@ -45,6 +46,7 @@ import { updateStatus, removeStatus, getStatus } from "./hooks/use-status"; import { showToast } from "./utils/toast"; import { interruptedOnboarding } from "./components/dialogs/onboarding-dialog"; import { WebExtensionRelay } from "./utils/web-extension-relay"; +import { hashNavigate } from "./navigation"; const relay = new WebExtensionRelay(); @@ -197,6 +199,22 @@ export default function AppEffects({ setShow }) { setTheme(isSystemThemeDark ? "dark" : "light"); }, [isSystemThemeDark, followSystemTheme, setTheme]); + useEffect(() => { + AppEventManager.subscribe(DESKTOP_APP_EVENTS.createItem, ({ itemType }) => { + switch (itemType) { + case "note": + hashNavigate("/notes/create", { addNonce: true, replace: true }); + break; + case "notebook": + hashNavigate("/notebooks/create", { replace: true }); + break; + case "reminder": + hashNavigate("/reminders/create", { replace: true }); + break; + } + }); + }, []); + return ; } diff --git a/apps/web/src/commands/set-desktop-integration.ts b/apps/web/src/commands/set-desktop-integration.ts new file mode 100644 index 000000000..568ddc826 --- /dev/null +++ b/apps/web/src/commands/set-desktop-integration.ts @@ -0,0 +1,26 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { invokeCommand } from "./index"; + +export default function setDesktopIntegration( + settings: DesktopIntegrationSettings +) { + invokeCommand("setDesktopIntegration", settings); +} diff --git a/apps/web/src/common/index.js b/apps/web/src/common/index.js index 088585578..e92fa9348 100644 --- a/apps/web/src/common/index.js +++ b/apps/web/src/common/index.js @@ -47,19 +47,19 @@ export const CREATE_BUTTON_MAP = { }, notebooks: { title: "Create a notebook", - onClick: () => hashNavigate("/notebooks/create") + onClick: () => hashNavigate("/notebooks/create", { replace: true }) }, topics: { title: "Create a topic", - onClick: () => hashNavigate(`/topics/create`) + onClick: () => hashNavigate(`/topics/create`, { replace: true }) }, tags: { title: "Create a tag", - onClick: () => hashNavigate(`/tags/create`) + onClick: () => hashNavigate(`/tags/create`, { replace: true }) }, reminders: { title: "Add a reminder", - onClick: () => hashNavigate(`/reminders/create`) + onClick: () => hashNavigate(`/reminders/create`, { replace: true }) } }; diff --git a/apps/web/src/global.d.ts b/apps/web/src/global.d.ts index b76744c37..15fb1e490 100644 --- a/apps/web/src/global.d.ts +++ b/apps/web/src/global.d.ts @@ -23,9 +23,18 @@ type SpellCheckerOptions = { enabledLanguages: Language[]; enabled: boolean; }; + +type DesktopIntegrationSettings = { + autoStart: boolean; + startMinimized: boolean; + minimizeToSystemTray: boolean; + closeToSystemTray: boolean; +}; + declare interface Window { config: { static spellChecker(): Promise; + static desktopIntegration(): Promise; }; native: { static gzip({ diff --git a/apps/web/src/hooks/use-desktop-integration.ts b/apps/web/src/hooks/use-desktop-integration.ts new file mode 100644 index 000000000..ae531d6db --- /dev/null +++ b/apps/web/src/hooks/use-desktop-integration.ts @@ -0,0 +1,50 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { useCallback, useEffect, useState } from "react"; +import setDesktopIntegration from "../commands/set-desktop-integration"; + +export default function useDesktopIntegration() { + const [settings, changeSettings] = useState(); + + const setupDesktopIntegration = useCallback(async () => { + const settings = await window.config.desktopIntegration(); + changeSettings(settings); + return settings; + }, []); + + useEffect(() => { + if (!window.config) return; + (async function () { + await setupDesktopIntegration(); + })(); + }, [setupDesktopIntegration]); + + const set = useCallback( + async (_settings: Partial) => { + if (!settings) return; + + setDesktopIntegration({ ...settings, ..._settings }); + await setupDesktopIntegration(); + }, + [settings, setupDesktopIntegration] + ); + + return [settings, set] as const; +} diff --git a/apps/web/src/views/settings.js b/apps/web/src/views/settings.js index 98e32f7ed..d68a7b15e 100644 --- a/apps/web/src/views/settings.js +++ b/apps/web/src/views/settings.js @@ -77,6 +77,7 @@ import { scheduleBackups } from "../common/reminders"; import usePrivacyMode from "../hooks/use-privacy-mode"; import { useTelemetry } from "../hooks/use-telemetry"; import useSpellChecker from "../hooks/use-spell-checker"; +import useDesktopIntegration from "../hooks/use-desktop-integration"; function subscriptionStatusToString(user) { const status = user?.subscription?.type; @@ -147,6 +148,7 @@ function Settings() { privacy: false, developer: false, notifications: false, + desktop: false, other: true }); const isVaultCreated = useAppStore((store) => store.isVaultCreated); @@ -196,6 +198,8 @@ function Settings() { const [privacyMode, setPrivacyMode] = usePrivacyMode(); const [showReminderNotifications, setShowReminderNotifications] = usePersistentState("reminderNotifications", true); + const [desktopIntegration, changeDesktopIntegration] = + useDesktopIntegration(); const [corsProxy, setCorsProxy] = usePersistentState( "corsProxy", "https://cors.notesnook.com" @@ -534,6 +538,61 @@ function Settings() { )} + {isDesktop() && ( + <> +
{ + setGroups((g) => ({ ...g, desktop: !g.desktop })); + }} + /> + {groups.desktop && ( + <> + { + changeDesktopIntegration({ + autoStart: !desktopIntegration.autoStart + }); + }} + isToggled={desktopIntegration.autoStart} + /> + {desktopIntegration.autoStart && ( + { + changeDesktopIntegration({ + startMinimized: !desktopIntegration.startMinimized + }); + }} + isToggled={desktopIntegration.startMinimized} + /> + )} + { + changeDesktopIntegration({ + minimizeToSystemTray: + !desktopIntegration.minimizeToSystemTray + }); + }} + isToggled={desktopIntegration.minimizeToSystemTray} + /> + { + changeDesktopIntegration({ + closeToSystemTray: !desktopIntegration.closeToSystemTray + }); + }} + isToggled={desktopIntegration.closeToSystemTray} + /> + + )} + + )} +