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

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