Refresh launcher release presentation

This commit is contained in:
Andraxion 2026-06-26 21:36:41 -04:00
parent b632e98ad1
commit c063041efc
4 changed files with 267 additions and 285 deletions

View file

@ -1,10 +1,14 @@
import type { CSSProperties } from "react";
import { useEffect, useState } from "react";
import { openWorldshaperStudioWindow } from "./worldshaperStudio/windowing";
import {
buildWorldshaperStudioUrl,
openWorldshaperStudioWindow,
} from "./worldshaperStudio/windowing";
import { CHANGELOG_SECTIONS, CHANGELOG_SPLASH_VERSION } from "./worldshaperStudio/changelogData";
CHANGELOG_SECTIONS,
CHANGELOG_SPLASH_FOOTNOTE,
CHANGELOG_SPLASH_KICKER,
CHANGELOG_SPLASH_TITLE,
CHANGELOG_SPLASH_VERSION,
} from "./worldshaperStudio/changelogData";
import type { ChangelogItem } from "./worldshaperStudio/changelogData";
import launcherBackground from "../background.png";
type WorldDefaultPayload = {
@ -14,16 +18,10 @@ type WorldDefaultPayload = {
};
};
type LaunchState = "resolving" | "opening" | "opened" | "blocked" | "error";
type LaunchState = "ready" | "opening" | "opened" | "blocked" | "error";
const DEFAULT_EDITOR_WORLD_ID_FALLBACK = "overworld";
let didAttemptInitialLaunch = false;
let rememberedLaunchState: LaunchState | "" = "";
let rememberedStatus = "";
let rememberedError = "";
let rememberedWorldId = "";
async function resolveDefaultWorldId(): Promise<string> {
const response = await fetch("/api/world-default");
if (!response.ok) {
@ -39,225 +37,139 @@ function openStudioPopup(worldId: string): boolean {
return Boolean(popup);
}
function openStudioInCurrentTab(worldId: string): void {
window.location.assign(buildWorldshaperStudioUrl(worldId, window, { worldId }));
}
function openRepo(): void {
window.location.assign("https://repo.andraxion.net/");
}
function WorldshaperLauncher() {
const [launchState, setLaunchState] = useState<LaunchState>("resolving");
const [status, setStatus] = useState("Preparing Worldshaper Studio...");
const [launchState, setLaunchState] = useState<LaunchState>("ready");
const [status, setStatus] = useState("Launch Worldshaper Studio in its floating window.");
const [error, setError] = useState("");
const [worldId, setWorldId] = useState("");
const [worldId, setWorldId] = useState(DEFAULT_EDITOR_WORLD_ID_FALLBACK);
useEffect(() => {
let cancelled = false;
async function attemptInitialLaunch() {
if (didAttemptInitialLaunch) {
setWorldId(rememberedWorldId);
setLaunchState(rememberedLaunchState || "blocked");
setStatus(rememberedStatus || "Open Worldshaper Studio.");
setError(rememberedError);
return;
}
didAttemptInitialLaunch = true;
setLaunchState("resolving");
setError("");
setStatus("Preparing Worldshaper Studio...");
try {
const resolvedWorldId = await resolveDefaultWorldId().catch(() => DEFAULT_EDITOR_WORLD_ID_FALLBACK);
void resolveDefaultWorldId()
.then((resolvedWorldId) => {
if (cancelled) {
return;
}
setWorldId(resolvedWorldId);
rememberedWorldId = resolvedWorldId;
setLaunchState("opening");
setStatus(`Opening Worldshaper Studio for ${resolvedWorldId}...`);
if (openStudioPopup(resolvedWorldId)) {
setLaunchState("opened");
setStatus("Worldshaper Studio opened in a separate window.");
rememberedLaunchState = "opened";
rememberedStatus = "Worldshaper Studio opened in a separate window.";
rememberedError = "";
return;
}
setLaunchState("blocked");
setStatus("Your browser blocked the studio window.");
rememberedLaunchState = "blocked";
rememberedStatus = "Your browser blocked the studio window.";
rememberedError = "";
} catch (nextError: unknown) {
})
.catch(() => {
if (cancelled) {
return;
}
const nextErrorText = String(nextError || "Failed to prepare Worldshaper Studio.");
setLaunchState("error");
setError(nextErrorText);
setStatus("Worldshaper Studio unavailable.");
rememberedLaunchState = "error";
rememberedStatus = "Worldshaper Studio unavailable.";
rememberedError = nextErrorText;
}
}
void attemptInitialLaunch();
setWorldId(DEFAULT_EDITOR_WORLD_ID_FALLBACK);
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
if (launchState !== "blocked" || !worldId) {
return;
}
function retryLaunch() {
if (!worldId) {
return;
}
setLaunchState("opening");
setStatus(`Opening Worldshaper Studio for ${worldId}...`);
if (openStudioPopup(worldId)) {
setLaunchState("opened");
setStatus("Worldshaper Studio opened in a separate window.");
rememberedLaunchState = "opened";
rememberedStatus = "Worldshaper Studio opened in a separate window.";
rememberedError = "";
return;
}
setLaunchState("blocked");
setStatus("Your browser blocked the studio window.");
rememberedLaunchState = "blocked";
rememberedStatus = "Your browser blocked the studio window.";
rememberedError = "";
}
function handlePointerDown() {
retryLaunch();
}
function handleKeyDown() {
retryLaunch();
}
window.addEventListener("pointerdown", handlePointerDown, { once: true });
window.addEventListener("keydown", handleKeyDown, { once: true });
return () => {
window.removeEventListener("pointerdown", handlePointerDown);
window.removeEventListener("keydown", handleKeyDown);
};
}, [launchState, worldId]);
async function handleRetry(): Promise<void> {
async function handleLaunch(): Promise<void> {
setError("");
setLaunchState("resolving");
setStatus("Preparing Worldshaper Studio...");
const nextWorldId = worldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK;
setLaunchState("opening");
setStatus(`Opening Worldshaper Studio for ${nextWorldId}...`);
try {
const resolvedWorldId = worldId || await resolveDefaultWorldId().catch(() => DEFAULT_EDITOR_WORLD_ID_FALLBACK);
const resolvedWorldId = nextWorldId || await resolveDefaultWorldId().catch(() => DEFAULT_EDITOR_WORLD_ID_FALLBACK);
setWorldId(resolvedWorldId);
rememberedWorldId = resolvedWorldId;
setLaunchState("opening");
setStatus(`Opening Worldshaper Studio for ${resolvedWorldId}...`);
if (openStudioPopup(resolvedWorldId)) {
setLaunchState("opened");
setStatus("Worldshaper Studio opened in a separate window.");
rememberedLaunchState = "opened";
rememberedStatus = "Worldshaper Studio opened in a separate window.";
rememberedError = "";
return;
}
setLaunchState("blocked");
setStatus("Your browser blocked the studio window.");
rememberedLaunchState = "blocked";
rememberedStatus = "Your browser blocked the studio window.";
rememberedError = "";
setStatus("Your browser blocked the studio window. Use the launch button again after allowing popups.");
} catch (nextError: unknown) {
const nextErrorText = String(nextError || "Failed to prepare Worldshaper Studio.");
setLaunchState("error");
setError(nextErrorText);
setStatus("Worldshaper Studio unavailable.");
rememberedLaunchState = "error";
rememberedStatus = "Worldshaper Studio unavailable.";
rememberedError = nextErrorText;
}
}
function handleOpenHere(): void {
const nextWorldId = worldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK;
openStudioInCurrentTab(nextWorldId);
}
const isBusy = launchState === "resolving" || launchState === "opening";
const showFallbackActions = launchState === "blocked" || launchState === "opened" || launchState === "error";
const isBusy = launchState === "opening";
return (
<main
className="launcher-shell"
style={{ "--launcher-background-image": `url(${launcherBackground})` } as CSSProperties}
>
<section className="launcher-card">
<p className="launcher-eyebrow">New RPG</p>
<h1 className="launcher-title">Worldshaper Studio</h1>
<p className="launcher-status">{status}</p>
{launchState === "blocked" ? (
<p className="launcher-hint">
Click anywhere or use the button below if your browser asks for permission to open the studio window.
</p>
) : null}
{launchState === "opened" ? (
<p className="launcher-hint">
The editor is running in its own window. You can leave this tab open as a quick relaunch point, or close it.
</p>
) : null}
{error ? <p className="launcher-error">{error}</p> : null}
{showFallbackActions ? (
<div className="launcher-stack">
<section className="launcher-card">
<p className="launcher-eyebrow">New RPG</p>
<h1 className="launcher-title">Worldshaper Studio</h1>
<p className="launcher-status">{status}</p>
{launchState === "blocked" ? (
<p className="launcher-hint">
Allow the popup, then use the studio button again to launch the floating editor window.
</p>
) : null}
{launchState === "opened" ? (
<p className="launcher-hint">
The studio is open in its own slim window. This page stays behind as your release board and relaunch point.
</p>
) : null}
{launchState === "ready" ? (
<p className="launcher-hint">
The editor is designed to live in its own floating window, so the launcher keeps the first step clean.
</p>
) : null}
{error ? <p className="launcher-error">{error}</p> : null}
<div className="launcher-actions">
<button type="button" className="launcher-primary-btn" onClick={() => void handleRetry()} disabled={isBusy}>
<button type="button" className="launcher-primary-btn" onClick={() => void handleLaunch()} disabled={isBusy}>
Open Floating Studio
</button>
<button type="button" className="launcher-secondary-btn" onClick={handleOpenHere} disabled={isBusy}>
Open In This Tab
</button>
<button type="button" className="launcher-secondary-btn" onClick={openRepo} disabled={isBusy}>
Open Repo
</button>
</div>
) : null}
<section className="launcher-whats-new">
<div className="launcher-whats-new-head">
<h2 className="launcher-whats-new-title">What&apos;s New</h2>
<p className="launcher-whats-new-version">{CHANGELOG_SPLASH_VERSION}</p>
</section>
<section className="launcher-changelog-window" aria-labelledby="launcher-whats-new-title">
<div className="launcher-changelog-titlebar">
<div className="launcher-changelog-title">What&apos;s New</div>
<div className="launcher-changelog-hint">Current release highlights</div>
</div>
<div className="launcher-whats-new-list">
{CHANGELOG_SECTIONS.map((section) => (
<section key={section.title} className="launcher-whats-new-section">
<h3 className="launcher-whats-new-section-title">{section.title}</h3>
<ul className="launcher-whats-new-bullets">
{section.items.map((item, index) => {
const key = `${section.title}-${index}`;
if (typeof item === "string") {
return <li key={key}>{item}</li>;
}
return (
<li key={key}>
<div>{item.text}</div>
{item.note ? <div className="launcher-whats-new-note">{item.note}</div> : null}
</li>
);
})}
</ul>
</section>
))}
<div className="launcher-changelog-body">
<div className="changelog-splash-card">
<div className="changelog-splash-hero">
<div className="changelog-splash-kicker">{CHANGELOG_SPLASH_KICKER}</div>
<div className="changelog-splash-title" id="launcher-whats-new-title">{CHANGELOG_SPLASH_TITLE}</div>
<div className="changelog-splash-meta">Release {CHANGELOG_SPLASH_VERSION}</div>
</div>
<div className="changelog-splash-list">
{CHANGELOG_SECTIONS.map((section) => (
<section key={section.title} className="changelog-splash-section">
<h3 className="changelog-splash-section-title">{section.title}</h3>
<ul className="changelog-splash-bullets">
{section.items.map((item, index) => {
const key = `${section.title}-${index}`;
const normalizedItem: ChangelogItem = item;
if (typeof normalizedItem === "string") {
return <li key={key}>{normalizedItem}</li>;
}
return (
<li key={key}>
<div>{normalizedItem.text}</div>
{normalizedItem.note ? <div className="changelog-splash-bullet-note">{normalizedItem.note}</div> : null}
</li>
);
})}
</ul>
</section>
))}
</div>
<div className="changelog-splash-footer">
<div className="changelog-splash-footnote">{CHANGELOG_SPLASH_FOOTNOTE}</div>
</div>
</div>
</div>
</section>
</section>
</div>
</main>
);
}