Added loading 5et from zip release, or cloning git repo. Improvements to CSS and some missing functionality
This commit is contained in:
1 parent
e241442302
commit
8be4c8d571
12 files changed
+1383
-104
No files matched your search
+235
-5
@@ -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 />;
|
||||
}
|
||||
Reference in new issue
Block a user