Add common setting types

This commit is contained in:
Zack Rauen 2022-10-02 23:08:10 -04:00
parent 87652dab8e
commit 70194a7114
18 changed files with 579 additions and 10 deletions

View File

@ -70,5 +70,31 @@ export default [
{type: "switch", id: "inspectElement", value: false, enableWith: "devTools"},
{type: "switch", id: "devToolsWarning", value: false, enableWith: "devTools"},
]
}
},
// {
// type: "category",
// id: "debug",
// name: "Debug",
// collapsible: true,
// shown: true,
// settings: [
// {name: "Text test", note: "Just testing it", type: "text", id: "texttest", value: ""},
// {name: "Slider test", note: "Just testing it", type: "slider", id: "slidertest", value: 30, min: 20, max: 50, step: 10},
// {
// name: "Radio test",
// note: "Just testing it",
// type: "radio",
// id: "radiotest",
// value: "test",
// options: [
// {name: "First", value: 30, description: "little hint"},
// {name: "IDK", value: "test", description: "who cares"},
// {name: "Something", value: 666, description: "something else"},
// {name: "Last", value: "last", description: "nothing more to add"}
// ]
// },
// {name: "Keybind test", note: "Just testing it", type: "keybind", id: "keybindtest", value: ["Control", "H"]},
// {name: "Color test", note: "Just testing it", type: "color", id: "colortest", value: "#ff0000", defaultValue: "#ffffff"},
// ]
// }
];

View File

