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
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
Reference in new issue
Block a user