feat: 移动端沉浸式 Shorts 与播放器/下载徽章体验优化
- 移动端移除底部导航,Shorts 视频改为圆角卡片并适配 iOS 裁剪 - 播放进度条全局贴到视频最底,移动端精简控件(仅播放/全屏) - 右侧互动栏下移,首页进入播放页增加模糊封面过渡 - 关于页下载入口改为标准 App Store / Google Play 徽章 - 新增自动连播下一集与自动播放被拦截时的开启声音提示
This commit is contained in:
1 parent
6fd936ac97
commit
2d1f754a46
11 files changed
+267
-54
No files matched your search
@@ -45,7 +45,7 @@ function formatTime(seconds) {
|
||||
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function VideoPlayer({ src, poster, autoPlay = true, muted = true, onProgress }) {
|
||||
export function VideoPlayer({ src, poster, autoPlay = true, muted = true, soundHint = 'Tap for sound', onProgress, onEnded }) {
|
||||
const wrapRef = useRef(null);
|
||||
const videoRef = useRef(null);
|
||||
const hideTimerRef = useRef(null);
|
||||
@@ -58,6 +58,7 @@ export function VideoPlayer({ src, poster, autoPlay = true, muted = true, onProg
|
||||
const [reloadKey, setReloadKey] = useState(0);
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const [isMuted, setIsMuted] = useState(() => (soundEnabled ? false : muted));
|
||||
const [soundBlocked, setSoundBlocked] = useState(false);
|
||||
const [volume, setVolume] = useState(1);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
@@ -104,6 +105,7 @@ export function VideoPlayer({ src, poster, autoPlay = true, muted = true, onProg
|
||||
setReady(false);
|
||||
setError('');
|
||||
setPlaying(false);
|
||||
setSoundBlocked(false);
|
||||
setCurrentTime(0);
|
||||
setBuffered(0);
|
||||
const hls = new Hls({ enableWorker: true });
|
||||
@@ -157,10 +159,11 @@ export function VideoPlayer({ src, poster, autoPlay = true, muted = true, onProg
|
||||
setIsMuted(!wantSound);
|
||||
video.play().catch((playError) => {
|
||||
if (playError.name === 'AbortError') return;
|
||||
// 浏览器拦截带声音的自动播放时,降级为静音播放
|
||||
// 浏览器拦截带声音的自动播放时,降级为静音播放并提示用户手动开启
|
||||
if (playError.name === 'NotAllowedError' && wantSound) {
|
||||
video.muted = true;
|
||||
setIsMuted(true);
|
||||
setSoundBlocked(true);
|
||||
video.play().catch(() => {});
|
||||
return;
|
||||
}
|
||||
@@ -218,6 +221,16 @@ export function VideoPlayer({ src, poster, autoPlay = true, muted = true, onProg
|
||||
seekToRatio((event.clientX - rect.left) / rect.width);
|
||||
};
|
||||
|
||||
const enableSound = () => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
video.muted = false;
|
||||
setIsMuted(false);
|
||||
setSoundBlocked(false);
|
||||
persistSoundPreference(true);
|
||||
video.play().catch(() => {});
|
||||
};
|
||||
|
||||
const toggleMute = () => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
@@ -294,6 +307,7 @@ export function VideoPlayer({ src, poster, autoPlay = true, muted = true, onProg
|
||||
onPause={() => { setPlaying(false); setControlsVisible(true); }}
|
||||
onWaiting={() => setReady(false)}
|
||||
onPlaying={() => setReady(true)}
|
||||
onEnded={onEnded}
|
||||
onLoadedMetadata={(event) => setDuration(event.currentTarget.duration || 0)}
|
||||
onTimeUpdate={(event) => {
|
||||
const video = event.currentTarget;
|
||||
@@ -307,6 +321,13 @@ export function VideoPlayer({ src, poster, autoPlay = true, muted = true, onProg
|
||||
|
||||
{!ready && !error && <span className="vp-spinner" aria-hidden="true" />}
|
||||
|
||||
{soundBlocked && !error && (
|
||||
<button className="vp-sound-hint" type="button" onClick={enableSound}>
|
||||
<VolumeX size={15} />
|
||||
<span>{soundHint}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<button
|
||||
className="vp-error"
|
||||
|
||||
Reference in new issue
Block a user