@ -111,11 +111,16 @@ export default new class SettingsManager {
for (const setting in this.state[id][category]) {
if (previousState[category][setting] == undefined) continue;
const settingObj = this.getSetting(id, category, setting);
if (settingObj.type == "switch") this.state[id][category][setting] = previousState[category][setting];
if (settingObj.type == "number") this.state[id][category][setting] = previousState[category][setting];
if (settingObj.type == "dropdown") {
const exists = settingObj.options.some(o => o.value == previousState[category][setting]);
if (exists) this.state[id][category][setting] = previousState[category][setting];
switch (settingObj.type) {
case "radio":
case "dropdown": {
const exists = settingObj.options.some(o => o.value == previousState[category][setting]);
if (exists) this.state[id][category][setting] = previousState[category][setting];
break;
}
default: {
this.state[id][category][setting] = previousState[category][setting];
}
}
}
}

View File

@ -139,6 +139,10 @@
font-weight: 400;
}
.bd-setting-item:not(.inline) .bd-setting-note {
margin-bottom: 10px;
}
.bd-setting-divider {
width: 100%;
height: 1px;

View File

@ -0,0 +1,65 @@
.bd-color-picker-container {
display: flex;
}
.bd-color-picker-controls {
padding-left: 1px;
padding-top: 2px;
display: flex;
}
.bd-color-picker-default {
cursor: pointer;
width: 72px;
height: 54px;
border-radius: 4px;
margin-right: 9px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 1px;
}
.bd-color-picker-custom {
position: relative;
display: inline-table;
}
.bd-color-picker-custom svg {
position: absolute;
top: 5px;
right: 5px;
}
.bd-color-picker {
outline: none;
width: 70px;
border: none;
height: 54px;
margin-top: 1px;
border-radius: 4px;
cursor: pointer;
}
.bd-color-picker::-webkit-color-swatch {
border: none;
}
.bd-color-picker-swatch {
display: flex;
flex-wrap: wrap;
align-content: flex-start;
margin-left: 5px !important;
max-width: 340px;
}
.bd-color-picker-swatch-item {
cursor: pointer;
border-radius: 4px;
width: 23px;
height: 23px;
margin: 4px;
display: flex;
align-items: center;
justify-content: center;
}

View File

@ -0,0 +1,54 @@
.bd-keybind-wrap {
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;
height: 40px;
cursor: pointer;
}
.bd-keybind-wrap input {
outline: none;
border: none;
pointer-events: none;
color: var(--text-normal);
background: none;
font-size: 16px;
text-transform: uppercase;
font-weight: 700;
}
.bd-keybind-wrap.recording {
border-color: hsla(359, calc(var(--saturation-factor, 1)*82.6%), 59.4%, .3);
}
.bd-keybind-wrap.recording {
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;
}
.bd-keybind-clear {
background: none!important;
opacity: 0.5;
padding-right: 4px!important;
}
.bd-keybind-clear:hover {
background: none;
opacity: 1;
}
.bd-keybind-clear svg {
width: 18px !important;
height: 18px !important;
}

View File

@ -0,0 +1,55 @@
.bd-radio-group {
min-width: 300px;
}
.bd-radio-option {
display: flex;
align-items: center;
padding: 10px;
margin-bottom: 8px;
cursor: pointer;
user-select: none;
background-color: var(--background-secondary);
border-radius: 3px;
color: var(--interactive-normal);
}
.bd-radio-option:hover {
background-color: var(--background-modifier-hover);
}
.bd-radio-option.bd-radio-selected {
background-color: var(--background-modifier-selected);
color: var(--interactive-active);
}
.bd-radio-option input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.bd-radio-icon {
margin-right: 10px;
}
.bd-radio-label-wrap {
display: flex;
flex-direction: column;
}
.bd-radio-label {
font-family: var(--font-primary);
font-size: 16px;
line-height: 20px;
font-weight: 500;
}
.bd-radio-description {
font-family: var(--font-primary);
font-size: 14px;
line-height: 18px;
font-weight: 400;
}

View File

@ -0,0 +1,42 @@
.bd-slider-wrap {
display: flex;
color: var(--text-normal);
align-items: center;
}
.bd-slider-label {
background: var(--brand-experiment);
font-weight: 700;
padding: 5px;
margin-right: 10px;
border-radius: 5px;
}
.bd-slider-input {
/* -webkit-appearance: none; */
height: 8px;
border-radius: 4px;
appearance: none;
min-width: 350px;
border-radius: 5px;
background: hsl(217,calc(var(--saturation-factor, 1)*7.6%),33.5%);
outline: none;
transition: opacity .2s;
background-image: linear-gradient(var(--brand-experiment), var(--brand-experiment));
background-size: 70% 100%;
background-repeat: no-repeat;
}
/* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */
.bd-slider-input::-webkit-slider-thumb {
appearance: none;
width: 10px;
height: 24px;
top: 50%;
border-radius: 3px;
background-color: hsl(0,calc(var(--saturation-factor, 1)*0%),100%);
border: 1px solid hsl(210,calc(var(--saturation-factor, 1)*2.9%),86.7%);
-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;
}

View File

@ -0,0 +1,11 @@
.bd-text-input {
min-width: 250px;
font-size: 16px;
box-sizing: border-box;
border-radius: 3px;
color: var(--text-normal);
background-color: var(--input-background);
border: none;
padding: 10px;
height: 40px;
}

View File

@ -2,7 +2,8 @@ import {React} from "modules";
export default class CloseButton extends React.Component {
render() {
return <svg viewBox="0 0 12 12" style={{width: "18px", height: "18px"}}>
const size = this.props.size || "18px";
return <svg viewBox="0 0 12 12" style={{width: size, height: size}}>
<g className="background" fill="none" fillRule="evenodd">
<path d="M0 0h12v12H0" />
<path className="fill" fill="#dcddde" d="M9.5 3.205L8.795 2.5 6 5.295 3.205 2.5l-.705.705L5.295 6 2.5 8.795l.705.705L6 6.705 8.795 9.5l.705-.705L6.705 6" />

View File

@ -0,0 +1,11 @@
import {React} from "modules";
export default class Keyboard extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg className={this.props.className} viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}}>
<path d="M20 5H4c-1.1 0-1.99.9-1.99 2L2 17c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2V8zm0 3h2v2h-2v-2zM8 8h2v2H8V8zm0 3h2v2H8v-2zm-1 2H5v-2h2v2zm0-3H5V8h2v2zm9 7H8v-2h8v2zm0-4h-2v-2h2v2zm0-3h-2V8h2v2zm3 3h-2v-2h2v2zm0-3h-2V8h2v2z" />
<path fill="none" d="M0 0h24v24H0zm0 0h24v24H0z" />
</svg>;
}
}

View File

