aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJustice Almanzar <superdash993@gmail.com>2023-01-13 17:15:45 -0500
committerGitHub <noreply@github.com>2023-01-13 23:15:45 +0100
commitea748dfb605386b80a4919183ad6fa9249a82e21 (patch)
tree8660c5d192ac553e7574d6b510e18f99c7ac5ddd /src
parent6c5fcc4119d05389bbc71bd3e52090f6fd29b10c (diff)
downloadVencord-ea748dfb605386b80a4919183ad6fa9249a82e21.tar.gz
Vencord-ea748dfb605386b80a4919183ad6fa9249a82e21.tar.bz2
Vencord-ea748dfb605386b80a4919183ad6fa9249a82e21.zip
feat: Typesafe Settings Definitions (#403)
Co-authored-by: Ven <vendicated@riseup.net>
Diffstat (limited to 'src')
-rw-r--r--src/api/settings.ts19
-rw-r--r--src/components/PluginSettings/PluginModal.tsx1
-rw-r--r--src/components/PluginSettings/components/SettingBooleanComponent.tsx6
-rw-r--r--src/components/PluginSettings/components/SettingNumericComponent.tsx6
-rw-r--r--src/components/PluginSettings/components/SettingSelectComponent.tsx6
-rw-r--r--src/components/PluginSettings/components/SettingSliderComponent.tsx6
-rw-r--r--src/components/PluginSettings/components/SettingTextComponent.tsx6
-rw-r--r--src/components/PluginSettings/components/index.ts3
-rw-r--r--src/plugins/index.ts12
-rw-r--r--src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts14
-rw-r--r--src/plugins/shikiCodeblocks/index.ts104
-rw-r--r--src/plugins/shikiCodeblocks/settings.ts123
-rw-r--r--src/plugins/shikiCodeblocks/types.ts14
-rw-r--r--src/plugins/shikiCodeblocks/utils/misc.ts7
-rw-r--r--src/utils/types.ts139
15 files changed, 287 insertions, 179 deletions
diff --git a/src/api/settings.ts b/src/api/settings.ts
index 384647c..9bae8b7 100644
--- a/src/api/settings.ts
+++ b/src/api/settings.ts
@@ -19,7 +19,7 @@
import IpcEvents from "@utils/IpcEvents";
import Logger from "@utils/Logger";
import { mergeDefaults } from "@utils/misc";
-import { OptionType } from "@utils/types";
+import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
import { React } from "@webpack/common";
import plugins from "~plugins";
@@ -146,6 +146,7 @@ export const Settings = makeProxy(settings);
* @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?: string[]) {
const [, forceUpdate] = React.useReducer(() => ({}), {});
@@ -200,3 +201,19 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
}
}
}
+
+export function definePluginSettings<D extends SettingsDefinition, C extends SettingsChecks<D>>(def: D, checks?: C) {
+ const definedSettings: DefinedSettings<D> = {
+ 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}`)
+ ).plugins[definedSettings.pluginName] as any,
+ def,
+ checks: checks ?? {},
+ pluginName: "",
+ };
+ return definedSettings;
+}
diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx
index 4656850..43e1d31 100644
--- a/src/components/PluginSettings/PluginModal.tsx
+++ b/src/components/PluginSettings/PluginModal.tsx
@@ -144,6 +144,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
onChange={onChange}
onError={onError}
pluginSettings={pluginSettings}
+ definedSettings={plugin.settings}
/>
);
});
diff --git a/src/components/PluginSettings/components/SettingBooleanComponent.tsx b/src/components/PluginSettings/components/SettingBooleanComponent.tsx
index 0aaafa0..c90af16 100644
--- a/src/components/PluginSettings/components/SettingBooleanComponent.tsx
+++ b/src/components/PluginSettings/components/SettingBooleanComponent.tsx
@@ -21,7 +21,7 @@ import { Forms, React, Select } from "@webpack/common";
import { ISettingElementProps } from ".";
-export function SettingBooleanComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) {
+export function SettingBooleanComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) {
const def = pluginSettings[id] ?? option.default;
const [state, setState] = React.useState(def ?? false);
@@ -37,7 +37,7 @@ export function SettingBooleanComponent({ option, pluginSettings, id, onChange,
];
function handleChange(newValue: boolean): void {
- const isValid = (option.isValid && option.isValid(newValue)) ?? true;
+ const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else {
@@ -51,7 +51,7 @@ export function SettingBooleanComponent({ option, pluginSettings, id, onChange,
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<Select
- isDisabled={option.disabled?.() ?? false}
+ isDisabled={option.disabled?.call(definedSettings) ?? false}
options={options}
placeholder={option.placeholder ?? "Select an option"}
maxVisibleItems={5}
diff --git a/src/components/PluginSettings/components/SettingNumericComponent.tsx b/src/components/PluginSettings/components/SettingNumericComponent.tsx
index 3457783..12e8e36 100644
--- a/src/components/PluginSettings/components/SettingNumericComponent.tsx
+++ b/src/components/PluginSettings/components/SettingNumericComponent.tsx
@@ -23,7 +23,7 @@ import { ISettingElementProps } from ".";
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);
-export function SettingNumericComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionNumber>) {
+export function SettingNumericComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionNumber>) {
function serialize(value: any) {
if (option.type === OptionType.BIGINT) return BigInt(value);
return Number(value);
@@ -37,7 +37,7 @@ export function SettingNumericComponent({ option, pluginSettings, id, onChange,
}, [error]);
function handleChange(newValue) {
- const isValid = (option.isValid && option.isValid(newValue)) ?? true;
+ const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
@@ -58,7 +58,7 @@ export function SettingNumericComponent({ option, pluginSettings, id, onChange,
value={state}
onChange={handleChange}
placeholder={option.placeholder ?? "Enter a number"}
- disabled={option.disabled?.() ?? false}
+ disabled={option.disabled?.call(definedSettings) ?? false}
{...option.componentProps}
/>
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
diff --git a/src/components/PluginSettings/components/SettingSelectComponent.tsx b/src/components/PluginSettings/components/SettingSelectComponent.tsx
index 8a5bee1..164a29a 100644
--- a/src/components/PluginSettings/components/SettingSelectComponent.tsx
+++ b/src/components/PluginSettings/components/SettingSelectComponent.tsx
@@ -21,7 +21,7 @@ import { Forms, React, Select } from "@webpack/common";
import { ISettingElementProps } from ".";
-export function SettingSelectComponent({ option, pluginSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) {
+export function SettingSelectComponent({ option, pluginSettings, definedSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) {
const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value;
const [state, setState] = React.useState<any>(def ?? null);
@@ -32,7 +32,7 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr
}, [error]);
function handleChange(newValue) {
- const isValid = (option.isValid && option.isValid(newValue)) ?? true;
+ const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else {
@@ -45,7 +45,7 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<Select
- isDisabled={option.disabled?.() ?? false}
+ isDisabled={option.disabled?.call(definedSettings) ?? false}
options={option.options}
placeholder={option.placeholder ?? "Select an option"}
maxVisibleItems={5}
diff --git a/src/components/PluginSettings/components/SettingSliderComponent.tsx b/src/components/PluginSettings/components/SettingSliderComponent.tsx
index 2d31696..3dda19b 100644
--- a/src/components/PluginSettings/components/SettingSliderComponent.tsx
+++ b/src/components/PluginSettings/components/SettingSliderComponent.tsx
@@ -29,7 +29,7 @@ export function makeRange(start: number, end: number, step = 1) {
return ranges;
}
-export function SettingSliderComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionSlider>) {
+export function SettingSliderComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionSlider>) {
const def = pluginSettings[id] ?? option.default;
const [error, setError] = React.useState<string | null>(null);
@@ -39,7 +39,7 @@ export function SettingSliderComponent({ option, pluginSettings, id, onChange, o
}, [error]);
function handleChange(newValue: number): void {
- const isValid = (option.isValid && option.isValid(newValue)) ?? true;
+ const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else {
@@ -52,7 +52,7 @@ export function SettingSliderComponent({ option, pluginSettings, id, onChange, o
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<Slider
- disabled={option.disabled?.() ?? false}
+ disabled={option.disabled?.call(definedSettings) ?? false}
markers={option.markers}
minValue={option.markers[0]}
maxValue={option.markers[option.markers.length - 1]}
diff --git a/src/components/PluginSettings/components/SettingTextComponent.tsx b/src/components/PluginSettings/components/SettingTextComponent.tsx
index b92fcec..599593f 100644
--- a/src/components/PluginSettings/components/SettingTextComponent.tsx
+++ b/src/components/PluginSettings/components/SettingTextComponent.tsx
@@ -21,7 +21,7 @@ import { Forms, React, TextInput } from "@webpack/common";
import { ISettingElementProps } from ".";
-export function SettingTextComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) {
+export function SettingTextComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) {
const [state, setState] = React.useState(pluginSettings[id] ?? option.default ?? null);
const [error, setError] = React.useState<string | null>(null);
@@ -30,7 +30,7 @@ export function SettingTextComponent({ option, pluginSettings, id, onChange, onE
}, [error]);
function handleChange(newValue) {
- const isValid = (option.isValid && option.isValid(newValue)) ?? true;
+ const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else {
@@ -47,7 +47,7 @@ export function SettingTextComponent({ option, pluginSettings, id, onChange, onE
value={state}
onChange={handleChange}
placeholder={option.placeholder ?? "Enter a value"}
- disabled={option.disabled?.() ?? false}
+ disabled={option.disabled?.call(definedSettings) ?? false}
{...option.componentProps}
/>
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
diff --git a/src/components/PluginSettings/components/index.ts b/src/components/PluginSettings/components/index.ts
index d44fb38..52745ea 100644
--- a/src/components/PluginSettings/components/index.ts
+++ b/src/components/PluginSettings/components/index.ts
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { PluginOptionBase } from "@utils/types";
+import { DefinedSettings, PluginOptionBase } from "@utils/types";
export interface ISettingElementProps<T extends PluginOptionBase> {
option: T;
@@ -27,6 +27,7 @@ export interface ISettingElementProps<T extends PluginOptionBase> {
};
id: string;
onError(hasError: boolean): void;
+ definedSettings?: DefinedSettings;
}
export * from "./BadgeComponent";
diff --git a/src/plugins/index.ts b/src/plugins/index.ts
index c0325d4..6ac221d 100644
--- a/src/plugins/index.ts
+++ b/src/plugins/index.ts
@@ -60,7 +60,16 @@ for (const p of pluginsValues) {
});
}
-for (const p of pluginsValues)
+for (const p of pluginsValues) {
+ if (p.settings) {
+ p.settings.pluginName = p.name;
+ p.options ??= {};
+ for (const [name, def] of Object.entries(p.settings.def)) {
+ const checks = p.settings.checks?.[name];
+ p.options[name] = { ...def, ...checks };
+ }
+ }
+
if (p.patches && isPluginEnabled(p.name)) {
for (const patch of p.patches) {
patch.plugin = p.name;
@@ -69,6 +78,7 @@ for (const p of pluginsValues)
patches.push(patch);
}
}
+}
export const startAllPlugins = traceFunction("startAllPlugins", function startAllPlugins() {
for (const name in Plugins)
diff --git a/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts b/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts
index 50b0fc9..22954ce 100644
--- a/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts
+++ b/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts
@@ -16,25 +16,25 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { useSettings } from "@api/settings";
+import { PartialExcept } from "@utils/types";
import { React } from "@webpack/common";
import { shiki } from "../api/shiki";
-import { ShikiSettings } from "../types";
+import { settings as pluginSettings, ShikiSettings } from "../settings";
-export function useShikiSettings(settingKeys: (keyof ShikiSettings)[], overrides?: Record<string, any>) {
- const settings = useSettings(settingKeys.map(key => `plugins.ShikiCodeblocks.${key}`)).plugins.ShikiCodeblocks as ShikiSettings;
+export function useShikiSettings<F extends keyof ShikiSettings>(settingKeys: F[], overrides?: Partial<ShikiSettings>) {
+ const settings: Partial<ShikiSettings> = pluginSettings.use(settingKeys);
const [isLoading, setLoading] = React.useState(false);
- const withOverrides = { ...settings, ...overrides };
+ const withOverrides = { ...settings, ...overrides } as PartialExcept<ShikiSettings, F>;
const themeUrl = withOverrides.customTheme || withOverrides.theme;
if (overrides) {
- const willChangeTheme = shiki.currentThemeUrl && themeUrl !== shiki.currentThemeUrl;
+ const willChangeTheme = shiki.currentThemeUrl && themeUrl && themeUrl !== shiki.currentThemeUrl;
const noOverrides = Object.keys(overrides).length === 0;
if (isLoading && (!willChangeTheme || noOverrides)) setLoading(false);
- if ((!isLoading && willChangeTheme)) {
+ if (!isLoading && willChangeTheme) {
setLoading(true);
shiki.setTheme(themeUrl);
}
diff --git a/src/plugins/shikiCodeblocks/index.ts b/src/plugins/shikiCodeblocks/index.ts
index 428a273..58e55b4 100644
--- a/src/plugins/shikiCodeblocks/index.ts
+++ b/src/plugins/shikiCodeblocks/index.ts
@@ -18,26 +18,19 @@
import "./shiki.css";
-import { disableStyle, enableStyle } from "@api/Styles";
+import { enableStyle } from "@api/Styles";
import { Devs } from "@utils/constants";
-import { parseUrl } from "@utils/misc";
-import { wordsFromPascal, wordsToTitle } from "@utils/text";
-import definePlugin, { OptionType } from "@utils/types";
+import definePlugin from "@utils/types";
import previewExampleText from "~fileContent/previewExample.tsx";
-import { Settings } from "../../Vencord";
import { shiki } from "./api/shiki";
-import { themes } from "./api/themes";
import { createHighlighter } from "./components/Highlighter";
import deviconStyle from "./devicon.css?managed";
-import { DeviconSetting, HljsSetting, ShikiSettings } from "./types";
+import { settings } from "./settings";
+import { DeviconSetting } from "./types";
import { clearStyles } from "./utils/createStyle";
-const themeNames = Object.keys(themes);
-
-const getSettings = () => Settings.plugins.ShikiCodeblocks as ShikiSettings;
-
export default definePlugin({
name: "ShikiCodeblocks",
description: "Brings vscode-style codeblocks into Discord, powered by Shiki",
@@ -52,10 +45,10 @@ export default definePlugin({
},
],
start: async () => {
- if (getSettings().useDevIcon !== DeviconSetting.Disabled)
+ if (settings.store.useDevIcon !== DeviconSetting.Disabled)
enableStyle(deviconStyle);
- await shiki.init(getSettings().customTheme || getSettings().theme);
+ await shiki.init(settings.store.customTheme || settings.store.theme);
},
stop: () => {
shiki.destroy();
@@ -67,90 +60,7 @@ export default definePlugin({
isPreview: true,
tempSettings,
}),
- options: {
- theme: {
- type: OptionType.SELECT,
- description: "Default themes",
- options: themeNames.map(themeName => ({
- label: wordsToTitle(wordsFromPascal(themeName)),
- value: themes[themeName],
- default: themes[themeName] === themes.DarkPlus,
- })),
- disabled: () => !!getSettings().customTheme,
- onChange: shiki.setTheme,
- },
- customTheme: {
- type: OptionType.STRING,
- description: "A link to a custom vscode theme",
- placeholder: themes.MaterialCandy,
- isValid: value => {
- if (!value) return true;
- const url = parseUrl(value);
- if (!url) return "Must be a valid URL";
-
- if (!url.pathname.endsWith(".json")) return "Must be a json file";
-
- return true;
- },
- onChange: value => shiki.setTheme(value || getSettings().theme),
- },
- tryHljs: {
- type: OptionType.SELECT,
- description: "Use the more lightweight default Discord highlighter and theme.",
- options: [
- {
- label: "Never",
- value: HljsSetting.Never,
- },
- {
- label: "Prefer Shiki instead of Highlight.js",
- value: HljsSetting.Secondary,
- default: true,
- },
- {
- label: "Prefer Highlight.js instead of Shiki",
- value: HljsSetting.Primary,
- },
- {
- label: "Always",
- value: HljsSetting.Always,
- },
- ],
- },
- useDevIcon: {
- type: OptionType.SELECT,
- description: "How to show language icons on codeblocks",
- options: [
- {
- label: "Disabled",
- value: DeviconSetting.Disabled,
- },
- {
- label: "Colorless",
- value: DeviconSetting.Greyscale,
- default: true,
- },
- {
- label: "Colored",
- value: DeviconSetting.Color,
- },
- ],
- onChange: (newValue: DeviconSetting) => {
- if (newValue === DeviconSetting.Disabled) disableStyle(deviconStyle);
- else enableStyle(deviconStyle);
- },
- },
- bgOpacity: {
- type: OptionType.SLIDER,
- description: "Background opacity",
- markers: [0, 20, 40, 60, 80, 100],
- default: 100,
- componentProps: {
- stickToMarkers: false,
- onValueRender: null, // Defaults to percentage
- },
- },
- },
+ settings,
// exports
shiki,
diff --git a/src/plugins/shikiCodeblocks/settings.ts b/src/plugins/shikiCodeblocks/settings.ts
new file mode 100644
index 0000000..ff5afc2
--- /dev/null
+++ b/src/plugins/shikiCodeblocks/settings.ts
@@ -0,0 +1,123 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2023 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 { definePluginSettings } from "@api/settings";
+import { disableStyle, enableStyle } from "@api/Styles";
+import { parseUrl } from "@utils/misc";
+import { wordsFromPascal, wordsToTitle } from "@utils/text";
+import { OptionType } from "@utils/types";
+
+import { shiki } from "./api/shiki";
+import { themes } from "./api/themes";
+import deviconStyle from "./devicon.css?managed";
+import { DeviconSetting, HljsSetting } from "./types";
+
+const themeNames = Object.keys(themes) as (keyof typeof themes)[];
+
+export type ShikiSettings = typeof settings.store;
+export const settings = definePluginSettings({
+ theme: {
+ type: OptionType.SELECT,
+ description: "Default themes",
+ options: themeNames.map(themeName => ({
+ label: wordsToTitle(wordsFromPascal(themeName)),
+ value: themes[themeName],
+ default: themes[themeName] === themes.DarkPlus,
+ })),
+ onChange: shiki.setTheme,
+ },
+ customTheme: {
+ type: OptionType.STRING,
+ description: "A link to a custom vscode theme",
+ placeholder: themes.MaterialCandy,
+ onChange: value => {
+ shiki.setTheme(value || settings.store.theme);
+ },
+ },
+ tryHljs: {
+ type: OptionType.SELECT,
+ description: "Use the more lightweight default Discord highlighter and theme.",
+ options: [
+ {
+ label: "Never",
+ value: HljsSetting.Never,
+ },
+ {
+ label: "Prefer Shiki instead of Highlight.js",
+ value: HljsSetting.Secondary,
+ default: true,
+ },
+ {
+ label: "Prefer Highlight.js instead of Shiki",
+ value: HljsSetting.Primary,
+ },
+ {
+ label: "Always",
+ value: HljsSetting.Always,
+ },
+ ],
+ },
+ useDevIcon: {
+ type: OptionType.SELECT,
+ description: "How to show language icons on codeblocks",
+ options: [
+ {
+ label: "Disabled",
+ value: DeviconSetting.Disabled,
+ },
+ {
+ label: "Colorless",
+ value: DeviconSetting.Greyscale,
+ default: true,
+ },
+ {
+ label: "Colored",
+ value: DeviconSetting.Color,
+ },
+ ],
+ onChange: (newValue: DeviconSetting) => {
+ if (newValue === DeviconSetting.Disabled) disableStyle(deviconStyle);
+ else enableStyle(deviconStyle);
+ },
+ },
+ bgOpacity: {
+ type: OptionType.SLIDER,
+ description: "Background opacity",
+ markers: [0, 20, 40, 60, 80, 100],
+ default: 100,
+ componentProps: {
+ stickToMarkers: false,
+ onValueRender: null, // Defaults to percentage
+ },
+ },
+}, {
+ theme: {
+ disabled() { return !!this.store.customTheme; },
+ },
+ customTheme: {
+ isValid(value) {
+ if (!value) return true;
+ const url = parseUrl(value);
+ if (!url) return "Must be a valid URL";
+
+ if (!url.pathname.endsWith(".json")) return "Must be a json file";
+
+ return true;
+ },
+ }
+});
diff --git a/src/plugins/shikiCodeblocks/types.ts b/src/plugins/shikiCodeblocks/types.ts
index ee5aa9e..e724ea4 100644
--- a/src/plugins/shikiCodeblocks/types.ts
+++ b/src/plugins/shikiCodeblocks/types.ts
@@ -23,8 +23,6 @@ import type {
IThemeRegistration,
} from "@vap/shiki";
-import type { Settings } from "../../Vencord";
-
/** This must be atleast a subset of the `@vap/shiki-worker` spec */
export type ShikiSpec = {
setOnigasm: ({ wasm }: { wasm: string; }) => Promise<void>;
@@ -64,15 +62,3 @@ export enum DeviconSetting {
Greyscale = "GREYSCALE",
Color = "COLOR"
}
-
-type CommonSettings = {
- [K in keyof Settings["plugins"][string]as K extends `${infer V}` ? K : never]: Settings["plugins"][string][K];
-};
-
-export interface ShikiSettings extends CommonSettings {
- theme: string;
- customTheme: string;
- tryHljs: HljsSetting;
- useDevIcon: DeviconSetting;
- bgOpacity: number;
-}
diff --git a/src/plugins/shikiCodeblocks/utils/misc.ts b/src/plugins/shikiCodeblocks/utils/misc.ts
index fefe938..e0c5263 100644
--- a/src/plugins/shikiCodeblocks/utils/misc.ts
+++ b/src/plugins/shikiCodeblocks/utils/misc.ts
@@ -21,7 +21,7 @@ import { hljs } from "@webpack/common";
import { resolveLang } from "../api/languages";
import { HighlighterProps } from "../components/Highlighter";
-import { HljsSetting, ShikiSettings } from "../types";
+import { HljsSetting } from "../types";
export const cl = classNameFactory("shiki-");
@@ -30,7 +30,7 @@ export const shouldUseHljs = ({
tryHljs,
}: {
lang: HighlighterProps["lang"],
- tryHljs: ShikiSettings["tryHljs"],
+ tryHljs: HljsSetting,
}) => {
const hljsLang = lang ? hljs?.getLanguage?.(lang) : null;
const shikiLang = lang ? resolveLang(lang) : null;
@@ -45,7 +45,6 @@ export const shouldUseHljs = ({
return !langName && !!hljsLang;
case HljsSetting.Never:
return false;
+ default: return false;
}
-
- return false;
};
diff --git a/src/utils/types.ts b/src/utils/types.ts
index d3083fc..5ab6857 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -81,9 +81,15 @@ export interface PluginDef {
target?: "WEB" | "DESKTOP" | "BOTH";
/**
* Optionally provide settings that the user can configure in the Plugins tab of settings.
+ * @deprecated Use `settings` instead
*/
+ // TODO: Remove when everything is migrated to `settings`
options?: Record<string, PluginOptionsItem>;
/**
+ * Optionally provide settings that the user can configure in the Plugins tab of settings.
+ */
+ settings?: DefinedSettings;
+ /**
* Check that this returns true before allowing a save to complete.
* If a string is returned, show the error to the user.
*/
@@ -107,19 +113,25 @@ export enum OptionType {
COMPONENT,
}
-export type PluginOptionsItem =
- | PluginOptionString
- | PluginOptionNumber
- | PluginOptionBoolean
- | PluginOptionSelect
- | PluginOptionSlider
- | PluginOptionComponent;
+export type SettingsDefinition = Record<string, PluginSettingDef>;
+export type SettingsChecks<D extends SettingsDefinition> = {
+ [K in keyof D]?: D[K] extends PluginSettingComponentDef ? IsDisabled<DefinedSettings<D>> :
+ (IsDisabled<DefinedSettings<D>> & IsValid<PluginSettingType<D[K]>, DefinedSettings<D>>);
+};
-export interface PluginOptionBase {
+export type PluginSettingDef = (
+ | PluginSettingStringDef
+ | PluginSettingNumberDef
+ | PluginSettingBooleanDef
+ | PluginSettingSelectDef
+ | PluginSettingSliderDef
+ | PluginSettingComponentDef
+) & PluginSettingCommon;
+
+export interface PluginSettingCommon {
description: string;
placeholder?: string;
onChange?(newValue: any): void;
- disabled?(): boolean;
restartNeeded?: boolean;
componentProps?: Record<string, any>;
/**
@@ -127,49 +139,47 @@ export interface PluginOptionBase {
*/
target?: "WEB" | "DESKTOP" | "BOTH";
}
-
-export interface PluginOptionString extends PluginOptionBase {
- type: OptionType.STRING;
+interface IsDisabled<D = unknown> {
/**
- * Prevents the user from saving settings if this is false or a string
+ * Checks if this setting should be disabled
*/
- isValid?(value: string): boolean | string;
- default?: string;
+ disabled?(this: D): boolean;
}
-
-export interface PluginOptionNumber extends PluginOptionBase {
- type: OptionType.NUMBER | OptionType.BIGINT;
+interface IsValid<T, D = unknown> {
/**
* Prevents the user from saving settings if this is false or a string
*/
- isValid?(value: number | BigInt): boolean | string;
- default?: number;
+ isValid?(this: D, value: T): boolean | string;
}
-export interface PluginOptionBoolean extends PluginOptionBase {
+export interface PluginSettingStringDef {
+ type: OptionType.STRING;
+ default?: string;
+}
+export interface PluginSettingNumberDef {
+ type: OptionType.NUMBER;
+ default?: number;
+}
+export interface PluginSettingBigIntDef {
+ type: OptionType.BIGINT;
+ default?: BigInt;
+}
+export interface PluginSettingBooleanDef {
type: OptionType.BOOLEAN;
- /**
- * Prevents the user from saving settings if this is false or a string
- */
- isValid?(value: boolean): boolean | string;
default?: boolean;
}
-export interface PluginOptionSelect extends PluginOptionBase {
+export interface PluginSettingSelectDef {
type: OptionType.SELECT;
- /**
- * Prevents the user from saving settings if this is false or a string
- */
- isValid?(value: PluginOptionSelectOption): boolean | string;
- options: PluginOptionSelectOption[];
+ options: readonly PluginSettingSelectOption[];
}
-export interface PluginOptionSelectOption {
+export interface PluginSettingSelectOption {
label: string;
value: string | number | boolean;
default?: boolean;
}
-export interface PluginOptionSlider extends PluginOptionBase {
+export interface PluginSettingSliderDef {
type: OptionType.SLIDER;
/**
* All the possible values in the slider. Needs at least two values.
@@ -183,10 +193,6 @@ export interface PluginOptionSlider extends PluginOptionBase {
* If false, allow users to select values in-between your markers.
*/
stickToMarkers?: boolean;
- /**
- * Prevents the user from saving settings if this is false or a string
- */
- isValid?(value: number): boolean | string;
}
interface IPluginOptionComponentProps {
@@ -206,12 +212,67 @@ interface IPluginOptionComponentProps {
/**
* The options object
*/
- option: PluginOptionComponent;
+ option: PluginSettingComponentDef;
}
-export interface PluginOptionComponent extends PluginOptionBase {
+export interface PluginSettingComponentDef {
type: OptionType.COMPONENT;
component: (props: IPluginOptionComponentProps) => JSX.Element;
}
+/** Maps a `PluginSettingDef` to its value type */
+type PluginSettingType<O extends PluginSettingDef> = O extends PluginSettingStringDef ? string :
+ O extends PluginSettingNumberDef ? number :
+ O extends PluginSettingBigIntDef ? BigInt :
+ O extends PluginSettingBooleanDef ? boolean :
+ O extends PluginSettingSelectDef ? O["options"][number]["value"] :
+ O extends PluginSettingSliderDef ? number :
+ O extends PluginSettingComponentDef ? any :
+ never;
+
+type SettingsStore<D extends SettingsDefinition> = {
+ [K in keyof D]: PluginSettingType<D[K]>;
+};
+
+/** An instance of defined plugin settings */
+export interface DefinedSettings<D extends SettingsDefinition = SettingsDefinition, C extends SettingsChecks<D> = {}> {
+ /** Shorthand for `Vencord.Settings.plugins.PluginName`, but with typings */
+ store: SettingsStore<D>;
+ /**
+ * React hook for getting the settings for this plugin
+ * @param filter optional filter to avoid rerenders for irrelavent settings
+ */
+ use<F extends Extract<keyof D, string>>(filter?: F[]): Pick<SettingsStore<D>, F>;
+ /** Definitions of each setting */
+ def: D;
+ /** Setting methods with return values that could rely on other settings */
+ checks: C;
+ /**
+ * Name of the plugin these settings belong to,
+ * will be an empty string until plugin is initialized
+ */
+ pluginName: string;
+}
+
+export type PartialExcept<T, R extends keyof T> = Partial<T> & Required<Pick<T, R>>;
+
export type IpcRes<V = any> = { ok: true; value: V; } | { ok: false, error: any; };
+
+/* -------------------------------------------- */
+/* Legacy Options Types */
+/* -------------------------------------------- */
+
+export type PluginOptionBase = PluginSettingCommon & IsDisabled;
+export type PluginOptionsItem =
+ | PluginOptionString
+ | PluginOptionNumber
+ | PluginOptionBoolean
+ | PluginOptionSelect
+ | PluginOptionSlider
+ | PluginOptionComponent;
+export type PluginOptionString = PluginSettingStringDef & PluginSettingCommon & IsDisabled & IsValid<string>;
+export type PluginOptionNumber = (PluginSettingNumberDef | PluginSettingBigIntDef) & PluginSettingCommon & IsDisabled & IsValid<number | BigInt>;
+export type PluginOptionBoolean = PluginSettingBooleanDef & PluginSettingCommon & IsDisabled & IsValid<boolean>;
+export type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & IsDisabled & IsValid<PluginSettingSelectOption>;
+export type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid<number>;
+export type PluginOptionComponent = PluginSettingComponentDef & PluginSettingCommon;