258 lines
8.3 KiB
TypeScript
258 lines
8.3 KiB
TypeScript
/*
|
|
* Vencord, a modification for Discord's desktop app
|
|
* Copyright (c) 2022 Vendicated and contributors
|
|
*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import { debounce } from "@shared/debounce";
|
|
import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore";
|
|
import { localStorage } from "@utils/localStorage";
|
|
import { Logger } from "@utils/Logger";
|
|
import { mergeDefaults } from "@utils/mergeDefaults";
|
|
import { putCloudSettings } from "@utils/settingsSync";
|
|
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
|
import { React } from "@webpack/common";
|
|
|
|
import plugins from "~plugins";
|
|
|
|
const logger = new Logger("Settings");
|
|
export interface Settings {
|
|
autoUpdate: boolean;
|
|
autoUpdateNotification: boolean,
|
|
useQuickCss: boolean;
|
|
enableReactDevtools: boolean;
|
|
themeLinks: string[];
|
|
enabledThemes: string[];
|
|
frameless: boolean;
|
|
transparent: boolean;
|
|
winCtrlQ: boolean;
|
|
macosVibrancyStyle:
|
|
| "content"
|
|
| "fullscreen-ui"
|
|
| "header"
|
|
| "hud"
|
|
| "menu"
|
|
| "popover"
|
|
| "selection"
|
|
| "sidebar"
|
|
| "titlebar"
|
|
| "tooltip"
|
|
| "under-page"
|
|
| "window"
|
|
| undefined;
|
|
disableMinSize: boolean;
|
|
winNativeTitleBar: boolean;
|
|
plugins: {
|
|
[plugin: string]: {
|
|
enabled: boolean;
|
|
[setting: string]: any;
|
|
};
|
|
};
|
|
|
|
notifications: {
|
|
timeout: number;
|
|
position: "top-right" | "bottom-right";
|
|
useNative: "always" | "never" | "not-focused";
|
|
logLimit: number;
|
|
};
|
|
|
|
cloud: {
|
|
authenticated: boolean;
|
|
url: string;
|
|
settingsSync: boolean;
|
|
settingsSyncVersion: number;
|
|
};
|
|
}
|
|
|
|
const DefaultSettings: Settings = {
|
|
autoUpdate: true,
|
|
autoUpdateNotification: true,
|
|
useQuickCss: true,
|
|
themeLinks: [],
|
|
enabledThemes: [],
|
|
enableReactDevtools: false,
|
|
frameless: false,
|
|
transparent: false,
|
|
winCtrlQ: false,
|
|
macosVibrancyStyle: undefined,
|
|
disableMinSize: false,
|
|
winNativeTitleBar: false,
|
|
plugins: {},
|
|
|
|
notifications: {
|
|
timeout: 5000,
|
|
position: "bottom-right",
|
|
useNative: "not-focused",
|
|
logLimit: 50
|
|
},
|
|
|
|
cloud: {
|
|
authenticated: false,
|
|
url: "https://api.vencord.dev/",
|
|
settingsSync: false,
|
|
settingsSyncVersion: 0
|
|
}
|
|
};
|
|
|
|
const settings = !IS_REPORTER ? VencordNative.settings.get() : {} as Settings;
|
|
mergeDefaults(settings, DefaultSettings);
|
|
|
|
const saveSettingsOnFrequentAction = debounce(async () => {
|
|
if (Settings.cloud.settingsSync && Settings.cloud.authenticated) {
|
|
await putCloudSettings();
|
|
delete localStorage.Vencord_settingsDirty;
|
|
}
|
|
}, 60_000);
|
|
|
|
|
|
export const SettingsStore = new SettingsStoreClass(settings, {
|
|
readOnly: true,
|
|
getDefaultValue({
|
|
target,
|
|
key,
|
|
path
|
|
}) {
|
|
const v = target[key];
|
|
if (!plugins) return v; // plugins not initialised yet. this means this path was reached by being called on the top level
|
|
|
|
if (path === "plugins" && key in plugins)
|
|
return target[key] = {
|
|
enabled: IS_REPORTER ?? plugins[key].required ?? plugins[key].enabledByDefault ?? false
|
|
};
|
|
|
|
// 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?.[key];
|
|
if (!setting) return v;
|
|
|
|
if ("default" in setting)
|
|
// normal setting with a default value
|
|
return (target[key] = setting.default);
|
|
|
|
if (setting.type === OptionType.SELECT) {
|
|
const def = setting.options.find(o => o.default);
|
|
if (def)
|
|
target[key] = def.value;
|
|
return def?.value;
|
|
}
|
|
}
|
|
}
|
|
return v;
|
|
}
|
|
});
|
|
|
|
if (!IS_REPORTER) {
|
|
SettingsStore.addGlobalChangeListener((_, path) => {
|
|
SettingsStore.plain.cloud.settingsSyncVersion = Date.now();
|
|
localStorage.Vencord_settingsDirty = true;
|
|
saveSettingsOnFrequentAction();
|
|
VencordNative.settings.set(SettingsStore.plain, path);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
/**
|
|
* A smart settings object. Altering props automagically saves
|
|
* the updated settings to disk.
|
|
* This recursively proxies objects. If you need the object non proxied, use {@link PlainSettings}
|
|
*/
|
|
export const Settings = SettingsStore.store;
|
|
|
|
/**
|
|
* Settings hook for React components. Returns a smart settings
|
|
* object that automagically triggers a rerender if any properties
|
|
* are altered
|
|
* @param paths An optional list of paths to whitelist for rerenders
|
|
* @returns Settings
|
|
*/
|
|
// TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later
|
|
export function useSettings(paths?: UseSettings<Settings>[]) {
|
|
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
|
|
|
React.useEffect(() => {
|
|
if (paths) {
|
|
paths.forEach(p => SettingsStore.addChangeListener(p, forceUpdate));
|
|
return () => paths.forEach(p => SettingsStore.removeChangeListener(p, forceUpdate));
|
|
} else {
|
|
SettingsStore.addGlobalChangeListener(forceUpdate);
|
|
return () => SettingsStore.removeGlobalChangeListener(forceUpdate);
|
|
}
|
|
}, []);
|
|
|
|
return SettingsStore.store;
|
|
}
|
|
|
|
export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
|
const { plugins } = SettingsStore.plain;
|
|
if (name in plugins) return;
|
|
|
|
for (const oldName of oldNames) {
|
|
if (oldName in plugins) {
|
|
logger.info(`Migrating settings from old name ${oldName} to ${name}`);
|
|
plugins[name] = plugins[oldName];
|
|
delete plugins[oldName];
|
|
SettingsStore.markAsChanged();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function definePluginSettings<
|
|
Def extends SettingsDefinition,
|
|
Checks extends SettingsChecks<Def>,
|
|
PrivateSettings extends object = {}
|
|
>(def: Def, checks?: Checks) {
|
|
const definedSettings: DefinedSettings<Def, Checks, PrivateSettings> = {
|
|
get store() {
|
|
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
|
|
return Settings.plugins[definedSettings.pluginName] as any;
|
|
},
|
|
use: settings => useSettings(
|
|
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings<Settings>[]
|
|
).plugins[definedSettings.pluginName] as any,
|
|
def,
|
|
checks: checks ?? {} as any,
|
|
pluginName: "",
|
|
|
|
withPrivateSettings<T extends object>() {
|
|
return this as DefinedSettings<Def, Checks, T>;
|
|
}
|
|
};
|
|
|
|
return definedSettings;
|
|
}
|
|
|
|
type UseSettings<T extends object> = ResolveUseSettings<T>[keyof T];
|
|
|
|
type ResolveUseSettings<T extends object> = {
|
|
[Key in keyof T]:
|
|
Key extends string
|
|
? T[Key] extends Record<string, unknown>
|
|
// @ts-ignore "Type instantiation is excessively deep and possibly infinite"
|
|
? UseSettings<T[Key]> extends string ? `${Key}.${UseSettings<T[Key]>}` : never
|
|
: Key
|
|
: never;
|
|
};
|