mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
优化移动端播放
This commit is contained in:
@@ -46,6 +46,9 @@ import {
|
|||||||
PLAYER_MENU_LABEL,
|
PLAYER_MENU_LABEL,
|
||||||
PLAYER_POPOVER,
|
PLAYER_POPOVER,
|
||||||
PLAYER_RANGE_OVERLAY,
|
PLAYER_RANGE_OVERLAY,
|
||||||
|
PLAYER_SHEET,
|
||||||
|
PLAYER_SHEET_BODY,
|
||||||
|
PLAYER_SHEET_HEADER,
|
||||||
PLAYER_TEXT_BUTTON,
|
PLAYER_TEXT_BUTTON,
|
||||||
PLAYER_TRACK,
|
PLAYER_TRACK,
|
||||||
PLAYER_TRACK_FILL,
|
PLAYER_TRACK_FILL,
|
||||||
@@ -193,10 +196,17 @@ type PlayerControlsProps = {
|
|||||||
streamOffset?: number
|
streamOffset?: number
|
||||||
/** Absolute seek on the full timeline; return true when handled (e.g. HLS restart). */
|
/** Absolute seek on the full timeline; return true when handled (e.g. HLS restart). */
|
||||||
onSeekAbsolute?: (seconds: number) => boolean
|
onSeekAbsolute?: (seconds: number) => boolean
|
||||||
|
/**
|
||||||
|
* 竖屏剧场模式:控制栏按钮放大到触控尺寸(44px 起),清晰度/倍速/
|
||||||
|
* 选集/字幕弹层改为视频区底部的动作面板,避免右侧抽屉与悬浮小弹层
|
||||||
|
* 在窄屏下被裁剪。桌面端保持原样。
|
||||||
|
*/
|
||||||
|
theater?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PlayerControls({
|
export function PlayerControls({
|
||||||
videoRef,
|
videoRef,
|
||||||
|
theater = false,
|
||||||
volume: volumeProp = 1,
|
volume: volumeProp = 1,
|
||||||
onVolumeChange,
|
onVolumeChange,
|
||||||
onVolumeCommit,
|
onVolumeCommit,
|
||||||
@@ -910,7 +920,7 @@ export function PlayerControls({
|
|||||||
const compactOnlySections = (
|
const compactOnlySections = (
|
||||||
<>
|
<>
|
||||||
{showQuality && hasQualityGroups && (
|
{showQuality && hasQualityGroups && (
|
||||||
<div className="sm:hidden">
|
<div className={theater ? undefined : 'sm:hidden'}>
|
||||||
<p className={PLAYER_MENU_LABEL}>清晰度</p>
|
<p className={PLAYER_MENU_LABEL}>清晰度</p>
|
||||||
<QualityItems
|
<QualityItems
|
||||||
groups={qualityGroups}
|
groups={qualityGroups}
|
||||||
@@ -919,7 +929,7 @@ export function PlayerControls({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="sm:hidden">
|
<div className={theater ? undefined : 'sm:hidden'}>
|
||||||
<p className={PLAYER_MENU_LABEL}>播放速度</p>
|
<p className={PLAYER_MENU_LABEL}>播放速度</p>
|
||||||
<SpeedItems
|
<SpeedItems
|
||||||
playbackRate={playbackRateProp}
|
playbackRate={playbackRateProp}
|
||||||
@@ -927,7 +937,7 @@ export function PlayerControls({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{onTogglePlaylist && (
|
{onTogglePlaylist && (
|
||||||
<div className="sm:hidden">
|
<div className={theater ? undefined : 'sm:hidden'}>
|
||||||
<p className={PLAYER_MENU_LABEL}>选集</p>
|
<p className={PLAYER_MENU_LABEL}>选集</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -947,7 +957,7 @@ export function PlayerControls({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{subs.length > 0 && (
|
{subs.length > 0 && (
|
||||||
<div className="sm:hidden">
|
<div className={theater ? undefined : 'sm:hidden'}>
|
||||||
<p className={PLAYER_MENU_LABEL}>字幕</p>
|
<p className={PLAYER_MENU_LABEL}>字幕</p>
|
||||||
<SubtitleItems
|
<SubtitleItems
|
||||||
subs={subs}
|
subs={subs}
|
||||||
@@ -965,7 +975,7 @@ export function PlayerControls({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={`sm:hidden ${PLAYER_MENU_DIVIDER}`} />
|
<div className={theater ? PLAYER_MENU_DIVIDER : `sm:hidden ${PLAYER_MENU_DIVIDER}`} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1028,6 +1038,33 @@ export function PlayerControls({
|
|||||||
<>
|
<>
|
||||||
{seekOverlay}
|
{seekOverlay}
|
||||||
{playbackRateOverlay}
|
{playbackRateOverlay}
|
||||||
|
{theater && settingsMenuOpen ? (
|
||||||
|
<div
|
||||||
|
className="absolute inset-x-0 bottom-0 z-40 flex items-end justify-center"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onPointerUp={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
onClick={() => setSettingsMenuOpen(false)}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<div className={`relative ${PLAYER_SHEET}`}>
|
||||||
|
<div className={PLAYER_SHEET_HEADER}>
|
||||||
|
<span className="text-[13px] font-semibold text-white/90">播放器设置</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSettingsMenuOpen(false)}
|
||||||
|
className={PLAYER_ICON_BUTTON}
|
||||||
|
title="关闭设置"
|
||||||
|
>
|
||||||
|
<Minimize size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className={PLAYER_SHEET_BODY}>{settingsPanel}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div
|
<div
|
||||||
className={`pointer-events-auto absolute inset-x-0 bottom-0 z-20 bg-gradient-to-t from-black/85 via-black/45 to-transparent px-2.5 pb-1.5 pt-12 transition-opacity duration-300 sm:px-3.5 sm:pb-2.5 ${
|
className={`pointer-events-auto absolute inset-x-0 bottom-0 z-20 bg-gradient-to-t from-black/85 via-black/45 to-transparent px-2.5 pb-1.5 pt-12 transition-opacity duration-300 sm:px-3.5 sm:pb-2.5 ${
|
||||||
uiVisible ? 'opacity-100' : 'pointer-events-none opacity-0'
|
uiVisible ? 'opacity-100' : 'pointer-events-none opacity-0'
|
||||||
@@ -1042,11 +1079,11 @@ export function PlayerControls({
|
|||||||
>
|
>
|
||||||
{/* 进度条:单独占一行贴着画面底部,轨道只有 3px,悬停才变粗并露出圆点手柄 */}
|
{/* 进度条:单独占一行贴着画面底部,轨道只有 3px,悬停才变粗并露出圆点手柄 */}
|
||||||
<div
|
<div
|
||||||
className="group/seek relative flex h-5 w-full items-center"
|
className={`group/seek relative flex w-full items-center ${theater ? 'h-9' : 'h-5'}`}
|
||||||
onMouseMove={handleSeekHover}
|
onMouseMove={handleSeekHover}
|
||||||
onMouseLeave={() => setSeekHover(null)}
|
onMouseLeave={() => setSeekHover(null)}
|
||||||
>
|
>
|
||||||
<div className={`${PLAYER_TRACK} group-hover/seek:h-[5px]`}>
|
<div className={`${PLAYER_TRACK} ${theater ? 'h-[5px]' : ''} group-hover/seek:h-[5px]`}>
|
||||||
<div
|
<div
|
||||||
className={`${PLAYER_TRACK_FILL} bg-white/30`}
|
className={`${PLAYER_TRACK_FILL} bg-white/30`}
|
||||||
style={{ width: `${bufferedRatio * 100}%` }}
|
style={{ width: `${bufferedRatio * 100}%` }}
|
||||||
@@ -1057,7 +1094,7 @@ export function PlayerControls({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none absolute h-3 w-3 -translate-x-1/2 rounded-full bg-white opacity-0 shadow-md transition-opacity duration-150 group-hover/seek:opacity-100"
|
className={`pointer-events-none absolute h-3 w-3 -translate-x-1/2 rounded-full bg-white shadow-md transition-opacity duration-150 group-hover/seek:opacity-100 ${theater ? 'opacity-100' : 'opacity-0'}`}
|
||||||
style={{ left: `${playedRatio * 100}%` }}
|
style={{ left: `${playedRatio * 100}%` }}
|
||||||
/>
|
/>
|
||||||
{tooltipVisible && durationSafe > 0 ? (
|
{tooltipVisible && durationSafe > 0 ? (
|
||||||
@@ -1085,14 +1122,14 @@ export function PlayerControls({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-0.5 flex items-center gap-0.5 sm:mt-1 sm:gap-1">
|
<div className={`flex items-center ${theater ? 'mt-1 gap-1' : 'mt-0.5 gap-0.5 sm:mt-1 sm:gap-1'}`}>
|
||||||
{/* 播放 / 暂停 */}
|
{/* 播放 / 暂停 */}
|
||||||
<button
|
<button
|
||||||
onClick={togglePlay}
|
onClick={togglePlay}
|
||||||
className={`${PLAYER_ICON_BUTTON} h-8 w-8 sm:h-9 sm:w-9`}
|
className={`${PLAYER_ICON_BUTTON} ${theater ? 'h-11 w-11' : 'h-8 w-8 sm:h-9 sm:w-9'}`}
|
||||||
title={playing ? '暂停 (Space)' : '播放 (Space)'}
|
title={playing ? '暂停 (Space)' : '播放 (Space)'}
|
||||||
>
|
>
|
||||||
{playing ? <Pause size={19} /> : <Play size={19} />}
|
{playing ? <Pause size={theater ? 24 : 19} /> : <Play size={theater ? 24 : 19} />}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 上一集 / 下一集:单条媒体(电影)没有上下集时整组隐藏,少两个死按钮 */}
|
{/* 上一集 / 下一集:单条媒体(电影)没有上下集时整组隐藏,少两个死按钮 */}
|
||||||
@@ -1101,7 +1138,7 @@ export function PlayerControls({
|
|||||||
<button
|
<button
|
||||||
onClick={onPrevEpisode}
|
onClick={onPrevEpisode}
|
||||||
disabled={!hasPrevEpisode}
|
disabled={!hasPrevEpisode}
|
||||||
className={PLAYER_ICON_BUTTON}
|
className={`${PLAYER_ICON_BUTTON} ${theater ? 'h-11 w-11' : ''}`}
|
||||||
title={
|
title={
|
||||||
hasPrevEpisode
|
hasPrevEpisode
|
||||||
? prevEpisodeTitle
|
? prevEpisodeTitle
|
||||||
@@ -1115,7 +1152,7 @@ export function PlayerControls({
|
|||||||
<button
|
<button
|
||||||
onClick={onNextEpisode}
|
onClick={onNextEpisode}
|
||||||
disabled={!hasNextEpisode}
|
disabled={!hasNextEpisode}
|
||||||
className={PLAYER_ICON_BUTTON}
|
className={`${PLAYER_ICON_BUTTON} ${theater ? 'h-11 w-11' : ''}`}
|
||||||
title={
|
title={
|
||||||
hasNextEpisode
|
hasNextEpisode
|
||||||
? nextEpisodeTitle
|
? nextEpisodeTitle
|
||||||
@@ -1223,7 +1260,7 @@ export function PlayerControls({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onOpenDanmaku}
|
onClick={onOpenDanmaku}
|
||||||
disabled={!onOpenDanmaku}
|
disabled={!onOpenDanmaku}
|
||||||
className={`${PLAYER_ICON_BUTTON} ${danmakuOpen ? 'bg-white/15' : ''}`}
|
className={`${PLAYER_ICON_BUTTON} ${theater ? 'h-11 w-11' : ''} ${danmakuOpen ? 'bg-white/15' : ''}`}
|
||||||
title={danmakuOpen ? '弹幕设置(已打开)' : '弹幕设置'}
|
title={danmakuOpen ? '弹幕设置(已打开)' : '弹幕设置'}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -1330,16 +1367,16 @@ export function PlayerControls({
|
|||||||
setSubtitleMenuOpen(false)
|
setSubtitleMenuOpen(false)
|
||||||
setSettingsMenuOpen((v) => !v)
|
setSettingsMenuOpen((v) => !v)
|
||||||
}}
|
}}
|
||||||
className={`${PLAYER_ICON_BUTTON} ${settingsMenuOpen ? 'bg-white/15 text-white' : ''}`}
|
className={`${PLAYER_ICON_BUTTON} ${theater ? 'h-11 w-11' : ''} ${settingsMenuOpen ? 'bg-white/15 text-white' : ''}`}
|
||||||
title="播放器设置"
|
title="播放器设置"
|
||||||
>
|
>
|
||||||
<Settings2 size={17} />
|
<Settings2 size={theater ? 21 : 17} />
|
||||||
</button>
|
</button>
|
||||||
{/* 识别到 VR 素材但还没进 VR 时,用一个小圆点提示设置里有东西可开 */}
|
{/* 识别到 VR 素材但还没进 VR 时,用一个小圆点提示设置里有东西可开 */}
|
||||||
{vr360Detected && !vr360 && (
|
{vr360Detected && !vr360 && (
|
||||||
<span className="pointer-events-none absolute right-0.5 top-0.5 h-1.5 w-1.5 rounded-full bg-amber-300" />
|
<span className="pointer-events-none absolute right-0.5 top-0.5 h-1.5 w-1.5 rounded-full bg-amber-300" />
|
||||||
)}
|
)}
|
||||||
{settingsMenuOpen && (
|
{settingsMenuOpen && !theater && (
|
||||||
<div className={`${PLAYER_POPOVER} absolute bottom-11 right-0 max-h-[70vh] w-[15rem] overflow-y-auto`}>
|
<div className={`${PLAYER_POPOVER} absolute bottom-11 right-0 max-h-[70vh] w-[15rem] overflow-y-auto`}>
|
||||||
{settingsPanel}
|
{settingsPanel}
|
||||||
</div>
|
</div>
|
||||||
@@ -1358,10 +1395,10 @@ export function PlayerControls({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={toggleFullscreen}
|
onClick={toggleFullscreen}
|
||||||
className={PLAYER_ICON_BUTTON}
|
className={`${PLAYER_ICON_BUTTON} ${theater ? 'h-11 w-11' : ''}`}
|
||||||
title={fullscreen ? '退出全屏 (F)' : '全屏 (F)'}
|
title={fullscreen ? '退出全屏 (F)' : '全屏 (F)'}
|
||||||
>
|
>
|
||||||
{fullscreen ? <Minimize size={17} /> : <Maximize size={17} />}
|
{fullscreen ? <Minimize size={theater ? 21 : 17} /> : <Maximize size={theater ? 21 : 17} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import { Check, ChevronDown, ChevronRight, Eye, EyeOff, Film, Hash, KeyRound, Loader2, MessageSquareText, RefreshCw, Search, Server, Settings2, Sparkles, Tag, X } from 'lucide-react'
|
import { Check, ChevronDown, ChevronRight, Eye, EyeOff, Film, Hash, KeyRound, Loader2, MessageSquareText, RefreshCw, Search, Server, Settings2, Sparkles, Tag, X } from 'lucide-react'
|
||||||
|
|
||||||
import type { DanmakuAnime, DanmakuEpisode, DanmakuLoadedInfo } from '../api/danmaku'
|
import type { DanmakuAnime, DanmakuEpisode, DanmakuLoadedInfo } from '../api/danmaku'
|
||||||
import { PLAYER_DRAWER, PLAYER_ICON_BUTTON, PLAYER_PANEL_HEADER } from './playerTheme'
|
import { PLAYER_DRAWER, PLAYER_ICON_BUTTON, PLAYER_PANEL_HEADER, PLAYER_SHEET, PLAYER_SHEET_BODY, PLAYER_SHEET_HEADER } from './playerTheme'
|
||||||
|
|
||||||
// PlayerDanmakuPanel — the on-player danmaku control panel. It displays
|
// PlayerDanmakuPanel — the on-player danmaku control panel. It displays
|
||||||
// the matched danmaku details (anime title, episode title, comment count,
|
// the matched danmaku details (anime title, episode title, comment count,
|
||||||
@@ -56,6 +56,11 @@ type PlayerDanmakuPanelProps = {
|
|||||||
danmakuInfo?: DanmakuLoadedInfo | null
|
danmakuInfo?: DanmakuLoadedInfo | null
|
||||||
onSelectEpisode: (episodeId: number, animeTitle: string, episodeTitle: string) => void
|
onSelectEpisode: (episodeId: number, animeTitle: string, episodeTitle: string) => void
|
||||||
onResetAuto: () => void
|
onResetAuto: () => void
|
||||||
|
/**
|
||||||
|
* 竖屏剧场模式:弹幕面板改为视频区底部的动作面板,而不是右侧抽屉,
|
||||||
|
* 窄屏下搜索框与滑杆不再被压扁。
|
||||||
|
*/
|
||||||
|
theater?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PlayerDanmakuPanel({
|
export function PlayerDanmakuPanel({
|
||||||
@@ -90,6 +95,7 @@ export function PlayerDanmakuPanel({
|
|||||||
danmakuInfo,
|
danmakuInfo,
|
||||||
onSelectEpisode,
|
onSelectEpisode,
|
||||||
onResetAuto,
|
onResetAuto,
|
||||||
|
theater = false,
|
||||||
}: PlayerDanmakuPanelProps) {
|
}: PlayerDanmakuPanelProps) {
|
||||||
const [draft, setDraft] = useState(search)
|
const [draft, setDraft] = useState(search)
|
||||||
const [advancedOpen, setAdvancedOpen] = useState(false)
|
const [advancedOpen, setAdvancedOpen] = useState(false)
|
||||||
@@ -201,9 +207,13 @@ export function PlayerDanmakuPanel({
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
onPointerUp={(e) => e.stopPropagation()}
|
onPointerUp={(e) => e.stopPropagation()}
|
||||||
className={`absolute inset-y-0 right-0 z-30 ${PLAYER_DRAWER}`}
|
className={
|
||||||
|
theater
|
||||||
|
? `absolute inset-x-0 bottom-0 z-40 ${PLAYER_SHEET}`
|
||||||
|
: `absolute inset-y-0 right-0 z-30 ${PLAYER_DRAWER}`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className={PLAYER_PANEL_HEADER}>
|
<div className={theater ? PLAYER_SHEET_HEADER : PLAYER_PANEL_HEADER}>
|
||||||
<div className="flex min-w-0 items-center gap-2 text-sm font-semibold">
|
<div className="flex min-w-0 items-center gap-2 text-sm font-semibold">
|
||||||
<MessageSquareText size={16} className="shrink-0 text-rose-400" />
|
<MessageSquareText size={16} className="shrink-0 text-rose-400" />
|
||||||
<span className="truncate">弹幕设置</span>
|
<span className="truncate">弹幕设置</span>
|
||||||
@@ -214,7 +224,7 @@ export function PlayerDanmakuPanel({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-3.5">
|
<div className={theater ? PLAYER_SHEET_BODY : 'min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-3.5'}>
|
||||||
{/* 弹幕总开关。从操作栏的「弹」按钮直接进来就到了这里,所以这一项就是
|
{/* 弹幕总开关。从操作栏的「弹」按钮直接进来就到了这里,所以这一项就是
|
||||||
用户找的「开关弹幕」;文案跟旧的按钮提示保持一致,避免换个说法让人找不到。 */}
|
用户找的「开关弹幕」;文案跟旧的按钮提示保持一致,避免换个说法让人找不到。 */}
|
||||||
<label className="mb-3 flex cursor-pointer items-center justify-between rounded-lg bg-white/5 px-2.5 py-2 text-sm transition hover:bg-white/10">
|
<label className="mb-3 flex cursor-pointer items-center justify-between rounded-lg bg-white/5 px-2.5 py-2 text-sm transition hover:bg-white/10">
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { Layers, ListVideo } from 'lucide-react'
|
||||||
|
|
||||||
|
import type { Media } from '../types'
|
||||||
|
import { mediaVersionLabel, mediaVersionMatches } from '../utils/mediaVersion'
|
||||||
|
|
||||||
|
// PlayerMobileTheaterInfo — 竖屏剧场模式下视频下方的可滚动内容区。
|
||||||
|
//
|
||||||
|
// 竖屏手机不再用全屏居中黑边布局:视频贴顶按 16:9 自适应高度,
|
||||||
|
// 下面这块区域接管标题、选集横滑、版本切换和简介,信息密度与
|
||||||
|
// 主流手机视频应用保持一致。桌面端与横屏手机不渲染它。
|
||||||
|
|
||||||
|
type PlayerMobileTheaterInfoProps = {
|
||||||
|
media: Media | null
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
playbackModeLabel?: string
|
||||||
|
qualityLabel?: string
|
||||||
|
episodes: Media[]
|
||||||
|
currentMediaId: string
|
||||||
|
currentEpisodeIndex: number
|
||||||
|
currentVersions?: Media[]
|
||||||
|
onSelectEpisode: (media: Media) => void
|
||||||
|
onSelectVersion?: (media: Media) => void
|
||||||
|
onOpenPlaylist?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function episodeShortLabel(ep: Media): string {
|
||||||
|
if (ep.episode_num > 0) return `${ep.episode_num}`
|
||||||
|
const title = ep.episode_title?.trim() || ep.title?.trim() || ''
|
||||||
|
return title ? title.slice(0, 4) : '·'
|
||||||
|
}
|
||||||
|
|
||||||
|
function episodeFullLabel(ep: Media): string {
|
||||||
|
if (ep.episode_num > 0) {
|
||||||
|
const title = ep.episode_title?.trim()
|
||||||
|
return title && title !== ep.title?.trim() ? `第 ${ep.episode_num} 集 · ${title}` : `第 ${ep.episode_num} 集`
|
||||||
|
}
|
||||||
|
return ep.episode_title?.trim() || ep.title?.trim() || '未命名'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PlayerMobileTheaterInfo({
|
||||||
|
media,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
playbackModeLabel,
|
||||||
|
qualityLabel,
|
||||||
|
episodes,
|
||||||
|
currentMediaId,
|
||||||
|
currentEpisodeIndex,
|
||||||
|
currentVersions = [],
|
||||||
|
onSelectEpisode,
|
||||||
|
onSelectVersion,
|
||||||
|
onOpenPlaylist,
|
||||||
|
}: PlayerMobileTheaterInfoProps) {
|
||||||
|
const hasEpisodeList = episodes.length > 1
|
||||||
|
const versionsToSwitch = currentVersions.length > 1 ? currentVersions : []
|
||||||
|
const overview = media?.overview?.trim() || ''
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain bg-black px-4 pb-8 pt-3 text-white">
|
||||||
|
<h1 className="text-[15px] font-semibold leading-snug text-white/95">{title || '加载中…'}</h1>
|
||||||
|
{subtitle ? <p className="mt-1 text-xs text-white/55">{subtitle}</p> : null}
|
||||||
|
{(playbackModeLabel || qualityLabel) && (
|
||||||
|
<div className="mt-2 flex flex-wrap items-center gap-1.5">
|
||||||
|
{qualityLabel ? (
|
||||||
|
<span className="rounded-md bg-white/10 px-2 py-0.5 text-[11px] font-medium text-white/80">
|
||||||
|
{qualityLabel}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{playbackModeLabel ? (
|
||||||
|
<span className="rounded-md bg-white/10 px-2 py-0.5 text-[11px] text-white/60">
|
||||||
|
{playbackModeLabel}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasEpisodeList ? (
|
||||||
|
<section className="mt-4">
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<h2 className="flex items-center gap-1.5 text-[13px] font-semibold text-white/90">
|
||||||
|
<ListVideo size={14} className="text-rose-400" />
|
||||||
|
选集
|
||||||
|
<span className="font-mono text-[11px] font-normal text-white/45">
|
||||||
|
{currentEpisodeIndex >= 0 ? `${currentEpisodeIndex + 1}/${episodes.length}` : `${episodes.length} 集`}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
{onOpenPlaylist ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onOpenPlaylist}
|
||||||
|
className="rounded-md px-2 py-1 text-[11px] text-white/55 transition hover:bg-white/10 hover:text-white"
|
||||||
|
>
|
||||||
|
全部 >
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="scrollbar-hide -mx-4 flex gap-1.5 overflow-x-auto px-4 pb-1">
|
||||||
|
{episodes.map((ep) => {
|
||||||
|
const active = mediaVersionMatches(ep, currentMediaId)
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={ep.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSelectEpisode(ep)}
|
||||||
|
title={episodeFullLabel(ep)}
|
||||||
|
className={`flex h-10 min-w-12 shrink-0 items-center justify-center rounded-lg px-2 text-xs font-medium tabular-nums transition ${
|
||||||
|
active
|
||||||
|
? 'bg-rose-500 text-white'
|
||||||
|
: 'bg-white/[0.07] text-white/80 active:bg-white/15'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{episodeShortLabel(ep)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{versionsToSwitch.length > 0 ? (
|
||||||
|
<section className="mt-4">
|
||||||
|
<h2 className="mb-2 flex items-center gap-1.5 text-[13px] font-semibold text-white/90">
|
||||||
|
<Layers size={13} className="text-rose-400" />
|
||||||
|
版本
|
||||||
|
<span className="font-mono text-[11px] font-normal text-white/45">
|
||||||
|
{versionsToSwitch.length} 个
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{versionsToSwitch.map((version) => {
|
||||||
|
const active = mediaVersionMatches(version, currentMediaId)
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={version.id}
|
||||||
|
type="button"
|
||||||
|
disabled={active}
|
||||||
|
onClick={() => onSelectVersion?.(version)}
|
||||||
|
className={`flex w-full items-center gap-2 rounded-lg border px-2.5 py-2 text-left transition disabled:cursor-default ${
|
||||||
|
active
|
||||||
|
? 'border-rose-500/50 bg-rose-500/15 text-white'
|
||||||
|
: 'border-white/5 bg-white/5 text-white/85 active:bg-white/10'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="min-w-0 flex-1 truncate text-xs font-medium">
|
||||||
|
{mediaVersionLabel(version)}
|
||||||
|
</span>
|
||||||
|
{active ? <span className="shrink-0 text-[10px] text-rose-300">播放中</span> : null}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{overview ? (
|
||||||
|
<section className="mt-4">
|
||||||
|
<h2 className="mb-1.5 text-[13px] font-semibold text-white/90">简介</h2>
|
||||||
|
<p className="text-xs leading-relaxed text-white/60">{overview}</p>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<p className="mt-5 text-center text-[10px] text-white/30">将手机横放可获得全屏沉浸式播放体验</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
mediaVersionMatches,
|
mediaVersionMatches,
|
||||||
mediaVersionSourceLabel,
|
mediaVersionSourceLabel,
|
||||||
} from '../utils/mediaVersion'
|
} from '../utils/mediaVersion'
|
||||||
import { PLAYER_DRAWER, PLAYER_ICON_BUTTON, PLAYER_PANEL_HEADER } from './playerTheme'
|
import { PLAYER_DRAWER, PLAYER_ICON_BUTTON, PLAYER_PANEL_HEADER, PLAYER_SHEET, PLAYER_SHEET_BODY, PLAYER_SHEET_HEADER } from './playerTheme'
|
||||||
|
|
||||||
export type SeasonGroup = {
|
export type SeasonGroup = {
|
||||||
season: number
|
season: number
|
||||||
@@ -27,6 +27,11 @@ type PlayerPlaylistPanelProps = {
|
|||||||
onSelectEpisode: (media: Media) => void
|
onSelectEpisode: (media: Media) => void
|
||||||
/** 切换到同一集/同一条目的另一个版本。 */
|
/** 切换到同一集/同一条目的另一个版本。 */
|
||||||
onSelectVersion?: (media: Media) => void
|
onSelectVersion?: (media: Media) => void
|
||||||
|
/**
|
||||||
|
* 竖屏剧场模式:选集面板改为视频区底部的动作面板,而不是右侧抽屉,
|
||||||
|
* 避免窄屏下抽屉遮挡返回按钮、宫格被压扁。
|
||||||
|
*/
|
||||||
|
theater?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,6 +55,7 @@ export function PlayerPlaylistPanel({
|
|||||||
episodes,
|
episodes,
|
||||||
onSelectEpisode,
|
onSelectEpisode,
|
||||||
onSelectVersion,
|
onSelectVersion,
|
||||||
|
theater = false,
|
||||||
}: PlayerPlaylistPanelProps) {
|
}: PlayerPlaylistPanelProps) {
|
||||||
const [filterText, setFilterText] = useState('')
|
const [filterText, setFilterText] = useState('')
|
||||||
/** 正在挑选版本的剧集 id;null = 宫格直接播放。 */
|
/** 正在挑选版本的剧集 id;null = 宫格直接播放。 */
|
||||||
@@ -133,9 +139,13 @@ export function PlayerPlaylistPanel({
|
|||||||
// 舞台用 pointerdown/pointerup 判断「移动端轻触画面」,抽屉上的触摸不该参与。
|
// 舞台用 pointerdown/pointerup 判断「移动端轻触画面」,抽屉上的触摸不该参与。
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
onPointerUp={(e) => e.stopPropagation()}
|
onPointerUp={(e) => e.stopPropagation()}
|
||||||
className={`absolute inset-y-0 right-0 z-30 ${PLAYER_DRAWER}`}
|
className={
|
||||||
|
theater
|
||||||
|
? `absolute inset-x-0 bottom-0 z-40 ${PLAYER_SHEET}`
|
||||||
|
: `absolute inset-y-0 right-0 z-30 ${PLAYER_DRAWER}`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className={PLAYER_PANEL_HEADER}>
|
<div className={theater ? PLAYER_SHEET_HEADER : PLAYER_PANEL_HEADER}>
|
||||||
<div className="flex min-w-0 items-center gap-2 text-sm font-semibold">
|
<div className="flex min-w-0 items-center gap-2 text-sm font-semibold">
|
||||||
<span className="truncate">{hasEpisodeList ? '选集' : '版本'}</span>
|
<span className="truncate">{hasEpisodeList ? '选集' : '版本'}</span>
|
||||||
<span className="shrink-0 font-mono text-[11px] font-normal text-white/45">
|
<span className="shrink-0 font-mono text-[11px] font-normal text-white/45">
|
||||||
@@ -147,6 +157,8 @@ export function PlayerPlaylistPanel({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={theater ? PLAYER_SHEET_BODY : 'flex min-h-0 flex-1 flex-col overflow-hidden'}>
|
||||||
|
|
||||||
{/* 当前条目的版本切换:多版本时置顶,随时可换 */}
|
{/* 当前条目的版本切换:多版本时置顶,随时可换 */}
|
||||||
{versionsToSwitch.length > 0 && (
|
{versionsToSwitch.length > 0 && (
|
||||||
<div className="shrink-0 border-b border-white/10 px-3 py-2.5">
|
<div className="shrink-0 border-b border-white/10 px-3 py-2.5">
|
||||||
@@ -255,9 +267,9 @@ export function PlayerPlaylistPanel({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 集数宫格 */}
|
{/* 集数宫格:剧场模式下底部面板只做单层滚动,宫格区不再自己滚动 */}
|
||||||
{hasEpisodeList ? (
|
{hasEpisodeList ? (
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto p-3 select-none">
|
<div className={theater ? 'p-3 select-none' : 'min-h-0 flex-1 overflow-y-auto p-3 select-none'}>
|
||||||
{filteredEpisodes.length === 0 ? (
|
{filteredEpisodes.length === 0 ? (
|
||||||
<div className="py-10 text-center text-xs text-white/40">
|
<div className="py-10 text-center text-xs text-white/40">
|
||||||
{filterText ? '未找到匹配的剧集' : '暂无剧集列表'}
|
{filterText ? '未找到匹配的剧集' : '暂无剧集列表'}
|
||||||
@@ -326,11 +338,12 @@ export function PlayerPlaylistPanel({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
versionsToSwitch.length === 0 && (
|
versionsToSwitch.length === 0 && (
|
||||||
<div className="flex flex-1 items-center justify-center px-4 text-center text-xs text-white/40">
|
<div className="flex items-center justify-center px-4 py-8 text-center text-xs text-white/40">
|
||||||
暂无可切换的内容
|
暂无可切换的内容
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,22 @@ export const PLAYER_POPOVER =
|
|||||||
export const PLAYER_DRAWER =
|
export const PLAYER_DRAWER =
|
||||||
`flex h-full w-[88%] flex-col border-white/10 ${PANEL_BG} text-white ${PANEL_SHADOW} backdrop-blur-xl sm:w-[380px] sm:border-l`
|
`flex h-full w-[88%] flex-col border-white/10 ${PANEL_BG} text-white ${PANEL_SHADOW} backdrop-blur-xl sm:w-[380px] sm:border-l`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 底部动作面板(竖屏剧场模式):贴着视频区底部向上弹出,内容最多占
|
||||||
|
* 视频区高度的 72%,留出一些画面可见;底部垫上安全区高度,避免
|
||||||
|
* iPhone 小横条遮挡最后一行选项。
|
||||||
|
*/
|
||||||
|
export const PLAYER_SHEET =
|
||||||
|
`z-40 flex max-h-[72%] min-h-[8rem] w-full flex-col overflow-hidden rounded-t-2xl border border-white/10 border-b-0 ${PANEL_BG} text-white ${PANEL_SHADOW} backdrop-blur-xl`
|
||||||
|
|
||||||
|
/** 底部动作面板的标题栏(带关闭按钮)。 */
|
||||||
|
export const PLAYER_SHEET_HEADER =
|
||||||
|
'flex shrink-0 items-center justify-between gap-2 border-b border-white/10 px-4 py-2.5'
|
||||||
|
|
||||||
|
/** 底部动作面板的可滚动内容区,底部垫上安全区高度。 */
|
||||||
|
export const PLAYER_SHEET_BODY =
|
||||||
|
'min-h-0 flex-1 overflow-y-auto overscroll-contain px-2 pb-[calc(0.75rem+env(safe-area-inset-bottom,0px))] pt-1'
|
||||||
|
|
||||||
/** 操作栏里的图标按钮。所有图标按钮共用同一尺寸与悬停反馈,排在一起才整齐。 */
|
/** 操作栏里的图标按钮。所有图标按钮共用同一尺寸与悬停反馈,排在一起才整齐。 */
|
||||||
export const PLAYER_ICON_BUTTON =
|
export const PLAYER_ICON_BUTTON =
|
||||||
'flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-white/85 transition hover:bg-white/15 hover:text-white disabled:cursor-not-allowed disabled:text-white/30 disabled:hover:bg-transparent sm:h-8 sm:w-8'
|
'flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-white/85 transition hover:bg-white/15 hover:text-white disabled:cursor-not-allowed disabled:text-white/30 disabled:hover:bg-transparent sm:h-8 sm:w-8'
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
const THEATER_MAX_WIDTH = 768
|
||||||
|
|
||||||
|
function queryTheater(): boolean {
|
||||||
|
if (typeof window === 'undefined') return false
|
||||||
|
// 窄屏 + 竖屏才进剧场模式:横屏手机自动回到全屏沉浸式布局。
|
||||||
|
return window.innerWidth < THEATER_MAX_WIDTH && window.innerHeight >= window.innerWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否使用移动端竖屏剧场布局。
|
||||||
|
*
|
||||||
|
* 桌面端与横屏手机保持原来的全屏居中播放器;只有竖屏手机进
|
||||||
|
* 「视频贴顶 + 下方内容区」的剧场模式,避免 16:9 视频在竖屏里
|
||||||
|
* 上下大黑边、控制栏远离画面的问题。
|
||||||
|
*/
|
||||||
|
export function useIsMobileTheater(): boolean {
|
||||||
|
const [theater, setTheater] = useState(queryTheater)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onChange = () => setTheater(queryTheater())
|
||||||
|
window.addEventListener('resize', onChange)
|
||||||
|
window.addEventListener('orientationchange', onChange)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', onChange)
|
||||||
|
window.removeEventListener('orientationchange', onChange)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return theater
|
||||||
|
}
|
||||||
@@ -39,6 +39,8 @@ import { classifyDirectPlayError } from './directPlayError'
|
|||||||
import { apiErrorMessage } from './StrmManagePage'
|
import { apiErrorMessage } from './StrmManagePage'
|
||||||
import { PlayerTopBar } from './PlayerTopBar'
|
import { PlayerTopBar } from './PlayerTopBar'
|
||||||
import { PlayerVideoStage } from './PlayerVideoStage'
|
import { PlayerVideoStage } from './PlayerVideoStage'
|
||||||
|
import { PlayerMobileTheaterInfo } from '../components/PlayerMobileTheaterInfo'
|
||||||
|
import { useIsMobileTheater } from '../hooks/useIsMobileTheater'
|
||||||
import { PlayerDanmakuPanel } from '../components/PlayerDanmakuPanel'
|
import { PlayerDanmakuPanel } from '../components/PlayerDanmakuPanel'
|
||||||
import { PlayerPlaylistPanel } from '../components/PlayerPlaylistPanel'
|
import { PlayerPlaylistPanel } from '../components/PlayerPlaylistPanel'
|
||||||
import {
|
import {
|
||||||
@@ -1655,6 +1657,10 @@ export function PlayerPage() {
|
|||||||
media?.title?.trim() ||
|
media?.title?.trim() ||
|
||||||
''
|
''
|
||||||
|
|
||||||
|
// 竖屏手机走剧场布局:视频贴顶按 16:9 自适应,下方是标题/选集/简介。
|
||||||
|
// 桌面端与横屏手机保持原来的全屏居中沉浸式布局。
|
||||||
|
const isMobileTheater = useIsMobileTheater()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-full flex-1 flex-col overflow-hidden bg-black">
|
<div className="relative flex h-full w-full flex-1 flex-col overflow-hidden bg-black">
|
||||||
<PlayerTopBar
|
<PlayerTopBar
|
||||||
@@ -1667,6 +1673,7 @@ export function PlayerPage() {
|
|||||||
onBack={goBack}
|
onBack={goBack}
|
||||||
/>
|
/>
|
||||||
<PlayerVideoStage
|
<PlayerVideoStage
|
||||||
|
theater={isMobileTheater}
|
||||||
media={media}
|
media={media}
|
||||||
loadError={loadError}
|
loadError={loadError}
|
||||||
playerError={playerError}
|
playerError={playerError}
|
||||||
@@ -1730,6 +1737,7 @@ export function PlayerPage() {
|
|||||||
onDismissVr360Guide={dismissVr360Guide}
|
onDismissVr360Guide={dismissVr360Guide}
|
||||||
playlistPanel={
|
playlistPanel={
|
||||||
<PlayerPlaylistPanel
|
<PlayerPlaylistPanel
|
||||||
|
theater={isMobileTheater}
|
||||||
open={playlistOpen}
|
open={playlistOpen}
|
||||||
onClose={() => setPlaylistOpen(false)}
|
onClose={() => setPlaylistOpen(false)}
|
||||||
currentMediaId={media?.id ?? ''}
|
currentMediaId={media?.id ?? ''}
|
||||||
@@ -1741,6 +1749,7 @@ export function PlayerPage() {
|
|||||||
}
|
}
|
||||||
danmakuPanel={
|
danmakuPanel={
|
||||||
<PlayerDanmakuPanel
|
<PlayerDanmakuPanel
|
||||||
|
theater={isMobileTheater}
|
||||||
open={danmakuOpen}
|
open={danmakuOpen}
|
||||||
onClose={() => setDanmakuOpen(false)}
|
onClose={() => setDanmakuOpen(false)}
|
||||||
enabled={danmakuEnabled}
|
enabled={danmakuEnabled}
|
||||||
@@ -1775,6 +1784,22 @@ export function PlayerPage() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{isMobileTheater ? (
|
||||||
|
<PlayerMobileTheaterInfo
|
||||||
|
media={media?.id === id ? media : null}
|
||||||
|
title={topBarTitle}
|
||||||
|
subtitle={topBarSubtitle}
|
||||||
|
playbackModeLabel={playbackModeLabel}
|
||||||
|
qualityLabel={showQuality ? qualityLabel : undefined}
|
||||||
|
episodes={playlistEpisodes}
|
||||||
|
currentMediaId={media?.id ?? ''}
|
||||||
|
currentEpisodeIndex={currentEpisodeIndex}
|
||||||
|
currentVersions={currentVersions}
|
||||||
|
onSelectEpisode={playEpisode}
|
||||||
|
onSelectVersion={switchVersion}
|
||||||
|
onOpenPlaylist={togglePlaylistOpen}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,11 @@ function buildSubtitleRenderGroups(
|
|||||||
|
|
||||||
type PlayerVideoStageProps = {
|
type PlayerVideoStageProps = {
|
||||||
media: Media | null
|
media: Media | null
|
||||||
|
/**
|
||||||
|
* 竖屏剧场模式:舞台不再全屏居中,而是贴顶按 16:9 自适应高度;
|
||||||
|
* 弹层改为底部动作面板,避免右侧抽屉与悬浮小弹层在窄屏下被裁剪。
|
||||||
|
*/
|
||||||
|
theater?: boolean
|
||||||
/** 媒体元数据加载失败提示(非空时替代「加载中」展示)。 */
|
/** 媒体元数据加载失败提示(非空时替代「加载中」展示)。 */
|
||||||
loadError?: string
|
loadError?: string
|
||||||
playerError: string
|
playerError: string
|
||||||
@@ -234,6 +239,7 @@ type PlayerVideoStageProps = {
|
|||||||
|
|
||||||
export function PlayerVideoStage({
|
export function PlayerVideoStage({
|
||||||
media,
|
media,
|
||||||
|
theater = false,
|
||||||
loadError,
|
loadError,
|
||||||
playerError,
|
playerError,
|
||||||
subs,
|
subs,
|
||||||
@@ -725,12 +731,22 @@ export function PlayerVideoStage({
|
|||||||
const assTrack = subtitleIndex >= 0 ? subs[subtitleIndex] : undefined
|
const assTrack = subtitleIndex >= 0 ? subs[subtitleIndex] : undefined
|
||||||
// VR 模式下画面由 WebGL 画布输出,舞台窗口本身就是「镜头」,
|
// VR 模式下画面由 WebGL 画布输出,舞台窗口本身就是「镜头」,
|
||||||
// 不再按视频宽高比留黑边。
|
// 不再按视频宽高比留黑边。
|
||||||
|
//
|
||||||
|
// 竖屏剧场模式(theater):舞台不再全屏居中,视频贴顶按容器宽度
|
||||||
|
// 16:9 自适应高度,下方留给标题/选集/简介内容区。stage 本体的
|
||||||
|
// 高度只包住视频区,控制栏与弹层都相对这块区域定位。
|
||||||
const wrapperStyle = vr360
|
const wrapperStyle = vr360
|
||||||
? {
|
? {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
}
|
}
|
||||||
: videoRatio
|
: theater
|
||||||
|
? {
|
||||||
|
aspectRatio: '16 / 9',
|
||||||
|
width: '100%',
|
||||||
|
maxHeight: '100%',
|
||||||
|
}
|
||||||
|
: videoRatio
|
||||||
? {
|
? {
|
||||||
aspectRatio: `${videoRatio}`,
|
aspectRatio: `${videoRatio}`,
|
||||||
width: isWiderThanStage ? '100%' : 'auto',
|
width: isWiderThanStage ? '100%' : 'auto',
|
||||||
@@ -747,7 +763,11 @@ export function PlayerVideoStage({
|
|||||||
<div
|
<div
|
||||||
ref={stageRef}
|
ref={stageRef}
|
||||||
data-player-stage
|
data-player-stage
|
||||||
className="relative flex h-full w-full flex-1 items-center justify-center overflow-hidden bg-black"
|
className={
|
||||||
|
theater
|
||||||
|
? 'relative flex w-full shrink-0 items-start justify-center overflow-hidden bg-black'
|
||||||
|
: 'relative flex h-full w-full flex-1 items-center justify-center overflow-hidden bg-black'
|
||||||
|
}
|
||||||
onPointerDown={handleStagePointerDown}
|
onPointerDown={handleStagePointerDown}
|
||||||
onPointerMove={handleStagePointerMove}
|
onPointerMove={handleStagePointerMove}
|
||||||
onMouseLeave={() => setLockHovered(false)}
|
onMouseLeave={() => setLockHovered(false)}
|
||||||
@@ -851,6 +871,7 @@ export function PlayerVideoStage({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<PlayerControls
|
<PlayerControls
|
||||||
|
theater={theater}
|
||||||
videoRef={videoRef}
|
videoRef={videoRef}
|
||||||
volume={playerVolume}
|
volume={playerVolume}
|
||||||
onVolumeChange={onPlayerVolumeChange}
|
onVolumeChange={onPlayerVolumeChange}
|
||||||
|
|||||||
Reference in New Issue
Block a user