Files
QRForge-selfhosted/src/dist/js/qrcode-style-tools.js
T
dillard c3c6f167e0 Fase 3 priority 2: presets, random style, qr scanner, PWA
Preset system: qr_presets table (migration 005) plus a presets.php AJAX
endpoint (list/save/delete, CSRF-protected, scoped to the logged-in user's
own id - presets are personal, never shared across accounts). UI/JS lives in
dist/js/qrcode-style-tools.js.

Random style button: client-side only, fills foreground/background with a
random hex color pair (playful randomize, no contrast/scannability
guarantee).

Qr scanner (scan_qrcode.php): camera or image upload, decoded entirely
client-side via html5-qrcode (CDN, pinned to 2.3.8).

PWA: manifest.json + service-worker.js, icons generated from the existing
DynamicQRCode_Original.png glyph. The service worker only caches static
assets (css/js/images) and deliberately never touches PHP pages, since those
carry CSRF tokens and session-specific content that must never be cached.

Fixes a gap found while testing: qrcode_options.php is only a shared partial
for the static qr forms - the dynamic qr form (form_dynamic_add.php) has its
own separate copy of the foreground/background/level/size/filename/format
fields (pre-existing structure, not something introduced here). That meant
frame_text and the new preset/random-style UI never showed up on the
dynamic qr page. Added the same fields there too, verified with a dynamic qr
plus frame text (150x180px, the expected +30px padding).
2026-07-09 00:49:45 +02:00

170 lines
6.6 KiB
JavaScript

// Preset system + random style button for the qr code generation forms (Fase 3, priority 2).
(function () {
function csrfToken() {
var meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute('content') : '';
}
function setColor(id, hex) {
var input = document.getElementById(id);
if (!input) {
return;
}
input.value = hex;
input.dispatchEvent(new Event('change'));
try {
// Sync the bootstrap-colorpicker widget/swatch if it was initialized on this input.
if (window.jQuery) {
jQuery(input).colorpicker('setValue', hex);
}
} catch (e) {
// Colorpicker not initialized on this page - the raw value above is still correct.
}
}
function randomHexColor() {
var value = Math.floor(Math.random() * 0xFFFFFF).toString(16);
return '#' + ('000000' + value).slice(-6);
}
function loadPresets() {
var select = document.getElementById('preset_select');
if (!select) {
return;
}
fetch('presets.php?action=list')
.then(function (response) { return response.json(); })
.then(function (json) {
if (json.status !== 200) {
return;
}
json.data.forEach(function (preset) {
var option = document.createElement('option');
option.value = preset.id;
option.textContent = preset.name;
option.dataset.foreground = preset.foreground;
option.dataset.background = preset.background;
option.dataset.level = preset.level;
option.dataset.size = preset.size;
select.appendChild(option);
});
})
.catch(function () { /* presets are a nice-to-have, fail silently */ });
}
document.addEventListener('DOMContentLoaded', function () {
loadPresets();
var randomBtn = document.getElementById('random_style_btn');
if (randomBtn) {
randomBtn.addEventListener('click', function () {
setColor('foreground', randomHexColor());
setColor('background', randomHexColor());
});
}
var presetSelect = document.getElementById('preset_select');
if (presetSelect) {
presetSelect.addEventListener('change', function () {
var option = presetSelect.options[presetSelect.selectedIndex];
if (!option.value) {
return;
}
setColor('foreground', option.dataset.foreground);
setColor('background', option.dataset.background);
var levelSelect = document.querySelector('select[name="level"]');
if (levelSelect) {
levelSelect.value = option.dataset.level;
}
var sizeSelect = document.getElementById('size');
if (sizeSelect) {
sizeSelect.value = option.dataset.size;
}
});
}
var saveBtn = document.getElementById('preset_save_btn');
if (saveBtn) {
saveBtn.addEventListener('click', function () {
var nameInput = document.getElementById('preset_name');
var name = nameInput.value.trim();
if (!name) {
alert('Enter a name for this preset first.');
return;
}
var body = new URLSearchParams();
body.set('action', 'save');
body.set('name', name);
body.set('foreground', document.getElementById('foreground').value);
body.set('background', document.getElementById('background').value);
body.set('level', document.querySelector('select[name="level"]').value);
body.set('size', document.getElementById('size').value);
fetch('presets.php', {
method: 'POST',
headers: { 'X-CSRF-Token': csrfToken() },
body: body
})
.then(function (response) { return response.json(); })
.then(function (json) {
if (json.status !== 200) {
alert('Could not save preset: ' + json.data);
return;
}
var option = document.createElement('option');
option.value = json.data.id;
option.textContent = json.data.name;
option.dataset.foreground = document.getElementById('foreground').value;
option.dataset.background = document.getElementById('background').value;
option.dataset.level = document.querySelector('select[name="level"]').value;
option.dataset.size = document.getElementById('size').value;
presetSelect.appendChild(option);
presetSelect.value = option.value;
nameInput.value = '';
})
.catch(function () { alert('Could not save preset (network error).'); });
});
}
var deleteBtn = document.getElementById('preset_delete_btn');
if (deleteBtn) {
deleteBtn.addEventListener('click', function () {
var option = presetSelect.options[presetSelect.selectedIndex];
if (!option.value) {
return;
}
if (!confirm('Delete preset "' + option.textContent + '"?')) {
return;
}
var body = new URLSearchParams();
body.set('action', 'delete');
body.set('id', option.value);
fetch('presets.php', {
method: 'POST',
headers: { 'X-CSRF-Token': csrfToken() },
body: body
})
.then(function (response) { return response.json(); })
.then(function (json) {
if (json.status === 200) {
option.remove();
} else {
alert('Could not delete preset: ' + json.data);
}
})
.catch(function () { alert('Could not delete preset (network error).'); });
});
}
});
})();