Create settings builder

This commit is contained in:
Zerebos 2024-12-05 16:31:40 -05:00
parent 53b4bed979
commit 00b5d800a0
No known key found for this signature in database
4 changed files with 110 additions and 40 deletions

View File

@ -26,6 +26,7 @@ import SearchInput from "@ui/settings/components/search";
import SliderInput from "@ui/settings/components/slider";
import SwitchInput from "@ui/settings/components/switch";
import TextInput from "@ui/settings/components/textbox";
import SettingGroup from "@ui/settings/group";
const bounded = new Map();
const PluginAPI = new AddonAPI(PluginManager);
@ -79,6 +80,7 @@ export default class BdApi {
get SliderInput() {return SliderInput;},
get SwitchInput() {return SwitchInput;},
get TextInput() {return TextInput;},
get SettingGroup() {return SettingGroup;}
};
Net = {fetch};
}

View File

@ -4,6 +4,8 @@ import Modals from "@ui/modals";
import Toasts from "@ui/toasts";
import Notices from "@ui/notices";
import Tooltip from "@ui/tooltip";
import Group, {buildSetting} from "@ui/settings/group";
import React from "@modules/react";
/**
@ -137,6 +139,64 @@ const UI = {
if (data.error) throw new Error(data.error);
return data;
},
/**
* Creates a single setting wrapped in a setting item that has a name and note.
* The shape of the object should match the props of the component you want to render, check the
* `BdApi.Components` section for details. Shown below are ones common to all setting types.
* @param {object} setting
* @param {string} setting.id Identifier to used for callbacks
* @param {string} setting.name Visual name to display
* @param {string} setting.note Visual description to display
* @param {any} setting.value Current value of the setting
* @param {CallableFunction} [setting.onChange] Callback when the value changes (only argument is new value)
* @param {boolean} [setting.disabled] Whether this setting is disabled
* @returns A SettingItem with a an input as the child
*/
buildSetting(setting) {
return buildSetting(setting);
},
/**
* Creates a settings panel (react element) based on json-like data.
*
* The `settings` array here is an array of the same settings types described in `buildSetting` above.
* However, this API allows one additional setting "type" called `group`. This has the same properties
* as the React Component found under the `Components` API.
*
* `onChange` will always be given 3 arguments: group id, setting id, and setting value. In the case
* that you have settings on the "root" of the panel, the group id is `null`.
*
* `onDrawerToggle` is given 2 arguments: group id, and the current shown state. You can use this to
* save drawer states.
*
* `getDrawerState` is given 2 arguments: group id, and the default shown state. You can use this to
* recall a saved drawer state.
*
* @param {object} props
* @param {Array<object>} props.settings Array of settings to show
* @param {CallableFunction} props.onChange Function called on every change
* @param {CallableFunction} [props.onDrawerToggle] Optionally used to save drawer states
* @param {CallableFunction} [props.getDrawerState] Optionially used to recall drawer states
* @returns React element usable for a settings panel
*/
buildSettingsPanel({settings, onChange, onDrawerToggle, getDrawerState}) {
if (!settings?.length) throw new Error("No settings provided!");
if (typeof(onChange) !== "function") throw new Error("No change listener provided!");
return React.createElement(React.Fragment, null, settings.map(setting => {
if (setting.type === "group") {
const shownByDefault = setting.hasOwnProperty("shown") ? setting.shown : true;
const groupProps = Object.assign({}, setting, {
onChange,
onDrawerToggle: state => onDrawerToggle?.(setting.id, state),
shown: getDrawerState?.(setting.id, shownByDefault) ?? shownByDefault
});
return React.createElement(Group, groupProps);
}
return buildSetting(Object.assign({}, setting, {onChange: value => onChange(null, setting.id, value)}));
}));
}
};

View File

@ -1,3 +1,5 @@
import DiscordModules from "./discordmodules";
export default DiscordModules.React;
import DiscordModules from "./discordmodules";
/** @type {import("react")} */
const React = DiscordModules.React;
export default React;
export const ReactDOM = DiscordModules.ReactDOM;

View File

