}) => {
+ const [ delayActive, setDelayActive ] = useState<"loading" | boolean>(() => {
+ props.events.fire("query_setting", { setting: "ppt-release-delay" });
+ return "loading";
+ });
+
+ const [ delay, setDelay ] = useState<"loading" | number>(() => {
+ props.events.fire("query_setting", { setting: "ppt-release-delay-active" });
+ return "loading";
+ });
+
+ const [ isActive, setActive ] = useState(false);
+
+ props.events.reactUse("notify_setting", event => {
+ if(event.setting === "vad-type")
+ setActive(event.value === "push_to_talk");
+ else if(event.setting === "ppt-release-delay")
+ setDelay(event.value);
+ else if(event.setting === "ppt-release-delay-active")
+ setDelayActive(event.value);
+ });
+
+ return (
+
+ { props.events.fire("action_set_setting", { setting: "ppt-release-delay-active", value: value })}}
+ disabled={!isActive}
+ value={delayActive === true}
+ label={Delay on Push to Talk}
+ />
+
+ {
+ if(event.target.value === "") {
+ const target = event.target;
+ setImmediate(() => target.value = "");
+ return;
+ }
+
+
+ const newValue = event.target.valueAsNumber;
+ if(isNaN(newValue))
+ return;
+
+ if(newValue < 0 || newValue > 4000)
+ return;
+
+ props.events.fire("action_set_setting", { setting: "ppt-release-delay", value: newValue });
+ }}
+ />}
+ />
+
+ );
+}
+
+const VadSelector = (props: { events: Registry }) => {
+ const [ selectedType, setSelectedType ] = useState(() => {
+ props.events.fire("query_setting", { setting: "vad-type" });
+ return "loading";
+ });
+
+ props.events.reactUse("notify_setting", event => {
+ if(event.setting !== "vad-type")
+ return;
+
+ setSelectedType(event.value);
+ });
+
+ return (
+
+
+
+
{ props.events.fire("action_set_setting", { setting: "vad-type", value: "push_to_talk" }) }}
+ selected={selectedType === "push_to_talk"}
+ disabled={selectedType === "loading"}
+ >
+ Push to Talk
+
+
+
+
+
{ props.events.fire("action_set_setting", { setting: "vad-type", value: "threshold" }) }}
+ selected={selectedType === "threshold"}
+ disabled={selectedType === "loading"}
+ >
+ Voice activity detection
+
+
+
+
{ props.events.fire("action_set_setting", { setting: "vad-type", value: "active" }) }}
+ selected={selectedType === "active"}
+ disabled={selectedType === "loading"}
+ >
+ Always active
+
+
+
+
+ );
+}
+
+const ThresholdSelector = (props: { events: Registry }) => {
+ const refSlider = useRef();
+ const [ value, setValue ] = useState<"loading" | number>(() => {
+ props.events.fire("query_setting", { setting: "threshold-threshold" });
+ return "loading";
+ });
+
+ const [ isActive, setActive ] = useState(false);
+
+ props.events.reactUse("notify_setting", event => {
+ if(event.setting === "threshold-threshold") {
+ refSlider.current?.setState({ value: event.value });
+ setValue(event.value);
+ } else if(event.setting === "vad-type") {
+ setActive(event.value === "threshold");
+ }
+ });
+
+ return (
+
+ )
+};
+
+export const MicrophoneSettings = (props: { events: Registry }) => (
+
+)
\ No newline at end of file
diff --git a/shared/js/ui/react-elements/Button.scss b/shared/js/ui/react-elements/Button.scss
index 5c384634..a188c825 100644
--- a/shared/js/ui/react-elements/Button.scss
+++ b/shared/js/ui/react-elements/Button.scss
@@ -77,6 +77,10 @@ html:root {
border-bottom-color: var(--button-yellow);
}
+ &.color-none {
+ --keep-alive: true;
+ }
+
&.type-normal { }
&.type-small {
diff --git a/shared/js/ui/react-elements/Button.tsx b/shared/js/ui/react-elements/Button.tsx
index 30e26cca..57f0da81 100644
--- a/shared/js/ui/react-elements/Button.tsx
+++ b/shared/js/ui/react-elements/Button.tsx
@@ -3,7 +3,7 @@ import * as React from "react";
const cssStyle = require("./Button.scss");
export interface ButtonProperties {
- color?: "green" | "blue" | "red" | "purple" | "brown" | "yellow" | "default";
+ color?: "green" | "blue" | "red" | "purple" | "brown" | "yellow" | "default" | "none";
type?: "normal" | "small" | "extra-small";
className?: string;
diff --git a/shared/js/ui/react-elements/InputField.scss b/shared/js/ui/react-elements/InputField.scss
index ce45935b..e076cfbb 100644
--- a/shared/js/ui/react-elements/InputField.scss
+++ b/shared/js/ui/react-elements/InputField.scss
@@ -46,7 +46,7 @@ html:root {
color: var(--boxed-input-field-placeholder);
};
- .prefix {
+ .prefix, .suffix {
flex-grow: 0;
flex-shrink: 0;
@@ -65,6 +65,10 @@ html:root {
@include transition($button_hover_animation_time ease-in-out);
}
+ .suffix {
+ padding-left: 0;
+ }
+
&.is-invalid {
background-color: var(--boxed-input-field-invalid-background);
border-color: var(--boxed-input-field-invalid-border);
@@ -80,7 +84,7 @@ html:root {
color: var(--boxed-input-field-focus-text);
- .prefix {
+ .prefix, .suffix {
width: 0;
padding-left: 0;
padding-right: 0;
@@ -125,6 +129,7 @@ html:root {
}
&.disabled, &:disabled {
+ @include user-select(none);
background-color: var(--boxed-input-field-disabled-background);
}
@@ -140,6 +145,12 @@ html:root {
}
}
+ &::-webkit-inner-spin-button,
+ &::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+
@include transition($button_hover_animation_time ease-in-out);
}
diff --git a/shared/js/ui/react-elements/InputField.tsx b/shared/js/ui/react-elements/InputField.tsx
index 1f0e80c9..74db72b3 100644
--- a/shared/js/ui/react-elements/InputField.tsx
+++ b/shared/js/ui/react-elements/InputField.tsx
@@ -5,6 +5,8 @@ const cssStyle = require("./InputField.scss");
export interface BoxedInputFieldProperties {
prefix?: string;
+ suffix?: string;
+
placeholder?: string;
disabled?: boolean;
@@ -79,6 +81,7 @@ export class BoxedInputField extends React.Component this.props.onInput(event.currentTarget.value))}
onKeyDown={e => this.onKeyDown(e)}
/>}
+ {this.props.suffix ? {this.props.suffix} : undefined}
{this.props.rightIcon ? this.props.rightIcon() : ""}
)
diff --git a/shared/js/ui/react-elements/RadioButton.scss b/shared/js/ui/react-elements/RadioButton.scss
new file mode 100644
index 00000000..4efed01f
--- /dev/null
+++ b/shared/js/ui/react-elements/RadioButton.scss
@@ -0,0 +1,68 @@
+@import "../../../css/static/mixin";
+@import "../../../css/static/properties";
+
+.container {
+ $button_size: 1.2em;
+ $mark_size: .6em;
+
+ position: relative;
+
+ width: $button_size;
+ height: $button_size;
+
+ cursor: pointer;
+
+ overflow: hidden;
+
+ background-color: #272626;
+ border-radius: 50%;
+
+ align-self: center;
+ margin-right: .5em;
+
+ input {
+ position: absolute;
+ width: 0;
+ height: 0;
+ opacity: 0;
+ }
+
+ .mark {
+ position: absolute;
+ opacity: 0;
+
+ top: ($button_size - $mark_size) / 2;
+ bottom: ($button_size - $mark_size) / 2;
+ right: ($button_size - $mark_size) / 2;
+ left: ($button_size - $mark_size) / 2;
+
+ background-color: #46c0ec;
+ box-shadow: 0 0 .5em 1px rgba(70, 192, 236, 0.4);
+ border-radius: 50%;
+
+ @include transition(.4s);
+ }
+
+ input:checked + .mark {
+ opacity: 1;
+ }
+
+ @include transition(background-color $button_hover_animation_time);
+
+ -webkit-box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5);
+ -moz-box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5);
+ box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5);
+}
+
+label:hover > .container, .container:hover {
+ &.container, > .container {
+ background-color: #2c2b2b;
+ }
+}
+
+label.disabled > .container, .container.disabled, .container:disabled {
+ &.container, > .container {
+ pointer-events: none!important;
+ background-color: #1a1919!important;
+ }
+}
\ No newline at end of file
diff --git a/shared/js/ui/react-elements/RadioButton.tsx b/shared/js/ui/react-elements/RadioButton.tsx
new file mode 100644
index 00000000..f7c14f14
--- /dev/null
+++ b/shared/js/ui/react-elements/RadioButton.tsx
@@ -0,0 +1,28 @@
+import * as React from "react";
+
+const cssStyle = require("./RadioButton.scss");
+export const RadioButton = (props: {
+ children?: React.ReactNode | React.ReactNode[],
+
+ name: string,
+ selected: boolean,
+
+ disabled?: boolean
+
+ onChange: (checked: boolean) => void,
+}) => {
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/shared/js/ui/react-elements/Slider.scss b/shared/js/ui/react-elements/Slider.scss
index 981d45b7..76199cd8 100644
--- a/shared/js/ui/react-elements/Slider.scss
+++ b/shared/js/ui/react-elements/Slider.scss
@@ -18,6 +18,10 @@ html:root {
--slider-disabled-thumb-color: #4d4d4d;
}
+.documentClass {
+ @include user-select(none);
+}
+
.container {
font-size: .8em;
@@ -40,7 +44,6 @@ html:root {
.filler {
position: absolute;
- left: 0;
top: 0;
bottom: 0;
diff --git a/shared/js/ui/react-elements/Slider.tsx b/shared/js/ui/react-elements/Slider.tsx
index dd1fb612..7802b92c 100644
--- a/shared/js/ui/react-elements/Slider.tsx
+++ b/shared/js/ui/react-elements/Slider.tsx
@@ -13,6 +13,9 @@ export interface SliderProperties {
disabled?: boolean;
className?: string;
+ classNameFiller?: string;
+ inverseFiller?: boolean;
+
unit?: string;
tooltip?: (value: number) => ReactElement | string;
@@ -36,8 +39,8 @@ export class Slider extends React.Component {
private readonly mouseListener;
private readonly mouseUpListener;
- private readonly refTooltip = React.createRef();
- private readonly refSlider = React.createRef();
+ protected readonly refTooltip = React.createRef();
+ protected readonly refSlider = React.createRef();
constructor(props) {
super(props);
@@ -87,6 +90,7 @@ export class Slider extends React.Component {
if(!this.documentListenersRegistered) return;
this.documentListenersRegistered = false;
+ document.body.classList.remove(cssStyle.documentClass);
document.removeEventListener('mousemove', this.mouseListener);
document.removeEventListener('touchmove', this.mouseListener);
@@ -100,6 +104,7 @@ export class Slider extends React.Component {
if(this.documentListenersRegistered) return;
this.documentListenersRegistered = true;
+ document.body.classList.add(cssStyle.documentClass);
document.addEventListener('mousemove', this.mouseListener);
document.addEventListener('touchmove', this.mouseListener);
@@ -124,7 +129,10 @@ export class Slider extends React.Component {
onMouseDown={e => this.enableSliderMode(e)}
onTouchStart={e => this.enableSliderMode(e)}
>
-
+
this.props.tooltip ? this.props.tooltip(this.state.value) : this.renderTooltip()}>
@@ -132,14 +140,14 @@ export class Slider extends React.Component {
);
}
- private enableSliderMode(event: React.MouseEvent | React.TouchEvent) {
+ protected enableSliderMode(event: React.MouseEvent | React.TouchEvent) {
this.setState({ active: true });
this.registerDocumentListener();
this.mouseListener(event);
this.refTooltip.current?.setState({ forceShow: true });
}
- private renderTooltip() {
+ protected renderTooltip() {
return {this.state.value + (this.props.unit || "")};
}
diff --git a/shared/js/ui/react-elements/internal-modal/Controller.ts b/shared/js/ui/react-elements/internal-modal/Controller.ts
index ed87c654..0e89b2a3 100644
--- a/shared/js/ui/react-elements/internal-modal/Controller.ts
+++ b/shared/js/ui/react-elements/internal-modal/Controller.ts
@@ -94,4 +94,4 @@ export class InternalModalController {
- if(this._ppt_timeout)
- clearTimeout(this._ppt_timeout);
+ if(this.pptTimeout)
+ clearTimeout(this.pptTimeout);
- this._ppt_timeout = setTimeout(() => {
+ this.pptTimeout = setTimeout(() => {
const f = this.input.get_filter(filter.Type.STATE) as filter.StateFilter;
if(f) f.set_state(true);
- }, Math.min(this.config.vad_push_to_talk.delay, 0));
+ }, Math.max(this.config.vad_push_to_talk.delay, 0));
},
callback_press: () => {
- if(this._ppt_timeout)
- clearTimeout(this._ppt_timeout);
+ if(this.pptTimeout)
+ clearTimeout(this.pptTimeout);
const f = this.input.get_filter(filter.Type.STATE) as filter.StateFilter;
if(f) f.set_state(false);
@@ -82,7 +82,7 @@ export class RecorderProfile {
cancel: false
} as KeyHook;
- this._ppt_hook_registered = false;
+ this.pptHookRegistered = false;
this.record_supported = true;
}
@@ -152,7 +152,7 @@ export class RecorderProfile {
}
}
- private save(enforce?: boolean) {
+ private save() {
if(!this.volatile)
settings.changeGlobal(Settings.FN_PROFILE_RECORD(this.name), this.config);
}
@@ -161,9 +161,9 @@ export class RecorderProfile {
if(!this.input) return;
this.input.clear_filter();
- if(this._ppt_hook_registered) {
- ppt.unregister_key_hook(this._ppt_hook);
- this._ppt_hook_registered = false;
+ if(this.pptHookRegistered) {
+ ppt.unregister_key_hook(this.pptHook);
+ this.pptHookRegistered = false;
}
if(this.config.vad_type === "threshold") {
@@ -174,6 +174,7 @@ export class RecorderProfile {
/* legacy client support */
if('set_attack_smooth' in filter_)
filter_.set_attack_smooth(.25);
+
if('set_release_smooth' in filter_)
filter_.set_release_smooth(.9);
@@ -183,9 +184,9 @@ export class RecorderProfile {
await filter_.set_state(true);
for(const key of ["key_alt", "key_ctrl", "key_shift", "key_windows", "key_code"])
- this._ppt_hook[key] = this.config.vad_push_to_talk[key];
- ppt.register_key_hook(this._ppt_hook);
- this._ppt_hook_registered = true;
+ this.pptHook[key] = this.config.vad_push_to_talk[key];
+ ppt.register_key_hook(this.pptHook);
+ this.pptHookRegistered = true;
this.input.enable_filter(filter.Type.STATE);
} else if(this.config.vad_type === "active") {}
@@ -254,7 +255,7 @@ export class RecorderProfile {
}
- current_device() : InputDevice | undefined { return this.input.current_device(); }
+ current_device() : InputDevice | undefined { return this.input?.current_device(); }
set_device(device: InputDevice | undefined) : Promise {
this.config.device_id = device ? device.unique_id : undefined;
this.save();