mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
fix: keep mobile fullscreen controls visible
This commit is contained in:
@@ -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
@@ -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",
|
||||
|
||||
@@ -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)'}
|
||||
>
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -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",
|
||||
|
||||
@@ -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;/);
|
||||
});
|
||||
Reference in New Issue
Block a user