Initial commit: KGV-PWA plugin with manifest, service worker and install prompt

This commit is contained in:
Ronny Grobel
2026-04-20 21:12:48 +02:00
commit e6930011ab
5 changed files with 891 additions and 0 deletions

84
assets/css/kgv-pwa.css Normal file
View File

@@ -0,0 +1,84 @@
/**
* KGV PWA Install-Banner
*/
#kgv-pwa-install-banner {
position: fixed;
bottom: -80px;
left: 0;
right: 0;
z-index: 99999;
display: flex;
align-items: center;
gap: 12px;
padding: 14px 20px;
background: #1b5e20;
color: #fff;
font-family: inherit;
font-size: 14px;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.25);
transition: bottom 0.3s ease;
}
#kgv-pwa-install-banner.kgv-pwa-banner--visible {
bottom: 0;
}
.kgv-pwa-banner__text {
flex: 1;
}
.kgv-pwa-banner__install {
flex-shrink: 0;
padding: 7px 18px;
background: #fff;
color: #1b5e20;
border: none;
border-radius: 4px;
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.kgv-pwa-banner__install:hover,
.kgv-pwa-banner__install:focus {
background: #e8f5e9;
outline: 2px solid #fff;
outline-offset: 2px;
}
.kgv-pwa-banner__close {
flex-shrink: 0;
background: transparent;
border: none;
color: #fff;
font-size: 20px;
line-height: 1;
cursor: pointer;
padding: 0 4px;
opacity: 0.8;
}
.kgv-pwa-banner__close:hover,
.kgv-pwa-banner__close:focus {
opacity: 1;
outline: 1px solid rgba(255, 255, 255, 0.6);
border-radius: 2px;
}
/* Schmale Bildschirme */
@media ( max-width: 480px ) {
#kgv-pwa-install-banner {
flex-wrap: wrap;
gap: 8px;
}
.kgv-pwa-banner__text {
flex-basis: 100%;
}
.kgv-pwa-banner__install {
flex: 1;
}
}

124
assets/js/pwa-register.js Normal file
View File

@@ -0,0 +1,124 @@
/**
* KGV PWA Service Worker Registrierung & Install-Prompt
*/
( function () {
'use strict';
var cfg = window.kgvPwaConfig || {};
var swUrl = cfg.swUrl || '/sw.js';
var scope = cfg.scope || '/';
// ------------------------------------------------------------------
// Service Worker registrieren
// ------------------------------------------------------------------
if ( 'serviceWorker' in navigator ) {
window.addEventListener( 'load', function () {
navigator.serviceWorker
.register( swUrl, { scope: scope } )
.catch( function ( err ) {
if ( window.console && console.warn ) {
console.warn( 'KGV PWA: Service Worker konnte nicht registriert werden.', err );
}
} );
} );
}
// ------------------------------------------------------------------
// Install-Banner
// ------------------------------------------------------------------
var deferredPrompt = null;
var banner = null;
/**
* Banner-Element erzeugen und in den DOM einhängen.
*/
function createBanner() {
banner = document.createElement( 'div' );
banner.id = 'kgv-pwa-install-banner';
banner.setAttribute( 'role', 'region' );
banner.setAttribute( 'aria-label', 'App installieren' );
banner.innerHTML =
'<span class="kgv-pwa-banner__text">Diese Seite als App installieren</span>' +
'<button class="kgv-pwa-banner__install" type="button">Installieren</button>' +
'<button class="kgv-pwa-banner__close" type="button" aria-label="Schließen">&times;</button>';
document.body.appendChild( banner );
banner.querySelector( '.kgv-pwa-banner__install' ).addEventListener( 'click', triggerInstall );
banner.querySelector( '.kgv-pwa-banner__close' ).addEventListener( 'click', dismissBanner );
}
function showBanner() {
if ( ! banner ) {
createBanner();
}
banner.classList.add( 'kgv-pwa-banner--visible' );
}
function dismissBanner() {
if ( banner ) {
banner.classList.remove( 'kgv-pwa-banner--visible' );
}
// Nicht mehr anzeigen bis zum nächsten Tag
try {
sessionStorage.setItem( 'kgv_pwa_install_dismissed', '1' );
} catch ( e ) {}
}
/**
* Nativen Browser-Installationsdialog öffnen.
* Kann auch von einem beliebigen Button auf der Seite aufgerufen werden:
* document.dispatchEvent( new CustomEvent('kgv-pwa-install') );
*/
function triggerInstall() {
if ( ! deferredPrompt ) {
if ( isIos() ) {
window.alert( cfg.iosInstallNotice || 'Auf iPhone/iPad: Teilen > Zum Home-Bildschirm.' );
}
return;
}
deferredPrompt.prompt();
deferredPrompt.userChoice.then( function ( result ) {
deferredPrompt = null;
dismissBanner();
if ( window.console && console.info ) {
console.info( 'KGV PWA: Installationsentscheidung:', result.outcome );
}
} );
}
// Externer Aufruf via Custom Event ermöglichen
document.addEventListener( 'kgv-pwa-install', triggerInstall );
// ------------------------------------------------------------------
// beforeinstallprompt Browser signalisiert Installierbarkeit
// ------------------------------------------------------------------
window.addEventListener( 'beforeinstallprompt', function ( e ) {
// Standard-Mini-Infobar des Browsers unterdrücken
e.preventDefault();
deferredPrompt = e;
// Banner nicht zeigen, wenn der Nutzer ihn in dieser Sitzung bereits weggeklickt hat
try {
if ( sessionStorage.getItem( 'kgv_pwa_install_dismissed' ) ) {
return;
}
} catch ( err ) {}
showBanner();
} );
// ------------------------------------------------------------------
// appinstalled nach erfolgreicher Installation Banner verbergen
// ------------------------------------------------------------------
window.addEventListener( 'appinstalled', function () {
deferredPrompt = null;
dismissBanner();
} );
function isIos() {
return /iphone|ipad|ipod/i.test( window.navigator.userAgent || '' );
}
} )();

