Fase 3 feedback round: style preview, icon upload, font choice, optional bitcoin amount, WPA3

- Default the Owner select to the creator's own account instead of "All", so
  superadmin-created codes no longer silently become visible to every admin
  (the underlying NULL-fallback sharing behavior for an explicit "All" choice
  is unchanged).
- Add a live color/precision/size preview swatch next to the preset picker.
- Let the frame text use a chosen DejaVu font + font size instead of a fixed
  GD bitmap font.
- Add an optional self-uploaded icon rendered above the qr code (not embedded
  in it, so scanability is unaffected).
- Make the Bitcoin qr amount optional; a standing wallet address is useful
  without forcing a one-off amount per code.
- Add a WPA3 option to the WiFi qr encryption select.
- Fix a real bug surfaced while testing the preview/style JS: qrcode_options.php
  was included once per static qr type (16 times on one page) and each
  inclusion re-executed <script src="qrcode-style-tools.js">, so every button
  click fired once per type - e.g. saving one preset wrote 16 duplicate rows,
  and every tab except the first ("Text") had dead random-style/preset
  buttons since only the first DOM match ever got a listener. Moved the
  script include to load once per page and rewrote the JS to scope every
  lookup to the triggering element's own tab-pane/form instead of relying on
  getElementById's first-match behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0196bLhZhuxwK3MkHuKLe5WB
