Eigenes CSS statt Framework, Vanilla-JavaScript, heller und dunkler Modus. Ohne JavaScript bleibt alles bedienbar; das Liniendiagramm faellt auf eine selbst gezeichnete Balkendarstellung zurueck, wenn das CDN nicht erreichbar ist. Chart.js ist auf eine Version festgelegt und per Subresource Integrity an seinen Hash gebunden. Die Domain-Seite enthaelt eine Schritt-fuer-Schritt-Anleitung fuer DNS, Hosting-Panel und Zertifikat samt Fehlersuche.
212 lines
11 KiB
PHP
212 lines
11 KiB
PHP
<?php
|
||
|
||
/**
|
||
* @var array<string,mixed> $link
|
||
* @var string $shortUrl
|
||
* @var App\Qr\QrOptions $options
|
||
* @var list<int> $sizes
|
||
* @var list<string> $levels
|
||
* @var string|null $warning
|
||
* @var bool $logoIsSvg
|
||
* @var bool $imagickAvailable
|
||
* @var App\Security\Csrf $csrf
|
||
*/
|
||
|
||
$id = (int) $link['id'];
|
||
|
||
$levelLabels = [
|
||
'L' => 'L – niedrig (7 %)',
|
||
'M' => 'M – mittel (15 %)',
|
||
'Q' => 'Q – hoch (25 %)',
|
||
'H' => 'H – sehr hoch (30 %)',
|
||
];
|
||
?>
|
||
<div class="page-head">
|
||
<div>
|
||
<h1>QR-Code</h1>
|
||
<p class="muted">
|
||
<button type="button" class="linkcopy mono" data-copy="<?= $e($shortUrl) ?>"
|
||
title="In die Zwischenablage kopieren"><?= $e($shortUrl) ?></button>
|
||
</p>
|
||
</div>
|
||
<div class="page-head__actions">
|
||
<a class="btn btn--ghost" href="<?= $e($url('/admin/links/' . $id . '/stats')) ?>">Statistik</a>
|
||
<a class="btn btn--ghost" href="<?= $e($url('/admin/links')) ?>">Zur Liste</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="qr-layout"
|
||
data-qr
|
||
data-preview-url="<?= $e($url('/admin/links/' . $id . '/qr/preview')) ?>"
|
||
data-download-url="<?= $e($url('/admin/links/' . $id . '/qr/download')) ?>">
|
||
|
||
<section class="card qr-preview">
|
||
<div class="qr-preview__stage" style="--qr-bg: <?= $e($options->background) ?>">
|
||
<img id="qr-image" alt="Vorschau des QR-Codes"
|
||
src="<?= $e($url('/admin/links/' . $id . '/qr/preview?' . http_build_query($options->toQueryParams()))) ?>">
|
||
<div class="qr-preview__spinner" data-qr-spinner hidden aria-hidden="true"></div>
|
||
</div>
|
||
|
||
<p class="muted small center">Der QR-Code verweist auf den Kurzlink – die Ziel-URL kannst du also
|
||
später ändern, ohne den Code neu drucken zu müssen.</p>
|
||
|
||
<div class="qr-preview__warning" data-qr-warning <?= $warning === null ? 'hidden' : '' ?>>
|
||
<div class="alert alert--warn" data-qr-warning-text><?= $e($warning ?? '') ?></div>
|
||
</div>
|
||
|
||
<div class="qr-download">
|
||
<a class="btn btn--primary" data-qr-download="png" href="#" download>PNG herunterladen</a>
|
||
<a class="btn" data-qr-download="svg" href="#" download>SVG herunterladen</a>
|
||
</div>
|
||
<p class="muted small center">SVG ist vektorbasiert und damit für den Druck in jeder Größe geeignet.</p>
|
||
</section>
|
||
|
||
<div class="qr-controls">
|
||
<form method="post" action="<?= $e($url('/admin/links/' . $id . '/qr')) ?>" class="card form" id="qr-form">
|
||
<?= $csrf->field() ?>
|
||
<h2>Gestaltung</h2>
|
||
|
||
<div class="grid grid--2 tight">
|
||
<div class="field">
|
||
<label for="foreground">Vordergrund</label>
|
||
<div class="colorpair">
|
||
<input type="color" id="foreground" name="foreground" value="<?= $e($options->foreground) ?>"
|
||
data-qr-input data-color-sync="foreground_text">
|
||
<input type="text" id="foreground_text" value="<?= $e($options->foreground) ?>"
|
||
class="mono" maxlength="7" spellcheck="false" aria-label="Vordergrundfarbe als Hex-Wert"
|
||
data-color-sync="foreground">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label for="background">Hintergrund</label>
|
||
<div class="colorpair">
|
||
<input type="color" id="background" name="background" value="<?= $e($options->background) ?>"
|
||
data-qr-input data-color-sync="background_text">
|
||
<input type="text" id="background_text" value="<?= $e($options->background) ?>"
|
||
class="mono" maxlength="7" spellcheck="false" aria-label="Hintergrundfarbe als Hex-Wert"
|
||
data-color-sync="background">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<span class="label">Voreinstellungen</span>
|
||
<div class="swatches">
|
||
<button type="button" class="swatch" data-preset="#000000,#ffffff"
|
||
style="--fg:#000000;--bg:#ffffff" title="Schwarz auf Weiß"></button>
|
||
<button type="button" class="swatch" data-preset="#1f2937,#f9fafb"
|
||
style="--fg:#1f2937;--bg:#f9fafb" title="Anthrazit"></button>
|
||
<button type="button" class="swatch" data-preset="#0b4f9e,#ffffff"
|
||
style="--fg:#0b4f9e;--bg:#ffffff" title="Blau"></button>
|
||
<button type="button" class="swatch" data-preset="#14532d,#ffffff"
|
||
style="--fg:#14532d;--bg:#ffffff" title="Grün"></button>
|
||
<button type="button" class="swatch" data-preset="#7c2d12,#fff7ed"
|
||
style="--fg:#7c2d12;--bg:#fff7ed" title="Warmbraun"></button>
|
||
<button type="button" class="swatch" data-preset="#4c1d95,#f5f3ff"
|
||
style="--fg:#4c1d95;--bg:#f5f3ff" title="Violett"></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid--2 tight">
|
||
<div class="field">
|
||
<label for="size">Größe</label>
|
||
<select id="size" name="size" data-qr-input>
|
||
<?php foreach ($sizes as $size): ?>
|
||
<option value="<?= (int) $size ?>" <?= $options->size === $size ? 'selected' : '' ?>>
|
||
<?= (int) $size ?> × <?= (int) $size ?> px
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<p class="field__hint">Gilt für den PNG-Download; SVG skaliert verlustfrei.</p>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label for="error_correction">Fehlerkorrektur</label>
|
||
<select id="error_correction" name="error_correction" data-qr-input
|
||
<?= $options->hasLogo() ? 'disabled' : '' ?>>
|
||
<?php foreach ($levels as $level): ?>
|
||
<option value="<?= $e($level) ?>" <?= $options->errorCorrection === $level ? 'selected' : '' ?>>
|
||
<?= $e($levelLabels[$level] ?? $level) ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<?php if ($options->hasLogo()): ?>
|
||
<input type="hidden" name="error_correction" value="H">
|
||
<p class="field__hint">Wegen des Logos fest auf <strong>H</strong> gesetzt.</p>
|
||
<?php else: ?>
|
||
<p class="field__hint">Höher = robuster gegen Beschädigung, aber dichteres Muster.</p>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label for="margin">Randbreite (Quiet Zone): <output id="margin_out"><?= (int) $options->margin ?></output> Module</label>
|
||
<input type="range" id="margin" name="margin" min="0" max="10" step="1"
|
||
value="<?= (int) $options->margin ?>" data-qr-input data-output="margin_out">
|
||
<p class="field__hint">Die Spezifikation empfiehlt mindestens 4 Module ruhige Fläche rundherum.</p>
|
||
</div>
|
||
|
||
<?php if ($options->hasLogo()): ?>
|
||
<div class="field">
|
||
<label for="logo_scale">Logogröße: <output id="logo_scale_out"><?= (int) $options->logoScale ?></output> % der Breite</label>
|
||
<input type="range" id="logo_scale" name="logo_scale" min="10" max="30" step="1"
|
||
value="<?= (int) $options->logoScale ?>" data-qr-input data-output="logo_scale_out">
|
||
</div>
|
||
|
||
<div class="field field--check">
|
||
<label class="check">
|
||
<input type="checkbox" name="logo_punchout" value="1" data-qr-input
|
||
<?= $options->logoPunchout ? 'checked' : '' ?>>
|
||
<span>Fläche hinter dem Logo freistellen</span>
|
||
</label>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="form__actions">
|
||
<button type="submit" class="btn btn--primary">Einstellungen speichern</button>
|
||
<button type="reset" class="btn btn--ghost" data-qr-reset>Zurücksetzen</button>
|
||
</div>
|
||
<p class="field__hint">Speichern ist optional – Vorschau und Download nutzen immer die aktuellen Werte.</p>
|
||
</form>
|
||
|
||
<section class="card">
|
||
<h2>Logo in der Mitte</h2>
|
||
|
||
<?php if ($options->hasLogo()): ?>
|
||
<div class="logo-current">
|
||
<img src="<?= $e($url('/admin/uploads/' . rawurlencode((string) $options->logoPath))) ?>"
|
||
alt="Aktuelles Logo" class="logo-current__img">
|
||
<div class="logo-current__meta">
|
||
<span class="mono small"><?= $e($options->logoPath) ?></span>
|
||
<form method="post" action="<?= $e($url('/admin/links/' . $id . '/qr/logo/delete')) ?>"
|
||
data-confirm="Logo wirklich entfernen?">
|
||
<?= $csrf->field() ?>
|
||
<button type="submit" class="btn btn--sm btn--danger">Logo entfernen</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if ($logoIsSvg && !$imagickAvailable): ?>
|
||
<div class="alert alert--warn">
|
||
Dieses SVG-Logo kann nur im <strong>SVG-Download</strong> eingebettet werden. Für PNG-Ausgaben
|
||
wird die PHP-Erweiterung <code>imagick</code> benötigt – alternativ ein PNG-Logo hochladen.
|
||
</div>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
|
||
<form method="post" action="<?= $e($url('/admin/links/' . $id . '/qr/logo')) ?>"
|
||
enctype="multipart/form-data" class="form">
|
||
<?= $csrf->field() ?>
|
||
<div class="field">
|
||
<label for="logo"><?= $options->hasLogo() ? 'Anderes Logo hochladen' : 'Logo hochladen' ?></label>
|
||
<input type="file" id="logo" name="logo" accept="image/png,image/jpeg,image/svg+xml" required>
|
||
<p class="field__hint">PNG, JPG oder SVG. Das Logo wird mittig eingesetzt und passend skaliert;
|
||
die Fehlerkorrektur wird dabei automatisch auf Level H gesetzt.</p>
|
||
</div>
|
||
<button type="submit" class="btn">Hochladen</button>
|
||
</form>
|
||
</section>
|
||
</div>
|
||
</div>
|