Первоначальная версия 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,215 @@
import {
LogOut,
Maximize,
MessageSquare,
Mic,
MicOff,
Minimize,
PictureInPicture2,
ScreenShare,
ScreenShareOff,
Settings,
Video,
VideoOff,
} from 'lucide-react'
import { Track, type ScreenShareCaptureOptions } from 'livekit-client'
import { DisconnectButton, useTrackToggle } from '@livekit/components-react'
import { useToast } from '@/components/ui/ToastProvider'
/**
* Опции захвата демонстрации экрана: `audio: true` — звук
* вкладки/экрана там, где браузер его отдаёт (Chrome/Edge — вкладка почти
* всегда, целый экран — только Windows); `selfBrowserSurface: 'exclude'`
* — не предлагать в списке
* источников собственную вкладку (зеркальный туннель самой конференции);
* `surfaceSwitching: 'include'` — разрешить переключать источник прямо во
* время демонстрации, не останавливая её; `systemAudio: 'include'` — не
* запрещать захват системного звука при выборе «весь экран». Вынесено в
* модульную константу — `useTrackToggle` держит `JSON.stringify(captureOptions)`
* в зависимостях внутреннего `useMemo`, инлайновый литерал был бы безвреден,
* но константа явнее фиксирует неизменность опций.
*/
const SCREEN_SHARE_CAPTURE_OPTIONS: ScreenShareCaptureOptions = {
audio: true,
selfBrowserSurface: 'exclude',
surfaceSwitching: 'include',
systemAudio: 'include',
}
interface RoomToolbarProps {
/** Показывать ли кнопку чата — `JoinOut.chat_enabled` И чат не помечен недоступным (close-код 4404). */
chatVisible: boolean
chatOpen: boolean
/** Число непрочитанных сообщений, накопленных со времени последнего открытия панели. */
chatUnreadCount: number
onToggleChat: () => void
/** Открыть диалог настроек устройств. */
onToggleSettings: () => void
/** `document.fullscreenEnabled` — false скрывает кнопку. */
fullscreenSupported: boolean
fullscreenActive: boolean
onToggleFullscreen: () => void
/** Доступен ли мини-плеер хоть в каком-то виде (Document PiP либо video-фолбэк) — false в Firefox скрывает кнопку. */
pipSupported: boolean
pipActive: boolean
onTogglePiP: () => void
}
/**
* Нижний тулбар комнаты: микрофон/камера/демонстрация экрана/
* настройки устройств/полноэкранный режим/мини-плеер/чат/выход — собственные
* кнопки на хуках LiveKit (useTrackToggle/DisconnectButton) и панели чата,
* стилизованные по design/mockups/room.html.
*/
export function RoomToolbar({
chatVisible,
chatOpen,
chatUnreadCount,
onToggleChat,
onToggleSettings,
fullscreenSupported,
fullscreenActive,
onToggleFullscreen,
pipSupported,
pipActive,
onTogglePiP,
}: RoomToolbarProps) {
const toast = useToast()
const mic = useTrackToggle({ source: Track.Source.Microphone })
const camera = useTrackToggle({ source: Track.Source.Camera })
const screenShare = useTrackToggle({
source: Track.Source.ScreenShare,
captureOptions: SCREEN_SHARE_CAPTURE_OPTIONS,
// Отмена браузерного диалога выбора экрана/окна (пользователь нажал
// «Отмена») приходит сюда как NotAllowedError — молча игнорируем, это не
// ошибка приложения. Прочие сбои (например, `NotReadableError` при
// занятом источнике) показываем тостом.
onDeviceError: (error) => {
if (error.name === 'NotAllowedError') return
toast.show('Не удалось начать демонстрацию экрана', 'error')
},
})
return (
<footer className="room-toolbar">
<button
type="button"
{...mic.buttonProps}
className={`tb-btn${mic.enabled ? '' : ' is-off'}`}
aria-label={mic.enabled ? 'Выключить микрофон' : 'Включить микрофон'}
>
<span className="icon-shell">
{mic.enabled ? (
<Mic className="lucide" aria-hidden="true" />
) : (
<MicOff className="lucide" aria-hidden="true" />
)}
</span>
<span className="label">Микрофон</span>
</button>
<button
type="button"
{...camera.buttonProps}
className={`tb-btn${camera.enabled ? '' : ' is-off'}`}
aria-label={camera.enabled ? 'Выключить камеру' : 'Включить камеру'}
>
<span className="icon-shell">
{camera.enabled ? (
<Video className="lucide" aria-hidden="true" />
) : (
<VideoOff className="lucide" aria-hidden="true" />
)}
</span>
<span className="label">Камера</span>
</button>
<button
type="button"
{...screenShare.buttonProps}
className={`tb-btn${screenShare.enabled ? ' is-sharing' : ''}`}
aria-label={screenShare.enabled ? 'Остановить демонстрацию экрана' : 'Демонстрировать экран'}
>
<span className="icon-shell">
{screenShare.enabled ? (
<ScreenShareOff className="lucide" aria-hidden="true" />
) : (
<ScreenShare className="lucide" aria-hidden="true" />
)}
</span>
<span className="label">Демонстрация</span>
</button>
<button
type="button"
className="tb-btn"
aria-label="Настройки устройств"
onClick={onToggleSettings}
>
<span className="icon-shell">
<Settings className="lucide" aria-hidden="true" />
</span>
<span className="label">Устройства</span>
</button>
{fullscreenSupported && (
<button
type="button"
className={`tb-btn${fullscreenActive ? ' is-panel-open' : ''}`}
aria-label={fullscreenActive ? 'Выйти из полноэкранного режима' : 'Развернуть на весь экран'}
aria-pressed={fullscreenActive}
onClick={onToggleFullscreen}
>
<span className="icon-shell">
{fullscreenActive ? (
<Minimize className="lucide" aria-hidden="true" />
) : (
<Maximize className="lucide" aria-hidden="true" />
)}
</span>
<span className="label">Экран</span>
</button>
)}
{pipSupported && (
<button
type="button"
className={`tb-btn${pipActive ? ' is-panel-open' : ''}`}
aria-label={pipActive ? 'Свернуть мини-окно' : 'Открыть мини-окно'}
aria-pressed={pipActive}
onClick={onTogglePiP}
>
<span className="icon-shell">
<PictureInPicture2 className="lucide" aria-hidden="true" />
</span>
<span className="label">Мини-окно</span>
</button>
)}
{chatVisible && (
<button
type="button"
className={`tb-btn${chatOpen ? ' is-panel-open' : ''}`}
aria-label={chatOpen ? 'Свернуть чат' : 'Открыть чат'}
aria-pressed={chatOpen}
onClick={onToggleChat}
>
<span className="icon-shell">
<MessageSquare className="lucide" aria-hidden="true" />
{chatUnreadCount > 0 && (
<span className="badge-count">{chatUnreadCount > 9 ? '9+' : chatUnreadCount}</span>
)}
</span>
<span className="label">Чат</span>
</button>
)}
<DisconnectButton className="tb-btn danger" aria-label="Выйти из конференции">
<span className="icon-shell">
<LogOut className="lucide" aria-hidden="true" />
</span>
<span className="label">Выйти</span>
</DisconnectButton>
</footer>
)
}