Merge pull request #1785 from BetterDiscord/no-zlib
Add useful utilities to reduce need for libraries
This commit is contained in:
commit
e3dcbb78f7
|
@ -1,3 +1,4 @@
|
|||
dist/
|
||||
node_modules
|
||||
.env
|
||||
.idea/
|
|
@ -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};
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -14,3 +14,8 @@
|
|||
border-radius: 3px;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.bd-number-input:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -8,4 +8,9 @@
|
|||
border: none;
|
||||
padding: 10px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-text-input:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)));
|
||||
});
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -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>}
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -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}
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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} />;
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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
|
||||
*/
|
|
@ -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);
|
||||
|
|
|
@ -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} />;
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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") {
|
||||
|
|
Loading…
Reference in New Issue