// frontend\src\components\ui\FinishedDownloadsCardsView.tsx 'use client' import { memo, useCallback, useEffect, useMemo, useRef, useState, type ReactNode, } from 'react' import Card from './Card' import Button from './Button' import type { RecordJob, FinishedPostworkSummary, GalleryModelFlags, InlinePlayState } from '../../types' import FinishedVideoPreview from './FinishedVideoPreview' import SwipeCard, { type SwipeCardHandle } from './SwipeCard' import RecordJobActions from './RecordJobActions' import TagOverflowRow from './TagOverflowRow' import PreviewScrubber from './PreviewScrubber' import { isHotName, stripHotPrefix } from './hotName' import type { FinishedDownloadsTeaserState } from './teaserPlayback' import { shouldAnimateTeaser, shouldEnableTeaserAudio, } from './teaserPlayback' import { SpeakerWaveIcon } from '@heroicons/react/24/solid' import { buildSpriteFrameStyle, parseJobMeta, readPreviewSpriteInfo, scrubProgressRatioFromIndex, } from './previewSprite' import ModelGenderIcon from './ModelGenderIcon' import { parseFinishedTags, resolveFinishedModelFlags } from './finishedModelFlags' import CountryFlag, { resolveFinishedCountry } from './CountryFlag' import { type FinishedSelectionStore, } from './finishedSelectionStore' import SpritePreloader from './SpritePreloader' import FinishedDownloadsDesktopCard, { SelectionCheckboxOverlay, } from './FinishedDownloadsDesktopCard' import PreviewRatingOverlay from './PreviewRatingOverlay' import PreviewMetaOverlay from './PreviewMetaOverlay' type SwipeRefs = { current: Map } type Props = { rows: RecordJob[] isLoading?: boolean isSmall: boolean selectionStore: FinishedSelectionStore onToggleSelected: (job: RecordJob) => void onToggleSelectAllPage: (checked: boolean) => void onSelectOnly: (job: RecordJob) => void blurPreviews?: boolean durations: Record inlinePlay: InlinePlayState teaserState: FinishedDownloadsTeaserState deletingKeys: Set keepingKeys: Set removingKeys: Set swipeRefs: SwipeRefs assetNonce?: number jobForDetails: (job: RecordJob) => RecordJob handleScrubberClickIndex: (job: RecordJob, segmentIndex: number, segmentCount: number) => void // helpers keyFor: (j: RecordJob) => string baseName: (p: string) => string modelNameFromOutput: (output?: string) => string runtimeOf: (job: RecordJob) => string sizeBytesOf: (job: RecordJob) => number | null formatBytes: (bytes?: number | null) => string lower: (s: string) => string // callbacks/actions onHoverPreviewKeyChange?: (key: string | null) => void onOpenPlayer: (job: RecordJob) => void openPlayer: (job: RecordJob) => void onOpenPlayerAt?: (job: RecordJob, seconds: number) => void startInlineAt?: (key: string, seconds: number, domId: string) => void startInline: (key: string) => void tryAutoplayInline: (domId: string) => boolean registerTeaserHost: (key: string) => (el: HTMLDivElement | null) => void handleDuration: (job: RecordJob, seconds: number) => void handleResolution: (job: RecordJob, w: number, h: number) => void resolutionLabelOf: (job: RecordJob) => string deleteVideo: (job: RecordJob) => Promise keepVideo: (job: RecordJob) => Promise releasePlayingFile: (file: string, opts?: { close?: boolean }) => Promise modelsByKey: Record activeTagSet: Set onToggleTagFilter: (tag: string) => void onToggleHot?: (job: RecordJob) => void | Promise onToggleFavorite?: (job: RecordJob) => void | Promise onToggleLike?: (job: RecordJob) => void | Promise onToggleWatch?: (job: RecordJob) => void | Promise onSplit?: (job: RecordJob) => void | Promise onAddToDownloads?: (job: RecordJob) => void | Promise postworkByFile: Record getPostworkSummaryForOutput: ( output: string | undefined, summaries: Record ) => FinishedPostworkSummary | undefined renderPostworkBadge?: (badge?: FinishedPostworkSummary) => ReactNode forcePreviewMuted?: boolean mobileTeaserAudioUnlocked?: boolean onUnlockMobileTeaserAudio?: () => void renderIntegrityBadge?: (job: RecordJob) => ReactNode renderRatingOverlay?: (job: RecordJob) => ReactNode } function CardBlurWrapper({ blurred, animateUnblurOnMount, children, }: { blurred?: boolean animateUnblurOnMount?: boolean children: ReactNode }) { const [entered, setEntered] = useState(!animateUnblurOnMount) useEffect(() => { if (!animateUnblurOnMount) return const raf = requestAnimationFrame(() => { setEntered(true) }) return () => cancelAnimationFrame(raf) }, [animateUnblurOnMount]) const className = [ 'relative will-change-[filter,transform] transition-[filter,transform,opacity] duration-180 ease-out', blurred ? 'blur-[1.5px] saturate-90 scale-[0.995]' : entered ? 'blur-0 saturate-100 scale-100' : 'blur-[1.5px] saturate-90 scale-[0.995]', ] .filter(Boolean) .join(' ') return
{children}
} function PromoteToFrontWrapper({ animateOnMount, children, }: { animateOnMount?: boolean children: ReactNode }) { const [entered, setEntered] = useState(!animateOnMount) useEffect(() => { if (!animateOnMount) return const raf = requestAnimationFrame(() => { setEntered(true) }) return () => cancelAnimationFrame(raf) }, [animateOnMount]) return (
{children}
) } function LoadingCardSkeleton({ blurred = false }: { blurred?: boolean }) { return (