diff options
author | Justice Almanzar <superdash993@gmail.com> | 2023-01-13 17:15:45 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-13 23:15:45 +0100 |
commit | ea748dfb605386b80a4919183ad6fa9249a82e21 (patch) | |
tree | 8660c5d192ac553e7574d6b510e18f99c7ac5ddd | |
parent | 6c5fcc4119d05389bbc71bd3e52090f6fd29b10c (diff) | |
download | Vencord-ea748dfb605386b80a4919183ad6fa9249a82e21.tar.gz Vencord-ea748dfb605386b80a4919183ad6fa9249a82e21.tar.bz2 Vencord-ea748dfb605386b80a4919183ad6fa9249a82e21.zip |
feat: Typesafe Settings Definitions (#403)
Co-authored-by: Ven <vendicated@riseup.net>
-rw-r--r-- | src/api/settings.ts | 19 | ||||
-rw-r--r-- | src/components/PluginSettings/PluginModal.tsx | 1 | ||||
-rw-r--r-- | src/components/PluginSettings/components/SettingBooleanComponent.tsx | 6 | ||||
-rw-r--r-- | src/components/PluginSettings/components/SettingNumericComponent.tsx | 6 | ||||
-rw-r--r-- | src/components/PluginSettings/components/SettingSelectComponent.tsx | 6 | ||||
-rw-r--r-- | src/components/PluginSettings/components/SettingSliderComponent.tsx | 6 | ||||
-rw-r--r-- | src/components/PluginSettings/components/SettingTextComponent.tsx | 6 | ||||
-rw-r--r-- | src/components/PluginSettings/components/index.ts | 3 | ||||
-rw-r--r-- | src/plugins/index.ts | 12 | ||||
-rw-r--r-- | src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts | 14 | ||||
-rw-r--r-- | src/plugins/shikiCodeblocks/index.ts | 104 | ||||
-rw-r--r-- | src/plugins/shikiCodeblocks/settings.ts | 123 | ||||
-rw-r--r-- | src/plugins/shikiCodeblocks/types.ts | 14 | ||||
-rw-r--r-- | src/plugins/shikiCodeblocks/utils/misc.ts | 7 | ||||
-rw-r--r-- | src/utils/types.ts | 139 |
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; |