diff --git a/package.json b/package.json index f9d6265..b938f52 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "start": "pnpm build && electron .", "start:dev": "pnpm build --dev && electron .", "start:watch": "tsx scripts/startWatch.mts", - "test": "echo \"Error: no test specified\" && exit 1", + "test": "pnpm lint && pnpm testTypes", + "testTypes": "tsc --noEmit", "watch": "pnpm build --watch" }, "devDependencies": { diff --git a/src/main/index.ts b/src/main/index.ts index cf51e33..eccfb75 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -63,7 +63,7 @@ async function createWindows() { splash.destroy(); mainWin!.show(); - if (Settings.maximized) { + if (Settings.store.maximized) { mainWin!.maximize(); } }); diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 3bdaf98..9d22673 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -13,7 +13,7 @@ import { debounce } from "shared/utils/debounce"; import { IpcEvents } from "../shared/IpcEvents"; import { VENCORD_FILES_DIR, VENCORD_QUICKCSS_FILE } from "./constants"; import { mainWin } from "./mainWindow"; -import { PlainSettings, setSettings } from "./settings"; +import { Settings } from "./settings"; ipcMain.on(IpcEvents.GET_VENCORD_PRELOAD_FILE, e => { e.returnValue = join(VENCORD_FILES_DIR, "preload.js"); @@ -32,7 +32,7 @@ ipcMain.on(IpcEvents.GET_RENDERER_CSS_FILE, e => { }); ipcMain.on(IpcEvents.GET_SETTINGS, e => { - e.returnValue = PlainSettings; + e.returnValue = Settings.plain; }); ipcMain.on(IpcEvents.GET_VERSION, e => { @@ -40,7 +40,7 @@ ipcMain.on(IpcEvents.GET_VERSION, e => { }); ipcMain.handle(IpcEvents.SET_SETTINGS, (_, settings) => { - setSettings(settings); + Settings.setData(settings); }); ipcMain.handle(IpcEvents.RELAUNCH, () => { diff --git a/src/main/mainWindow.ts b/src/main/mainWindow.ts index e214a0c..470079d 100644 --- a/src/main/mainWindow.ts +++ b/src/main/mainWindow.ts @@ -78,7 +78,7 @@ function initTray(win: BrowserWindow) { function initMenuBar(win: BrowserWindow) { const isWindows = process.platform === "win32"; - const wantCtrlQ = !isWindows || VencordSettings.winCtrlQ; + const wantCtrlQ = !isWindows || VencordSettings.store.winCtrlQ; const menu = Menu.buildFromTemplate([ { @@ -174,7 +174,7 @@ function initMenuBar(win: BrowserWindow) { } function getWindowBoundsOptions(): BrowserWindowConstructorOptions { - const { x, y, width, height } = Settings.windowBounds ?? {}; + const { x, y, width, height } = Settings.store.windowBounds ?? {}; const options = { width: width ?? DEFAULT_WIDTH, @@ -186,7 +186,7 @@ function getWindowBoundsOptions(): BrowserWindowConstructorOptions { options.y = y; } - if (!Settings.disableMinSize) { + if (!Settings.store.disableMinSize) { options.minWidth = MIN_WIDTH; options.minHeight = MIN_HEIGHT; } @@ -196,8 +196,8 @@ function getWindowBoundsOptions(): BrowserWindowConstructorOptions { function initWindowBoundsListeners(win: BrowserWindow) { const saveState = () => { - Settings.maximized = win.isMaximized(); - Settings.minimized = win.isMinimized(); + Settings.store.maximized = win.isMaximized(); + Settings.store.minimized = win.isMinimized(); }; win.on("maximize", saveState); @@ -205,7 +205,7 @@ function initWindowBoundsListeners(win: BrowserWindow) { win.on("unmaximize", saveState); const saveBounds = () => { - Settings.windowBounds = win.getBounds(); + Settings.store.windowBounds = win.getBounds(); }; win.on("resize", saveBounds); @@ -224,12 +224,12 @@ export function createMainWindow() { preload: join(__dirname, "preload.js") }, icon: ICON_PATH, - frame: VencordSettings.frameless !== true, + frame: VencordSettings.store.frameless !== true, ...getWindowBoundsOptions() })); win.on("close", e => { - if (isQuitting || Settings.minimizeToTray === false) return; + if (isQuitting || Settings.store.minimizeToTray === false) return; e.preventDefault(); win.hide(); @@ -243,7 +243,9 @@ export function createMainWindow() { makeLinksOpenExternally(win); const subdomain = - Settings.discordBranch === "canary" || Settings.discordBranch === "ptb" ? `${Settings.discordBranch}.` : ""; + Settings.store.discordBranch === "canary" || Settings.store.discordBranch === "ptb" + ? `${Settings.store.discordBranch}.` + : ""; win.loadURL(`https://${subdomain}discord.com/app`); diff --git a/src/main/settings.ts b/src/main/settings.ts index 507046a..370d9ea 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -7,7 +7,7 @@ import { readFileSync, writeFileSync } from "fs"; import { join } from "path"; import type { Settings as TSettings } from "shared/settings"; -import { makeChangeListenerProxy } from "shared/utils/makeChangeListenerProxy"; +import { SettingsStore } from "shared/utils/makeChangeListenerProxy"; import { DATA_DIR, VENCORD_SETTINGS_FILE } from "./constants"; @@ -24,23 +24,11 @@ function loadSettings(file: string, name: string) { } } catch {} - const makeSettingsProxy = (settings: T) => - makeChangeListenerProxy(settings, target => writeFileSync(file, JSON.stringify(target, null, 4))); + const store = new SettingsStore(settings); + store.addGlobalChangeListener(o => writeFileSync(file, JSON.stringify(o, null, 4))); - return [settings, makeSettingsProxy] as const; + return store; } -// eslint-disable-next-line prefer-const -let [PlainSettings, makeSettingsProxy] = loadSettings(SETTINGS_FILE, "VencordDesktop"); -export let Settings = makeSettingsProxy(PlainSettings); - -const [PlainVencordSettings, makeVencordSettingsProxy] = loadSettings(VENCORD_SETTINGS_FILE, "Vencord"); -export const VencordSettings = makeVencordSettingsProxy(PlainVencordSettings); - -export function setSettings(settings: TSettings) { - writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 4)); - PlainSettings = settings; - Settings = makeSettingsProxy(settings); -} - -export { PlainSettings, PlainVencordSettings }; +export const Settings = loadSettings(SETTINGS_FILE, "Vencord Desktop"); +export const VencordSettings = loadSettings(VENCORD_SETTINGS_FILE, "Vencord"); diff --git a/src/main/utils/makeLinksOpenExternally.ts b/src/main/utils/makeLinksOpenExternally.ts index 59e8593..4da4969 100644 --- a/src/main/utils/makeLinksOpenExternally.ts +++ b/src/main/utils/makeLinksOpenExternally.ts @@ -25,7 +25,7 @@ export function makeLinksOpenExternally(win: BrowserWindow) { switch (protocol) { case "http:": case "https:": - if (Settings.openLinksWithElectron) { + if (Settings.store.openLinksWithElectron) { return { action: "allow" }; } // eslint-disable-next-line no-fallthrough diff --git a/src/renderer/settings.ts b/src/renderer/settings.ts index 8aa8cd3..9e68525 100644 --- a/src/renderer/settings.ts +++ b/src/renderer/settings.ts @@ -5,26 +5,23 @@ */ import type { Settings as TSettings } from "shared/settings"; -import { makeChangeListenerProxy } from "shared/utils/makeChangeListenerProxy"; +import { SettingsStore } from "shared/utils/makeChangeListenerProxy"; import { Common } from "./vencord"; -const signals = new Set<() => void>(); - export const PlainSettings = VencordDesktopNative.settings.get() as TSettings; -export const Settings = makeChangeListenerProxy(PlainSettings, s => { - VencordDesktopNative.settings.set(s); - signals.forEach(fn => fn()); -}); +export const Settings = new SettingsStore(PlainSettings); export function useSettings() { const [, update] = Common.React.useReducer(x => x + 1, 0); + Common.React.useEffect(() => { - signals.add(update); - return () => signals.delete(update); + Settings.addGlobalChangeListener(update); + + return () => Settings.removeGlobalChangeListener(update); }, []); - return Settings; + return Settings.store; } export function getValueAndOnChange(key: keyof TSettings) { diff --git a/src/shared/utils/makeChangeListenerProxy.ts b/src/shared/utils/makeChangeListenerProxy.ts index aabc83e..53739f2 100644 --- a/src/shared/utils/makeChangeListenerProxy.ts +++ b/src/shared/utils/makeChangeListenerProxy.ts @@ -4,23 +4,67 @@ * Copyright (c) 2023 Vendicated and Vencord contributors */ -export function makeChangeListenerProxy(object: T, onChange: (object: T) => void, _root = object): T { - return new Proxy(object, { - get(target, key) { - const v = target[key]; - if (typeof v === "object" && !Array.isArray(v) && v !== null) - return makeChangeListenerProxy(v, onChange, _root); +export class SettingsStore { + public declare store: T; + private globalListeners = new Set<(newData: T) => void>(); + private pathListeners = new Map void>>(); - return v; - }, + public constructor(public plain: T) { + this.store = this.makeProxy(plain); + } - set(target, key, value) { - if (target[key] === value) return true; + private makeProxy(object: any, root: T = object, path: string = "") { + const self = this; - Reflect.set(target, key, value); - onChange(_root); + return new Proxy(object, { + get(target, key: string) { + const v = target[key]; - return true; - } - }); + if (typeof v === "object" && v !== null && !Array.isArray(v)) + return self.makeProxy(v, root, `${path}${path && "."}${key}`); + + return v; + }, + set(target, key: string, value) { + if (target[key] === value) return true; + + Reflect.set(target, key, value); + const setPath = `${path}${path && "."}${key}`; + + self.globalListeners.forEach(cb => cb(root)); + self.pathListeners.get(setPath)?.forEach(cb => cb(value)); + + return true; + } + }); + } + + public setData(value: T) { + this.plain = value; + this.store = this.makeProxy(value); + + this.globalListeners.forEach(cb => cb(value)); + } + + public addGlobalChangeListener(cb: (store: T) => void) { + this.globalListeners.add(cb); + } + + public addChangeListener(path: string, cb: (data: any) => void) { + const listeners = this.pathListeners.get(path) ?? new Set(); + listeners.add(cb); + this.pathListeners.set(path, listeners); + } + + public removeGlobalChangeListener(cb: (store: T) => void) { + this.globalListeners.delete(cb); + } + + public removeChangeListener(path: string, cb: (data: any) => void) { + const listeners = this.pathListeners.get(path); + if (!listeners) return; + + listeners.delete(cb); + if (!listeners.size) this.pathListeners.delete(path); + } }