feat: Add WFW-Aushang web app with PWA support, offline caching, and dark mode
- Created index.html for the main application interface with responsive design and dark mode support. - Added manifest.webmanifest for PWA configuration, including app icons and display settings. - Implemented service worker (sw.js) for offline caching of assets and network-first strategy for versioning. - Introduced version.json to manage app versioning.
This commit is contained in:
321
zuss/app.js
Normal file
321
zuss/app.js
Normal file
@@ -0,0 +1,321 @@
|
||||
/* WFW-Aushang PWA
|
||||
* - Listet Dateien aus public WebDAV Share
|
||||
* - Öffnet PDFs über PDF.js
|
||||
* - Kioskiges Install/Update Overlay
|
||||
*/
|
||||
|
||||
const SHARE_TOKEN = "T9e7WESBXxy6rSD";
|
||||
const SHARE_ROOT = "/"; // im Share
|
||||
const DAV_BASE = `https://home.x-s.at/public.php/dav/files/${SHARE_TOKEN}`;
|
||||
|
||||
const PDFJS_VIEWER = "https://home.x-s.at/pdfjs/web/viewer.html?file=";
|
||||
|
||||
// Versioning für Overlay / Update-Erkennung
|
||||
const APP_VERSION = "2025.12.24.2";
|
||||
const DISMISS_KEY = "wfw_overlay_dismissed_for";
|
||||
|
||||
const VERSION_URL = "./version.json";
|
||||
|
||||
function $(id){ return document.getElementById(id); }
|
||||
|
||||
function applyTheme(theme){
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
localStorage.setItem("wfw_theme", theme);
|
||||
}
|
||||
function initTheme(){
|
||||
const saved = localStorage.getItem("wfw_theme");
|
||||
if (saved) return applyTheme(saved);
|
||||
const prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
applyTheme(prefersDark ? "dark" : "light");
|
||||
}
|
||||
|
||||
function isInstalledPWA(){
|
||||
const m = window.matchMedia ? window.matchMedia("(display-mode: standalone)") : null;
|
||||
const m2 = window.matchMedia ? window.matchMedia("(display-mode: fullscreen)") : null;
|
||||
const isStandalone = !!(m && m.matches);
|
||||
const isFullscreen = !!(m2 && m2.matches);
|
||||
const isIOSStandalone = window.navigator.standalone === true;
|
||||
return isStandalone || isFullscreen || isIOSStandalone;
|
||||
}
|
||||
|
||||
function showOverlay({title, text, primaryText, hint=""}){
|
||||
$("ovTitle").textContent = title;
|
||||
$("ovText").textContent = text;
|
||||
$("ovPrimaryBtn").textContent = primaryText;
|
||||
$("ovHint").textContent = hint;
|
||||
$("installOverlay").hidden = false;
|
||||
}
|
||||
function hideOverlay(){
|
||||
$("installOverlay").hidden = true;
|
||||
}
|
||||
function markOverlayDismissed(latestVersion){
|
||||
sessionStorage.setItem(DISMISS_KEY, latestVersion || APP_VERSION);
|
||||
}
|
||||
function isOverlayDismissed(latestVersion){
|
||||
const v = sessionStorage.getItem(DISMISS_KEY);
|
||||
return v && v === (latestVersion || APP_VERSION);
|
||||
}
|
||||
|
||||
let deferredPrompt = null;
|
||||
window.addEventListener("beforeinstallprompt", (e) => {
|
||||
e.preventDefault();
|
||||
deferredPrompt = e;
|
||||
});
|
||||
|
||||
// --- Service Worker ---
|
||||
async function registerSW(){
|
||||
if (!("serviceWorker" in navigator)) return null;
|
||||
try {
|
||||
const reg = await navigator.serviceWorker.register("./sw.js");
|
||||
// Optional: updaten, falls neue Version
|
||||
try { await reg.update(); } catch {}
|
||||
return reg;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Install / Update Overlay ---
|
||||
async function checkInstallOrUpdateOverlay(reg){
|
||||
let latest = APP_VERSION;
|
||||
try {
|
||||
const r = await fetch(VERSION_URL, { cache: "no-store" });
|
||||
if (r.ok) {
|
||||
const j = await r.json();
|
||||
if (j && typeof j.version === "string") latest = j.version;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const installed = isInstalledPWA();
|
||||
const outdated = (latest !== APP_VERSION);
|
||||
|
||||
// Wenn installiert UND aktuell -> kein Overlay
|
||||
if (installed && !outdated) return;
|
||||
|
||||
// Wenn veraltet -> "Aktualisieren" erzwingen (kioskig)
|
||||
if (outdated) {
|
||||
showOverlay({
|
||||
title: "Update verfügbar",
|
||||
text: `Es gibt eine neue Version (${latest}). Bitte aktualisieren.`,
|
||||
primaryText: "Aktualisieren",
|
||||
hint: "Tipp: Nach dem Update wird die App neu geladen."
|
||||
});
|
||||
|
||||
$("ovPrimaryBtn").onclick = async () => {
|
||||
try { await reg?.update?.(); } catch {}
|
||||
// Cache-bust / reload
|
||||
location.reload();
|
||||
};
|
||||
|
||||
$("ovContinueBtn").onclick = () => hideOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
// Nicht installiert -> "Installieren" (oder Hinweis wenn Prompt fehlt)
|
||||
const canInstall = !!deferredPrompt && !installed;
|
||||
|
||||
if (canInstall) {
|
||||
showOverlay({
|
||||
title: "App installieren",
|
||||
text: "Installiert läuft WFW-Aushang schneller und im Kiosk-Modus.",
|
||||
primaryText: "Installieren",
|
||||
hint: ""
|
||||
});
|
||||
|
||||
$("ovPrimaryBtn").onclick = async () => {
|
||||
try {
|
||||
deferredPrompt.prompt();
|
||||
await deferredPrompt.userChoice;
|
||||
} catch {}
|
||||
deferredPrompt = null;
|
||||
hideOverlay();
|
||||
};
|
||||
|
||||
} else {
|
||||
// Fallback: wenn Chrome (noch) keinen Install-Prompt gibt
|
||||
showOverlay({
|
||||
title: "App installieren",
|
||||
text: "Wenn der Install-Button fehlt: Chrome Menü (⋮) → „App installieren“.",
|
||||
primaryText: "OK",
|
||||
hint: ""
|
||||
});
|
||||
|
||||
$("ovPrimaryBtn").onclick = () => hideOverlay();
|
||||
}
|
||||
|
||||
$("ovContinueBtn").onclick = () => hideOverlay();
|
||||
}
|
||||
|
||||
// --- WebDAV Listing via PROPFIND ---
|
||||
async function propfind(url, depth=1){
|
||||
const res = await fetch(url, {
|
||||
method: "PROPFIND",
|
||||
headers: { "Depth": String(depth) }
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const t = await res.text().catch(()=> "");
|
||||
throw new Error(`PROPFIND ${res.status} ${res.statusText} ${t.slice(0,200)}`);
|
||||
}
|
||||
return await res.text();
|
||||
}
|
||||
|
||||
function parseDavResponse(xmlText){
|
||||
const parser = new DOMParser();
|
||||
const xml = parser.parseFromString(xmlText, "application/xml");
|
||||
const responses = [...xml.getElementsByTagNameNS("*","response")];
|
||||
|
||||
const items = responses.map(r => {
|
||||
const hrefEl = r.getElementsByTagNameNS("*","href")[0];
|
||||
const href = hrefEl ? hrefEl.textContent : "";
|
||||
|
||||
const propstat = r.getElementsByTagNameNS("*","propstat")[0];
|
||||
const prop = propstat ? propstat.getElementsByTagNameNS("*","prop")[0] : null;
|
||||
|
||||
const isCollection = !!(prop && prop.getElementsByTagNameNS("*","collection")[0]);
|
||||
|
||||
const sizeEl = prop ? prop.getElementsByTagNameNS("*","getcontentlength")[0] : null;
|
||||
const size = sizeEl ? parseInt(sizeEl.textContent || "0", 10) : 0;
|
||||
|
||||
const modEl = prop ? prop.getElementsByTagNameNS("*","getlastmodified")[0] : null;
|
||||
const lastmod = modEl ? modEl.textContent : "";
|
||||
|
||||
return { href, isCollection, size, lastmod };
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function humanSize(bytes){
|
||||
if (!bytes || bytes < 1024) return bytes ? `${bytes} B` : "";
|
||||
const kb = bytes/1024;
|
||||
if (kb < 1024) return `${kb.toFixed(0)} KB`;
|
||||
const mb = kb/1024;
|
||||
return `${mb.toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function safeDecode(s){
|
||||
try { return decodeURIComponent(s); } catch { return s; }
|
||||
}
|
||||
|
||||
function extractNameFromHref(href){
|
||||
const noQuery = href.split("?")[0];
|
||||
const parts = noQuery.split("/").filter(Boolean);
|
||||
const last = parts[parts.length-1] || "";
|
||||
return safeDecode(last);
|
||||
}
|
||||
|
||||
function pdfUrlFromHref(href){
|
||||
// href ist serverseitig percent-encoded; wir wollen eine saubere absolute URL für public.php/dav
|
||||
// href kommt meist als /public.php/dav/files/<token>/... oder voll
|
||||
if (href.startsWith("http")) return href;
|
||||
return `https://home.x-s.at${href}`;
|
||||
}
|
||||
|
||||
function openPdfInPdfJs(fileUrl){
|
||||
// PDF.js erwartet file=ENCODED_URL
|
||||
const full = PDFJS_VIEWER + encodeURIComponent(fileUrl) + "#zoom=page-width";
|
||||
window.location.href = full;
|
||||
}
|
||||
|
||||
function renderList(files){
|
||||
const list = $("list");
|
||||
list.innerHTML = "";
|
||||
|
||||
if (!files.length) {
|
||||
$("status").textContent = "Keine PDFs gefunden.";
|
||||
return;
|
||||
}
|
||||
|
||||
for (const f of files) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "item";
|
||||
|
||||
const icon = document.createElement("div");
|
||||
icon.className = "pdfIcon";
|
||||
icon.textContent = "📄";
|
||||
|
||||
const name = document.createElement("div");
|
||||
name.className = "name";
|
||||
name.textContent = f.name;
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "meta";
|
||||
meta.textContent = humanSize(f.size);
|
||||
|
||||
row.appendChild(icon);
|
||||
row.appendChild(name);
|
||||
row.appendChild(meta);
|
||||
|
||||
row.onclick = () => openPdfInPdfJs(f.url);
|
||||
|
||||
list.appendChild(row);
|
||||
}
|
||||
|
||||
$("status").textContent = `PDFs: ${files.length}`;
|
||||
}
|
||||
|
||||
async function load(){
|
||||
$("status").textContent = "Lade Liste…";
|
||||
|
||||
const basePath = SHARE_ROOT.replace(/^\/?/, "/").replace(/\/?$/, "/");
|
||||
const url = DAV_BASE + basePath; // z.B. .../TOKEN/
|
||||
|
||||
const xml = await propfind(url, 1);
|
||||
const items = parseDavResponse(xml);
|
||||
|
||||
// Erstes Element ist meistens das Verzeichnis selbst -> rausfiltern
|
||||
const files = items
|
||||
.filter(it => !it.isCollection)
|
||||
.map(it => {
|
||||
const name = extractNameFromHref(it.href);
|
||||
return {
|
||||
name,
|
||||
size: it.size || 0,
|
||||
url: pdfUrlFromHref(it.href)
|
||||
};
|
||||
})
|
||||
.filter(f => f.name.toLowerCase().endsWith(".pdf"));
|
||||
|
||||
// Sortierung: nach Dateiname absteigend (oft Datum im Namen)
|
||||
files.sort((a,b) => b.name.localeCompare(a.name, "de"));
|
||||
|
||||
renderList(files);
|
||||
}
|
||||
|
||||
function closeAushang(){
|
||||
// kiosk-typisch: zurück oder tab schließen (falls erlaubt)
|
||||
try { window.close(); } catch {}
|
||||
history.back();
|
||||
}
|
||||
|
||||
// Boot
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
initTheme();
|
||||
|
||||
$("themeBtn").addEventListener("click", () => {
|
||||
const cur = document.documentElement.getAttribute("data-theme") || "light";
|
||||
applyTheme(cur === "dark" ? "light" : "dark");
|
||||
});
|
||||
|
||||
$("refreshBtn").addEventListener("click", () => load());
|
||||
$("closeBtn").addEventListener("click", closeAushang);
|
||||
|
||||
const reg = await registerSW();
|
||||
await checkInstallOrUpdateOverlay(reg);
|
||||
|
||||
try {
|
||||
await load();
|
||||
} catch (e) {
|
||||
$("status").textContent = "Fehler beim Laden der Liste.";
|
||||
// In kiosk mode: Fehlertext kurz, Details nur im Overlay-Hint
|
||||
showOverlay({
|
||||
title: "Fehler",
|
||||
text: "Die Liste konnte nicht geladen werden.",
|
||||
primaryText: "OK",
|
||||
hint: String(e).slice(0, 140)
|
||||
});
|
||||
$("ovPrimaryBtn").onclick = () => hideOverlay();
|
||||
$("ovContinueBtn").onclick = () => hideOverlay();
|
||||
}
|
||||
});
|
||||
BIN
zuss/icons/icon-192.png
Normal file
BIN
zuss/icons/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
zuss/icons/icon-512.png
Normal file
BIN
zuss/icons/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 279 KiB |
285
zuss/index.html
Normal file
285
zuss/index.html
Normal file
@@ -0,0 +1,285 @@
|
||||
<!doctype html>
|
||||
<html lang="de" translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
|
||||
<title>WFW-Aushang</title>
|
||||
|
||||
<link rel="manifest" href="./manifest.webmanifest">
|
||||
<meta name="theme-color" content="#0b1020">
|
||||
|
||||
<!-- iOS / PWA -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="WFW-Aushang">
|
||||
<link rel="apple-touch-icon" href="./icons/icon-192.png">
|
||||
|
||||
<style>
|
||||
:root{
|
||||
--bg: #f6f7fb;
|
||||
--panel: rgba(255,255,255,.75);
|
||||
--panelSolid: #ffffff;
|
||||
--text: #0b1220;
|
||||
--muted: rgba(11,18,32,.62);
|
||||
--border: rgba(11,18,32,.12);
|
||||
--shadow: 0 10px 30px rgba(11,18,32,.12);
|
||||
--accent: #2563eb;
|
||||
--btnText: #ffffff;
|
||||
--glass: blur(12px);
|
||||
}
|
||||
[data-theme="dark"]{
|
||||
--bg: #070a12;
|
||||
--panel: rgba(17,24,39,.55);
|
||||
--panelSolid: #0b1020;
|
||||
--text: #e5e7eb;
|
||||
--muted: rgba(229,231,235,.65);
|
||||
--border: rgba(229,231,235,.14);
|
||||
--shadow: 0 14px 40px rgba(0,0,0,.55);
|
||||
--accent: #60a5fa;
|
||||
--btnText: #061022;
|
||||
}
|
||||
|
||||
html, body { height:100%; }
|
||||
body{
|
||||
margin:0;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
||||
background:
|
||||
radial-gradient(1200px 900px at 10% 0%, rgba(37,99,235,.12), transparent 55%),
|
||||
radial-gradient(900px 700px at 90% 10%, rgba(99,102,241,.12), transparent 60%),
|
||||
radial-gradient(900px 700px at 50% 110%, rgba(16,185,129,.10), transparent 60%),
|
||||
var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
header{
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
padding: 14px 16px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: color-mix(in srgb, var(--panelSolid) 70%, transparent);
|
||||
backdrop-filter: var(--glass);
|
||||
}
|
||||
.headrow{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap: 10px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1{
|
||||
margin:0;
|
||||
font-size: 18px;
|
||||
letter-spacing: .2px;
|
||||
}
|
||||
.headbtns{ display:flex; gap:10px; align-items:center; }
|
||||
|
||||
.iconbtn{
|
||||
border: 1px solid var(--border);
|
||||
background: color-mix(in srgb, var(--panelSolid) 80%, transparent);
|
||||
color: var(--text);
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
cursor:pointer;
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,.06);
|
||||
}
|
||||
.iconbtn:active{ transform: translateY(1px); }
|
||||
|
||||
.wrap{
|
||||
padding: 14px 12px 96px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.status{
|
||||
margin: 12px 4px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.list{
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
background: var(--panel);
|
||||
backdrop-filter: var(--glass);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.item{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap: 12px;
|
||||
padding: 12px 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
}
|
||||
.item:first-child{ border-top:none; }
|
||||
.item:hover{ background: color-mix(in srgb, var(--panelSolid) 22%, transparent); }
|
||||
|
||||
.pdfIcon{
|
||||
width: 38px; height: 38px;
|
||||
border-radius: 12px;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
|
||||
font-size: 18px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.name{
|
||||
flex:1;
|
||||
overflow:hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 650;
|
||||
}
|
||||
.meta{
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
text-align:right;
|
||||
min-width: 92px;
|
||||
}
|
||||
|
||||
.bottom{
|
||||
position: fixed;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
padding: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
background: color-mix(in srgb, var(--panelSolid) 70%, transparent);
|
||||
backdrop-filter: var(--glass);
|
||||
z-index: 6;
|
||||
}
|
||||
.bottom .inner{
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
}
|
||||
|
||||
.btn{
|
||||
padding: 11px 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--accent);
|
||||
color: var(--btnText);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.btn.secondary{
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
}
|
||||
.bottom .btn{
|
||||
width: min(520px, 100%);
|
||||
font-size: 16px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
/* Kioskiges Overlay */
|
||||
.ov{
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 18px;
|
||||
background: rgba(0,0,0,.62);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
.ovCard{
|
||||
width: min(560px, 100%);
|
||||
border-radius: 22px;
|
||||
border: 1px solid rgba(255,255,255,.14);
|
||||
background: color-mix(in srgb, var(--panelSolid) 92%, transparent);
|
||||
box-shadow: 0 18px 60px rgba(0,0,0,.45);
|
||||
padding: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
.ovTitle{
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
margin: 6px 0 8px;
|
||||
letter-spacing: .2px;
|
||||
}
|
||||
.ovText{
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.35;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
.ovPrimary{
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
font-size: 16px;
|
||||
border-radius: 16px;
|
||||
border: 0;
|
||||
background: var(--accent);
|
||||
color: var(--btnText);
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,.25);
|
||||
}
|
||||
.ovPrimary:active{ transform: translateY(1px); }
|
||||
.ovLink{
|
||||
margin-top: 12px;
|
||||
display: inline-block;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 8px 10px;
|
||||
color: color-mix(in srgb, var(--text) 82%, transparent);
|
||||
font-size: 13px;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
opacity: .85;
|
||||
}
|
||||
.ovHint{
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Kiosk Overlay -->
|
||||
<div id="installOverlay" class="ov" hidden>
|
||||
<div class="ovCard">
|
||||
<div class="ovTitle" id="ovTitle">WFW-Aushang</div>
|
||||
<div class="ovText" id="ovText">Installiere die App für Kiosk-Modus und schnelle PDFs.</div>
|
||||
|
||||
<button id="ovPrimaryBtn" class="ovPrimary">Installieren</button>
|
||||
<button id="ovContinueBtn" class="ovLink" type="button">Weiter im Browser</button>
|
||||
|
||||
|
||||
<div class="ovHint" id="ovHint"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<div class="headrow">
|
||||
<h1>WFW-Aushang</h1>
|
||||
<div class="headbtns">
|
||||
<button class="iconbtn" id="themeBtn" title="Dark Mode">🌓</button>
|
||||
<button class="iconbtn" id="refreshBtn" title="Neu laden">↻</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="status" id="status">Lade Liste…</div>
|
||||
<div class="list" id="list"></div>
|
||||
</div>
|
||||
|
||||
<div class="bottom">
|
||||
<div class="inner">
|
||||
<button class="btn" id="closeBtn">Aushang schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
14
zuss/manifest.webmanifest
Normal file
14
zuss/manifest.webmanifest
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "WFW-Aushang",
|
||||
"short_name": "WFW-Aushang",
|
||||
"start_url": "/zuss/",
|
||||
"scope": "/zuss/",
|
||||
"display": "fullscreen",
|
||||
"orientation": "portrait",
|
||||
"background_color": "#0b1020",
|
||||
"theme_color": "#0b1020",
|
||||
"icons": [
|
||||
{ "src": "/zuss/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "/zuss/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
|
||||
]
|
||||
}
|
||||
59
zuss/sw.js
Normal file
59
zuss/sw.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// Simple offline cache for kiosk usage
|
||||
const CACHE = "wfw-aushang-2025.12.24.2";
|
||||
|
||||
const ASSETS = [
|
||||
"/zuss/",
|
||||
"/zuss/index.html",
|
||||
"/zuss/app.js",
|
||||
"/zuss/manifest.webmanifest",
|
||||
"/zuss/version.json",
|
||||
"/zuss/icons/icon-192.png",
|
||||
"/zuss/icons/icon-512.png"
|
||||
];
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE).then((c) => c.addAll(ASSETS)).then(() => self.skipWaiting())
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
const keys = await caches.keys();
|
||||
await Promise.all(keys.map(k => (k === CACHE ? null : caches.delete(k))));
|
||||
await self.clients.claim();
|
||||
})()
|
||||
);
|
||||
});
|
||||
|
||||
// Network-first for version.json + list, cache-first for static assets
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// Only handle same-origin requests
|
||||
if (url.origin !== self.location.origin) return;
|
||||
|
||||
const isVersion = url.pathname.endsWith("/zuss/version.json");
|
||||
|
||||
if (isVersion) {
|
||||
event.respondWith(
|
||||
fetch(event.request, { cache: "no-store" }).catch(() => caches.match(event.request))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache-first for our own static assets
|
||||
if (url.pathname.startsWith("/zuss/")) {
|
||||
event.respondWith(
|
||||
caches.match(event.request).then((cached) => {
|
||||
if (cached) return cached;
|
||||
return fetch(event.request).then((resp) => {
|
||||
const copy = resp.clone();
|
||||
caches.open(CACHE).then((c) => c.put(event.request, copy));
|
||||
return resp;
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
1
zuss/version.json
Normal file
1
zuss/version.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "version": "2025.12.24.2" }
|
||||
Reference in New Issue
Block a user