Two kids, two looks: ?theme=dolphin or ?theme=dinosaur picks one, and the choice is remembered in the browser so the installed home-screen app keeps it. Everything that differs per child lives in one Skin object per file under web/src/lib/skins/; layout and feature toggles in lib/theme.ts stay shared. The dolphin renders pixel-identical to before. The hardcoded sea hues (210 music, 175 typing game, 340 accent) are now CSS variables written once at startup, and the Gecko-friendly sRGB gradient stops come from the skin. The dinosaur is jungle green with a lava-orange accent, its own mascot, icons, manifest and browser colour. Nothing underwater is left in the dinosaur skin: leaves instead of bubbles (play-view canvas, room page, typing game), a walking mascot, lava blobs in the bubble game, a hatching egg instead of the treasure chest, dino pets that walk or fly, a prehistoric speed ladder with the T-Rex as the hero rung, and renamed games and light colours. Pets and ladder rungs are keyed by the backend's existing ids, used as slots, so the backend and save format are unchanged. Pets show as emoji until drawings exist. python-backend/.gitignore: ignore tippen-curriculum-dino.yml, the dino device's personal curriculum, like the original. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
3.2 KiB
JavaScript
85 lines
3.2 KiB
JavaScript
// A deliberately small service worker: it exists so iOS treats this as an installable
|
|
// app and so the shell survives a flaky wifi moment. It does NOT try to make the player
|
|
// work offline - the audio, the library and the state all live on the mouse, and a
|
|
// cached answer for any of those would be a lie.
|
|
|
|
const SHELL = "musikdelphin-shell-v2";
|
|
|
|
// There is no cover cache any more; `activate` below deletes every cache that is not
|
|
// SHELL, which retires the two generations of cover cache this worker used to keep.
|
|
self.addEventListener("install", (event) => {
|
|
event.waitUntil(
|
|
caches
|
|
.open(SHELL)
|
|
.then((cache) =>
|
|
cache.addAll([
|
|
"/",
|
|
"/manifest.webmanifest",
|
|
"/dolphin-mascot.png",
|
|
"/dinosaur-mascot.png",
|
|
"/fonts/nunito.css",
|
|
"/fonts/nunito-latin.woff2",
|
|
"/fonts/nunito-latin-ext.woff2",
|
|
]),
|
|
)
|
|
.then(() => self.skipWaiting()),
|
|
);
|
|
});
|
|
|
|
self.addEventListener("activate", (event) => {
|
|
event.waitUntil(
|
|
caches
|
|
.keys()
|
|
.then((names) =>
|
|
Promise.all(names.filter((n) => n !== SHELL).map((n) => caches.delete(n))),
|
|
)
|
|
.then(() => self.clients.claim()),
|
|
);
|
|
});
|
|
|
|
self.addEventListener("fetch", (event) => {
|
|
const request = event.request;
|
|
if (request.method !== "GET") return;
|
|
|
|
const url = new URL(request.url);
|
|
if (url.origin !== self.location.origin) return;
|
|
|
|
// Cover art is NOT cached here, and that is deliberate. This worker only ever
|
|
// registers on localhost or over HTTPS (see the README), which on this setup means
|
|
// the kiosk on the device itself - where the backend is the same machine, and a
|
|
// Cache Storage lookup plus a revalidating fetch plus a cache write is strictly more
|
|
// work than just asking for the file.
|
|
//
|
|
// It was measured, after a stale-while-revalidate version of this handler made
|
|
// searching visibly worse. Typing "conni" over a 343-album library, keydown to
|
|
// painted, worst keystroke: 5580 ms through this worker against 461 ms with
|
|
// Network.setBypassServiceWorker on. The main thread was not running any JavaScript
|
|
// during those stalls - it sat waiting while ImageDecodeTask blocked on bytes this
|
|
// worker was serialising through a single thread.
|
|
//
|
|
// If the player ever moves behind a TLS proxy for the tablet, revisit: caching covers
|
|
// is worth something over wifi, and would want a budget rather than every cover on
|
|
// every load.
|
|
|
|
// Everything else under /api is live state. Never cache it, never serve it stale.
|
|
if (url.pathname.startsWith("/api/")) return;
|
|
|
|
// The shell: network first so a rebuilt frontend is picked up on the next load,
|
|
// falling back to the cache when the mouse is off or out of range.
|
|
event.respondWith(
|
|
fetch(request)
|
|
.then((response) => {
|
|
if (response.ok) {
|
|
const copy = response.clone();
|
|
caches.open(SHELL).then((cache) => cache.put(request, copy));
|
|
}
|
|
return response;
|
|
})
|
|
.catch(async () => {
|
|
const hit = await caches.match(request);
|
|
// A client-side app: any in-scope navigation resolves to the one document.
|
|
return hit ?? (await caches.match("/")) ?? Response.error();
|
|
}),
|
|
);
|
|
});
|