From b66903cf52fb8e2aaa5318bae5cf1a556d141f33 Mon Sep 17 00:00:00 2001 From: Ven Date: Tue, 18 Oct 2022 22:53:37 +0200 Subject: [PATCH] Settings: Implement plugin options defaults (#117) --- src/api/settings.ts | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/api/settings.ts b/src/api/settings.ts index a5bda838..c2301c67 100644 --- a/src/api/settings.ts +++ b/src/api/settings.ts @@ -2,6 +2,7 @@ import plugins from "plugins"; import IpcEvents from "../utils/IpcEvents"; import { React } from "../webpack/common"; import { mergeDefaults } from "../utils/misc"; +import { OptionType } from "../utils/types"; export interface Settings { notifyAboutUpdates: boolean; @@ -30,9 +31,6 @@ for (const plugin in plugins) { try { var settings = JSON.parse(VencordNative.ipc.sendSync(IpcEvents.GET_SETTINGS)) as Settings; - for (const key in DefaultSettings) { - settings[key] ??= DefaultSettings[key]; - } mergeDefaults(settings, DefaultSettings); } catch (err) { console.error("Corrupt settings file. ", err); @@ -42,24 +40,52 @@ try { type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; }; const subscriptions = new Set(); +// Wraps the passed settings object in a Proxy to nicely handle change listeners and default values function makeProxy(settings: Settings, root = settings, path = ""): Settings { return new Proxy(settings, { get(target, p: string) { const v = target[p]; + + // using "in" is important in the following cases to properly handle falsy or nullish values + if (!(p in target)) { + // Since the property is not set, check if this is a plugin's setting and if so, try to resolve + // the default value. + if (path.startsWith("plugins.")) { + const plugin = path.slice("plugins.".length); + if (plugin in plugins) { + const setting = plugins[plugin].options?.[p]; + if (!setting) return v; + if ("default" in setting) + // normal setting with a default value + return setting.default; + if (setting.type === OptionType.SELECT) + return setting.options.find(o => o.default)?.value; + } + } + return v; + } + + // Recursively proxy Objects with the updated property path if (typeof v === "object" && !Array.isArray(v) && v !== null) return makeProxy(v, root, `${path}${path && "."}${p}`); + + // primitive or similar, no need to proxy further return v; }, + set(target, p: string, v) { + // avoid unnecessary updates to React Components and other listeners if (target[p] === v) return true; target[p] = v; + // Call any listeners that are listening to a setting of this path const setPath = `${path}${path && "."}${p}`; for (const subscription of subscriptions) { if (!subscription._path || subscription._path === setPath) { subscription(v, setPath); } } + // And don't forget to persist the settings! VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(root, null, 4)); return true; } @@ -70,6 +96,9 @@ function makeProxy(settings: Settings, root = settings, path = ""): Settings { * Same as {@link Settings} but unproxied. You should treat this as readonly, * as modifying properties on this will not save to disk or call settings * listeners. + * WARNING: default values specified in plugin.options will not be ensured here. In other words, + * settings for which you specified a default value may be uninitialised. If you need proper + * handling for default values, use {@link Settings} */ export const PlainSettings = settings; /** @@ -78,6 +107,7 @@ export const PlainSettings = settings; * This recursively proxies objects. If you need the object non proxied, use {@link PlainSettings} */ export const Settings = makeProxy(settings); + /** * Settings hook for React components. Returns a smart settings * object that automagically triggers a rerender if any properties