feat: 移动端沉浸式 Shorts 与播放器/下载徽章体验优化

- 移动端移除底部导航,Shorts 视频改为圆角卡片并适配 iOS 裁剪
- 播放进度条全局贴到视频最底,移动端精简控件(仅播放/全屏)
- 右侧互动栏下移,首页进入播放页增加模糊封面过渡
- 关于页下载入口改为标准 App Store / Google Play 徽章
- 新增自动连播下一集与自动播放被拦截时的开启声音提示
This commit is contained in:
Zhou Weidong committed 2026-11-12 15:14:09 +08:00
1 parent 6fd936ac97
commit 2d1f754a46
11 files changed
+267 -54

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
// Author: fruit.com.cn
import { useLayoutEffect, useRef } from 'react';
const DURATION = 420;
const FADE = 180;
export function HeroTransition({ data, onDone }) {
const ref = useRef(null);
useLayoutEffect(() => {
if (!data) return undefined;
const node = ref.current;
if (!node) return undefined;
if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
onDone();
return undefined;
}
const rect = data;
const scale = Math.max(window.innerWidth / rect.width, window.innerHeight / rect.height) * 1.04;
const tx = (window.innerWidth - rect.width * scale) / 2 - rect.left;
const ty = (window.innerHeight - rect.height * scale) / 2 - rect.top;
const expand = node.animate(
[
{ transform: 'translate(0, 0) scale(1)', borderRadius: '12px' },
{ transform: `translate(${tx}px, ${ty}px) scale(${scale})`, borderRadius: '0px' },
],
{ duration: DURATION, easing: 'cubic-bezier(.2, .7, .3, 1)', fill: 'forwards' },
);
expand.onfinish = () => {
const fade = node.animate([{ opacity: 1 }, { opacity: 0 }], { duration: FADE, fill: 'forwards' });
fade.onfinish = onDone;
};
return () => expand.cancel();
}, [data, onDone]);
if (!data) return null;
const rect = data;
return (
<div
ref={ref}
className="hero-transition"
aria-hidden="true"
style={{ left: rect.left, top: rect.top, width: rect.width, height: rect.height }}
>
{data.src && <img src={data.src} alt="" />}
</div>
);
}
+10 -1
View File
@@ -3,11 +3,20 @@ import { MoreVertical, Play } from 'lucide-react';
import { Link } from 'react-router-dom';
import { useLocale } from '../../app/providers/AppProviders';
import { daysAgoLabel, formatViews } from '../lib/format';
import { setHeroTransition } from '../lib/heroTransition';
export function MediaCard({ video, compact = false, to }) {
const { locale } = useLocale();
const image = video.image?.startsWith('/') || video.image?.startsWith('http') ? video.image : `/assets/${video.image}`;
const target = to || `/watch/${video.id}`;
const meta = [formatViews(video.views, locale.code), daysAgoLabel(locale.code)].filter(Boolean).join(' · ');
return <article className={`media-card ${compact ? 'compact' : ''}`}><Link to={target} className="media-thumb"><img src={image} alt={video.title} loading="lazy" /><b>{video.duration}</b><span className="thumb-play"><Play size={16} fill="currentColor" /></span></Link>{compact ? <Link to={target} className="short-media-title">{video.title}</Link> : <div className="media-meta"><span className="channel-avatar">{video.channel?.[0] || 'Y'}</span><div className="media-copy"><Link to={target} className="media-title">{video.title}</Link><small>{video.channel}<br />{meta}</small></div><button className="media-more" type="button" aria-label="More actions"><MoreVertical size={18} /></button></div>}</article>;
const rememberRect = (event) => {
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
const thumb = event.currentTarget.querySelector('.media-thumb');
const img = thumb?.querySelector('img');
if (!thumb || !img) return;
const rect = thumb.getBoundingClientRect();
setHeroTransition({ left: rect.left, top: rect.top, width: rect.width, height: rect.height, src: img.currentSrc || img.src });
};
return <article className={`media-card ${compact ? 'compact' : ''}`} onClickCapture={rememberRect}><Link to={target} className="media-thumb"><img src={image} alt={video.title} loading="lazy" /><b>{video.duration}</b><span className="thumb-play"><Play size={16} fill="currentColor" /></span></Link>{compact ? <Link to={target} className="short-media-title">{video.title}</Link> : <div className="media-meta"><span className="channel-avatar">{video.channel?.[0] || 'Y'}</span><div className="media-copy"><Link to={target} className="media-title">{video.title}</Link><small>{video.channel}<br />{meta}</small></div><button className="media-more" type="button" aria-label="More actions"><MoreVertical size={18} /></button></div>}</article>;
}
+23 -2
View File
@@ -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"
+16
View File
@@ -0,0 +1,16 @@
// Author: fruit.com.cn
// 记录用户点击卡片的封面与位置,用于进入播放页时的放大过渡
let pending = null;
export function setHeroTransition(data) {
pending = data;
}
export function getHeroTransition() {
const data = pending;
if (!data) return null;
window.setTimeout(() => {
if (pending === data) pending = null;
}, 1200);
return data;
}
+114 -30
View File
@@ -140,7 +140,8 @@ body { background: var(--bg); color: var(--text); font-family: Roboto, Arial, He
.short-column { display: flex; flex-direction: column; justify-content: center; width: 100%; height: 100%; }
.short-player-wrap { position: relative; flex: 1; min-height: 0; }
.short-player-wrap::after { content: ''; position: absolute; inset: auto 0 0; display: none; height: 32%; background: linear-gradient(transparent, #000b); pointer-events: none; }
.short-slide-player { width: 100%; height: 100%; }
.short-slide-player { width: 100%; height: 100%; overflow: hidden; border-radius: 16px; background: #000; transform: translateZ(0); isolation: isolate; }
.short-slide-player .vp, .short-slide-player .video-player { border-radius: inherit; }
.short-slide-player .video-player { width: 100%; height: 100%; object-fit: contain; }
.short-slide-placeholder { position: relative; display: grid; place-items: center; width: 100%; height: 100%; }
.short-slide-placeholder img { width: 100%; height: 100%; object-fit: cover; opacity: .4; }
@@ -187,7 +188,8 @@ body { background: var(--bg); color: var(--text); font-family: Roboto, Arial, He
.watch-stage { position: relative; display: grid; place-items: center; padding: 22px 20px; overflow: hidden; border-radius: 24px; background: radial-gradient(120% 90% at 50% 0%, #1d1e25, #09090b 72%); box-shadow: 0 30px 80px #0006, inset 0 1px 0 #ffffff0d; }
.watch-stage::before { content: ''; position: absolute; inset: -35%; background-image: var(--watch-poster); background-position: center; background-size: cover; filter: blur(56px) saturate(1.4); opacity: .4; transform: scale(1.15); pointer-events: none; }
.watch-stage::after { content: ''; position: absolute; inset: 0; background: radial-gradient(72% 72% at 50% 46%, #00000000 20%, #08080ae0 92%); pointer-events: none; }
.watch-player { position: relative; z-index: 1; width: auto; max-width: 100%; height: min(64vh, 620px); aspect-ratio: 9 / 16; overflow: hidden; border-radius: 18px; background: #000; box-shadow: 0 40px 100px #000e; touch-action: none; }
.watch-player { position: relative; z-index: 1; width: auto; max-width: 100%; height: min(64vh, 620px); aspect-ratio: 9 / 16; overflow: hidden; border-radius: 18px; background: #000; box-shadow: 0 40px 100px #000e; touch-action: none; transform: translateZ(0); isolation: isolate; }
.watch-player .vp, .watch-player .video-player { border-radius: inherit; }
.watch-player .video-player { width: 100%; height: 100%; object-fit: contain; }
.watch-player-placeholder { position: relative; display: grid; place-items: center; width: 100%; height: 100%; }
.watch-player-placeholder img { width: 100%; height: 100%; object-fit: cover; opacity: .4; }
@@ -297,12 +299,13 @@ body { background: var(--bg); color: var(--text); font-family: Roboto, Arial, He
.about-secondary:hover { transform: translateY(-2px); background: #ffffff20; }
.about-stores { display: flex; flex-direction: column; gap: 10px; margin-top: 30px; }
.about-stores-label { color: #8b90a0; font-size: 11px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; }
.about-stores-row { display: flex; flex-wrap: wrap; gap: 10px; }
.about-store { display: inline-flex; align-items: center; gap: 10px; padding: 10px 16px; border-radius: 14px; background: #ffffff0f; color: #fff; backdrop-filter: blur(8px); transition: background .2s ease, transform .2s ease; }
.about-store:hover { background: #ffffff1f; transform: translateY(-2px); }
.about-store > span { display: flex; flex-direction: column; line-height: 1.2; }
.about-store small { color: #9298a6; font-size: 10px; }
.about-store b { color: #fff; font-size: 13px; font-weight: 700; }
.about-stores-row { display: flex; flex-wrap: wrap; gap: 12px; }
.store-badge { display: inline-flex; align-items: center; gap: 10px; height: 54px; padding: 0 20px 0 16px; border-radius: 12px; background: #000; color: #fff; box-shadow: inset 0 0 0 1px #ffffff47, 0 12px 28px #00000059; transition: transform .22s cubic-bezier(.34, 1.56, .64, 1), box-shadow .22s ease; }
.store-badge:hover { transform: translateY(-2px); box-shadow: inset 0 0 0 1px #ffffff73, 0 18px 36px #00000073; }
.store-badge svg { flex: none; }
.store-badge-copy { display: flex; flex-direction: column; line-height: 1.1; }
.store-badge-copy small { color: #cfcfcf; font-size: 9.5px; letter-spacing: .04em; }
.store-badge-copy b { color: #fff; font-size: 17px; letter-spacing: .01em; }
.about-values { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 28px; }
.about-values span { padding: 6px 12px; border-radius: 999px; background: #ffffff10; color: #c3c8d4; font-size: 12px; }
.about-hero-visual { display: grid; place-items: center; min-height: 360px; }
@@ -371,7 +374,7 @@ body { background: var(--bg); color: var(--text); font-family: Roboto, Arial, He
.search-form.open { position: absolute; top: var(--topbar); left: 0; right: 0; z-index: 40; display: flex; max-width: none; height: auto; margin: 0; padding: 8px 12px; border-radius: 0; background: var(--bg); border-bottom: 1px solid #222; }
.search-form.open input { height: 40px; border-radius: 20px 0 0 20px; }
.search-form.open button { height: 40px; border-radius: 0 20px 20px 0; }
.content, .content-wide { padding: 0 12px 84px; }
.content, .content-wide { padding: 0 12px calc(24px + env(safe-area-inset-bottom)); }
.youtube-category-bar { top: var(--topbar); margin: 0 -12px; padding: 10px 12px; gap: 8px; }
.youtube-category-chip { padding: 7px 12px; font-size: 13px; }
.video-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 28px 10px; }
@@ -385,26 +388,28 @@ body { background: var(--bg); color: var(--text); font-family: Roboto, Arial, He
.shorts-row { display: flex; gap: 10px; overflow-x: auto; margin: 0; padding-bottom: 2px; }
.shorts-row .media-card { flex: 0 0 150px; }
.shorts-row .media-thumb { aspect-ratio: 9 / 16; }
.shorts-stage, .shorts-stage-wide { left: 0; bottom: 64px; }
.shorts-stage, .shorts-stage-wide { left: 0; bottom: 0; }
.short-slide { padding: 8px 10px calc(8px + env(safe-area-inset-bottom)); background: #212121; }
.short-slide-stage { display: block; width: 100%; padding: 0; }
.short-column { display: block; height: 100%; }
.short-player-wrap { position: static; flex: none; height: 100%; }
.short-player-wrap::after { display: block; }
.short-slide-info { position: absolute; left: 16px; right: 84px; bottom: 22px; width: auto; margin: 0; padding: 0; pointer-events: none; text-shadow: 0 2px 14px #000; }
.short-slide-player { border-radius: 20px; box-shadow: 0 22px 54px #000a; }
.short-slide-info { position: absolute; left: 26px; right: 94px; bottom: 30px; width: auto; margin: 0; padding: 0; pointer-events: none; text-shadow: 0 2px 14px #000; }
.short-channel { gap: 8px; margin-bottom: 8px; }
.short-avatar { width: 32px; height: 32px; font-size: 13px; }
.short-channel-name { font-size: 13px; }
.short-follow { padding: 5px 12px; font-size: 11px; }
.short-rail { left: auto; right: 8px; bottom: 104px; gap: 12px; }
.short-rail { left: auto; right: 18px; bottom: 84px; gap: 12px; }
.short-rail .rail-icon { width: 42px; height: 42px; }
.short-rail small { font-size: 10px; }
.shorts-stage .feed-arrows { display: none; }
.shorts-toast { bottom: 84px; }
.shorts-skeleton-wrap { width: 100%; height: 100%; padding: 0; }
.shorts-skeleton-screen { border-radius: 0; }
.shorts-skeleton-rail { right: 8px; bottom: 104px; gap: 12px; }
.shorts-toast { bottom: calc(20px + env(safe-area-inset-bottom)); }
.shorts-skeleton-wrap { width: 100%; height: 100%; padding: 8px 10px calc(8px + env(safe-area-inset-bottom)); }
.shorts-skeleton-screen { border-radius: 14px; }
.shorts-skeleton-rail { right: 18px; bottom: 112px; gap: 12px; }
.skeleton-dot { width: 42px; height: 42px; }
.shorts-skeleton-copy { right: 84px; bottom: 22px; left: 16px; }
.shorts-skeleton-copy { right: 94px; bottom: 30px; left: 26px; }
.comments-sheet { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: min(82vh, 640px); border-top: 1px solid #303030; border-left: 0; border-radius: 16px 16px 0 0; }
.comments-backdrop { top: 0; }
.upload-backdrop { padding: 12px; }
@@ -417,8 +422,9 @@ body { background: var(--bg); color: var(--text); font-family: Roboto, Arial, He
.about-hero p { font-size: 14px; }
.about-hero-actions { margin-top: 20px; }
.about-stores { margin-top: 22px; }
.about-stores-row { flex-direction: column; }
.about-store { justify-content: flex-start; }
.about-stores-row { gap: 10px; }
.store-badge { height: 50px; padding: 0 16px 0 13px; gap: 8px; }
.store-badge-copy b { font-size: 15px; }
.about-values { margin-top: 20px; }
.about-section { margin-top: 32px; }
.about-section > h2 { font-size: 21px; }
@@ -430,7 +436,7 @@ body { background: var(--bg); color: var(--text); font-family: Roboto, Arial, He
.shorts-state { top: 12px; }
.watch-page { max-width: none; padding: 0; }
.watch-stage { margin: 0 2px; padding: 16px 10px; border-radius: 18px; }
.watch-player { height: min(62vh, 520px); border-radius: 14px; }
.watch-player { height: min(62vh, 520px); border-radius: 16px; }
.watch-skeleton-player { width: min(100%, 260px); }
.watch-meta { margin: 16px 2px 8px; font-size: 12px; }
.watch-title { font-size: 19px; margin-bottom: 14px; }
@@ -443,9 +449,6 @@ body { background: var(--bg); color: var(--text); font-family: Roboto, Arial, He
.watch-side { position: static; margin-top: 24px; padding: 14px; border-radius: 16px; }
.watch-episode-list { max-height: none; }
.history-heading h1 { font-size: 20px; }
.bottom-nav { position: fixed; left: 0; right: 0; bottom: 0; z-index: 30; display: flex; height: 64px; background: #151515; border-top: 1px solid #292929; }
.bottom-nav a { flex: 1; display: grid; place-items: center; gap: 3px; color: #aaa; font-size: 10px; line-height: 1.2; }
.bottom-nav a.active { color: #fff; }
.sidebar { display: none; top: var(--topbar); bottom: 0; z-index: 50; width: min(240px, calc(100vw - 56px)); padding: 12px; overflow-y: auto; background: var(--bg); box-shadow: 0 0 24px #000a; }
.sidebar.open { display: block; }
.sidebar.closed { display: none; }
@@ -459,21 +462,21 @@ body { background: var(--bg); color: var(--text); font-family: Roboto, Arial, He
.vp { position: relative; display: block; width: 100%; height: 100%; overflow: hidden; background: #000; outline: none; }
.vp .video-player { display: block; width: 100%; height: 100%; object-fit: contain; background: #000; }
.vp.is-playing:not(.show-controls) { cursor: none; }
.vp-controls { position: absolute; left: 0; right: 0; bottom: 0; z-index: 3; padding: 38px 14px 10px; background: linear-gradient(transparent, #000000bd 42%, #000000f2); opacity: 0; transform: translateY(10px); transition: opacity .24s ease, transform .28s cubic-bezier(.22, .61, .36, 1); pointer-events: none; }
.vp-controls { position: absolute; left: 0; right: 0; bottom: 0; z-index: 3; display: flex; flex-direction: column-reverse; padding: 38px 14px 0; background: linear-gradient(transparent, #000000bd 42%, #000000f2); opacity: 0; transform: translateY(10px); transition: opacity .24s ease, transform .28s cubic-bezier(.22, .61, .36, 1); pointer-events: none; }
.vp.show-controls .vp-controls { opacity: 1; transform: none; pointer-events: auto; }
.vp.show-controls .vp-progress { animation: vp-rise .34s cubic-bezier(.22, .61, .36, 1) both; }
.vp.show-controls .vp-row { animation: vp-rise .38s cubic-bezier(.22, .61, .36, 1) .05s both; }
@keyframes vp-rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.vp-progress { position: relative; display: flex; align-items: center; height: 18px; cursor: pointer; touch-action: none; }
.vp-progress { position: relative; display: flex; align-items: flex-end; height: 14px; cursor: pointer; touch-action: none; }
.vp-progress-track { position: relative; width: 100%; height: 4px; overflow: hidden; border-radius: 999px; background: #ffffff24; box-shadow: inset 0 1px 2px #0006; transition: height .18s ease; }
.vp-progress:hover .vp-progress-track { height: 6px; }
.vp-buffered { position: absolute; top: 0; bottom: 0; left: 0; background: #ffffff38; transition: width .3s ease; }
.vp-played { position: absolute; top: 0; bottom: 0; left: 0; border-radius: 999px; background: linear-gradient(90deg, #ffffff, #ffd9e2); box-shadow: 0 0 12px #ffffff66; transition: width .12s linear; }
.vp-thumb { position: absolute; top: 50%; width: 12px; height: 12px; border-radius: 50%; background: #fff; box-shadow: 0 0 0 4px #ffffff1f, 0 0 16px #ffffff73; transform: translate(-50%, -50%) scale(0); transition: transform .2s cubic-bezier(.34, 1.56, .64, 1), box-shadow .2s ease; pointer-events: none; }
.vp-progress:hover .vp-thumb, .vp-progress:active .vp-thumb { transform: translate(-50%, -50%) scale(1); }
.vp-thumb { position: absolute; bottom: 2px; width: 12px; height: 12px; border-radius: 50%; background: #fff; box-shadow: 0 0 0 4px #ffffff1f, 0 0 16px #ffffff73; transform: translate(-50%, 50%) scale(0); transition: transform .2s cubic-bezier(.34, 1.56, .64, 1), box-shadow .2s ease; pointer-events: none; }
.vp-progress:hover .vp-thumb, .vp-progress:active .vp-thumb { transform: translate(-50%, 50%) scale(1); }
.vp-progress-tip { position: absolute; top: -30px; z-index: 5; padding: 3px 8px; border-radius: 7px; background: #0d0d0ff2; color: #fff; font-size: 11px; font-variant-numeric: tabular-nums; transform: translateX(-50%); pointer-events: none; box-shadow: 0 8px 20px #000a; backdrop-filter: blur(8px); animation: vp-tip-in .16s ease both; }
@keyframes vp-tip-in { from { opacity: 0; transform: translate(-50%, 4px); } to { opacity: 1; transform: translate(-50%, 0); } }
.vp-row { display: flex; align-items: center; gap: 8px; margin-top: 4px; color: #fff; }
.vp-row { display: flex; align-items: center; gap: 8px; margin-top: 0; padding-bottom: 4px; color: #fff; }
.vp-btn { position: relative; display: grid; place-items: center; width: 34px; height: 34px; border: 0; border-radius: 50%; background: transparent; color: #fff; cursor: pointer; transition: background .2s ease, box-shadow .2s ease, transform .16s cubic-bezier(.34, 1.56, .64, 1); }
.vp-btn svg { transition: transform .18s ease; }
.vp-btn:hover { background: #ffffff22; box-shadow: inset 0 0 0 1px #ffffff1a, 0 4px 14px #0006; }
@@ -492,10 +495,16 @@ body { background: var(--bg); color: var(--text); font-family: Roboto, Arial, He
.vp-big-play:hover { transform: scale(1.08); background: #000000cc; box-shadow: 0 0 0 8px #ffffff14, 0 16px 40px #000a; }
.vp-big-play:active { transform: scale(.96); }
@keyframes vp-pop { from { opacity: 0; transform: scale(.82); } to { opacity: 1; transform: scale(1); } }
.vp-sound-hint { position: absolute; top: 12px; left: 50%; z-index: 5; display: inline-flex; align-items: center; gap: 6px; padding: 7px 12px; border: 0; border-radius: 999px; background: #000000b8; color: #fff; font-size: 12px; cursor: pointer; transform: translateX(-50%); backdrop-filter: blur(8px); box-shadow: 0 10px 26px #0008; }
.vp-sound-hint:hover { background: #000000d6; }
.vp-error { position: absolute; left: 50%; bottom: 54px; z-index: 4; max-width: 82%; padding: 10px 16px; border-radius: 999px; background: #241919ee; color: #ffd4cf; font-size: 12px; cursor: pointer; transform: translateX(-50%); backdrop-filter: blur(8px); }
@media (max-width: 760px) {
.vp-controls { padding: 30px 10px 8px; }
.vp-row { gap: 4px; }
.vp-controls { padding: 30px 8px 0; }
.vp-row { gap: 2px; padding: 0 2px 2px; }
.vp-row .vp-time, .vp-volume { display: none; }
.vp-progress { height: 12px; }
.vp-progress-track, .vp-progress:hover .vp-progress-track { height: 3px; border-radius: 0; }
.vp-thumb { display: none; }
.vp-btn { width: 36px; height: 36px; }
.vp-big-play { width: 56px; height: 56px; }
}
@@ -529,3 +538,78 @@ body { background: var(--bg); color: var(--text); font-family: Roboto, Arial, He
.watch-actions button.active::after { content: ''; position: absolute; inset: 0; border-radius: inherit; border: 1px solid #fff; pointer-events: none; animation: ymb-like-ring .5s ease-out forwards; }
.short-rail .rail-icon { position: relative; }
.short-rail button.active .rail-icon::after { content: ''; position: absolute; inset: -1px; border-radius: 50%; border: 1px solid #fff; pointer-events: none; animation: ymb-like-ring .5s ease-out forwards; }
/* 进入播放页:从点击位置放大封面(高斯模糊)铺满,再淡出 */
.hero-transition { position: fixed; z-index: 1200; overflow: hidden; border-radius: 12px; pointer-events: none; transform-origin: top left; background: #09090b; box-shadow: 0 30px 80px #000a; animation: none; }
.hero-transition img { display: block; width: 100%; height: 100%; object-fit: cover; filter: blur(18px); transform: scale(1.14); }
.hero-transition::after { content: ''; position: absolute; inset: 0; background: radial-gradient(110% 110% at 50% 42%, #00000000 28%, #09090bcc 100%); }
/* 进入播放页:播放器轻微放大淡入 */
@keyframes ymb-player-in {
from { opacity: 0; transform: scale(.955); }
to { opacity: 1; transform: none; }
}
.watch-stage { animation: ymb-player-in .46s cubic-bezier(.2, .7, .3, 1) both; }
@media (prefers-reduced-motion: reduce) { .watch-stage { animation: none; } }
/* 页面动效:仅使用 transform / opacity,走合成层,不影响布局与滚动性能 */
@keyframes ymb-page-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: none; }
}
@keyframes ymb-card-in {
from { opacity: 0; transform: translateY(14px) scale(.992); }
to { opacity: 1; transform: none; }
}
@keyframes ymb-shelf-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: none; }
}
.content > * { animation: ymb-page-in .3s cubic-bezier(.2, .7, .3, 1) both; }
.content > .hero-transition { animation: none; }
.video-grid .media-card,
.shorts-row .media-card { animation: ymb-card-in .34s cubic-bezier(.2, .7, .3, 1) both; }
.video-grid .media-card:nth-child(1) { animation-delay: .02s; }
.video-grid .media-card:nth-child(2) { animation-delay: .05s; }
.video-grid .media-card:nth-child(3) { animation-delay: .08s; }
.video-grid .media-card:nth-child(4) { animation-delay: .11s; }
.video-grid .media-card:nth-child(5) { animation-delay: .14s; }
.video-grid .media-card:nth-child(6) { animation-delay: .17s; }
.video-grid .media-card:nth-child(7) { animation-delay: .2s; }
.video-grid .media-card:nth-child(8) { animation-delay: .23s; }
.shorts-row .media-card:nth-child(1) { animation-delay: .04s; }
.shorts-row .media-card:nth-child(2) { animation-delay: .08s; }
.shorts-row .media-card:nth-child(3) { animation-delay: .12s; }
.shorts-row .media-card:nth-child(4) { animation-delay: .16s; }
.shorts-row .media-card:nth-child(5) { animation-delay: .2s; }
.media-thumb { position: relative; overflow: hidden; transition: box-shadow .25s ease; }
.media-thumb::after { content: ''; position: absolute; inset: 0; border-radius: inherit; box-shadow: 0 18px 42px #0008; opacity: 0; transition: opacity .25s ease; pointer-events: none; }
.media-card:hover .media-thumb::after { opacity: 1; }
.media-thumb img { transition: transform .45s cubic-bezier(.2, .7, .3, 1); }
.media-card:hover .media-thumb img { transform: scale(1.045); }
.media-thumb .thumb-play { transition: transform .3s cubic-bezier(.34, 1.56, .64, 1), opacity .3s ease; }
.media-card:hover .thumb-play { transform: scale(1.08); }
.youtube-category-chip { transition: background .18s ease, color .18s ease, transform .16s ease; }
.youtube-category-chip:active { transform: scale(.96); }
@media (hover: none) {
.media-card:hover .media-thumb img { transform: none; }
}
@supports (animation-timeline: view()) {
.youtube-home-feed > *,
.youtube-shorts-shelf {
animation: ymb-shelf-in linear both;
animation-timeline: view();
animation-range: entry 0% entry 55%;
}
}
@media (prefers-reduced-motion: reduce) {
.content > *, .video-grid .media-card, .shorts-row .media-card,
.youtube-home-feed > *, .youtube-shorts-shelf { animation: none; }
.media-thumb img, .media-thumb::after, .thumb-play { transition: none; }
}
/* 全站文字统一不加粗 */
body,
body * { font-weight: 400 !important; }
b,
strong { font-weight: 400 !important; }