- 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.
321 lines
9.2 KiB
JavaScript
321 lines
9.2 KiB
JavaScript
/* 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();
|
|
}
|
|
}); |