/** * Single dispatch point from a detected event to its card, shared by the live * and VoD feeds. Frames load lazily through `getFrame` (IndexedDB keeps them * out of the listed records); the Inspect action (open the frame in the * screenshot page in a new tab, leaving the running scan undisturbed) is * derived from it here. */ import clsx from "clsx"; import { SCANNER_PAGE } from "~/utils/urls"; import type { PlayerAbilityMap } from "../core/ability-harvest"; import { DEATH_EVENT_TYPE, type DeathData, } from "../core/detectors/death/index"; import { KILL_EVENT_TYPE, type KillData } from "../core/detectors/kill/index"; import { MAP_START_EVENT_TYPE, type MapStartData, } from "../core/detectors/map-start/index"; import { MINIMAP_EVENT_TYPE, type MinimapData, } from "../core/detectors/minimap/index"; import { OBJECTIVE_EVENT_TYPE, type ObjectiveData, } from "../core/detectors/objective/index"; import { PLAYER_STATUS_EVENT_TYPE, type PlayerStatusData, } from "../core/detectors/objective/player-status"; import { STRIP_WEAPONS_EVENT_TYPE, type StripWeaponsData, } from "../core/detectors/objective/strip-weapons"; import type { ScoreboardData } from "../core/detectors/scoreboard/index"; import { SCOREBOARD_OWN_EVENT_TYPE, type ScoreboardOwnData, } from "../core/detectors/scoreboard-own/index"; import { scannerSearchParams } from "../scanner-search-params"; import type { SendStatus } from "../store/events"; import { newInspectKey, putInspectFrame } from "../store/inspect"; import { DeathCard } from "./DeathCard"; import styles from "./EventCard.module.css"; import type { FixtureData } from "./fixture-export"; import { useEventTimeFormatter } from "./format"; import { KillCard } from "./KillCard"; import { MapStartCard } from "./MapStartCard"; import { MinimapCard } from "./MinimapCard"; import { ObjectiveCard } from "./ObjectiveCard"; import { PlayerStatusCard } from "./PlayerStatusCard"; import { ScoreboardCard } from "./ScoreboardCard"; import { ScoreboardOwnCard } from "./ScoreboardOwnCard"; import { StripWeaponsCard } from "./StripWeaponsCard"; export type GetFrame = () => Promise; export function EventCard(props: { type: string; t: number; confidence: number; data: FixtureData; thumbnail?: string; detectedAt?: number; /** lazy loader for the exact analyzed frame; enables Inspect + fixture export */ getFrame?: GetFrame; /** Scoreboard only: abilities harvested from the match's death events */ abilities?: PlayerAbilityMap; /** sendou.ink /ingest status of this event; absent = never attempted */ send?: SendStatus; /** when set, shows a Send/Retry button that sends this event's match batch */ onSend?: () => void; }) { const { type, t, confidence, data, thumbnail, detectedAt, getFrame } = props; // window.open must run synchronously in the click gesture (popup blockers); // the frame write catches up and the new tab polls for it const onInspect = getFrame ? () => { const key = newInspectKey(); window.open( scannerSearchParams.href(SCANNER_PAGE, { tab: "screenshot", inspect: key, }), "_blank", ); void getFrame().then((frame) => { if (frame) void putInspectFrame(key, frame); }); } : undefined; const shared = { t, confidence, thumbnail, detectedAt, getFrame, onInspect }; const card = renderCard(type, data, shared, props.abilities); if (!props.send && !props.onSend) return card; return (
{card}
); } /** `unlinked` has no strip styling of its own */ const SEND_STATE_CLASS: Record = { queued: styles.queued, sending: styles.sending, sent: styles.sent, unlinked: undefined, failed: styles.failed, }; const SEND_LABELS: Record = { queued: "queued", sending: "sending…", sent: "sent", unlinked: "waiting for report", failed: "failed", }; function SendStrip({ send, onSend, }: { send?: SendStatus; onSend?: () => void; }) { const state = send?.state; const formatSentAt = useEventTimeFormatter(); return (
sendou.ink: {state ? SEND_LABELS[state] : "not sent"} {state === "sent" && send ? ` ${formatSentAt(send.at)}` : null} {send?.error ? ( {send.error} ) : null} {onSend && state !== "sent" && state !== "sending" ? ( ) : null}
); } function renderCard( type: string, data: FixtureData, shared: { t: number; confidence: number; thumbnail?: string; detectedAt?: number; getFrame?: GetFrame; onInspect?: () => void; }, abilities?: PlayerAbilityMap, ) { return type === DEATH_EVENT_TYPE ? ( ) : type === KILL_EVENT_TYPE ? ( ) : type === MAP_START_EVENT_TYPE ? ( ) : type === SCOREBOARD_OWN_EVENT_TYPE ? ( ) : type === MINIMAP_EVENT_TYPE ? ( ) : type === OBJECTIVE_EVENT_TYPE ? ( ) : type === PLAYER_STATUS_EVENT_TYPE ? ( ) : type === STRIP_WEAPONS_EVENT_TYPE ? ( ) : ( ); }