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 />;
}
+102
View File
@@ -213,6 +213,108 @@ body {
color: var(--text-1);
}
.setup-wrap {
padding: 20px;
}
.setup-panel {
width: min(860px, 100%);
display: grid;
gap: 12px;
}
.setup-meta {
color: var(--text-2) !important;
font-size: 0.85rem;
}
.setup-error {
color: #ff9b9b !important;
}
.setup-form {
margin-top: 4px;
display: grid;
gap: 12px;
}
.setup-mode-row {
display: flex;
gap: 8px;
}
.setup-mode-btn {
border: 1px solid var(--line-strong);
border-radius: 9px;
background: rgba(10, 16, 28, 0.8);
color: var(--text-1);
padding: 7px 10px;
cursor: pointer;
}
.setup-mode-btn.is-active {
border-color: rgba(120, 205, 255, 0.75);
background: linear-gradient(140deg, rgba(63, 132, 207, 0.38), rgba(36, 75, 116, 0.82));
color: var(--text-0);
}
.setup-field {
display: grid;
gap: 5px;
}
.setup-field span {
color: var(--text-1);
font-size: 0.85rem;
}
.setup-field input {
width: 100%;
border: 1px solid var(--line-strong);
border-radius: 10px;
background: rgba(8, 14, 24, 0.88);
color: var(--text-0);
padding: 10px 12px;
}
.setup-zip-row {
display: grid;
grid-template-columns: 1fr auto;
gap: 8px;
}
.setup-zip-pick {
border: 1px solid var(--line-strong);
border-radius: 10px;
background: rgba(18, 32, 50, 0.94);
color: var(--text-1);
padding: 10px 12px;
cursor: pointer;
}
.setup-checkbox {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-1);
}
.setup-submit {
justify-self: start;
border: 1px solid rgba(120, 205, 255, 0.65);
border-radius: 10px;
background: linear-gradient(140deg, rgba(56, 126, 198, 0.9), rgba(29, 87, 146, 0.95));
color: var(--text-0);
font-weight: 600;
padding: 10px 14px;
cursor: pointer;
}
.setup-submit:disabled {
opacity: 0.7;
cursor: default;
}
@keyframes frame-enter {
from {
opacity: 0.5;
+26
View File
@@ -2,6 +2,32 @@
interface DesktopApi {
getWikiUrl: () => Promise<string>;
getSetupState: () => Promise<SetupState>;
pickSourceZip: () => Promise<string | null>;
runSetup: (input: SetupRunInput) => Promise<SetupRunResult>;
resetOnboarding: () => Promise<SetupRunResult>;
}
interface SetupState {
needsSetup: boolean;
sourceDir: string | null;
sourceRepoUrl: string;
imageRepoUrl: string;
imageBaseUrl: string;
}
interface SetupRunInput {
mode: "url" | "zip";
sourceRepoUrl?: string;
sourceZipPath?: string;
imageRepoUrl?: string;
imageBaseUrl?: string;
downloadImages?: boolean;
}
interface SetupRunResult {
ok: boolean;
error?: string;
}
interface Window {
+1 -2
View File
@@ -15,7 +15,7 @@ export const WIKI_NAV_SECTIONS: WikiNavSection[] = [
items: [
{ label: "Home", path: "/5etools.html", description: "Main dashboard and tools" },
{ label: "Search", path: "/search.html", description: "Global content search" },
{ label: "Quick Reference", path: "/quickreference.html", description: "Fast rules lookup" }
{ label: "Rules", path: "/variantrules.html", description: "System and optional rules" },
]
},
{
@@ -34,7 +34,6 @@ export const WIKI_NAV_SECTIONS: WikiNavSection[] = [
{ label: "Spells", path: "/spells.html", description: "Spell catalog and filters" },
{ label: "Items", path: "/items.html", description: "Equipment and magic items" },
{ label: "Bestiary", path: "/bestiary.html", description: "Creatures and stat blocks" },
{ label: "Rules", path: "/variantrules.html", description: "System and optional rules" },
{ label: "Tables", path: "/tables.html", description: "Reference tables and charts" }
]
}