Files
OneCAD-Draft/web-app.html
T
Afonsop 6af8d8ba8c feat: restore-all session recovery, ?file= deep links, offline PWA
Session recovery: more than one pending snapshot adds a 'Restore all'
button to the Recover Drawing modal — every snapshot goes through the
exact same guarded path as a single restore (deleted only after its
own open succeeds). The open-tab list and active tab persist to
localStorage on web (open/save/close) and the remembered tab is
re-selected once a restore batch settles; the modal notes how many
drawings the session had. Nothing is auto-opened.

Deep links: ?file=<url|name> opens a drawing at boot. http(s) URLs
must end in .dwg/.dxf and are fetched with the same streaming,
64 MB-capped, 8 s-deadline reader the remote images use, then go
through the guarded open queue; cross-origin opens announce the
origin first. Bare names resolve from the browser's recent storage,
with a hint when absent. Everything else (file:, javascript:, drive
letters, non-drawing tails) is refused with a console note.

Offline PWA: the app was installable (manifest) but downloaded its
55 MB bundle on every visit. A cache-first service worker now serves
the whole bundle from Cache Storage after one online visit —
navigations network-first so updates land, hashed assets
stale-while-revalidate, non-GET/cross-origin passthrough, old caches
cleaned on activate, and the waiting-worker update flow documented.
The Pages workflow stamps the cache version from the bundle hash and
fails if sw.js is missing or unstamped.

(cherry picked from commit 5f9b345e)

Ported onto main (#1717 review): the cache-version stamp moved from the
skipped build-web.sh wrapper into the Pages workflow; the tab-close hook
lands in main's on_tab_close, which also cancels pinned control requests.
2026-10-12 12:02:05 +03:00

117 lines
4.8 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex, follow" />
<meta name="theme-color" content="#1e1e1e" />
<title>Open CAD Studio Web App</title>
<link data-trunk rel="icon" href="assets/logo.svg" />
<style>
html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #1e1e1e; }
canvas { display: block; }
#loading {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 22px;
background: #1e1e1e;
color: #cfcfcf;
font-family: system-ui, sans-serif;
z-index: 9999;
transition: opacity 0.4s ease;
}
#loading.hide { opacity: 0; pointer-events: none; }
#loading .title { font-size: 22px; font-weight: 600; letter-spacing: 0.5px; }
#loading .title span { color: #3399e6; }
#loading .sub { font-size: 12px; color: #777; }
#loading .spinner {
width: 46px;
height: 46px;
border: 4px solid #2e2e2e;
border-top-color: #3399e6;
border-radius: 50%;
animation: ocs-spin 0.9s linear infinite;
}
@keyframes ocs-spin { to { transform: rotate(360deg); } }
</style>
<link data-trunk rel="rust" data-bin="OpenCADStudio" />
<link data-trunk rel="copy-dir" href="web/fonts" />
<link data-trunk rel="copy-file" href="web/ocs-parse-worker.js" />
<link data-trunk rel="copy-file" href="web/discussions.json" />
<link data-trunk rel="copy-file" href="web/videos.json" />
<link data-trunk rel="copy-file" href="web/locale-labels.json" />
<link data-trunk rel="copy-file" href="web/ocs-control.js" />
<link data-trunk rel="copy-file" href="web/sw.js" />
</head>
<body>
<div id="loading">
<div class="spinner"></div>
<div class="title">Open <span>CAD</span> Studio</div>
<div class="sub">Loading…</div>
</div>
<script src="ocs-control.js" defer></script>
<script>
// Offline support: register the service worker. It is copied next to
// this page by Trunk (see the copy-file link in <head>), so the
// relative URL resolves to /app/sw.js and the registration scope is
// /app/ — the whole app. Registered immediately (not on load) so the
// worker starts capturing this visit's asset requests as early as
// possible. sw.js never reloads the page on update: a new worker
// waits until every tab on the old version has closed.
(() => {
if (!("serviceWorker" in navigator)) return;
navigator.serviceWorker.register("sw.js").catch(error => {
console.warn("service worker registration failed; offline support disabled:", error);
});
})();
</script>
<script>
(() => {
const splash = document.getElementById("loading");
fetch("locale-labels.json").then(response => response.json()).then(labels => {
let saved;
try {
saved = JSON.parse(localStorage.getItem("opencadstudio.settings"))?.settings?.language;
} catch {}
const preferred = navigator.languages?.length ? navigator.languages : [navigator.language];
const requested = typeof saved === "string" && saved !== "system" ? [saved, ...preferred] : preferred;
const available = Object.keys(labels);
let locale = "en-US";
for (const candidate of requested) {
try {
const language = new Intl.Locale(candidate);
const match = available.find(value => value.toLowerCase() === language.baseName.toLowerCase())
|| (language.language === "zh"
? (language.maximize().script === "Hant" ? "zh-TW" : "zh-CN")
: available.find(value => value.startsWith(language.language + "-")));
if (match) { locale = match; break; }
} catch {}
}
document.title = labels[locale].title;
document.documentElement.lang = locale;
document.documentElement.dir = locale === "ar-SA" ? "rtl" : "ltr";
const caption = splash?.querySelector(".sub");
if (caption) caption.textContent = labels[locale].loading;
}).catch(() => {});
const done = () => {
if (!splash) return;
splash.classList.add("hide");
setTimeout(() => splash.remove(), 450);
};
if (document.querySelector("canvas")) return done();
const observer = new MutationObserver(() => {
if (document.querySelector("canvas")) {
observer.disconnect();
done();
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();
</script>
</body>
</html>