Using React for the microphone settings
parent
405bc7512d
commit
56d9e9fa16
|
@ -1,4 +1,7 @@
|
|||
# Changelog:
|
||||
* **11.08.20**
|
||||
- Fixed the voice push to talk delay
|
||||
|
||||
* **09.08.20**
|
||||
- Added a "watch to gather" context menu entry for clients
|
||||
- Disassembled the current client icon sprite into his icons
|
||||
|
|
|
@ -1 +1,19 @@
|
|||
<svg id="Flat" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><g fill="#8690fa"><circle cx="48" cy="304" r="24"/><circle cx="192" cy="448" r="24"/><circle cx="101" cy="395" r="24"/><circle cx="48" cy="304" r="24"/><path d="m216 448a24 24 0 1 0 -24 24 24 24 0 0 0 24-24"/><circle cx="101" cy="395" r="24"/><circle cx="48" cy="192" r="24"/><circle cx="192" cy="48" r="24"/><circle cx="101" cy="101" r="24"/><circle cx="48" cy="192" r="24"/><path d="m216 48a24 24 0 1 1 -24-24 24 24 0 0 1 24 24"/><circle cx="101" cy="101" r="24"/><path d="m311.992 472a24 24 0 0 1 -6.433-47.123 185.506 185.506 0 0 0 96.328-64.917 181.561 181.561 0 0 0 38.113-111.96c0-81.5-55.349-154.248-134.6-176.922a24 24 0 0 1 13.2-46.147 236.543 236.543 0 0 1 121 82.086 230.506 230.506 0 0 1 .276 282.283 233.819 233.819 0 0 1 -121.421 81.812 24.04 24.04 0 0 1 -6.463.888z"/></g><path d="m456.029 488a24.512 24.512 0 0 1 -2.679-.148l-144-16a24 24 0 0 1 -20.474-30.278l40-144a24 24 0 1 1 46.248 12.848l-32.454 116.838 115.98 12.886a24 24 0 0 1 -2.621 47.854z" fill="#5153ff"/></svg>
|
||||
<svg id="Flat" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#7289da">
|
||||
<circle cx="48" cy="304" r="24"/>
|
||||
<circle cx="192" cy="448" r="24"/>
|
||||
<circle cx="101" cy="395" r="24"/>
|
||||
<circle cx="48" cy="304" r="24"/>
|
||||
<path d="m216 448a24 24 0 1 0 -24 24 24 24 0 0 0 24-24"/>
|
||||
<circle cx="101" cy="395" r="24"/>
|
||||
<circle cx="48" cy="192" r="24"/>
|
||||
<circle cx="192" cy="48" r="24"/>
|
||||
<circle cx="101" cy="101" r="24"/>
|
||||
<circle cx="48" cy="192" r="24"/>
|
||||
<path d="m216 48a24 24 0 1 1 -24-24 24 24 0 0 1 24 24"/>
|
||||
<circle cx="101" cy="101" r="24"/>
|
||||
<path d="m311.992 472a24 24 0 0 1 -6.433-47.123 185.506 185.506 0 0 0 96.328-64.917 181.561 181.561 0 0 0 38.113-111.96c0-81.5-55.349-154.248-134.6-176.922a24 24 0 0 1 13.2-46.147 236.543 236.543 0 0 1 121 82.086 230.506 230.506 0 0 1 .276 282.283 233.819 233.819 0 0 1 -121.421 81.812 24.04 24.04 0 0 1 -6.463.888z"/>
|
||||
</g>
|
||||
<path d="m456.029 488a24.512 24.512 0 0 1 -2.679-.148l-144-16a24 24 0 0 1 -20.474-30.278l40-144a24 24 0 1 1 46.248 12.848l-32.454 116.838 115.98 12.886a24 24 0 0 1 -2.621 47.854z"
|
||||
fill="#7289da"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
|
@ -994,7 +994,7 @@ export class LocalClientEntry extends ClientEntry {
|
|||
renameSelf(new_name: string) : Promise<boolean> {
|
||||
const old_name = this.properties.client_nickname;
|
||||
this.updateVariables({ key: "client_nickname", value: new_name }); /* change it locally */
|
||||
return this.handle.serverConnection.command_helper.updateClient("client_nickname", new_name).then((e) => {
|
||||
return this.handle.serverConnection.send_command("clientupdate", { client_nickname: new_name }).then((e) => {
|
||||
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, new_name);
|
||||
this.channelTree.client.log.log(EventType.CLIENT_NICKNAME_CHANGED_OWN, {
|
||||
client: this.log_data(),
|
||||
|
|
|
@ -294,8 +294,8 @@ function initializeStepIdentity(tag: JQuery, event_registry: Registry<emodal.new
|
|||
function initializeStepMicrophone(tag: JQuery, event_registry: Registry<emodal.newcomer>, modal: Modal) {
|
||||
const microphone_events = new Registry<emodal.settings.microphone>();
|
||||
//microphone_events.enable_debug("settings-microphone");
|
||||
modal_settings.initialize_audio_microphone_controller(microphone_events);
|
||||
modal_settings.initialize_audio_microphone_view(tag, microphone_events);
|
||||
//modal_settings.initialize_audio_microphone_controller(microphone_events);
|
||||
//modal_settings.initialize_audio_microphone_view(tag, microphone_events);
|
||||
modal.close_listener.push(() => microphone_events.fire_async("deinitialize"));
|
||||
|
||||
let help_animation_done = false;
|
||||
|
|
|
@ -32,6 +32,8 @@ import {KeyMapSettings} from "tc-shared/ui/modal/settings/Keymap";
|
|||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import {NotificationSettings} from "tc-shared/ui/modal/settings/Notifications";
|
||||
import {initialize_audio_microphone_controller, MicrophoneSettingsEvents} from "tc-shared/ui/modal/settings/Microphone";
|
||||
import {MicrophoneSettings} from "tc-shared/ui/modal/settings/MicrophoneRenderer";
|
||||
|
||||
export function spawnSettingsModal(default_page?: string) : Modal {
|
||||
let modal: Modal;
|
||||
|
@ -443,12 +445,16 @@ function settings_general_chat(container: JQuery, modal: Modal) {
|
|||
}
|
||||
|
||||
function settings_audio_microphone(container: JQuery, modal: Modal) {
|
||||
const registry = new Registry<events.modal.settings.microphone>();
|
||||
registry.enableDebug("settings-microphone");
|
||||
modal_settings.initialize_audio_microphone_controller(registry);
|
||||
modal_settings.initialize_audio_microphone_view(container, registry);
|
||||
const registry = new Registry<MicrophoneSettingsEvents>();
|
||||
initialize_audio_microphone_controller(registry);
|
||||
|
||||
const entry = <MicrophoneSettings events={registry} />;
|
||||
ReactDOM.render(entry, container[0]);
|
||||
modal.close_listener.push(() => {
|
||||
ReactDOM.unmountComponentAtNode(container[0]);
|
||||
registry.fire("notify_destroy");
|
||||
});
|
||||
|
||||
modal.close_listener.push(() => registry.fire_async("deinitialize"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1698,699 +1704,6 @@ export namespace modal_settings {
|
|||
event_registry.fire("select-profile", { profile_id: "default" });
|
||||
event_registry.fire("select-identity-type", { profile_id: "default", identity_type: undefined });
|
||||
}
|
||||
|
||||
export function initialize_audio_microphone_controller(event_registry: Registry<events.modal.settings.microphone>) {
|
||||
/* level meters */
|
||||
{
|
||||
const level_meters: {[key: string]:Promise<LevelMeter>} = {};
|
||||
const level_info: {[key: string]:any} = {};
|
||||
let level_update_task;
|
||||
|
||||
const destroy_meters = () => {
|
||||
Object.keys(level_meters).forEach(e => {
|
||||
const meter = level_meters[e];
|
||||
delete level_meters[e];
|
||||
|
||||
meter.then(e => e.destory());
|
||||
});
|
||||
Object.keys(level_info).forEach(e => delete level_info[e]);
|
||||
};
|
||||
|
||||
const update_level_meter = () => {
|
||||
destroy_meters();
|
||||
|
||||
for(const device of arecorder.devices()) {
|
||||
let promise = arecorder.create_levelmeter(device).then(meter => {
|
||||
meter.set_observer(level => {
|
||||
if(level_meters[device.unique_id] !== promise) return; /* old level meter */
|
||||
|
||||
level_info[device.unique_id] = {
|
||||
device_id: device.unique_id,
|
||||
status: "success",
|
||||
level: level
|
||||
};
|
||||
});
|
||||
return Promise.resolve(meter);
|
||||
}).catch(error => {
|
||||
if(level_meters[device.unique_id] !== promise) return; /* old level meter */
|
||||
level_info[device.unique_id] = {
|
||||
device_id: device.unique_id,
|
||||
status: "error",
|
||||
|
||||
error: error
|
||||
};
|
||||
|
||||
log.warn(LogCategory.AUDIO, tr("Failed to initialize a level meter for device %s (%s): %o"), device.unique_id, device.driver + ":" + device.name, error);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
level_meters[device.unique_id] = promise;
|
||||
}
|
||||
};
|
||||
|
||||
level_update_task = setInterval(() => {
|
||||
event_registry.fire("update-device-level", {
|
||||
devices: Object.keys(level_info).map(e => level_info[e])
|
||||
});
|
||||
}, 50);
|
||||
|
||||
event_registry.on("query-device-result", event => {
|
||||
if(event.status !== "success") return;
|
||||
|
||||
update_level_meter();
|
||||
});
|
||||
|
||||
event_registry.on("deinitialize", event => {
|
||||
destroy_meters();
|
||||
clearInterval(level_update_task);
|
||||
});
|
||||
}
|
||||
|
||||
/* device list */
|
||||
{
|
||||
event_registry.on("query-devices", event => {
|
||||
Promise.resolve().then(() => {
|
||||
return arecorder.device_refresh_available() && event.refresh_list ? arecorder.refresh_devices() : Promise.resolve();
|
||||
}).catch(error => {
|
||||
log.warn(LogCategory.AUDIO, tr("Failed to refresh device list: %o"), error);
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
const devices = arecorder.devices();
|
||||
|
||||
event_registry.fire_async("query-device-result", {
|
||||
status: "success",
|
||||
active_device: default_recorder.current_device() ? default_recorder.current_device().unique_id : "none",
|
||||
devices: devices.map(e => { return { id: e.unique_id, name: e.name, driver: e.driver }})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
event_registry.on("set-device", event => {
|
||||
const device = arecorder.devices().find(e => e.unique_id === event.device_id);
|
||||
if(!device && event.device_id !== "none") {
|
||||
event_registry.fire_async("set-device-result", { status: "error", error: tr("Invalid device id"), device_id: event.device_id });
|
||||
return;
|
||||
}
|
||||
|
||||
default_recorder.set_device(device).then(() => {
|
||||
console.debug(tr("Changed default microphone device"));
|
||||
event_registry.fire_async("set-device-result", { status: "success", device_id: event.device_id });
|
||||
}).catch((error) => {
|
||||
log.warn(LogCategory.AUDIO, tr("Failed to change microphone to device %s: %o"), device ? device.unique_id : "none", error);
|
||||
event_registry.fire_async("set-device-result", { status: "success", device_id: event.device_id });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* settings */
|
||||
{
|
||||
event_registry.on("query-settings", event => {
|
||||
event_registry.fire_async("query-settings-result", {
|
||||
status: "success",
|
||||
info: {
|
||||
volume: default_recorder.get_volume(),
|
||||
vad_type: default_recorder.get_vad_type(),
|
||||
vad_ppt: {
|
||||
key: default_recorder.get_vad_ppt_key(),
|
||||
release_delay: Math.abs(default_recorder.get_vad_ppt_delay()),
|
||||
release_delay_active: default_recorder.get_vad_ppt_delay() >= 0
|
||||
},
|
||||
vad_threshold: {
|
||||
threshold: default_recorder.get_vad_threshold()
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
event_registry.on("set-setting", event => {
|
||||
const ensure_type = (type: "object" | "string" | "boolean" | "number" | "undefined") => {
|
||||
if(typeof event.value !== type) {
|
||||
event_registry.fire_async("set-setting-result", { status: "error", error: tr("Invalid value type for key") + " (expected: " + type + ", received: " + typeof event.value + ")", setting: event.setting });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
switch (event.setting) {
|
||||
case "volume":
|
||||
if(!ensure_type("number")) return;
|
||||
default_recorder.set_volume(event.value);
|
||||
break;
|
||||
|
||||
case "threshold-threshold":
|
||||
if(!ensure_type("number")) return;
|
||||
default_recorder.set_vad_threshold(event.value);
|
||||
break;
|
||||
|
||||
case "vad-type":
|
||||
if(!ensure_type("string")) return;
|
||||
if(!default_recorder.set_vad_type(event.value)) {
|
||||
event_registry.fire_async("set-setting-result", { status: "error", error: tr("Unknown VAD type"), setting: event.setting });
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case "ppt-key":
|
||||
if(!ensure_type("object")) return;
|
||||
default_recorder.set_vad_ppt_key(event.value);
|
||||
break;
|
||||
|
||||
case "ppt-release-delay":
|
||||
if(!ensure_type("number")) return;
|
||||
const sign = default_recorder.get_vad_ppt_delay() >= 0 ? 1 : -1;
|
||||
default_recorder.set_vad_ppt_delay(sign * event.value);
|
||||
break;
|
||||
|
||||
case "ppt-release-delay-active":
|
||||
if(!ensure_type("boolean")) return;
|
||||
default_recorder.set_vad_ppt_delay(Math.abs(default_recorder.get_vad_ppt_delay()) * (event.value ? 1 : -1));
|
||||
break;
|
||||
|
||||
default:
|
||||
event_registry.fire_async("set-setting-result", { status: "error", error: tr("Invalid setting key"), setting: event.setting });
|
||||
return;
|
||||
}
|
||||
event_registry.fire_async("set-setting-result", { status: "success", setting: event.setting, value: event.value });
|
||||
});
|
||||
}
|
||||
|
||||
aplayer.on_ready(() => event_registry.fire_async("audio-initialized", {}));
|
||||
}
|
||||
export function initialize_audio_microphone_view(container: JQuery, event_registry: Registry<events.modal.settings.microphone>) {
|
||||
/* device list */
|
||||
{
|
||||
/* actual list */
|
||||
{
|
||||
const container_devices = container.find(".container-devices");
|
||||
const volume_bar_tags: {[key: string]:{ volume: JQuery, error: JQuery }} = {};
|
||||
let pending_changes = 0;
|
||||
let default_device_id;
|
||||
|
||||
const build_device = (device: { id: string, name: string, driver: string }, selected: boolean) => {
|
||||
let tag_volume: JQuery, tag_volume_error: JQuery;
|
||||
const tag = $.spawn("div").attr("device-id", device ? device.id : "none").addClass("device").toggleClass("selected", selected).append(
|
||||
$.spawn("div").addClass("container-selected").append(
|
||||
$.spawn("div").addClass("icon_em client-apply"),
|
||||
$.spawn("div").addClass("icon-loading").append(
|
||||
$.spawn("img").attr("src", "img/icon_settings_loading.svg")
|
||||
)
|
||||
),
|
||||
$.spawn("div").addClass("container-name").append(
|
||||
$.spawn("div").addClass("device-driver").text(
|
||||
device ? (device.driver || "Unknown driver") : "No device"
|
||||
),
|
||||
$.spawn("div").addClass("device-name").text(
|
||||
device ? (device.name || "Unknown name") : "No device"
|
||||
),
|
||||
),
|
||||
$.spawn("div").addClass("container-activity").append(
|
||||
$.spawn("div").addClass("container-activity-bar").append(
|
||||
tag_volume = $.spawn("div").addClass("bar-hider"),
|
||||
tag_volume_error = $.spawn("div").addClass("bar-error")
|
||||
)
|
||||
)
|
||||
);
|
||||
tag_volume.css('width', '100%'); /* initially hide the bar */
|
||||
if(device)
|
||||
volume_bar_tags[device.id] = { volume: tag_volume, error: tag_volume_error };
|
||||
|
||||
tag.on('click', event => {
|
||||
if(tag.hasClass("selected") || pending_changes > 0) return;
|
||||
|
||||
event_registry.fire("set-device", { device_id: device ? device.id : "none" });
|
||||
});
|
||||
|
||||
return tag;
|
||||
};
|
||||
|
||||
event_registry.on("set-device", event => {
|
||||
pending_changes++;
|
||||
|
||||
const default_device = container_devices.find(".selected");
|
||||
default_device_id = default_device.attr("device-id");
|
||||
default_device.removeClass("selected");
|
||||
|
||||
const new_device = container_devices.find(".device[device-id='" + event.device_id + "']");
|
||||
new_device.addClass("loading");
|
||||
});
|
||||
event_registry.on("set-device-result", event => {
|
||||
pending_changes--;
|
||||
container_devices.find(".loading").removeClass("loading");
|
||||
|
||||
if(event.status !== "success") {
|
||||
createErrorModal(tr("Failed to change microphone"), formatMessage(tr("Failed to change the microphone to the target microphone{:br:}{}"), event.status === "timeout" ? tr("Timeout") : event.error || tr("Unknown error"))).open();
|
||||
} else {
|
||||
default_device_id = event.device_id;
|
||||
}
|
||||
|
||||
container_devices.find(".device[device-id='" + default_device_id + "']").addClass("selected");
|
||||
});
|
||||
|
||||
event_registry.on('query-devices', event => {
|
||||
Object.keys(volume_bar_tags).forEach(e => delete volume_bar_tags[e]);
|
||||
container_devices.find(".device").remove();
|
||||
container_devices.find(".overlay").hide();
|
||||
container_devices.find(".overlay.overlay-loading").show();
|
||||
});
|
||||
|
||||
event_registry.on("query-device-result", event => {
|
||||
container_devices.find(".device").remove();
|
||||
container_devices.find(".overlay").hide();
|
||||
|
||||
if(event.status !== "success") {
|
||||
const container_text = container_devices.find(".overlay.overlay-error").show().find(".error-text");
|
||||
container_text.text(event.status === "timeout" ? tr("Timeout while loading") : event.error || tr("An unknown error happened"));
|
||||
return;
|
||||
}
|
||||
|
||||
build_device(undefined, event.active_device === "none").appendTo(container_devices);
|
||||
for(const device of event.devices)
|
||||
build_device(device, event.active_device === device.id).appendTo(container_devices);
|
||||
});
|
||||
|
||||
event_registry.on("update-device-level", event => {
|
||||
for(const device of event.devices) {
|
||||
const tags = volume_bar_tags[device.device_id];
|
||||
if(!tags) continue;
|
||||
|
||||
let level = typeof device.level === "number" ? device.level : 0;
|
||||
if(level > 100) level = 100;
|
||||
else if(level < 0) level = 0;
|
||||
tags.error.attr('title', device.error || null).text(device.error || null);
|
||||
tags.volume.css('width', (100 - level) + '%');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* device list update button */
|
||||
{
|
||||
|
||||
const button_update = container.find(".button-update");
|
||||
event_registry.on(["query-devices", "set-device"], event => button_update.prop("disabled", true));
|
||||
event_registry.on(["query-device-result", "set-device-result"], event => button_update.prop("disabled", false));
|
||||
|
||||
button_update.on("click", event => event_registry.fire("query-devices", { refresh_list: true }));
|
||||
}
|
||||
}
|
||||
|
||||
/* settings */
|
||||
{
|
||||
/* TODO: Query settings error handling */
|
||||
|
||||
/* volume */
|
||||
{
|
||||
const container_volume = container.find(".container-volume");
|
||||
const slider_tag = container_volume.find(".container-slider");
|
||||
let triggered_events = 0;
|
||||
let last_value = -1;
|
||||
|
||||
const slider = sliderfy(slider_tag, {
|
||||
min_value: 0,
|
||||
max_value: 100,
|
||||
step: 1,
|
||||
initial_value: 0
|
||||
});
|
||||
|
||||
slider_tag.on('change', event => {
|
||||
const value = parseInt(slider_tag.attr("value"));
|
||||
if(last_value === value) return;
|
||||
|
||||
triggered_events++;
|
||||
event_registry.fire("set-setting", { setting: "volume", value: value });
|
||||
});
|
||||
|
||||
event_registry.on("query-settings-result", event => {
|
||||
if(event.status !== "success") return;
|
||||
|
||||
last_value = event.info.volume;
|
||||
slider.value(event.info.volume);
|
||||
});
|
||||
|
||||
event_registry.on("set-setting-result", event => {
|
||||
if(event.setting !== "volume") return;
|
||||
if(triggered_events > 0) {
|
||||
triggered_events--;
|
||||
return;
|
||||
}
|
||||
if(event.status !== "success") return;
|
||||
|
||||
last_value = event.value;
|
||||
slider.value(event.value);
|
||||
});
|
||||
}
|
||||
|
||||
/* vad type */
|
||||
{
|
||||
const container_select = container.find(".container-select-vad");
|
||||
let last_value;
|
||||
|
||||
container_select.find("input").on('change', event => {
|
||||
if(!(event.target as HTMLInputElement).checked)
|
||||
return;
|
||||
|
||||
const mode = (event.target as HTMLInputElement).value;
|
||||
if(mode === last_value) return;
|
||||
|
||||
event_registry.fire("set-setting", { setting: "vad-type", value: mode });
|
||||
});
|
||||
|
||||
const select_vad_type = type => {
|
||||
let elements = container_select.find('input[value="' + type + '"]');
|
||||
if(elements.length < 1)
|
||||
elements = container_select.find('input[value]');
|
||||
elements.first().trigger('click');
|
||||
};
|
||||
|
||||
event_registry.on("query-settings-result", event => {
|
||||
if(event.status !== "success") return;
|
||||
|
||||
last_value = event.info.vad_type;
|
||||
select_vad_type(event.info.vad_type);
|
||||
});
|
||||
|
||||
event_registry.on("set-setting-result", event => {
|
||||
if(event.setting !== "vad-type") return;
|
||||
if(event.status !== "success") {
|
||||
createErrorModal(tr("Failed to change setting"), formatMessage(tr("Failed to change vad type{:br:}{}"), event.status === "timeout" ? tr("Timeout") : event.error || tr("Unknown error"))).open();
|
||||
} else {
|
||||
last_value = event.value;
|
||||
}
|
||||
|
||||
select_vad_type(last_value);
|
||||
});
|
||||
}
|
||||
|
||||
/* Sensitivity */
|
||||
{
|
||||
const container_sensitivity = container.find(".container-sensitivity");
|
||||
|
||||
const container_bar = container_sensitivity.find(".container-activity-bar");
|
||||
const bar_hider = container_bar.find(".bar-hider");
|
||||
|
||||
let last_value;
|
||||
let triggered_events = 0;
|
||||
let enabled;
|
||||
|
||||
const slider = sliderfy(container_bar, {
|
||||
min_value: 0,
|
||||
max_value: 100,
|
||||
step: 1,
|
||||
initial_value: 0
|
||||
});
|
||||
|
||||
const set_enabled = value => {
|
||||
if(enabled === value) return;
|
||||
|
||||
enabled = value;
|
||||
container_sensitivity.toggleClass("disabled", !value);
|
||||
};
|
||||
|
||||
container_bar.on('change', event => {
|
||||
const value = parseInt(container_bar.attr("value"));
|
||||
if(last_value === value) return;
|
||||
|
||||
triggered_events++;
|
||||
event_registry.fire("set-setting", { setting: "threshold-threshold", value: value });
|
||||
});
|
||||
|
||||
event_registry.on("query-settings", event => set_enabled(false));
|
||||
event_registry.on("query-settings-result", event => {
|
||||
if(event.status !== "success") return;
|
||||
|
||||
last_value = event.info.vad_threshold.threshold;
|
||||
slider.value(event.info.vad_threshold.threshold);
|
||||
set_enabled(event.info.vad_type === "threshold");
|
||||
});
|
||||
|
||||
event_registry.on("set-setting-result", event => {
|
||||
if(event.setting === "threshold-threshold") {
|
||||
if(event.status !== "success") return;
|
||||
|
||||
if(triggered_events > 0) {
|
||||
triggered_events--;
|
||||
return;
|
||||
}
|
||||
|
||||
last_value = event.value;
|
||||
slider.value(event.value);
|
||||
} else if(event.setting === "vad-type") {
|
||||
if(event.status !== "success") return;
|
||||
|
||||
set_enabled(event.value === "threshold");
|
||||
}
|
||||
});
|
||||
|
||||
let selected_device;
|
||||
event_registry.on("query-device-result", event => {
|
||||
if(event.status !== "success") return;
|
||||
|
||||
selected_device = event.active_device;
|
||||
});
|
||||
event_registry.on("set-device-result", event => {
|
||||
if(event.status !== "success") return;
|
||||
|
||||
selected_device = event.device_id;
|
||||
});
|
||||
|
||||
|
||||
bar_hider.css("width", "100%");
|
||||
event_registry.on("update-device-level", event => {
|
||||
if(!enabled) return;
|
||||
|
||||
const data = event.devices.find(e => e.device_id === selected_device);
|
||||
let level = data && typeof data.level === "number" ? data.level : 0;
|
||||
if(level > 100) level = 100;
|
||||
else if(level < 0) level = 0;
|
||||
|
||||
bar_hider.css("width", (100 - level) + "%");
|
||||
});
|
||||
|
||||
set_enabled(false);
|
||||
}
|
||||
|
||||
/* ppt settings */
|
||||
{
|
||||
/* PPT Key */
|
||||
{
|
||||
const button_key = container.find(".container-ppt button");
|
||||
event_registry.on("query-settings", event => button_key.prop("disabled", true).text(tr("loading")));
|
||||
let last_value;
|
||||
|
||||
event_registry.on("query-settings-result", event => {
|
||||
if(event.status !== "success") return;
|
||||
|
||||
button_key.prop('disabled', event.info.vad_type !== "push_to_talk");
|
||||
button_key.text(last_value = key_description(event.info.vad_ppt.key));
|
||||
});
|
||||
|
||||
|
||||
event_registry.on("set-setting", event => {
|
||||
if(event.setting !== "ppt-key") return;
|
||||
|
||||
button_key.prop("enabled", false);
|
||||
button_key.text(tr("applying"));
|
||||
});
|
||||
|
||||
event_registry.on("set-setting-result", event => {
|
||||
if(event.setting === "vad-type") {
|
||||
if(event.status !== "success") return;
|
||||
|
||||
button_key.prop('disabled', event.value !== "push_to_talk");
|
||||
} else if(event.setting === "ppt-key") {
|
||||
if(event.status !== "success") {
|
||||
createErrorModal(tr("Failed to change PPT key"), formatMessage(tr("Failed to change PPT key:{:br:}{}"), event.status === "timeout" ? tr("Timeout") : event.error || tr("Unknown error"))).open();
|
||||
} else {
|
||||
last_value = key_description(event.value);
|
||||
}
|
||||
button_key.text(last_value);
|
||||
}
|
||||
});
|
||||
|
||||
button_key.on('click', event => {
|
||||
spawnKeySelect(key => {
|
||||
if(!key) return;
|
||||
|
||||
event_registry.fire("set-setting", { setting: "ppt-key", value: key });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* delay */
|
||||
{
|
||||
const container_delay = container.find(".container-ppt-delay");
|
||||
|
||||
/* toggle button */
|
||||
{
|
||||
const input_enabled = container_delay.find("input.delay-enabled");
|
||||
const update_enabled_state = () => {
|
||||
const value = !loading && !applying && ppt_selected;
|
||||
input_enabled.prop("disabled", !value).parent().toggleClass("disabled", !value);
|
||||
};
|
||||
|
||||
let last_state;
|
||||
let loading = true, applying = false, ppt_selected = false;
|
||||
|
||||
event_registry.on("query-settings", event => { loading = true; update_enabled_state(); });
|
||||
event_registry.on("query-settings-result", event => {
|
||||
if(event.status !== "success") return;
|
||||
|
||||
loading = false;
|
||||
ppt_selected = event.info.vad_type === "push_to_talk";
|
||||
update_enabled_state();
|
||||
input_enabled.prop("checked", last_state = event.info.vad_ppt.release_delay_active);
|
||||
});
|
||||
|
||||
event_registry.on("set-setting", event => {
|
||||
if(event.setting !== "ppt-release-delay-active") return;
|
||||
|
||||
applying = true;
|
||||
update_enabled_state();
|
||||
});
|
||||
|
||||
event_registry.on("set-setting-result", event => {
|
||||
if(event.setting === "vad-type") {
|
||||
if(event.status !== "success") return;
|
||||
|
||||
ppt_selected = event.value === "push_to_talk";
|
||||
update_enabled_state();
|
||||
} else if(event.setting === "ppt-release-delay-active") {
|
||||
applying = false;
|
||||
update_enabled_state();
|
||||
|
||||
if(event.status !== "success") {
|
||||
createErrorModal(tr("Failed to change PPT delay state"), formatMessage(tr("Failed to change PPT delay state:{:br:}{}"), event.status === "timeout" ? tr("Timeout") : event.error || tr("Unknown error"))).open();
|
||||
} else {
|
||||
last_state = event.value;
|
||||
}
|
||||
input_enabled.prop("checked", last_state);
|
||||
}
|
||||
});
|
||||
|
||||
input_enabled.on('change', event => {
|
||||
event_registry.fire("set-setting", { setting: "ppt-release-delay-active", value: input_enabled.prop("checked") });
|
||||
});
|
||||
}
|
||||
|
||||
/* delay input */
|
||||
{
|
||||
const input_time = container_delay.find("input.delay-time");
|
||||
const update_enabled_state = () => {
|
||||
const value = !loading && !applying && ppt_selected && delay_active;
|
||||
input_time.prop("disabled", !value).parent().toggleClass("disabled", !value);
|
||||
};
|
||||
|
||||
let last_state;
|
||||
let loading = true, applying = false, ppt_selected = false, delay_active = false;
|
||||
|
||||
event_registry.on("query-settings", event => { loading = true; update_enabled_state(); });
|
||||
event_registry.on("query-settings-result", event => {
|
||||
if(event.status !== "success") return;
|
||||
|
||||
loading = false;
|
||||
ppt_selected = event.info.vad_type === "push_to_talk";
|
||||
delay_active = event.info.vad_ppt.release_delay_active;
|
||||
update_enabled_state();
|
||||
input_time.val(last_state = event.info.vad_ppt.release_delay);
|
||||
});
|
||||
|
||||
event_registry.on("set-setting", event => {
|
||||
if(event.setting !== "ppt-release-delay") return;
|
||||
|
||||
applying = true;
|
||||
update_enabled_state();
|
||||
});
|
||||
|
||||
event_registry.on("set-setting-result", event => {
|
||||
if(event.setting === "vad-type") {
|
||||
if(event.status !== "success") return;
|
||||
|
||||
ppt_selected = event.value === "push_to_talk";
|
||||
update_enabled_state();
|
||||
} else if(event.setting === "ppt-release-delay-active") {
|
||||
if(event.status !== "success") return;
|
||||
|
||||
delay_active = event.value;
|
||||
update_enabled_state();
|
||||
} else if(event.setting === "ppt-release-delay") {
|
||||
applying = false;
|
||||
update_enabled_state();
|
||||
|
||||
if(event.status !== "success") {
|
||||
createErrorModal(tr("Failed to change PPT delay"), formatMessage(tr("Failed to change PPT delay:{:br:}{}"), event.status === "timeout" ? tr("Timeout") : event.error || tr("Unknown error"))).open();
|
||||
} else {
|
||||
last_state = event.value;
|
||||
}
|
||||
input_time.val(last_state);
|
||||
}
|
||||
});
|
||||
|
||||
input_time.on('change', event => {
|
||||
event_registry.fire("set-setting", { setting: "ppt-release-delay", value: parseInt(input_time.val() as any) });
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* timeouts */
|
||||
{
|
||||
/* device query */
|
||||
{
|
||||
let timeout;
|
||||
event_registry.on('query-devices', event => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
event_registry.fire("query-device-result", { status: "timeout" });
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
event_registry.on("query-device-result", event => clearTimeout(timeout));
|
||||
}
|
||||
|
||||
/* device set */
|
||||
{
|
||||
let timeouts = {};
|
||||
event_registry.on('set-device', event => {
|
||||
clearTimeout(timeouts[event.device_id]);
|
||||
timeouts[event.device_id] = setTimeout(() => {
|
||||
event_registry.fire("set-device-result", { status: "timeout", device_id: event.device_id });
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
event_registry.on("set-device-result", event => clearTimeout(timeouts[event.device_id]));
|
||||
}
|
||||
|
||||
/* settings query */
|
||||
{
|
||||
let timeout;
|
||||
event_registry.on('query-settings', event => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
event_registry.fire("query-settings-result", { status: "timeout" });
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
event_registry.on("query-settings-result", event => clearTimeout(timeout));
|
||||
}
|
||||
|
||||
/* settings change */
|
||||
{
|
||||
let timeouts = {};
|
||||
event_registry.on('set-setting', event => {
|
||||
clearTimeout(timeouts[event.setting]);
|
||||
timeouts[event.setting] = setTimeout(() => {
|
||||
event_registry.fire("set-setting-result", { status: "timeout", setting: event.setting });
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
event_registry.on("set-setting-result", event => clearTimeout(timeouts[event.setting]));
|
||||
}
|
||||
}
|
||||
|
||||
event_registry.on("audio-initialized", () => {
|
||||
event_registry.fire("query-settings");
|
||||
event_registry.fire("query-devices", { refresh_list: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function settings_identity_forum(container: JQuery, modal: Modal, update_profiles: () => any) {
|
||||
|
|
|
@ -0,0 +1,579 @@
|
|||
@import "../../../../css/static/properties";
|
||||
@import "../../../../css/static/mixin";
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-width: 43em;
|
||||
min-height: 41em;
|
||||
|
||||
position: relative;
|
||||
|
||||
.left, .right {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
width: calc(50% - .5em); /* the .5em for the padding/margin */
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.left {
|
||||
margin-right: 1em;
|
||||
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
|
||||
.body {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-height: 6.5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
border: 1px $color_list_border solid;
|
||||
border-radius: $border_radius_large;
|
||||
|
||||
background-color: $color_list_background;
|
||||
|
||||
.buttons {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
height: 3.5em;
|
||||
padding: .5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
border: none;
|
||||
border-top: 1px $color_list_border solid;
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
:not(.spacer) {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
min-width: 8em;
|
||||
height: 2.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
padding-right: .5em; /* for the sliders etc*/
|
||||
justify-content: flex-start;
|
||||
|
||||
.body {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
/* microphone */
|
||||
.containerVolume {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
height: 3em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* microphone */
|
||||
.containerSelectVad {
|
||||
margin-top: .5em;
|
||||
width: 100%;
|
||||
|
||||
.fieldset {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
> .containerOption {
|
||||
padding: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
> label {
|
||||
flex-shrink: 0;
|
||||
min-width: 5em;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
height: 1.7em;
|
||||
|
||||
a {
|
||||
align-self: center;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
|
||||
height: 2em;
|
||||
font-size: .75em;
|
||||
|
||||
align-self: center;
|
||||
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.containerButton {
|
||||
flex-shrink: 1;
|
||||
margin-left: .5em;
|
||||
|
||||
min-width: 3em;
|
||||
width: 15em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* microphone */
|
||||
.containerSensitivity {
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
position: relative;
|
||||
|
||||
.slider {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
|
||||
.filler {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.containerBar {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.containerActivityBar {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* microphone */
|
||||
.containerAdvanced {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
.containerPptDelay {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
.input {
|
||||
width: 6em;
|
||||
font-size: .7em;
|
||||
|
||||
input {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* speaker */
|
||||
.containerVolumeMaster {
|
||||
.filler {
|
||||
background-color: #2b8541;
|
||||
}
|
||||
}
|
||||
|
||||
.containerVolumeSoundpack {
|
||||
padding-top: .75em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 2.6em;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
padding-bottom: .5em;
|
||||
|
||||
a {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
align-self: flex-end;
|
||||
|
||||
font-weight: bold;
|
||||
color: #e0e0e0;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
height: 2em;
|
||||
align-self: flex-end;
|
||||
|
||||
margin-left: 1em;
|
||||
min-width: 8em;
|
||||
}
|
||||
}
|
||||
|
||||
.containerDevices {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-height: 3em;
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
@include chat-scrollbar-vertical();
|
||||
|
||||
.device {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
height: 3em;
|
||||
width: 100%;
|
||||
|
||||
.containerSelected {
|
||||
/* the selected border */
|
||||
margin-top: 1px;
|
||||
margin-bottom: 1px;
|
||||
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
width: 3em;
|
||||
position: relative;
|
||||
|
||||
border: none;
|
||||
border-right: 1px solid #242527;
|
||||
|
||||
> * {
|
||||
padding: .5em;
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
> :global(.icon_em) {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
> .iconLoading {
|
||||
img {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
-webkit-animation:spin 4s linear infinite;
|
||||
-moz-animation:spin 4s linear infinite;
|
||||
animation:spin 4s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.containerName {
|
||||
/* the selected border */
|
||||
margin-top: 1px;
|
||||
margin-bottom: 1px;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-width: 4em;
|
||||
|
||||
padding: .5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
border: none;
|
||||
|
||||
.driver {
|
||||
font-size: .8em;
|
||||
line-height: 1em;
|
||||
|
||||
color: #6a6a6a;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.name {
|
||||
line-height: 1em;
|
||||
|
||||
color: #999999;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.containerActivity {
|
||||
/* the selected border */
|
||||
margin-top: 1px;
|
||||
margin-bottom: 1px;
|
||||
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
padding: .5em;
|
||||
|
||||
width: 10em;
|
||||
|
||||
border: none;
|
||||
border-left: 1px solid #242527;
|
||||
|
||||
.bar {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
width: 8em;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color_list_hover;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
> div {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
border-bottom: 1px solid #242527;
|
||||
border-top: 1px solid #242527;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
background-color: #28292b;
|
||||
|
||||
a {
|
||||
font-size: 1.3em;
|
||||
align-self: center;
|
||||
color: #9e9494;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.containerActivityBar {
|
||||
$bar_height: 1em;
|
||||
|
||||
$thumb_width: .6em;
|
||||
$thumb_height: 2em;
|
||||
|
||||
position: relative;
|
||||
align-self: center;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
height: $bar_height;
|
||||
border-radius: $border_radius_large;
|
||||
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
.hider {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
background-color: #242527;
|
||||
|
||||
-webkit-box-shadow: inset 0 0 2px 0 rgba(0,0,0,0.75);
|
||||
-moz-box-shadow: inset 0 0 2px 0 rgba(0,0,0,0.75);
|
||||
box-shadow: inset 0 0 2px 0 rgba(0,0,0,0.75);
|
||||
|
||||
border-bottom-right-radius: $border_radius_large;
|
||||
border-top-right-radius: $border_radius_large;
|
||||
|
||||
@include transition(.06s ease-in-out);
|
||||
}
|
||||
|
||||
&[value] {
|
||||
overflow: visible; /* for the thumb */
|
||||
|
||||
border-bottom-left-radius: $border_radius_large;
|
||||
border-top-left-radius: $border_radius_large;
|
||||
}
|
||||
|
||||
.text {
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
line-height: 1em;
|
||||
font-size: .8em;
|
||||
|
||||
padding-left: .2em;
|
||||
padding-right: .2em;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&.error {
|
||||
color: #a10000;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
height: $thumb_height;
|
||||
width: $thumb_width;
|
||||
|
||||
margin-left: -($thumb_width / 2);
|
||||
margin-right: -($thumb_width / 2);
|
||||
|
||||
margin-top: -($thumb_height - $bar_height) / 2;
|
||||
margin-bottom: -($thumb_height - $bar_height) / 2;
|
||||
|
||||
background-color: #808080;
|
||||
|
||||
.tooltip {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
|
||||
.hider {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
background-color: #4d4d4d;
|
||||
|
||||
.tooltip {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-webkit-box-shadow: inset 0 0 2px 0 rgba(0,0,0,0.25);
|
||||
-moz-box-shadow: inset 0 0 2px 0 rgba(0,0,0,0.25);
|
||||
box-shadow: inset 0 0 2px 0 rgba(0,0,0,0.25);
|
||||
|
||||
/* Permalink - use to edit and share this gradient: https://colorzilla.com/gradient-editor/#70407e+0,45407e+100 */
|
||||
background: rgb(112,64,126); /* Old browsers */
|
||||
background: -moz-linear-gradient(left, rgba(112,64,126,1) 0%, rgba(69,64,126,1) 100%); /* FF3.6-15 */
|
||||
background: -webkit-linear-gradient(left, rgba(112,64,126,1) 0%,rgba(69,64,126,1) 100%); /* Chrome10-25,Safari5.1-6 */
|
||||
background: linear-gradient(to right, rgba(112,64,126,1) 0%,rgba(69,64,126,1) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#70407e', endColorstr='#45407e',GradientType=1 ); /* IE6-9 */
|
||||
}
|
||||
|
||||
@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
|
||||
@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
|
||||
@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }
|
|
@ -0,0 +1,271 @@
|
|||
import * as aplayer from "tc-backend/audio/player";
|
||||
import * as React from "react";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {LevelMeter} from "tc-shared/voice/RecorderBase";
|
||||
import * as arecorder from "tc-backend/audio/recorder";
|
||||
import * as log from "tc-shared/log";
|
||||
import {LogCategory, logWarn} from "tc-shared/log";
|
||||
import {default_recorder} from "tc-shared/voice/RecorderProfile";
|
||||
import * as loader from "tc-loader";
|
||||
import {Stage} from "tc-loader";
|
||||
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {MicrophoneSettings} from "tc-shared/ui/modal/settings/MicrophoneRenderer";
|
||||
|
||||
export type MicrophoneSetting = "volume" | "vad-type" | "ppt-key" | "ppt-release-delay" | "ppt-release-delay-active" | "threshold-threshold";
|
||||
|
||||
export type MicrophoneDevice = {
|
||||
id: string,
|
||||
name: string,
|
||||
driver: string
|
||||
};
|
||||
|
||||
|
||||
export interface MicrophoneSettingsEvents {
|
||||
"query_devices": { refresh_list: boolean },
|
||||
|
||||
"query_setting": {
|
||||
setting: MicrophoneSetting
|
||||
},
|
||||
|
||||
"action_set_selected_device": { deviceId: string },
|
||||
"action_set_selected_device_result": {
|
||||
deviceId: string, /* on error it will contain the current selected device */
|
||||
status: "success" | "error",
|
||||
|
||||
error?: string
|
||||
},
|
||||
|
||||
"action_set_setting": {
|
||||
setting: MicrophoneSetting;
|
||||
value: any;
|
||||
},
|
||||
|
||||
notify_setting: {
|
||||
setting: MicrophoneSetting;
|
||||
value: any;
|
||||
}
|
||||
|
||||
"notify_devices": {
|
||||
status: "success" | "error" | "audio-not-initialized",
|
||||
|
||||
error?: string,
|
||||
devices?: MicrophoneDevice[]
|
||||
selectedDevice?: string;
|
||||
},
|
||||
|
||||
notify_device_level: {
|
||||
level: {[key: string]: {
|
||||
deviceId: string,
|
||||
status: "success" | "error",
|
||||
|
||||
level?: number,
|
||||
error?: string
|
||||
}}
|
||||
},
|
||||
|
||||
notify_destroy: {}
|
||||
}
|
||||
|
||||
export function initialize_audio_microphone_controller(events: Registry<MicrophoneSettingsEvents>) {
|
||||
/* level meters */
|
||||
{
|
||||
const level_meters: {[key: string]:Promise<LevelMeter>} = {};
|
||||
const level_info: {[key: string]:any} = {};
|
||||
let level_update_task;
|
||||
|
||||
const destroy_meters = () => {
|
||||
Object.keys(level_meters).forEach(e => {
|
||||
const meter = level_meters[e];
|
||||
delete level_meters[e];
|
||||
|
||||
meter.then(e => e.destory());
|
||||
});
|
||||
Object.keys(level_info).forEach(e => delete level_info[e]);
|
||||
};
|
||||
|
||||
const update_level_meter = () => {
|
||||
destroy_meters();
|
||||
|
||||
for(const device of arecorder.devices()) {
|
||||
let promise = arecorder.create_levelmeter(device).then(meter => {
|
||||
meter.set_observer(level => {
|
||||
if(level_meters[device.unique_id] !== promise) return; /* old level meter */
|
||||
|
||||
level_info[device.unique_id] = {
|
||||
deviceId: device.unique_id,
|
||||
status: "success",
|
||||
level: level
|
||||
};
|
||||
});
|
||||
return Promise.resolve(meter);
|
||||
}).catch(error => {
|
||||
if(level_meters[device.unique_id] !== promise) return; /* old level meter */
|
||||
level_info[device.unique_id] = {
|
||||
deviceId: device.unique_id,
|
||||
status: "error",
|
||||
|
||||
error: error
|
||||
};
|
||||
|
||||
log.warn(LogCategory.AUDIO, tr("Failed to initialize a level meter for device %s (%s): %o"), device.unique_id, device.driver + ":" + device.name, error);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
level_meters[device.unique_id] = promise;
|
||||
}
|
||||
};
|
||||
|
||||
level_update_task = setInterval(() => {
|
||||
events.fire("notify_device_level", {
|
||||
level: level_info
|
||||
});
|
||||
}, 50);
|
||||
|
||||
events.on("notify_devices", event => {
|
||||
if(event.status !== "success") return;
|
||||
|
||||
update_level_meter();
|
||||
});
|
||||
|
||||
events.on("notify_destroy", event => {
|
||||
destroy_meters();
|
||||
clearInterval(level_update_task);
|
||||
});
|
||||
}
|
||||
|
||||
/* device list */
|
||||
{
|
||||
events.on("query_devices", event => {
|
||||
if(!aplayer.initialized()) {
|
||||
events.fire_async("notify_devices", { status: "audio-not-initialized" });
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
return arecorder.device_refresh_available() && event.refresh_list ? arecorder.refresh_devices() : Promise.resolve();
|
||||
}).catch(error => {
|
||||
log.warn(LogCategory.AUDIO, tr("Failed to refresh device list: %o"), error);
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
const devices = arecorder.devices();
|
||||
|
||||
events.fire_async("notify_devices", {
|
||||
status: "success",
|
||||
selectedDevice: default_recorder.current_device() ? default_recorder.current_device().unique_id : "none",
|
||||
devices: devices.map(e => { return { id: e.unique_id, name: e.name, driver: e.driver }})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
events.on("action_set_selected_device", event => {
|
||||
const device = arecorder.devices().find(e => e.unique_id === event.deviceId);
|
||||
if(!device && event.deviceId !== "none") {
|
||||
events.fire_async("action_set_selected_device_result", { status: "error", error: tr("Invalid device id"), deviceId: default_recorder.current_device().unique_id });
|
||||
return;
|
||||
}
|
||||
|
||||
default_recorder.set_device(device).then(() => {
|
||||
console.debug(tr("Changed default microphone device"));
|
||||
events.fire_async("action_set_selected_device_result", { status: "success", deviceId: event.deviceId });
|
||||
}).catch((error) => {
|
||||
log.warn(LogCategory.AUDIO, tr("Failed to change microphone to device %s: %o"), device ? device.unique_id : "none", error);
|
||||
events.fire_async("action_set_selected_device_result", { status: "success", deviceId: event.deviceId });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* settings */
|
||||
{
|
||||
events.on("query_setting", event => {
|
||||
let value;
|
||||
switch (event.setting) {
|
||||
case "volume":
|
||||
value = default_recorder.get_volume();
|
||||
break;
|
||||
|
||||
case "threshold-threshold":
|
||||
value = default_recorder.get_vad_threshold();
|
||||
break;
|
||||
|
||||
case "vad-type":
|
||||
value = default_recorder.get_vad_type();
|
||||
break;
|
||||
|
||||
case "ppt-key":
|
||||
value = default_recorder.get_vad_ppt_key();
|
||||
break;
|
||||
|
||||
case "ppt-release-delay":
|
||||
value = Math.abs(default_recorder.get_vad_ppt_delay());
|
||||
break;
|
||||
|
||||
case "ppt-release-delay-active":
|
||||
value = default_recorder.get_vad_ppt_delay() > 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
events.fire_async("notify_setting", { setting: event.setting, value: value });
|
||||
});
|
||||
|
||||
events.on("action_set_setting", event => {
|
||||
const ensure_type = (type: "object" | "string" | "boolean" | "number" | "undefined") => {
|
||||
if(typeof event.value !== type) {
|
||||
logWarn(LogCategory.GENERAL, tr("Failed to change microphone setting (Invalid value type supplied. Expected %s, Received: %s)"),
|
||||
type,
|
||||
typeof event.value
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
switch (event.setting) {
|
||||
case "volume":
|
||||
if(!ensure_type("number")) return;
|
||||
default_recorder.set_volume(event.value);
|
||||
break;
|
||||
|
||||
case "threshold-threshold":
|
||||
if(!ensure_type("number")) return;
|
||||
default_recorder.set_vad_threshold(event.value);
|
||||
break;
|
||||
|
||||
case "vad-type":
|
||||
if(!ensure_type("string")) return;
|
||||
if(!default_recorder.set_vad_type(event.value)) {
|
||||
logWarn(LogCategory.GENERAL, tr("Failed to change recorders VAD type to %s"), event.value);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case "ppt-key":
|
||||
if(!ensure_type("object")) return;
|
||||
default_recorder.set_vad_ppt_key(event.value);
|
||||
break;
|
||||
|
||||
case "ppt-release-delay":
|
||||
if(!ensure_type("number")) return;
|
||||
const sign = default_recorder.get_vad_ppt_delay() >= 0 ? 1 : -1;
|
||||
default_recorder.set_vad_ppt_delay(sign * event.value);
|
||||
break;
|
||||
|
||||
case "ppt-release-delay-active":
|
||||
if(!ensure_type("boolean")) return;
|
||||
default_recorder.set_vad_ppt_delay(Math.abs(default_recorder.get_vad_ppt_delay()) * (event.value ? 1 : -1));
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
events.fire_async("notify_setting", { setting: event.setting, value: event.value });
|
||||
});
|
||||
}
|
||||
|
||||
if(!aplayer.initialized()) {
|
||||
aplayer.on_ready(() => { events.fire_async("query_devices"); });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,458 @@
|
|||
import * as React from "react";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||
import {modal, Registry} from "tc-shared/events";
|
||||
import {MicrophoneDevice, MicrophoneSettingsEvents} from "tc-shared/ui/modal/settings/Microphone";
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
||||
import {ClientIcon} from "svg-sprites/client-icons";
|
||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||
import {Slider} from "tc-shared/ui/react-elements/Slider";
|
||||
import MicrophoneSettings = modal.settings.MicrophoneSettings;
|
||||
import {RadioButton} from "tc-shared/ui/react-elements/RadioButton";
|
||||
import {VadType} from "tc-shared/voice/RecorderProfile";
|
||||
import {key_description, KeyDescriptor} from "tc-shared/PPTListener";
|
||||
import {spawnKeySelect} from "tc-shared/ui/modal/ModalKeySelect";
|
||||
import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
|
||||
import {BoxedInputField} from "tc-shared/ui/react-elements/InputField";
|
||||
|
||||
const cssStyle = require("./Microphone.scss");
|
||||
|
||||
type MicrophoneSelectedState = "selected" | "applying" | "unselected";
|
||||
const MicrophoneStatus = (props: { state: MicrophoneSelectedState }) => {
|
||||
switch (props.state) {
|
||||
case "applying":
|
||||
return (
|
||||
<div key={"applying"} className={cssStyle.iconLoading}>
|
||||
<img draggable={false} src="img/icon_settings_loading.svg" alt={tr("applying")} />
|
||||
</div>
|
||||
);
|
||||
|
||||
case "unselected":
|
||||
return null;
|
||||
|
||||
case "selected":
|
||||
return <ClientIconRenderer key={"selected"} icon={ClientIcon.Apply} />;
|
||||
}
|
||||
}
|
||||
|
||||
type ActivityBarStatus = { mode: "success" } | { mode: "error", message: string } | { mode: "loading" };
|
||||
const ActivityBar = (props: { events: Registry<MicrophoneSettingsEvents>, deviceId: string, disabled?: boolean }) => {
|
||||
const refHider = useRef<HTMLDivElement>();
|
||||
const [ status, setStatus ] = useState<ActivityBarStatus>({ mode: "loading" });
|
||||
|
||||
props.events.reactUse("notify_device_level", event => {
|
||||
const device = event.level[props.deviceId];
|
||||
if(!device) {
|
||||
if(status.mode === "loading")
|
||||
return;
|
||||
|
||||
setStatus({ mode: "loading" });
|
||||
} else if(device.status === "success") {
|
||||
if(status.mode !== "success") {
|
||||
setStatus({ mode: "success" });
|
||||
}
|
||||
refHider.current.style.width = (100 - device.level) + "%";
|
||||
} else {
|
||||
if(status.mode === "error" && status.message === device.error)
|
||||
return;
|
||||
|
||||
setStatus({ mode: "error", message: device.error });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
let error;
|
||||
switch (status.mode) {
|
||||
case "error":
|
||||
error = <div className={cssStyle.text + " " + cssStyle.error} key={"error"}>{status.message}</div>;
|
||||
break;
|
||||
|
||||
case "loading":
|
||||
error = <div className={cssStyle.text} key={"loading"}><Translatable>Loading</Translatable> <LoadingDots /></div>;
|
||||
break;
|
||||
|
||||
case "success":
|
||||
error = undefined;
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<div className={cssStyle.containerActivityBar + " " + cssStyle.bar + " " + (props.disabled ? cssStyle.disabled : "")}>
|
||||
<div ref={refHider} className={cssStyle.hider} style={{ width: "100%" }} />
|
||||
{error}
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
const Microphone = (props: { events: Registry<MicrophoneSettingsEvents>, device: MicrophoneDevice, state: MicrophoneSelectedState, onClick: () => void }) => {
|
||||
return (
|
||||
<div className={cssStyle.device + " " + (props.state === "unselected" ? "" : cssStyle.selected)} onClick={props.onClick}>
|
||||
<div className={cssStyle.containerSelected}>
|
||||
<MicrophoneStatus state={props.state} />
|
||||
</div>
|
||||
<div className={cssStyle.containerName}>
|
||||
<div className={cssStyle.driver}>{props.device.driver}</div>
|
||||
<div className={cssStyle.name}>{props.device.name}</div>
|
||||
</div>
|
||||
<div className={cssStyle.containerActivity}>
|
||||
<ActivityBar events={props.events} deviceId={props.device.id} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MicrophoneList = (props: { events: Registry<MicrophoneSettingsEvents> }) => {
|
||||
const [ state, setState ] = useState<"normal" | "loading" | "error" | "audio-not-initialized">(() => {
|
||||
props.events.fire("query_devices");
|
||||
return "loading";
|
||||
});
|
||||
const [ selectedDevice, setSelectedDevice ] = useState<{ deviceId: string, mode: "selected" | "selecting" }>();
|
||||
const [ deviceList, setDeviceList ] = useState<MicrophoneDevice[]>([]);
|
||||
const [ error, setError ] = useState(undefined);
|
||||
|
||||
props.events.reactUse("notify_devices", event => {
|
||||
setSelectedDevice(undefined);
|
||||
switch (event.status) {
|
||||
case "success":
|
||||
setDeviceList(event.devices.slice(0));
|
||||
setState("normal");
|
||||
setSelectedDevice({ mode: "selected", deviceId: event.selectedDevice });
|
||||
break;
|
||||
|
||||
case "error":
|
||||
setError(event.error || tr("Unknown error"));
|
||||
setState("error");
|
||||
break;
|
||||
|
||||
case "audio-not-initialized":
|
||||
setState("audio-not-initialized");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
props.events.reactUse("action_set_selected_device", event => {
|
||||
setSelectedDevice({ mode: "selecting", deviceId: event.deviceId });
|
||||
});
|
||||
|
||||
props.events.reactUse("action_set_selected_device_result", event => {
|
||||
if(event.status === "error")
|
||||
createErrorModal(tr("Failed to select microphone"), tra("Failed to select microphone:\n{}", event.error)).open();
|
||||
|
||||
setSelectedDevice({ mode: "selected", deviceId: event.deviceId });
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cssStyle.body + " " + cssStyle.containerDevices}>
|
||||
<div className={cssStyle.overlay + " " + (state !== "audio-not-initialized" ? cssStyle.hidden : undefined)}>
|
||||
<a>
|
||||
<Translatable>The web audio play hasn't been initialized yet.</Translatable>
|
||||
<Translatable>Click somewhere on the base to initialize it.</Translatable>
|
||||
</a>
|
||||
</div>
|
||||
<div className={cssStyle.overlay + " " + (state !== "error" ? cssStyle.hidden : undefined)}>
|
||||
<a>{error}</a>
|
||||
</div>
|
||||
<div className={cssStyle.overlay + " " + (state !== "loading" ? cssStyle.hidden : undefined)}>
|
||||
<a><Translatable>Loading</Translatable> <LoadingDots/></a>
|
||||
</div>
|
||||
{deviceList.map(e => <Microphone
|
||||
key={"d-" + e.id}
|
||||
device={e}
|
||||
events={props.events}
|
||||
state={e.id === selectedDevice?.deviceId ? selectedDevice.mode === "selecting" ? "applying" : "selected" : "unselected"}
|
||||
onClick={() => {
|
||||
if(state !== "normal" || selectedDevice?.mode === "selecting")
|
||||
return;
|
||||
|
||||
props.events.fire("action_set_selected_device", { deviceId: e.id });
|
||||
}}
|
||||
/>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ListRefreshButton = (props: { events: Registry<MicrophoneSettingsEvents> }) => {
|
||||
const [ updateTimeout, setUpdateTimeout ] = useState(Date.now() + 2000);
|
||||
|
||||
useEffect(() => {
|
||||
if(updateTimeout === 0)
|
||||
return;
|
||||
|
||||
const id = setTimeout(() => {
|
||||
setUpdateTimeout(0);
|
||||
}, Math.max(updateTimeout - Date.now(), 0));
|
||||
return () => clearTimeout(id);
|
||||
});
|
||||
|
||||
props.events.reactUse("query_devices", () => setUpdateTimeout(Date.now() + 2000));
|
||||
|
||||
return <Button disabled={updateTimeout > 0} type={"small"} color={"blue"} onClick={() => props.events.fire("query_devices", { refresh_list: true })}>
|
||||
<Translatable>Update</Translatable>
|
||||
</Button>;
|
||||
}
|
||||
|
||||
const VolumeSettings = (props: { events: Registry<MicrophoneSettingsEvents> }) => {
|
||||
const refSlider = useRef<Slider>();
|
||||
const [ value, setValue ] = useState<"loading" | number>(() => {
|
||||
props.events.fire("query_setting", { setting: "volume" });
|
||||
return "loading";
|
||||
})
|
||||
|
||||
props.events.reactUse("notify_setting", event => {
|
||||
if(event.setting !== "volume")
|
||||
return;
|
||||
|
||||
console.error("Set value: %o", event.value);
|
||||
refSlider.current?.setState({ value: event.value });
|
||||
setValue(event.value);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cssStyle.containerVolume}>
|
||||
<a><Translatable>Volume</Translatable></a>
|
||||
<Slider
|
||||
ref={refSlider}
|
||||
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
stepSize={1}
|
||||
value={value === "loading" ? 0 : value}
|
||||
unit={"%"}
|
||||
disabled={value === "loading"}
|
||||
|
||||
onChange={value => props.events.fire("action_set_setting", { setting: "volume", value: value })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
const PPTKeyButton = React.memo((props: { events: Registry<MicrophoneSettingsEvents> }) => {
|
||||
const [ key, setKey ] = useState<"loading" | KeyDescriptor>(() => {
|
||||
props.events.fire("query_setting", { setting: "ppt-key" });
|
||||
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-key")
|
||||
setKey(event.value);
|
||||
});
|
||||
|
||||
if(key === "loading") {
|
||||
return <Button color={"none"} disabled={true} key={"loading"}><Translatable>loading</Translatable> <LoadingDots /></Button>;
|
||||
} else {
|
||||
return <Button
|
||||
color={"none"}
|
||||
key={"key"}
|
||||
disabled={!isActive}
|
||||
onClick={() => {
|
||||
spawnKeySelect(key => {
|
||||
if(!key) return;
|
||||
|
||||
props.events.fire("action_set_setting", { setting: "ppt-key", value: key });
|
||||
});
|
||||
}}
|
||||
>{key_description(key)}</Button>;
|
||||
}
|
||||
});
|
||||
|
||||
const PPTDelaySettings = (props: { events: Registry<MicrophoneSettingsEvents> }) => {
|
||||
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 (
|
||||
<div className={cssStyle.containerPptDelay}>
|
||||
<Checkbox
|
||||
onChange={value => { props.events.fire("action_set_setting", { setting: "ppt-release-delay-active", value: value })}}
|
||||
disabled={!isActive}
|
||||
value={delayActive === true}
|
||||
label={<Translatable>Delay on Push to Talk</Translatable>}
|
||||
/>
|
||||
|
||||
<BoxedInputField
|
||||
className={cssStyle.input}
|
||||
disabled={!isActive || delayActive === "loading" || !delayActive}
|
||||
suffix={"ms"}
|
||||
inputBox={() => <input
|
||||
type="number"
|
||||
min={0}
|
||||
max={4000}
|
||||
step={1}
|
||||
value={delay}
|
||||
disabled={!isActive || delayActive === "loading" || !delayActive}
|
||||
onChange={event => {
|
||||
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 });
|
||||
}}
|
||||
/>}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const VadSelector = (props: { events: Registry<MicrophoneSettingsEvents> }) => {
|
||||
const [ selectedType, setSelectedType ] = useState<VadType | "loading">(() => {
|
||||
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 (
|
||||
<div className={cssStyle.containerSelectVad}>
|
||||
<div className={cssStyle.fieldset}>
|
||||
<div className={cssStyle.containerOption}>
|
||||
<RadioButton
|
||||
name={"vad-type"}
|
||||
onChange={() => { props.events.fire("action_set_setting", { setting: "vad-type", value: "push_to_talk" }) }}
|
||||
selected={selectedType === "push_to_talk"}
|
||||
disabled={selectedType === "loading"}
|
||||
>
|
||||
<a><Translatable>Push to Talk</Translatable></a>
|
||||
</RadioButton>
|
||||
<div className={cssStyle.containerButton}>
|
||||
<PPTKeyButton events={props.events} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={cssStyle.containerOption}>
|
||||
<RadioButton
|
||||
name={"vad-type"}
|
||||
onChange={() => { props.events.fire("action_set_setting", { setting: "vad-type", value: "threshold" }) }}
|
||||
selected={selectedType === "threshold"}
|
||||
disabled={selectedType === "loading"}
|
||||
>
|
||||
<a><Translatable>Voice activity detection</Translatable></a>
|
||||
</RadioButton>
|
||||
</div>
|
||||
<div className={cssStyle.containerOption}>
|
||||
<RadioButton
|
||||
name={"vad-type"}
|
||||
onChange={() => { props.events.fire("action_set_setting", { setting: "vad-type", value: "active" }) }}
|
||||
selected={selectedType === "active"}
|
||||
disabled={selectedType === "loading"}
|
||||
>
|
||||
<a><Translatable>Always active</Translatable></a>
|
||||
</RadioButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ThresholdSelector = (props: { events: Registry<MicrophoneSettingsEvents> }) => {
|
||||
const refSlider = useRef<Slider>();
|
||||
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 (
|
||||
<div className={cssStyle.containerSensitivity}>
|
||||
<div className={cssStyle.containerBar}>
|
||||
<ActivityBar events={props.events} deviceId={"default"} disabled={!isActive} />
|
||||
</div>
|
||||
<Slider
|
||||
ref={refSlider}
|
||||
|
||||
className={cssStyle.slider}
|
||||
classNameFiller={cssStyle.filler}
|
||||
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
stepSize={1}
|
||||
value={value === "loading" ? 0 : value}
|
||||
unit={"%"}
|
||||
|
||||
disabled={value === "loading" || !isActive}
|
||||
|
||||
onChange={value => {}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const MicrophoneSettings = (props: { events: Registry<MicrophoneSettingsEvents> }) => (
|
||||
<div className={cssStyle.container}>
|
||||
<div className={cssStyle.left}>
|
||||
<div className={cssStyle.header}>
|
||||
<a><Translatable>Select your Microphone Device</Translatable></a>
|
||||
<ListRefreshButton events={props.events} />
|
||||
</div>
|
||||
<MicrophoneList events={props.events} />
|
||||
</div>
|
||||
<div className={cssStyle.right}>
|
||||
<div className={cssStyle.header}>
|
||||
<a><Translatable>Microphone Settings</Translatable></a>
|
||||
</div>
|
||||
<div className={cssStyle.body}>
|
||||
<VolumeSettings events={props.events} />
|
||||
<VadSelector events={props.events} />
|
||||
</div>
|
||||
<div className={cssStyle.header}>
|
||||
<a><Translatable>Sensitivity Settings</Translatable></a>
|
||||
</div>
|
||||
<div className={cssStyle.body}>
|
||||
<ThresholdSelector events={props.events} />
|
||||
</div>
|
||||
<div className={cssStyle.header}>
|
||||
<a><Translatable>Advanced Settings</Translatable></a>
|
||||
</div>
|
||||
<div className={cssStyle.body}>
|
||||
<div className={cssStyle.containerAdvanced}>
|
||||
<PPTDelaySettings events={props.events} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
|
@ -77,6 +77,10 @@ html:root {
|
|||
border-bottom-color: var(--button-yellow);
|
||||
}
|
||||
|
||||
&.color-none {
|
||||
--keep-alive: true;
|
||||
}
|
||||
|
||||
&.type-normal { }
|
||||
|
||||
&.type-small {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<BoxedInputFieldProperties,
|
|||
onInput={this.props.onInput && (event => this.props.onInput(event.currentTarget.value))}
|
||||
onKeyDown={e => this.onKeyDown(e)}
|
||||
/>}
|
||||
{this.props.suffix ? <a key={"suffix"} className={cssStyle.suffix}>{this.props.suffix}</a> : undefined}
|
||||
{this.props.rightIcon ? this.props.rightIcon() : ""}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
<label>
|
||||
<div className={cssStyle.container + " " + (props.disabled ? cssStyle.disabled : "")}>
|
||||
<input
|
||||
disabled={props.disabled}
|
||||
type={"radio"}
|
||||
name={props.name}
|
||||
onChange={event => props.onChange(event.target.checked)}
|
||||
checked={props.selected} />
|
||||
<div className={cssStyle.mark} />
|
||||
</div>
|
||||
{props.children}
|
||||
</label>
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<SliderProperties, SliderState> {
|
|||
private readonly mouseListener;
|
||||
private readonly mouseUpListener;
|
||||
|
||||
private readonly refTooltip = React.createRef<Tooltip>();
|
||||
private readonly refSlider = React.createRef<HTMLDivElement>();
|
||||
protected readonly refTooltip = React.createRef<Tooltip>();
|
||||
protected readonly refSlider = React.createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -87,6 +90,7 @@ export class Slider extends React.Component<SliderProperties, SliderState> {
|
|||
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<SliderProperties, SliderState> {
|
|||
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<SliderProperties, SliderState> {
|
|||
onMouseDown={e => this.enableSliderMode(e)}
|
||||
onTouchStart={e => this.enableSliderMode(e)}
|
||||
>
|
||||
<div className={cssStyle.filler} style={{right: (100 - offset) + "%"}} />
|
||||
<div className={cssStyle.filler + " " + (this.props.classNameFiller || "")} style={{
|
||||
right: this.props.inverseFiller ? 0 : (100 - offset) + "%",
|
||||
left: this.props.inverseFiller ? offset + "%" : 0
|
||||
}} />
|
||||
<Tooltip ref={this.refTooltip} tooltip={() => this.props.tooltip ? this.props.tooltip(this.state.value) : this.renderTooltip()}>
|
||||
<div className={cssStyle.thumb} style={{left: offset + "%"}} />
|
||||
</Tooltip>
|
||||
|
@ -132,14 +140,14 @@ export class Slider extends React.Component<SliderProperties, SliderState> {
|
|||
);
|
||||
}
|
||||
|
||||
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 <a>{this.state.value + (this.props.unit || "")}</a>;
|
||||
}
|
||||
|
||||
|
|
|
@ -94,4 +94,4 @@ export class InternalModalController<InstanceType extends InternalModal = Intern
|
|||
}
|
||||
}
|
||||
|
||||
export abstract class InternalModal extends AbstractModal { }
|
||||
export abstract class InternalModal extends AbstractModal {}
|
|
@ -54,27 +54,27 @@ export class RecorderProfile {
|
|||
|
||||
record_supported: boolean;
|
||||
|
||||
private _ppt_hook: KeyHook;
|
||||
private _ppt_timeout: number;
|
||||
private _ppt_hook_registered: boolean;
|
||||
private pptHook: KeyHook;
|
||||
private pptTimeout: number;
|
||||
private pptHookRegistered: boolean;
|
||||
|
||||
constructor(name: string, volatile?: boolean) {
|
||||
this.name = name;
|
||||
this.volatile = typeof(volatile) === "boolean" ? volatile : false;
|
||||
|
||||
this._ppt_hook = {
|
||||
this.pptHook = {
|
||||
callback_release: () => {
|
||||
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<void> {
|
||||
this.config.device_id = device ? device.unique_id : undefined;
|
||||
this.save();
|
||||
|
|
Loading…
Reference in New Issue