Files
QR-Code-Generator/main.js
T
elpatron 11d4586ea7 fix: vCard-Mode bei allen vCard-URL-Parametern erkennen
Erweitert die Auto-Erkennung für vCard-Links, damit auch Links mit nur Titel, Website, Adresse oder Notiz automatisch in den vCard-Modus wechseln. So funktionieren geteilte vCard-URLs konsistent ohne manuelle Moduswahl.

Made-with: Cursor
2026-04-25 14:09:36 +02:00

1086 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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');
const backgroundColor = document.getElementById('background');
const downloadBtn = document.getElementById('download');
const qrcodeCanvas = document.getElementById('qrcode');
const qrcodeImg = document.getElementById('qrcode-img');
const errorMessage = document.getElementById('error-message');
const infoButton = document.getElementById('info-button');
const infoPanel = document.getElementById('info-panel');
const infoClose = document.getElementById('info-close');
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;
let titleTimeoutId = null;
titleElement.addEventListener('click', function() {
titleClickCount++;
clearTimeout(titleTimeoutId);
if (titleClickCount === 1) {
this.textContent = "Just a [bleep]ing QR Code";
titleTimeoutId = setTimeout(() => {
this.textContent = "Just a QR Code";
titleClickCount = 0;
}, 500);
}
});
// Info panel toggle
infoButton.addEventListener('click', function() {
infoPanel.classList.add('open');
});
infoClose.addEventListener('click', function() {
infoPanel.classList.remove('open');
});
// Close info panel when clicking outside
document.addEventListener('click', function(event) {
if (infoPanel.classList.contains('open') &&
!infoPanel.contains(event.target) &&
event.target !== infoButton) {
infoPanel.classList.remove('open');
}
});
// Close info panel with Escape key
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && infoPanel.classList.contains('open')) {
infoPanel.classList.remove('open');
}
});
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 input content
function toggleClearButton() {
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';
}
}
// Initialize clear button visibility
toggleClearButton();
// Clear text when clear button is clicked
clearTextBtn.addEventListener('click', 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();
});
// Update clear button visibility when text changes
textInput.addEventListener('input', function() {
toggleClearButton();
if (contentModeSelect.value === 'text') {
generateQRCode();
}
});
let qr = null;
// Generate QR code when settings change
sizeSelect.addEventListener('change', generateQRCode);
errorCorrectionSelect.addEventListener('change', generateQRCode);
foregroundColor.addEventListener('input', generateQRCode);
backgroundColor.addEventListener('input', generateQRCode);
// Generate QR code when wifi settings change
wifiSsidInput.addEventListener('input', function() {
if (contentModeSelect.value === 'wifi') {
updateTextDisplay();
}
generateQRCode();
toggleClearButton();
});
wifiPasswordInput.addEventListener('input', function() {
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');
if (/^[\w\-.:/?&=#%+@!;,]*$/i.test(safeText)) {
textInput.value = safeText;
}
}
if (urlParams.has('ssid')) {
const safeSsid = urlParams.get('ssid');
if (/^[^<>"'`\\]*$/.test(safeSsid)) {
wifiSsidInput.value = safeSsid;
}
}
if (urlParams.has('password')) {
const safePassword = urlParams.get('password');
if (/^[^<>"'`\\]*$/.test(safePassword)) {
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)) {
sizeSelect.value = sizeValue;
}
}
if (urlParams.has('errorCorrection')) {
const ecValue = urlParams.get('errorCorrection').toUpperCase();
if (["L", "M", "Q", "H"].includes(ecValue)) {
errorCorrectionSelect.value = ecValue;
}
}
if (urlParams.has('foreground')) {
const fgValue = urlParams.get('foreground');
if (/^#[0-9A-F]{6}$/i.test(fgValue)) {
foregroundColor.value = fgValue;
}
}
if (urlParams.has('background')) {
const bgValue = urlParams.get('background');
if (/^#[0-9A-F]{6}$/i.test(bgValue)) {
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 (023) und Minute (059) 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, 10);
const errorCorrection = errorCorrectionSelect.value.toLowerCase();
const fgColor = foregroundColor.value;
const bgColor = backgroundColor.value;
// Update the gabe link color to match foreground color
const gabeLink = document.querySelector('.credit a');
if (gabeLink) {
gabeLink.style.color = fgColor;
}
// Clear previous error
errorMessage.textContent = '';
downloadBtn.style.display = 'none';
if (mode !== 'vcard') {
updateVCardPreview('');
}
let qrText = '';
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 (023) und Minute (059) 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 (023 / 059) 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 === '') {
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;
}
// Create new QR code on canvas
try {
if (!qr) {
qr = new QRious({
element: qrcodeCanvas,
size: size,
value: qrText,
foreground: fgColor,
background: bgColor,
level: errorCorrection
});
} else {
qr.set({
size: size,
value: qrText,
foreground: fgColor,
background: bgColor,
level: errorCorrection
});
}
// Convert canvas to image
const dataURL = qrcodeCanvas.toDataURL('image/png');
qrcodeImg.src = dataURL;
// Show image (hide canvas) and download button
qrcodeCanvas.style.display = 'none';
qrcodeImg.style.display = 'block';
downloadBtn.style.display = 'inline-block';
} catch (err) {
errorMessage.textContent = 'Error generating QR code: ' + err.message;
qrcodeCanvas.style.display = 'none';
qrcodeImg.style.display = 'none';
}
}
// Helper function to escape special characters in wifi strings
function escapeSpecialChars(str) {
return str.replace(/[\\;:,]/g, '\\$&');
}
function downloadQRCode() {
// Use the image source for download
const link = document.createElement('a');
link.download = 'qrcode.png';
link.href = qrcodeImg.src;
// Simulate click to trigger download
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Show success feedback
const originalText = downloadBtn.textContent;
downloadBtn.textContent = 'Downloaded!';
// Reset button after a short delay
setTimeout(function() {
downloadBtn.textContent = originalText;
}, 2000);
}
// 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) {
textInput.value = 'WIFI:S:' + escapeSpecialChars(wifiSsid) + ';T:WPA;P:' + escapeSpecialChars(wifiPassword) + ';;';
} else if (wifiSsid && !wifiPassword) {
textInput.value = 'WIFI:S:' + escapeSpecialChars(wifiSsid) + ';T:nopass;;';
}
toggleClearButton();
}
function updateShareHint() {
const mode = contentModeSelect.value;
const wifiPassword = wifiPasswordInput.value.trim();
if (mode === 'wifi' && wifiPassword) {
shareHint.style.display = 'block';
} else {
shareHint.style.display = 'none';
}
}
wifiPasswordInput.addEventListener('input', updateShareHint);
wifiSsidInput.addEventListener('input', updateShareHint);
textInput.addEventListener('input', updateShareHint);
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();
const size = sizeSelect.value;
const errorCorrection = errorCorrectionSelect.value;
const fgColor = foregroundColor.value;
const bgColor = backgroundColor.value;
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();
navigator.clipboard.writeText(url).then(function() {
const original = shareBtn.textContent;
shareBtn.textContent = 'Link kopiert!';
setTimeout(function() { shareBtn.textContent = original; }, 2000);
}, function() {
alert('Konnte Link nicht kopieren.');
});
});
});