mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-25 04:37:13 -05:00
Screenshot page fixes
This commit is contained in:
@@ -28,7 +28,9 @@ running capture (`LiveView`), a past session (`PastSessionView`) and a
|
||||
scanned VoD (`VodView`): header, clip strip, then match cards
|
||||
(`components/MatchCard.tsx`) newest first. Views are picked by the `view`
|
||||
search param (`scanner-search-params.ts`: `live`, `session&id=`,
|
||||
`vod&name=`, `clips`, and the debug-gated `debug` / dev-only `fixtures`).
|
||||
`vod&name=`, `clips`, `debug` and the dev-only `fixtures`). Nothing links to
|
||||
the `debug` screenshot view: dropping an image on the landing's File card
|
||||
opens it, for anyone, through the same handoff Inspect uses.
|
||||
|
||||
- **Controllers are module singletons**, not view state: the capture
|
||||
(`components/live-session.ts`) and a running VoD scan
|
||||
@@ -61,10 +63,11 @@ search param (`scanner-search-params.ts`: `live`, `session&id=`,
|
||||
`Matches` (`core/csv/matches.ts`, one row per game, the rows the cards
|
||||
render) and `Raw detections` (`core/csv/events.ts`, one row per event).
|
||||
Column names stay English keys.
|
||||
- **Debug gate** (`use-debug.ts`: DEV/ADMIN role or `?debug=true`): the
|
||||
image/screenshot view (`ScreenshotPage.tsx`), the dev-only fixtures view,
|
||||
- **Debug gate** (`use-debug.ts`: DEV/ADMIN role or `?debug=true`):
|
||||
`Save frame as fixture`, `?telemetry=true`, and the `Raw detections`
|
||||
disclosure inside a match card (the per-event cards with Inspect).
|
||||
disclosure inside a match card (the per-event cards with Inspect). The
|
||||
screenshot view (`ScreenshotPage.tsx`) is not gated; the fixtures view is
|
||||
dev-only.
|
||||
|
||||
## Clips
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/**
|
||||
* The landing: two entry cards (Live / File), the clip history strip and
|
||||
* the sessions list. Anyone can capture, scan files and get clips locally;
|
||||
* only uploading needs a login. Dropping a file anywhere here starts a scan.
|
||||
* only uploading needs a login. Dropping a file anywhere here starts a scan;
|
||||
* an image opens the screenshot view instead, through the same handoff
|
||||
* Inspect uses.
|
||||
*/
|
||||
import clsx from "clsx";
|
||||
import { Play, Upload } from "lucide-react";
|
||||
@@ -17,6 +19,7 @@ import {
|
||||
import { SCANNER_PAGE } from "~/utils/urls";
|
||||
import { scannerSearchParams } from "../scanner-search-params";
|
||||
import { MAX_HISTORY_CLIPS, type ScannerClip } from "../store/clips";
|
||||
import { newInspectKey, putInspectFrame } from "../store/inspect";
|
||||
import { ClipDialog } from "./ClipDialog";
|
||||
import { ClipStrip } from "./ClipStrip";
|
||||
import { useClips } from "./clips-feed";
|
||||
@@ -71,10 +74,20 @@ export function LandingView() {
|
||||
};
|
||||
|
||||
const scanFile = (file: File) => {
|
||||
if (file.type.startsWith("image/")) {
|
||||
void inspectScreenshot(file);
|
||||
return;
|
||||
}
|
||||
void startVodScan(file, { saveClips: saveClipsFromFile, telemetry });
|
||||
setParams({ view: "vod", name: file.name });
|
||||
};
|
||||
|
||||
const inspectScreenshot = async (file: File) => {
|
||||
const key = newInspectKey();
|
||||
await putInspectFrame(key, file);
|
||||
setParams({ view: "debug", inspect: key });
|
||||
};
|
||||
|
||||
return (
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: drag-and-drop target for the whole landing; the file input is the accessible path
|
||||
<div
|
||||
@@ -131,13 +144,16 @@ export function LandingView() {
|
||||
<Upload size={16} aria-hidden />
|
||||
File
|
||||
</h2>
|
||||
<p className={styles.cardText}>Scan a recorded VoD.</p>
|
||||
<p className={styles.cardText}>
|
||||
Scan a recorded VoD, or a screenshot to see what the scanner reads
|
||||
from it.
|
||||
</p>
|
||||
<label className={styles.dropzone}>
|
||||
Drop a video here or{" "}
|
||||
Drop a video or screenshot here or{" "}
|
||||
<span className={styles.choose}>choose file</span>
|
||||
<input
|
||||
type="file"
|
||||
accept="video/*"
|
||||
accept="video/*,image/*"
|
||||
className={styles.fileInput}
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
|
||||
@@ -20,7 +20,6 @@ import { PastSessionView } from "./PastSessionView";
|
||||
import styles from "./ScannerApp.module.css";
|
||||
import { ScreenshotPage } from "./ScreenshotPage";
|
||||
import { setUploadUser } from "./upload";
|
||||
import { useDebug } from "./use-debug";
|
||||
import { VodView } from "./VodView";
|
||||
import { cancelVodScan } from "./vod-scan";
|
||||
|
||||
@@ -35,7 +34,6 @@ export function ScannerApp() {
|
||||
const [view] = useSearchParam(scannerSearchParams, "view");
|
||||
const user = useUser();
|
||||
const live = useLiveSession();
|
||||
const debug = useDebug();
|
||||
|
||||
// the controllers run outside React and need the login to decide on uploads
|
||||
useEffect(() => {
|
||||
@@ -65,7 +63,7 @@ export function ScannerApp() {
|
||||
<VodView />
|
||||
) : view === "clips" ? (
|
||||
<ClipsView />
|
||||
) : view === "debug" && debug ? (
|
||||
) : view === "debug" ? (
|
||||
<ScreenshotPage />
|
||||
) : view === "fixtures" && process.env.NODE_ENV === "development" ? (
|
||||
<FixturesPage />
|
||||
|
||||
@@ -129,6 +129,13 @@
|
||||
}
|
||||
|
||||
/* one pill per player slot */
|
||||
.downloads {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--s-2);
|
||||
margin-block: var(--s-3);
|
||||
}
|
||||
|
||||
.statusSlots {
|
||||
display: inline-flex;
|
||||
gap: var(--s-2);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists/types";
|
||||
import { useSearchParam } from "~/modules/search-params/hooks";
|
||||
import { mainWeaponImageUrl } from "~/utils/urls";
|
||||
@@ -107,26 +108,26 @@ function LabeledCrop(props: {
|
||||
);
|
||||
}
|
||||
|
||||
/** One pill per player slot: number = alive, ★ = special held, ✗ = splatted. */
|
||||
function StatusSlots(props: { data: PlayerStatusData }) {
|
||||
/** One pill per player slot, both sides; a lit pill is a slot the flag was read on. */
|
||||
function StatusSlots(props: {
|
||||
flags: PlayerStatusData["special"] | PlayerStatusData["dead"];
|
||||
tone: "special" | "dead";
|
||||
}) {
|
||||
return (
|
||||
<span className={styles.statusSlots}>
|
||||
{([0, 1] as const).map((side) => (
|
||||
<span key={side} className={styles.statusSide}>
|
||||
{props.data.dead[side].map((dead, slot) => {
|
||||
const special = !dead && props.data.special[side][slot];
|
||||
return (
|
||||
<span
|
||||
key={slot}
|
||||
className={clsx(styles.statusSlot, {
|
||||
[styles.dead]: dead,
|
||||
[styles.special]: special,
|
||||
})}
|
||||
>
|
||||
{dead ? "✗" : special ? "★" : slot + 1}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{props.flags.map((side, sideIndex) => (
|
||||
<span key={sideIndex} className={styles.statusSide}>
|
||||
{side.map((on, slot) => (
|
||||
<span
|
||||
key={slot}
|
||||
className={clsx(styles.statusSlot, {
|
||||
[styles.dead]: on && props.tone === "dead",
|
||||
[styles.special]: on && props.tone === "special",
|
||||
})}
|
||||
>
|
||||
{slot + 1}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
@@ -554,18 +555,20 @@ export function ScreenshotPage() {
|
||||
</div>
|
||||
|
||||
{frame && !busy ? (
|
||||
<p>
|
||||
<button
|
||||
type="button"
|
||||
<div className={styles.downloads}>
|
||||
<SendouButton
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
downloadExpectedJson(event?.data ?? null, event?.type)
|
||||
}
|
||||
>
|
||||
Download expected.json
|
||||
</button>{" "}
|
||||
<button
|
||||
type="button"
|
||||
disabled={!event}
|
||||
</SendouButton>
|
||||
<SendouButton
|
||||
variant="outlined"
|
||||
size="small"
|
||||
isDisabled={!event}
|
||||
onClick={() =>
|
||||
downloadCsv(
|
||||
"screenshot-events.csv",
|
||||
@@ -574,8 +577,8 @@ export function ScreenshotPage() {
|
||||
}
|
||||
>
|
||||
Download CSV
|
||||
</button>
|
||||
</p>
|
||||
</SendouButton>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{Object.keys(results).length > 0 ? (
|
||||
@@ -834,8 +837,14 @@ export function ScreenshotPage() {
|
||||
{status ? (
|
||||
<>
|
||||
<Stat label="layout">{status.data.layout}</Stat>
|
||||
<Stat label="players">
|
||||
<StatusSlots data={status.data} />
|
||||
<Stat label="special">
|
||||
<StatusSlots
|
||||
flags={status.data.special}
|
||||
tone="special"
|
||||
/>
|
||||
</Stat>
|
||||
<Stat label="splatted">
|
||||
<StatusSlots flags={status.data.dead} tone="dead" />
|
||||
</Stat>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* The settings popover, opened from ⚙ on the landing and the live header:
|
||||
* the upload and clip toggles, the retention notes, and the debug links for
|
||||
* those who see them. The
|
||||
* source lives on the landing's Live card, the one place it must be right.
|
||||
* the upload and clip toggles, the retention notes, and in development the
|
||||
* fixtures link. The source lives on the landing's Live card, the one place
|
||||
* it must be right.
|
||||
*/
|
||||
import { Settings } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
@@ -25,14 +25,12 @@ import {
|
||||
useScannerSettings,
|
||||
} from "./settings";
|
||||
import { isLoggedIn } from "./upload";
|
||||
import { useDebug } from "./use-debug";
|
||||
|
||||
/** a step of one frame-ish: fine enough to tune by ear, coarse enough to reach a second in a few clicks */
|
||||
const AUDIO_OFFSET_STEP_MS = 25;
|
||||
|
||||
export function SettingsPopover() {
|
||||
const settings = useScannerSettings();
|
||||
const debug = useDebug();
|
||||
const loggedIn = isLoggedIn();
|
||||
|
||||
return (
|
||||
@@ -122,26 +120,18 @@ export function SettingsPopover() {
|
||||
<br />
|
||||
Sessions: last 30 days or {MAX_SESSIONS} sessions.
|
||||
</p>
|
||||
{debug ? (
|
||||
{process.env.NODE_ENV === "development" ? (
|
||||
<section className={styles.section}>
|
||||
<span className={styles.label}>Debug</span>
|
||||
<div className={styles.row}>
|
||||
<Link
|
||||
to={scannerSearchParams.href(SCANNER_PAGE, { view: "debug" })}
|
||||
to={scannerSearchParams.href(SCANNER_PAGE, {
|
||||
view: "fixtures",
|
||||
})}
|
||||
defaultShouldRevalidate={false}
|
||||
>
|
||||
Image / screenshot view
|
||||
Fixtures
|
||||
</Link>
|
||||
{process.env.NODE_ENV === "development" ? (
|
||||
<Link
|
||||
to={scannerSearchParams.href(SCANNER_PAGE, {
|
||||
view: "fixtures",
|
||||
})}
|
||||
defaultShouldRevalidate={false}
|
||||
>
|
||||
Fixtures
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user