diff --git a/apps/web/desktop/config/windowstate.js b/apps/web/desktop/config/window-state.ts similarity index 69% rename from apps/web/desktop/config/windowstate.js rename to apps/web/desktop/config/window-state.ts index fd4ff3444..a122c4e1d 100644 --- a/apps/web/desktop/config/windowstate.js +++ b/apps/web/desktop/config/window-state.ts @@ -18,36 +18,63 @@ along with this program. If not, see . */ import { JSONStorage } from "../jsonstorage"; -import { screen as _screen, remote } from "electron"; -const screen = _screen || remote.screen; +import { screen as _screen, BrowserWindow } from "electron"; +const screen = _screen; -class WindowState { - constructor(options) { - this.winRef = null; +type WindowStateOptions = { + storageKey: string; + maximize: boolean; + fullScreen: boolean; + defaultWidth: number; + defaultHeight: number; +}; + +type SerializableWindowState = { + x: number; + y: number; + width: number; + height: number; + displayBounds?: Electron.Rectangle; + isMaximized?: boolean; + isFullScreen?: boolean; +}; + +export class WindowState { + private readonly config: WindowStateOptions; + private state: SerializableWindowState; + private windowRef: BrowserWindow | undefined; + private readonly eventHandlingDelay = 100; + private stateChangeTimer: NodeJS.Timeout | undefined; + + constructor(options?: Partial) { + this.windowRef = undefined; this.stateChangeTimer = undefined; - this.eventHandlingDelay = 100; this.config = { storageKey: "windowState", maximize: true, fullScreen: true, + defaultHeight: 800, + defaultWidth: 600, ...options }; // Load previous state - this.state = JSONStorage.get(this.config.storageKey, {}); + const defaultState: SerializableWindowState = { + width: this.config.defaultWidth, + height: this.config.defaultHeight, + x: 0, + y: 0 + }; + this.state = JSONStorage.get( + this.config.storageKey, + defaultState + ); // Check state validity - this.validateState(); - - // Set state fallback values - this.state = { - width: this.config.defaultWidth || 800, - height: this.config.defaultHeight || 600, - ...this.state - }; + this.validateState(defaultState); } - isNormal(win) { + isNormal(win: BrowserWindow) { return !win.isMaximized() && !win.isMinimized() && !win.isFullScreen(); } @@ -76,7 +103,9 @@ class WindowState { }; } - windowWithinBounds(bounds) { + windowWithinBounds(bounds: Electron.Rectangle) { + if (!this.state) return false; + return ( this.state.x >= bounds.x && this.state.y >= bounds.y && @@ -97,12 +126,12 @@ class WindowState { } } - validateState() { + validateState(defaultState: SerializableWindowState) { const isValid = this.state && (this.hasBounds() || this.state.isMaximized || this.state.isFullScreen); if (!isValid) { - this.state = null; + this.state = defaultState; return; } @@ -111,8 +140,8 @@ class WindowState { } } - updateState(win) { - win = win || this.winRef; + updateState(win?: BrowserWindow) { + win = win || this.windowRef; if (!win) { return; } @@ -133,7 +162,7 @@ class WindowState { } } - saveState(win) { + saveState(win?: BrowserWindow) { // Update window state only if it was provided if (win) { this.updateState(win); @@ -145,7 +174,7 @@ class WindowState { stateChangeHandler = () => { // Handles both 'resize' and 'move' - clearTimeout(this.stateChangeTimer); + if (this.stateChangeTimer) clearTimeout(this.stateChangeTimer); this.stateChangeTimer = setTimeout( () => this.updateState(), this.eventHandlingDelay @@ -163,7 +192,7 @@ class WindowState { this.saveState(); }; - manage(win) { + manage(win: BrowserWindow) { if (this.config.maximize && this.state.isMaximized) { win.maximize(); } @@ -174,17 +203,17 @@ class WindowState { win.on("move", this.stateChangeHandler); win.on("close", this.closeHandler); win.on("closed", this.closedHandler); - this.winRef = win; + this.windowRef = win; } unmanage() { - if (this.winRef) { - this.winRef.removeListener("resize", this.stateChangeHandler); - this.winRef.removeListener("move", this.stateChangeHandler); - clearTimeout(this.stateChangeTimer); - this.winRef.removeListener("close", this.closeHandler); - this.winRef.removeListener("closed", this.closedHandler); - this.winRef = null; + if (this.windowRef) { + this.windowRef.removeListener("resize", this.stateChangeHandler); + this.windowRef.removeListener("move", this.stateChangeHandler); + if (this.stateChangeTimer) clearTimeout(this.stateChangeTimer); + this.windowRef.removeListener("close", this.closeHandler); + this.windowRef.removeListener("closed", this.closedHandler); + this.windowRef = undefined; } } @@ -216,5 +245,3 @@ class WindowState { return this.state.isFullScreen; } } - -export { WindowState }; diff --git a/apps/web/desktop/electron.js b/apps/web/desktop/electron.js index 99a1239b6..64693461f 100644 --- a/apps/web/desktop/electron.js +++ b/apps/web/desktop/electron.js @@ -27,7 +27,7 @@ import { getBackgroundColor, getTheme, setTheme } from "./config/theme"; import getZoomFactor from "./ipc/calls/getZoomFactor"; import { logger } from "./logger"; import { setupMenu } from "./menu"; -import { WindowState } from "./config/windowstate"; +import { WindowState } from "./config/window-state"; import { sendMessageToRenderer } from "./ipc/utils"; import { EVENTS } from "./events"; import "./ipc/index.js"; @@ -38,6 +38,7 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { getDesktopIntegration } from "./config/desktopIntegration"; import { AutoLaunch } from "./autolaunch"; +import bringToFront from "./ipc/actions/bringToFront"; if (!RELEASE) { require("electron-reloader")(module); @@ -251,14 +252,14 @@ function setupTray() { label: "Show Notesnook", type: "normal", icon: APP_ICON_PATH, - click: showApp + click: bringToFront }, { type: "separator" }, { label: "New note", type: "normal", click: () => { - showApp(); + bringToFront(); sendMessageToRenderer(EVENTS.createItem, { itemType: "note" }); } }, @@ -266,7 +267,7 @@ function setupTray() { label: "New notebook", type: "normal", click: () => { - showApp(); + bringToFront(); sendMessageToRenderer(EVENTS.createItem, { itemType: "notebook" }); } }, @@ -279,24 +280,12 @@ function setupTray() { } } ]); - tray.on("double-click", showApp); - tray.on("click", showApp); + tray.on("double-click", bringToFront); + tray.on("click", bringToFront); 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(); diff --git a/apps/web/desktop/ipc/actions/bringToFront.js b/apps/web/desktop/ipc/actions/bringToFront.js index f67a95ff4..86e4c0128 100644 --- a/apps/web/desktop/ipc/actions/bringToFront.js +++ b/apps/web/desktop/ipc/actions/bringToFront.js @@ -17,8 +17,18 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ +import { WindowState } from "../../config/window-state"; + export default () => { if (!globalThis.window) return; + + if (globalThis.window.isMinimized()) { + if (new WindowState({}).isMaximized) { + globalThis.window.maximize(); + } else globalThis.window.restore(); + } globalThis.window.show(); + globalThis.window.focus(); globalThis.window.moveTop(); + globalThis.window.webContents.focus(); };