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
@@ -1,9 +1,10 @@
|
||||
// Author: fruit.com.cn
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Bookmark, MessageCircle, Share2, ThumbsUp } from 'lucide-react';
|
||||
import { useLocale } from '../../app/providers/AppProviders';
|
||||
import { CommentsSheet } from '../../shared/components/CommentsSheet';
|
||||
import { HeroTransition } from '../../shared/components/HeroTransition';
|
||||
import { VideoPlayer } from '../../shared/components/player/VideoPlayer';
|
||||
import { WatchPageSkeleton } from '../../shared/components/skeletons';
|
||||
import { useEngagement } from '../../shared/hooks/useEngagement';
|
||||
@@ -11,6 +12,7 @@ import { useFollowedChannels } from '../../shared/hooks/useFollowedChannels';
|
||||
import { useRequest } from '../../shared/hooks/useRequest';
|
||||
import { useToast } from '../../shared/hooks/useToast';
|
||||
import { formatCount, formatViews } from '../../shared/lib/format';
|
||||
import { getHeroTransition } from '../../shared/lib/heroTransition';
|
||||
import { selectPlaybackUrl } from '../../shared/lib/media';
|
||||
import { reportWatchProgress } from '../../shared/lib/watchProgress';
|
||||
import { normalizeDrama } from '../../entities/drama/model';
|
||||
@@ -56,6 +58,11 @@ function WatchContent({ video }) {
|
||||
const swipeStart = useRef(null);
|
||||
const playerRef = useRef(null);
|
||||
const wheelRef = useRef({ accum: 0, locked: false, lastAt: 0, lastAbs: 0 });
|
||||
const endedTimerRef = useRef(null);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (endedTimerRef.current) window.clearTimeout(endedTimerRef.current);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
engagement.syncStats(stats.data);
|
||||
@@ -64,11 +71,16 @@ function WatchContent({ video }) {
|
||||
|
||||
const changeEpisode = (episode, scroll = false) => {
|
||||
if (!episode || episode.id === currentEpisode?.id) return;
|
||||
if (endedTimerRef.current) window.clearTimeout(endedTimerRef.current);
|
||||
setEpisodeId(episode.id);
|
||||
if (scroll) playerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
};
|
||||
const goNext = () => changeEpisode(nextEpisode);
|
||||
const goPrevious = () => changeEpisode(previousEpisode);
|
||||
const handleEnded = () => {
|
||||
if (!nextEpisode || nextEpisode.locked) return;
|
||||
endedTimerRef.current = window.setTimeout(() => changeEpisode(nextEpisode), 320);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 捕获阶段接管画面内的滚轮,任何幅度都不交给页面滚动
|
||||
@@ -153,7 +165,7 @@ function WatchContent({ video }) {
|
||||
<div ref={playerRef} className="watch-stage" style={{ '--watch-poster': `url("${video.image}")` }}>
|
||||
<div className="watch-player" onTouchStart={onTouchStart} onTouchEnd={onTouchEnd}>
|
||||
{src
|
||||
? <VideoPlayer key={currentEpisode.id} src={src} poster={video.image} autoPlay onProgress={(position, duration, flush) => reportWatchProgress(currentEpisode.id, position, duration, 'watch', flush)} />
|
||||
? <VideoPlayer key={currentEpisode.id} src={src} poster={video.image} autoPlay soundHint={locale.soundHint} onEnded={handleEnded} onProgress={(position, duration, flush) => reportWatchProgress(currentEpisode.id, position, duration, 'watch', flush)} />
|
||||
: <div className="watch-player-placeholder"><img src={video.image} alt={video.title} /><span>{currentEpisode.locked ? locale.lockedEpisode : playback.loading ? locale.loading : playback.error?.message || locale.playbackUnavailable}</span></div>}
|
||||
<span className="watch-swipe-tip">{locale.episodeSwipeHint}</span>
|
||||
<div className="watch-arrows">
|
||||
@@ -198,6 +210,8 @@ function WatchContent({ video }) {
|
||||
export function WatchPage() {
|
||||
const { id } = useParams();
|
||||
const { locale } = useLocale();
|
||||
const [hero, setHero] = useState(() => getHeroTransition());
|
||||
const clearHero = useCallback(() => setHero(null), []);
|
||||
const drama = useRequest(
|
||||
({ signal }) => dramaApi.get(id, { signal }).then((data) => normalizeDrama(data, locale.code)),
|
||||
[id, locale.code],
|
||||
@@ -209,8 +223,12 @@ export function WatchPage() {
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [id]);
|
||||
|
||||
if (!drama.data && drama.loading) return <WatchPageSkeleton label={locale.loading} />;
|
||||
if (!drama.data) return <div className="api-note warning">{drama.error?.message || locale.empty}</div>;
|
||||
|
||||
return <WatchContent key={drama.data.id} video={drama.data} />;
|
||||
return (
|
||||
<>
|
||||
<HeroTransition data={hero} onDone={clearHero} />
|
||||
{!drama.data && drama.loading && <WatchPageSkeleton label={locale.loading} />}
|
||||
{!drama.data && !drama.loading && <div className="api-note warning">{drama.error?.message || locale.empty}</div>}
|
||||
{drama.data && <WatchContent key={drama.data.id} video={drama.data} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user