diff --git a/renderer/src/data/settings.js b/renderer/src/data/settings.js index 5fb700ea..0209d474 100644 --- a/renderer/src/data/settings.js +++ b/renderer/src/data/settings.js @@ -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"}, + // ] + // } ]; \ No newline at end of file diff --git a/renderer/src/modules/settingsmanager.js b/renderer/src/modules/settingsmanager.js index f8ded34c..c41652ef 100644 --- a/renderer/src/modules/settingsmanager.js +++ b/renderer/src/modules/settingsmanager.js @@ -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]; + } } } } diff --git a/renderer/src/styles/ui/bdsettings.css b/renderer/src/styles/ui/bdsettings.css index a0a05100..6a534e89 100644 --- a/renderer/src/styles/ui/bdsettings.css +++ b/renderer/src/styles/ui/bdsettings.css @@ -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; diff --git a/renderer/src/styles/ui/colorpicker.css b/renderer/src/styles/ui/colorpicker.css new file mode 100644 index 00000000..24f240c0 --- /dev/null +++ b/renderer/src/styles/ui/colorpicker.css @@ -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; +} \ No newline at end of file diff --git a/renderer/src/styles/ui/keybind.css b/renderer/src/styles/ui/keybind.css new file mode 100644 index 00000000..0385cb31 --- /dev/null +++ b/renderer/src/styles/ui/keybind.css @@ -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; +} \ No newline at end of file diff --git a/renderer/src/styles/ui/radio.css b/renderer/src/styles/ui/radio.css new file mode 100644 index 00000000..bf23b332 --- /dev/null +++ b/renderer/src/styles/ui/radio.css @@ -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; +} \ No newline at end of file diff --git a/renderer/src/styles/ui/slider.css b/renderer/src/styles/ui/slider.css new file mode 100644 index 00000000..90c8e4c2 --- /dev/null +++ b/renderer/src/styles/ui/slider.css @@ -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; + } \ No newline at end of file diff --git a/renderer/src/styles/ui/textbox.css b/renderer/src/styles/ui/textbox.css new file mode 100644 index 00000000..a277a2e3 --- /dev/null +++ b/renderer/src/styles/ui/textbox.css @@ -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; + } \ No newline at end of file diff --git a/renderer/src/ui/icons/close.jsx b/renderer/src/ui/icons/close.jsx index 404a2d2b..b93cba8e 100644 --- a/renderer/src/ui/icons/close.jsx +++ b/renderer/src/ui/icons/close.jsx @@ -2,7 +2,8 @@ import {React} from "modules"; export default class CloseButton extends React.Component { render() { - return + const size = this.props.size || "18px"; + return diff --git a/renderer/src/ui/icons/keyboard.jsx b/renderer/src/ui/icons/keyboard.jsx new file mode 100644 index 00000000..5472f2a3 --- /dev/null +++ b/renderer/src/ui/icons/keyboard.jsx @@ -0,0 +1,11 @@ +import {React} from "modules"; + +export default class Keyboard extends React.Component { + render() { + const size = this.props.size || "24px"; + return + + + ; + } +} \ No newline at end of file diff --git a/renderer/src/ui/icons/radio.jsx b/renderer/src/ui/icons/radio.jsx new file mode 100644 index 00000000..d56d9c84 --- /dev/null +++ b/renderer/src/ui/icons/radio.jsx @@ -0,0 +1,12 @@ +import {React} from "modules"; + +export default class Radio extends React.Component { + render() { + const size = this.props.size || "24px"; + return + + {this.props.checked && } + {!this.props.checked && } + ; + } +} \ No newline at end of file diff --git a/renderer/src/ui/settings/components/color.jsx b/renderer/src/ui/settings/components/color.jsx new file mode 100644 index 00000000..019e144e --- /dev/null +++ b/renderer/src/ui/settings/components/color.jsx @@ -0,0 +1,109 @@ +import {React, WebpackModules} from "modules"; + +const TooltipWrapper = WebpackModules.getByPrototypes("renderTooltip"); + +const Checkmark = React.memo((props) => ( + + + +)); + +const Dropper = React.memo((props) => ( + + + + + + +)); + +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
+
+ + {props => ( +
this.onChange({target: {value: defaultValue}})}> + {intValue === resolveColor(defaultValue, false) + ? + : null + } +
+ )} +
+ + {props => ( +
+ + +
+ )} +
+
+
+ { + colors.map((int, index) => ( +
this.onChange({target: {value: int}})}> + {intValue === int + ? + : null + } +
+ )) + } +
+
; + } +} \ No newline at end of file diff --git a/renderer/src/ui/settings/components/item.jsx b/renderer/src/ui/settings/components/item.jsx index 12e4b65f..e7fc2075 100644 --- a/renderer/src/ui/settings/components/item.jsx +++ b/renderer/src/ui/settings/components/item.jsx @@ -2,12 +2,13 @@ import {React} from "modules"; export default class SettingItem extends React.Component { render() { - return
+ return
- {this.props.children} + {this.props.inline && this.props.children}
{this.props.note}
+ {!this.props.inline && this.props.children}
; } diff --git a/renderer/src/ui/settings/components/keybind.jsx b/renderer/src/ui/settings/components/keybind.jsx new file mode 100644 index 00000000..dd028bfc --- /dev/null +++ b/renderer/src/ui/settings/components/keybind.jsx @@ -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
+ +
+ + {clearable && } +
+
; + } +} \ No newline at end of file diff --git a/renderer/src/ui/settings/components/radio.jsx b/renderer/src/ui/settings/components/radio.jsx new file mode 100644 index 00000000..fbacc3fa --- /dev/null +++ b/renderer/src/ui/settings/components/radio.jsx @@ -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 ; + } + + render() { + return
+ {this.props.options.map(this.renderOption)} +
; + } +} + +/* */ \ No newline at end of file diff --git a/renderer/src/ui/settings/components/slider.jsx b/renderer/src/ui/settings/components/slider.jsx new file mode 100644 index 00000000..c8b95447 --- /dev/null +++ b/renderer/src/ui/settings/components/slider.jsx @@ -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
+
{this.state.value}
+
; + } +} \ No newline at end of file diff --git a/renderer/src/ui/settings/components/textbox.jsx b/renderer/src/ui/settings/components/textbox.jsx new file mode 100644 index 00000000..3a2b9d73 --- /dev/null +++ b/renderer/src/ui/settings/components/textbox.jsx @@ -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 ; + } +} \ No newline at end of file diff --git a/renderer/src/ui/settings/group.jsx b/renderer/src/ui/settings/group.jsx index b65b331f..6d401e91 100644 --- a/renderer/src/ui/settings/group.jsx +++ b/renderer/src/ui/settings/group.jsx @@ -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 = ; if (setting.type == "number") component = ; if (setting.type == "switch") component = ; + if (setting.type == "text") component = ; + if (setting.type == "slider") component = ; + if (setting.type == "radio") component = ; + if (setting.type == "keybind") component = ; + if (setting.type == "color") component = ; if (!component) return null; - return {component}; + return {component}; })}
{this.props.showDivider && }