|
|
@@ -0,0 +1,208 @@
|
|
|
+import React from "react";
|
|
|
+import {
|
|
|
+ useCurrentFrame,
|
|
|
+ useVideoConfig,
|
|
|
+ AbsoluteFill,
|
|
|
+ interpolate,
|
|
|
+} from "remotion";
|
|
|
+import type { RemotionScene } from "../Root";
|
|
|
+import { GITHUB_TRENDING_PALETTE } from "../base/theme/colors";
|
|
|
+import { SubtitleBar } from "../base/components/subtitle-bar";
|
|
|
+import { Watermark } from "../base/components/watermark";
|
|
|
+import { formatCount, type RepoMeta } from "@pipeline/shared";
|
|
|
+import { Tag, ColorDot } from "./index";
|
|
|
+
|
|
|
+interface SummarySceneProps {
|
|
|
+ scene: RemotionScene;
|
|
|
+ totalFrames: number;
|
|
|
+ channelName?: string;
|
|
|
+}
|
|
|
+
|
|
|
+// Circled digits 1-20 for clean rank badges. Fall back to "<n>." past 20.
|
|
|
+const CIRCLED = ["①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩",
|
|
|
+ "⑪", "⑫", "⑬", "⑭", "⑮", "⑯", "⑰", "⑱", "⑲", "⑳"];
|
|
|
+
|
|
|
+function rankBadge(idx: number): string {
|
|
|
+ return idx < CIRCLED.length ? CIRCLED[idx] : `${idx + 1}.`;
|
|
|
+}
|
|
|
+
|
|
|
+export const GithubTrendingSummaryScene: React.FC<SummarySceneProps> = ({
|
|
|
+ scene,
|
|
|
+ channelName,
|
|
|
+}) => {
|
|
|
+ const frame = useCurrentFrame();
|
|
|
+ const { width, height } = useVideoConfig();
|
|
|
+ const isPortrait = height > width;
|
|
|
+ const palette = GITHUB_TRENDING_PALETTE[0]; // navy — consistent with cover
|
|
|
+
|
|
|
+ const sceneDur = scene.endFrame - scene.startFrame;
|
|
|
+ const fadeOpacity = interpolate(
|
|
|
+ frame,
|
|
|
+ [0, 8, sceneDur - 8, sceneDur],
|
|
|
+ [0, 1, 1, 0],
|
|
|
+ { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
|
+ );
|
|
|
+
|
|
|
+ const repos: RepoMeta[] = scene.trendingRepos ?? [];
|
|
|
+ const repoCount = repos.length;
|
|
|
+ const title = scene.title || "今日精选";
|
|
|
+
|
|
|
+ const pad = isPortrait ? 48 : 80;
|
|
|
+ const footerReserved = isPortrait ? 500 : 140;
|
|
|
+ const titleFontSize = isPortrait ? 92 : 72;
|
|
|
+ const subtitleFontSize = isPortrait ? 36 : 28;
|
|
|
+ const rankFontSize = isPortrait ? 56 : 44;
|
|
|
+ const nameFontSize = isPortrait ? 48 : 36;
|
|
|
+ const rowGap = isPortrait ? 24 : 16;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <AbsoluteFill style={{
|
|
|
+ opacity: fadeOpacity,
|
|
|
+ background: `linear-gradient(135deg, ${palette.from} 0%, ${palette.to} 100%)`,
|
|
|
+ }}>
|
|
|
+ {/* Subtle grid overlay — matches the content scene aesthetic */}
|
|
|
+ <div style={{
|
|
|
+ position: "absolute", inset: 0,
|
|
|
+ backgroundImage: `
|
|
|
+ linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
|
|
|
+ linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px)
|
|
|
+ `,
|
|
|
+ backgroundSize: "80px 80px",
|
|
|
+ }} />
|
|
|
+
|
|
|
+ {/* Top accent line */}
|
|
|
+ <div style={{
|
|
|
+ position: "absolute", top: 0, left: 0, width: "100%", height: 4,
|
|
|
+ background: `linear-gradient(90deg, ${palette.accent}, ${palette.accent}88)`,
|
|
|
+ }} />
|
|
|
+
|
|
|
+ <div style={{
|
|
|
+ position: "relative",
|
|
|
+ display: "flex",
|
|
|
+ flexDirection: "column",
|
|
|
+ width: "100%",
|
|
|
+ height: "100%",
|
|
|
+ boxSizing: "border-box",
|
|
|
+ padding: `${pad}px ${pad}px ${footerReserved}px`,
|
|
|
+ }}>
|
|
|
+ {/* Title + subtitle */}
|
|
|
+ <div>
|
|
|
+ <div style={{
|
|
|
+ fontSize: titleFontSize,
|
|
|
+ fontWeight: 800,
|
|
|
+ color: "white",
|
|
|
+ fontFamily: "Noto Sans SC",
|
|
|
+ lineHeight: 1.15,
|
|
|
+ letterSpacing: "-0.01em",
|
|
|
+ }}>
|
|
|
+ {title}
|
|
|
+ </div>
|
|
|
+ <div style={{
|
|
|
+ fontSize: subtitleFontSize,
|
|
|
+ fontWeight: 500,
|
|
|
+ color: "rgba(255,255,255,0.65)",
|
|
|
+ fontFamily: "Noto Sans SC",
|
|
|
+ marginTop: 12,
|
|
|
+ }}>
|
|
|
+ {repoCount} 个优质项目
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Divider */}
|
|
|
+ <div style={{
|
|
|
+ height: 1,
|
|
|
+ background: "rgba(255,255,255,0.18)",
|
|
|
+ marginTop: 24,
|
|
|
+ }} />
|
|
|
+
|
|
|
+ {/* Repo list — flex:1, evenly distributed */}
|
|
|
+ <div style={{
|
|
|
+ flex: 1,
|
|
|
+ display: "flex",
|
|
|
+ flexDirection: "column",
|
|
|
+ justifyContent: "space-evenly",
|
|
|
+ paddingTop: 16,
|
|
|
+ minHeight: 0,
|
|
|
+ }}>
|
|
|
+ {repos.map((repo, idx) => {
|
|
|
+ const language = repo.language || "";
|
|
|
+ const languageColor = repo.languageColor || palette.accent;
|
|
|
+ const starsLabel = repo.stars != null ? `${formatCount(repo.stars)} stars` : "";
|
|
|
+ const license = repo.license || "";
|
|
|
+ return (
|
|
|
+ <div key={repo.fullName} style={{
|
|
|
+ display: "flex",
|
|
|
+ alignItems: "center",
|
|
|
+ gap: isPortrait ? 24 : 18,
|
|
|
+ }}>
|
|
|
+ <span style={{
|
|
|
+ fontSize: rankFontSize,
|
|
|
+ fontWeight: 700,
|
|
|
+ color: palette.accent,
|
|
|
+ fontFamily: "Noto Sans SC",
|
|
|
+ lineHeight: 1,
|
|
|
+ flexShrink: 0,
|
|
|
+ minWidth: isPortrait ? 56 : 44,
|
|
|
+ textAlign: "center",
|
|
|
+ }}>
|
|
|
+ {rankBadge(idx)}
|
|
|
+ </span>
|
|
|
+ <div style={{
|
|
|
+ display: "flex",
|
|
|
+ flexDirection: "column",
|
|
|
+ gap: 8,
|
|
|
+ minWidth: 0,
|
|
|
+ flex: 1,
|
|
|
+ }}>
|
|
|
+ <div style={{
|
|
|
+ fontSize: nameFontSize,
|
|
|
+ fontWeight: 700,
|
|
|
+ color: "white",
|
|
|
+ fontFamily: "Noto Sans SC",
|
|
|
+ lineHeight: 1.2,
|
|
|
+ overflow: "hidden",
|
|
|
+ textOverflow: "ellipsis",
|
|
|
+ whiteSpace: "nowrap",
|
|
|
+ }}>
|
|
|
+ {repo.fullName}
|
|
|
+ </div>
|
|
|
+ {(language || starsLabel || license) && (
|
|
|
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 10 }}>
|
|
|
+ {language && (
|
|
|
+ <Tag accent={palette.accent} size={isPortrait ? "large" : "default"}>
|
|
|
+ <ColorDot color={languageColor} size={isPortrait ? 16 : 14} />
|
|
|
+ {language}
|
|
|
+ </Tag>
|
|
|
+ )}
|
|
|
+ {starsLabel && (
|
|
|
+ <Tag accent={palette.accent} size={isPortrait ? "large" : "default"}>
|
|
|
+ {starsLabel}
|
|
|
+ </Tag>
|
|
|
+ )}
|
|
|
+ {license && (
|
|
|
+ <Tag accent={palette.accent} size={isPortrait ? "large" : "default"}>
|
|
|
+ {license}
|
|
|
+ </Tag>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {scene.wordTimestamps.length > 0 && (
|
|
|
+ <SubtitleBar
|
|
|
+ wordTimestamps={scene.wordTimestamps}
|
|
|
+ style={isPortrait ? { bottom: 380 } : undefined}
|
|
|
+ fontSize={isPortrait ? 56 : undefined}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ <Watermark text={channelName} />
|
|
|
+ </AbsoluteFill>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default GithubTrendingSummaryScene;
|