Display-Seiten individuell gestalten
Some checks are pending
CI / PHP Lint (push) Waiting to run
CI / PHP Lint (pull_request) Waiting to run
CI / PHP Lint-1 (pull_request) Waiting to run
CI / PHP Lint-1 (push) Waiting to run
CI / Unit Tests & Static Analysis (pull_request) Waiting to run
CI / Unit Tests & Static Analysis (push) Waiting to run

Jede Display-Seite bringt jetzt ihr eigenes Erscheinungsbild mit – der
Empfang sieht anders aus als der Tagungsraum nebenan:

- eigenes Logo (leer = Logo aus den Einstellungen)
- formatfüllendes Hintergrundbild mit einstellbarer Abdunklung (0–90 %),
  damit die Karte auf hellen Fotos lesbar bleibt
- eigene Akzentfarbe für den Knopf (leer = Farbe aus dem Design-Tab)
- Karte wahlweise hell oder dunkel; auf Fotos wirkt dunkel meist ruhiger

Logo und Hintergrund lassen sich hochladen oder als URL hinterlegen; beim
Löschen einer Display-Seite verschwinden die hochgeladenen Dateien mit.

Nebenbei aufgeräumt: das Bildfeld (Vorschau + Upload + URL + Entfernen)
lag als Funktion in admin/settings.php und wird jetzt von beiden Seiten
genutzt – Ui::imageField() für die Darstellung, Upload::resolveField()
für die Auswertung.

Sicherheit: die Akzentfarbe landet in einem style-Attribut, deshalb wird
sie sowohl beim Speichern als auch beim Ausgeben auf eine echte Hex-Farbe
geprüft; ein Test hält das fest.

Migration 0006, database.sql nachgezogen, 4 neue Tests (42 gesamt),
Screenshots ergänzt, Version 2.8.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Friederich Loheide 2026-09-23 16:58:54 +00:00
parent 943e427150
commit 4159b92268
31 changed files with 405 additions and 82 deletions

View file

@ -124,6 +124,35 @@ class Kiosk
];
}
/**
* Gestaltung einer Display-Seite: eigene Werte, sonst die des Systems.
*
* @return array{logo:string,background:string,overlay:float,accent:string,card:string}
*/
public static function appearance($db, array $kiosk): array
{
$accent = strtolower(trim((string)($kiosk['accent_color'] ?? '')));
if (!preg_match('/^#[0-9a-f]{6}$/', $accent)) {
$accent = '';
}
$overlay = (int)($kiosk['bg_overlay'] ?? 45);
$overlay = max(0, min(90, $overlay));
$logo = trim((string)($kiosk['logo_url'] ?? ''));
if ($logo === '' && $db) {
$logo = (string)$db->getSetting('logo_url', '');
}
return [
'logo' => $logo,
'background' => trim((string)($kiosk['background_url'] ?? '')),
'overlay' => (float)$overlay / 100, // immer float, auch bei 0
'accent' => $accent,
'card' => ($kiosk['card_style'] ?? 'light') === 'dark' ? 'dark' : 'light',
];
}
/** Öffentliche Adresse eines Kiosks. */
public static function publicUrl(string $token, string $baseUrl = ''): string
{

View file

@ -116,6 +116,46 @@ class Ui
. '</style>';
}
/**
* Bildfeld mit Vorschau, Upload und URL-Eingabe.
* Wird von den Einstellungen und der Kiosk-Verwaltung genutzt.
*/
public static function imageField(
string $name,
string $label,
?string $value,
string $hint = '',
string $accept = 'image/*',
string $base = '../'
): string {
$value = (string)$value;
$preview = self::mediaUrl($value, $base);
$esc = static fn ($text) => htmlspecialchars((string)$text, ENT_QUOTES);
$t = static fn ($key, $fallback) => function_exists('__') ? __($key) : $fallback;
$thumb = $preview !== ''
? '<img src="' . $esc($preview) . '" alt="">'
: '<i class="fas fa-image" aria-hidden="true"></i>';
$remove = $value !== ''
? '<label class="chk"><input type="checkbox" name="' . $esc($name) . '_remove" value="1"> '
. $esc($t('settings_image_remove', 'Bild entfernen')) . '</label>'
: '';
return '<div class="form-group">'
. '<label>' . $esc($label) . '</label>'
. '<div class="image-field">'
. '<div class="image-preview">' . $thumb . '</div>'
. '<div class="image-field-controls">'
. '<input type="file" name="' . $esc($name) . '_file" accept="' . $esc($accept) . '">'
. '<input type="text" name="' . $esc($name) . '" value="' . $esc($value) . '" '
. 'placeholder="' . $esc($t('settings_image_url_placeholder', 'https://… oder Datei hochladen')) . '">'
. $remove
. '</div></div>'
. ($hint !== '' ? '<div class="help-text">' . $esc($hint) . '</div>' : '')
. '</div>';
}
/**
* Version aus der Datei VERSION im Projektstamm.
* Damit tragen Oberfläche und Release-Paket dieselbe Nummer.

View file

@ -120,6 +120,39 @@ class Upload
return 'uploads/' . $name;
}
/**
* Neuer Wert eines Bildfeldes aus dem Formular.
*
* Reihenfolge: hochgeladene Datei schlaegt alles, danach der
* Entfernen-Schalter, sonst gilt das URL-Feld. Wird eine zuvor
* hochgeladene Datei ersetzt oder entfernt, verschwindet sie auch
* von der Platte.
*
* @param string $name Feldname (erwartet <name>, <name>_file, <name>_remove)
* @param string $current bisher gespeicherter Wert
* @param string $kind 'image' oder 'favicon'
*/
public static function resolveField(string $name, string $current, string $kind = 'image'): string
{
$uploaded = self::store($_FILES[$name . '_file'] ?? [], $kind);
if ($uploaded !== '') {
self::delete($current);
return $uploaded;
}
if (!empty($_POST[$name . '_remove'])) {
self::delete($current);
return '';
}
$value = trim((string)($_POST[$name] ?? ''));
if ($value !== $current && self::isLocal($current) && !self::isLocal($value)) {
self::delete($current);
}
return $value;
}
/**
* Entfernt aktive Inhalte aus SVG-Dateien (Skripte, Event-Handler,
* externe Verweise). Lieber eine Grafik verlieren als eine XSS-Luecke.