feat(room): выбор режима показа участников и скрытие остальных
Раскладка сцены больше не выбирается автоматически — пользователь выбирает
один из трёх режимов:
- «Стандарт» — как раньше: крупная плитка плюс карусель остальных сбоку,
кого показать крупно, по-прежнему решает pickStageFocus;
- «Плитки» — все участники равными плитками, без выделенного крупного;
- «Живые плитки» — сетка только из тех, у кого включена камера, остальные
в карусели сбоку; если камеру не включил никто, сетка была бы пустой —
режим вырождается в «Плитки».
Демонстрация экрана перебивает выбранный режим: пока в комнате есть активный
шэр, сцена ведёт себя как «Стандарт» (смысл плиточных режимов — равноправие
участников, а демонстрация неравноправна по определению). Режим при этом
живёт в состоянии RoomPage, поэтому по завершении шэра вид сам возвращается
к выбранному.
Скрытие остальных убирает карусель, основная область занимает сцену целиком.
Скрыть можно из меню «Вид», из шторки настроек на мобильном и кнопкой прямо
над колонкой миниатюр; вернуть — кнопкой «Показать остальных (N)» на сцене,
которая видна всё время, пока кто-то скрыт. В режиме «Плитки» скрывать
нечего, переключатель там заблокирован с пояснением.
Переключатель на широком экране — кнопка «Вид» с поповером; на мобильном её
нет (тулбар там и так ужат до пяти кнопок), те же настройки идут секцией в
шторке. Видимость решается условным рендерингом в React, а не новым
CSS-правилом поверх медиазапроса — в room.css за это уже была битва
специфичности.
Режим сохраняется между заходами в комнату (localStorage, своим модулем —
LocalUserChoices у LiveKit фиксированной структуры, поля под раскладку там
нет). Скрытие не сохраняется: разовое действие по ходу разговора, войти в
новую конференцию без половины участников — сюрприз, а не удобство.
Сетка собрана своим StageGrid поверх тех же публичных хуков, что использует
GridLayout: библиотечный компонент не пробрасывает gridLayouts, а её набор
раскладок требует 560px уже для 2x2 — на телефоне это две плитки на страницу.
Свой набор даёт 2x2 от 360px и портретную 2x3. Индикатор страниц тоже свой:
PaginationControl/PaginationIndicator из пакета не экспортируются.
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { X } from 'lucide-react'
|
||||
import { useMediaDeviceSelect, usePersistentUserChoices } from '@livekit/components-react'
|
||||
import { useToast } from '@/components/ui/ToastProvider'
|
||||
import { useIsCompactViewport } from '@/hooks/useIsCompactViewport'
|
||||
import { useModalDismiss } from '@/hooks/useModalDismiss'
|
||||
import { StageViewOptions, type StageViewProps } from '@/components/room/StageViewOptions'
|
||||
import { isAudioOutputSelectable, saveAudioOutputDeviceId } from '@/lib/audioOutputDevice'
|
||||
|
||||
interface DeviceSettingsDialogProps {
|
||||
interface DeviceSettingsDialogProps extends StageViewProps {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
/** Совпадает с мобильным брейкпоинтом комнаты (`room.css`, `max-width: 600px`) — ниже него панель рендерится шторкой снизу вместо модалки. */
|
||||
const COMPACT_VIEWPORT_QUERY = '(max-width: 600px)'
|
||||
/** Свайп ручки шторки вниз дальше этого порога (px) закрывает панель, меньше — она возвращается на место. */
|
||||
const SHEET_DISMISS_THRESHOLD_PX = 80
|
||||
|
||||
@@ -19,20 +19,6 @@ function deviceLabel(device: MediaDeviceInfo, index: number, fallback: string):
|
||||
return device.label || `${fallback} ${index + 1}`
|
||||
}
|
||||
|
||||
/** Живое отслеживание мобильной ширины — та же схема, что системная тема в `useTheme.ts` (matchMedia + change-листенер). */
|
||||
function useIsCompactViewport(): boolean {
|
||||
const [isCompact, setIsCompact] = useState(() => window.matchMedia(COMPACT_VIEWPORT_QUERY).matches)
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia(COMPACT_VIEWPORT_QUERY)
|
||||
const handleChange = (event: MediaQueryListEvent) => setIsCompact(event.matches)
|
||||
media.addEventListener('change', handleChange)
|
||||
return () => media.removeEventListener('change', handleChange)
|
||||
}, [])
|
||||
|
||||
return isCompact
|
||||
}
|
||||
|
||||
/**
|
||||
* Панель «Настройки устройств» — три селекта на хуках `@livekit/components-react`:
|
||||
* список устройств и переключение целиком в `useMediaDeviceSelect` (сама
|
||||
@@ -47,12 +33,23 @@ function useIsCompactViewport(): boolean {
|
||||
* модалки — по клику вне, Escape (`useModalDismiss`) и свайпу вниз за ручку.
|
||||
* Десктоп не меняется.
|
||||
*
|
||||
* Там же, и только там, первой секцией идёт «Вид» (режим показа участников и
|
||||
* скрытие остальных): на мобильном тулбар ужат до пяти кнопок, отдельной
|
||||
* кнопки «Вид» там нет — см. `RoomToolbar`. Поэтому и заголовок панели на
|
||||
* мобильном шире по смыслу («Настройки», а не «Настройки устройств»).
|
||||
*
|
||||
* ДОЛЖЕН рендериться внутри `<LiveKitRoom>`: `useMediaDeviceSelect` без явно
|
||||
* переданного `room` берёт активную комнату из `RoomContext` — вне контекста
|
||||
* он создал бы отдельный, ни с чем не связанный `Room()` и переключал бы
|
||||
* устройство «в никуда».
|
||||
*/
|
||||
export function DeviceSettingsDialog({ onClose }: DeviceSettingsDialogProps) {
|
||||
export function DeviceSettingsDialog({
|
||||
onClose,
|
||||
layoutMode,
|
||||
onLayoutModeChange,
|
||||
hideOthers,
|
||||
onHideOthersChange,
|
||||
}: DeviceSettingsDialogProps) {
|
||||
const toast = useToast()
|
||||
const { saveAudioInputDeviceId, saveVideoInputDeviceId } = usePersistentUserChoices()
|
||||
const mic = useMediaDeviceSelect({ kind: 'audioinput' })
|
||||
@@ -143,12 +140,26 @@ export function DeviceSettingsDialog({ onClose }: DeviceSettingsDialogProps) {
|
||||
)}
|
||||
|
||||
<div className="room-modal-head">
|
||||
<h2 id="device-settings-title">Настройки устройств</h2>
|
||||
<h2 id="device-settings-title">{isCompact ? 'Настройки' : 'Настройки устройств'}</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>
|
||||
|
||||
{/* Обёртка НЕ `.room-field`: там `label { display: block }` со
|
||||
специфичностью выше, чем у `.stage-view-mode` — пункты режимов
|
||||
рассыпались бы в столбик (кружок отдельно, текст под ним). */}
|
||||
{isCompact && (
|
||||
<div className="stage-view-section">
|
||||
<StageViewOptions
|
||||
layoutMode={layoutMode}
|
||||
onLayoutModeChange={onLayoutModeChange}
|
||||
hideOthers={hideOthers}
|
||||
onHideOthersChange={onHideOthersChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="room-field">
|
||||
<label htmlFor="device-settings-mic">Микрофон</label>
|
||||
<select
|
||||
|
||||
Reference in New Issue
Block a user