fix: keep mobile fullscreen controls visible

This commit is contained in:
kuekhaoyang
2026-07-29 22:36:05 +08:00
parent 46f6eee2a6
commit 0294f59602
9 changed files with 137 additions and 25 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## 4.9.18 - 2026-07-29
- 修复移动端播放器控制栏在窄屏下被裁切、最右侧“系统全屏”按钮不可见的问题(#224)。
- 控制栏现在根据播放器自身宽度进入紧凑布局:隐藏窄屏音量入口、压缩间距,并在极窄播放器中只显示当前播放时间;画中画、AirPlay、Cast、网页全屏与系统全屏按钮继续保留。
- 新增移动端播放器控制栏回归测试,锁定两个全屏入口在窄屏下不会再次被可选播放按钮挤出可视区。
## 4.9.17 - 2026-07-29
- 新增基于主内容时长残差的组合式 M3U8 插播检测;残差只作为辅助证据,单个 `.033/.333/.867` 等合法分片不再被直接删除。
+11 -1
View File
@@ -4,8 +4,18 @@
"name": "KVideo",
"branch": "main"
},
"currentVersion": "4.9.17",
"currentVersion": "4.9.18",
"releases": [
{
"version": "4.9.18",
"publishedAt": "2026-07-29",
"title": "修复移动端系统全屏按钮",
"notes": [
"修复移动端播放器控制栏在窄屏下被裁切、最右侧系统全屏按钮不可见的问题(#224)。",
"控制栏按播放器自身宽度切换紧凑布局,窄屏隐藏音量入口并压缩间距,极窄播放器只显示当前播放时间。",
"画中画、AirPlay、Cast、网页全屏与系统全屏按钮全部保留,并新增响应式控制栏回归测试。"
]
},
{
"version": "4.9.17",
"publishedAt": "2026-07-29",
+55
View File
@@ -1,4 +1,8 @@
/* Video Player Components */
.kvideo-container {
container-type: inline-size;
}
.spinner {
width: 48px;
height: 48px;
@@ -435,3 +439,54 @@
cursor: pointer;
touch-action: manipulation;
}
/*
* Keep fullscreen actions visible when the player itself becomes narrow.
* Container queries are used instead of viewport queries because the player
* can also be narrow inside the desktop player/sidebar grid.
*/
@container (max-width: 36rem) {
.kvideo-container .player-controls-bar {
padding: 0.5rem;
}
.kvideo-container .player-controls-row {
gap: 0.25rem;
}
.kvideo-container .player-controls-left {
gap: 0.375rem;
}
.kvideo-container .player-volume-control {
display: none;
}
.kvideo-container .player-controls-right {
flex: 0 0 auto;
gap: 0.25rem;
}
.kvideo-container .player-time-display {
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.75rem;
}
}
@container (max-width: 24rem) {
.kvideo-container .player-controls-row {
gap: 0.125rem;
}
.kvideo-container .player-controls-left,
.kvideo-container .player-controls-right {
gap: 0.25rem;
}
.kvideo-container .player-duration-display {
display: none;
}
}
@@ -73,8 +73,8 @@ export function DesktopControls(props: DesktopControlsProps) {
/>
{/* Controls Bar */}
<div className="bg-gradient-to-t from-black/90 via-black/70 to-transparent px-4 pb-4 pt-2">
<div className="flex items-center justify-between gap-4">
<div className="player-controls-bar bg-gradient-to-t from-black/90 via-black/70 to-transparent px-4 pb-4 pt-2">
<div className="player-controls-row flex min-w-0 items-center justify-between gap-4">
<DesktopLeftControls {...props} formatTime={formatTime} />
<DesktopRightControls {...props} />
</div>
@@ -32,30 +32,33 @@ export function DesktopLeftControls({
formatTime
}: DesktopLeftControlsProps) {
return (
<div className="flex items-center gap-3">
<div className="player-controls-left flex min-w-0 flex-1 items-center gap-3">
{/* Play/Pause */}
<button
onClick={onTogglePlay}
className="btn-icon"
className="btn-icon shrink-0"
aria-label={isPlaying ? 'Pause' : 'Play'}
>
{isPlaying ? <Icons.Pause size={20} /> : <Icons.Play size={20} />}
</button>
{/* Volume */}
<DesktopVolumeControl
volumeBarRef={volumeBarRef}
volume={volume}
isMuted={isMuted}
showVolumeBar={showVolumeBar}
onToggleMute={onToggleMute}
onVolumeChange={onVolumeChange}
onVolumeMouseDown={onVolumeMouseDown}
/>
<div className="player-volume-control shrink-0">
<DesktopVolumeControl
volumeBarRef={volumeBarRef}
volume={volume}
isMuted={isMuted}
showVolumeBar={showVolumeBar}
onToggleMute={onToggleMute}
onVolumeChange={onVolumeChange}
onVolumeMouseDown={onVolumeMouseDown}
/>
</div>
{/* Time */}
<span className="text-white text-sm font-medium tabular-nums">
{formatTime(currentTime)} / {formatTime(duration)}
<span className="player-time-display min-w-0 truncate text-sm font-medium text-white tabular-nums">
<span>{formatTime(currentTime)}</span>
<span className="player-duration-display"> / {formatTime(duration)}</span>
</span>
</div>
);
@@ -29,13 +29,13 @@ export function DesktopRightControls({
onShowCastMenu
}: DesktopRightControlsProps) {
return (
<div className="relative z-50 flex items-center gap-3">
<div className="player-controls-right relative z-50 flex shrink-0 items-center gap-3">
{/* Picture-in-Picture */}
{
isPiPSupported && (
<button
onClick={onTogglePictureInPicture}
className="btn-icon"
className="btn-icon shrink-0"
aria-label="画中画"
title="画中画"
>
@@ -49,7 +49,7 @@ export function DesktopRightControls({
isAirPlaySupported && (
<button
onClick={onShowAirPlayMenu}
className="btn-icon"
className="btn-icon shrink-0"
aria-label="隔空播放"
title="隔空播放"
>
@@ -63,7 +63,7 @@ export function DesktopRightControls({
isCastAvailable && (
<button
onClick={onShowCastMenu}
className="btn-icon"
className="btn-icon shrink-0"
aria-label="投屏"
title="投屏"
>
@@ -75,7 +75,7 @@ export function DesktopRightControls({
{/* Web Fullscreen */}
<button
onClick={onToggleWebFullscreen}
className="btn-icon"
className="btn-icon shrink-0"
aria-label={isWebFullscreen ? '退出网页全屏' : '网页全屏'}
title={isWebFullscreen ? '退出网页全屏 (W)' : '网页全屏 (W)'}
>
@@ -87,7 +87,7 @@ export function DesktopRightControls({
{/* Native Fullscreen */}
<button
onClick={onToggleNativeFullscreen}
className="btn-icon"
className="btn-icon shrink-0"
aria-label={isNativeFullscreen ? '退出系统全屏' : '系统全屏'}
title={isNativeFullscreen ? '退出系统全屏 (F)' : '系统全屏 (F)'}
>
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "kvideo",
"version": "4.9.17",
"version": "4.9.18",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "kvideo",
"version": "4.9.17",
"version": "4.9.18",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "kvideo",
"version": "4.9.17",
"version": "4.9.18",
"private": true,
"scripts": {
"dev": "node scripts/next-with-lan-access.mjs dev",
+38
View File
@@ -0,0 +1,38 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
const projectRoot = process.cwd();
test('narrow player controls reserve space for both fullscreen actions', () => {
const controls = readFileSync(
join(projectRoot, 'components/player/desktop/DesktopControls.tsx'),
'utf8',
);
const leftControls = readFileSync(
join(projectRoot, 'components/player/desktop/DesktopLeftControls.tsx'),
'utf8',
);
const rightControls = readFileSync(
join(projectRoot, 'components/player/desktop/DesktopRightControls.tsx'),
'utf8',
);
const styles = readFileSync(
join(projectRoot, 'app/styles/video-player.css'),
'utf8',
);
assert.match(controls, /player-controls-row flex min-w-0/);
assert.match(leftControls, /player-controls-left flex min-w-0 flex-1/);
assert.match(leftControls, /player-volume-control/);
assert.match(leftControls, /player-duration-display/);
assert.match(rightControls, /player-controls-right[^\n]*shrink-0/);
assert.match(rightControls, /onClick=\{onToggleWebFullscreen\}/);
assert.match(rightControls, /onClick=\{onToggleNativeFullscreen\}/);
assert.match(styles, /\.kvideo-container\s*\{\s*container-type:\s*inline-size;/);
assert.match(styles, /@container \(max-width: 36rem\)[\s\S]*?\.player-volume-control\s*\{\s*display:\s*none;/);
assert.match(styles, /@container \(max-width: 36rem\)[\s\S]*?\.player-controls-right\s*\{[\s\S]*?flex:\s*0 0 auto;/);
assert.match(styles, /@container \(max-width: 24rem\)[\s\S]*?\.player-duration-display\s*\{\s*display:\s*none;/);
});