Первоначальная версия VidConf

This commit is contained in:
2026-07-23 01:04:01 +03:00
commit 896455381a
335 changed files with 61527 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
import { X } from 'lucide-react'
import { useMediaDeviceSelect, usePersistentUserChoices } from '@livekit/components-react'
import { useToast } from '@/components/ui/ToastProvider'
interface DeviceSettingsDialogProps {
onClose: () => void
}
/** Человекочитаемая подпись пункта списка устройств — `label` пуст, пока нет разрешения на медиа. */
function deviceLabel(device: MediaDeviceInfo, index: number, fallback: string): string {
return device.label || `${fallback} ${index + 1}`
}
/**
* Диалог «Настройки устройств» — два селекта
* на хуках `@livekit/components-react`: список устройств и переключение —
* целиком в `useMediaDeviceSelect` (сама подписана на
* `RoomEvent.MediaDevicesChanged`), персист выбора — в `usePersistentUserChoices`
* (localStorage, читается заново при следующем входе в комнату — см.
* `RoomPage.tsx`, `options` пропс `LiveKitRoom`).
*
* ДОЛЖЕН рендериться внутри `<LiveKitRoom>`: `useMediaDeviceSelect` без явно
* переданного `room` берёт активную комнату из `RoomContext` — вне контекста
* он создал бы отдельный, ни с чем не связанный `Room()` и переключал бы
* устройство «в никуда».
*/
export function DeviceSettingsDialog({ onClose }: DeviceSettingsDialogProps) {
const toast = useToast()
const { saveAudioInputDeviceId, saveVideoInputDeviceId } = usePersistentUserChoices()
const mic = useMediaDeviceSelect({ kind: 'audioinput' })
const camera = useMediaDeviceSelect({ kind: 'videoinput' })
async function handleMicChange(deviceId: string) {
try {
await mic.setActiveMediaDevice(deviceId)
saveAudioInputDeviceId(deviceId)
} catch {
// activeDeviceId хука — источник истины, состояние селекта само не меняется.
toast.show('Не удалось переключить микрофон — устройство занято или отключено', 'error')
}
}
async function handleCameraChange(deviceId: string) {
try {
await camera.setActiveMediaDevice(deviceId)
saveVideoInputDeviceId(deviceId)
} catch {
toast.show('Не удалось переключить камеру — устройство занято или отключено', 'error')
}
}
return (
<div
className="room-modal-overlay"
role="dialog"
aria-modal="true"
aria-labelledby="device-settings-title"
onClick={onClose}
>
<div className="room-modal-panel" onClick={(e) => e.stopPropagation()}>
<div className="room-modal-head">
<h2 id="device-settings-title">Настройки устройств</h2>
<button type="button" className="room-modal-close" aria-label="Закрыть" onClick={onClose}>
<X className="lucide" style={{ width: 16, height: 16 }} aria-hidden="true" />
</button>
</div>
<div className="room-field">
<label htmlFor="device-settings-mic">Микрофон</label>
<select
id="device-settings-mic"
value={mic.activeDeviceId}
onChange={(e) => void handleMicChange(e.target.value)}
>
{/* Заглушка на случай, пока activeDeviceId не совпадает ни с одним
устройством из списка (нет разрешения на медиа/список ещё не
перечислен) — без неё controlled-select рассинхронизируется с
DOM (ни одна из настоящих option не соответствует value). */}
<option value="" disabled>
Определяется
</option>
{mic.devices.map((device, index) => (
<option key={device.deviceId} value={device.deviceId}>
{deviceLabel(device, index, 'Микрофон')}
</option>
))}
</select>
</div>
<div className="room-field">
<label htmlFor="device-settings-camera">Камера</label>
<select
id="device-settings-camera"
value={camera.activeDeviceId}
onChange={(e) => void handleCameraChange(e.target.value)}
>
<option value="" disabled>
Определяется
</option>
{camera.devices.map((device, index) => (
<option key={device.deviceId} value={device.deviceId}>
{deviceLabel(device, index, 'Камера')}
</option>
))}
</select>
</div>
</div>
</div>
)
}