@ -0,0 +1,12 @@
import {React} from "modules";
export default class Radio extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg className={this.props.className} viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}}>
<path fill="none" d="M0 0h24v24H0z" />
{this.props.checked && <path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" />}
{!this.props.checked && <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" />}
</svg>;
}
}

View File

@ -0,0 +1,109 @@
import {React, WebpackModules} from "modules";
const TooltipWrapper = WebpackModules.getByPrototypes("renderTooltip");
const Checkmark = React.memo((props) => (
<svg width="16" height="16" viewBox="0 0 24 24" {...props}>
<path fillRule="evenodd" clipRule="evenodd" fill={props.color ?? "#fff"} d="M8.99991 16.17L4.82991 12L3.40991 13.41L8.99991 19L20.9999 7.00003L19.5899 5.59003L8.99991 16.17Z" />
</svg>
));
const Dropper = React.memo((props) => (
<svg width="14" height="14" viewBox="0 0 16 16" {...props}>
<g fill="none">
<path d="M-4-4h24v24H-4z"/>
<path fill={props.color ?? "#fff"} d="M14.994 1.006C13.858-.257 11.904-.3 10.72.89L8.637 2.975l-.696-.697-1.387 1.388 5.557 5.557 1.387-1.388-.697-.697 1.964-1.964c1.13-1.13 1.3-2.985.23-4.168zm-13.25 10.25c-.225.224-.408.48-.55.764L.02 14.37l1.39 1.39 2.35-1.174c.283-.14.54-.33.765-.55l4.808-4.808-2.776-2.776-4.813 4.803z" />
</g>
</svg>
));
const defaultColors = [1752220, 3066993, 3447003, 10181046, 15277667, 15844367, 15105570, 15158332, 9807270, 6323595, 1146986, 2067276, 2123412, 7419530, 11342935, 12745742, 11027200, 10038562, 9936031, 5533306];
const resolveColor = (color, hex = true) => {
switch (typeof color) {
case (hex && "number"): return `#${color.toString(16)}`;
case (!hex && "string"): return Number.parseInt(color.replace("#", ""), 16);
case (!hex && "number"): return color;
case (hex && "string"): return color;
default: return color;
}
};
const getRGB = (color) => {
let result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color);
if (result) return [parseInt(result[1]), parseInt(result[2]), parseInt(result[3])];
result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)%\s*,\s*([0-9]+(?:\.[0-9]+)?)%\s*,\s*([0-9]+(?:\.[0-9]+)?)%\s*\)/.exec(color);
if (result) return [parseFloat(result[1]) * 2.55, parseFloat(result[2]) * 2.55, parseFloat(result[3]) * 2.55];
result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color);
if (result) return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)];
result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color);
if (result) return [parseInt(result[1] + result[1], 16), parseInt(result[2] + result[2], 16), parseInt(result[3] + result[3], 16)];
};
const luma = (color) => {
const rgb = (typeof(color) === "string") ? getRGB(color) : color;
return (0.2126 * rgb[0]) + (0.7152 * rgb[1]) + (0.0722 * rgb[2]); // SMPTE C, Rec. 709 weightings
};
const getContrastColor = (color) => {
return (luma(color) >= 165) ? "#000" : "#fff";
};
export default class Color extends React.Component {
constructor(props) {
super(props);
this.state = {value: this.props.value};
this.onChange = this.onChange.bind(this);
}
onChange(e) {
this.setState({value: e.target.value});
if (this.props.onChange) this.props.onChange(resolveColor(e.target.value));
}
render() {
const intValue = resolveColor(this.state.value, false);
const {colors = defaultColors, defaultValue} = this.props;
return <div className="bd-color-picker-container">
<div className="bd-color-picker-controls">
<TooltipWrapper text="Default" position="bottom">
{props => (
<div {...props} className="bd-color-picker-default" style={{backgroundColor: resolveColor(defaultValue)}} onClick={() => this.onChange({target: {value: defaultValue}})}>
{intValue === resolveColor(defaultValue, false)
? <Checkmark width="25" height="25" />
: null
}
</div>
)}
</TooltipWrapper>
<TooltipWrapper text="Custom Color" position="bottom">
{props => (
<div className="bd-color-picker-custom">
<Dropper color={getContrastColor(resolveColor(this.state.value, true))} />
<input {...props} style={{backgroundColor: resolveColor(this.state.value)}} type="color" className="bd-color-picker" value={resolveColor(this.state.value)} onChange={this.onChange} />
</div>
)}
</TooltipWrapper>
</div>
<div className="bd-color-picker-swatch">
{
colors.map((int, index) => (
<div key={index} className="bd-color-picker-swatch-item" style={{backgroundColor: resolveColor(int)}} onClick={() => this.onChange({target: {value: int}})}>
{intValue === int
? <Checkmark color={getContrastColor(resolveColor(this.state.value, true))} />
: null
}
</div>
))
}
</div>
</div>;
}
}

