feat(room): полноэкранный режим на мобильном с выезжающим тулбаром
Вход — кнопка «Экран» в тулбаре, теперь видна и на мобильном (по аналогии с десктопом, а не спрятана в шторку настроек). В полноэкранном режиме топбар и тулбар выходят из потока и лежат оверлеем поверх сцены: тап/клик по сцене вне элементов управления показывает их, сами прячутся через несколько секунд бездействия; на десктопе дополнительно — наведение мыши в нижнюю полосу экрана. Работает одинаково на мобильном и десктопе. Кнопка настроек устройств в тулбаре на мобильном подписана «Настройки» вместо «Устройства». Раскладка кнопок мобильного тулбара, когда они не помещаются в один ряд (7+, обычный случай с «Экраном» и «Очередью» у организатора), стала равномерной сеткой на 4 колонки вместо переноса «как получится» через flex-wrap.
This commit is contained in:
@@ -22,6 +22,12 @@ import { loadAudioOutputDeviceId } from '@/lib/audioOutputDevice'
|
||||
import { buildPublishDefaults } from '@/lib/publishQualityCap'
|
||||
import { loadStageLayoutMode, saveStageLayoutMode, type StageLayoutMode } from '@/lib/stageLayoutMode'
|
||||
|
||||
/** Сколько мс держать топбар/тулбар видимыми в полноэкранном режиме без взаимодействия, прежде чем спрятать их снова. */
|
||||
const FULLSCREEN_CONTROLS_AUTO_HIDE_MS = 4000
|
||||
|
||||
/** Полоса у нижнего края экрана (px) — наведение мыши в неё в полноэкранном режиме на десктопе показывает тулбар без клика. */
|
||||
const FULLSCREEN_FOOTER_HOVER_ZONE_PX = 72
|
||||
|
||||
interface RoomJoinState {
|
||||
livekitUrl: string
|
||||
token: string
|
||||
@@ -175,6 +181,73 @@ export function RoomPage() {
|
||||
const pip = useRoomPiP(roomRootRef)
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
|
||||
// Топбар и тулбар в полноэкранном режиме — оверлей поверх сцены (см.
|
||||
// `.room-fullscreen-overlay` в room.css), а не часть потока: показываются
|
||||
// по тапу/клику по сцене вне элементов управления и прячутся сами через
|
||||
// FULLSCREEN_CONTROLS_AUTO_HIDE_MS бездействия. На десктопе есть ещё второй
|
||||
// способ показать их — навести мышь в нижнюю полосу экрана (без клика),
|
||||
// как в большинстве видеоплееров; клик остаётся основным способом на
|
||||
// тач-устройствах, где наведения не бывает. Обычный (не полноэкранный)
|
||||
// режим этот стейт не использует вовсе.
|
||||
const [fullscreenControlsVisible, setFullscreenControlsVisible] = useState(true)
|
||||
// Счётчик «попроси показать и отсчитать заново» — растёт на каждый клик/
|
||||
// наведение, даже если панель УЖЕ видима (иначе непрерывное наведение не
|
||||
// продлевало бы таймер: setState(true) поверх уже true не меняет состояние
|
||||
// и не перезапускает эффект ниже).
|
||||
const [fullscreenControlsTick, setFullscreenControlsTick] = useState(0)
|
||||
|
||||
// Показ при входе в полноэкранный режим — «подгонка состояния во время
|
||||
// рендера» (см. тот же приём у `chatSeenCount` выше), а не setState в теле
|
||||
// эффекта (react-hooks/set-state-in-effect): само планирование таймера
|
||||
// авто-скрытия — ниже, отдельным эффектом, и не вызывает setState
|
||||
// синхронно в своём теле.
|
||||
const [prevFullscreenActive, setPrevFullscreenActive] = useState(fullscreen.active)
|
||||
if (fullscreen.active !== prevFullscreenActive) {
|
||||
setPrevFullscreenActive(fullscreen.active)
|
||||
if (fullscreen.active) {
|
||||
setFullscreenControlsVisible(true)
|
||||
setFullscreenControlsTick((tick) => tick + 1)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!fullscreen.active || !fullscreenControlsVisible) return
|
||||
const timer = setTimeout(() => setFullscreenControlsVisible(false), FULLSCREEN_CONTROLS_AUTO_HIDE_MS)
|
||||
return () => clearTimeout(timer)
|
||||
}, [fullscreen.active, fullscreenControlsVisible, fullscreenControlsTick])
|
||||
|
||||
// Наведение мыши в нижнюю полосу экрана — только десктопный способ показать
|
||||
// элементы управления без клика; на тач-устройствах `mousemove` в таком виде
|
||||
// не приходит, слушатель им не вредит и не мешает.
|
||||
useEffect(() => {
|
||||
if (!fullscreen.active) return
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
if (window.innerHeight - e.clientY <= FULLSCREEN_FOOTER_HOVER_ZONE_PX) {
|
||||
setFullscreenControlsVisible(true)
|
||||
setFullscreenControlsTick((tick) => tick + 1)
|
||||
}
|
||||
}
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
return () => window.removeEventListener('mousemove', handleMouseMove)
|
||||
}, [fullscreen.active])
|
||||
|
||||
// Клик/тап по сцене переключает видимость — но не по элементам управления
|
||||
// внутри неё (кнопки плиток, булавка закрепления, «Показать остальных» и
|
||||
// т.п.: `closest` поднимается от места клика и гасит переключение, если
|
||||
// по дороге встретился интерактивный элемент) и не по открытой панели чата
|
||||
// (`.chat-panel` — та рендерится внутри того же `.room-main`, но клики по
|
||||
// тексту сообщений не должны прятать/показывать тулбар).
|
||||
const handleStageAreaClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!fullscreen.active) return
|
||||
const target = e.target as HTMLElement
|
||||
if (target.closest('button, a, input, select, textarea, [role="dialog"], .chat-panel')) return
|
||||
setFullscreenControlsVisible((visible) => !visible)
|
||||
setFullscreenControlsTick((tick) => tick + 1)
|
||||
},
|
||||
[fullscreen.active],
|
||||
)
|
||||
|
||||
// Вид сцены живёт здесь, а не в `RoomStage`: переключатели — в тулбаре и в
|
||||
// шторке настроек, а сцена их только читает (общий предок).
|
||||
//
|
||||
@@ -281,7 +354,7 @@ export function RoomPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-theme="room" ref={roomRootRef}>
|
||||
<div data-theme="room" ref={roomRootRef} className={fullscreen.active ? 'room-fullscreen-overlay' : undefined}>
|
||||
<LiveKitRoom
|
||||
serverUrl={joinState.livekitUrl}
|
||||
token={joinState.token}
|
||||
@@ -307,8 +380,13 @@ export function RoomPage() {
|
||||
непрерывно, весь цикл «открыл мини-окно → вернул» их не касается. */}
|
||||
<RoomAudioRenderer />
|
||||
<div data-lk-theme="default" className="room-shell">
|
||||
<RoomTopbar title={joinState.title ?? null} slug={slug} number={joinState.number} />
|
||||
<div className="room-main">
|
||||
<RoomTopbar
|
||||
title={joinState.title ?? null}
|
||||
slug={slug}
|
||||
number={joinState.number}
|
||||
overlayVisible={fullscreenControlsVisible}
|
||||
/>
|
||||
<div className="room-main" onClick={handleStageAreaClick}>
|
||||
{pip.mode === 'document' ? (
|
||||
// Сцена сейчас рисуется в PiP-окне (через createPortal ниже) —
|
||||
// основное окно вместо неё показывает заглушку с возвратом.
|
||||
@@ -365,6 +443,7 @@ export function RoomPage() {
|
||||
onLayoutModeChange={handleLayoutModeChange}
|
||||
hideOthers={hideOthers}
|
||||
onHideOthersChange={setHideOthers}
|
||||
overlayVisible={fullscreenControlsVisible}
|
||||
/>
|
||||
</div>
|
||||
{settingsOpen && (
|
||||
|
||||
Reference in New Issue
Block a user