146 lines
4.8 KiB
JavaScript
146 lines
4.8 KiB
JavaScript
import React, { useEffect, useMemo, useState } from 'react'
|
|
|
|
function getScreenshotName(item, fallbackIndex) {
|
|
if (item && typeof item === 'object' && typeof item.name === 'string' && item.name.trim()) {
|
|
return item.name.trim()
|
|
}
|
|
|
|
return `Screenshot ${fallbackIndex + 1}`
|
|
}
|
|
|
|
function resolveScreenshotSource(item) {
|
|
if (!item) return { src: null, revoke: null }
|
|
|
|
if (typeof item === 'string') {
|
|
return { src: item, revoke: null }
|
|
}
|
|
|
|
if (typeof item === 'object') {
|
|
if (typeof item.preview === 'string' && item.preview) {
|
|
return { src: item.preview, revoke: null }
|
|
}
|
|
|
|
if (typeof item.src === 'string' && item.src) {
|
|
return { src: item.src, revoke: null }
|
|
}
|
|
|
|
if (typeof item.url === 'string' && item.url) {
|
|
return { src: item.url, revoke: null }
|
|
}
|
|
|
|
if (typeof File !== 'undefined' && item instanceof File) {
|
|
const objectUrl = URL.createObjectURL(item)
|
|
return {
|
|
src: objectUrl,
|
|
revoke: () => URL.revokeObjectURL(objectUrl),
|
|
}
|
|
}
|
|
}
|
|
|
|
return { src: null, revoke: null }
|
|
}
|
|
|
|
export default function ArchiveScreenshotPicker({
|
|
screenshots = [],
|
|
selectedIndex = 0,
|
|
onSelect,
|
|
compact = false,
|
|
title = 'Screenshots',
|
|
description = 'Choose which screenshot should be used as the default preview.',
|
|
}) {
|
|
const [resolvedScreenshots, setResolvedScreenshots] = useState([])
|
|
|
|
useEffect(() => {
|
|
const cleanup = []
|
|
const next = (Array.isArray(screenshots) ? screenshots : []).map((item, index) => {
|
|
const { src, revoke } = resolveScreenshotSource(item)
|
|
if (revoke) cleanup.push(revoke)
|
|
|
|
return {
|
|
src,
|
|
alt: getScreenshotName(item, index),
|
|
}
|
|
}).filter((item) => Boolean(item.src))
|
|
|
|
setResolvedScreenshots(next)
|
|
|
|
return () => {
|
|
cleanup.forEach((revoke) => revoke())
|
|
}
|
|
}, [screenshots])
|
|
|
|
const normalizedIndex = useMemo(() => {
|
|
if (resolvedScreenshots.length === 0) return 0
|
|
if (!Number.isFinite(selectedIndex)) return 0
|
|
return Math.min(Math.max(0, Math.floor(selectedIndex)), resolvedScreenshots.length - 1)
|
|
}, [resolvedScreenshots.length, selectedIndex])
|
|
|
|
const selectedScreenshot = resolvedScreenshots[normalizedIndex] ?? null
|
|
|
|
if (!selectedScreenshot) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className={compact ? 'space-y-3' : 'space-y-4'}>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div>
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-white/45">{title}</p>
|
|
<p className="mt-1 text-xs text-white/55">{description}</p>
|
|
</div>
|
|
<span className="rounded-full border border-emerald-300/30 bg-emerald-400/10 px-2.5 py-1 text-[11px] text-emerald-100">
|
|
Default preview
|
|
</span>
|
|
</div>
|
|
|
|
<div className={compact ? 'overflow-hidden rounded-2xl border border-white/10 bg-black/25' : 'overflow-hidden rounded-3xl border border-white/10 bg-black/25'}>
|
|
<img
|
|
src={selectedScreenshot.src}
|
|
alt={selectedScreenshot.alt}
|
|
className={compact ? 'h-40 w-full object-cover' : 'h-56 w-full object-cover sm:h-72'}
|
|
loading="lazy"
|
|
decoding="async"
|
|
/>
|
|
</div>
|
|
|
|
<div className={compact ? 'grid grid-cols-4 gap-2' : 'grid grid-cols-2 gap-3 sm:grid-cols-4'}>
|
|
{resolvedScreenshots.map((item, index) => {
|
|
const isSelected = index === normalizedIndex
|
|
|
|
return (
|
|
<button
|
|
key={`${item.src}-${index}`}
|
|
type="button"
|
|
onClick={() => onSelect?.(index)}
|
|
aria-label={`Use ${item.alt} as default screenshot`}
|
|
className={[
|
|
'group relative overflow-hidden rounded-2xl border text-left transition',
|
|
isSelected
|
|
? 'border-emerald-300/45 bg-emerald-400/10 shadow-[0_0_0_1px_rgba(52,211,153,0.22)]'
|
|
: 'border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.05]',
|
|
].join(' ')}
|
|
aria-pressed={isSelected}
|
|
>
|
|
<img
|
|
src={item.src}
|
|
alt={item.alt}
|
|
className={compact ? 'h-16 w-full object-cover' : 'h-20 w-full object-cover'}
|
|
loading="lazy"
|
|
decoding="async"
|
|
/>
|
|
<div className="flex items-center justify-between gap-2 px-2.5 py-2">
|
|
<span className="truncate text-[11px] text-white/70">{item.alt}</span>
|
|
<span className={[
|
|
'shrink-0 rounded-full px-2 py-0.5 text-[10px]',
|
|
isSelected ? 'bg-emerald-300/20 text-emerald-100' : 'bg-white/10 text-white/45',
|
|
].join(' ')}>
|
|
{isSelected ? 'Default' : 'Use'}
|
|
</span>
|
|
</div>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
} |