Release v0.1.8 2026-07-13
This commit is contained in:
+167
-53
@@ -1,6 +1,7 @@
|
||||
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { buildCollections, MAX_HUNTER_NAME_LENGTH, MODE_COPY, normalizeHunterName } from "../frontend/data";
|
||||
import { formatPlayTime, formatSaveTimestamp } from "../frontend/saveRepository";
|
||||
import { resolveSaveContinuation, saveVersionsMatch } from "../frontend/saveContinuation";
|
||||
import { useActiveHunter, useFrontendStore } from "../frontend/store";
|
||||
import type { GameModeId, SaveSlotId, SaveSlotState } from "../frontend/types";
|
||||
import { useMenuController, type MenuAction } from "../input/useMenuController";
|
||||
@@ -80,11 +81,46 @@ function ControllerLegend({ back = false }: { back?: boolean }) {
|
||||
return <div className="controller-legend"><span><b>{DEFAULT_CONTROLLER_GLYPHS.confirm}</b> Select</span>{back && <span><b>{DEFAULT_CONTROLLER_GLYPHS.back}</b> Back</span>}<span><b>+</b> Navigate</span></div>;
|
||||
}
|
||||
|
||||
function SaveLibraryContext({ slots, accountId }: { slots: readonly SaveSlotState[]; accountId: string | null }) {
|
||||
return (
|
||||
<FrontSurface className="login-save-context" bottom ariaLabel="Save slot information">
|
||||
<header className="context-header"><span>Device saves</span><b>{accountId ? "SERVER LINKED" : "OFFLINE READY"}</b></header>
|
||||
<div className="login-save-list">
|
||||
{slots.map((slot) => {
|
||||
const save = slot.local ?? slot.online;
|
||||
const healer = save ? HEALER_CLASSES[save.activeClassId] : null;
|
||||
return (
|
||||
<article key={slot.id} className={save ? "has-save" : "is-empty"}>
|
||||
<b>{String(slot.id).padStart(2, "0")}</b>
|
||||
{save ? (
|
||||
<>
|
||||
<div className="login-save-avatar">{save.hunterName[0]}</div>
|
||||
<span>
|
||||
<small>{slot.local ? "On this Thor" : "Online copy"}</small>
|
||||
<strong>{save.hunterName}</strong>
|
||||
<em>Level {save.healers[save.activeClassId].level} {healer?.name} · {save.location}</em>
|
||||
</span>
|
||||
<time><strong>{formatPlayTime(save.playSeconds)}</strong><small>{formatSaveTimestamp(save.updatedAt)}</small></time>
|
||||
</>
|
||||
) : (
|
||||
<span className="login-empty-copy"><small>Available slot</small><strong>New hunter</strong><em>Continue offline to create</em></span>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<footer className="login-save-footer"><span>Save details update from upper-screen selection</span><b>LOWER DISPLAY · INFORMATION ONLY</b></footer>
|
||||
</FrontSurface>
|
||||
);
|
||||
}
|
||||
|
||||
function LoginScreen() {
|
||||
const restoreSession = useFrontendStore((state) => state.restoreSession);
|
||||
const signIn = useFrontendStore((state) => state.signIn);
|
||||
const createAccount = useFrontendStore((state) => state.createAccount);
|
||||
const continueOffline = useFrontendStore((state) => state.continueOffline);
|
||||
const slots = useFrontendStore((state) => state.slots);
|
||||
const accountId = useFrontendStore((state) => state.accountId);
|
||||
const notice = useFrontendStore((state) => state.notice);
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
@@ -101,7 +137,7 @@ function LoginScreen() {
|
||||
{ id: "password", run: () => passwordRef.current?.focus() },
|
||||
{ id: "sign-in", run: () => { void signIn(username, password); } },
|
||||
{ id: "create-account", run: () => { void createAccount(username, password); } },
|
||||
{ id: "offline", run: continueOffline },
|
||||
{ id: "continue", run: continueOffline },
|
||||
], [continueOffline, createAccount, password, signIn, username]);
|
||||
const controller = useMenuController(actions);
|
||||
|
||||
@@ -116,7 +152,7 @@ function LoginScreen() {
|
||||
<div className="login-copy">
|
||||
<span>Offline-first hunter records</span>
|
||||
<h1>Keep everyone standing.</h1>
|
||||
<p>Your save always lives on this device. Sign in only when you want a second copy for PC ↔ AYN Thor handoff.</p>
|
||||
<p>Continue to your saved hunters. Sign in when you want online copies for PC ↔ AYN Thor handoff.</p>
|
||||
</div>
|
||||
<form className="login-panel" onSubmit={(event) => { event.preventDefault(); submitSignIn(); }}>
|
||||
<label htmlFor="account-username">Username</label>
|
||||
@@ -153,38 +189,27 @@ function LoginScreen() {
|
||||
<FocusButton id="create-account" focusedId={controller.focusedId} focus={controller.focus} className="front-secondary" type="button" onClick={() => { void createAccount(username, password); }}>
|
||||
<span>Create account</span><small>Required for first sync</small>
|
||||
</FocusButton>
|
||||
<FocusButton id="offline" focusedId={controller.focusedId} focus={controller.focus} className="front-secondary" type="button" onClick={continueOffline}>
|
||||
<span>Continue with offline save</span><small>No account required</small>
|
||||
<FocusButton id="continue" focusedId={controller.focusedId} focus={controller.focus} className="front-secondary" type="button" onClick={continueOffline}>
|
||||
<span>Continue</span><small>Choose saved hunter</small>
|
||||
</FocusButton>
|
||||
</form>
|
||||
{notice && <div className="front-notice" role="status" aria-live="polite">{notice}</div>}
|
||||
<ControllerLegend />
|
||||
</FrontSurface>
|
||||
}
|
||||
bottom={
|
||||
<FrontSurface className="login-context" bottom ariaLabel="Offline save explanation">
|
||||
<BrandMark compact />
|
||||
<div className="offline-promise">
|
||||
<span className="context-kicker">How saving works</span>
|
||||
<ol>
|
||||
<li><b>01</b><span><strong>Play offline</strong><small>Every change writes to device storage first.</small></span></li>
|
||||
<li><b>02</b><span><strong>Create or sign in</strong><small>Account is secured by the TrueNAS game server.</small></span></li>
|
||||
<li><b>03</b><span><strong>Move devices</strong><small>Upload or download any of your three server save slots.</small></span></li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="device-route"><span>PC</span><i>↔</i><b>ONLINE COPY</b><i>↔</i><span>THOR</span></div>
|
||||
</FrontSurface>
|
||||
}
|
||||
bottom={<SaveLibraryContext slots={slots} accountId={accountId} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SlotCard({ slot, selected, focused, onSelect, onFocus }: { slot: SaveSlotState; selected: boolean; focused: boolean; onSelect: () => void; onFocus: () => void }) {
|
||||
const save = slot.local;
|
||||
const continuation = resolveSaveContinuation(slot);
|
||||
const save = slot.local ?? slot.online;
|
||||
const healer = save ? HEALER_CLASSES[save.activeClassId] : null;
|
||||
const copyStatus = continuation === "choose" ? "Newer online" : continuation === "online" ? "Online only" : null;
|
||||
return (
|
||||
<button className={`save-slot ${selected ? "is-selected" : ""} ${focused ? "is-controller-focused" : ""}`} onClick={onSelect} onFocus={onFocus} onPointerEnter={onFocus}>
|
||||
<span className="slot-number">Slot {String(slot.id).padStart(2, "0")}</span>
|
||||
<span className="slot-number">Slot {String(slot.id).padStart(2, "0")}{copyStatus && <b>{copyStatus}</b>}</span>
|
||||
{save ? (
|
||||
<>
|
||||
<div className="slot-portrait">{save.hunterName[0]}<i>✦</i></div>
|
||||
@@ -213,11 +238,15 @@ function SaveScreen() {
|
||||
const copySlot = useFrontendStore((state) => state.copySlot);
|
||||
const deleteSlot = useFrontendStore((state) => state.deleteSlot);
|
||||
const navigate = useFrontendStore((state) => state.navigate);
|
||||
const [dialog, setDialog] = useState<"create" | "copy" | "delete" | null>(null);
|
||||
const [dialog, setDialog] = useState<"create" | "copy" | "delete" | "version" | null>(null);
|
||||
const [hunterName, setHunterName] = useState("");
|
||||
const [resolvingOnline, setResolvingOnline] = useState(false);
|
||||
const [versionError, setVersionError] = useState("");
|
||||
const selected = slots.find((slot) => slot.id === selectedSlotId)!;
|
||||
const hasLocal = Boolean(selected.local);
|
||||
const hasOnline = Boolean(selected.online);
|
||||
const continuation = resolveSaveContinuation(selected);
|
||||
const primaryActionId = continuation === "create" ? "create" : "play";
|
||||
|
||||
const finishCreation = () => {
|
||||
if (createSlot(selectedSlotId, hunterName)) {
|
||||
@@ -235,7 +264,41 @@ function SaveScreen() {
|
||||
requestDisplaySurface("top");
|
||||
};
|
||||
|
||||
const actions = useMemo<MenuAction[]>(() => dialog === "create"
|
||||
const continueWithOnline = async () => {
|
||||
if (resolvingOnline) return;
|
||||
setResolvingOnline(true);
|
||||
setVersionError("");
|
||||
await downloadSlot(selectedSlotId);
|
||||
const refreshed = useFrontendStore.getState().slots.find((slot) => slot.id === selectedSlotId);
|
||||
if (refreshed && saveVersionsMatch(refreshed.local, refreshed.online)) {
|
||||
setDialog(null);
|
||||
setResolvingOnline(false);
|
||||
playSlot(selectedSlotId);
|
||||
return;
|
||||
}
|
||||
setVersionError(useFrontendStore.getState().notice || "Online save could not be loaded.");
|
||||
setResolvingOnline(false);
|
||||
};
|
||||
|
||||
const continueSelected = () => {
|
||||
if (continuation === "create") return openCreation();
|
||||
if (continuation === "local") return playSlot(selectedSlotId);
|
||||
if (continuation === "online") {
|
||||
void continueWithOnline();
|
||||
return;
|
||||
}
|
||||
setVersionError("");
|
||||
setDialog("version");
|
||||
requestDisplaySurface("top");
|
||||
};
|
||||
|
||||
const actions = useMemo<MenuAction[]>(() => dialog === "version"
|
||||
? [
|
||||
{ id: "version-online", run: () => { void continueWithOnline(); }, enabled: !resolvingOnline },
|
||||
{ id: "version-local", run: () => { setDialog(null); playSlot(selectedSlotId); }, enabled: !resolvingOnline },
|
||||
{ id: "cancel-version", run: () => setDialog(null), enabled: !resolvingOnline },
|
||||
]
|
||||
: dialog === "create"
|
||||
? [
|
||||
{ id: "confirm-create", run: finishCreation },
|
||||
{ id: "cancel-create", run: () => setDialog(null) },
|
||||
@@ -248,17 +311,25 @@ function SaveScreen() {
|
||||
{ id: "cancel-delete", run: () => setDialog(null) },
|
||||
]
|
||||
: [
|
||||
...slots.map((slot) => ({ id: `slot-${slot.id}`, run: () => selectSlot(slot.id) })),
|
||||
{ id: hasLocal ? "play" : "create", run: () => hasLocal ? playSlot(selectedSlotId) : openCreation() },
|
||||
{ id: "upload", run: () => uploadSlot(selectedSlotId), enabled: hasLocal && Boolean(accountId) },
|
||||
{ id: "download", run: () => downloadSlot(selectedSlotId), enabled: hasOnline && Boolean(accountId) },
|
||||
{ id: "copy", run: () => openSaveDialog("copy"), enabled: hasLocal },
|
||||
{ id: "delete", run: () => openSaveDialog("delete"), enabled: hasLocal },
|
||||
{ id: "back", run: () => navigate("login") },
|
||||
], [accountId, copySlot, createSlot, deleteSlot, dialog, downloadSlot, hasLocal, hasOnline, hunterName, navigate, playSlot, selectSlot, selectedSlotId, slots, uploadSlot]);
|
||||
const controller = useMenuController(actions, { onBack: () => dialog ? setDialog(null) : navigate("login") });
|
||||
...slots.map((slot, index) => ({
|
||||
id: `slot-${slot.id}`,
|
||||
run: () => selectSlot(slot.id),
|
||||
neighbors: {
|
||||
left: `slot-${slots[Math.max(0, index - 1)].id}`,
|
||||
right: `slot-${slots[Math.min(slots.length - 1, index + 1)].id}`,
|
||||
down: primaryActionId,
|
||||
},
|
||||
})),
|
||||
{ id: primaryActionId, run: continueSelected, enabled: !resolvingOnline, neighbors: { up: `slot-${selectedSlotId}`, right: "upload" } },
|
||||
{ id: "upload", run: () => uploadSlot(selectedSlotId), enabled: hasLocal && Boolean(accountId), neighbors: { left: primaryActionId, right: "download", up: "slot-1" } },
|
||||
{ id: "download", run: () => downloadSlot(selectedSlotId), enabled: hasOnline && Boolean(accountId), neighbors: { left: "upload", right: "copy", up: "slot-2" } },
|
||||
{ id: "copy", run: () => openSaveDialog("copy"), enabled: hasLocal, neighbors: { left: "download", right: "delete", up: "slot-2" } },
|
||||
{ id: "delete", run: () => openSaveDialog("delete"), enabled: hasLocal, neighbors: { left: "copy", right: "back", up: "slot-3" } },
|
||||
{ id: "back", run: () => navigate("login"), neighbors: { left: "delete", up: "slot-3" } },
|
||||
], [accountId, continuation, copySlot, createSlot, deleteSlot, dialog, downloadSlot, hasLocal, hasOnline, hunterName, navigate, playSlot, primaryActionId, resolvingOnline, selectSlot, selectedSlotId, slots, uploadSlot]);
|
||||
const controller = useMenuController(actions, { onBack: () => dialog ? resolvingOnline ? undefined : setDialog(null) : navigate("login") });
|
||||
|
||||
const cloudStatus = !accountId ? "Offline mode" : selected.online ? "Online version available" : "No online version";
|
||||
const cloudStatus = continuation === "choose" ? "Newer online save" : !accountId ? "Offline mode" : selected.online ? "Online version available" : "No online version";
|
||||
return (
|
||||
<DualDisplayFrame
|
||||
top={
|
||||
@@ -276,10 +347,47 @@ function SaveScreen() {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="save-footer"><span>Autosave <b>OFFLINE FIRST</b></span><ControllerLegend back /></div>
|
||||
<div className="save-top-actions">
|
||||
<FocusButton id={primaryActionId} focusedId={controller.focusedId} focus={controller.focus} className="front-primary" disabled={resolvingOnline} onClick={continueSelected}>
|
||||
<span>{continuation === "create" ? "Create hunter" : resolvingOnline ? "Loading online save…" : "Continue"}</span>
|
||||
<small>{continuation === "create" ? `Use slot ${selectedSlotId}` : continuation === "online" ? "Download online copy" : continuation === "choose" ? "Choose online or device copy" : `Slot ${selectedSlotId} · ${selected.local?.hunterName}`}</small>
|
||||
</FocusButton>
|
||||
<FocusButton id="upload" focusedId={controller.focusedId} focus={controller.focus} disabled={!hasLocal || !accountId} onClick={() => uploadSlot(selectedSlotId)}><strong>Upload</strong><small>Device → server</small></FocusButton>
|
||||
<FocusButton id="download" focusedId={controller.focusedId} focus={controller.focus} disabled={!hasOnline || !accountId} onClick={() => downloadSlot(selectedSlotId)}><strong>Download</strong><small>Server → device</small></FocusButton>
|
||||
<FocusButton id="copy" focusedId={controller.focusedId} focus={controller.focus} disabled={!hasLocal} onClick={() => openSaveDialog("copy")}><strong>Copy</strong><small>Duplicate save</small></FocusButton>
|
||||
<FocusButton id="delete" focusedId={controller.focusedId} focus={controller.focus} disabled={!hasLocal} className="danger-link" onClick={() => openSaveDialog("delete")}><strong>Delete</strong><small>Erase device copy</small></FocusButton>
|
||||
<FocusButton id="back" focusedId={controller.focusedId} focus={controller.focus} onClick={() => navigate("login")}><strong>Back</strong><small>Login screen</small></FocusButton>
|
||||
</div>
|
||||
<div className="save-footer"><span>Autosave <b>OFFLINE FIRST</b></span><div className="save-top-notice" role="status" aria-live="polite">{notice || "Lower display shows selected save details."}</div><ControllerLegend back /></div>
|
||||
{dialog && (
|
||||
<div className="front-dialog" role="dialog" aria-modal="true" aria-label={dialog === "create" ? "Name new hunter" : dialog === "copy" ? "Copy save" : "Delete save"}>
|
||||
{dialog === "create" ? (
|
||||
<div className={`front-dialog ${dialog === "version" ? "version-dialog" : ""}`} role="dialog" aria-modal="true" aria-label={dialog === "version" ? "Choose save version" : dialog === "create" ? "Name new hunter" : dialog === "copy" ? "Copy save" : "Delete save"}>
|
||||
{dialog === "version" && selected.local && selected.online ? (
|
||||
<div className="version-choice-dialog">
|
||||
<span>Newer online save found</span>
|
||||
<h2>Which save do you want?</h2>
|
||||
<p>Online choice replaces this device copy. Device choice keeps online copy unchanged.</p>
|
||||
<div className="version-comparison">
|
||||
<article className="is-newer">
|
||||
<header><span>Online copy</span><b>NEWER</b></header>
|
||||
<strong>{selected.online.hunterName}</strong>
|
||||
<time>{formatSaveTimestamp(selected.online.updatedAt)}</time>
|
||||
<small>{formatPlayTime(selected.online.playSeconds)} · Level {selected.online.healers[selected.online.activeClassId].level}</small>
|
||||
</article>
|
||||
<article>
|
||||
<header><span>Device copy</span><b>OFFLINE</b></header>
|
||||
<strong>{selected.local.hunterName}</strong>
|
||||
<time>{formatSaveTimestamp(selected.local.updatedAt)}</time>
|
||||
<small>{formatPlayTime(selected.local.playSeconds)} · Level {selected.local.healers[selected.local.activeClassId].level}</small>
|
||||
</article>
|
||||
</div>
|
||||
<div className="dialog-actions version-actions">
|
||||
<FocusButton id="version-online" focusedId={controller.focusedId} focus={controller.focus} className="front-primary" disabled={resolvingOnline} onClick={() => { void continueWithOnline(); }}><span>{resolvingOnline ? "Loading…" : "Continue online copy"}</span><small>{formatSaveTimestamp(selected.online.updatedAt)}</small></FocusButton>
|
||||
<FocusButton id="version-local" focusedId={controller.focusedId} focus={controller.focus} disabled={resolvingOnline} onClick={() => { setDialog(null); playSlot(selectedSlotId); }}><span>Continue device copy</span><small>{formatSaveTimestamp(selected.local.updatedAt)}</small></FocusButton>
|
||||
<FocusButton id="cancel-version" focusedId={controller.focusedId} focus={controller.focus} disabled={resolvingOnline} onClick={() => setDialog(null)}>Cancel</FocusButton>
|
||||
</div>
|
||||
{versionError && <div className="version-choice-error" role="alert">{versionError}</div>}
|
||||
</div>
|
||||
) : dialog === "create" ? (
|
||||
<form onSubmit={(event) => { event.preventDefault(); finishCreation(); }}>
|
||||
<span>New offline save</span><h2>Name your hunter</h2><p>This name identifies the character in local and online save lists.</p>
|
||||
<label htmlFor="new-hunter-name">Hunter name</label>
|
||||
@@ -324,31 +432,37 @@ function SaveScreen() {
|
||||
</FrontSurface>
|
||||
}
|
||||
bottom={
|
||||
<FrontSurface className="save-context" bottom ariaLabel="Selected save management">
|
||||
<FrontSurface className="save-context" bottom ariaLabel="Selected save information">
|
||||
<header className="context-header"><span>Slot {selectedSlotId}</span><b>{cloudStatus}</b></header>
|
||||
<div className="selected-save-summary">
|
||||
{selected.local ? (
|
||||
<><div className="summary-avatar">{selected.local.hunterName[0]}</div><span><small>Local record</small><h2>{selected.local.hunterName}</h2><p>{selected.local.location} · {formatPlayTime(selected.local.playSeconds)}</p><time>{formatSaveTimestamp(selected.local.updatedAt)}</time></span></>
|
||||
{selected.local ?? selected.online ? (
|
||||
<><div className="summary-avatar">{(selected.local ?? selected.online)!.hunterName[0]}</div><span><small>{selected.local ? "Device save" : "Online copy only"}</small><h2>{(selected.local ?? selected.online)!.hunterName}</h2><p>{(selected.local ?? selected.online)!.location}</p><time>{formatSaveTimestamp((selected.local ?? selected.online)!.updatedAt)}</time></span></>
|
||||
) : (
|
||||
<><div className="summary-avatar is-empty">+</div><span><small>Local record</small><h2>Empty slot</h2><p>Create a hunter or download an online version.</p></span></>
|
||||
)}
|
||||
</div>
|
||||
{selected.online && <div className="online-record"><span><b>ONLINE</b>{selected.online.hunterName}</span><time>{formatSaveTimestamp(selected.online.updatedAt)}</time></div>}
|
||||
<div className="save-actions">
|
||||
<FocusButton id={hasLocal ? "play" : "create"} focusedId={controller.focusedId} focus={controller.focus} className="front-primary" onClick={() => hasLocal ? playSlot(selectedSlotId) : openCreation()}>
|
||||
{hasLocal ? "Continue offline save" : "Create new hunter"}<small>{DEFAULT_CONTROLLER_GLYPHS.confirm}</small>
|
||||
</FocusButton>
|
||||
<div className="sync-actions">
|
||||
<FocusButton id="upload" focusedId={controller.focusedId} focus={controller.focus} disabled={!hasLocal || !accountId} onClick={() => uploadSlot(selectedSlotId)}>↑ Sync offline to server</FocusButton>
|
||||
<FocusButton id="download" focusedId={controller.focusedId} focus={controller.focus} disabled={!hasOnline || !accountId} onClick={() => downloadSlot(selectedSlotId)}>↓ Overwrite with online</FocusButton>
|
||||
</div>
|
||||
<div className="record-actions">
|
||||
<FocusButton id="copy" focusedId={controller.focusedId} focus={controller.focus} disabled={!hasLocal} onClick={() => openSaveDialog("copy")}>Copy save</FocusButton>
|
||||
<FocusButton id="delete" focusedId={controller.focusedId} focus={controller.focus} disabled={!hasLocal} className="danger-link" onClick={() => openSaveDialog("delete")}>Delete save</FocusButton>
|
||||
<FocusButton id="back" focusedId={controller.focusedId} focus={controller.focus} onClick={() => navigate("login")}>Back</FocusButton>
|
||||
</div>
|
||||
{(selected.local ?? selected.online) && (() => {
|
||||
const save = (selected.local ?? selected.online)!;
|
||||
const healer = HEALER_CLASSES[save.activeClassId];
|
||||
return (
|
||||
<>
|
||||
<div className="save-dossier-stats">
|
||||
<span><small>Active healer</small><strong>Lv {save.healers[save.activeClassId].level}</strong><em>{healer.name}</em></span>
|
||||
<span><small>Play time</small><strong>{formatPlayTime(save.playSeconds)}</strong><em>Local activity</em></span>
|
||||
<span><small>Boss kills</small><strong>{save.stats.totalBossKills}</strong><em>{save.stats.flawlessClears} flawless</em></span>
|
||||
</div>
|
||||
<div className="save-dossier-records">
|
||||
<span><small>Roguelike best</small><b>Round {save.stats.highestRoguelikeRound}</b></span>
|
||||
<span><small>Endless best</small><b>{save.stats.highestRogueTrialsEndlessKills} kills</b></span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
<div className="save-copy-state">
|
||||
<span><i className={selected.local ? "is-present" : ""} />Device copy<b>{selected.local ? formatSaveTimestamp(selected.local.updatedAt) : "Not present"}</b></span>
|
||||
<span><i className={selected.online ? "is-present" : ""} />Online copy<b>{selected.online ? formatSaveTimestamp(selected.online.updatedAt) : accountId ? "Not uploaded" : "Sign-in required"}</b></span>
|
||||
</div>
|
||||
<div className="front-notice is-lower">{notice || "All gameplay changes save to local storage automatically."}</div>
|
||||
<div className="front-notice is-lower">{notice || "Use upper display for every save action. Details here follow selected slot."}</div>
|
||||
</FrontSurface>
|
||||
}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user