Compare commits
10 Commits
5478d943e5
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e257a09f0 | |||
| b0eaaf7694 | |||
| 30872ad780 | |||
| 11d4586ea7 | |||
| 319ee44aae | |||
| 0ca6e891cc | |||
| 7790fe6dda | |||
| 8dac716c28 | |||
| ca3ed44ff3 | |||
| d5cd4427d8 |
@@ -0,0 +1 @@
|
||||
*.zip
|
||||
@@ -7,7 +7,7 @@ Dieses Projekt basiert auf dem Code von https://qr.alster.space/. Er wurde um ve
|
||||
## Features
|
||||
|
||||
- Vollständig clientseitig (keine Server-Kommunikation)
|
||||
- URL-Parameter für alle Einstellungen
|
||||
- URL-Parameter für alle Einstellungen (inkl. Modus Text/WLAN/Termin)
|
||||
- Anpassbare Größen und Farben
|
||||
- Verschiedene Fehlerkorrektur-Level
|
||||
- Download-Funktion
|
||||
@@ -69,9 +69,15 @@ Die App bietet spezielle Eingabefelder für WiFi-Daten:
|
||||
|
||||
Die Anwendung unterstützt folgende URL-Parameter:
|
||||
|
||||
- `text` - Text oder URL für den QR-Code
|
||||
- `ssid` - WiFi SSID
|
||||
- `password` - WiFi Passwort
|
||||
- `mode` - Inhaltstyp: `text`, `wifi` oder `event` (Termin/Kalender). Ohne `mode` wird bei vorhandenen Termin-Feldern automatisch der Termin-Modus gewählt.
|
||||
- `text` - Text oder URL für den QR-Code (Modus Text)
|
||||
- `ssid` - WiFi SSID (Modus WLAN)
|
||||
- `password` - WiFi Passwort (Modus WLAN)
|
||||
- `eventTitle` - Titel des Termins (Modus `event`)
|
||||
- `eventStart` - Beginn im Format `YYYY-MM-DDTHH:mm` (lokale Ortszeit Europe/Berlin, z. B. `2026-04-16T14:30`)
|
||||
- `eventEnd` - Ende (optional), gleiches Format wie `eventStart`
|
||||
- `eventLocation` - Ort (optional)
|
||||
- `eventDescription` - Beschreibung (optional)
|
||||
- `size` - Größe (128, 256, 512, 1024)
|
||||
- `errorCorrection` - Fehlerkorrektur (L, M, Q, H)
|
||||
- `foreground` - Vordergrundfarbe (Hex-Code)
|
||||
@@ -92,6 +98,11 @@ http://localhost:8080/?ssid=MeinWLAN&password=MeinPasswort123
|
||||
http://localhost:8080/?ssid=OffenesWLAN
|
||||
```
|
||||
|
||||
**Termin (Kalender, iCal im QR):**
|
||||
```
|
||||
http://localhost:8080/?mode=event&eventTitle=Team-Meeting&eventStart=2026-04-16T14:30&eventEnd=2026-04-16T15:30
|
||||
```
|
||||
|
||||
**Achtung:** Das WLAN-Passwort ist im Link im Klartext sichtbar!
|
||||
|
||||
## WiFi QR-Code Format
|
||||
@@ -133,7 +144,7 @@ Das Smartphone erkennt automatisch, dass es sich um WiFi-Daten handelt und biete
|
||||
|
||||
### Teilen-Funktion
|
||||
|
||||
Mit dem Button "Teilen" kann ein Link mit allen aktuellen Einstellungen (inkl. WiFi-Daten) in die Zwischenablage kopiert werden. Dieser Link kann weitergegeben werden und öffnet die App direkt mit den gewählten Einstellungen.
|
||||
Mit dem Button "Teilen" kann ein Link mit allen aktuellen Einstellungen (inkl. WiFi- oder Termin-Daten) in die Zwischenablage kopiert werden. Dieser Link kann weitergegeben werden und öffnet die App direkt mit den gewählten Einstellungen.
|
||||
|
||||
**Achtung:** Wenn ein WLAN-Passwort eingegeben ist, wird dieses im Link im Klartext übertragen!
|
||||
|
||||
|
||||
+6
-2
@@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
qr-generator:
|
||||
build: .
|
||||
@@ -7,3 +5,9 @@ services:
|
||||
- "8080:80"
|
||||
container_name: qr-generator
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
+202
-18
@@ -1,11 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<html lang="de"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://plausible.elpatron.me; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://plausible.elpatron.me; object-src 'none';">
|
||||
<title>QR ohne Schnickschnack</title>
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' fill='white' rx='6' ry='6'/><rect x='4' y='4' width='8' height='8' fill='black'/><rect x='20' y='4' width='8' height='8' fill='black'/><rect x='4' y='20' width='8' height='8' fill='black'/><rect x='14' y='14' width='4' height='4' fill='black'/></svg>">
|
||||
<script defer data-domain="qr.elpatron.me" src="https://plausible.elpatron.me/js/script.js"></script>
|
||||
<script src="./assets/qrious.min.js"></script>
|
||||
<script src="./main.js"></script>
|
||||
<!--
|
||||
@@ -53,7 +54,7 @@
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
input, select {
|
||||
input, select, textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
@@ -64,7 +65,12 @@
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
input:focus, select:focus {
|
||||
textarea {
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
input:focus, select:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: #1e3a8a;
|
||||
box-shadow: 0 0 0 3px rgba(30, 58, 138, 0.1);
|
||||
@@ -295,6 +301,73 @@
|
||||
text-decoration: underline;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.event-berlin-preview {
|
||||
font-size: 14px;
|
||||
color: #4b5563;
|
||||
margin: -8px 0 16px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.event-berlin-preview:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.event-datetime-hint {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
margin: -4px 0 12px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.time-24h-block {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.time-24h-block .time-24h-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.time-24h-block input[type="number"] {
|
||||
width: 4.25rem;
|
||||
margin-bottom: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.time-24h-sep {
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.time-24h-hint {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.vcard-preview-label {
|
||||
margin: 4px 0 6px 0;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.vcard-preview {
|
||||
margin: 0 0 15px 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
background: #f9fafb;
|
||||
color: #374151;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
min-height: 80px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -303,22 +376,130 @@
|
||||
<button id="info-button" class="info-button" aria-label="Information">?</button>
|
||||
|
||||
<div class="input-section">
|
||||
<label for="text">Text oder URL:</label>
|
||||
<div class="text-input-wrapper">
|
||||
<input type="text" id="text" placeholder="https://qr.medisoftware.org" value="" autofocus="">
|
||||
<button type="button" id="clear-text" class="clear-button" aria-label="Clear text" style="display: none;">✕</button>
|
||||
<label for="content-mode">QR-Inhalt:</label>
|
||||
<select id="content-mode">
|
||||
<option value="text">Text oder URL</option>
|
||||
<option value="wifi">WLAN</option>
|
||||
<option value="event">Termin (Kalender)</option>
|
||||
<option value="vcard">vCard (Kontakt)</option>
|
||||
</select>
|
||||
|
||||
<div id="section-text" class="content-section">
|
||||
<label for="text">Text oder URL:</label>
|
||||
<div class="text-input-wrapper">
|
||||
<input type="text" id="text" placeholder="https://qr.medisoftware.org" value="" autofocus="">
|
||||
<button type="button" id="clear-text" class="clear-button" aria-label="Clear text" style="display: none;">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options-row">
|
||||
<div class="options-col">
|
||||
<label for="wifi-ssid">WLAN SSID:</label>
|
||||
<input type="text" id="wifi-ssid" placeholder="">
|
||||
<div id="section-wifi" class="content-section" style="display: none;">
|
||||
<div class="options-row">
|
||||
<div class="options-col">
|
||||
<label for="wifi-ssid">WLAN SSID:</label>
|
||||
<input type="text" id="wifi-ssid" placeholder="">
|
||||
</div>
|
||||
|
||||
<div class="options-col">
|
||||
<label for="wifi-password">WLAN Passwort:</label>
|
||||
<input type="password" id="wifi-password" placeholder="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="section-event" class="content-section" style="display: none;" lang="de-DE">
|
||||
<label for="event-title">Titel:</label>
|
||||
<input type="text" id="event-title" placeholder="" autocomplete="off">
|
||||
|
||||
<p class="event-datetime-hint">Datum über das Kalenderfeld; Uhrzeit als <strong>Stunden 0–23</strong> und <strong>Minuten 0–59</strong> (reines 24h-Format, kein AM/PM). Ortszeit <strong>Europe/Berlin</strong>.</p>
|
||||
|
||||
<div class="options-row">
|
||||
<div class="options-col">
|
||||
<label for="event-start-date">Beginn – Datum:</label>
|
||||
<input type="date" id="event-start-date" autocomplete="off">
|
||||
</div>
|
||||
<div class="options-col">
|
||||
<div class="time-24h-block">
|
||||
<span class="label" style="display:block; margin-bottom:5px; font-weight:500; color:#374151;">Beginn – Uhrzeit (24h):</span>
|
||||
<div class="time-24h-row" role="group" aria-label="Beginn Uhrzeit 24 Stunden">
|
||||
<input type="number" id="event-start-hour" min="0" max="23" step="1" placeholder="HH" inputmode="numeric" autocomplete="off" aria-label="Stunde Beginn, 0 bis 23">
|
||||
<span class="time-24h-sep">:</span>
|
||||
<input type="number" id="event-start-minute" min="0" max="59" step="1" placeholder="MM" inputmode="numeric" autocomplete="off" aria-label="Minute Beginn, 0 bis 59">
|
||||
</div>
|
||||
<p class="time-24h-hint">Stunde 0–23, Minute 0–59</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="options-row">
|
||||
<div class="options-col">
|
||||
<label for="event-end-date">Ende (optional) – Datum:</label>
|
||||
<input type="date" id="event-end-date" autocomplete="off">
|
||||
</div>
|
||||
<div class="options-col">
|
||||
<div class="time-24h-block">
|
||||
<span class="label" style="display:block; margin-bottom:5px; font-weight:500; color:#374151;">Ende (optional) – Uhrzeit (24h):</span>
|
||||
<div class="time-24h-row" role="group" aria-label="Ende Uhrzeit 24 Stunden">
|
||||
<input type="number" id="event-end-hour" min="0" max="23" step="1" placeholder="HH" inputmode="numeric" autocomplete="off" aria-label="Stunde Ende, 0 bis 23">
|
||||
<span class="time-24h-sep">:</span>
|
||||
<input type="number" id="event-end-minute" min="0" max="59" step="1" placeholder="MM" inputmode="numeric" autocomplete="off" aria-label="Minute Ende, 0 bis 59">
|
||||
</div>
|
||||
<p class="time-24h-hint">Stunde 0–23, Minute 0–59</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="event-berlin-preview" id="event-berlin-preview" aria-live="polite"></p>
|
||||
|
||||
<label for="event-location">Ort (optional):</label>
|
||||
<input type="text" id="event-location" placeholder="" autocomplete="off">
|
||||
|
||||
<label for="event-description">Beschreibung (optional):</label>
|
||||
<textarea id="event-description" placeholder="" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div id="section-vcard" class="content-section" style="display: none;">
|
||||
<div class="options-row">
|
||||
<div class="options-col">
|
||||
<label for="vcard-first-name">Vorname:</label>
|
||||
<input type="text" id="vcard-first-name" placeholder="Max" autocomplete="off">
|
||||
</div>
|
||||
<div class="options-col">
|
||||
<label for="vcard-last-name">Nachname:</label>
|
||||
<input type="text" id="vcard-last-name" placeholder="Mustermann" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options-col">
|
||||
<label for="wifi-password">WLAN Passwort:</label>
|
||||
<input type="password" id="wifi-password" placeholder="">
|
||||
<div class="options-row">
|
||||
<div class="options-col">
|
||||
<label for="vcard-org">Organisation:</label>
|
||||
<input type="text" id="vcard-org" placeholder="Knorrlabs Inc." autocomplete="off">
|
||||
</div>
|
||||
<div class="options-col">
|
||||
<label for="vcard-title">Position (optional):</label>
|
||||
<input type="text" id="vcard-title" placeholder="Master Of Desaster" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options-row">
|
||||
<div class="options-col">
|
||||
<label for="vcard-phone">Telefon (optional):</label>
|
||||
<input type="tel" id="vcard-phone" placeholder="+49 30 1234567" autocomplete="off">
|
||||
</div>
|
||||
<div class="options-col">
|
||||
<label for="vcard-email">E-Mail (optional):</label>
|
||||
<input type="email" id="vcard-email" placeholder="max@beispiel.de" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="vcard-website">Webseite (optional):</label>
|
||||
<input type="url" id="vcard-website" placeholder="https://beispiel.de" autocomplete="off">
|
||||
|
||||
<label for="vcard-address">Adresse (optional):</label>
|
||||
<textarea id="vcard-address" placeholder="Musterstraße 1, 10115 Berlin" rows="2"></textarea>
|
||||
|
||||
<label for="vcard-note">Notiz (optional):</label>
|
||||
<textarea id="vcard-note" placeholder="Erreichbar Mo-Fr 9-17 Uhr" rows="2"></textarea>
|
||||
|
||||
<p class="vcard-preview-label">vCard-Vorschau:</p>
|
||||
<pre id="vcard-preview" class="vcard-preview" aria-live="polite">Bitte mindestens Vorname, Nachname oder Organisation eingeben.</pre>
|
||||
</div>
|
||||
|
||||
<div class="options-row">
|
||||
@@ -384,7 +565,7 @@
|
||||
|
||||
<p>Not today.</p>
|
||||
|
||||
<p>Rather than bemoan the fact that there's no easy way to get QR ohne Schnickschnak without the low-key parasitic monetization, I found a library (qrious) and put together just enough Javascript code to generate QR codes in the browser. No ads, no trackers, no sending your data to someone else's server for who-knows-what-reason.</p>
|
||||
<p>Rather than bemoan the fact that there's no easy way to get QR ohne Schnickschnak without the low-key parasitic monetization, I found a library (qrious) and put together just enough Javascript code to generate QR codes in the browser. No ads; page views are counted with self-hosted Plausible (privacy-friendly, no cookies). Your QR content is not sent anywhere.</p>
|
||||
|
||||
<p>Go ahead and inspect the page source. Save it to your computer, copy it, remix it, whatever you want.</p>
|
||||
|
||||
@@ -393,12 +574,15 @@
|
||||
<p>This is the web we were trying to build.</p>
|
||||
|
||||
<p>Be excellent to each other.</p>
|
||||
|
||||
<h3>Modi</h3>
|
||||
<p>Unter <strong>QR-Inhalt</strong> wählst du, was kodiert wird: freier Text oder URL, WLAN-Zugangsdaten (WIFI-QR), ein <strong>Kalendertermin</strong> oder eine <strong>vCard</strong>. Im Modus „Termin“ erzeugt der QR-Code einen Standard-iCalendar-Eintrag (VEVENT), den viele Smartphones beim Scannen in die Kalender-App übernehmen. Datumseingabe als <strong>TT/MM/JJJJ</strong>, Uhrzeit im <strong>24-Stunden-Format</strong>; die Zeitzone ist <strong>Europe/Berlin</strong>. Im Modus „vCard“ wird ein Kontakt im vCard-Format (3.0) erzeugt, den viele Geräte direkt als Kontakt speichern. Deine Eingaben für den QR-Code werden nicht an einen Server gesendet.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
Dieser QR-Code-Generator funktioniert vollständig in deinem Browser. Es werden keine Daten an einen Server gesendet. <i>Quellcode und README bei <a href="https://gitea.elpatron.me/elpatron/QR-Code-Generator" target="_blank">Gitea</a></i>.
|
||||
<p class="credit">Made with ❤️ and 🍪 by Markus. Self hosted on <a href="https://unraid.net" target="_blank">Unraid</a> for <a href="https://medisoftware.de" target="_blank">medisoftware</a>. Credits: <a href="https://qr.alster.space/" target="_blank">alsternerd</a></p>
|
||||
Dieser QR-Code-Generator funktioniert vollständig in deinem Browser; QR-Inhalte werden nicht hochgeladen. Seitenaufrufe werden mit selbst gehostetem Plausible aggregiert (ohne Cookies). <i>Quellcode und README bei <a href="https://gitea.elpatron.me/elpatron/QR-Code-Generator" target="_blank">Gitea</a></i>.
|
||||
<p class="credit">Made with ❤️ and 🍪 by <a href="mailto:elpatron+qr@mailbox.org">Markus F.J. Busche</a>. Self hosted on <a href="https://www.proxmox.com/" target="_blank">Proxmox</a>. Credits: <a href="https://qr.alster.space/" target="_blank">alsternerd</a></p>
|
||||
</div>
|
||||
|
||||
</body></html>
|
||||
@@ -2,8 +2,31 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
const textInput = document.getElementById('text');
|
||||
const clearTextBtn = document.getElementById('clear-text');
|
||||
const contentModeSelect = document.getElementById('content-mode');
|
||||
const sectionText = document.getElementById('section-text');
|
||||
const sectionWifi = document.getElementById('section-wifi');
|
||||
const sectionEvent = document.getElementById('section-event');
|
||||
const sectionVcard = document.getElementById('section-vcard');
|
||||
const wifiSsidInput = document.getElementById('wifi-ssid');
|
||||
const wifiPasswordInput = document.getElementById('wifi-password');
|
||||
const eventTitleInput = document.getElementById('event-title');
|
||||
const eventStartDateInput = document.getElementById('event-start-date');
|
||||
const eventStartHourInput = document.getElementById('event-start-hour');
|
||||
const eventStartMinuteInput = document.getElementById('event-start-minute');
|
||||
const eventEndDateInput = document.getElementById('event-end-date');
|
||||
const eventEndHourInput = document.getElementById('event-end-hour');
|
||||
const eventEndMinuteInput = document.getElementById('event-end-minute');
|
||||
const eventLocationInput = document.getElementById('event-location');
|
||||
const eventDescriptionInput = document.getElementById('event-description');
|
||||
const vcardFirstNameInput = document.getElementById('vcard-first-name');
|
||||
const vcardLastNameInput = document.getElementById('vcard-last-name');
|
||||
const vcardOrgInput = document.getElementById('vcard-org');
|
||||
const vcardTitleInput = document.getElementById('vcard-title');
|
||||
const vcardPhoneInput = document.getElementById('vcard-phone');
|
||||
const vcardEmailInput = document.getElementById('vcard-email');
|
||||
const vcardWebsiteInput = document.getElementById('vcard-website');
|
||||
const vcardAddressInput = document.getElementById('vcard-address');
|
||||
const vcardNoteInput = document.getElementById('vcard-note');
|
||||
const sizeSelect = document.getElementById('size');
|
||||
const errorCorrectionSelect = document.getElementById('errorCorrection');
|
||||
const foregroundColor = document.getElementById('foreground');
|
||||
@@ -18,6 +41,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const titleElement = document.getElementById('title');
|
||||
const shareBtn = document.getElementById('share');
|
||||
const shareHint = document.getElementById('share-hint');
|
||||
const eventBerlinPreview = document.getElementById('event-berlin-preview');
|
||||
const vcardPreview = document.getElementById('vcard-preview');
|
||||
|
||||
const MAX_EVENT_TITLE = 2000;
|
||||
const MAX_EVENT_LOCATION = 1000;
|
||||
const MAX_EVENT_DESCRIPTION = 8000;
|
||||
const MAX_VCARD_FIELD = 500;
|
||||
|
||||
// Title Easter egg
|
||||
let titleClickCount = 0;
|
||||
@@ -62,13 +92,56 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
function toggleContentSections() {
|
||||
const mode = contentModeSelect.value;
|
||||
sectionText.style.display = mode === 'text' ? 'block' : 'none';
|
||||
sectionWifi.style.display = mode === 'wifi' ? 'block' : 'none';
|
||||
sectionEvent.style.display = mode === 'event' ? 'block' : 'none';
|
||||
sectionVcard.style.display = mode === 'vcard' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
contentModeSelect.addEventListener('change', function() {
|
||||
toggleContentSections();
|
||||
updateBerlinPreview();
|
||||
generateQRCode();
|
||||
if (this.value === 'text') {
|
||||
textInput.focus();
|
||||
} else if (this.value === 'wifi') {
|
||||
wifiSsidInput.focus();
|
||||
} else if (this.value === 'event') {
|
||||
eventTitleInput.focus();
|
||||
} else if (this.value === 'vcard') {
|
||||
vcardFirstNameInput.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Set random compliment as default text and select it
|
||||
textInput.value = 'https://medisoftware.de';
|
||||
textInput.select();
|
||||
|
||||
// Show/hide clear button based on text input content
|
||||
// Show/hide clear button based on input content
|
||||
function toggleClearButton() {
|
||||
if (textInput.value.length > 0) {
|
||||
const hasText = textInput.value.length > 0;
|
||||
const hasWifi = wifiSsidInput.value.trim().length > 0 || wifiPasswordInput.value.trim().length > 0;
|
||||
const hasEvent = eventTitleInput.value.trim().length > 0 ||
|
||||
eventStartDateInput.value.length > 0 ||
|
||||
eventStartHourInput.value !== '' ||
|
||||
eventStartMinuteInput.value !== '' ||
|
||||
eventEndDateInput.value.length > 0 ||
|
||||
eventEndHourInput.value !== '' ||
|
||||
eventEndMinuteInput.value !== '' ||
|
||||
eventLocationInput.value.trim().length > 0 ||
|
||||
eventDescriptionInput.value.trim().length > 0;
|
||||
const hasVCard = vcardFirstNameInput.value.trim().length > 0 ||
|
||||
vcardLastNameInput.value.trim().length > 0 ||
|
||||
vcardOrgInput.value.trim().length > 0 ||
|
||||
vcardTitleInput.value.trim().length > 0 ||
|
||||
vcardPhoneInput.value.trim().length > 0 ||
|
||||
vcardEmailInput.value.trim().length > 0 ||
|
||||
vcardWebsiteInput.value.trim().length > 0 ||
|
||||
vcardAddressInput.value.trim().length > 0 ||
|
||||
vcardNoteInput.value.trim().length > 0;
|
||||
if (hasText || hasWifi || hasEvent || hasVCard) {
|
||||
clearTextBtn.style.display = 'block';
|
||||
} else {
|
||||
clearTextBtn.style.display = 'none';
|
||||
@@ -83,6 +156,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
textInput.value = '';
|
||||
wifiSsidInput.value = '';
|
||||
wifiPasswordInput.value = '';
|
||||
eventTitleInput.value = '';
|
||||
eventStartDateInput.value = '';
|
||||
eventStartHourInput.value = '';
|
||||
eventStartMinuteInput.value = '';
|
||||
eventEndDateInput.value = '';
|
||||
eventEndHourInput.value = '';
|
||||
eventEndMinuteInput.value = '';
|
||||
eventLocationInput.value = '';
|
||||
eventDescriptionInput.value = '';
|
||||
vcardFirstNameInput.value = '';
|
||||
vcardLastNameInput.value = '';
|
||||
vcardOrgInput.value = '';
|
||||
vcardTitleInput.value = '';
|
||||
vcardPhoneInput.value = '';
|
||||
vcardEmailInput.value = '';
|
||||
vcardWebsiteInput.value = '';
|
||||
vcardAddressInput.value = '';
|
||||
vcardNoteInput.value = '';
|
||||
contentModeSelect.value = 'text';
|
||||
toggleContentSections();
|
||||
updateBerlinPreview();
|
||||
toggleClearButton();
|
||||
textInput.focus();
|
||||
generateQRCode();
|
||||
@@ -91,10 +185,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Update clear button visibility when text changes
|
||||
textInput.addEventListener('input', function() {
|
||||
toggleClearButton();
|
||||
// Only generate QR code if no wifi data is being entered
|
||||
const wifiSsid = wifiSsidInput.value.trim();
|
||||
const wifiPassword = wifiPasswordInput.value.trim();
|
||||
if (!wifiSsid && !wifiPassword) {
|
||||
if (contentModeSelect.value === 'text') {
|
||||
generateQRCode();
|
||||
}
|
||||
});
|
||||
@@ -109,22 +200,146 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Generate QR code when wifi settings change
|
||||
wifiSsidInput.addEventListener('input', function() {
|
||||
updateTextDisplay();
|
||||
if (contentModeSelect.value === 'wifi') {
|
||||
updateTextDisplay();
|
||||
}
|
||||
generateQRCode();
|
||||
toggleClearButton();
|
||||
});
|
||||
wifiPasswordInput.addEventListener('input', function() {
|
||||
updateTextDisplay();
|
||||
if (contentModeSelect.value === 'wifi') {
|
||||
updateTextDisplay();
|
||||
}
|
||||
generateQRCode();
|
||||
toggleClearButton();
|
||||
});
|
||||
|
||||
eventTitleInput.addEventListener('input', function() {
|
||||
updateBerlinPreview();
|
||||
generateQRCode();
|
||||
toggleClearButton();
|
||||
});
|
||||
function onEventDateTimeInput() {
|
||||
updateBerlinPreview();
|
||||
generateQRCode();
|
||||
toggleClearButton();
|
||||
}
|
||||
eventStartDateInput.addEventListener('input', onEventDateTimeInput);
|
||||
eventStartDateInput.addEventListener('change', onEventDateTimeInput);
|
||||
eventStartHourInput.addEventListener('input', onEventDateTimeInput);
|
||||
eventStartHourInput.addEventListener('change', onEventDateTimeInput);
|
||||
eventStartMinuteInput.addEventListener('input', onEventDateTimeInput);
|
||||
eventStartMinuteInput.addEventListener('change', onEventDateTimeInput);
|
||||
eventEndDateInput.addEventListener('input', onEventDateTimeInput);
|
||||
eventEndDateInput.addEventListener('change', onEventDateTimeInput);
|
||||
eventEndHourInput.addEventListener('input', onEventDateTimeInput);
|
||||
eventEndHourInput.addEventListener('change', onEventDateTimeInput);
|
||||
eventEndMinuteInput.addEventListener('input', onEventDateTimeInput);
|
||||
eventEndMinuteInput.addEventListener('change', onEventDateTimeInput);
|
||||
|
||||
function clampTimeNumberInput(el, max) {
|
||||
if (el.value === '') {
|
||||
return;
|
||||
}
|
||||
var v = parseInt(el.value, 10);
|
||||
if (isNaN(v)) {
|
||||
return;
|
||||
}
|
||||
if (v < 0) {
|
||||
el.value = '0';
|
||||
} else if (v > max) {
|
||||
el.value = String(max);
|
||||
}
|
||||
}
|
||||
eventStartHourInput.addEventListener('blur', function() { clampTimeNumberInput(eventStartHourInput, 23); });
|
||||
eventStartMinuteInput.addEventListener('blur', function() { clampTimeNumberInput(eventStartMinuteInput, 59); });
|
||||
eventEndHourInput.addEventListener('blur', function() { clampTimeNumberInput(eventEndHourInput, 23); });
|
||||
eventEndMinuteInput.addEventListener('blur', function() { clampTimeNumberInput(eventEndMinuteInput, 59); });
|
||||
eventLocationInput.addEventListener('input', function() {
|
||||
generateQRCode();
|
||||
toggleClearButton();
|
||||
});
|
||||
eventDescriptionInput.addEventListener('input', function() {
|
||||
generateQRCode();
|
||||
toggleClearButton();
|
||||
});
|
||||
function onVcardInput() {
|
||||
generateQRCode();
|
||||
toggleClearButton();
|
||||
}
|
||||
vcardFirstNameInput.addEventListener('input', onVcardInput);
|
||||
vcardLastNameInput.addEventListener('input', onVcardInput);
|
||||
vcardOrgInput.addEventListener('input', onVcardInput);
|
||||
vcardTitleInput.addEventListener('input', onVcardInput);
|
||||
vcardPhoneInput.addEventListener('input', onVcardInput);
|
||||
vcardEmailInput.addEventListener('input', onVcardInput);
|
||||
vcardWebsiteInput.addEventListener('input', onVcardInput);
|
||||
vcardAddressInput.addEventListener('input', onVcardInput);
|
||||
vcardNoteInput.addEventListener('input', onVcardInput);
|
||||
|
||||
// Download QR code as image
|
||||
downloadBtn.addEventListener('click', downloadQRCode);
|
||||
|
||||
function sanitizeUrlString(s, maxLen) {
|
||||
if (typeof s !== 'string') {
|
||||
return '';
|
||||
}
|
||||
return s.replace(/\0/g, '').slice(0, maxLen);
|
||||
}
|
||||
|
||||
function isValidDatetimeLocal(s) {
|
||||
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(s);
|
||||
}
|
||||
|
||||
function combineDateTimeFromParts(dateStr, hourStr, minuteStr) {
|
||||
if (!dateStr || hourStr === '' || minuteStr === '') {
|
||||
return '';
|
||||
}
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
||||
return '';
|
||||
}
|
||||
var h = parseInt(String(hourStr).trim(), 10);
|
||||
var mi = parseInt(String(minuteStr).trim(), 10);
|
||||
if (isNaN(h) || isNaN(mi)) {
|
||||
return '';
|
||||
}
|
||||
if (h < 0 || h > 23 || mi < 0 || mi > 59) {
|
||||
return '';
|
||||
}
|
||||
var pad = function(n) { return String(n).padStart(2, '0'); };
|
||||
return dateStr + 'T' + pad(h) + ':' + pad(mi);
|
||||
}
|
||||
|
||||
function applyCombinedToDateTimeInputs(combined, dateInput, hourInput, minuteInput) {
|
||||
if (!isValidDatetimeLocal(combined)) {
|
||||
return;
|
||||
}
|
||||
var splitT = combined.split('T');
|
||||
var d = splitT[0];
|
||||
var t = splitT[1];
|
||||
var tm = /^(\d{2}):(\d{2})$/.exec(t);
|
||||
if (!tm) {
|
||||
return;
|
||||
}
|
||||
dateInput.value = d;
|
||||
hourInput.value = String(parseInt(tm[1], 10));
|
||||
minuteInput.value = String(parseInt(tm[2], 10));
|
||||
}
|
||||
|
||||
// Or check for URL parameters if present
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
let explicitModeFromUrl = false;
|
||||
if (urlParams.has('mode')) {
|
||||
const m = urlParams.get('mode');
|
||||
if (m === 'text' || m === 'wifi' || m === 'event' || m === 'vcard') {
|
||||
contentModeSelect.value = m;
|
||||
explicitModeFromUrl = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (urlParams.has('text')) {
|
||||
const safeText = urlParams.get('text');
|
||||
// Nur erlaubte Zeichen (Buchstaben, Zahlen, gängige URL-Zeichen)
|
||||
if (/^[\w\-.:/?&=#%+@!;,]*$/i.test(safeText)) {
|
||||
textInput.value = safeText;
|
||||
}
|
||||
@@ -141,6 +356,72 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
wifiPasswordInput.value = safePassword;
|
||||
}
|
||||
}
|
||||
|
||||
const hasEventParams = urlParams.has('eventTitle') || urlParams.has('eventStart');
|
||||
if (!explicitModeFromUrl && hasEventParams) {
|
||||
contentModeSelect.value = 'event';
|
||||
}
|
||||
if (urlParams.has('eventTitle')) {
|
||||
eventTitleInput.value = sanitizeUrlString(urlParams.get('eventTitle'), MAX_EVENT_TITLE);
|
||||
}
|
||||
if (urlParams.has('eventStart')) {
|
||||
const es = urlParams.get('eventStart');
|
||||
if (isValidDatetimeLocal(es)) {
|
||||
applyCombinedToDateTimeInputs(es, eventStartDateInput, eventStartHourInput, eventStartMinuteInput);
|
||||
}
|
||||
}
|
||||
if (urlParams.has('eventEnd')) {
|
||||
const ee = urlParams.get('eventEnd');
|
||||
if (isValidDatetimeLocal(ee)) {
|
||||
applyCombinedToDateTimeInputs(ee, eventEndDateInput, eventEndHourInput, eventEndMinuteInput);
|
||||
}
|
||||
}
|
||||
if (urlParams.has('eventLocation')) {
|
||||
eventLocationInput.value = sanitizeUrlString(urlParams.get('eventLocation'), MAX_EVENT_LOCATION);
|
||||
}
|
||||
if (urlParams.has('eventDescription')) {
|
||||
eventDescriptionInput.value = sanitizeUrlString(urlParams.get('eventDescription'), MAX_EVENT_DESCRIPTION);
|
||||
}
|
||||
const hasVcardParams = urlParams.has('vcardFirstName') ||
|
||||
urlParams.has('vcardLastName') ||
|
||||
urlParams.has('vcardOrg') ||
|
||||
urlParams.has('vcardTitle') ||
|
||||
urlParams.has('vcardPhone') ||
|
||||
urlParams.has('vcardEmail') ||
|
||||
urlParams.has('vcardWebsite') ||
|
||||
urlParams.has('vcardAddress') ||
|
||||
urlParams.has('vcardNote');
|
||||
if (!explicitModeFromUrl && !hasEventParams && hasVcardParams) {
|
||||
contentModeSelect.value = 'vcard';
|
||||
}
|
||||
if (urlParams.has('vcardFirstName')) {
|
||||
vcardFirstNameInput.value = sanitizeUrlString(urlParams.get('vcardFirstName'), MAX_VCARD_FIELD);
|
||||
}
|
||||
if (urlParams.has('vcardLastName')) {
|
||||
vcardLastNameInput.value = sanitizeUrlString(urlParams.get('vcardLastName'), MAX_VCARD_FIELD);
|
||||
}
|
||||
if (urlParams.has('vcardOrg')) {
|
||||
vcardOrgInput.value = sanitizeUrlString(urlParams.get('vcardOrg'), MAX_VCARD_FIELD);
|
||||
}
|
||||
if (urlParams.has('vcardTitle')) {
|
||||
vcardTitleInput.value = sanitizeUrlString(urlParams.get('vcardTitle'), MAX_VCARD_FIELD);
|
||||
}
|
||||
if (urlParams.has('vcardPhone')) {
|
||||
vcardPhoneInput.value = sanitizeUrlString(urlParams.get('vcardPhone'), MAX_VCARD_FIELD);
|
||||
}
|
||||
if (urlParams.has('vcardEmail')) {
|
||||
vcardEmailInput.value = sanitizeUrlString(urlParams.get('vcardEmail'), MAX_VCARD_FIELD);
|
||||
}
|
||||
if (urlParams.has('vcardWebsite')) {
|
||||
vcardWebsiteInput.value = sanitizeUrlString(urlParams.get('vcardWebsite'), MAX_VCARD_FIELD);
|
||||
}
|
||||
if (urlParams.has('vcardAddress')) {
|
||||
vcardAddressInput.value = sanitizeUrlString(urlParams.get('vcardAddress'), MAX_VCARD_FIELD);
|
||||
}
|
||||
if (urlParams.has('vcardNote')) {
|
||||
vcardNoteInput.value = sanitizeUrlString(urlParams.get('vcardNote'), MAX_VCARD_FIELD);
|
||||
}
|
||||
|
||||
if (urlParams.has('size')) {
|
||||
const sizeValue = urlParams.get('size');
|
||||
if (["128", "256", "512", "1024"].includes(sizeValue)) {
|
||||
@@ -165,13 +446,274 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
backgroundColor.value = bgValue;
|
||||
}
|
||||
}
|
||||
|
||||
toggleContentSections();
|
||||
updateBerlinPreview();
|
||||
generateQRCode();
|
||||
|
||||
function daysInMonth(y, mo) {
|
||||
return new Date(y, mo, 0).getDate();
|
||||
}
|
||||
|
||||
function parseDatetimeLocalValue(s) {
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/.exec(s);
|
||||
if (!m) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
y: parseInt(m[1], 10),
|
||||
mo: parseInt(m[2], 10),
|
||||
d: parseInt(m[3], 10),
|
||||
h: parseInt(m[4], 10),
|
||||
mi: parseInt(m[5], 10)
|
||||
};
|
||||
}
|
||||
|
||||
function addHoursWall(parts, deltaH) {
|
||||
let y = parts.y;
|
||||
let mo = parts.mo;
|
||||
let d = parts.d;
|
||||
let h = parts.h + deltaH;
|
||||
let mi = parts.mi;
|
||||
while (h >= 24) {
|
||||
h -= 24;
|
||||
d += 1;
|
||||
const dim = daysInMonth(y, mo);
|
||||
if (d > dim) {
|
||||
d = 1;
|
||||
mo += 1;
|
||||
if (mo > 12) {
|
||||
mo = 1;
|
||||
y += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { y: y, mo: mo, d: d, h: h, mi: mi };
|
||||
}
|
||||
|
||||
function wallTimeToICal(p) {
|
||||
const pad = function(n) { return String(n).padStart(2, '0'); };
|
||||
return String(p.y) + pad(p.mo) + pad(p.d) + 'T' + pad(p.h) + pad(p.mi) + '00';
|
||||
}
|
||||
|
||||
function compareWallTime(a, b) {
|
||||
return (a.y * 1e8 + a.mo * 1e6 + a.d * 1e4 + a.h * 100 + a.mi) -
|
||||
(b.y * 1e8 + b.mo * 1e6 + b.d * 1e4 + b.h * 100 + b.mi);
|
||||
}
|
||||
|
||||
function formatSlashDateTime24hBerlin(p) {
|
||||
const pad = function(n) { return String(n).padStart(2, '0'); };
|
||||
return pad(p.d) + '/' + pad(p.mo) + '/' + p.y + ', ' + pad(p.h) + ':' + pad(p.mi);
|
||||
}
|
||||
|
||||
function updateBerlinPreview() {
|
||||
if (!eventBerlinPreview) {
|
||||
return;
|
||||
}
|
||||
if (contentModeSelect.value !== 'event') {
|
||||
eventBerlinPreview.textContent = '';
|
||||
return;
|
||||
}
|
||||
const startRaw = combineDateTimeFromParts(
|
||||
eventStartDateInput.value,
|
||||
eventStartHourInput.value,
|
||||
eventStartMinuteInput.value
|
||||
);
|
||||
const endD = eventEndDateInput.value;
|
||||
const endH = eventEndHourInput.value;
|
||||
const endM = eventEndMinuteInput.value;
|
||||
const endTimeComplete = endH !== '' && endM !== '';
|
||||
const endTimeAny = endH !== '' || endM !== '';
|
||||
let endRaw = '';
|
||||
if (endD && endTimeComplete) {
|
||||
endRaw = combineDateTimeFromParts(endD, endH, endM);
|
||||
} else if (endD && !endTimeComplete) {
|
||||
eventBerlinPreview.textContent =
|
||||
'Bitte geben Sie für das Ende Datum sowie Stunde (0–23) und Minute (0–59) an, oder lassen Sie das Ende vollständig leer.';
|
||||
return;
|
||||
} else if (!endD && endTimeAny) {
|
||||
eventBerlinPreview.textContent =
|
||||
'Bitte geben Sie für das Ende ein Datum ein, oder entfernen Sie Stunde/Minute.';
|
||||
return;
|
||||
}
|
||||
const sp = parseDatetimeLocalValue(startRaw);
|
||||
if (!sp) {
|
||||
eventBerlinPreview.textContent = '';
|
||||
return;
|
||||
}
|
||||
let endDisplay;
|
||||
if (endRaw) {
|
||||
const ep = parseDatetimeLocalValue(endRaw);
|
||||
if (!ep) {
|
||||
eventBerlinPreview.textContent = 'Beginn: ' + formatSlashDateTime24hBerlin(sp) + ' (Europe/Berlin)';
|
||||
return;
|
||||
}
|
||||
endDisplay = ep;
|
||||
} else {
|
||||
endDisplay = addHoursWall(sp, 1);
|
||||
}
|
||||
eventBerlinPreview.textContent =
|
||||
'Beginn: ' + formatSlashDateTime24hBerlin(sp) +
|
||||
' · Ende: ' + formatSlashDateTime24hBerlin(endDisplay) +
|
||||
' (Europe/Berlin)';
|
||||
}
|
||||
|
||||
function escapeICalText(str) {
|
||||
return String(str)
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '')
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/;/g, '\\;')
|
||||
.replace(/,/g, '\\,')
|
||||
.replace(/\n/g, '\\n');
|
||||
}
|
||||
|
||||
function formatICalUTCStamp() {
|
||||
const d = new Date();
|
||||
const pad = function(n) { return String(n).padStart(2, '0'); };
|
||||
return (
|
||||
d.getUTCFullYear() +
|
||||
pad(d.getUTCMonth() + 1) +
|
||||
pad(d.getUTCDate()) +
|
||||
'T' +
|
||||
pad(d.getUTCHours()) +
|
||||
pad(d.getUTCMinutes()) +
|
||||
pad(d.getUTCSeconds()) +
|
||||
'Z'
|
||||
);
|
||||
}
|
||||
|
||||
function newEventUid() {
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||
return crypto.randomUUID() + '@qr-generator.local';
|
||||
}
|
||||
return 'evt-' + String(Date.now()) + '-' + String(Math.random()).slice(2) + '@qr-generator.local';
|
||||
}
|
||||
|
||||
function buildICalEvent(opts) {
|
||||
const summary = opts.summary;
|
||||
const dtStart = opts.dtStart;
|
||||
const dtEnd = opts.dtEnd;
|
||||
const location = opts.location;
|
||||
const description = opts.description;
|
||||
|
||||
const vtimezoneBerlin = [
|
||||
'BEGIN:VTIMEZONE',
|
||||
'TZID:Europe/Berlin',
|
||||
'BEGIN:DAYLIGHT',
|
||||
'DTSTART:19810329T020000',
|
||||
'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU',
|
||||
'TZNAME:CEST',
|
||||
'TZOFFSETFROM:+0100',
|
||||
'TZOFFSETTO:+0200',
|
||||
'END:DAYLIGHT',
|
||||
'BEGIN:STANDARD',
|
||||
'DTSTART:19961027T030000',
|
||||
'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU',
|
||||
'TZNAME:CET',
|
||||
'TZOFFSETFROM:+0200',
|
||||
'TZOFFSETTO:+0100',
|
||||
'END:STANDARD',
|
||||
'END:VTIMEZONE'
|
||||
].join('\r\n');
|
||||
|
||||
const lines = [
|
||||
'BEGIN:VCALENDAR',
|
||||
'VERSION:2.0',
|
||||
'PRODID:-//qr-generator//DE',
|
||||
'CALSCALE:GREGORIAN',
|
||||
vtimezoneBerlin,
|
||||
'BEGIN:VEVENT',
|
||||
'UID:' + opts.uid,
|
||||
'DTSTAMP:' + opts.dtStamp,
|
||||
'DTSTART;TZID=Europe/Berlin:' + dtStart,
|
||||
'DTEND;TZID=Europe/Berlin:' + dtEnd,
|
||||
'SUMMARY:' + escapeICalText(summary)
|
||||
];
|
||||
if (location && location.trim()) {
|
||||
lines.push('LOCATION:' + escapeICalText(location.trim()));
|
||||
}
|
||||
if (description && description.trim()) {
|
||||
lines.push('DESCRIPTION:' + escapeICalText(description.trim()));
|
||||
}
|
||||
lines.push('END:VEVENT', 'END:VCALENDAR');
|
||||
return lines.join('\r\n');
|
||||
}
|
||||
|
||||
function escapeVCardText(str) {
|
||||
return String(str)
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '')
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/;/g, '\\;')
|
||||
.replace(/,/g, '\\,')
|
||||
.replace(/\n/g, '\\n');
|
||||
}
|
||||
|
||||
function buildVCard(opts) {
|
||||
const firstName = opts.firstName.trim();
|
||||
const lastName = opts.lastName.trim();
|
||||
const org = opts.org.trim();
|
||||
const title = opts.title.trim();
|
||||
const phone = opts.phone.trim();
|
||||
const email = opts.email.trim();
|
||||
const website = opts.website.trim();
|
||||
const address = opts.address.trim();
|
||||
const note = opts.note.trim();
|
||||
const displayName = [firstName, lastName].filter(Boolean).join(' ').trim() || org;
|
||||
|
||||
const lines = [
|
||||
'BEGIN:VCARD',
|
||||
'VERSION:3.0',
|
||||
'N:' + escapeVCardText(lastName) + ';' + escapeVCardText(firstName) + ';;;',
|
||||
'FN:' + escapeVCardText(displayName)
|
||||
];
|
||||
if (org) {
|
||||
lines.push('ORG:' + escapeVCardText(org));
|
||||
}
|
||||
if (title) {
|
||||
lines.push('TITLE:' + escapeVCardText(title));
|
||||
}
|
||||
if (phone) {
|
||||
lines.push('TEL;TYPE=CELL:' + escapeVCardText(phone));
|
||||
}
|
||||
if (email) {
|
||||
lines.push('EMAIL;TYPE=INTERNET:' + escapeVCardText(email));
|
||||
}
|
||||
if (website) {
|
||||
lines.push('URL:' + escapeVCardText(website));
|
||||
}
|
||||
if (address) {
|
||||
lines.push('ADR;TYPE=WORK:;;' + escapeVCardText(address) + ';;;;');
|
||||
}
|
||||
if (note) {
|
||||
lines.push('NOTE:' + escapeVCardText(note));
|
||||
}
|
||||
lines.push('END:VCARD');
|
||||
return lines.join('\r\n');
|
||||
}
|
||||
|
||||
function updateVCardPreview(text) {
|
||||
if (!vcardPreview) {
|
||||
return;
|
||||
}
|
||||
if (contentModeSelect.value !== 'vcard') {
|
||||
vcardPreview.textContent = '';
|
||||
return;
|
||||
}
|
||||
if (text && String(text).trim()) {
|
||||
vcardPreview.textContent = String(text);
|
||||
return;
|
||||
}
|
||||
vcardPreview.textContent = 'Bitte mindestens Vorname, Nachname oder Organisation eingeben.';
|
||||
}
|
||||
|
||||
function generateQRCode() {
|
||||
const mode = contentModeSelect.value;
|
||||
const text = textInput.value.trim();
|
||||
const wifiSsid = wifiSsidInput.value.trim();
|
||||
const wifiPassword = wifiPasswordInput.value.trim();
|
||||
const size = parseInt(sizeSelect.value);
|
||||
const size = parseInt(sizeSelect.value, 10);
|
||||
const errorCorrection = errorCorrectionSelect.value.toLowerCase();
|
||||
const fgColor = foregroundColor.value;
|
||||
const bgColor = backgroundColor.value;
|
||||
@@ -185,29 +727,162 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Clear previous error
|
||||
errorMessage.textContent = '';
|
||||
downloadBtn.style.display = 'none';
|
||||
if (mode !== 'vcard') {
|
||||
updateVCardPreview('');
|
||||
}
|
||||
|
||||
// Determine what to encode
|
||||
let qrText = text;
|
||||
let qrText = '';
|
||||
|
||||
// If wifi credentials are provided, generate wifi QR code
|
||||
if (wifiSsid && wifiPassword) {
|
||||
// Wifi QR code format: WIFI:S:<SSID>;T:WPA;P:<password>;;
|
||||
qrText = `WIFI:S:${escapeSpecialChars(wifiSsid)};T:WPA;P:${escapeSpecialChars(wifiPassword)};;`;
|
||||
} else if (wifiSsid && !wifiPassword) {
|
||||
// SSID only (open network)
|
||||
qrText = `WIFI:S:${escapeSpecialChars(wifiSsid)};T:nopass;;`;
|
||||
} else if (!wifiSsid && wifiPassword) {
|
||||
// Password without SSID is invalid
|
||||
errorMessage.textContent = 'Bitte geben Sie eine SSID ein, wenn Sie ein Passwort angeben';
|
||||
qrcodeCanvas.style.display = 'none';
|
||||
qrcodeImg.style.display = 'none';
|
||||
return;
|
||||
if (mode === 'event') {
|
||||
const title = eventTitleInput.value.trim();
|
||||
const startRaw = combineDateTimeFromParts(
|
||||
eventStartDateInput.value,
|
||||
eventStartHourInput.value,
|
||||
eventStartMinuteInput.value
|
||||
);
|
||||
const endD = eventEndDateInput.value;
|
||||
const endH = eventEndHourInput.value;
|
||||
const endM = eventEndMinuteInput.value;
|
||||
const endTimeComplete = endH !== '' && endM !== '';
|
||||
const endTimeAny = endH !== '' || endM !== '';
|
||||
const loc = eventLocationInput.value;
|
||||
const desc = eventDescriptionInput.value;
|
||||
|
||||
if (!title) {
|
||||
errorMessage.textContent = 'Bitte geben Sie einen Titel für den Termin ein';
|
||||
qrcodeCanvas.style.display = 'none';
|
||||
qrcodeImg.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
if (!startRaw) {
|
||||
errorMessage.textContent =
|
||||
'Bitte geben Sie Datum sowie Stunde (0–23) und Minute (0–59) für den Beginn ein';
|
||||
qrcodeCanvas.style.display = 'none';
|
||||
qrcodeImg.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
if (endD && !endTimeComplete) {
|
||||
errorMessage.textContent =
|
||||
'Für das Ende: bitte Stunde und Minute ergänzen (0–23 / 0–59) oder Ende leer lassen';
|
||||
qrcodeCanvas.style.display = 'none';
|
||||
qrcodeImg.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
if (!endD && endTimeAny) {
|
||||
errorMessage.textContent = 'Für das Ende bitte zuerst ein Datum wählen oder Zeitfelder leeren';
|
||||
qrcodeCanvas.style.display = 'none';
|
||||
qrcodeImg.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const startParts = parseDatetimeLocalValue(startRaw);
|
||||
if (!startParts) {
|
||||
errorMessage.textContent = 'Ungültige Startzeit (Datum und 24h-Uhrzeit prüfen)';
|
||||
qrcodeCanvas.style.display = 'none';
|
||||
qrcodeImg.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const dtStart = wallTimeToICal(startParts);
|
||||
|
||||
let endParts;
|
||||
const endRaw = endD && endTimeComplete ? combineDateTimeFromParts(endD, endH, endM) : '';
|
||||
if (endRaw) {
|
||||
endParts = parseDatetimeLocalValue(endRaw);
|
||||
if (!endParts) {
|
||||
errorMessage.textContent = 'Ungültige Endzeit';
|
||||
qrcodeCanvas.style.display = 'none';
|
||||
qrcodeImg.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
endParts = addHoursWall(startParts, 1);
|
||||
}
|
||||
|
||||
if (compareWallTime(endParts, startParts) <= 0) {
|
||||
errorMessage.textContent = 'Die Endzeit muss nach dem Beginn liegen (Ortszeit Europe/Berlin)';
|
||||
qrcodeCanvas.style.display = 'none';
|
||||
qrcodeImg.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const dtEndStr = wallTimeToICal(endParts);
|
||||
|
||||
qrText = buildICalEvent({
|
||||
uid: newEventUid(),
|
||||
dtStamp: formatICalUTCStamp(),
|
||||
summary: title.slice(0, MAX_EVENT_TITLE),
|
||||
dtStart: dtStart,
|
||||
dtEnd: dtEndStr,
|
||||
location: loc.slice(0, MAX_EVENT_LOCATION),
|
||||
description: desc.slice(0, MAX_EVENT_DESCRIPTION)
|
||||
});
|
||||
|
||||
textInput.value = qrText;
|
||||
toggleClearButton();
|
||||
} else if (mode === 'vcard') {
|
||||
const firstName = vcardFirstNameInput.value.slice(0, MAX_VCARD_FIELD);
|
||||
const lastName = vcardLastNameInput.value.slice(0, MAX_VCARD_FIELD);
|
||||
const org = vcardOrgInput.value.slice(0, MAX_VCARD_FIELD);
|
||||
const title = vcardTitleInput.value.slice(0, MAX_VCARD_FIELD);
|
||||
const phone = vcardPhoneInput.value.slice(0, MAX_VCARD_FIELD);
|
||||
const email = vcardEmailInput.value.slice(0, MAX_VCARD_FIELD);
|
||||
const website = vcardWebsiteInput.value.slice(0, MAX_VCARD_FIELD);
|
||||
const address = vcardAddressInput.value.slice(0, MAX_VCARD_FIELD);
|
||||
const note = vcardNoteInput.value.slice(0, MAX_VCARD_FIELD);
|
||||
const hasIdentity = firstName.trim() || lastName.trim() || org.trim();
|
||||
|
||||
if (!hasIdentity) {
|
||||
updateVCardPreview('');
|
||||
errorMessage.textContent = 'Bitte geben Sie mindestens Vorname, Nachname oder Organisation an';
|
||||
qrcodeCanvas.style.display = 'none';
|
||||
qrcodeImg.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
qrText = buildVCard({
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
org: org,
|
||||
title: title,
|
||||
phone: phone,
|
||||
email: email,
|
||||
website: website,
|
||||
address: address,
|
||||
note: note
|
||||
});
|
||||
updateVCardPreview(qrText);
|
||||
textInput.value = qrText;
|
||||
toggleClearButton();
|
||||
} else if (mode === 'wifi') {
|
||||
if (wifiSsid && wifiPassword) {
|
||||
qrText = 'WIFI:S:' + escapeSpecialChars(wifiSsid) + ';T:WPA;P:' + escapeSpecialChars(wifiPassword) + ';;';
|
||||
} else if (wifiSsid && !wifiPassword) {
|
||||
qrText = 'WIFI:S:' + escapeSpecialChars(wifiSsid) + ';T:nopass;;';
|
||||
} else if (!wifiSsid && wifiPassword) {
|
||||
errorMessage.textContent = 'Bitte geben Sie eine SSID ein, wenn Sie ein Passwort angeben';
|
||||
qrcodeCanvas.style.display = 'none';
|
||||
qrcodeImg.style.display = 'none';
|
||||
return;
|
||||
} else {
|
||||
qrText = text;
|
||||
}
|
||||
} else {
|
||||
qrText = text;
|
||||
}
|
||||
|
||||
// Validate input
|
||||
if (qrText === '') {
|
||||
errorMessage.textContent = 'Bitte geben Sie Text/URL oder Wifi-Daten ein';
|
||||
// Hide both canvas and image
|
||||
if (mode === 'wifi') {
|
||||
errorMessage.textContent = 'Bitte geben Sie Text/URL oder WLAN-Daten ein';
|
||||
} else if (mode === 'event') {
|
||||
errorMessage.textContent = 'Termin konnte nicht erzeugt werden';
|
||||
} else if (mode === 'vcard') {
|
||||
errorMessage.textContent = 'vCard konnte nicht erzeugt werden';
|
||||
} else {
|
||||
errorMessage.textContent = 'Bitte geben Sie Text oder eine URL ein';
|
||||
}
|
||||
qrcodeCanvas.style.display = 'none';
|
||||
qrcodeImg.style.display = 'none';
|
||||
return;
|
||||
@@ -270,34 +945,29 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
downloadBtn.textContent = 'Downloaded!';
|
||||
|
||||
// Reset button after a short delay
|
||||
setTimeout(() => {
|
||||
setTimeout(function() {
|
||||
downloadBtn.textContent = originalText;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Function to update the text display with current QR code content
|
||||
// Function to update the text display with current QR code content (WLAN)
|
||||
function updateTextDisplay() {
|
||||
const wifiSsid = wifiSsidInput.value.trim();
|
||||
const wifiPassword = wifiPasswordInput.value.trim();
|
||||
|
||||
if (wifiSsid && wifiPassword) {
|
||||
// Show wifi QR code format
|
||||
textInput.value = `WIFI:S:${escapeSpecialChars(wifiSsid)};T:WPA;P:${escapeSpecialChars(wifiPassword)};;`;
|
||||
textInput.value = 'WIFI:S:' + escapeSpecialChars(wifiSsid) + ';T:WPA;P:' + escapeSpecialChars(wifiPassword) + ';;';
|
||||
} else if (wifiSsid && !wifiPassword) {
|
||||
// Show open network format
|
||||
textInput.value = `WIFI:S:${escapeSpecialChars(wifiSsid)};T:nopass;;`;
|
||||
} else {
|
||||
// If no wifi data, keep original text input value
|
||||
// Don't change it here to avoid overwriting user input
|
||||
textInput.value = 'WIFI:S:' + escapeSpecialChars(wifiSsid) + ';T:nopass;;';
|
||||
}
|
||||
|
||||
// Update clear button visibility
|
||||
toggleClearButton();
|
||||
}
|
||||
|
||||
function updateShareHint() {
|
||||
const mode = contentModeSelect.value;
|
||||
const wifiPassword = wifiPasswordInput.value.trim();
|
||||
if (wifiPassword) {
|
||||
if (mode === 'wifi' && wifiPassword) {
|
||||
shareHint.style.display = 'block';
|
||||
} else {
|
||||
shareHint.style.display = 'none';
|
||||
@@ -307,11 +977,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
wifiPasswordInput.addEventListener('input', updateShareHint);
|
||||
wifiSsidInput.addEventListener('input', updateShareHint);
|
||||
textInput.addEventListener('input', updateShareHint);
|
||||
// Initial anzeigen, falls Passwort schon gesetzt
|
||||
contentModeSelect.addEventListener('change', updateShareHint);
|
||||
updateShareHint();
|
||||
|
||||
shareBtn.addEventListener('click', function() {
|
||||
const params = new URLSearchParams();
|
||||
const mode = contentModeSelect.value;
|
||||
const text = textInput.value.trim();
|
||||
const wifiSsid = wifiSsidInput.value.trim();
|
||||
const wifiPassword = wifiPasswordInput.value.trim();
|
||||
@@ -320,22 +991,94 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const fgColor = foregroundColor.value;
|
||||
const bgColor = backgroundColor.value;
|
||||
|
||||
// Entscheide, was kodiert werden soll
|
||||
if (wifiSsid) params.set('ssid', wifiSsid);
|
||||
if (wifiPassword) params.set('password', wifiPassword);
|
||||
if (!wifiSsid && !wifiPassword && text) params.set('text', text);
|
||||
if (size !== '256') params.set('size', size);
|
||||
if (errorCorrection !== 'M') params.set('errorCorrection', errorCorrection);
|
||||
if (fgColor !== '#000000') params.set('foreground', fgColor);
|
||||
if (bgColor !== '#ffffff') params.set('background', bgColor);
|
||||
params.set('mode', mode);
|
||||
|
||||
if (mode === 'event') {
|
||||
if (eventTitleInput.value.trim()) {
|
||||
params.set('eventTitle', eventTitleInput.value.trim());
|
||||
}
|
||||
const shareStart = combineDateTimeFromParts(
|
||||
eventStartDateInput.value,
|
||||
eventStartHourInput.value,
|
||||
eventStartMinuteInput.value
|
||||
);
|
||||
const shareEnd = combineDateTimeFromParts(
|
||||
eventEndDateInput.value,
|
||||
eventEndHourInput.value,
|
||||
eventEndMinuteInput.value
|
||||
);
|
||||
if (shareStart) {
|
||||
params.set('eventStart', shareStart);
|
||||
}
|
||||
if (shareEnd) {
|
||||
params.set('eventEnd', shareEnd);
|
||||
}
|
||||
if (eventLocationInput.value.trim()) {
|
||||
params.set('eventLocation', eventLocationInput.value.trim());
|
||||
}
|
||||
if (eventDescriptionInput.value.trim()) {
|
||||
params.set('eventDescription', eventDescriptionInput.value.trim());
|
||||
}
|
||||
} else if (mode === 'vcard') {
|
||||
if (vcardFirstNameInput.value.trim()) {
|
||||
params.set('vcardFirstName', vcardFirstNameInput.value.trim());
|
||||
}
|
||||
if (vcardLastNameInput.value.trim()) {
|
||||
params.set('vcardLastName', vcardLastNameInput.value.trim());
|
||||
}
|
||||
if (vcardOrgInput.value.trim()) {
|
||||
params.set('vcardOrg', vcardOrgInput.value.trim());
|
||||
}
|
||||
if (vcardTitleInput.value.trim()) {
|
||||
params.set('vcardTitle', vcardTitleInput.value.trim());
|
||||
}
|
||||
if (vcardPhoneInput.value.trim()) {
|
||||
params.set('vcardPhone', vcardPhoneInput.value.trim());
|
||||
}
|
||||
if (vcardEmailInput.value.trim()) {
|
||||
params.set('vcardEmail', vcardEmailInput.value.trim());
|
||||
}
|
||||
if (vcardWebsiteInput.value.trim()) {
|
||||
params.set('vcardWebsite', vcardWebsiteInput.value.trim());
|
||||
}
|
||||
if (vcardAddressInput.value.trim()) {
|
||||
params.set('vcardAddress', vcardAddressInput.value.trim());
|
||||
}
|
||||
if (vcardNoteInput.value.trim()) {
|
||||
params.set('vcardNote', vcardNoteInput.value.trim());
|
||||
}
|
||||
} else if (mode === 'wifi') {
|
||||
if (wifiSsid) {
|
||||
params.set('ssid', wifiSsid);
|
||||
}
|
||||
if (wifiPassword) {
|
||||
params.set('password', wifiPassword);
|
||||
}
|
||||
} else {
|
||||
if (text) {
|
||||
params.set('text', text);
|
||||
}
|
||||
}
|
||||
|
||||
if (size !== '256') {
|
||||
params.set('size', size);
|
||||
}
|
||||
if (errorCorrection !== 'M') {
|
||||
params.set('errorCorrection', errorCorrection);
|
||||
}
|
||||
if (fgColor !== '#000000') {
|
||||
params.set('foreground', fgColor);
|
||||
}
|
||||
if (bgColor !== '#ffffff') {
|
||||
params.set('background', bgColor);
|
||||
}
|
||||
|
||||
const url = window.location.origin + window.location.pathname + '?' + params.toString();
|
||||
// In Zwischenablage kopieren
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
navigator.clipboard.writeText(url).then(function() {
|
||||
const original = shareBtn.textContent;
|
||||
shareBtn.textContent = 'Link kopiert!';
|
||||
setTimeout(() => { shareBtn.textContent = original; }, 2000);
|
||||
}, () => {
|
||||
setTimeout(function() { shareBtn.textContent = original; }, 2000);
|
||||
}, function() {
|
||||
alert('Konnte Link nicht kopieren.');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user