Added loading 5et from zip release, or cloning git repo. Improvements to CSS and some missing functionality
CI / Verify (build + tests) (push) Failing after 12s
CI / Package (Linux) (push) Has been skipped
CI / Package (Windows) (push) Has been skipped
CI / Package (macOS arm64 source) (push) Has been skipped

This commit is contained in:
boris committed 2026-02-24 23:11:31 +00:00
1 parent e241442302
commit 8be4c8d571
12 files changed
+1383 -104

No files matched your search

+235 -5
View File
@@ -1,12 +1,187 @@
import { useEffect, useMemo, useState } from "react";
import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
import { WIKI_NAV_SECTIONS } from "./wikiNav";
const DEFAULT_PAGE = "/5etools.html";
export default function App() {
function SetupView() {
const [setupState, setSetupState] = useState<SetupState | null>(null);
const [sourceMode, setSourceMode] = useState<"url" | "zip">("url");
const [sourceRepoUrl, setSourceRepoUrl] = useState("");
const [sourceZipPath, setSourceZipPath] = useState("");
const [imageRepoUrl, setImageRepoUrl] = useState("");
const [imageBaseUrl, setImageBaseUrl] = useState("");
const [downloadImages, setDownloadImages] = useState(true);
const [error, setError] = useState("");
const [status, setStatus] = useState("Checking local 5etools source...");
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
window.desktopApi
.getSetupState()
.then((state) => {
setSetupState(state);
setSourceRepoUrl(state.sourceRepoUrl);
setImageRepoUrl(state.imageRepoUrl);
setImageBaseUrl(state.imageBaseUrl);
setStatus(state.needsSetup ? "Local 5etools source not found. Configure first-run setup below." : "Setup already complete.");
})
.catch((err: unknown) => {
const message = err instanceof Error ? err.message : "Unknown setup-state error";
setError(message);
setStatus("Failed to load setup state.");
});
}, []);
const onSubmit = async (event: FormEvent) => {
event.preventDefault();
setError("");
setIsSubmitting(true);
setStatus(
sourceMode === "zip"
? "Extracting local source ZIP and preparing files. This can take a few minutes..."
: "Downloading and preparing local 5etools source. This can take a few minutes..."
);
try {
const result = await window.desktopApi.runSetup({
mode: sourceMode,
sourceRepoUrl,
sourceZipPath,
imageRepoUrl,
imageBaseUrl,
downloadImages
});
if (!result.ok) {
setError(result.error || "Setup failed.");
setStatus("Setup failed.");
setIsSubmitting(false);
return;
}
setStatus("Setup complete. Launching wiki...");
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown setup error";
setError(message);
setStatus("Setup failed.");
setIsSubmitting(false);
}
};
const onClickBrowseZip = async () => {
const selected = await window.desktopApi.pickSourceZip();
if (selected) {
setSourceZipPath(selected);
}
};
return (
<main className="status-wrap setup-wrap">
<section className="status-panel setup-panel">
<h1>First Run Setup</h1>
<p>{status}</p>
{setupState?.sourceDir ? <p className="setup-meta">Detected local source: {setupState.sourceDir}</p> : null}
{error ? <p className="setup-error">{error}</p> : null}
<form className="setup-form" onSubmit={onSubmit}>
<div className="setup-mode-row" role="radiogroup" aria-label="Source input mode">
<button
type="button"
className={sourceMode === "url" ? "setup-mode-btn is-active" : "setup-mode-btn"}
onClick={() => setSourceMode("url")}
disabled={isSubmitting}
>
Source URL
</button>
<button
type="button"
className={sourceMode === "zip" ? "setup-mode-btn is-active" : "setup-mode-btn"}
onClick={() => setSourceMode("zip")}
disabled={isSubmitting}
>
Source ZIP
</button>
</div>
{sourceMode === "url" ? (
<label className="setup-field">
<span>5etools source repository URL</span>
<input
type="url"
value={sourceRepoUrl}
onChange={(event) => setSourceRepoUrl(event.target.value)}
required={sourceMode === "url"}
disabled={isSubmitting}
placeholder="https://github.com/5etools-mirror-2/5etools-mirror-2.github.io.git"
/>
</label>
) : (
<label className="setup-field">
<span>5etools source ZIP file</span>
<div className="setup-zip-row">
<input
type="text"
value={sourceZipPath}
onChange={(event) => setSourceZipPath(event.target.value)}
required={sourceMode === "zip"}
disabled={isSubmitting}
placeholder="/path/to/5etools-source.zip"
/>
<button type="button" className="setup-zip-pick" onClick={onClickBrowseZip} disabled={isSubmitting}>
Browse
</button>
</div>
</label>
)}
<label className="setup-field">
<span>5etools image repository URL</span>
<input
type="url"
value={imageRepoUrl}
onChange={(event) => setImageRepoUrl(event.target.value)}
required
disabled={isSubmitting}
placeholder="https://github.com/5etools-mirror-2/5etools-img"
/>
</label>
<label className="setup-field">
<span>External image base URL fallback</span>
<input
type="url"
value={imageBaseUrl}
onChange={(event) => setImageBaseUrl(event.target.value)}
required
disabled={isSubmitting}
placeholder="https://5e.tools/img/"
/>
</label>
<label className="setup-checkbox">
<input
type="checkbox"
checked={downloadImages}
onChange={(event) => setDownloadImages(event.target.checked)}
disabled={isSubmitting}
/>
<span>Download local image repository (recommended)</span>
</label>
<button className="setup-submit" type="submit" disabled={isSubmitting}>
{isSubmitting ? "Setting up..." : "Run Setup"}
</button>
</form>
</section>
</main>
);
}
function WikiView() {
const [wikiUrl, setWikiUrl] = useState<string>("");
const [error, setError] = useState<string>("");
const [currentPath, setCurrentPath] = useState<string>(DEFAULT_PAGE);
const frameRef = useRef<HTMLIFrameElement | null>(null);
useEffect(() => {
window.desktopApi
@@ -18,6 +193,43 @@ export default function App() {
});
}, []);
useEffect(() => {
const onMessage = (event: MessageEvent<unknown>) => {
const frameWindow = frameRef.current?.contentWindow;
if (!frameWindow || event.source !== frameWindow) {
return;
}
if (!event.data || typeof event.data !== "object") {
return;
}
const data = event.data as { type?: string };
if (data.type !== "5etd:reset-onboarding") {
return;
}
void (async () => {
const result = await window.desktopApi.resetOnboarding();
if (result.ok) {
return;
}
frameWindow.postMessage(
{
type: "5etd:reset-onboarding-result",
ok: false,
error: result.error || "Failed to reset onboarding."
},
"*"
);
})();
};
window.addEventListener("message", onMessage);
return () => window.removeEventListener("message", onMessage);
}, []);
const frameSrc = useMemo(() => {
if (!wikiUrl) {
return "";
@@ -54,8 +266,13 @@ export default function App() {
<header className="sidebar-brand">
<p className="sidebar-kicker">5etools Desktop</p>
<h1 className="sidebar-title">Compendium</h1>
<p className="sidebar-copy">5etools Desktop by <b>notBoris</b>.</p>
<p className="sidebar-copy-subtitle">This application is not affiliated with the 5etools Project. 5etools® is a Registered Trademark of The Norr Group Ltd. All rights reserved.</p>
<p className="sidebar-copy">
5etools Desktop by <b>notBoris</b>.
</p>
<p className="sidebar-copy-subtitle">
This application is not affiliated with the 5etools Project. 5etools® is a Registered Trademark of The Norr Group Ltd.
All rights reserved.
</p>
</header>
<nav className="sidebar-nav" aria-label="Wiki Sections">
@@ -83,9 +300,22 @@ export default function App() {
<section className="workspace">
<section className="frame-shell frame-shell--full">
<iframe className="wiki-frame" src={frameSrc} title="5etools Wiki" />
<iframe ref={frameRef} className="wiki-frame" src={frameSrc} title="5etools Wiki" />
</section>
</section>
</main>
);
}
export default function App() {
const isSetupMode = useMemo(() => {
const params = new URLSearchParams(window.location.search);
return params.get("setup") === "1";
}, []);
if (isSetupMode) {
return <SetupView />;
}
return <WikiView />;
}