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 {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"; import {IDevice} from "tc-shared/audio/recorder"; const cssStyle = require("./Microphone.scss"); type MicrophoneSelectedState = "selected" | "applying" | "unselected"; const MicrophoneStatus = (props: { state: MicrophoneSelectedState }) => { switch (props.state) { case "applying": return (
{tr("applying")}
); case "unselected": return null; case "selected": return ; } } type ActivityBarStatus = { mode: "success" } | { mode: "error", message: string } | { mode: "loading" } | { mode: "uninitialized" }; const ActivityBar = (props: { events: Registry, deviceId: string, disabled?: boolean }) => { const refHider = useRef(); const [ status, setStatus ] = useState({ mode: "loading" }); props.events.reactUse("notify_device_level", event => { if(event.status === "uninitialized") { if(status.mode === "uninitialized") return; setStatus({ mode: "uninitialized" }); } else if(event.status === "no-permissions") { const noPermissionsMessage = tr("no permissions"); if(status.mode === "error" && status.message === noPermissionsMessage) return; setStatus({ mode: "error", message: noPermissionsMessage }); } else { 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 =
{status.message}
; break; case "loading": error =
Loading 
; break; case "success": error = undefined; break; } return (
{error}
) }; const Microphone = (props: { events: Registry, device: MicrophoneDevice, state: MicrophoneSelectedState, onClick: () => void }) => { return (
{props.device.driver}
{props.device.name}
{props.device.id === IDevice.NoDeviceId ? undefined : }
); }; type MicrophoneListState = { type: "normal" | "loading" | "audio-not-initialized" } | { type: "error", message: string } | { type: "no-permissions", bySystem: boolean } const PermissionDeniedOverlay = (props: { bySystem: boolean, shown: boolean, onRequestPermission: () => void }) => { if(props.bySystem) { return ( ); } else { return ( ); } } const MicrophoneList = (props: { events: Registry }) => { const [ state, setState ] = useState(() => { props.events.fire("query_devices"); return { type: "loading" }; }); const [ selectedDevice, setSelectedDevice ] = useState<{ deviceId: string, mode: "selected" | "selecting" }>(); const [ deviceList, setDeviceList ] = useState([]); const [ error, setError ] = useState(undefined); props.events.reactUse("notify_devices", event => { setSelectedDevice(undefined); switch (event.status) { case "success": setDeviceList(event.devices.slice(0)); setState({ type: "normal" }); setSelectedDevice({ mode: "selected", deviceId: event.selectedDevice }); break; case "error": setState({ type: "error", message: event.error || tr("Unknown error") }); break; case "audio-not-initialized": setState({ type: "audio-not-initialized" }); break; case "no-permissions": setState({ type: "no-permissions", bySystem: event.shouldAsk }); 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 (
Please grant access to your microphone.
props.events.fire("action_request_permissions")} /> { if(state.type !== "normal" || selectedDevice?.mode === "selecting") return; props.events.fire("action_set_selected_device", { deviceId: IDevice.NoDeviceId }); }} /> {deviceList.map(e => { if(state.type !== "normal" || selectedDevice?.mode === "selecting") return; props.events.fire("action_set_selected_device", { deviceId: e.id }); }} />)}
) } const ListRefreshButton = (props: { events: Registry }) => { 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 ; } const VolumeSettings = (props: { events: Registry }) => { const refSlider = useRef(); 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; refSlider.current?.setState({ value: event.value }); setValue(event.value); }); return (
Volume props.events.fire("action_set_setting", { setting: "volume", value: value })} />
) }; const PPTKeyButton = React.memo((props: { events: Registry }) => { 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 ; } else { return ; } }); const PPTDelaySettings = (props: { events: Registry }) => { 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 [ currentDevice, setCurrentDevice ] = useState(undefined); 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"); } }); props.events.reactUse("notify_devices", event => { setCurrentDevice(event.selectedDevice); }); props.events.reactUse("action_set_selected_device_result", event => { setCurrentDevice(event.deviceId); }); return (
{ props.events.fire("action_set_setting", { setting: "threshold-threshold", value: value })}} />
) }; export const MicrophoneSettings = (props: { events: Registry }) => ( )