up up and away
This commit is contained in:
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"sakunpanthi.gitea-integration"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"editor.fontSize": 18
|
||||||
|
}
|
||||||
579
zuss/app.js
579
zuss/app.js
@@ -1,321 +1,394 @@
|
|||||||
/* WFW-Aushang PWA
|
/* WFW-Aushang PWA
|
||||||
* - Listet Dateien aus public WebDAV Share
|
* - Listet Dateien aus public WebDAV Share
|
||||||
* - Öffnet PDFs über PDF.js
|
* - Öffnet PDFs über PDF.js
|
||||||
* - Kioskiges Install/Update Overlay
|
* - Install/Update Overlay mit beforeinstallprompt.prompt()
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const SHARE_TOKEN = "T9e7WESBXxy6rSD";
|
const APP_VERSION = "2025.12.24.6";
|
||||||
const SHARE_ROOT = "/"; // im Share
|
const TOKEN = "T9e7WESBXxy6rSD";
|
||||||
const DAV_BASE = `https://home.x-s.at/public.php/dav/files/${SHARE_TOKEN}`;
|
const BASE = "https://home.x-s.at";
|
||||||
|
const SHARE_DAV = `${BASE}/public.php/dav/files/${TOKEN}`;
|
||||||
const PDFJS_VIEWER = "https://home.x-s.at/pdfjs/web/viewer.html?file=";
|
const PDFJS = `${BASE}/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";
|
|
||||||
|
|
||||||
|
// relative to /zuss/
|
||||||
const VERSION_URL = "./version.json";
|
const VERSION_URL = "./version.json";
|
||||||
|
const LIST_START_DIR = "/"; // share root
|
||||||
|
|
||||||
function $(id){ return document.getElementById(id); }
|
let deferredInstallPrompt = null;
|
||||||
|
let latestKnown = APP_VERSION;
|
||||||
|
|
||||||
function applyTheme(theme){
|
const $ = (id) => document.getElementById(id);
|
||||||
document.documentElement.setAttribute("data-theme", theme);
|
|
||||||
localStorage.setItem("wfw_theme", theme);
|
function showError(msg) {
|
||||||
|
const el = $("err");
|
||||||
|
el.style.display = "block";
|
||||||
|
el.textContent = msg;
|
||||||
}
|
}
|
||||||
function initTheme(){
|
function clearError() {
|
||||||
const saved = localStorage.getItem("wfw_theme");
|
const el = $("err");
|
||||||
if (saved) return applyTheme(saved);
|
el.style.display = "none";
|
||||||
const prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
el.textContent = "";
|
||||||
applyTheme(prefersDark ? "dark" : "light");
|
|
||||||
}
|
}
|
||||||
|
function setStatus(msg) { $("status").textContent = msg; }
|
||||||
|
|
||||||
function isInstalledPWA(){
|
function showOverlay(title, text, primaryText, showPrimary = true) {
|
||||||
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;
|
$("ovTitle").textContent = title;
|
||||||
$("ovText").textContent = text;
|
$("ovText").textContent = text;
|
||||||
$("ovPrimaryBtn").textContent = primaryText;
|
$("ovPrimaryBtn").textContent = primaryText;
|
||||||
$("ovHint").textContent = hint;
|
$("ovPrimaryBtn").style.display = showPrimary ? "flex" : "none";
|
||||||
$("installOverlay").hidden = false;
|
$("ovHint").style.display = "none";
|
||||||
|
$("installOverlay").style.display = "block";
|
||||||
}
|
}
|
||||||
function hideOverlay(){
|
function hideOverlay() {
|
||||||
$("installOverlay").hidden = true;
|
$("installOverlay").style.display = "none";
|
||||||
}
|
}
|
||||||
function markOverlayDismissed(latestVersion){
|
function showManualInstallHint() {
|
||||||
sessionStorage.setItem(DISMISS_KEY, latestVersion || APP_VERSION);
|
$("ovHint").style.display = "block";
|
||||||
}
|
$("ovHint").textContent =
|
||||||
function isOverlayDismissed(latestVersion){
|
"Hinweis: Falls kein Install-Dialog erscheint: Chrome Menü (⋮) → „App installieren“ / „Zum Startbildschirm hinzufügen“.";
|
||||||
const v = sessionStorage.getItem(DISMISS_KEY);
|
|
||||||
return v && v === (latestVersion || APP_VERSION);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let deferredPrompt = null;
|
function compareVersions(a, b) {
|
||||||
window.addEventListener("beforeinstallprompt", (e) => {
|
// "2025.12.24.3" style; compare numeric segments
|
||||||
e.preventDefault();
|
const pa = String(a).split(".").map(n => parseInt(n, 10) || 0);
|
||||||
deferredPrompt = e;
|
const pb = String(b).split(".").map(n => parseInt(n, 10) || 0);
|
||||||
});
|
const len = Math.max(pa.length, pb.length);
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const da = pa[i] || 0, db = pb[i] || 0;
|
||||||
|
if (da > db) return 1;
|
||||||
|
if (da < db) return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Service Worker ---
|
async function fetchLatestVersion() {
|
||||||
async function registerSW(){
|
|
||||||
if (!("serviceWorker" in navigator)) return null;
|
|
||||||
try {
|
try {
|
||||||
const reg = await navigator.serviceWorker.register("./sw.js");
|
const r = await fetch(VERSION_URL, { cache: "no-store" });
|
||||||
// Optional: updaten, falls neue Version
|
if (!r.ok) throw new Error(`version.json HTTP ${r.status}`);
|
||||||
try { await reg.update(); } catch {}
|
const j = await r.json();
|
||||||
return reg;
|
if (j && j.version) {
|
||||||
} catch {
|
latestKnown = j.version;
|
||||||
return null;
|
sessionStorage.setItem("wfw_latest", latestKnown);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// not fatal
|
||||||
|
console.warn("Could not fetch version.json:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Install / Update Overlay ---
|
function getInstalledVersion() {
|
||||||
async function checkInstallOrUpdateOverlay(reg){
|
return localStorage.getItem("wfw_installed_version") || null;
|
||||||
let latest = APP_VERSION;
|
}
|
||||||
try {
|
function markInstalledVersion(v) {
|
||||||
const r = await fetch(VERSION_URL, { cache: "no-store" });
|
localStorage.setItem("wfw_installed_version", v);
|
||||||
if (r.ok) {
|
}
|
||||||
const j = await r.json();
|
function markOverlayDismissed(v) {
|
||||||
if (j && typeof j.version === "string") latest = j.version;
|
localStorage.setItem("wfw_dismissed_version", v);
|
||||||
}
|
}
|
||||||
} catch {}
|
function getDismissedVersion() {
|
||||||
|
return localStorage.getItem("wfw_dismissed_version") || null;
|
||||||
|
}
|
||||||
|
|
||||||
const installed = isInstalledPWA();
|
async function checkInstallOrUpdateOverlay() {
|
||||||
const outdated = (latest !== APP_VERSION);
|
await fetchLatestVersion();
|
||||||
|
|
||||||
// Wenn installiert UND aktuell -> kein Overlay
|
const installed = getInstalledVersion();
|
||||||
if (installed && !outdated) return;
|
const dismissed = getDismissedVersion();
|
||||||
|
const isStandalone = window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true;
|
||||||
// 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."
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// If installed version < latest -> show update overlay
|
||||||
|
if (installed && compareVersions(installed, latestKnown) < 0) {
|
||||||
|
showOverlay(
|
||||||
|
"Update verfügbar",
|
||||||
|
`Neue Version verfügbar (${latestKnown}). Bitte updaten für die neuesten Fixes.`,
|
||||||
|
"Update jetzt"
|
||||||
|
);
|
||||||
$("ovPrimaryBtn").onclick = async () => {
|
$("ovPrimaryBtn").onclick = async () => {
|
||||||
try { await reg?.update?.(); } catch {}
|
try {
|
||||||
// Cache-bust / reload
|
// ask SW to update + reload
|
||||||
|
const reg = await navigator.serviceWorker.getRegistration();
|
||||||
|
if (reg) await reg.update();
|
||||||
|
} catch {}
|
||||||
|
markInstalledVersion(latestKnown);
|
||||||
location.reload();
|
location.reload();
|
||||||
};
|
};
|
||||||
|
$("ovContinueBtn").onclick = () => {
|
||||||
$("ovContinueBtn").onclick = () => hideOverlay();
|
markOverlayDismissed(latestKnown);
|
||||||
|
hideOverlay();
|
||||||
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nicht installiert -> "Installieren" (oder Hinweis wenn Prompt fehlt)
|
// If not installed (or standalone not detected), show install overlay until latest dismissed/installed
|
||||||
const canInstall = !!deferredPrompt && !installed;
|
if (!installed && dismissed !== latestKnown && !isStandalone) {
|
||||||
|
showOverlay(
|
||||||
|
"WFW-Aushang",
|
||||||
|
"Installiere mich!",
|
||||||
|
"Installieren"
|
||||||
|
);
|
||||||
|
|
||||||
if (canInstall) {
|
$("ovPrimaryBtn").onclick = async (e) => {
|
||||||
showOverlay({
|
e.preventDefault(); e.stopPropagation();
|
||||||
title: "App installieren",
|
await doInstall();
|
||||||
text: "Installiert läuft WFW-Aushang schneller und im Kiosk-Modus.",
|
};
|
||||||
primaryText: "Installieren",
|
$("ovContinueBtn").onclick = (e) => {
|
||||||
hint: ""
|
e.preventDefault(); e.stopPropagation();
|
||||||
});
|
markOverlayDismissed(latestKnown);
|
||||||
|
|
||||||
$("ovPrimaryBtn").onclick = async () => {
|
|
||||||
try {
|
|
||||||
deferredPrompt.prompt();
|
|
||||||
await deferredPrompt.userChoice;
|
|
||||||
} catch {}
|
|
||||||
deferredPrompt = null;
|
|
||||||
hideOverlay();
|
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 doInstall() {
|
||||||
async function propfind(url, depth=1){
|
if (!deferredInstallPrompt) {
|
||||||
const res = await fetch(url, {
|
showManualInstallHint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
deferredInstallPrompt.prompt(); // <-- IMPORTANT
|
||||||
|
const res = await deferredInstallPrompt.userChoice;
|
||||||
|
deferredInstallPrompt = null;
|
||||||
|
|
||||||
|
if (res && res.outcome === "accepted") {
|
||||||
|
// We'll also set installed version on appinstalled event
|
||||||
|
$("ovTitle").textContent = "Installiert ✓";
|
||||||
|
$("ovText").textContent = "Danke! Du kannst die App jetzt wie eine normale App starten.";
|
||||||
|
markInstalledVersion(latestKnown);
|
||||||
|
setTimeout(() => hideOverlay(), 800);
|
||||||
|
} else {
|
||||||
|
showManualInstallHint();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Install prompt failed:", e);
|
||||||
|
showManualInstallHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WebDAV listing **/
|
||||||
|
async function propfind(dir) {
|
||||||
|
const url = `${SHARE_DAV}${dir.endsWith("/") ? dir : dir + "/"}`;
|
||||||
|
const body = `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<d:propfind xmlns:d="DAV:">
|
||||||
|
<d:prop>
|
||||||
|
<d:displayname/>
|
||||||
|
<d:getcontenttype/>
|
||||||
|
<d:getcontentlength/>
|
||||||
|
<d:resourcetype/>
|
||||||
|
<d:getlastmodified/>
|
||||||
|
</d:prop>
|
||||||
|
</d:propfind>`;
|
||||||
|
const r = await fetch(url, {
|
||||||
method: "PROPFIND",
|
method: "PROPFIND",
|
||||||
headers: { "Depth": String(depth) }
|
headers: { "Depth": "1", "Content-Type": "application/xml" },
|
||||||
|
body
|
||||||
});
|
});
|
||||||
|
if (!r.ok) {
|
||||||
if (!res.ok) {
|
const t = await r.text().catch(()=> "");
|
||||||
const t = await res.text().catch(()=> "");
|
throw new Error(`PROPFIND ${r.status} ${r.statusText}\n${t.slice(0, 400)}`);
|
||||||
throw new Error(`PROPFIND ${res.status} ${res.statusText} ${t.slice(0,200)}`);
|
|
||||||
}
|
}
|
||||||
return await res.text();
|
return await r.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDavResponse(xmlText){
|
function parseDavMultistatus(xmlText, dir) {
|
||||||
const parser = new DOMParser();
|
const p = new DOMParser();
|
||||||
const xml = parser.parseFromString(xmlText, "application/xml");
|
const xml = p.parseFromString(xmlText, "application/xml");
|
||||||
const responses = [...xml.getElementsByTagNameNS("*","response")];
|
const responses = Array.from(xml.getElementsByTagNameNS("DAV:", "response"));
|
||||||
|
|
||||||
const items = responses.map(r => {
|
const items = [];
|
||||||
const hrefEl = r.getElementsByTagNameNS("*","href")[0];
|
|
||||||
const href = hrefEl ? hrefEl.textContent : "";
|
|
||||||
|
|
||||||
const propstat = r.getElementsByTagNameNS("*","propstat")[0];
|
for (const resp of responses) {
|
||||||
const prop = propstat ? propstat.getElementsByTagNameNS("*","prop")[0] : null;
|
const hrefEl = resp.getElementsByTagNameNS("DAV:", "href")[0];
|
||||||
|
if (!hrefEl) continue;
|
||||||
|
const href = decodeURIComponent(hrefEl.textContent || "");
|
||||||
|
|
||||||
const isCollection = !!(prop && prop.getElementsByTagNameNS("*","collection")[0]);
|
// Skip the directory itself (first response)
|
||||||
|
const shareRoot = `/public.php/dav/files/${TOKEN}`;
|
||||||
|
if (href.endsWith(shareRoot + (dir === "/" ? "/" : dir))) continue;
|
||||||
|
|
||||||
const sizeEl = prop ? prop.getElementsByTagNameNS("*","getcontentlength")[0] : null;
|
const displayEl = resp.getElementsByTagNameNS("DAV:", "displayname")[0];
|
||||||
const size = sizeEl ? parseInt(sizeEl.textContent || "0", 10) : 0;
|
const name = displayEl ? (displayEl.textContent || "").trim() : "";
|
||||||
|
|
||||||
const modEl = prop ? prop.getElementsByTagNameNS("*","getlastmodified")[0] : null;
|
const rtEl = resp.getElementsByTagNameNS("DAV:", "resourcetype")[0];
|
||||||
const lastmod = modEl ? modEl.textContent : "";
|
const isCollection = rtEl && rtEl.getElementsByTagNameNS("DAV:", "collection").length > 0;
|
||||||
|
|
||||||
return { href, isCollection, size, lastmod };
|
const ctEl = resp.getElementsByTagNameNS("DAV:", "getcontenttype")[0];
|
||||||
});
|
const contentType = ctEl ? (ctEl.textContent || "").trim() : "";
|
||||||
|
|
||||||
|
const lmEl = resp.getElementsByTagNameNS("DAV:", "getlastmodified")[0];
|
||||||
|
const lastMod = lmEl ? (lmEl.textContent || "").trim() : "";
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
href,
|
||||||
|
name: name || href.split("/").filter(Boolean).slice(-1)[0] || "",
|
||||||
|
isDir: !!isCollection,
|
||||||
|
contentType,
|
||||||
|
lastMod
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// directories first, then by name
|
||||||
|
items.sort((a,b)=> (a.isDir === b.isDir ? a.name.localeCompare(b.name) : (a.isDir ? -1 : 1)));
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
function humanSize(bytes){
|
function toPdfJsUrl(publicPath) {
|
||||||
if (!bytes || bytes < 1024) return bytes ? `${bytes} B` : "";
|
// publicPath must be the share file path relative to share root, starting with "/"
|
||||||
const kb = bytes/1024;
|
const fileUrl = `${SHARE_DAV}${publicPath}`; // direct public dav file URL
|
||||||
if (kb < 1024) return `${kb.toFixed(0)} KB`;
|
return `${PDFJS}${encodeURIComponent(fileUrl)}#zoom=page-width`;
|
||||||
const mb = kb/1024;
|
|
||||||
return `${mb.toFixed(1)} MB`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeDecode(s){
|
function renderList(items, currentDir) {
|
||||||
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");
|
const list = $("list");
|
||||||
list.innerHTML = "";
|
list.innerHTML = "";
|
||||||
|
|
||||||
if (!files.length) {
|
// no folder browsing (kiosk): show only PDFs in root+subdirs? you asked "only directory content" of share root.
|
||||||
$("status").textContent = "Keine PDFs gefunden.";
|
// We'll show only currentDir content; no folder nav UI.
|
||||||
|
const pdfs = items.filter(it => !it.isDir && (it.name.toLowerCase().endsWith(".pdf") || it.contentType.includes("pdf")));
|
||||||
|
|
||||||
|
if (pdfs.length === 0) {
|
||||||
|
setStatus("Keine PDFs gefunden.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const f of files) {
|
setStatus(`PDFs: ${pdfs.length}`);
|
||||||
const row = document.createElement("div");
|
|
||||||
row.className = "item";
|
|
||||||
|
|
||||||
const icon = document.createElement("div");
|
for (const it of pdfs) {
|
||||||
icon.className = "pdfIcon";
|
const a = document.createElement("a");
|
||||||
icon.textContent = "📄";
|
a.className = "row";
|
||||||
|
a.href = "#";
|
||||||
|
a.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
const name = document.createElement("div");
|
// Build relative path within share
|
||||||
name.className = "name";
|
// href includes /public.php/dav/files/<token>/...; we want path after that
|
||||||
name.textContent = f.name;
|
const marker = `/public.php/dav/files/${TOKEN}`;
|
||||||
|
const idx = it.href.indexOf(marker);
|
||||||
|
let rel = "/";
|
||||||
|
if (idx >= 0) rel = it.href.slice(idx + marker.length);
|
||||||
|
if (!rel.startsWith("/")) rel = "/" + rel;
|
||||||
|
|
||||||
const meta = document.createElement("div");
|
window.location.href = toPdfJsUrl(rel);
|
||||||
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();
|
const ico = document.createElement("div");
|
||||||
|
ico.className = "ico";
|
||||||
|
ico.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M7 3h7l3 3v15a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z" stroke="currentColor" stroke-width="2" opacity=".9"/>
|
||||||
|
<path d="M14 3v4h4" stroke="currentColor" stroke-width="2" opacity=".9"/>
|
||||||
|
<path d="M7.5 14.5c2-2.5 3.5-5 4.2-7.5 1.1 3.8 2.8 7 4.8 9.5" stroke="currentColor" stroke-width="2" opacity=".9"/>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
const mid = document.createElement("div");
|
||||||
|
const nm = document.createElement("div");
|
||||||
|
nm.className = "name";
|
||||||
|
nm.textContent = it.name;
|
||||||
|
const sub = document.createElement("div");
|
||||||
|
sub.className = "sub";
|
||||||
|
sub.textContent = it.lastMod ? it.lastMod : "";
|
||||||
|
mid.appendChild(nm);
|
||||||
|
mid.appendChild(sub);
|
||||||
|
|
||||||
|
const badge = document.createElement("div");
|
||||||
|
badge.className = "badge";
|
||||||
|
badge.textContent = "PDF";
|
||||||
|
|
||||||
|
a.appendChild(ico);
|
||||||
|
a.appendChild(mid);
|
||||||
|
a.appendChild(document.createElement("div")).className = "spacer";
|
||||||
|
a.appendChild(badge);
|
||||||
|
|
||||||
|
list.appendChild(a);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadList() {
|
||||||
|
clearError();
|
||||||
|
setStatus("Lade Liste…");
|
||||||
|
try {
|
||||||
|
const xml = await propfind(LIST_START_DIR);
|
||||||
|
const items = parseDavMultistatus(xml, LIST_START_DIR);
|
||||||
|
renderList(items, LIST_START_DIR);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setStatus("Fehler");
|
||||||
|
showError(String(e && e.message ? e.message : e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerSW() {
|
||||||
|
if (!("serviceWorker" in navigator)) return;
|
||||||
|
try {
|
||||||
|
const reg = await navigator.serviceWorker.register("./sw.js", { scope: "./" });
|
||||||
|
// Trigger update check
|
||||||
|
reg.update().catch(()=>{});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("SW register failed:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(t){
|
||||||
|
document.documentElement.setAttribute("data-theme", t);
|
||||||
|
localStorage.setItem("wfw_theme", t);
|
||||||
|
$("themeBtn").textContent = t === "dark" ? "🌙 Dunkel" : "☀️ Hell";
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTheme(){
|
||||||
|
const saved = localStorage.getItem("wfw_theme");
|
||||||
|
if (saved){
|
||||||
|
applyTheme(saved);
|
||||||
|
} else {
|
||||||
|
// Default: System
|
||||||
|
const prefersLight = window.matchMedia("(prefers-color-scheme: light)").matches;
|
||||||
|
applyTheme(prefersLight ? "light" : "dark");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener("appinstalled", () => {
|
||||||
|
// user installed the app
|
||||||
|
markInstalledVersion(latestKnown);
|
||||||
|
markOverlayDismissed(latestKnown);
|
||||||
|
hideOverlay();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("beforeinstallprompt", (e) => {
|
||||||
|
// We want our own overlay, so we prevent default banner
|
||||||
|
e.preventDefault();
|
||||||
|
deferredInstallPrompt = e;
|
||||||
|
// Show overlay if not yet installed
|
||||||
|
checkInstallOrUpdateOverlay();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extra safety: capture clicks even if something gets weird on mobile
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
const t = e.target;
|
||||||
|
if (!t) return;
|
||||||
|
|
||||||
|
if (t.id === "ovContinueBtn") {
|
||||||
|
e.preventDefault(); e.stopPropagation();
|
||||||
|
markOverlayDismissed(latestKnown);
|
||||||
|
hideOverlay();
|
||||||
|
}
|
||||||
|
if (t.id === "ovPrimaryBtn") {
|
||||||
|
// If handler was overwritten, still attempt install/update
|
||||||
|
// (doInstall will show manual hint if prompt not available)
|
||||||
|
}
|
||||||
|
}, { capture: true });
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
$("refreshBtn").addEventListener("click", (e) => { e.preventDefault(); loadList(); });
|
||||||
|
$("closeBtn").addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// Kiosk-ish: close tab if possible, else just go back
|
||||||
|
window.close();
|
||||||
|
setTimeout(() => { history.back(); }, 150);
|
||||||
|
});
|
||||||
|
initTheme();
|
||||||
|
$("themeBtn").addEventListener("click", () => {
|
||||||
|
const current = document.documentElement.getAttribute("data-theme") || "dark";
|
||||||
|
applyTheme(current === "dark" ? "light" : "dark");
|
||||||
|
});
|
||||||
|
await registerSW();
|
||||||
|
await checkInstallOrUpdateOverlay();
|
||||||
|
await loadList();
|
||||||
});
|
});
|
||||||
378
zuss/index.html
378
zuss/index.html
@@ -1,285 +1,173 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="de" translate="no">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||||||
|
<meta name="theme-color" content="#0b1020" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
|
||||||
|
<link rel="manifest" href="./manifest.webmanifest">
|
||||||
|
<link rel="icon" href="./icons/icon-192.png">
|
||||||
|
<link rel="apple-touch-icon" href="./icons/icon-192.png">
|
||||||
|
|
||||||
<title>WFW-Aushang</title>
|
<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>
|
<style>
|
||||||
:root{
|
:root,
|
||||||
--bg: #f6f7fb;
|
:root[data-theme="dark"]{
|
||||||
--panel: rgba(255,255,255,.75);
|
--bg:#0b1020; --card:#121a33; --text:#e8eeff; --muted:#9fb0ff;
|
||||||
--panelSolid: #ffffff;
|
--line:rgba(255,255,255,.12); --btn:#2b60ff; --btn2:#1c254b;
|
||||||
--text: #0b1220;
|
--ok:#2bff9f; --warn:#ffd36b;
|
||||||
--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"]{
|
:root[data-theme="light"]{
|
||||||
--bg: #070a12;
|
--bg:#f6f7fb; --card:#ffffff; --text:#0b1020; --muted:#5060a8;
|
||||||
--panel: rgba(17,24,39,.55);
|
--line:rgba(0,0,0,.10); --btn:#2b60ff; --btn2:#eef1ff;
|
||||||
--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%; }
|
|
||||||
|
*{ box-sizing:border-box; }
|
||||||
body{
|
body{
|
||||||
margin:0;
|
margin:0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Arial;
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
background:var(--bg); color:var(--text);
|
||||||
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{
|
.wrap{
|
||||||
padding: 14px 12px 96px;
|
max-width: 860px; margin: 0 auto; padding: 16px 16px 92px;
|
||||||
max-width: 980px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
h1{ font-size: 20px; margin: 8px 0 14px; letter-spacing:.3px; }
|
||||||
|
.list{
|
||||||
|
display:flex; flex-direction:column; gap:10px;
|
||||||
|
}
|
||||||
|
.row{
|
||||||
|
display:flex; align-items:center; gap:12px;
|
||||||
|
background:var(--card); border:1px solid var(--line);
|
||||||
|
border-radius:16px; padding:12px 14px;
|
||||||
|
text-decoration:none; color:inherit;
|
||||||
|
}
|
||||||
|
.row:active{ transform: scale(0.995); }
|
||||||
|
.ico{
|
||||||
|
width:38px; height:38px; border-radius:12px;
|
||||||
|
display:grid; place-items:center;
|
||||||
|
background:rgba(43,96,255,.16);
|
||||||
|
border:1px solid rgba(43,96,255,.22);
|
||||||
|
flex:0 0 auto;
|
||||||
|
}
|
||||||
|
.name{ font-size: 16px; line-height:1.2; }
|
||||||
|
.sub{ font-size: 12px; color: var(--muted); margin-top:3px; }
|
||||||
|
.spacer{ flex:1; }
|
||||||
|
.badge{
|
||||||
|
font-size:12px; color:var(--muted);
|
||||||
|
border:1px solid var(--line); padding:6px 10px; border-radius:999px;
|
||||||
|
background:rgba(0,0,0,.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer{
|
||||||
|
position:fixed; left:0; right:0; bottom:0;
|
||||||
|
padding:12px 12px calc(12px + env(safe-area-inset-bottom));
|
||||||
|
background:linear-gradient(to top, rgba(11,16,32,.95), rgba(11,16,32,.55), rgba(11,16,32,0));
|
||||||
|
pointer-events:none;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: light){
|
||||||
|
.footer{ background:linear-gradient(to top, rgba(246,247,251,.95), rgba(246,247,251,.55), rgba(246,247,251,0)); }
|
||||||
|
}
|
||||||
|
.footerInner{
|
||||||
|
max-width: 860px; margin:0 auto; display:flex; gap:10px; pointer-events:auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BIG BUTTONS */
|
||||||
|
.btn{
|
||||||
|
border:0; border-radius:18px;
|
||||||
|
padding:18px 18px; /* bigger */
|
||||||
|
font-size:18px; /* bigger */
|
||||||
|
font-weight:800;
|
||||||
|
min-height:64px; /* bigger */
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
cursor:pointer;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,.22);
|
||||||
|
text-decoration:none;
|
||||||
|
user-select:none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.btnPrimary{ background:var(--btn); color:white; flex:1; }
|
||||||
|
.btnSecondary{ background:var(--btn2); color:var(--text); flex:1; border:1px solid var(--line); box-shadow:none; }
|
||||||
|
.btn:active{ transform: scale(0.995); }
|
||||||
|
|
||||||
.status{
|
.status{
|
||||||
margin: 12px 4px;
|
margin: 8px 0 14px; color: var(--muted); font-size: 13px;
|
||||||
font-size: 13px;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
}
|
||||||
|
.error{
|
||||||
.list{
|
background: rgba(255,80,80,.12);
|
||||||
border: 1px solid var(--border);
|
border:1px solid rgba(255,80,80,.25);
|
||||||
border-radius: 18px;
|
color: #ffb7b7;
|
||||||
overflow: hidden;
|
padding: 10px 12px;
|
||||||
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-radius: 14px;
|
||||||
border: 1px solid var(--border);
|
font-size: 13px;
|
||||||
background: var(--accent);
|
white-space: pre-wrap;
|
||||||
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 */
|
/* INSTALL/UPDATE OVERLAY */
|
||||||
.ov{
|
#installOverlay{
|
||||||
position: fixed;
|
position:fixed; inset:0;
|
||||||
inset: 0;
|
display:none;
|
||||||
z-index: 9999;
|
z-index:2147483647;
|
||||||
display: grid;
|
background: rgba(0,0,0,.55);
|
||||||
place-items: center;
|
backdrop-filter: blur(10px);
|
||||||
padding: 18px;
|
pointer-events:auto;
|
||||||
background: rgba(0,0,0,.62);
|
padding: 18px 14px;
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
}
|
}
|
||||||
|
#installOverlay *{ pointer-events:auto; }
|
||||||
.ovCard{
|
.ovCard{
|
||||||
width: min(560px, 100%);
|
max-width: 560px;
|
||||||
|
margin: 10vh auto 0;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--line);
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
border: 1px solid rgba(255,255,255,.14);
|
padding: 18px 16px;
|
||||||
background: color-mix(in srgb, var(--panelSolid) 92%, transparent);
|
box-shadow: 0 20px 70px rgba(0,0,0,.35);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
.ovTitle{ font-size: 18px; font-weight: 850; margin: 6px 0 6px; }
|
||||||
|
.ovText{ color: var(--muted); font-size: 14px; line-height: 1.35; margin: 0 0 14px; }
|
||||||
|
.ovBtns{ display:flex; gap:12px; margin-top: 10px; }
|
||||||
|
.ovBtns .btn{ flex:1; }
|
||||||
.ovHint{
|
.ovHint{
|
||||||
margin-top: 10px;
|
margin-top: 12px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
padding-top: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- Kiosk Overlay -->
|
<div class="wrap">
|
||||||
<div id="installOverlay" class="ov" hidden>
|
<h1>WFW-Aushang</h1>
|
||||||
|
<div id="status" class="status">Lade Liste…</div>
|
||||||
|
<div id="err" class="error" style="display:none"></div>
|
||||||
|
<div id="list" class="list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<div class="footerInner">
|
||||||
|
<button id="closeBtn" class="btn btnSecondary">Aushang schließen</button>
|
||||||
|
<button id="themeBtn" class="btn btnSecondary">🌙 Dunkel</button>
|
||||||
|
<button id="refreshBtn" class="btn btnPrimary">↻ Refresh</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="installOverlay" role="dialog" aria-modal="true">
|
||||||
<div class="ovCard">
|
<div class="ovCard">
|
||||||
<div class="ovTitle" id="ovTitle">WFW-Aushang</div>
|
<div class="ovTitle" id="ovTitle">WFW-Aushang</div>
|
||||||
<div class="ovText" id="ovText">Installiere die App für Kiosk-Modus und schnelle PDFs.</div>
|
<p class="ovText" id="ovText">Installiere mich!</p>
|
||||||
|
<div class="ovBtns">
|
||||||
<button id="ovPrimaryBtn" class="ovPrimary">Installieren</button>
|
<button id="ovPrimaryBtn" class="btn btnPrimary">Installieren</button>
|
||||||
<button id="ovContinueBtn" class="ovLink" type="button">Weiter im Browser</button>
|
<button id="ovContinueBtn" class="btn btnSecondary">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>
|
||||||
</div>
|
<div class="ovHint" id="ovHint" style="display:none"></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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="./app.js"></script>
|
<script src="./app.js?v=2025.12.24.6"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "WFW-Aushang",
|
"name": "WFW-Aushang",
|
||||||
"short_name": "WFW-Aushang",
|
"short_name": "WFW-Aushang",
|
||||||
"start_url": "/zuss/",
|
"start_url": "/zuss/?source=pwa",
|
||||||
"scope": "/zuss/",
|
"scope": "/zuss/",
|
||||||
"display": "fullscreen",
|
"display": "standalone",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"background_color": "#0b1020",
|
"background_color": "#0b1020",
|
||||||
"theme_color": "#0b1020",
|
"theme_color": "#0b1020",
|
||||||
@@ -12,3 +12,4 @@
|
|||||||
{ "src": "/zuss/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
|
{ "src": "/zuss/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
70
zuss/sw.js
70
zuss/sw.js
@@ -1,5 +1,4 @@
|
|||||||
// Simple offline cache for kiosk usage
|
const CACHE = "wfw-aushang-2025.12.24.6";
|
||||||
const CACHE = "wfw-aushang-2025.12.24.2";
|
|
||||||
|
|
||||||
const ASSETS = [
|
const ASSETS = [
|
||||||
"/zuss/",
|
"/zuss/",
|
||||||
@@ -12,48 +11,51 @@ const ASSETS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
self.addEventListener("install", (event) => {
|
self.addEventListener("install", (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil((async () => {
|
||||||
caches.open(CACHE).then((c) => c.addAll(ASSETS)).then(() => self.skipWaiting())
|
const cache = await caches.open(CACHE);
|
||||||
);
|
await cache.addAll(ASSETS);
|
||||||
|
self.skipWaiting();
|
||||||
|
})());
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("activate", (event) => {
|
self.addEventListener("activate", (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil((async () => {
|
||||||
(async () => {
|
const keys = await caches.keys();
|
||||||
const keys = await caches.keys();
|
await Promise.all(keys.map(k => (k === CACHE ? null : caches.delete(k))));
|
||||||
await Promise.all(keys.map(k => (k === CACHE ? null : caches.delete(k))));
|
self.clients.claim();
|
||||||
await self.clients.claim();
|
})());
|
||||||
})()
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Network-first for version.json + list, cache-first for static assets
|
|
||||||
self.addEventListener("fetch", (event) => {
|
self.addEventListener("fetch", (event) => {
|
||||||
const url = new URL(event.request.url);
|
const req = event.request;
|
||||||
|
const url = new URL(req.url);
|
||||||
|
|
||||||
// Only handle same-origin requests
|
// Network-first for version.json (so updates are found)
|
||||||
if (url.origin !== self.location.origin) return;
|
if (url.pathname.endsWith("/zuss/version.json")) {
|
||||||
|
event.respondWith((async () => {
|
||||||
const isVersion = url.pathname.endsWith("/zuss/version.json");
|
try {
|
||||||
|
const fresh = await fetch(req, { cache: "no-store" });
|
||||||
if (isVersion) {
|
const cache = await caches.open(CACHE);
|
||||||
event.respondWith(
|
cache.put(req, fresh.clone());
|
||||||
fetch(event.request, { cache: "no-store" }).catch(() => caches.match(event.request))
|
return fresh;
|
||||||
);
|
} catch {
|
||||||
|
const cached = await caches.match(req);
|
||||||
|
return cached || new Response('{"version":"2025.12.24.6"}', { headers: { "Content-Type": "application/json" } });
|
||||||
|
}
|
||||||
|
})());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache-first for our own static assets
|
// Cache-first for app shell
|
||||||
if (url.pathname.startsWith("/zuss/")) {
|
if (url.pathname.startsWith("/zuss/")) {
|
||||||
event.respondWith(
|
event.respondWith((async () => {
|
||||||
caches.match(event.request).then((cached) => {
|
const cached = await caches.match(req);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
return fetch(event.request).then((resp) => {
|
const fresh = await fetch(req);
|
||||||
const copy = resp.clone();
|
const cache = await caches.open(CACHE);
|
||||||
caches.open(CACHE).then((c) => c.put(event.request, copy));
|
cache.put(req, fresh.clone());
|
||||||
return resp;
|
return fresh;
|
||||||
});
|
})());
|
||||||
})
|
return;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{ "version": "2025.12.24.2" }
|
{ "version": "2025.12.24.6" }
|
||||||
|
|||||||
Reference in New Issue
Block a user