Web frontend
This commit is contained in:
BIN
web/public/apple-touch-icon.png
Normal file
BIN
web/public/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
web/public/dolphin-mascot.png
Normal file
BIN
web/public/dolphin-mascot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 697 KiB |
BIN
web/public/fonts/nunito-latin-ext.woff2
Normal file
BIN
web/public/fonts/nunito-latin-ext.woff2
Normal file
Binary file not shown.
BIN
web/public/fonts/nunito-latin.woff2
Normal file
BIN
web/public/fonts/nunito-latin.woff2
Normal file
Binary file not shown.
22
web/public/fonts/nunito.css
Normal file
22
web/public/fonts/nunito.css
Normal file
@@ -0,0 +1,22 @@
|
||||
/* Nunito, self-hosted: one variable file per subset covers 500-900.
|
||||
|
||||
Google Fonts would be a network round trip the mouse may not have, and a
|
||||
cached app shell rendered in the fallback face looks broken. latin-ext is
|
||||
what carries "Hörbücher" and "Käpt'n"; the Cyrillic and Vietnamese subsets
|
||||
Google also offers are dropped, since nothing in this UI can produce them. */
|
||||
@font-face {
|
||||
font-family: "Nunito";
|
||||
font-style: normal;
|
||||
font-weight: 500 900;
|
||||
font-display: swap;
|
||||
src: url("/fonts/nunito-latin-ext.woff2") format("woff2");
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Nunito";
|
||||
font-style: normal;
|
||||
font-weight: 500 900;
|
||||
font-display: swap;
|
||||
src: url("/fonts/nunito-latin.woff2") format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
BIN
web/public/icon-192.png
Normal file
BIN
web/public/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
web/public/icon-512.png
Normal file
BIN
web/public/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
BIN
web/public/icon-maskable-512.png
Normal file
BIN
web/public/icon-maskable-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
22
web/public/manifest.webmanifest
Normal file
22
web/public/manifest.webmanifest
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Musik Delphin",
|
||||
"short_name": "Musik",
|
||||
"description": "Musik und Hörbücher für die MusicMouse",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"background_color": "#0d2f3c",
|
||||
"theme_color": "#0d2f3c",
|
||||
"lang": "de",
|
||||
"icons": [
|
||||
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
|
||||
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
|
||||
{
|
||||
"src": "/icon-maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
79
web/public/sw.js
Normal file
79
web/public/sw.js
Normal file
@@ -0,0 +1,79 @@
|
||||
// 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-v1";
|
||||
const COVERS = "musikdelphin-covers-v1";
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(SHELL)
|
||||
.then((cache) =>
|
||||
cache.addAll([
|
||||
"/",
|
||||
"/manifest.webmanifest",
|
||||
"/dolphin-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 && n !== COVERS).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 immutable per album id and is the only heavy thing here.
|
||||
if (url.pathname.startsWith("/api/albums/")) {
|
||||
event.respondWith(
|
||||
caches.open(COVERS).then(async (cache) => {
|
||||
const hit = await cache.match(request);
|
||||
if (hit) return hit;
|
||||
const response = await fetch(request);
|
||||
if (response.ok) cache.put(request, response.clone());
|
||||
return response;
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user