diff --git a/apps/web/desktop/config/theme.js b/apps/web/desktop/config/theme.js index a0216c577..46271e07c 100644 --- a/apps/web/desktop/config/theme.js +++ b/apps/web/desktop/config/theme.js @@ -1,14 +1,14 @@ -const { default: storage } = require("electron-data-storage"); +const JSONStorage = require("../jsonstorage"); const { nativeTheme } = require("electron"); function getTheme() { - return storage.getSync("theme") || "light"; + return JSONStorage.get("theme") || "light"; } function setTheme(theme) { nativeTheme.themeSource = theme; if (global.win) global.win.setBackgroundColor(getBackgroundColor(theme)); - return storage.set("theme", theme); + return JSONStorage.set("theme", theme); } function getBackgroundColor(theme) { diff --git a/apps/web/desktop/config/windowstate.js b/apps/web/desktop/config/windowstate.js new file mode 100644 index 000000000..1565281ad --- /dev/null +++ b/apps/web/desktop/config/windowstate.js @@ -0,0 +1,199 @@ +const JSONStorage = require("../jsonstorage"); +const electron = require("electron"); +const screen = electron.screen || electron.remote.screen; + +class WindowState { + constructor(options) { + this.winRef = null; + this.stateChangeTimer = undefined; + this.eventHandlingDelay = 100; + this.config = { + storageKey: "windowState", + maximize: true, + fullScreen: true, + ...options, + }; + + // Load previous state + this.state = JSONStorage.get(this.config.storageKey, {}); + + // Check state validity + this.validateState(); + + // Set state fallback values + this.state = { + width: this.config.defaultWidth || 800, + height: this.config.defaultHeight || 600, + ...this.state, + }; + } + + isNormal(win) { + return !win.isMaximized() && !win.isMinimized() && !win.isFullScreen(); + } + + hasBounds() { + return ( + this.state && + Number.isInteger(this.state.x) && + Number.isInteger(this.state.y) && + Number.isInteger(this.state.width) && + this.state.width > 0 && + Number.isInteger(this.state.height) && + this.state.height > 0 + ); + } + + resetStateToDefault() { + const displayBounds = screen.getPrimaryDisplay().bounds; + + // Reset state to default values on the primary display + this.state = { + width: this.config.defaultWidth || 800, + height: this.config.defaultHeight || 600, + x: 0, + y: 0, + displayBounds, + }; + } + + windowWithinBounds(bounds) { + return ( + this.state.x >= bounds.x && + this.state.y >= bounds.y && + this.state.x + this.state.width <= bounds.x + bounds.width && + this.state.y + this.state.height <= bounds.y + bounds.height + ); + } + + ensureWindowVisibleOnSomeDisplay() { + const visible = screen.getAllDisplays().some((display) => { + return this.windowWithinBounds(display.bounds); + }); + + if (!visible) { + // Window is partially or fully not visible now. + // Reset it to safe defaults. + return this.resetStateToDefault(); + } + } + + validateState() { + const isValid = + this.state && + (this.hasBounds() || this.state.isMaximized || this.state.isFullScreen); + if (!isValid) { + this.state = null; + return; + } + + if (this.hasBounds() && this.state.displayBounds) { + this.ensureWindowVisibleOnSomeDisplay(); + } + } + + updateState(win) { + win = win || this.winRef; + if (!win) { + return; + } + // Don't throw an error when window was closed + try { + const winBounds = win.getBounds(); + if (this.isNormal(win)) { + this.state.x = winBounds.x; + this.state.y = winBounds.y; + this.state.width = winBounds.width; + this.state.height = winBounds.height; + } + this.state.isMaximized = win.isMaximized(); + this.state.isFullScreen = win.isFullScreen(); + this.state.displayBounds = screen.getDisplayMatching(winBounds).bounds; + } catch (err) {} + } + + saveState(win) { + // Update window state only if it was provided + if (win) { + this.updateState(win); + } + + // Save state + JSONStorage.set(this.config.storageKey, this.state); + } + + stateChangeHandler = () => { + // Handles both 'resize' and 'move' + clearTimeout(this.stateChangeTimer); + this.stateChangeTimer = setTimeout( + () => this.updateState(), + this.eventHandlingDelay + ); + }; + + closeHandler = () => { + this.updateState(); + }; + + closedHandler = async () => { + // Unregister listeners and save state + this.unmanage(); + + this.saveState(); + }; + + manage(win) { + if (this.config.maximize && this.state.isMaximized) { + win.maximize(); + } + if (this.config.fullScreen && this.state.isFullScreen) { + win.setFullScreen(true); + } + win.on("resize", this.stateChangeHandler); + win.on("move", this.stateChangeHandler); + win.on("close", this.closeHandler); + win.on("closed", this.closedHandler); + this.winRef = 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; + } + } + + get x() { + return this.state.x; + } + + get y() { + return this.state.y; + } + + get width() { + return this.state.width; + } + + get height() { + return this.state.height; + } + + get displayBounds() { + return this.state.displayBounds; + } + + get isMaximized() { + return this.state.isMaximized; + } + + get isFullScreen() { + return this.state.isFullScreen; + } +} + +module.exports = WindowState; diff --git a/apps/web/desktop/config/zoomfactor.js b/apps/web/desktop/config/zoomfactor.js index 0be610c6d..8cee836e5 100644 --- a/apps/web/desktop/config/zoomfactor.js +++ b/apps/web/desktop/config/zoomfactor.js @@ -1,12 +1,12 @@ -const storage = require("electron-data-storage").default; +const JSONStorage = require("../jsonstorage"); function getZoomFactor() { - let factor = parseFloat(storage.getSync("zoomFactor")); + let factor = parseFloat(JSONStorage.get("zoomFactor")); return isNaN(factor) ? 1.0 : factor; } function setZoomFactor(factor) { - return storage.set("zoomFactor", factor.toString()); + return JSONStorage.set("zoomFactor", factor.toString()); } module.exports = { setZoomFactor, getZoomFactor }; diff --git a/apps/web/desktop/electron.js b/apps/web/desktop/electron.js index 87b6f7a34..568548082 100644 --- a/apps/web/desktop/electron.js +++ b/apps/web/desktop/electron.js @@ -8,6 +8,7 @@ const { getBackgroundColor, getTheme, setTheme } = require("./config/theme"); const getZoomFactor = require("./ipc/calls/getZoomFactor"); const { logger } = require("./logger"); const { setupMenu } = require("./menu"); +const WindowState = require("./config/windowstate"); require("./ipc/index.js"); try { require("electron-reloader")(module); @@ -16,7 +17,19 @@ try { let mainWindow; async function createWindow() { + const mainWindowState = new WindowState({}); + console.log({ + x: mainWindowState.x, + y: mainWindowState.y, + width: mainWindowState.width, + height: mainWindowState.height, + }); mainWindow = new BrowserWindow({ + x: mainWindowState.x, + y: mainWindowState.y, + width: mainWindowState.width, + height: mainWindowState.height, + backgroundColor: getBackgroundColor(), darkTheme: getTheme() === "dark", autoHideMenuBar: true, @@ -35,15 +48,16 @@ async function createWindow() { preload: __dirname + "/preload.js", }, }); + mainWindowState.manage(mainWindow); + global.win = mainWindow; + setTheme(getTheme()); setupMenu(mainWindow); if (isDevelopment()) mainWindow.webContents.openDevTools({ mode: "right", activate: true }); - mainWindow.maximize(); - try { await mainWindow.loadURL(isDevelopment() ? "http://localhost:3000" : URL); } catch (e) { diff --git a/apps/web/desktop/ipc/actions/changeAppTheme.js b/apps/web/desktop/ipc/actions/changeAppTheme.js index c3c4205e1..0eb67d4e5 100644 --- a/apps/web/desktop/ipc/actions/changeAppTheme.js +++ b/apps/web/desktop/ipc/actions/changeAppTheme.js @@ -1,7 +1,7 @@ const { setTheme } = require("../../config/theme"); -module.exports = async (args) => { +module.exports = (args) => { if (!global.win) return; const { theme } = args; - await setTheme(theme); + setTheme(theme); }; diff --git a/apps/web/desktop/ipc/actions/setZoomFactor.js b/apps/web/desktop/ipc/actions/setZoomFactor.js index 9be9ff321..b914fc315 100644 --- a/apps/web/desktop/ipc/actions/setZoomFactor.js +++ b/apps/web/desktop/ipc/actions/setZoomFactor.js @@ -1,8 +1,8 @@ const { setZoomFactor } = require("../../config/zoomfactor"); -module.exports = async (args) => { +module.exports = (args) => { if (!global.win) return; const { zoomFactor } = args; global.win.webContents.setZoomFactor(zoomFactor); - await setZoomFactor(zoomFactor); + setZoomFactor(zoomFactor); }; diff --git a/apps/web/desktop/jsonstorage/index.js b/apps/web/desktop/jsonstorage/index.js new file mode 100644 index 000000000..66887d65a --- /dev/null +++ b/apps/web/desktop/jsonstorage/index.js @@ -0,0 +1,48 @@ +const fs = require("fs"); +const { app } = require("electron"); +const path = require("path"); + +const directory = app.getPath("userData"); +const filename = "config.json"; +const filePath = path.join(directory, filename); +class JSONStorage { + static get(key, def) { + const json = this.readJson(); + return json[key] || def; + } + + static set(key, value) { + const json = this.readJson(); + json[key] = value; + this.writeJson(json); + } + + static clear() { + this.writeJson({}); + } + + /** + * @private + */ + static readJson() { + try { + const json = fs.readFileSync(filePath, "utf-8"); + return JSON.parse(json); + } catch (e) { + console.error(e); + return {}; + } + } + + /** + * @private + */ + static writeJson(json) { + try { + fs.writeFileSync(filePath, JSON.stringify(json)); + } catch (e) { + console.error(e); + } + } +} +module.exports = JSONStorage; diff --git a/apps/web/desktop/package.json b/apps/web/desktop/package.json index b3de9ecfd..ba1ce0b2d 100644 --- a/apps/web/desktop/package.json +++ b/apps/web/desktop/package.json @@ -10,7 +10,6 @@ "dependencies": { "diary": "^0.1.6", "electron-better-ipc": "^2.0.1", - "electron-data-storage": "^1.0.7", "electron-serve": "^1.1.0", "electron-updater": "^4.3.8", "node-fetch": "^2.6.1"