feat: Typesafe Settings Definitions (#403)
Co-authored-by: Ven <vendicated@riseup.net>
This commit is contained in:
parent
6c5fcc4119
commit
ea748dfb60
15 changed files with 288 additions and 180 deletions
|
@ -19,7 +19,7 @@
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
import IpcEvents from "@utils/IpcEvents";
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
import { mergeDefaults } from "@utils/misc";
|
import { mergeDefaults } from "@utils/misc";
|
||||||
import { OptionType } from "@utils/types";
|
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
||||||
import { React } from "@webpack/common";
|
import { React } from "@webpack/common";
|
||||||
|
|
||||||
import plugins from "~plugins";
|
import plugins from "~plugins";
|
||||||
|
@ -146,6 +146,7 @@ export const Settings = makeProxy(settings);
|
||||||
* @param paths An optional list of paths to whitelist for rerenders
|
* @param paths An optional list of paths to whitelist for rerenders
|
||||||
* @returns Settings
|
* @returns Settings
|
||||||
*/
|
*/
|
||||||
|
// TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later
|
||||||
export function useSettings(paths?: string[]) {
|
export function useSettings(paths?: string[]) {
|
||||||
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -144,6 +144,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
pluginSettings={pluginSettings}
|
pluginSettings={pluginSettings}
|
||||||
|
definedSettings={plugin.settings}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { Forms, React, Select } from "@webpack/common";
|
||||||
|
|
||||||
import { ISettingElementProps } from ".";
|
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 def = pluginSettings[id] ?? option.default;
|
||||||
|
|
||||||
const [state, setState] = React.useState(def ?? false);
|
const [state, setState] = React.useState(def ?? false);
|
||||||
|
@ -37,7 +37,7 @@ export function SettingBooleanComponent({ option, pluginSettings, id, onChange,
|
||||||
];
|
];
|
||||||
|
|
||||||
function handleChange(newValue: boolean): void {
|
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);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else {
|
||||||
|
@ -51,7 +51,7 @@ export function SettingBooleanComponent({ option, pluginSettings, id, onChange,
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||||
<Select
|
<Select
|
||||||
isDisabled={option.disabled?.() ?? false}
|
isDisabled={option.disabled?.call(definedSettings) ?? false}
|
||||||
options={options}
|
options={options}
|
||||||
placeholder={option.placeholder ?? "Select an option"}
|
placeholder={option.placeholder ?? "Select an option"}
|
||||||
maxVisibleItems={5}
|
maxVisibleItems={5}
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);
|
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) {
|
function serialize(value: any) {
|
||||||
if (option.type === OptionType.BIGINT) return BigInt(value);
|
if (option.type === OptionType.BIGINT) return BigInt(value);
|
||||||
return Number(value);
|
return Number(value);
|
||||||
|
@ -37,7 +37,7 @@ export function SettingNumericComponent({ option, pluginSettings, id, onChange,
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
function handleChange(newValue) {
|
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);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
||||||
|
@ -58,7 +58,7 @@ export function SettingNumericComponent({ option, pluginSettings, id, onChange,
|
||||||
value={state}
|
value={state}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder={option.placeholder ?? "Enter a number"}
|
placeholder={option.placeholder ?? "Enter a number"}
|
||||||
disabled={option.disabled?.() ?? false}
|
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||||
{...option.componentProps}
|
{...option.componentProps}
|
||||||
/>
|
/>
|
||||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { Forms, React, Select } from "@webpack/common";
|
||||||
|
|
||||||
import { ISettingElementProps } from ".";
|
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 def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value;
|
||||||
|
|
||||||
const [state, setState] = React.useState<any>(def ?? null);
|
const [state, setState] = React.useState<any>(def ?? null);
|
||||||
|
@ -32,7 +32,7 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
function handleChange(newValue) {
|
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);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else {
|
||||||
|
@ -45,7 +45,7 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||||
<Select
|
<Select
|
||||||
isDisabled={option.disabled?.() ?? false}
|
isDisabled={option.disabled?.call(definedSettings) ?? false}
|
||||||
options={option.options}
|
options={option.options}
|
||||||
placeholder={option.placeholder ?? "Select an option"}
|
placeholder={option.placeholder ?? "Select an option"}
|
||||||
maxVisibleItems={5}
|
maxVisibleItems={5}
|
||||||
|
|
|
@ -29,7 +29,7 @@ export function makeRange(start: number, end: number, step = 1) {
|
||||||
return ranges;
|
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 def = pluginSettings[id] ?? option.default;
|
||||||
|
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
@ -39,7 +39,7 @@ export function SettingSliderComponent({ option, pluginSettings, id, onChange, o
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
function handleChange(newValue: number): void {
|
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);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else {
|
||||||
|
@ -52,7 +52,7 @@ export function SettingSliderComponent({ option, pluginSettings, id, onChange, o
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||||
<Slider
|
<Slider
|
||||||
disabled={option.disabled?.() ?? false}
|
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||||
markers={option.markers}
|
markers={option.markers}
|
||||||
minValue={option.markers[0]}
|
minValue={option.markers[0]}
|
||||||
maxValue={option.markers[option.markers.length - 1]}
|
maxValue={option.markers[option.markers.length - 1]}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { Forms, React, TextInput } from "@webpack/common";
|
||||||
|
|
||||||
import { ISettingElementProps } from ".";
|
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 [state, setState] = React.useState(pluginSettings[id] ?? option.default ?? null);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ export function SettingTextComponent({ option, pluginSettings, id, onChange, onE
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
function handleChange(newValue) {
|
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);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else {
|
||||||
|
@ -47,7 +47,7 @@ export function SettingTextComponent({ option, pluginSettings, id, onChange, onE
|
||||||
value={state}
|
value={state}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder={option.placeholder ?? "Enter a value"}
|
placeholder={option.placeholder ?? "Enter a value"}
|
||||||
disabled={option.disabled?.() ?? false}
|
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||||
{...option.componentProps}
|
{...option.componentProps}
|
||||||
/>
|
/>
|
||||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* 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> {
|
export interface ISettingElementProps<T extends PluginOptionBase> {
|
||||||
option: T;
|
option: T;
|
||||||
|
@ -27,6 +27,7 @@ export interface ISettingElementProps<T extends PluginOptionBase> {
|
||||||
};
|
};
|
||||||
id: string;
|
id: string;
|
||||||
onError(hasError: boolean): void;
|
onError(hasError: boolean): void;
|
||||||
|
definedSettings?: DefinedSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export * from "./BadgeComponent";
|
export * from "./BadgeComponent";
|
||||||
|
|
|
@ -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)) {
|
if (p.patches && isPluginEnabled(p.name)) {
|
||||||
for (const patch of p.patches) {
|
for (const patch of p.patches) {
|
||||||
patch.plugin = p.name;
|
patch.plugin = p.name;
|
||||||
|
@ -69,6 +78,7 @@ for (const p of pluginsValues)
|
||||||
patches.push(patch);
|
patches.push(patch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const startAllPlugins = traceFunction("startAllPlugins", function startAllPlugins() {
|
export const startAllPlugins = traceFunction("startAllPlugins", function startAllPlugins() {
|
||||||
for (const name in Plugins)
|
for (const name in Plugins)
|
||||||
|
|
|
@ -16,25 +16,25 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* 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 { React } from "@webpack/common";
|
||||||
|
|
||||||
import { shiki } from "../api/shiki";
|
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>) {
|
export function useShikiSettings<F extends keyof ShikiSettings>(settingKeys: F[], overrides?: Partial<ShikiSettings>) {
|
||||||
const settings = useSettings(settingKeys.map(key => `plugins.ShikiCodeblocks.${key}`)).plugins.ShikiCodeblocks as ShikiSettings;
|
const settings: Partial<ShikiSettings> = pluginSettings.use(settingKeys);
|
||||||
const [isLoading, setLoading] = React.useState(false);
|
const [isLoading, setLoading] = React.useState(false);
|
||||||
|
|
||||||
const withOverrides = { ...settings, ...overrides };
|
const withOverrides = { ...settings, ...overrides } as PartialExcept<ShikiSettings, F>;
|
||||||
const themeUrl = withOverrides.customTheme || withOverrides.theme;
|
const themeUrl = withOverrides.customTheme || withOverrides.theme;
|
||||||
|
|
||||||
if (overrides) {
|
if (overrides) {
|
||||||
const willChangeTheme = shiki.currentThemeUrl && themeUrl !== shiki.currentThemeUrl;
|
const willChangeTheme = shiki.currentThemeUrl && themeUrl && themeUrl !== shiki.currentThemeUrl;
|
||||||
const noOverrides = Object.keys(overrides).length === 0;
|
const noOverrides = Object.keys(overrides).length === 0;
|
||||||
|
|
||||||
if (isLoading && (!willChangeTheme || noOverrides)) setLoading(false);
|
if (isLoading && (!willChangeTheme || noOverrides)) setLoading(false);
|
||||||
if ((!isLoading && willChangeTheme)) {
|
if (!isLoading && willChangeTheme) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
shiki.setTheme(themeUrl);
|
shiki.setTheme(themeUrl);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,26 +18,19 @@
|
||||||
|
|
||||||
import "./shiki.css";
|
import "./shiki.css";
|
||||||
|
|
||||||
import { disableStyle, enableStyle } from "@api/Styles";
|
import { enableStyle } from "@api/Styles";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { parseUrl } from "@utils/misc";
|
import definePlugin from "@utils/types";
|
||||||
import { wordsFromPascal, wordsToTitle } from "@utils/text";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
|
|
||||||
import previewExampleText from "~fileContent/previewExample.tsx";
|
import previewExampleText from "~fileContent/previewExample.tsx";
|
||||||
|
|
||||||
import { Settings } from "../../Vencord";
|
|
||||||
import { shiki } from "./api/shiki";
|
import { shiki } from "./api/shiki";
|
||||||
import { themes } from "./api/themes";
|
|
||||||
import { createHighlighter } from "./components/Highlighter";
|
import { createHighlighter } from "./components/Highlighter";
|
||||||
import deviconStyle from "./devicon.css?managed";
|
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";
|
import { clearStyles } from "./utils/createStyle";
|
||||||
|
|
||||||
const themeNames = Object.keys(themes);
|
|
||||||
|
|
||||||
const getSettings = () => Settings.plugins.ShikiCodeblocks as ShikiSettings;
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ShikiCodeblocks",
|
name: "ShikiCodeblocks",
|
||||||
description: "Brings vscode-style codeblocks into Discord, powered by Shiki",
|
description: "Brings vscode-style codeblocks into Discord, powered by Shiki",
|
||||||
|
@ -52,10 +45,10 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
start: async () => {
|
start: async () => {
|
||||||
if (getSettings().useDevIcon !== DeviconSetting.Disabled)
|
if (settings.store.useDevIcon !== DeviconSetting.Disabled)
|
||||||
enableStyle(deviconStyle);
|
enableStyle(deviconStyle);
|
||||||
|
|
||||||
await shiki.init(getSettings().customTheme || getSettings().theme);
|
await shiki.init(settings.store.customTheme || settings.store.theme);
|
||||||
},
|
},
|
||||||
stop: () => {
|
stop: () => {
|
||||||
shiki.destroy();
|
shiki.destroy();
|
||||||
|
@ -67,90 +60,7 @@ export default definePlugin({
|
||||||
isPreview: true,
|
isPreview: true,
|
||||||
tempSettings,
|
tempSettings,
|
||||||
}),
|
}),
|
||||||
options: {
|
settings,
|
||||||
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
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// exports
|
// exports
|
||||||
shiki,
|
shiki,
|
||||||
|
|
123
src/plugins/shikiCodeblocks/settings.ts
Normal file
123
src/plugins/shikiCodeblocks/settings.ts
Normal file
|
@ -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;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
|
@ -23,8 +23,6 @@ import type {
|
||||||
IThemeRegistration,
|
IThemeRegistration,
|
||||||
} from "@vap/shiki";
|
} from "@vap/shiki";
|
||||||
|
|
||||||
import type { Settings } from "../../Vencord";
|
|
||||||
|
|
||||||
/** This must be atleast a subset of the `@vap/shiki-worker` spec */
|
/** This must be atleast a subset of the `@vap/shiki-worker` spec */
|
||||||
export type ShikiSpec = {
|
export type ShikiSpec = {
|
||||||
setOnigasm: ({ wasm }: { wasm: string; }) => Promise<void>;
|
setOnigasm: ({ wasm }: { wasm: string; }) => Promise<void>;
|
||||||
|
@ -64,15 +62,3 @@ export enum DeviconSetting {
|
||||||
Greyscale = "GREYSCALE",
|
Greyscale = "GREYSCALE",
|
||||||
Color = "COLOR"
|
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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { hljs } from "@webpack/common";
|
||||||
|
|
||||||
import { resolveLang } from "../api/languages";
|
import { resolveLang } from "../api/languages";
|
||||||
import { HighlighterProps } from "../components/Highlighter";
|
import { HighlighterProps } from "../components/Highlighter";
|
||||||
import { HljsSetting, ShikiSettings } from "../types";
|
import { HljsSetting } from "../types";
|
||||||
|
|
||||||
export const cl = classNameFactory("shiki-");
|
export const cl = classNameFactory("shiki-");
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ export const shouldUseHljs = ({
|
||||||
tryHljs,
|
tryHljs,
|
||||||
}: {
|
}: {
|
||||||
lang: HighlighterProps["lang"],
|
lang: HighlighterProps["lang"],
|
||||||
tryHljs: ShikiSettings["tryHljs"],
|
tryHljs: HljsSetting,
|
||||||
}) => {
|
}) => {
|
||||||
const hljsLang = lang ? hljs?.getLanguage?.(lang) : null;
|
const hljsLang = lang ? hljs?.getLanguage?.(lang) : null;
|
||||||
const shikiLang = lang ? resolveLang(lang) : null;
|
const shikiLang = lang ? resolveLang(lang) : null;
|
||||||
|
@ -45,7 +45,6 @@ export const shouldUseHljs = ({
|
||||||
return !langName && !!hljsLang;
|
return !langName && !!hljsLang;
|
||||||
case HljsSetting.Never:
|
case HljsSetting.Never:
|
||||||
return false;
|
return false;
|
||||||
|
default: return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -81,8 +81,14 @@ export interface PluginDef {
|
||||||
target?: "WEB" | "DESKTOP" | "BOTH";
|
target?: "WEB" | "DESKTOP" | "BOTH";
|
||||||
/**
|
/**
|
||||||
* Optionally provide settings that the user can configure in the Plugins tab of settings.
|
* 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>;
|
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.
|
* Check that this returns true before allowing a save to complete.
|
||||||
* If a string is returned, show the error to the user.
|
* If a string is returned, show the error to the user.
|
||||||
|
@ -107,19 +113,25 @@ export enum OptionType {
|
||||||
COMPONENT,
|
COMPONENT,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PluginOptionsItem =
|
export type SettingsDefinition = Record<string, PluginSettingDef>;
|
||||||
| PluginOptionString
|
export type SettingsChecks<D extends SettingsDefinition> = {
|
||||||
| PluginOptionNumber
|
[K in keyof D]?: D[K] extends PluginSettingComponentDef ? IsDisabled<DefinedSettings<D>> :
|
||||||
| PluginOptionBoolean
|
(IsDisabled<DefinedSettings<D>> & IsValid<PluginSettingType<D[K]>, DefinedSettings<D>>);
|
||||||
| PluginOptionSelect
|
};
|
||||||
| PluginOptionSlider
|
|
||||||
| PluginOptionComponent;
|
|
||||||
|
|
||||||
export interface PluginOptionBase {
|
export type PluginSettingDef = (
|
||||||
|
| PluginSettingStringDef
|
||||||
|
| PluginSettingNumberDef
|
||||||
|
| PluginSettingBooleanDef
|
||||||
|
| PluginSettingSelectDef
|
||||||
|
| PluginSettingSliderDef
|
||||||
|
| PluginSettingComponentDef
|
||||||
|
) & PluginSettingCommon;
|
||||||
|
|
||||||
|
export interface PluginSettingCommon {
|
||||||
description: string;
|
description: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
onChange?(newValue: any): void;
|
onChange?(newValue: any): void;
|
||||||
disabled?(): boolean;
|
|
||||||
restartNeeded?: boolean;
|
restartNeeded?: boolean;
|
||||||
componentProps?: Record<string, any>;
|
componentProps?: Record<string, any>;
|
||||||
/**
|
/**
|
||||||
|
@ -127,49 +139,47 @@ export interface PluginOptionBase {
|
||||||
*/
|
*/
|
||||||
target?: "WEB" | "DESKTOP" | "BOTH";
|
target?: "WEB" | "DESKTOP" | "BOTH";
|
||||||
}
|
}
|
||||||
|
interface IsDisabled<D = unknown> {
|
||||||
export interface PluginOptionString extends PluginOptionBase {
|
/**
|
||||||
type: OptionType.STRING;
|
* Checks if this setting should be disabled
|
||||||
|
*/
|
||||||
|
disabled?(this: D): boolean;
|
||||||
|
}
|
||||||
|
interface IsValid<T, D = unknown> {
|
||||||
/**
|
/**
|
||||||
* Prevents the user from saving settings if this is false or a string
|
* Prevents the user from saving settings if this is false or a string
|
||||||
*/
|
*/
|
||||||
isValid?(value: string): boolean | string;
|
isValid?(this: D, value: T): boolean | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginSettingStringDef {
|
||||||
|
type: OptionType.STRING;
|
||||||
default?: string;
|
default?: string;
|
||||||
}
|
}
|
||||||
|
export interface PluginSettingNumberDef {
|
||||||
export interface PluginOptionNumber extends PluginOptionBase {
|
type: OptionType.NUMBER;
|
||||||
type: OptionType.NUMBER | OptionType.BIGINT;
|
|
||||||
/**
|
|
||||||
* Prevents the user from saving settings if this is false or a string
|
|
||||||
*/
|
|
||||||
isValid?(value: number | BigInt): boolean | string;
|
|
||||||
default?: number;
|
default?: number;
|
||||||
}
|
}
|
||||||
|
export interface PluginSettingBigIntDef {
|
||||||
export interface PluginOptionBoolean extends PluginOptionBase {
|
type: OptionType.BIGINT;
|
||||||
|
default?: BigInt;
|
||||||
|
}
|
||||||
|
export interface PluginSettingBooleanDef {
|
||||||
type: OptionType.BOOLEAN;
|
type: OptionType.BOOLEAN;
|
||||||
/**
|
|
||||||
* Prevents the user from saving settings if this is false or a string
|
|
||||||
*/
|
|
||||||
isValid?(value: boolean): boolean | string;
|
|
||||||
default?: boolean;
|
default?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginOptionSelect extends PluginOptionBase {
|
export interface PluginSettingSelectDef {
|
||||||
type: OptionType.SELECT;
|
type: OptionType.SELECT;
|
||||||
/**
|
options: readonly PluginSettingSelectOption[];
|
||||||
* Prevents the user from saving settings if this is false or a string
|
|
||||||
*/
|
|
||||||
isValid?(value: PluginOptionSelectOption): boolean | string;
|
|
||||||
options: PluginOptionSelectOption[];
|
|
||||||
}
|
}
|
||||||
export interface PluginOptionSelectOption {
|
export interface PluginSettingSelectOption {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number | boolean;
|
value: string | number | boolean;
|
||||||
default?: boolean;
|
default?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginOptionSlider extends PluginOptionBase {
|
export interface PluginSettingSliderDef {
|
||||||
type: OptionType.SLIDER;
|
type: OptionType.SLIDER;
|
||||||
/**
|
/**
|
||||||
* All the possible values in the slider. Needs at least two values.
|
* 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.
|
* If false, allow users to select values in-between your markers.
|
||||||
*/
|
*/
|
||||||
stickToMarkers?: boolean;
|
stickToMarkers?: boolean;
|
||||||
/**
|
|
||||||
* Prevents the user from saving settings if this is false or a string
|
|
||||||
*/
|
|
||||||
isValid?(value: number): boolean | string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPluginOptionComponentProps {
|
interface IPluginOptionComponentProps {
|
||||||
|
@ -206,12 +212,67 @@ interface IPluginOptionComponentProps {
|
||||||
/**
|
/**
|
||||||
* The options object
|
* The options object
|
||||||
*/
|
*/
|
||||||
option: PluginOptionComponent;
|
option: PluginSettingComponentDef;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginOptionComponent extends PluginOptionBase {
|
export interface PluginSettingComponentDef {
|
||||||
type: OptionType.COMPONENT;
|
type: OptionType.COMPONENT;
|
||||||
component: (props: IPluginOptionComponentProps) => JSX.Element;
|
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; };
|
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;
|
||||||
|
|
Loading…
Reference in a new issue