@ -1,39 +1,45 @@
import React from "@modules/react";
import Drawer from "./drawer";
import Switch from "./components/switch";
import Dropdown from "./components/dropdown";
import Number from "./components/number";
import Item from "./components/item";
import Textbox from "./components/textbox";
import Slider from "./components/slider";
import Radio from "./components/radio";
import Keybind from "./components/keybind";
import Color from "./components/color";
const {useCallback} = React;
export default function Group({onChange, id, name, button, shown, onDrawerToggle, showDivider, collapsible, settings}) {
const change = useCallback((settingId, value) => {
if (id) onChange?.(id, settingId, value);
else onChange?.(settingId, value);
}, [id, onChange]);
return <Drawer collapsible={collapsible} name={name} button={button} shown={shown} onDrawerToggle={onDrawerToggle} showDivider={showDivider}>
{settings.filter(s => !s.hidden).map((setting) => {
let component = null;
const callback = value => change(setting.id, value);
if (setting.type == "dropdown") component = <Dropdown disabled={setting.disabled} id={setting.id} options={setting.options} value={setting.value} onChange={callback} />;
if (setting.type == "number") component = <Number disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={callback} />;
if (setting.type == "switch") component = <Switch disabled={setting.disabled} id={setting.id} checked={setting.value} onChange={callback} />;
if (setting.type == "text") component = <Textbox disabled={setting.disabled} id={setting.id} value={setting.value} onChange={callback} />;
if (setting.type == "slider") component = <Slider disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={callback} />;
if (setting.type == "radio") component = <Radio disabled={setting.disabled} id={setting.id} name={setting.id} options={setting.options} value={setting.value} onChange={callback} />;
if (setting.type == "keybind") component = <Keybind disabled={setting.disabled} id={setting.id} value={setting.value} max={setting.max} onChange={callback} />;
if (setting.type == "color") component = <Color disabled={setting.disabled} id={setting.id} value={setting.value} defaultValue={setting.defaultValue} colors={setting.colors} onChange={callback} />;
if (!component) return null;
return <Item id={setting.id} inline={setting.type !== "radio"} key={setting.id} name={setting.name} note={setting.note}>{component}</Item>;
})}
</Drawer>;
import React from "@modules/react";
import Drawer from "./drawer";
import Switch from "./components/switch";
import Dropdown from "./components/dropdown";
import Number from "./components/number";
import Item from "./components/item";
import Textbox from "./components/textbox";
import Slider from "./components/slider";
import Radio from "./components/radio";
import Keybind from "./components/keybind";
import Color from "./components/color";
const {useCallback} = React;
export default function Group({onChange, id, name, button, shown, onDrawerToggle, showDivider, collapsible, settings, children}) {
const change = useCallback((settingId, value) => {
if (id) onChange?.(id, settingId, value);
else onChange?.(settingId, value);
}, [id, onChange]);
return <Drawer collapsible={collapsible} name={name} button={button} shown={shown} onDrawerToggle={onDrawerToggle} showDivider={showDivider}>
{settings.length && settings.filter(s => !s.hidden).map((setting) => {
const callback = value => change(setting.id, value);
return buildSetting({...setting, onChange: callback});
})}
{children}
</Drawer>;
}
export function buildSetting(setting) {
let component = null;
if (setting.type == "dropdown") component = <Dropdown disabled={setting.disabled} id={setting.id} options={setting.options} value={setting.value} onChange={setting.onChange} />;
if (setting.type == "number") component = <Number disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={setting.onChange} />;
if (setting.type == "switch") component = <Switch disabled={setting.disabled} id={setting.id} checked={setting.value} onChange={setting.onChange} />;
if (setting.type == "text") component = <Textbox disabled={setting.disabled} id={setting.id} value={setting.value} onChange={setting.onChange} />;
if (setting.type == "slider") component = <Slider disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={setting.onChange} />;
if (setting.type == "radio") component = <Radio disabled={setting.disabled} id={setting.id} name={setting.id} options={setting.options} value={setting.value} onChange={setting.onChange} />;
if (setting.type == "keybind") component = <Keybind disabled={setting.disabled} id={setting.id} value={setting.value} max={setting.max} onChange={setting.onChange} />;
if (setting.type == "color") component = <Color disabled={setting.disabled} id={setting.id} value={setting.value} defaultValue={setting.defaultValue} colors={setting.colors} onChange={setting.onChange} />;
if (!component) return null;
return <Item id={setting.id} inline={setting.type !== "radio"} key={setting.id} name={setting.name} note={setting.note}>{component}</Item>;
}