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;
}
} );

519
kgv-pwa.php Normal file
View 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>
&nbsp;|&nbsp;
<?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
View 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