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 { 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. */ 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 : undefined; const sceneDur = durationInFrames; const fadeOpacity = 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; const footerReserved = isPortrait ? 500 : 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; // Under the weekly board, todayStars carries the WEEKLY gain. const starsLabel = repo?.stars != null ? `${formatCount(repo.stars)} stars` : ""; const weekGainLabel = 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: flex column. Header takes natural height, divider sits // right below it, content fills the remainder. paddingBottom reserves // space for the elevated subtitle (bottom: 380).
{renderHeader(92, "large", 16)}
) : ( // 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 PortraitContentProps { palette: { from: string; to: string; accent: string }; socialPreview?: { filename: string }; starHistory?: { filename: string }; github: RenderSegmentExtension; } // PortraitContent uses flex:1 to fill remaining height after the header // (which takes its natural height in the parent flex column). const PortraitContent: React.FC = ({ palette, socialPreview, starHistory, github, }) => { const hasAnyImage = !!(socialPreview || starHistory); return (
{/* Top row: images side-by-side, each flex:1 (same width) with natural height. Row sizes to tallest child; shorter child top-aligned. */} {hasAnyImage && (
{socialPreview && ( )} {starHistory && ( )}
)} {/* Bottom: 2 sections stacked (full height when no images) */}
); }; 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;