diff --git a/README.md b/README.md index 222038bb4..faa116882 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ never go missing from the bundle): sh scripts/build-web.sh ``` -The bundle ships a service worker (`web/sw.js`, deployed at `/app/sw.js`) that caches the app shell: after one online visit the app starts fully offline and the ~55 MB wasm bundle is served from the browser cache instead of re-downloaded. `build-web.sh` stamps a hash of the finished bundle into `sw.js` as the cache version, so every deploy ships a fresh cache and stale ones are cleaned up automatically. +The bundle ships a service worker (`web/sw.js`, deployed at `/app/sw.js`) that caches the app shell: after a first online visit registers it, the assets are captured on the following visit and the app then starts fully offline, the ~55 MB wasm bundle served from the browser cache instead of re-downloaded. A long-lived installed window keeps running its current version until all its tabs close (no mid-session disruption); the next launch picks up the update. `build-web.sh` stamps a hash of the finished bundle into `sw.js` as the cache version, so every deploy ships a fresh cache and stale ones are cleaned up automatically. `/app/` carries its own web app manifest, so the editor itself can be installed as a standalone app. ## Automation diff --git a/scripts/build-web.sh b/scripts/build-web.sh index 0f4ac8588..8cd192a3d 100644 --- a/scripts/build-web.sh +++ b/scripts/build-web.sh @@ -62,8 +62,11 @@ done echo "build-web.sh: dist/app/sw.js missing — is web/sw.js still listed as a copy-file link in web-app.html?" >&2 exit 1 } -bundle_hash=$(find dist/app -type f ! -name sw.js -print0 | sort -z | xargs -0 sha256sum | sha256sum | cut -c 1-12) -sed -i "s/^const CACHE_VERSION = \"[^\"]*\";/const CACHE_VERSION = \"$bundle_hash\";/" dist/app/sw.js +# Portable hashing: macOS ships shasum (no sha256sum) and BSD sed needs an +# explicit -i suffix — both handled so local builds work everywhere. +if command -v sha256sum >/dev/null 2>&1; then H="sha256sum"; else H="shasum -a 256"; fi +bundle_hash=$(find dist/app -type f ! -name sw.js -print0 | LC_ALL=C sort -z | xargs -0 $H | $H | cut -c 1-12) +sed -i.bak "s/^const CACHE_VERSION = \"[^\"]*\";/const CACHE_VERSION = \"$bundle_hash\";/" dist/app/sw.js && rm -f dist/app/sw.js.bak grep -q "^const CACHE_VERSION = \"$bundle_hash\";" dist/app/sw.js || { echo "error: failed to stamp the cache version into dist/app/sw.js" >&2 exit 1 diff --git a/src/app/update/file.rs b/src/app/update/file.rs index df8bd46e8..eab4d1755 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -1501,6 +1501,15 @@ impl OpenCADStudio { self.apply_display_defaults(idx); idx }; + // A Restore-all batch's LAST restore inserts its tab here — one + // async round AFTER its settle message. The focus flag therefore + // also fires here: the message-boundary hook alone could run + // before this insertion and be overwritten by it. + #[cfg(target_arch = "wasm32")] + if self.web_session_focus_pending && self.web_autosave_restore_all_remaining == 0 { + self.web_session_focus_pending = false; + self.select_remembered_session_tab(); + } let mut recovery_report = recovery_needed.then(|| { crate::io::recovery::RecoveryReport::recovered( @@ -3556,19 +3565,19 @@ impl OpenCADStudio { /// from the after-message hook (`web_session_focus_pending`), so the /// whole settle batch — including the last tab insertion — is done. #[cfg(target_arch = "wasm32")] - pub(super) fn select_remembered_session_tab(&mut self) { + pub(super) fn select_remembered_session_tab(&mut self) -> Task { let Some(session) = self.web_session.as_ref() else { - return; + return Task::none(); }; let Some(remembered) = session.tabs.get(session.active) else { - return; + return Task::none(); }; let recovered = crate::io::web_autosave::recovered_name(remembered); let Some(index) = self.tabs.iter().position(|tab| { let name = tab.tab_display_name(); name == remembered.as_str() || name == recovered }) else { - return; + return Task::none(); }; self.active_tab = index; // The active tab changed under the ribbon; re-sync its chips like @@ -3576,6 +3585,8 @@ impl OpenCADStudio { self.sync_ribbon_layers(); self.sync_ribbon_styles(); self.sync_ribbon_from_selection(); + + Task::none() } /// Remember the open-tab list (names + active index) in localStorage — diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 8a43e31c7..9862c5975 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -297,7 +297,7 @@ impl OpenCADStudio { #[cfg(target_arch = "wasm32")] if self.web_session_focus_pending { self.web_session_focus_pending = false; - self.select_remembered_session_tab(); + return self.select_remembered_session_tab(); } self.sync_open_command_history(); // Close the document-level first-touch transaction started by diff --git a/web-app.html b/web-app.html index b66bb39f0..02009a16a 100644 --- a/web-app.html +++ b/web-app.html @@ -1,6 +1,7 @@ + @@ -46,6 +47,8 @@ + +
diff --git a/web/app.webmanifest b/web/app.webmanifest new file mode 100644 index 000000000..0e79b2c53 --- /dev/null +++ b/web/app.webmanifest @@ -0,0 +1,18 @@ +{ + "name": "Open CAD Studio", + "short_name": "OCS", + "description": "A free CAD editor for DWG and DXF drawings.", + "start_url": "./", + "scope": "./", + "display": "standalone", + "background_color": "#1e1e1e", + "theme_color": "#1e1e1e", + "icons": [ + { + "src": "logo.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + } + ] +} diff --git a/web/logo.svg b/web/logo.svg new file mode 100644 index 000000000..7b4fdc6b5 --- /dev/null +++ b/web/logo.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/web/sw.js b/web/sw.js index b74d2c8ef..13483a675 100644 --- a/web/sw.js +++ b/web/sw.js @@ -89,9 +89,13 @@ self.addEventListener("fetch", (event) => { event.respondWith( (async () => { const cache = await caches.open(CACHE_NAME); + // Only the scope root is the app shell: caching any other in-scope + // navigation here would store e.g. a font or JSON body under the + // shell key and serve it for every offline navigation. + const isShell = new URL(request.url).pathname === SHELL_URL.pathname; try { const fresh = await fetch(request); - await put(cache, SHELL_URL, fresh); + if (isShell) await put(cache, SHELL_URL, fresh); return fresh; } catch { const cached = await cache.match(SHELL_URL);