up up and away
This commit is contained in:
581
zuss/app.js
581
zuss/app.js
@@ -1,321 +1,394 @@
|
||||
/* WFW-Aushang PWA
|
||||
* - Listet Dateien aus public WebDAV Share
|
||||
* - Öffnet PDFs über PDF.js
|
||||
* - Kioskiges Install/Update Overlay
|
||||
* - Install/Update Overlay mit beforeinstallprompt.prompt()
|
||||
*/
|
||||
|
||||
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 APP_VERSION = "2025.12.24.6";
|
||||
const TOKEN = "T9e7WESBXxy6rSD";
|
||||
const BASE = "https://home.x-s.at";
|
||||
const SHARE_DAV = `${BASE}/public.php/dav/files/${TOKEN}`;
|
||||
const PDFJS = `${BASE}/pdfjs/web/viewer.html?file=`;
|
||||
|
||||
// relative to /zuss/
|
||||
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){
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
localStorage.setItem("wfw_theme", theme);
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
function showError(msg) {
|
||||
const el = $("err");
|
||||
el.style.display = "block";
|
||||
el.textContent = msg;
|
||||
}
|
||||
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 clearError() {
|
||||
const el = $("err");
|
||||
el.style.display = "none";
|
||||
el.textContent = "";
|
||||
}
|
||||
function setStatus(msg) { $("status").textContent = msg; }
|
||||
|
||||
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=""}){
|
||||
function showOverlay(title, text, primaryText, showPrimary = true) {
|
||||
$("ovTitle").textContent = title;
|
||||
$("ovText").textContent = text;
|
||||
$("ovPrimaryBtn").textContent = primaryText;
|
||||
$("ovHint").textContent = hint;
|
||||
$("installOverlay").hidden = false;
|
||||
$("ovPrimaryBtn").style.display = showPrimary ? "flex" : "none";
|
||||
$("ovHint").style.display = "none";
|
||||
$("installOverlay").style.display = "block";
|
||||
}
|
||||
function hideOverlay(){
|
||||
$("installOverlay").hidden = true;
|
||||
function hideOverlay() {
|
||||
$("installOverlay").style.display = "none";
|
||||
}
|
||||
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);
|
||||
function showManualInstallHint() {
|
||||
$("ovHint").style.display = "block";
|
||||
$("ovHint").textContent =
|
||||
"Hinweis: Falls kein Install-Dialog erscheint: Chrome Menü (⋮) → „App installieren“ / „Zum Startbildschirm hinzufügen“.";
|
||||
}
|
||||
|
||||
let deferredPrompt = null;
|
||||
window.addEventListener("beforeinstallprompt", (e) => {
|
||||
e.preventDefault();
|
||||
deferredPrompt = e;
|
||||
});
|
||||
function compareVersions(a, b) {
|
||||
// "2025.12.24.3" style; compare numeric segments
|
||||
const pa = String(a).split(".").map(n => parseInt(n, 10) || 0);
|
||||
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 registerSW(){
|
||||
if (!("serviceWorker" in navigator)) return null;
|
||||
async function fetchLatestVersion() {
|
||||
try {
|
||||
const reg = await navigator.serviceWorker.register("./sw.js");
|
||||
// Optional: updaten, falls neue Version
|
||||
try { await reg.update(); } catch {}
|
||||
return reg;
|
||||
} catch {
|
||||
return null;
|
||||
const r = await fetch(VERSION_URL, { cache: "no-store" });
|
||||
if (!r.ok) throw new Error(`version.json HTTP ${r.status}`);
|
||||
const j = await r.json();
|
||||
if (j && j.version) {
|
||||
latestKnown = j.version;
|
||||
sessionStorage.setItem("wfw_latest", latestKnown);
|
||||
}
|
||||
} catch (e) {
|
||||
// not fatal
|
||||
console.warn("Could not fetch version.json:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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 {}
|
||||
function getInstalledVersion() {
|
||||
return localStorage.getItem("wfw_installed_version") || null;
|
||||
}
|
||||
function markInstalledVersion(v) {
|
||||
localStorage.setItem("wfw_installed_version", v);
|
||||
}
|
||||
function markOverlayDismissed(v) {
|
||||
localStorage.setItem("wfw_dismissed_version", v);
|
||||
}
|
||||
function getDismissedVersion() {
|
||||
return localStorage.getItem("wfw_dismissed_version") || null;
|
||||
}
|
||||
|
||||
const installed = isInstalledPWA();
|
||||
const outdated = (latest !== APP_VERSION);
|
||||
async function checkInstallOrUpdateOverlay() {
|
||||
await fetchLatestVersion();
|
||||
|
||||
// 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."
|
||||
});
|
||||
const installed = getInstalledVersion();
|
||||
const dismissed = getDismissedVersion();
|
||||
const isStandalone = window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true;
|
||||
|
||||
// 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 () => {
|
||||
try { await reg?.update?.(); } catch {}
|
||||
// Cache-bust / reload
|
||||
try {
|
||||
// ask SW to update + reload
|
||||
const reg = await navigator.serviceWorker.getRegistration();
|
||||
if (reg) await reg.update();
|
||||
} catch {}
|
||||
markInstalledVersion(latestKnown);
|
||||
location.reload();
|
||||
};
|
||||
|
||||
$("ovContinueBtn").onclick = () => hideOverlay();
|
||||
$("ovContinueBtn").onclick = () => {
|
||||
markOverlayDismissed(latestKnown);
|
||||
hideOverlay();
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// Nicht installiert -> "Installieren" (oder Hinweis wenn Prompt fehlt)
|
||||
const canInstall = !!deferredPrompt && !installed;
|
||||
// If not installed (or standalone not detected), show install overlay until latest dismissed/installed
|
||||
if (!installed && dismissed !== latestKnown && !isStandalone) {
|
||||
showOverlay(
|
||||
"WFW-Aushang",
|
||||
"Installiere mich!",
|
||||
"Installieren"
|
||||
);
|
||||
|
||||
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;
|
||||
$("ovPrimaryBtn").onclick = async (e) => {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
await doInstall();
|
||||
};
|
||||
$("ovContinueBtn").onclick = (e) => {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
markOverlayDismissed(latestKnown);
|
||||
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, {
|
||||
async function doInstall() {
|
||||
if (!deferredInstallPrompt) {
|
||||
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",
|
||||
headers: { "Depth": String(depth) }
|
||||
headers: { "Depth": "1", "Content-Type": "application/xml" },
|
||||
body
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const t = await res.text().catch(()=> "");
|
||||
throw new Error(`PROPFIND ${res.status} ${res.statusText} ${t.slice(0,200)}`);
|
||||
if (!r.ok) {
|
||||
const t = await r.text().catch(()=> "");
|
||||
throw new Error(`PROPFIND ${r.status} ${r.statusText}\n${t.slice(0, 400)}`);
|
||||
}
|
||||
return await res.text();
|
||||
return await r.text();
|
||||
}
|
||||
|
||||
function parseDavResponse(xmlText){
|
||||
const parser = new DOMParser();
|
||||
const xml = parser.parseFromString(xmlText, "application/xml");
|
||||
const responses = [...xml.getElementsByTagNameNS("*","response")];
|
||||
function parseDavMultistatus(xmlText, dir) {
|
||||
const p = new DOMParser();
|
||||
const xml = p.parseFromString(xmlText, "application/xml");
|
||||
const responses = Array.from(xml.getElementsByTagNameNS("DAV:", "response"));
|
||||
|
||||
const items = responses.map(r => {
|
||||
const hrefEl = r.getElementsByTagNameNS("*","href")[0];
|
||||
const href = hrefEl ? hrefEl.textContent : "";
|
||||
const items = [];
|
||||
|
||||
const propstat = r.getElementsByTagNameNS("*","propstat")[0];
|
||||
const prop = propstat ? propstat.getElementsByTagNameNS("*","prop")[0] : null;
|
||||
for (const resp of responses) {
|
||||
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 size = sizeEl ? parseInt(sizeEl.textContent || "0", 10) : 0;
|
||||
const displayEl = resp.getElementsByTagNameNS("DAV:", "displayname")[0];
|
||||
const name = displayEl ? (displayEl.textContent || "").trim() : "";
|
||||
|
||||
const modEl = prop ? prop.getElementsByTagNameNS("*","getlastmodified")[0] : null;
|
||||
const lastmod = modEl ? modEl.textContent : "";
|
||||
const rtEl = resp.getElementsByTagNameNS("DAV:", "resourcetype")[0];
|
||||
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;
|
||||
}
|
||||
|
||||
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 toPdfJsUrl(publicPath) {
|
||||
// publicPath must be the share file path relative to share root, starting with "/"
|
||||
const fileUrl = `${SHARE_DAV}${publicPath}`; // direct public dav file URL
|
||||
return `${PDFJS}${encodeURIComponent(fileUrl)}#zoom=page-width`;
|
||||
}
|
||||
|
||||
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){
|
||||
function renderList(items, currentDir) {
|
||||
const list = $("list");
|
||||
list.innerHTML = "";
|
||||
|
||||
if (!files.length) {
|
||||
$("status").textContent = "Keine PDFs gefunden.";
|
||||
// no folder browsing (kiosk): show only PDFs in root+subdirs? you asked "only directory content" of share root.
|
||||
// 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;
|
||||
}
|
||||
|
||||
for (const f of files) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "item";
|
||||
setStatus(`PDFs: ${pdfs.length}`);
|
||||
|
||||
const icon = document.createElement("div");
|
||||
icon.className = "pdfIcon";
|
||||
icon.textContent = "📄";
|
||||
for (const it of pdfs) {
|
||||
const a = document.createElement("a");
|
||||
a.className = "row";
|
||||
a.href = "#";
|
||||
a.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const name = document.createElement("div");
|
||||
name.className = "name";
|
||||
name.textContent = f.name;
|
||||
// Build relative path within share
|
||||
// href includes /public.php/dav/files/<token>/...; we want path after that
|
||||
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");
|
||||
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)
|
||||
window.location.href = toPdfJsUrl(rel);
|
||||
});
|
||||
$("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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user