import {useState} from 'react'; import {useTranslation} from 'react-i18next'; import {Clock, Ellipsis, ExternalLink, Eye, EyeOff, Gamepad2, Image as ImageIcon, ImageOff, Merge, PanelTop, PencilLine, RotateCcw, Timer} from 'lucide-react'; import {toast} from 'sonner'; import {Badge} from '@/components/ui/badge'; import {Button} from '@/components/ui/button'; import {Card} from '@/components/ui/card'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import {cn} from '@/lib/utils'; import GameCover from './GameCover.jsx'; import FitText from './FitText.jsx'; import ComingBadge from './ComingBadge.jsx'; import ProfileArtItems from './ProfileArtItems.jsx'; import RatingStars from './RatingStars.jsx'; import StatusMenu from './StatusMenu.jsx'; import TimeToBeat from './TimeToBeat.jsx'; import useSaveAppearance from '../hooks/useSaveAppearance.js'; import useSaveBacklogField from '../hooks/useSaveBacklogField.js'; import useSetStatus from '../hooks/useSetStatus.js'; import {usePreferences} from '../context/PreferencesContext.jsx'; import {UNDO_MS} from '../utils/backlog.js'; import {formatPlaytime} from '../utils/games.js'; import {setGameHidden} from '../utils/hiddenGames.js'; import {isSteamKey} from '../utils/manualGames.js'; import {platformLabel} from '../utils/platforms.js'; import {coverEffect, detailEffect} from '../utils/rewards.js'; const SPEEDRUN_PREFIX = 'https://www.speedrun.com/'; // The spans of each completed effect decoration; effects.css places them. const DETAIL_ELEMENTS = {'fx-detail-gold-shine': 8, 'fx-detail-neon-pulse': 1, 'fx-detail-holo': 1, 'fx-detail-ember': 6, 'fx-detail-platinum': 6}; /** * Status and rating are edited here and save at once. A status change needs no points check: GameDetail's * GamePointsLine sees the new status and does it. * * @param onSaved gets the saved backlog entry after a status or rating change */ export default function GameHeroCard({game, auditor, profileStyle, onCorrect, onMerge, onHiddenChanged, onProfileStyleSaved, onSaved}) { const {t} = useTranslation(); const {appearance, equipped} = usePreferences(); const saveAppearance = useSaveAppearance(); const [hiding, setHiding] = useState(false); const {setStatus, busy: statusBusy} = useSetStatus(game.appid, {onChanged: onSaved}); const {save, pending, busy: ratingBusy} = useSaveBacklogField(game.appid, {onSaved}); const onSteam = isSteamKey(game.appid); // Core checks the link too; a link from the API must never run script. const speedrunUrl = typeof game.speedrunUrl === 'string' && game.speedrunUrl.startsWith(SPEEDRUN_PREFIX) ? game.speedrunUrl : null; const isBackground = appearance.backgroundAppid === game.appid; const isBanner = appearance.showBanner && appearance.bannerAppid === game.appid; const detailFx = detailEffect(equipped, game.status); const applyHidden = async (hidden, {undoable = false} = {}) => { setHiding(true); try { const response = await setGameHidden(game.appid, hidden); if (response.status === 401) return; if (!response.ok) { toast.error(t('GAME_DETAIL.HIDE_ERROR')); return; } onHiddenChanged(hidden); const message = t(hidden ? 'GAME_DETAIL.HIDDEN_TOAST' : 'GAME_DETAIL.SHOWN_TOAST'); if (undoable) { toast.success(message, {duration: UNDO_MS, action: {label: t('MANUAL_GAME.UNDO'), onClick: () => applyHidden(!hidden)}}); } else { toast.success(message); } } catch (err) { console.error('Hide game error:', err); toast.error(t('INFO.ERROR.NETWORK_FAILURE')); } finally { setHiding(false); } }; return ( {detailFx && ( )}

{t('GAME_DETAIL.PLAYTIME', {time: formatPlaytime(game.totalPlaytimeMinutes ?? game.playtimeMinutes)})} setStatus(status, {previous: game.status})}/> {platformLabel(game) && ( {platformLabel(game)} )}
{t('BACKLOG.RATING_LABEL')} save('rating', value)} labelledBy="hero-rating" disabled={ratingBusy}/>
{speedrunUrl && ( )}
{/* Background, banner and profile art need Steam art, so a game off Steam has none of them. */}
{onSteam && ( )} {onSteam && (<> {isBackground ? ( saveAppearance({backgroundAppid: null})}> {t('GAME_DETAIL.REMOVE_BACKGROUND')} ) : ( saveAppearance({backgroundAppid: game.appid})}> {t('GAME_DETAIL.SET_BACKGROUND')} )} {isBanner ? ( saveAppearance({bannerAppid: null})}> {t('SETTINGS.BANNER_AUTO')} ) : ( saveAppearance({bannerAppid: game.appid, showBanner: true})}> {t('GAME_DETAIL.SET_BANNER')} )} {profileStyle && ( )} )} applyHidden(!game.hidden, {undoable: true})}> {game.hidden ? : } {t(game.hidden ? 'GAME_DETAIL.UNHIDE' : 'GAME_DETAIL.HIDE')} {auditor && (<> {t('CORRECTION.OPEN')} {t('MERGE.OPEN')} )}
); }