import { ArrowLeft, ChevronLeft, ChevronRight, CircleHelp, Clock3, Copy, Gamepad2, GitFork, Heart, PencilLine, Play, Share2, } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import type { PublicUserSummary } from '../../../packages/shared/src/contracts/auth'; import { buildPublicWorkDetailUrl } from '../../routing/appPageRoutes'; import { CopyCodeButton } from '../common/CopyCodeButton'; import { CopyFeedbackMessage } from '../common/CopyFeedbackMessage'; import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformIconButton } from '../common/PlatformIconButton'; import { PlatformPillBadge } from '../common/PlatformPillBadge'; import { useCopyFeedback } from '../common/useCopyFeedback'; import { ResolvedAssetImage } from '../ResolvedAssetImage'; import { buildPlatformWorldDisplayTags, formatPlatformWorkDisplayName, formatPlatformWorkDisplayTags, formatPlatformWorldTime, isBarkBattleGalleryEntry, isCustomWorldGalleryEntry, isEdutainmentGalleryEntry, isJumpHopGalleryEntry, isPuzzleClearGalleryEntry, isWoodenFishGalleryEntry, type PlatformPublicGalleryCard, resolvePlatformPublicWorkCode, resolvePlatformWorkAuthorDisplayName, resolvePlatformWorldCoverSlides, resolvePlatformWorldStats, } from '../rpg-entry/rpgEntryWorldPresentation'; export interface PlatformWorkDetailViewProps { entry: PlatformPublicGalleryCard; authorSummary?: PublicUserSummary | null; isBusy: boolean; error?: string | null; visibleCoverCount?: number; onBack: () => void; onLike: () => void; onStart: () => void; onRemix: () => void; actionMode?: 'remix' | 'edit'; } function formatCompactCount(value: number) { if (value >= 10000) { const normalized = value / 10000; return `${Number.isInteger(normalized) ? normalized.toFixed(0) : normalized.toFixed(1)}万`; } return `${value}`; } function getSourceLabel(entry: PlatformPublicGalleryCard) { if ('sourceType' in entry && entry.sourceType === 'puzzle') { return '拼图'; } if (isPuzzleClearGalleryEntry(entry)) { return '拼消消'; } if ('sourceType' in entry && entry.sourceType === 'big-fish') { return '大鱼吃小鱼'; } if (isJumpHopGalleryEntry(entry)) { return '跳一跳'; } if (isWoodenFishGalleryEntry(entry)) { return '敲木鱼'; } if ('sourceType' in entry && entry.sourceType === 'match3d') { return '抓大鹅'; } if ('sourceType' in entry && entry.sourceType === 'square-hole') { return '方洞挑战'; } if ('sourceType' in entry && entry.sourceType === 'visual-novel') { return '视觉小说'; } if (isBarkBattleGalleryEntry(entry)) { return '汪汪声浪'; } if (isEdutainmentGalleryEntry(entry)) { return entry.templateName; } if (isCustomWorldGalleryEntry(entry)) { return 'RPG'; } throw new Error('未知公开作品类型。'); } function getAuthorAvatarLabel(authorDisplayName: string) { return Array.from(authorDisplayName.trim() || '作')[0] ?? '作'; } const PLATFORM_WORK_COVER_CAROUSEL_INTERVAL_MS = 4200; export function PlatformWorkDetailView({ entry, authorSummary, isBusy, visibleCoverCount = 1, onBack, onLike, onStart, onRemix, actionMode = 'remix', }: PlatformWorkDetailViewProps) { const coverSlides = useMemo( () => resolvePlatformWorldCoverSlides(entry), [entry], ); const [activeCoverIndex, setActiveCoverIndex] = useState(0); const activeCoverSlide = coverSlides[activeCoverIndex] ?? coverSlides[0] ?? null; const coverImage = activeCoverSlide?.imageSrc ?? ''; const unlockedCoverCount = Math.max(1, Math.floor(visibleCoverCount)); const isActiveCoverVisible = activeCoverIndex < unlockedCoverCount; const appIconImage = coverSlides[0]?.imageSrc ?? ''; const hasCoverCarousel = coverSlides.length > 1; const publicWorkCode = resolvePlatformPublicWorkCode(entry); const normalizedAuthorAvatarUrl = authorSummary?.avatarUrl?.trim() ?? ''; const resolvedAuthorDisplayName = resolvePlatformWorkAuthorDisplayName( entry, authorSummary, ); const { copyState, copyText: copyWorkCodeText, resetCopyState: resetWorkCodeCopyState, } = useCopyFeedback(); const { copyState: shareState, copyText: copyShareText, resetCopyState: resetShareCopyState, } = useCopyFeedback(); const displayName = formatPlatformWorkDisplayName(entry.worldName); const tags = useMemo( () => formatPlatformWorkDisplayTags( [getSourceLabel(entry), ...buildPlatformWorldDisplayTags(entry, 3)], 4, ), [entry], ); const stats = resolvePlatformWorldStats(entry); const workActionLabel = actionMode === 'edit' ? '作品编辑' : '作品改造'; const WorkActionIcon = actionMode === 'edit' ? PencilLine : GitFork; const statItems = [ { label: '游玩', value: formatCompactCount(stats.playCount), unit: '次', icon: Gamepad2, tone: 'play', }, { label: '改造', value: formatCompactCount(stats.remixCount), unit: '次', icon: GitFork, tone: 'remix', }, { label: '点赞', value: formatCompactCount(stats.likeCount), unit: '赞', icon: Heart, tone: 'like', }, { label: '日期', value: formatPlatformWorldTime(stats.updatedAt ?? stats.publishedAt), icon: Clock3, tone: 'time', isTime: true, }, ]; useEffect(() => { setActiveCoverIndex(0); resetWorkCodeCopyState(); resetShareCopyState(); }, [ entry.profileId, coverSlides.length, resetShareCopyState, resetWorkCodeCopyState, ]); useEffect(() => { setActiveCoverIndex((current) => coverSlides.length > 0 ? Math.min(current, coverSlides.length - 1) : 0, ); }, [coverSlides.length]); useEffect(() => { if (!hasCoverCarousel) { return undefined; } const timerId = window.setInterval(() => { setActiveCoverIndex((current) => (current + 1) % coverSlides.length); }, PLATFORM_WORK_COVER_CAROUSEL_INTERVAL_MS); return () => { window.clearInterval(timerId); }; }, [coverSlides.length, hasCoverCarousel]); const showPreviousCover = () => { if (!hasCoverCarousel) { return; } setActiveCoverIndex( (current) => (current - 1 + coverSlides.length) % coverSlides.length, ); }; const showNextCover = () => { if (!hasCoverCarousel) { return; } setActiveCoverIndex((current) => (current + 1) % coverSlides.length); }; const copyPublicWorkCode = () => { if (!publicWorkCode) { return; } void copyWorkCodeText(publicWorkCode); }; const sharePublicWork = () => { if (!publicWorkCode) { return; } const shareText = `邀请你来玩《${entry.worldName}》\n作品号:${publicWorkCode}\n${buildPublicWorkDetailUrl(publicWorkCode)}`; void copyShareText(shareText); }; return (
{entry.summaryText}
{publicWorkCode ? (