117
assets/js/service-worker.js Normal file
View File

@@ -0,0 +1,117 @@
/**
* KGV PWA Service Worker
*
* Strategie:
* - HTML-Seiten: Network-first (frische Inhalte, Fallback auf Cache)
* - Statische Assets (CSS/JS/Bilder/Fonts): Cache-first
*/
'use strict';
const CACHE_NAME = 'kgv-pwa-v1';
const OFFLINE_PAGE = '/';
const PRECACHE_URLS = [
'/',
];
// -----------------------------------------------------------------------
// Install: Precache-Ressourcen laden
// -----------------------------------------------------------------------
self.addEventListener( 'install', function ( event ) {
event.waitUntil(
caches.open( CACHE_NAME ).then( function ( cache ) {
return cache.addAll( PRECACHE_URLS );
} )
);
self.skipWaiting();
} );
// -----------------------------------------------------------------------
// Activate: Alte Caches löschen
// -----------------------------------------------------------------------
self.addEventListener( 'activate', function ( event ) {
event.waitUntil(
caches.keys().then( function ( keys ) {
return Promise.all(
keys
.filter( function ( key ) { return key !== CACHE_NAME; } )
.map( function ( key ) { return caches.delete( key ); } )
);
} )
);
self.clients.claim();
} );
// -----------------------------------------------------------------------
// Fetch: Anfragen abfangen
// -----------------------------------------------------------------------
self.addEventListener( 'fetch', function ( event ) {
var request = event.request;
// Nur GET-Anfragen behandeln
if ( request.method !== 'GET' ) {
return;
}
// Nur Anfragen gleichen Ursprungs behandeln
if ( ! request.url.startsWith( self.location.origin ) ) {
return;
}
var url = new URL( request.url );
// Admin-Bereich und WP-Cron ausschließen
if (
url.pathname.startsWith( '/wp-admin' ) ||
url.pathname.startsWith( '/wp-cron' ) ||
url.pathname.includes( 'wp-login' )
) {
return;
}
// HTML-Navigation: Network-first
if ( request.mode === 'navigate' ) {
event.respondWith(
fetch( request )
.then( function ( response ) {
// Erfolgreiche Antwort im Cache speichern
if ( response && response.status === 200 ) {
var clone = response.clone();
caches.open( CACHE_NAME ).then( function ( cache ) {
cache.put( request, clone );
} );
}
return response;
} )
.catch( function () {
// Offline-Fallback: gecachte Startseite
return caches.match( request ).then( function ( cached ) {
return cached || caches.match( OFFLINE_PAGE );
} );
} )
);
return;
}
// Statische Assets: Cache-first
if ( /\.(css|js|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot|ico)(\?.*)?$/.test( url.pathname ) ) {
event.respondWith(
caches.match( request ).then( function ( cached ) {
if ( cached ) {
return cached;
}
return fetch( request ).then( function ( response ) {
if ( response && response.status === 200 ) {
var clone = response.clone();
caches.open( CACHE_NAME ).then( function ( cache ) {
cache.put( request, clone );
} );
}
return response;
} );
} )
);
return;
}
} );