import React from "react"; import { useCurrentFrame, useVideoConfig, AbsoluteFill, Img, staticFile, interpolate, } from "remotion"; import type { RemotionScene } from "../Root"; import type { RenderSegmentExtension } from "@pipeline/shared"; import { GITHUB_WEEKLY_PALETTE } from "../base/theme/colors"; import { SubtitleBar } from "../base/components/subtitle-bar"; import { Watermark } from "../base/components/watermark"; import { BoardPortrait, PORTRAIT_FOOTER_RESERVED } from "../base/components/board-portrait"; import { formatCount } from "@pipeline/shared"; /** * github-weekly content scene — the LIGHT magazine-weekly counterpart of the * github-trending scene. Paper background, dark slate text, editorial section * cards (white cards on paper, the inverse of the daily board's dark-on-dark), * and the same named images (social preview + star history) from the shared * github board extension. * * Also serves github-weekly-recap: same layout, but the per-scene tag shows * the repo's featured date ("推荐 · 8月12日", from extension.coveredDate) * instead of a weekly star gain — the recap's repo snapshot carries the * feature-time numbers, so a "本周 +N" label would be wrong there. */ interface GithubWeeklySceneProps { scene: RemotionScene; sceneIndex: number; totalScenes: number; totalFrames: number; title: string; channelName?: string; } const GithubWeeklyScene: React.FC = ({ scene, sceneIndex, channelName, }) => { const frame = useCurrentFrame(); const { width, height, durationInFrames } = useVideoConfig(); const isPortrait = height > width; const palette = GITHUB_WEEKLY_PALETTE[sceneIndex % GITHUB_WEEKLY_PALETTE.length]; const github: RenderSegmentExtension | undefined = scene.extension?.type === "github-trending" || scene.extension?.type === "github-weekly" || scene.extension?.type === "github-weekly-recap" ? scene.extension : undefined; const isRecap = scene.extension?.type === "github-weekly-recap"; const sceneDur = durationInFrames; // Landscape keeps the legacy soft fade. Portrait cuts hard between scenes: // Sequences don't overlap, so the old fade dipped both scenes through the // composition's black base (a black flash at every boundary) — and the // board's staggered element entrances now carry the motion anyway. const fadeOpacity = isPortrait ? 1 : interpolate( frame, [0, 8, sceneDur - 8, sceneDur], [0, 1, 1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" } ); // Named images from the shared board extension (resolved to filenames by the // renderer). Object, not positional array — socialPreview / starHistory. const socialPreview = github?.images?.socialPreview; const starHistory = github?.images?.starHistory; const pad = isPortrait ? 48 : 80; // Portrait reserves the new (smaller) subtitle band; landscape keeps 140. const footerReserved = isPortrait ? PORTRAIT_FOOTER_RESERVED : 140; // Landscape geometry (portrait uses flex layout below — no headerHeight). const landscapeHeaderHeight = 220; const landscapeDividerY = pad + landscapeHeaderHeight; const landscapeContentTop = landscapeDividerY + 24; const landscapeContentHeight = height - footerReserved - landscapeContentTop; const landscapeContentWidth = width - pad * 2; const repo = github?.repo; const language = repo?.language || ""; const languageColor = repo?.languageColor || palette.accent; const starsLabel = repo?.stars != null ? `${formatCount(repo.stars)} stars` : ""; // Under the weekly board, todayStars carries the WEEKLY gain. The recap // instead tags the scene with the repo's featured date ("2026-08-12" → // "推荐 · 8月12日"); its snapshot numbers are feature-time, not this week's. const featuredLabel = github?.coveredDate ? `推荐 · ${github.coveredDate.replace(/^\d{4}-/, "").replace("-", "月").replace(/^0/, "")}日` : ""; const weekGainLabel = isRecap ? featuredLabel : repo?.todayStars != null ? `本周 +${formatCount(repo.todayStars)}` : ""; const license = repo?.license || ""; const fullName = repo?.fullName || scene.title || ""; if (!github) { return (
{scene.title || scene.captionOrigin}
{(scene.caption?.length ?? 0) > 0 && }
); } const renderHeader = (fontSize: number, tagSize: "default" | "large", dotSize: number) => (
{fullName}
{language && ( {language} )} {weekGainLabel && ( {weekGainLabel} )} {starsLabel && {starsLabel}} {license && {license}}
); return ( {/* Subtle paper ruling (the light counterpart of the daily board's grid) */}
{/* Top accent rule — a double editorial rule, magazine style */}
{isPortrait ? ( // Portrait: the shared animated Hero board (base/components/board-portrait) // in the magazine skin — pill chips, left editorial bars, no rank // numeral; recap passes coveredDate so the tag reads "推荐 · 8月12日". github ? ( ) : ( // No-extension fallback keeps a simple centered column.
{scene.title || scene.captionOrigin}
) ) : ( // Landscape: absolute positioning (single-line 80px header). <>
{renderHeader(80, "default", 14)}
)} {(scene.caption?.length ?? 0) > 0 && ( )} ); }; // --- Sub-components (light counterparts of the github-trending ones) --- export const Tag: React.FC<{ accent: string; children: React.ReactNode; size?: "default" | "large"; emphasized?: boolean; }> = ({ accent, children, size = "default", emphasized }) => (
{children}
); export const ColorDot: React.FC<{ color: string; size?: number }> = ({ color, size = 14 }) => ( ); const ImageFrame: React.FC<{ children: React.ReactNode; flex?: string | number }> = ({ children, flex }) => (
{children}
); const Section: React.FC<{ title: string; accent: string; body: string; flex?: number; size?: "default" | "large"; }> = ({ title, accent, body, flex, size = "default" }) => { const isLarge = size === "large"; // Strip trailing punctuation that would look awkward if rendered at the // end of the clamped text (LLM occasionally leaves a hanging 。,,;). const cleanBody = body.replace(/[。,、,.;;::\s]+$/, ""); return (
{/* No color bar (it moved to the card's left border) — editorial small-caps style label with wide tracking instead. */} {title}
{cleanBody}
); }; interface LandscapeContentProps { contentTop: number; contentHeight: number; pad: number; contentWidth: number; palette: { from: string; to: string; accent: string }; socialPreview?: { filename: string }; starHistory?: { filename: string }; github: RenderSegmentExtension; } const LandscapeContent: React.FC = ({ contentTop, contentHeight, pad, contentWidth, palette, socialPreview, starHistory, github, }) => { const hasAnyImage = !!(socialPreview || starHistory); // Left column: cap width so both images stacked fit within contentHeight. // Assumes social preview ~2:1 and star history ~1.5:1 → combined height 7/6. const leftColWidth = hasAnyImage ? Math.min( Math.round(contentWidth * 0.4), Math.round((contentHeight - 20) / (7 / 6)), ) : 0; const colGap = 40; return (
{hasAnyImage && (
{socialPreview && ( )} {starHistory && ( )}
)}
); }; export default GithubWeeklyScene;