View File

@ -2,12 +2,13 @@ import {React} from "modules";
export default class SettingItem extends React.Component {
render() {
return <div className={"bd-setting-item"}>
return <div className={"bd-setting-item" + (this.props.inline ? " inline" : "")}>
<div className={"bd-setting-header"}>
<label htmlFor={this.props.id} className={"bd-setting-title"}>{this.props.name}</label>
{this.props.children}
{this.props.inline && this.props.children}
</div>
<div className={"bd-setting-note"}>{this.props.note}</div>
{!this.props.inline && this.props.children}
<div className={"bd-setting-divider"} />
</div>;
}

View File

@ -0,0 +1,80 @@
import {React} from "modules";
import Keyboard from "../../icons/keyboard";
import Close from "../../icons/close";
export default class Keybind extends React.Component {
constructor(props) {
super(props);
this.state = {value: this.props.value, isRecording: false};
this.onClick = this.onClick.bind(this);
this.keyHandler = this.keyHandler.bind(this);
this.clearKeybind = this.clearKeybind.bind(this);
this.accum = [];
this.max = this.props.max ?? 2;
}
componentDidMount() {
window.addEventListener("keydown", this.keyHandler);
}
componentWillUnmount() {
window.removeEventListener("keydown", this.keyHandler);
}
/**
*
* @param {KeyboardEvent} event
*/
keyHandler(event) {
if (!this.state.isRecording) return;
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
if (event.repeat || this.accum.includes(event.key)) return;
this.accum.push(event.key);
if (this.accum.length == this.max) {
if (this.props.onChange) this.props.onChange(this.accum);
this.setState({value: this.accum.slice(0), isRecording: false}, () => this.accum.splice(0, this.accum.length));
}
}
/**
*
* @param {MouseEvent} e
*/
onClick(e) {
if (e.target?.className?.includes?.("bd-keybind-clear") || e.target?.closest(".bd-button")?.className?.includes("bd-keybind-clear")) return this.clearKeybind(e);
this.setState({isRecording: !this.state.isRecording});
}
/**
*
* @param {MouseEvent} event
*/
clearKeybind(event) {
event.stopPropagation();
event.preventDefault();
this.accum.splice(0, this.accum.length);
if (this.props.onChange) this.props.onChange(this.accum);
this.setState({value: this.accum, isRecording: false});
}
display() {
if (this.state.isRecording) return "Recording...";
if (!this.state.value.length) return "N/A";
return this.state.value.join(" + ");
}
render() {
const {clearable = true} = this.props;
return <div className={"bd-keybind-wrap" + (this.state.isRecording ? " recording" : "")} onClick={this.onClick}>
<input readOnly={true} type="text" className="bd-keybind-input" value={this.display()} />
<div className="bd-keybind-controls">
<button className={"bd-button bd-keybind-record" + (this.state.isRecording ? " bd-button-danger" : "")}><Keyboard size="24px" /></button>
{clearable && <button onClick={this.clearKeybind} className="bd-button bd-keybind-clear"><Close size="24px" /></button>}
</div>
</div>;
}
}

View File

@ -0,0 +1,44 @@
import {React} from "modules";
import RadioIcon from "../../icons/radio";
export default class Radio extends React.Component {
constructor(props) {
super(props);
this.state = {value: this.props.options.findIndex(o => o.value === this.props.value)};
this.onChange = this.onChange.bind(this);
this.renderOption = this.renderOption.bind(this);
}
onChange(e) {
const index = parseInt(e.target.value);
const newValue = this.props.options[index].value;
this.setState({value: index});
if (this.props.onChange) this.props.onChange(newValue);
}
renderOption(opt, index) {
const isSelected = this.state.value === index;
return <label className={"bd-radio-option" + (isSelected ? " bd-radio-selected" : "")}>
<input onChange={this.onChange} type="radio" name={this.props.name} checked={isSelected} value={index} />
{/* <span className="bd-radio-button"></span> */}
<RadioIcon className="bd-radio-icon" size="24" checked={isSelected} />
<div className="bd-radio-label-wrap">
<div className="bd-radio-label">{opt.name}</div>
<div className="bd-radio-description">{opt.desc || opt.description}</div>
</div>
</label>;
}
render() {
return <div className="bd-radio-group">
{this.props.options.map(this.renderOption)}
</div>;
}
}
/* <label class="container">
<input type="radio" name="test" checked="checked">
<span class="checkmark"></span>
<div class="test">One<div class="desc">Description</div></div>
</label> */

View File

@ -0,0 +1,21 @@
import {React} from "modules";
export default class Slider extends React.Component {
constructor(props) {
super(props);
this.state = {value: this.props.value};
this.onChange = this.onChange.bind(this);
}
onChange(e) {
this.setState({value: e.target.value});
// e.target.style.backgroundSize = (e.target.value - this.props.min) * 100 / (this.props.max - this.props.min) + "% 100%";
if (this.props.onChange) this.props.onChange(e.target.value);
}
render() {
return <div className="bd-slider-wrap">
<div className="bd-slider-label">{this.state.value}</div><input onChange={this.onChange} type="range" className="bd-slider-input" min={this.props.min} max={this.props.max} step={this.props.step} value={this.state.value} style={{backgroundSize: (this.state.value - this.props.min) * 100 / (this.props.max - this.props.min) + "% 100%"}} />
</div>;
}
}

View File

@ -0,0 +1,18 @@
import {React} from "modules";
export default class Textbox extends React.Component {
constructor(props) {
super(props);
this.state = {value: this.props.value};
this.onChange = this.onChange.bind(this);
}
onChange(e) {
this.setState({value: e.target.value});
if (this.props.onChange) this.props.onChange(e.target.value);
}
render() {
return <input onChange={this.onChange} onKeyDown={this.props.onKeyDown} type="text" className="bd-text-input" placeholder={this.props.placeholder} maxLength={this.props.maxLength} value={this.state.value} />;
}
}

View File

@ -6,6 +6,11 @@ import Switch from "./components/switch";
import Dropdown from "./components/dropdown";
import Number from "./components/number";
import Item from "./components/item";
import Textbox from "./components/textbox";
import Slider from "./components/slider";
import Radio from "./components/radio";
import Keybind from "./components/keybind";
import Color from "./components/color";
const baseClassName = "bd-settings-group";
@ -64,8 +69,13 @@ export default class Group extends React.Component {
if (setting.type == "dropdown") component = <Dropdown disabled={setting.disabled} id={setting.id} options={setting.options} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
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={this.onChange.bind(this, setting.id)} />;
if (setting.type == "switch") component = <Switch disabled={setting.disabled} id={setting.id} checked={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "text") component = <Textbox disabled={setting.disabled} id={setting.id} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
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={this.onChange.bind(this, setting.id)} />;
if (setting.type == "radio") component = <Radio disabled={setting.disabled} id={setting.id} name={setting.id} options={setting.options} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "keybind") component = <Keybind disabled={setting.disabled} id={setting.id} value={setting.value} max={setting.max} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "color") component = <Color disabled={setting.disabled} id={setting.id} value={setting.value} defaultValue={setting.defaultValue} colors={setting.colors} onChange={this.onChange.bind(this, setting.id)} />;
if (!component) return null;
return <Item id={setting.id} key={setting.id} name={setting.name} note={setting.note}>{component}</Item>;
return <Item id={setting.id} inline={setting.type !== "radio"} key={setting.id} name={setting.name} note={setting.note}>{component}</Item>;
})}
</div>
{this.props.showDivider && <Divider />}