Launch directly into Worldshaper Studio
This commit is contained in:
parent
b4dbd4ee8e
commit
932a638daf
3 changed files with 337 additions and 2 deletions
225
src/WorldshaperLauncher.tsx
Normal file
225
src/WorldshaperLauncher.tsx
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
buildWorldshaperStudioUrl,
|
||||
openWorldshaperStudioWindow,
|
||||
} from "./worldshaperStudio/windowing";
|
||||
|
||||
type WorldDefaultPayload = {
|
||||
worldId?: string;
|
||||
world?: {
|
||||
id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type LaunchState = "resolving" | "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) {
|
||||
throw new Error(`Failed to load default world (${response.status}).`);
|
||||
}
|
||||
const payload = await response.json() as WorldDefaultPayload;
|
||||
const resolvedWorldId = String(payload.worldId || payload.world?.id || "").trim();
|
||||
return resolvedWorldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK;
|
||||
}
|
||||
|
||||
function openStudioPopup(worldId: string): boolean {
|
||||
const popup = openWorldshaperStudioWindow(worldId, window, { worldId });
|
||||
return Boolean(popup);
|
||||
}
|
||||
|
||||
function openStudioInCurrentTab(worldId: string): void {
|
||||
window.location.assign(buildWorldshaperStudioUrl(worldId, window, { worldId }));
|
||||
}
|
||||
|
||||
function WorldshaperLauncher() {
|
||||
const [launchState, setLaunchState] = useState<LaunchState>("resolving");
|
||||
const [status, setStatus] = useState("Preparing Worldshaper Studio...");
|
||||
const [error, setError] = useState("");
|
||||
const [worldId, setWorldId] = useState("");
|
||||
|
||||
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);
|
||||
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) {
|
||||
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();
|
||||
|
||||
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> {
|
||||
setError("");
|
||||
setLaunchState("resolving");
|
||||
setStatus("Preparing Worldshaper Studio...");
|
||||
try {
|
||||
const resolvedWorldId = worldId || 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 = "";
|
||||
} 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";
|
||||
|
||||
return (
|
||||
<main className="launcher-shell">
|
||||
<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-actions">
|
||||
<button type="button" className="launcher-primary-btn" onClick={() => void handleRetry()} disabled={isBusy}>
|
||||
Open Floating Studio
|
||||
</button>
|
||||
<button type="button" className="launcher-secondary-btn" onClick={handleOpenHere} disabled={isBusy}>
|
||||
Open In This Tab
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default WorldshaperLauncher;
|
||||
Loading…
Add table
Add a link
Reference in a new issue