This commit is contained in:
2026-07-09 23:11:36 +02:00
parent be9164850a
commit 9cdfbe7a10
12 changed files with 534 additions and 77 deletions
+121 -41
View File
@@ -1,12 +1,23 @@
// Preset system + random style button for the qr code generation forms (Fase 3, priority 2).
//
// The static qr "add" page stacks all qr-type forms (text/email/.../wifi/bitcoin/...) into
// the DOM at once as Bootstrap tab-panes, and every one of them repeats the same element ids
// (foreground, background, size, random_style_btn, ...). getElementById/getElementsBy* only
// ever finds the *first* of those (the "Text" tab), so every helper below is scoped to the
// specific tab-pane/form the triggering element lives in, and wired up via querySelectorAll
// so every tab gets working listeners, not just the first one.
(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);
function scopeOf(el) {
return el.closest('.tab-pane') || el.closest('form') || document;
}
function setColor(scope, id, hex) {
var input = scope.querySelector('#' + id);
if (!input) {
return;
}
@@ -27,12 +38,35 @@
return '#' + ('000000' + value).slice(-6);
}
function loadPresets() {
var select = document.getElementById('preset_select');
if (!select) {
function updateStylePreview(scope) {
var swatch = scope.querySelector('#style_preview_swatch');
var text = scope.querySelector('#style_preview_text');
if (!swatch || !text) {
return;
}
var foreground = scope.querySelector('#foreground');
var background = scope.querySelector('#background');
var levelSelect = scope.querySelector('select[name="level"]');
var sizeSelect = scope.querySelector('#size');
var fg = foreground ? foreground.value : '#000000';
var bg = background ? background.value : '#ffffff';
swatch.style.background = bg;
swatch.style.borderColor = fg;
var parts = [];
if (levelSelect) {
parts.push('Precision: ' + levelSelect.value);
}
if (sizeSelect) {
parts.push('Size: ' + sizeSelect.value + 'px');
}
text.textContent = parts.join(' · ');
}
function loadPresetsInto(select) {
fetch('presets.php?action=list')
.then(function (response) { return response.json(); })
.then(function (json) {
@@ -54,43 +88,69 @@
}
document.addEventListener('DOMContentLoaded', function () {
loadPresets();
document.querySelectorAll('#preset_select').forEach(loadPresetsInto);
var randomBtn = document.getElementById('random_style_btn');
if (randomBtn) {
randomBtn.addEventListener('click', function () {
setColor('foreground', randomHexColor());
setColor('background', randomHexColor());
document.querySelectorAll('#style_preview').forEach(function (row) {
updateStylePreview(scopeOf(row));
});
document.querySelectorAll('#foreground, #background').forEach(function (input) {
input.addEventListener('change', function () {
updateStylePreview(scopeOf(input));
});
}
});
var presetSelect = document.getElementById('preset_select');
if (presetSelect) {
document.querySelectorAll('select[name="level"]').forEach(function (select) {
select.addEventListener('change', function () {
updateStylePreview(scopeOf(select));
});
});
document.querySelectorAll('#size').forEach(function (select) {
select.addEventListener('change', function () {
updateStylePreview(scopeOf(select));
});
});
document.querySelectorAll('#random_style_btn').forEach(function (randomBtn) {
randomBtn.addEventListener('click', function () {
var scope = scopeOf(randomBtn);
setColor(scope, 'foreground', randomHexColor());
setColor(scope, 'background', randomHexColor());
updateStylePreview(scope);
});
});
document.querySelectorAll('#preset_select').forEach(function (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 scope = scopeOf(presetSelect);
var levelSelect = document.querySelector('select[name="level"]');
setColor(scope, 'foreground', option.dataset.foreground);
setColor(scope, 'background', option.dataset.background);
var levelSelect = scope.querySelector('select[name="level"]');
if (levelSelect) {
levelSelect.value = option.dataset.level;
}
var sizeSelect = document.getElementById('size');
var sizeSelect = scope.querySelector('#size');
if (sizeSelect) {
sizeSelect.value = option.dataset.size;
}
});
}
var saveBtn = document.getElementById('preset_save_btn');
if (saveBtn) {
updateStylePreview(scope);
});
});
document.querySelectorAll('#preset_save_btn').forEach(function (saveBtn) {
saveBtn.addEventListener('click', function () {
var nameInput = document.getElementById('preset_name');
var scope = scopeOf(saveBtn);
var nameInput = scope.querySelector('#preset_name');
var name = nameInput.value.trim();
if (!name) {
@@ -98,13 +158,18 @@
return;
}
var foreground = scope.querySelector('#foreground').value;
var background = scope.querySelector('#background').value;
var level = scope.querySelector('select[name="level"]').value;
var size = scope.querySelector('#size').value;
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);
body.set('foreground', foreground);
body.set('background', background);
body.set('level', level);
body.set('size', size);
fetch('presets.php', {
method: 'POST',
@@ -118,24 +183,33 @@
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;
// The preset list is shared across every tab, so mirror the new
// option into every preset_select on the page, not just this one.
document.querySelectorAll('#preset_select').forEach(function (select) {
var option = document.createElement('option');
option.value = json.data.id;
option.textContent = json.data.name;
option.dataset.foreground = foreground;
option.dataset.background = background;
option.dataset.level = level;
option.dataset.size = size;
select.appendChild(option);
if (select === scope.querySelector('#preset_select')) {
select.value = option.value;
}
});
nameInput.value = '';
})
.catch(function () { alert('Could not save preset (network error).'); });
});
}
});
var deleteBtn = document.getElementById('preset_delete_btn');
if (deleteBtn) {
document.querySelectorAll('#preset_delete_btn').forEach(function (deleteBtn) {
deleteBtn.addEventListener('click', function () {
var scope = scopeOf(deleteBtn);
var presetSelect = scope.querySelector('#preset_select');
var option = presetSelect.options[presetSelect.selectedIndex];
if (!option.value) {
return;
@@ -157,13 +231,19 @@
.then(function (response) { return response.json(); })
.then(function (json) {
if (json.status === 200) {
option.remove();
// Remove the matching option from every preset_select on the page.
document.querySelectorAll('#preset_select').forEach(function (select) {
var match = select.querySelector('option[value="' + option.value + '"]');
if (match) {
match.remove();
}
});
} else {
alert('Could not delete preset: ' + json.data);
}
})
.catch(function () { alert('Could not delete preset (network error).'); });
});
}
});
});
})();