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;
|
||||
}
|
||||
} );
|
||||
519
kgv-pwa.php
Normal file
519
kgv-pwa.php
Normal file
@@ -0,0 +1,519 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: KGV PWA
|
||||
* Plugin URI: https://apex-project.de/
|
||||
* Description: Progressive Web App Unterstützung fuer KGV-Webseiten – Web App Manifest, Service Worker und Install-Banner.
|
||||
* Version: 1.0.0
|
||||
* Author: Ronny Grobel
|
||||
* Author URI: https://apex-project.de/
|
||||
* Text Domain: kgv-pwa
|
||||
* Update URI: https://git.apex-project.de/Wordpress_Plugins/KGV-PWA
|
||||
* Gitea Plugin URI: https://git.apex-project.de/Wordpress_Plugins/KGV-PWA
|
||||
* Requires Plugins: KGV-Updater
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class KGV_PWA_Plugin {
|
||||
|
||||
const VERSION = '1.0.0';
|
||||
const OPTION_KEY = 'kgv_pwa_settings';
|
||||
const REWRITE_VERSION_OPTION = 'kgv_pwa_rewrite_version';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function instance() {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
add_action( 'init', array( $this, 'add_rewrite_rules' ) );
|
||||
add_action( 'init', array( $this, 'maybe_flush_rewrite_rules' ), 20 );
|
||||
add_filter( 'query_vars', array( $this, 'add_query_vars' ) );
|
||||
add_action( 'template_redirect', array( $this, 'handle_manifest' ) );
|
||||
add_action( 'template_redirect', array( $this, 'handle_service_worker' ) );
|
||||
add_action( 'wp_head', array( $this, 'output_head_tags' ) );
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
|
||||
add_action( 'admin_menu', array( $this, 'add_settings_page' ) );
|
||||
add_action( 'admin_init', array( $this, 'register_settings' ) );
|
||||
add_shortcode( 'kgv_pwa_install_button', array( $this, 'render_install_button' ) );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Activation / Deactivation
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public static function activate() {
|
||||
self::instance()->add_rewrite_rules();
|
||||
flush_rewrite_rules();
|
||||
update_option( self::REWRITE_VERSION_OPTION, self::VERSION );
|
||||
|
||||
if ( false === get_option( self::OPTION_KEY ) ) {
|
||||
add_option( self::OPTION_KEY, self::default_settings() );
|
||||
}
|
||||
}
|
||||
|
||||
public static function deactivate() {
|
||||
flush_rewrite_rules();
|
||||
delete_option( self::REWRITE_VERSION_OPTION );
|
||||
}
|
||||
|
||||
public function maybe_flush_rewrite_rules() {
|
||||
$installed_version = get_option( self::REWRITE_VERSION_OPTION, '' );
|
||||
|
||||
if ( self::VERSION === $installed_version ) {
|
||||
return;
|
||||
}
|
||||
|
||||
flush_rewrite_rules( false );
|
||||
update_option( self::REWRITE_VERSION_OPTION, self::VERSION );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Rewrite Rules & Query Vars
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function add_rewrite_rules() {
|
||||
add_rewrite_rule( '^manifest\.json$', 'index.php?kgv_pwa=manifest', 'top' );
|
||||
add_rewrite_rule( '^sw\.js$', 'index.php?kgv_pwa=sw', 'top' );
|
||||
}
|
||||
|
||||
public function add_query_vars( $vars ) {
|
||||
$vars[] = 'kgv_pwa';
|
||||
|
||||
return $vars;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Manifest Endpoint
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function handle_manifest() {
|
||||
if ( 'manifest' !== get_query_var( 'kgv_pwa' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = $this->get_settings();
|
||||
|
||||
$manifest = array(
|
||||
'name' => $settings['app_name'],
|
||||
'short_name' => $settings['short_name'],
|
||||
'description' => $settings['description'],
|
||||
'start_url' => home_url( '/' ),
|
||||
'scope' => home_url( '/' ),
|
||||
'display' => $settings['display'],
|
||||
'background_color' => $settings['background_color'],
|
||||
'theme_color' => $settings['theme_color'],
|
||||
'lang' => str_replace( '_', '-', get_locale() ),
|
||||
'icons' => $this->get_manifest_icons( $settings ),
|
||||
);
|
||||
|
||||
header( 'Content-Type: application/manifest+json; charset=utf-8' );
|
||||
header( 'Cache-Control: max-age=3600' );
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo wp_json_encode( $manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
|
||||
exit;
|
||||
}
|
||||
|
||||
private function get_manifest_icons( $settings ) {
|
||||
$icons = array();
|
||||
|
||||
foreach ( array( '192', '512' ) as $size ) {
|
||||
$key = 'icon_' . $size;
|
||||
if ( ! empty( $settings[ $key ] ) ) {
|
||||
$icons[] = array(
|
||||
'src' => esc_url_raw( $settings[ $key ] ),
|
||||
'sizes' => $size . 'x' . $size,
|
||||
'type' => 'image/png',
|
||||
'purpose' => 'any maskable',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: WordPress-Site-Icon
|
||||
if ( empty( $icons ) ) {
|
||||
$site_icon_512 = get_site_icon_url( 512 );
|
||||
$site_icon_192 = get_site_icon_url( 192 );
|
||||
|
||||
if ( $site_icon_512 ) {
|
||||
$icons[] = array(
|
||||
'src' => $site_icon_512,
|
||||
'sizes' => '512x512',
|
||||
'type' => 'image/png',
|
||||
'purpose' => 'any',
|
||||
);
|
||||
}
|
||||
|
||||
if ( $site_icon_192 ) {
|
||||
$icons[] = array(
|
||||
'src' => $site_icon_192,
|
||||
'sizes' => '192x192',
|
||||
'type' => 'image/png',
|
||||
'purpose' => 'any',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $icons;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Service Worker Endpoint
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function handle_service_worker() {
|
||||
if ( 'sw' !== get_query_var( 'kgv_pwa' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sw_file = plugin_dir_path( __FILE__ ) . 'assets/js/service-worker.js';
|
||||
|
||||
if ( ! file_exists( $sw_file ) ) {
|
||||
status_header( 404 );
|
||||
exit;
|
||||
}
|
||||
|
||||
header( 'Content-Type: application/javascript; charset=utf-8' );
|
||||
header( 'Service-Worker-Allowed: /' );
|
||||
header( 'Cache-Control: no-cache, no-store, must-revalidate' );
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
readfile( $sw_file );
|
||||
exit;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Frontend: Head Tags & Script
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function output_head_tags() {
|
||||
$settings = $this->get_settings();
|
||||
$app_name = esc_attr( $settings['app_name'] );
|
||||
$theme_hex = esc_attr( $settings['theme_color'] );
|
||||
$icon_url = ! empty( $settings['icon_192'] ) ? esc_url( $settings['icon_192'] ) : '';
|
||||
|
||||
echo '<link rel="manifest" href="' . esc_url( $this->get_manifest_url() ) . '">' . "\n";
|
||||
echo '<meta name="theme-color" content="' . $theme_hex . '">' . "\n";
|
||||
echo '<meta name="application-name" content="' . $app_name . '">' . "\n";
|
||||
echo '<meta name="apple-mobile-web-app-capable" content="yes">' . "\n";
|
||||
echo '<meta name="apple-mobile-web-app-status-bar-style" content="default">' . "\n";
|
||||
echo '<meta name="apple-mobile-web-app-title" content="' . $app_name . '">' . "\n";
|
||||
|
||||
if ( $icon_url ) {
|
||||
echo '<link rel="apple-touch-icon" href="' . $icon_url . '">' . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
private function get_manifest_url() {
|
||||
// Query-URL funktioniert auch dann, wenn Pretty-Rewrite noch nicht aktiv ist.
|
||||
return home_url( '/?kgv_pwa=manifest' );
|
||||
}
|
||||
|
||||
private function get_service_worker_url() {
|
||||
// Query-URL funktioniert auch dann, wenn Pretty-Rewrite noch nicht aktiv ist.
|
||||
return home_url( '/?kgv_pwa=sw' );
|
||||
}
|
||||
|
||||
public function enqueue_assets() {
|
||||
$sw_url = $this->get_service_worker_url();
|
||||
$scope = trailingslashit( home_url( '/' ) );
|
||||
|
||||
wp_enqueue_style(
|
||||
'kgv-pwa',
|
||||
plugin_dir_url( __FILE__ ) . 'assets/css/kgv-pwa.css',
|
||||
array(),
|
||||
self::VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'kgv-pwa-register',
|
||||
plugin_dir_url( __FILE__ ) . 'assets/js/pwa-register.js',
|
||||
array(),
|
||||
self::VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script(
|
||||
'kgv-pwa-register',
|
||||
'kgvPwaConfig',
|
||||
array(
|
||||
'swUrl' => esc_url_raw( $sw_url ),
|
||||
'scope' => esc_url_raw( $scope ),
|
||||
'iosInstallNotice' => __( 'Auf iPhone/iPad: Im Browser auf Teilen tippen und dann "Zum Home-Bildschirm" waehlen.', 'kgv-pwa' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcode [kgv_pwa_install_button] – rendert einen Installations-Button.
|
||||
* Kann in Seiten, Posts und Widgets verwendet werden.
|
||||
*
|
||||
* Attribute:
|
||||
* label – Beschriftung des Buttons (Standard: "App installieren")
|
||||
* class – Zusätzliche CSS-Klassen
|
||||
*/
|
||||
public function render_install_button( $atts ) {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'label' => __( 'App installieren', 'kgv-pwa' ),
|
||||
'class' => '',
|
||||
),
|
||||
$atts,
|
||||
'kgv_pwa_install_button'
|
||||
);
|
||||
|
||||
$classes = 'kgv-pwa-install-btn';
|
||||
if ( ! empty( $atts['class'] ) ) {
|
||||
$classes .= ' ' . sanitize_html_class( $atts['class'] );
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<button type="button" class="%s" onclick="document.dispatchEvent(new CustomEvent(\'kgv-pwa-install\'))">%s</button>',
|
||||
esc_attr( $classes ),
|
||||
esc_html( $atts['label'] )
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Admin Settings
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function add_settings_page() {
|
||||
add_options_page(
|
||||
__( 'KGV PWA', 'kgv-pwa' ),
|
||||
__( 'KGV PWA', 'kgv-pwa' ),
|
||||
'manage_options',
|
||||
'kgv-pwa',
|
||||
array( $this, 'render_settings_page' )
|
||||
);
|
||||
}
|
||||
|
||||
public function register_settings() {
|
||||
register_setting(
|
||||
'kgv_pwa_settings_group',
|
||||
self::OPTION_KEY,
|
||||
array( $this, 'sanitize_settings' )
|
||||
);
|
||||
}
|
||||
|
||||
public function sanitize_settings( $input ) {
|
||||
$defaults = self::default_settings();
|
||||
$output = array();
|
||||
|
||||
$output['app_name'] = isset( $input['app_name'] )
|
||||
? sanitize_text_field( $input['app_name'] )
|
||||
: $defaults['app_name'];
|
||||
|
||||
$output['short_name'] = isset( $input['short_name'] )
|
||||
? sanitize_text_field( $input['short_name'] )
|
||||
: $defaults['short_name'];
|
||||
|
||||
$output['description'] = isset( $input['description'] )
|
||||
? sanitize_textarea_field( $input['description'] )
|
||||
: '';
|
||||
|
||||
$output['theme_color'] = isset( $input['theme_color'] )
|
||||
&& preg_match( '/^#[0-9a-fA-F]{6}$/', $input['theme_color'] )
|
||||
? $input['theme_color']
|
||||
: $defaults['theme_color'];
|
||||
|
||||
$output['background_color'] = isset( $input['background_color'] )
|
||||
&& preg_match( '/^#[0-9a-fA-F]{6}$/', $input['background_color'] )
|
||||
? $input['background_color']
|
||||
: $defaults['background_color'];
|
||||
|
||||
$valid_display = array( 'standalone', 'fullscreen', 'minimal-ui', 'browser' );
|
||||
$output['display'] = isset( $input['display'] )
|
||||
&& in_array( $input['display'], $valid_display, true )
|
||||
? $input['display']
|
||||
: $defaults['display'];
|
||||
|
||||
$output['icon_192'] = isset( $input['icon_192'] ) ? esc_url_raw( $input['icon_192'] ) : '';
|
||||
$output['icon_512'] = isset( $input['icon_512'] ) ? esc_url_raw( $input['icon_512'] ) : '';
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
private static function default_settings() {
|
||||
return array(
|
||||
'app_name' => get_bloginfo( 'name' ),
|
||||
'short_name' => get_bloginfo( 'name' ),
|
||||
'description' => get_bloginfo( 'description' ),
|
||||
'theme_color' => '#2e7d32',
|
||||
'background_color' => '#ffffff',
|
||||
'display' => 'standalone',
|
||||
'icon_192' => '',
|
||||
'icon_512' => '',
|
||||
);
|
||||
}
|
||||
|
||||
public function get_settings() {
|
||||
$settings = get_option( self::OPTION_KEY, array() );
|
||||
|
||||
if ( ! is_array( $settings ) ) {
|
||||
$settings = array();
|
||||
}
|
||||
|
||||
return wp_parse_args( $settings, self::default_settings() );
|
||||
}
|
||||
|
||||
public function render_settings_page() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = $this->get_settings();
|
||||
$option_prefix = esc_attr( self::OPTION_KEY );
|
||||
$display_modes = array(
|
||||
'standalone' => 'Standalone',
|
||||
'fullscreen' => 'Fullscreen',
|
||||
'minimal-ui' => 'Minimal UI',
|
||||
'browser' => 'Browser',
|
||||
);
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e( 'KGV PWA Einstellungen', 'kgv-pwa' ); ?></h1>
|
||||
|
||||
<p>
|
||||
<?php esc_html_e( 'Manifest:', 'kgv-pwa' ); ?>
|
||||
<a href="<?php echo esc_url( $this->get_manifest_url() ); ?>" target="_blank">
|
||||
<?php echo esc_url( $this->get_manifest_url() ); ?>
|
||||
</a>
|
||||
|
|
||||
<?php esc_html_e( 'Service Worker:', 'kgv-pwa' ); ?>
|
||||
<a href="<?php echo esc_url( $this->get_service_worker_url() ); ?>" target="_blank">
|
||||
<?php echo esc_url( $this->get_service_worker_url() ); ?>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<form method="post" action="options.php">
|
||||
<?php settings_fields( 'kgv_pwa_settings_group' ); ?>
|
||||
<table class="form-table" role="presentation">
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="kgv_pwa_app_name"><?php esc_html_e( 'App-Name', 'kgv-pwa' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text"
|
||||
id="kgv_pwa_app_name"
|
||||
name="<?php echo $option_prefix; ?>[app_name]"
|
||||
value="<?php echo esc_attr( $settings['app_name'] ); ?>"
|
||||
class="regular-text">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="kgv_pwa_short_name"><?php esc_html_e( 'Kurzname', 'kgv-pwa' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text"
|
||||
id="kgv_pwa_short_name"
|
||||
name="<?php echo $option_prefix; ?>[short_name]"
|
||||
value="<?php echo esc_attr( $settings['short_name'] ); ?>"
|
||||
class="regular-text">
|
||||
<p class="description"><?php esc_html_e( 'Maximal 12 Zeichen empfohlen.', 'kgv-pwa' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="kgv_pwa_description"><?php esc_html_e( 'Beschreibung', 'kgv-pwa' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<textarea id="kgv_pwa_description"
|
||||
name="<?php echo $option_prefix; ?>[description]"
|
||||
rows="3"
|
||||
class="large-text"><?php echo esc_textarea( $settings['description'] ); ?></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="kgv_pwa_theme_color"><?php esc_html_e( 'Theme-Farbe', 'kgv-pwa' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="color"
|
||||
id="kgv_pwa_theme_color"
|
||||
name="<?php echo $option_prefix; ?>[theme_color]"
|
||||
value="<?php echo esc_attr( $settings['theme_color'] ); ?>">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="kgv_pwa_background_color"><?php esc_html_e( 'Hintergrundfarbe', 'kgv-pwa' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="color"
|
||||
id="kgv_pwa_background_color"
|
||||
name="<?php echo $option_prefix; ?>[background_color]"
|
||||
value="<?php echo esc_attr( $settings['background_color'] ); ?>">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="kgv_pwa_display"><?php esc_html_e( 'Anzeigemodus', 'kgv-pwa' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select id="kgv_pwa_display" name="<?php echo $option_prefix; ?>[display]">
|
||||
<?php foreach ( $display_modes as $val => $label ) : ?>
|
||||
<option value="<?php echo esc_attr( $val ); ?>"<?php selected( $settings['display'], $val ); ?>>
|
||||
<?php echo esc_html( $label ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="description"><?php esc_html_e( '"Standalone" empfohlen – App öffnet ohne Browser-UI.', 'kgv-pwa' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="kgv_pwa_icon_192"><?php esc_html_e( 'Icon 192 × 192 (URL)', 'kgv-pwa' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url"
|
||||
id="kgv_pwa_icon_192"
|
||||
name="<?php echo $option_prefix; ?>[icon_192]"
|
||||
value="<?php echo esc_url( $settings['icon_192'] ); ?>"
|
||||
class="large-text"
|
||||
placeholder="https://…/icon-192.png">
|
||||
<p class="description"><?php esc_html_e( 'PNG, 192 × 192 px. Leer lassen, um das WordPress-Site-Icon zu verwenden.', 'kgv-pwa' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="kgv_pwa_icon_512"><?php esc_html_e( 'Icon 512 × 512 (URL)', 'kgv-pwa' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url"
|
||||
id="kgv_pwa_icon_512"
|
||||
name="<?php echo $option_prefix; ?>[icon_512]"
|
||||
value="<?php echo esc_url( $settings['icon_512'] ); ?>"
|
||||
class="large-text"
|
||||
placeholder="https://…/icon-512.png">
|
||||
<p class="description"><?php esc_html_e( 'PNG, 512 × 512 px.', 'kgv-pwa' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<?php submit_button( __( 'Einstellungen speichern', 'kgv-pwa' ) ); ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
register_activation_hook( __FILE__, array( 'KGV_PWA_Plugin', 'activate' ) );
|
||||
register_deactivation_hook( __FILE__, array( 'KGV_PWA_Plugin', 'deactivate' ) );
|
||||
|
||||
add_action( 'plugins_loaded', array( 'KGV_PWA_Plugin', 'instance' ) );
|
||||
47
readme.txt
Normal file
47
readme.txt
Normal file
@@ -0,0 +1,47 @@
|
||||
=== KGV PWA ===
|
||||
Contributors: ronnygrobel
|
||||
Tags: pwa, progressive web app, manifest, service worker, offline
|
||||
Requires at least: 6.0
|
||||
Tested up to: 6.7
|
||||
Stable tag: 1.0.0
|
||||
Requires PHP: 7.4
|
||||
License: GPLv2 or later
|
||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
Progressive Web App Unterstützung für KGV-Webseiten.
|
||||
|
||||
== Beschreibung ==
|
||||
|
||||
KGV PWA erweitert KGV-Webseiten um vollständige Progressive Web App (PWA) Unterstützung:
|
||||
|
||||
* **Web App Manifest** – dynamisch generiert unter `/manifest.json`
|
||||
* **Service Worker** – Cache-Strategie für Offline-Fähigkeit, erreichbar unter `/sw.js`
|
||||
* **Installierbarkeit** – Besucher können die Seite als App auf dem Home-Bildschirm speichern
|
||||
* **Apple-Touch-Icon & Meta-Tags** – korrekte Darstellung auf iOS-Geräten
|
||||
|
||||
Alle relevanten Parameter (App-Name, Kurzname, Farben, Icons, Anzeigemodus) sind über
|
||||
**Einstellungen → KGV PWA** konfigurierbar.
|
||||
|
||||
== Installation ==
|
||||
|
||||
1. Plugin hochladen und aktivieren.
|
||||
2. Einstellungen unter **Einstellungen → KGV PWA** vornehmen.
|
||||
3. Nach der ersten Aktivierung werden die Rewrite-Regeln automatisch geflusht.
|
||||
|
||||
== Einstellungen ==
|
||||
|
||||
* **App-Name** – Vollständiger Name der App im Manifest
|
||||
* **Kurzname** – Kurzname (max. 12 Zeichen empfohlen) für den Home-Bildschirm
|
||||
* **Beschreibung** – Kurzbeschreibung im Manifest
|
||||
* **Theme-Farbe** – Farbe der Browser-/Status-Leiste
|
||||
* **Hintergrundfarbe** – Splashscreen-Hintergrund
|
||||
* **Anzeigemodus** – `standalone` (empfohlen), `fullscreen`, `minimal-ui`, `browser`
|
||||
* **Icon 192 × 192** – URL zu einem PNG-Icon (192 px)
|
||||
* **Icon 512 × 512** – URL zu einem PNG-Icon (512 px)
|
||||
|
||||
Werden keine Icons angegeben, wird automatisch das WordPress-Site-Icon verwendet.
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 1.0.0 =
|
||||
* Erstveröffentlichung
|
||||
Reference in New Issue
Block a user