Merge pull request #1785 from BetterDiscord/no-zlib

Add useful utilities to reduce need for libraries
This commit is contained in:
Zerebos 2024-12-11 19:39:32 -05:00 committed by GitHub
commit e3dcbb78f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1190 additions and 523 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
dist/
node_modules
.env
.idea/

View File

@ -1,4 +1,4 @@
import {ipcRenderer as IPC, shell} from "electron";
import {ipcRenderer as IPC, shell, webUtils} from "electron";
export const ipcRenderer = {
send: IPC.send.bind(IPC),
@ -9,4 +9,4 @@ export const ipcRenderer = {
off: IPC.off.bind(IPC)
};
export {shell};
export {shell, webUtils};

View File

@ -1,7 +1,12 @@
import config from "./config";
// fixed, improved, added, progress
export default {
title: "BetterDiscord",
subtitle: `v${config.version}`,
video: "https://www.youtube.com/embed/evyvq9eQTqA?si=opmzjGjUArT4VLrj&vq=hd720p&hd=1&rel=0&showinfo=0&mute=1&loop=1&autohide=1",
description: "A hotfix to get things going again. Plugins and Themes will take time to update.",
banner: "https://i.imgur.com/wuh5yMK.png",
blurb: "A hotfix to get things going again. Plugins and Themes will take time to update.",
changes: [
{
title: "Bugs Squashed",

View File

@ -1,4 +1,4 @@
import Logger from "@common/logger";
import BDLogger from "@common/logger";
import PluginManager from "@modules/pluginmanager";
import ThemeManager from "@modules/thememanager";
@ -15,6 +15,20 @@ import Webpack from "./webpack";
import * as Legacy from "./legacy";
import ContextMenu from "./contextmenu";
import fetch from "./fetch";
import Logger from "./logger";
import ColorInput from "@ui/settings/components/color";
import DropdownInput from "@ui/settings/components/dropdown";
import SettingItem from "@ui/settings/components/item";
import KeybindInput from "@ui/settings/components/keybind";
import NumberInput from "@ui/settings/components/number";
import RadioInput from "@ui/settings/components/radio";
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";
import ErrorBoundary from "@ui/errorboundary";
const bounded = new Map();
const PluginAPI = new AddonAPI(PluginManager);
@ -23,6 +37,7 @@ const PatcherAPI = new Patcher();
const DataAPI = new Data();
const DOMAPI = new DOM();
const ContextMenuAPI = new ContextMenu();
const DefaultLogger = new Logger();
/**
* `BdApi` is a globally (`window.BdApi`) accessible object for use by plugins and developers to make their lives easier.
@ -33,7 +48,7 @@ export default class BdApi {
if (!pluginName) return BdApi;
if (bounded.has(pluginName)) return bounded.get(pluginName);
if (typeof(pluginName) !== "string") {
Logger.error("BdApi", "Plugin name not a string, returning generic API!");
BDLogger.error("BdApi", "Plugin name not a string, returning generic API!");
return BdApi;
}
@ -44,6 +59,7 @@ export default class BdApi {
this.Patcher = new Patcher(pluginName);
this.Data = new Data(pluginName);
this.DOM = new DOM(pluginName);
this.Logger = new Logger(pluginName);
bounded.set(pluginName, this);
}
@ -57,7 +73,19 @@ export default class BdApi {
get ReactUtils() {return ReactUtils;}
get ContextMenu() {return ContextMenuAPI;}
Components = {
get Tooltip() {return DiscordModules.Tooltip;}
get Tooltip() {return DiscordModules.Tooltip;},
get ColorInput() {return ColorInput;},
get DropdownInput() {return DropdownInput;},
get SettingItem() {return SettingItem;},
get KeybindInput() {return KeybindInput;},
get NumberInput() {return NumberInput;},
get RadioInput() {return RadioInput;},
get SearchInput() {return SearchInput;},
get SliderInput() {return SliderInput;},
get SwitchInput() {return SwitchInput;},
get TextInput() {return TextInput;},
get SettingGroup() {return SettingGroup;},
get ErrorBoundary() {return ErrorBoundary;},
};
Net = {fetch};
}
@ -125,12 +153,38 @@ BdApi.DOM = DOMAPI;
*/
BdApi.ContextMenu = ContextMenuAPI;
/**
* An set of react components plugins can make use of.
* @type Components
*/
BdApi.Components = {
get Tooltip() {return DiscordModules.Tooltip;}
get Tooltip() {return DiscordModules.Tooltip;},
get ColorInput() {return ColorInput;},
get DropdownInput() {return DropdownInput;},
get SettingItem() {return SettingItem;},
get KeybindInput() {return KeybindInput;},
get NumberInput() {return NumberInput;},
get RadioInput() {return RadioInput;},
get SearchInput() {return SearchInput;},
get SliderInput() {return SliderInput;},
get SwitchInput() {return SwitchInput;},
get TextInput() {return TextInput;},
get SettingGroup() {return SettingGroup;},
get ErrorBoundary() {return ErrorBoundary;},
};
/**
* An instance of {@link Net} for using network related tools.
* @type Net
*/
BdApi.Net = {fetch};
/**
* An instance of {@link Logger} for logging information.
* @type Logger
*/
BdApi.Logger = DefaultLogger;
Object.freeze(BdApi);
Object.freeze(BdApi.Net);
Object.freeze(BdApi.prototype);

View File

@ -0,0 +1,130 @@
/**
* Simple logger for the lib and plugins.
*
* @module Logger
* @version 0.1.0
*/
/* eslint-disable no-console */
/**
* List of logging types.
*/
const LogTypes = {
error: "error",
debug: "debug",
log: "log",
warn: "warn",
info: "info"
};
const parseType = type => LogTypes[type] || "log";
/**
* `Logger` is a helper class to log data in a nice and consistent way. An instance is available on {@link BdApi}.
* @type Logger
* @summary {@link Logger} is a simple utility for logging information.
* @name Logger
*/
class Logger {
#pluginName = "";
#nameStyle = "color: #3a71c1; font-weight: 700;";
#messageStyle = "";
/**
* @param {string} pluginName - Name of the plugin
* @param {string} nameStyle - CSS to style the plugin name
* @param {string} messageStyle - CSS to style the main message
* @returns
*/
constructor(pluginName, nameStyle, messageStyle) {
if (!pluginName) return;
this.#pluginName = pluginName;
if (nameStyle) this.#nameStyle = nameStyle;
if (messageStyle) this.#messageStyle = messageStyle;
}
/**
* Logs an error using a collapsed error group with stacktrace.
*
* @param {string} pluginName - Name of the calling module.
* @param {string} message - Message or error to have logged.
* @param {Error} error - Error object to log with the message.
*/
stacktrace(pluginName, message, error) {
if (this.#pluginName) {
error = message;
message = pluginName;
pluginName = this.#pluginName;
}
console.error(`%c[${pluginName}]%c ${message}\n\n%c`, this.#nameStyle, "color: red; font-weight: 700;", "color: red;", error);
}
/**
* Logs an error message.
*
* @param {string} pluginName Name of the calling module
* @param {...any} message Messages to have logged.
*/
error(pluginName, ...message) {this.#_log(pluginName, message, "error");}
/**
* Logs a warning message.
*
* @param {string} module - Name of the calling module.
* @param {...any} message - Messages to have logged.
*/
warn(pluginName, ...message) {this.#_log(pluginName, message, "warn");}
/**
* Logs an informational message.
*
* @param {string} module - Name of the calling module.
* @param {...any} message - Messages to have logged.
*/
info(pluginName, ...message) {this.#_log(pluginName, message, "info");}
/**
* Logs used for debugging purposes.
*
* @param {string} module - Name of the calling module.
* @param {...any} message - Messages to have logged.
*/
debug(pluginName, ...message) {this.#_log(pluginName, message, "debug");}
/**
* Logs used for basic loggin.
*
* @param {string} module - Name of the calling module.
* @param {...any} message - Messages to have logged.
*/
log(pluginName, ...message) {this.#_log(pluginName, message);}
/**
* Logs strings using different console levels and a module label.
*
* @param {string} module - Name of the calling module.
* @param {any|Array<any>} message - Messages to have logged.
* @param {module:Logger.LogTypes} type - Type of log to use in console.
*/
#_log(pluginName, message, type = "log") {
type = parseType(type);
// Normalize messages to be an array for later spreading
if (!Array.isArray(message)) message = message ? [message] : [];
// If a name was set via constructor move the "name" to be part of the message
if (pluginName && this.#pluginName) message = [pluginName, ...message];
const displayName = this.#pluginName || pluginName;
console[type](`%c[${displayName}]%c`, this.#nameStyle, this.#messageStyle, ...message);
}
}
Object.freeze(Logger);
Object.freeze(Logger.prototype);
export default Logger;

View File

@ -4,6 +4,9 @@ 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";
import ErrorBoundary from "@ui/errorboundary";
/**
@ -57,6 +60,34 @@ const UI = {
return Modals.showConfirmationModal(title, content, options);
},
/**
* Shows a changelog modal in a similar style to Discord's. Customizable with images, videos, colored sections and supports markdown.
*
* The changes option is a array of objects that have this typing:
* ```ts
* interface Changes {
* title: string;
* type: "fixed" | "added" | "progress" | "changed";
* items: Array<string>;
* blurb?: string;
* }
* ```
*
* @param {object} options Information to display in the modal
* @param {string} options.title Title to show in the modal header
* @param {string} options.subtitle Title to show below the main header
* @param {string} [options.blurb] Text to show in the body of the modal before the list of changes
* @param {string} [options.banner] URL to an image to display as the banner of the modal
* @param {string} [options.video] Youtube link or url of a video file to use as the banner
* @param {string} [options.poster] URL to use for the video freeze-frame poster
* @param {string|ReactElement|Array<string|ReactElement>} [options.footer] What to show in the modal footer
* @param {Array<Changes>} [options.changes] List of changes to show (see description for details)
* @returns {string} The key used for this modal.
*/
showChangelogModal(options) {
return Modals.showChangelogModal(options);
},
/**
* This shows a toast similar to android towards the bottom of the screen.
*
@ -109,6 +140,77 @@ 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.type One of: dropdown, number, switch, text, slider, radio, keybind, color, custom
* @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 {ReactElement} [setting.children] Only used for "custom" type
* @param {CallableFunction} [setting.onChange] Callback when the value changes (only argument is new value)
* @param {boolean} [setting.disabled=false] Whether this setting is disabled
* @param {boolean} [setting.inline=true] Whether the input should render inline with the name (this is false by default for radio type)
* @returns A SettingItem with a an input as the child
*/
buildSettingItem(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 `category`. This has the same properties
* as the Group React Component found under the `Components` API.
*
* `onChange` will always be given 3 arguments: category id, setting id, and setting value. In the case
* that you have settings on the "root" of the panel, the category id is `null`. Any `onChange`
* listeners attached to individual settings will fire before the panel-level change listener.
*
* `onDrawerToggle` is given 2 arguments: category id, and the current shown state. You can use this to
* save drawer states.
*
* `getDrawerState` is given 2 arguments: category 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!");
return React.createElement(ErrorBoundary, null, settings.map(setting => {
if (!setting.id || !setting.type) throw new Error(`Setting item missing id or type`);
if (setting.type === "category") {
const shownByDefault = setting.hasOwnProperty("shown") ? setting.shown : true;
return React.createElement(Group, {
...setting,
onChange: onChange,
onDrawerToggle: state => onDrawerToggle?.(setting.id, state),
shown: getDrawerState?.(setting.id, shownByDefault) ?? shownByDefault
});
}
return buildSetting({
...setting,
onChange: value => {
setting?.onChange?.(value);
onChange(null, setting.id, value);
}
});
}));
}
};

View File

@ -1,4 +1,5 @@
import Utilities from "@modules/utilities";
import {comparator} from "@structs/semver";
/**
@ -71,6 +72,27 @@ const Utils = {
*/
className() {
return Utilities.className(...arguments);
},
/**
* Gets a nested value (if it exists) of an object safely. keyPath should be something like `key.key2.key3`.
* Numbers can be used for arrays as well like `key.key2.array.0.id`.
* @param {object} obj - object to get nested value from
* @param {string} keyPath - key path to the desired value
*/
getNestedValue(obj, keyPath) {
return Utilities.getNestedValue(obj, keyPath);
},
/**
* This works on semantic versioning e.g. "1.0.0".
*
* @param {string} currentVersion
* @param {string} newVersion
* @returns {number} 0 indicates equal, -1 indicates left hand greater, 1 indicates right hand greater
*/
semverCompare(currentVersion, newVersion) {
return comparator(currentVersion, newVersion);
}
};

View File

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

View File

@ -216,4 +216,16 @@ export default class Utilities {
return classes.join(" ");
}
/**
* Gets a nested value (if it exists) safely. Path should be something like `prop.prop2.prop3`.
* Numbers can be used for arrays as well like `prop.prop2.array.0.id`.
* @param {Object} obj - object to get nested value of
* @param {string} path - representation of the key path to obtain
*/
static getNestedValue(obj, path) {
return path.split(".").reduce(function(ob, prop) {
return ob && ob[prop];
}, obj);
}
}

View File

@ -119,8 +119,8 @@ export class Filters {
* @returns {module:WebpackModules.Filters~filter} - Combinatory filter of all arguments
*/
static combine(...filters) {
return module => {
return filters.every(filter => filter(module));
return (exports, module, id) => {
return filters.every(filter => filter(exports, module, id));
};
}
}
@ -398,7 +398,7 @@ export default class WebpackModules {
return new Promise((resolve) => {
const cancel = () => this.removeListener(listener);
const listener = function(exports) {
const listener = function(exports, module, id) {
if (!exports || exports === window || exports === document.documentElement || exports[Symbol.toStringTag] === "DOMTokenList") return;
let foundModule = null;
@ -407,14 +407,14 @@ export default class WebpackModules {
foundModule = null;
const wrappedExport = exports[key];
if (!wrappedExport) continue;
if (wrappedFilter(wrappedExport)) foundModule = wrappedExport;
if (wrappedFilter(wrappedExport, module, id)) foundModule = wrappedExport;
}
}
else {
if (exports.Z && wrappedFilter(exports.Z)) foundModule = defaultExport ? exports.Z : exports;
if (exports.ZP && wrappedFilter(exports.ZP)) foundModule = defaultExport ? exports.ZP : exports;
if (exports.__esModule && exports.default && wrappedFilter(exports.default)) foundModule = defaultExport ? exports.default : exports;
if (wrappedFilter(exports)) foundModule = exports;
if (exports.Z && wrappedFilter(exports.Z, module, id)) foundModule = defaultExport ? exports.Z : exports;
if (exports.ZP && wrappedFilter(exports.ZP, module, id)) foundModule = defaultExport ? exports.ZP : exports;
if (exports.__esModule && exports.default && wrappedFilter(exports.default, module, id)) foundModule = defaultExport ? exports.default : exports;
if (wrappedFilter(exports, module, id)) foundModule = exports;
}
@ -515,7 +515,7 @@ export default class WebpackModules {
const listeners = [...this.listeners];
for (let i = 0; i < listeners.length; i++) {
try {listeners[i](exports);}
try {listeners[i](exports, module, module.id);}
catch (error) {
Logger.stacktrace("WebpackModules", "Could not fire callback listener:", error);
}

View File

@ -48,6 +48,11 @@
transition: 150ms ease border-color;
}
.bd-select.bd-select-disabled {
cursor: not-allowed;
opacity: 0.5;
}
.bd-select:hover,
.bd-select.menu-open {
border-color: var(--background-tertiary);
@ -123,7 +128,7 @@
.bd-setting-header label {
font-weight: 500;
cursor: pointer;
/* cursor: pointer; */
overflow: hidden;
word-wrap: break-word;
font-size: 16px;

View File

@ -1,5 +1,16 @@
.bd-color-picker-container {
display: flex;
justify-content: center;
}
.bd-color-picker-container.bd-color-picker-disabled {
cursor: not-allowed;
opacity: 0.5;
}
.bd-color-picker-container.bd-color-picker-disabled > .bd-color-picker-controls,
.bd-color-picker-container.bd-color-picker-disabled > .bd-color-picker-swatch {
pointer-events: none;
}
.bd-color-picker-controls {
@ -10,8 +21,8 @@
.bd-color-picker-default {
cursor: pointer;
width: 72px;
height: 54px;
width: 75px;
height: 60px;
border-radius: 4px;
margin-right: 9px;
display: flex;
@ -29,13 +40,14 @@
position: absolute;
top: 5px;
right: 5px;
pointer-events: none;
}
.bd-color-picker {
outline: none;
width: 70px;
width: 75px;
border: none;
height: 54px;
height: 60px;
margin-top: 1px;
border-radius: 4px;
cursor: pointer;
@ -50,16 +62,35 @@
flex-wrap: wrap;
align-content: flex-start;
margin-left: 5px !important;
max-width: 340px;
max-width: 330px;
}
.bd-color-picker-swatch-item {
cursor: pointer;
border-radius: 4px;
width: 23px;
height: 23px;
width: 15px;
height: 15px;
margin: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.bd-setting-item.inline .bd-color-picker-swatch {
max-width: 220px;
margin-top: 1px;
}
.bd-setting-item.inline .bd-color-picker-default,
.bd-setting-item.inline .bd-color-picker {
width: 50px;
height: 40px
}
.bd-setting-item.inline .bd-color-picker-swatch-item {
height: 18px;
width: 18px;
margin: 2px 2px;
}

View File

@ -0,0 +1,69 @@
.bd-setting-item.inline .bd-file-input-wrap {
max-width: 300px;
}
.bd-file-input-wrap {
display: flex;
align-items: center;
gap: 5px;
position: relative;
min-width: 250px;
box-sizing: border-box;
border-radius: 3px;
background-color: hsla(0, calc(var(--saturation-factor, 1)*0%), 0%, .1);
border: 1px solid hsla(0, calc(var(--saturation-factor, 1)*0%), 0%, .3);
padding: 0 4px;
height: 40px;
}
.bd-file-input {
flex: 1;
outline: none;
color: var(--text-normal);
font-size: 16px;
font-weight: 600;
width: 100%;
cursor: pointer;
}
.bd-file-input::-webkit-file-upload-button {
height: 0;
width: 0;
padding: 0 !important;
margin: 0;
visibility: hidden;
user-select: none;
pointer-events: none;
}
.bd-file-input-wrap .bd-file-input-browse {
padding: 7px 16px;
}
.bd-file-input-wrap .bd-file-input-clear {
margin-left: 5px;
/* background: none!important; */
opacity: 0.5;
padding-right: 4px!important;
}
.bd-file-input-wrap .bd-file-input-clear:hover {
/* background: none; */
opacity: 1;
}
.bd-file-input-wrap .bd-file-input-clear svg {
width: 18px !important;
height: 18px !important;
}
.bd-file-input-wrap.bd-file-input-disabled {
cursor: not-allowed;
opacity: 0.5;
}
.bd-file-input-wrap.bd-file-input-disabled .bd-file-input-browse,
.bd-file-input-wrap.bd-file-input-disabled .bd-file-input,
.bd-file-input-wrap.bd-file-input-disabled .bd-file-input-clear {
pointer-events: none;
}

View File

@ -1,16 +1,29 @@
.bd-keybind-wrap {
display: flex;
align-items: center;
gap: 5px;
position: relative;
min-width: 250px;
box-sizing: border-box;
border-radius: 3px;
background-color: hsla(0, calc(var(--saturation-factor, 1)*0%), 0%, .1);
border: 1px solid hsla(0, calc(var(--saturation-factor, 1)*0%), 0%, .3);
padding: 10px;
padding: 0 4px;
height: 40px;
cursor: pointer;
}
.bd-keybind-wrap.bd-keybind-disabled {
cursor: not-allowed;
opacity: 0.5;
}
.bd-keybind-wrap.bd-keybind-disabled .bd-keybind-controls {
pointer-events: none;
}
.bd-keybind-wrap input {
flex: 1;
outline: none;
border: none;
pointer-events: none;
@ -18,7 +31,11 @@
background: none;
font-size: 16px;
text-transform: uppercase;
font-weight: 700;
font-weight: 600;
}
.bd-keybind-wrap input::placeholder {
text-transform: capitalize;
}
.bd-keybind-wrap.recording {
@ -29,28 +46,23 @@
box-shadow: 0 0 6px hsla(359, calc(var(--saturation-factor, 1)*82.6%), 59.4%, .3);
}
.bd-keybind-controls {
position: absolute;
right: 5px;
top: 3px;
display: flex;
align-items: center;
gap: 5px;
.bd-keybind-wrap.recording input {
color: hsl(359, calc(var(--saturation-factor, 1)*82.6%), 59.4%)
}
.bd-keybind-controls .bd-keybind-record {
padding: 4px 8px;
.bd-keybind-wrap .bd-keybind-record {
padding: 3px 8px;
}
.bd-keybind-clear {
margin-left: 5px;
background: none!important;
/* background: none!important; */
opacity: 0.5;
padding-right: 4px!important;
}
.bd-keybind-clear:hover {
background: none;
/* background: none; */
opacity: 1;
}

View File

@ -112,21 +112,25 @@
white-space: pre-wrap;
word-wrap: break-word;
width: 490px;
max-height: 800px;
max-height: min(800px, 60vh);
}
.bd-modal-medium {
width: 600px;
max-height: 800px;
max-height: min(800px, 60vh);
min-height: 400px;
}
.bd-modal-large {
width: 800px;
max-height: 960px;
max-height: min(960px, 70vh);
min-height: 400px;
}
.bd-addon-modal {
min-height: 0;
}
.bd-modal-header,
.bd-modal-footer {
position: relative;

View File

@ -14,3 +14,8 @@
border-radius: 3px;
width: 70px;
}
.bd-number-input:disabled {
cursor: not-allowed;
opacity: 0.5;
}

View File

@ -5,7 +5,8 @@
.bd-radio-option {
display: flex;
align-items: center;
padding: 10px;
border-left: 3px solid transparent;
padding: 10px 10px 10px 7px;
margin-bottom: 8px;
cursor: pointer;
user-select: none;
@ -14,6 +15,15 @@
color: var(--interactive-normal);
}
.bd-radio-group.bd-radio-disabled {
cursor: not-allowed;
opacity: 0.5;
}
.bd-radio-group.bd-radio-disabled .bd-radio-option {
pointer-events: none;
}
.bd-radio-option:hover {
background-color: var(--background-modifier-hover);
}

View File

@ -1,34 +1,66 @@
.bd-setting-item:not(.inline) .bd-slider-wrap {
margin-top: 10px;
}
.bd-slider-wrap {
display: flex;
flex-direction: column;
justify-content: center;
color: var(--text-normal);
align-items: center;
}
position: relative;
margin: 0 10px;
}
.bd-slider-label {
background: var(--brand-experiment);
.bd-slider-wrap.bd-slider-markers {
padding-bottom: 10px;
margin-bottom: 10px;
}
.bd-slider-wrap.bd-slider-disabled {
cursor: not-allowed;
opacity: 0.5;
}
.bd-slider-wrap.bd-slider-disabled > .bd-slider-input,
.bd-slider-wrap.bd-slider-disabled > .bd-slider-label,
.bd-slider-wrap.bd-slider-disabled > .bd-slider-track,
.bd-slider-wrap.bd-slider-disabled > .bd-slider-marker-container,
.bd-slider-wrap.bd-slider-disabled > .bd-slider-input::-webkit-slider-thumb {
pointer-events: none;
}
.bd-slider-label {
background: black;
font-weight: 700;
padding: 5px;
margin-right: 10px;
padding: 5px 7px;
border-radius: 5px;
}
opacity: 0;
pointer-events: none;
position: absolute;
top: -45px;
}
.bd-slider-input {
/* -webkit-appearance: none; */
height: 8px;
border-radius: 4px;
.bd-slider-input:hover + .bd-slider-label {
opacity: 1;
}
.bd-slider-input {
/* height: 8px; */
appearance: none;
min-width: 350px;
border-radius: 5px;
background: hsl(217,calc(var(--saturation-factor, 1)*7.6%),33.5%);
/* min-width: 350px; */
/* border-radius: 5px; */
outline: none;
transition: opacity .2s;
background-image: linear-gradient(var(--brand-experiment), var(--brand-experiment));
background-size: 70% 100%;
background-repeat: no-repeat;
}
pointer-events: none;
position: absolute;
background: none;
width: 100%;
z-index: 2;
}
/* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */
.bd-slider-input::-webkit-slider-thumb {
.bd-slider-input::-webkit-slider-thumb {
appearance: none;
width: 10px;
height: 24px;
@ -39,4 +71,51 @@
-webkit-box-shadow: 0 3px 1px 0 hsla(0,calc(var(--saturation-factor, 1)*0%),0%,.05),0 2px 2px 0 hsla(0,calc(var(--saturation-factor, 1)*0%),0%,.1),0 3px 3px 0 hsla(0,calc(var(--saturation-factor, 1)*0%),0%,.05);
box-shadow: 0 3px 1px 0 hsla(0,calc(var(--saturation-factor, 1)*0%),0%,.05),0 2px 2px 0 hsla(0,calc(var(--saturation-factor, 1)*0%),0%,.1),0 3px 3px 0 hsla(0,calc(var(--saturation-factor, 1)*0%),0%,.05);
cursor: ew-resize;
}
/* z-index: 3; */
pointer-events: all;
}
.bd-slider-track {
height: 8px;
border-radius: 4px;
min-width: 350px;
border-radius: 5px;
background: hsl(217,calc(var(--saturation-factor, 1)*7.6%),33.5%);
transition: opacity .2s;
background-image: linear-gradient(#3E82E5, #3E82E5);
background-size: 70% 100%;
background-repeat: no-repeat;
cursor: pointer;
width: 100%;
z-index: 1;
}
.bd-slider-marker-container {
display: flex;
width: 98%;
justify-content: space-between;
position: absolute;
bottom: 0;
}
.bd-slider-marker {
position: absolute;
transform: translateX(-50%);
font-size: 12px;
cursor: pointer;
}
.bd-slider-marker::before {
content: "";
position: absolute;
width: 2px;
height: 24px;
background: rgba(255, 255, 255, 0.2);
top: -26px;
left: calc(50% - 1px);
z-index: -1;
}
.bd-setting-item.inline:first-child:has(.bd-slider-wrap) {
padding-top: 50px;
}

View File

@ -8,4 +8,9 @@
border: none;
padding: 10px;
height: 40px;
}
}
.bd-text-input:disabled {
cursor: not-allowed;
opacity: 0.5;
}

View File

@ -24,7 +24,7 @@ function makeButton(button, value) {
function makeSwitch(control) {
return <Flex align={Flex.Align.CENTER} style={{gap: "10px"}}>
<Text>{control.label}</Text>
<Switch onChange={control.onChange} checked={control.checked} />
<Switch onChange={control.onChange} value={control.checked} />
</Flex>;
}

View File

@ -1,5 +1,3 @@
import Config from "@data/config";
import FormattableString from "@structs/string";
import Logger from "@common/logger";
@ -221,8 +219,6 @@ export default class Modals {
}
static showChangelogModal(options = {}) {
options = Object.assign({image: "https://i.imgur.com/wuh5yMK.png", description: "", changes: [], title: "BetterDiscord", subtitle: `v${Config.version}`}, options);
const key = this.openModal(props => {
return React.createElement(ErrorBoundary, null, React.createElement(ChangelogModal, Object.assign(options, props)));
});

View File

@ -57,7 +57,7 @@ function Video({src, poster}) {
}
export default function ChangelogModal({transitionState, footer, title, subtitle, onClose, video, poster, image, description, changes}) {
export default function ChangelogModal({transitionState, footer, title, subtitle, onClose, video, poster, banner, blurb, changes}) {
const ChangelogHeader = useMemo(() => <Header justify={Flex.Justify.BETWEEN}>
<Flex direction={Flex.Direction.VERTICAL}>
@ -78,23 +78,27 @@ export default function ChangelogModal({transitionState, footer, title, subtitle
</Footer>, [footer]);
const changelogItems = useMemo(() => {
const items = [video ? <Video src={video} poster={poster} /> : <img src={image} className="bd-changelog-poster" />];
if (description) items.push(<p>{SimpleMarkdownExt.parseToReact(description)}</p>);
for (let c = 0; c < changes.length; c++) {
const items = [];
if (video) items.push(<Video src={video} poster={poster} />);
else if (banner) items.push(<img src={banner} className="bd-changelog-poster" />);
if (blurb) items.push(<p>{SimpleMarkdownExt.parseToReact(blurb)}</p>);
for (let c = 0; c < changes?.length; c++) {
const entry = changes[c];
const type = "bd-changelog-" + entry.type;
const margin = c == 0 ? " bd-changelog-first" : "";
items.push(<h1 className={`bd-changelog-title ${type}${margin}`}>{entry.title}</h1>);
if (entry.description) items.push(<p>{SimpleMarkdownExt.parseToReact(entry.description)}</p>);
if (entry.blurb) items.push(<p>{SimpleMarkdownExt.parseToReact(entry.blurb)}</p>);
const list = <ul>{entry.items.map(i => <li>{SimpleMarkdownExt.parseToReact(i)}</li>)}</ul>;
items.push(list);
}
return items;
}, [description, video, image, poster, changes]);
}, [blurb, video, banner, poster, changes]);
return <Root className="bd-changelog-modal" transitionState={transitionState} size={Root.Sizes.MEDIUM} style={Root.Styles.STANDARD}>
{ChangelogHeader}
<Content>{changelogItems}</Content>
{ChangelogFooter}
{(footer || title === "BetterDiscord") && ChangelogFooter}
</Root>;
}

View File

@ -170,7 +170,7 @@ export default function AddonCard({addon, prefix, type, disabled, enabled: initi
<div className="bd-addon-header">
{type === "plugin" ? <ExtIcon size="18px" className="bd-icon" /> : <ThemeIcon size="18px" className="bd-icon" />}
<div className="bd-title">{title}</div>
<Switch internalState={false} disabled={disabled} checked={isEnabled} onChange={onChange} />
<Switch internalState={false} disabled={disabled} value={isEnabled} onChange={onChange} />
</div>
<div className="bd-description-wrap">
{disabled && <div className="banner banner-danger"><ErrorIcon className="bd-icon" />{`An error was encountered while trying to load this ${type}.`}</div>}

View File

@ -57,17 +57,18 @@ const getContrastColor = (color) => {
};
export default function Color({value: initialValue, onChange, colors = defaultColors, defaultValue}) {
export default function Color({value: initialValue, onChange, colors = defaultColors, defaultValue, disabled}) {
const [value, setValue] = useState(initialValue);
const change = useCallback((e) => {
if (disabled) return;
onChange?.(resolveColor(e.target.value));
setValue(e.target.value);
}, [onChange]);
}, [onChange, disabled]);
const intValue = resolveColor(value, false);
return <div className="bd-color-picker-container">
return <div className={`bd-color-picker-container ${disabled ? "bd-color-picker-disabled" : ""}`}>
<div className="bd-color-picker-controls">
<DiscordModules.Tooltip text="Default" position="bottom">
{defaultValue && <DiscordModules.Tooltip text="Default" position="bottom">
{props => (
<div {...props} className="bd-color-picker-default" style={{backgroundColor: resolveColor(defaultValue)}} onClick={() => change({target: {value: defaultValue}})}>
{intValue === resolveColor(defaultValue, false)
@ -76,17 +77,17 @@ export default function Color({value: initialValue, onChange, colors = defaultCo
}
</div>
)}
</DiscordModules.Tooltip>
</DiscordModules.Tooltip>}
<DiscordModules.Tooltip text={Strings.Settings.customColor} position="bottom">
{props => (
<div className="bd-color-picker-custom">
<Dropper color={getContrastColor(resolveColor(value, true))} />
<input {...props} style={{backgroundColor: resolveColor(value)}} type="color" className="bd-color-picker" value={resolveColor(value)} onChange={change} />
<input {...props} style={{backgroundColor: resolveColor(value)}} type="color" className="bd-color-picker" value={resolveColor(value)} onChange={change} disabled={disabled} />
</div>
)}
</DiscordModules.Tooltip>
</div>
<div className="bd-color-picker-swatch">
{colors?.length > 0 && <div className="bd-color-picker-swatch">
{
colors.map((int, index) => (
<div key={index} className="bd-color-picker-swatch-item" style={{backgroundColor: resolveColor(int)}} onClick={() => change({target: {value: int}})}>
@ -97,6 +98,6 @@ export default function Color({value: initialValue, onChange, colors = defaultCo
</div>
))
}
</div>
</div>}
</div>;
}

View File

@ -5,7 +5,7 @@ import Arrow from "@ui/icons/downarrow";
const {useState, useCallback} = React;
export default function Select({value: initialValue, options, style, onChange}) {
export default function Select({value: initialValue, options, style, onChange, disabled}) {
const [value, setValue] = useState(initialValue ?? options[0].value);
const change = useCallback((val) => {
onChange?.(val);
@ -23,11 +23,13 @@ export default function Select({value: initialValue, options, style, onChange})
event.preventDefault();
event.stopPropagation();
if (disabled) return;
const next = !open;
setOpen(next);
if (!next) return;
document.addEventListener("click", hideMenu);
}, [hideMenu, open]);
}, [hideMenu, open, disabled]);
// ?? options[0] provides a double failsafe
@ -40,7 +42,8 @@ export default function Select({value: initialValue, options, style, onChange})
const styleClass = style == "transparent" ? " bd-select-transparent" : "";
const isOpen = open ? " menu-open" : "";
return <div className={`bd-select${styleClass}${isOpen}`} onClick={showMenu}>
const isDisabled = disabled ? " bd-select-disabled" : "";
return <div className={`bd-select${styleClass}${isOpen}${isDisabled}`} onClick={showMenu}>
<div className="bd-select-value">{selected.label}</div>
<Arrow className="bd-select-arrow" />
{open && optionComponents}

View File

@ -0,0 +1,40 @@
import {webUtils} from "electron";
import React from "@modules/react";
import Button from "@ui/base/button";
import Close from "@ui/icons/close";
const {useRef, useCallback, useEffect} = React;
export default function Filepicker({multiple, accept, clearable, onChange, disabled, actions}) {
const inputRef = useRef(null);
const change = useCallback((e) => {
if (disabled) return;
const files = [];
for (const file of e.target.files) {
files.push(webUtils.getPathForFile(file));
}
onChange?.(multiple ? files : files[0]);
}, [onChange, disabled, multiple]);
const clear = useCallback(() => {
inputRef.current.value = "";
onChange?.(multiple ? [] : "");
}, [onChange, multiple]);
useEffect(() => {
if (!actions) return;
actions.clear = clear;
}, [clear, actions]);
const onClick = useCallback(() => {
inputRef.current?.click();
}, []);
return <div className={`bd-file-input-wrap ${disabled ? "bd-file-input-disabled" : ""}`}>
<Button size={Button.Sizes.ICON} look={Button.Looks.FILLED} color={Button.Colors.PRIMARY} className="bd-file-input-browse" onClick={onClick}>Browse</Button>
<input onChange={change} type="file" className="bd-file-input" multiple={multiple} accept={accept} disabled={disabled} ref={inputRef} />
{clearable && <Button size={Button.Sizes.ICON} look={Button.Looks.BLANK} onClick={clear} className="bd-file-input-clear"><Close size="24px" /></Button>}
</div>;
}

View File

@ -7,15 +7,19 @@ import Close from "@ui/icons/close";
const {useState, useCallback, useEffect} = React;
export default function Keybind({value: initialValue, onChange, max = 2, clearable = true}) {
export default function Keybind({value: initialValue, onChange, max = 4, clearable = false, disabled}) {
const [state, setState] = useState({value: initialValue, isRecording: false, accum: []});
useEffect(() => {
window.addEventListener("keydown", keyHandler, true);
return () => window.removeEventListener("keydown", keyHandler, true);
window.addEventListener("keydown", keyDownHandler, true);
window.addEventListener("keyup", keyUpHandler, true);
return () => {
window.removeEventListener("keydown", keyDownHandler, true);
window.removeEventListener("keyup", keyUpHandler, true);
};
});
const keyHandler = useCallback((event) => {
const keyDownHandler = useCallback((event) => {
if (!state.isRecording) return;
event.stopImmediatePropagation();
event.stopPropagation();
@ -29,25 +33,37 @@ export default function Keybind({value: initialValue, onChange, max = 2, clearab
}
}, [state, max, onChange]);
const keyUpHandler = useCallback((event) => {
if (!state.isRecording) return;
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
if (event.key === state.accum[0]) {
onChange?.(state.accum);
setState({value: state.accum.slice(0), isRecording: false, accum: []});
}
}, [state, onChange]);
const clearKeybind = useCallback((event) => {
event.stopPropagation();
event.preventDefault();
if (disabled) return;
if (onChange) onChange([]);
setState({...state, isRecording: false, value: [], accum: []});
}, [onChange, state]);
}, [onChange, state, disabled]);
const onClick = useCallback((e) => {
if (disabled) return;
if (e.target?.className?.includes?.("bd-keybind-clear") || e.target?.closest(".bd-button")?.className?.includes("bd-keybind-clear")) return clearKeybind(e);
setState({...state, isRecording: !state.isRecording});
}, [state, clearKeybind]);
}, [state, clearKeybind, disabled]);
const displayValue = state.isRecording ? "Recording..." : !state.value.length ? "N/A" : state.value.join(" + ");
return <div className={"bd-keybind-wrap" + (state.isRecording ? " recording" : "")} onClick={onClick}>
<input readOnly={true} type="text" className="bd-keybind-input" value={displayValue} />
<div className="bd-keybind-controls">
<Button size={Button.Sizes.ICON} look={Button.Looks.FILLED} color={state.isRecording ? Button.Colors.RED : Button.Colors.BRAND} className="bd-keybind-record" onClick={onClick}><Keyboard size="24px" /></Button>
const displayValue = !state.value.length ? "" : state.value.map(k => k === "Control" ? "Ctrl" : k).join(" + ");
return <div className={"bd-keybind-wrap" + (state.isRecording ? " recording" : "") + (disabled ? " bd-keybind-disabled" : "")} onClick={onClick}>
<Button size={Button.Sizes.ICON} look={Button.Looks.FILLED} color={state.isRecording ? Button.Colors.RED : Button.Colors.PRIMARY} className="bd-keybind-record" onClick={onClick}><Keyboard size="24px" /></Button>
<input readOnly={true} type="text" className="bd-keybind-input" value={displayValue} placeholder="No keybind set" disabled={disabled} />
{clearable && <Button size={Button.Sizes.ICON} look={Button.Looks.BLANK} onClick={clearKeybind} className="bd-keybind-clear"><Close size="24px" /></Button>}
</div>
</div>;
}

View File

@ -3,12 +3,12 @@ import React from "@modules/react";
const {useState, useCallback} = React;
export default function Number({value: initialValue, min, max, step, onChange}) {
export default function Number({value: initialValue, min, max, step, onChange, disabled}) {
const [value, setValue] = useState(initialValue);
const change = useCallback((e) => {
onChange?.(e.target.value);
setValue(e.target.value);
}, [onChange]);
return <input onChange={change} type="number" className="bd-number-input" min={min} max={max} step={step} value={value} />;
return <input onChange={change} type="number" className="bd-number-input" min={min} max={max} step={step} value={value} disabled={disabled} />;
}

View File

@ -5,19 +5,20 @@ import RadioIcon from "@ui/icons/radio";
const {useState, useCallback} = React;
export default function Radio({name, value, options, onChange}) {
export default function Radio({name, value, options, onChange, disabled}) {
const [index, setIndex] = useState(options.findIndex(o => o.value === value));
const change = useCallback((e) => {
if (disabled) return;
const newIndex = parseInt(e.target.value);
const newValue = options[newIndex].value;
onChange?.(newValue);
setIndex(newIndex);
}, [options, onChange]);
}, [options, onChange, disabled]);
function renderOption(opt, i) {
const isSelected = index === i;
return <label className={"bd-radio-option" + (isSelected ? " bd-radio-selected" : "")}>
<input onChange={change} type="radio" name={name} checked={isSelected} value={i} />
return <label className={"bd-radio-option" + (isSelected ? " bd-radio-selected" : "")} style={{borderColor: opt.color ?? "transparent"}}>
<input onChange={change} type="radio" name={name} checked={isSelected} value={i} disabled={disabled} />
{/* <span className="bd-radio-button"></span> */}
<RadioIcon className="bd-radio-icon" size="24" checked={isSelected} />
<div className="bd-radio-label-wrap">
@ -27,5 +28,5 @@ export default function Radio({name, value, options, onChange}) {
</label>;
}
return <div className="bd-radio-group">{options.map(renderOption)}</div>;
return <div className={`bd-radio-group ${disabled ? "bd-radio-disabled" : ""}`}>{options.map(renderOption)}</div>;
}

View File

@ -1,16 +1,61 @@
import React from "@modules/react";
const {useState, useCallback} = React;
const {useState, useCallback, useMemo, useRef} = React;
export default function Slider({value: initialValue, min, max, step, onChange}) {
export default function Slider({value: initialValue, min, max, step, onChange, disabled, units = "", markers = []}) {
const [value, setValue] = useState(initialValue);
const inputRef = useRef(null);
const change = useCallback((e) => {
if (disabled) return;
onChange?.(e.target.value);
setValue(e.target.value);
}, [onChange]);
}, [onChange, disabled]);
return <div className="bd-slider-wrap">
<div className="bd-slider-label">{value}</div><input onChange={change} type="range" className="bd-slider-input" min={min} max={max} step={step} value={value} style={{backgroundSize: (value - min) * 100 / (max - min) + "% 100%"}} />
const jumpToValue = useCallback(val => {
if (disabled) return;
onChange?.(val);
setValue(val);
}, [onChange, disabled]);
const percent = useCallback(val => {
return (val - min) * 100 / (max - min);
}, [min, max]);
const labelOffset = useMemo(() => {
const slope = (-62.5 - -25) / (max - min);
const offset = (value * slope) + -25;
if (offset < -62.5) return -62.5;
return offset;
}, [value, min, max]);
const trackClick = useCallback(e => {
const bounds = e.target.getBoundingClientRect();
const offsetX = e.clientX - bounds.left;
const offsetPercent = (offsetX / bounds.width);
const newValue = (offsetPercent * (max - min)) + min;
inputRef.current.value = newValue;
jumpToValue(inputRef.current?.value);
}, [max, min, jumpToValue, inputRef]);
return <div className={`bd-slider-wrap ${disabled ? "bd-slider-disabled" : ""} ${markers.length > 0 ? "bd-slider-markers" : ""}`}>
<input onChange={change} type="range" className="bd-slider-input" min={min} max={max} step={step} value={value} disabled={disabled} ref={inputRef} />
<div className="bd-slider-label" style={{left: `${percent(value)}%`, transform: `translateX(${labelOffset}%)`}}>{value}{units}</div>
<div className="bd-slider-track" style={{backgroundSize: percent(value) + "% 100%"}} onClick={trackClick}></div>
{markers?.length > 0 && <div className="bd-slider-marker-container">
{markers.map(m => <div className="bd-slider-marker" style={{left: percent(m) + "%"}} onClick={() => jumpToValue(m)}>{m}{units}</div>)}
</div>}
</div>;
}
/**
* label offset left:
*
* value - min
* ----------- x 100
* max - min
*/

View File

@ -3,7 +3,7 @@ import React from "@modules/react";
const {useState, useCallback} = React;
export default function Switch({id, checked: initialValue, disabled, onChange, internalState = true}) {
export default function Switch({id, value: initialValue, disabled, onChange, internalState = true}) {
const [checked, setChecked] = useState(initialValue);
const change = useCallback(() => {
onChange?.(!checked);

View File

@ -3,12 +3,13 @@ import React from "@modules/react";
const {useState, useCallback} = React;
export default function Textbox({value: initialValue, maxLength, placeholder, onKeyDown, onChange}) {
export default function Textbox({value: initialValue, maxLength, placeholder, onKeyDown, onChange, disabled}) {
const [value, setValue] = useState(initialValue);
const change = useCallback((e) => {
if (disabled) return;
onChange?.(e.target.value);
setValue(e.target.value);
}, [onChange]);
}, [onChange, disabled]);
return <input onChange={change} onKeyDown={onKeyDown} type="text" className="bd-text-input" placeholder={placeholder} maxLength={maxLength} value={value} />;
return <input onChange={change} onKeyDown={onKeyDown} type="text" className="bd-text-input" placeholder={placeholder} maxLength={maxLength} value={value} disabled={disabled} />;
}

View File

@ -10,30 +10,42 @@ import Slider from "./components/slider";
import Radio from "./components/radio";
import Keybind from "./components/keybind";
import Color from "./components/color";
import Filepicker from "./components/file";
const {useCallback} = React;
export default function Group({onChange, id, name, button, shown, onDrawerToggle, showDivider, collapsible, settings}) {
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.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>;
{settings?.length > 0 && settings.filter(s => !s.hidden).map((setting) => {
const callback = value => {
setting?.onChange?.(value);
change(setting.id, value);
};
return buildSetting({...setting, onChange: callback});
})}
{children}
</Drawer>;
}
export function buildSetting(setting) {
let children = null;
if (setting.type == "dropdown") children = <Dropdown {...setting} />;
if (setting.type == "number") children = <Number {...setting} />;
if (setting.type == "switch") children = <Switch {...setting} />;
if (setting.type == "text") children = <Textbox {...setting} />;
if (setting.type == "file") children = <Filepicker {...setting} />;
if (setting.type == "slider") children = <Slider {...setting} />;
if (setting.type == "radio") children = <Radio {...setting} />;
if (setting.type == "keybind") children = <Keybind {...setting} />;
if (setting.type == "color") children = <Color {...setting} />;
if (setting.type == "custom") children = setting.children;
if (!children) return null;
return <Item id={setting.id} inline={setting.hasOwnProperty("inline") ? setting.inline : setting.type !== "radio"} key={setting.id} name={setting.name} note={setting.note}>{children}</Item>;
}

View File

@ -81,7 +81,7 @@ export default class Tooltip {
/** Don't reshow if already active */
if (this.active) return;
this.active = true;
this.labelElement.textContent = this.label;
// this.labelElement.textContent = this.label;
this.container.append(this.element);
if (this.side == "top") {