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,5 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
import { ArrowRight, Clapperboard, Cpu, Play, Smartphone, Sparkles, Users } from 'lucide-react';
|
||||
import { ArrowRight, Clapperboard, Cpu, Play, Sparkles, Users } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useLocale } from '../../app/providers/AppProviders';
|
||||
import { aboutContent } from './content';
|
||||
@@ -12,6 +12,25 @@ const PILLARS = [
|
||||
{ key: 'compute', icon: Cpu },
|
||||
];
|
||||
|
||||
function AppleLogo() {
|
||||
return (
|
||||
<svg width="22" height="24" viewBox="0 0 384 512" fill="currentColor" aria-hidden="true">
|
||||
<path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function GooglePlayLogo() {
|
||||
return (
|
||||
<svg width="22" height="24" viewBox="0 0 512 512" aria-hidden="true">
|
||||
<path fill="#00A0FF" d="M64 40v432l248-216z" />
|
||||
<path fill="#00E676" d="M64 40l248 216 80-80z" />
|
||||
<path fill="#FF3A44" d="M64 472l248-216 80 80z" />
|
||||
<path fill="#FFCE00" d="M312 256l80-80 56 56c14 14 14 34 0 48l-56 56z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function AboutPage() {
|
||||
const { locale } = useLocale();
|
||||
const copy = aboutContent[locale.code] || aboutContent.en;
|
||||
@@ -30,13 +49,13 @@ export function AboutPage() {
|
||||
<div className="about-stores">
|
||||
<span className="about-stores-label">{copy.downloadLabel}</span>
|
||||
<div className="about-stores-row">
|
||||
<a className="about-store" href={APP_DOWNLOAD_URL} target="_blank" rel="noopener noreferrer">
|
||||
<Smartphone size={20} />
|
||||
<span><small>{copy.iosSmall}</small><b>App Store</b></span>
|
||||
<a className="store-badge" href={APP_DOWNLOAD_URL} target="_blank" rel="noopener noreferrer">
|
||||
<AppleLogo />
|
||||
<span className="store-badge-copy"><small>{copy.iosSmall}</small><b>App Store</b></span>
|
||||
</a>
|
||||
<a className="about-store" href={APP_DOWNLOAD_URL} target="_blank" rel="noopener noreferrer">
|
||||
<Play size={20} fill="currentColor" />
|
||||
<span><small>{copy.androidSmall}</small><b>Google Play</b></span>
|
||||
<a className="store-badge" href={APP_DOWNLOAD_URL} target="_blank" rel="noopener noreferrer">
|
||||
<GooglePlayLogo />
|
||||
<span className="store-badge-copy"><small>{copy.androidSmall}</small><b>Google Play</b></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ export function ShortSlide({ video, active, followed, onToggleFollow, onOpenComm
|
||||
<div className="short-player-wrap">
|
||||
<div className="short-slide-player">
|
||||
{src
|
||||
? <VideoPlayer src={src} poster={video.image} autoPlay={active} onProgress={(position, duration, flush) => reportWatchProgress(video.preview?.id, position, duration, 'shorts', flush)} />
|
||||
? <VideoPlayer src={src} poster={video.image} autoPlay={active} soundHint={locale.soundHint} onProgress={(position, duration, flush) => reportWatchProgress(video.preview?.id, position, duration, 'shorts', flush)} />
|
||||
: <div className="short-slide-placeholder"><img src={video.image} alt={video.title} /><span>{video.title}</span></div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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