Merge pull request #2803 from robintown/device-radio

Replace device dropdowns with radio buttons
This commit is contained in:
Robin 2024-11-21 10:47:44 -05:00 committed by GitHub
commit 8de96878c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 123 additions and 56 deletions

View File

@ -0,0 +1,18 @@
.selection {
gap: 0;
}
.title {
color: var(--cpd-color-text-secondary);
margin-block: var(--cpd-space-3x) 0;
}
.separator {
margin-block: 6px var(--cpd-space-4x);
}
.options {
display: flex;
flex-direction: column;
gap: var(--cpd-space-4x);
}

View File

@ -0,0 +1,71 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
import { ChangeEvent, FC, useCallback, useId } from "react";
import {
Heading,
InlineField,
Label,
RadioControl,
Separator,
} from "@vector-im/compound-web";
import { MediaDevice } from "../livekit/MediaDevicesContext";
import styles from "./DeviceSelection.module.css";
interface Props {
devices: MediaDevice;
caption: string;
}
export const DeviceSelection: FC<Props> = ({ devices, caption }) => {
const groupId = useId();
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
devices.select(e.target.value);
},
[devices],
);
if (devices.available.length == 0) return null;
return (
<div className={styles.selection}>
<Heading
type="body"
weight="semibold"
size="sm"
as="h4"
className={styles.title}
>
{caption}
</Heading>
<Separator className={styles.separator} />
<div className={styles.options}>
{devices.available.map(({ deviceId, label }, index) => (
<InlineField
key={deviceId}
name={groupId}
control={
<RadioControl
checked={deviceId === devices.selectedId}
onChange={onChange}
value={deviceId}
/>
}
>
<Label>
{!!label && label.trim().length > 0
? label
: `${caption} ${index + 1}`}
</Label>
</InlineField>
))}
</div>
</div>
);
};

View File

@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
import { ChangeEvent, FC, ReactNode, useCallback } from "react";
import { ChangeEvent, FC, useCallback } from "react";
import { Trans, useTranslation } from "react-i18next";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { Dropdown, Separator, Text } from "@vector-im/compound-web";
import { Root as Form, Text } from "@vector-im/compound-web";
import { Modal } from "../Modal";
import styles from "./SettingsModal.module.css";
@ -19,7 +19,6 @@ import { ProfileSettingsTab } from "./ProfileSettingsTab";
import { FeedbackSettingsTab } from "./FeedbackSettingsTab";
import {
useMediaDevices,
MediaDevice,
useMediaDeviceNames,
} from "../livekit/MediaDevicesContext";
import { widget } from "../widget";
@ -33,6 +32,7 @@ import {
import { isFirefox } from "../Platform";
import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
import { Slider } from "../Slider";
import { DeviceSelection } from "./DeviceSelection";
type SettingsTab =
| "audio"
@ -70,40 +70,6 @@ export const SettingsModal: FC<Props> = ({
);
const [duplicateTiles, setDuplicateTiles] = useSetting(duplicateTilesSetting);
// Generate a `SelectInput` with a list of devices for a given device kind.
const generateDeviceSelection = (
devices: MediaDevice,
caption: string,
): ReactNode => {
if (devices.available.length == 0) return null;
const values = devices.available.map(
({ deviceId, label }, index) =>
[
deviceId,
!!label && label.trim().length > 0
? label
: `${caption} ${index + 1}`,
] as [string, string],
);
return (
<Dropdown
label={caption}
defaultValue={
devices.selectedId === "" || !devices.selectedId
? "default"
: devices.selectedId
}
onValueChange={(id): void => devices.select(id)}
values={values}
// XXX This is unused because we set a defaultValue. The component
// shouldn't require this prop.
placeholder=""
/>
);
};
const optInDescription = (
<Text size="sm">
<Trans i18nKey="settings.opt_in_description">
@ -125,25 +91,30 @@ export const SettingsModal: FC<Props> = ({
name: t("common.audio"),
content: (
<>
{generateDeviceSelection(devices.audioInput, t("common.microphone"))}
{!isFirefox() &&
generateDeviceSelection(
devices.audioOutput,
t("settings.speaker_device_selection_label"),
)}
<Separator />
<div className={styles.volumeSlider}>
<label>{t("settings.audio_tab.effect_volume_label")}</label>
<p>{t("settings.audio_tab.effect_volume_description")}</p>
<Slider
label={t("video_tile.volume")}
value={soundVolume}
onValueChange={setSoundVolume}
min={0}
max={1}
step={0.01}
<Form>
<DeviceSelection
devices={devices.audioInput}
caption={t("common.microphone")}
/>
</div>
{!isFirefox() && (
<DeviceSelection
devices={devices.audioOutput}
caption={t("settings.speaker_device_selection_label")}
/>
)}
<div className={styles.volumeSlider}>
<label>{t("settings.audio_tab.effect_volume_label")}</label>
<p>{t("settings.audio_tab.effect_volume_description")}</p>
<Slider
label={t("video_tile.volume")}
value={soundVolume}
onValueChange={setSoundVolume}
min={0}
max={1}
step={0.01}
/>
</div>
</Form>
</>
),
};
@ -151,7 +122,14 @@ export const SettingsModal: FC<Props> = ({
const videoTab: Tab<SettingsTab> = {
key: "video",
name: t("common.video"),
content: generateDeviceSelection(devices.videoInput, t("common.camera")),
content: (
<Form>
<DeviceSelection
devices={devices.videoInput}
caption={t("common.camera")}
/>
</Form>
),
};
const preferencesTab: Tab<SettingsTab> = {