Initial commit: KGV-PWA plugin with manifest, service worker and install prompt
This commit is contained in:
84
assets/css/kgv-pwa.css
Normal file
84
assets/css/kgv-pwa.css
Normal 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
124
assets/js/pwa-register.js
Normal 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">×</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
117
assets/js/service-worker.js
Normal 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;
|
||||
}
|
||||
} );
|
||||
Reference in New Issue
Block a user