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).
This commit is contained in:
+14
@@ -89,6 +89,20 @@ CREATE TABLE IF NOT EXISTS `audit_log` (
|
|||||||
KEY `user_id` (`user_id`)
|
KEY `user_id` (`user_id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||||
|
|
||||||
|
-- Fase 3 (priority 2): saved color/style presets per user
|
||||||
|
CREATE TABLE IF NOT EXISTS `qr_presets` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int(25) NOT NULL,
|
||||||
|
`name` varchar(50) NOT NULL,
|
||||||
|
`foreground` varchar(10) NOT NULL,
|
||||||
|
`background` varchar(10) NOT NULL,
|
||||||
|
`level` varchar(1) NOT NULL DEFAULT 'L',
|
||||||
|
`size` int(10) unsigned NOT NULL DEFAULT 200,
|
||||||
|
`created_at` datetime NOT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `user_id` (`user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||||
|
|
||||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- Fase 3 (priority 2): saved color/style presets per user.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `qr_presets` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int(25) NOT NULL,
|
||||||
|
`name` varchar(50) NOT NULL,
|
||||||
|
`foreground` varchar(10) NOT NULL,
|
||||||
|
`background` varchar(10) NOT NULL,
|
||||||
|
`level` varchar(1) NOT NULL DEFAULT 'L',
|
||||||
|
`size` int(10) unsigned NOT NULL DEFAULT 200,
|
||||||
|
`created_at` datetime NOT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `user_id` (`user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Vendored
+169
@@ -0,0 +1,169 @@
|
|||||||
|
// 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).'); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
<div class="col-6 col-md-3">
|
<div class="col-6 col-md-3">
|
||||||
<label for="size">Size (px)</label>
|
<label for="size">Size (px)</label>
|
||||||
<select name="size" class="form-control">
|
<select name="size" id="size" class="form-control">
|
||||||
<option value="100">100</option>
|
<option value="100">100</option>
|
||||||
<option value="200">200</option>
|
<option value="200">200</option>
|
||||||
<option value="300">300</option>
|
<option value="300">300</option>
|
||||||
@@ -51,6 +51,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 mb-2">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<button type="button" id="random_style_btn" class="btn btn-outline-secondary btn-block">
|
||||||
|
<i class="fa fa-dice"></i> Random style
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<label for="preset_select">Load preset</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<select id="preset_select" class="form-control">
|
||||||
|
<option value="">-- Select --</option>
|
||||||
|
</select>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button type="button" id="preset_delete_btn" class="btn btn-outline-secondary" title="Delete selected preset"><i class="fa fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<label for="preset_name">Save as preset</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="preset_name" class="form-control" placeholder="Preset name" maxlength="50">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button type="button" id="preset_save_btn" class="btn btn-outline-secondary"><i class="fa fa-save"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="dist/js/qrcode-style-tools.js?nocache=<?php print rand();?>"></script>
|
||||||
|
|
||||||
<!-- Its use is not recommended. Read the documentation
|
<!-- Its use is not recommended. Read the documentation
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="logo">Logo</label>
|
<label for="logo">Logo</label>
|
||||||
@@ -94,6 +128,14 @@
|
|||||||
<option value="eps">EPS</option>
|
<option value="eps">EPS</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="frame_text">Frame text</label>
|
||||||
|
<input type="text" name="frame_text" value="" placeholder="e.g. Scan me" maxlength="60" class="form-control" id="frame_text">
|
||||||
|
<small class="form-text text-muted">Optional label rendered below the code. Only applies to PNG/JPEG/GIF, not SVG/EPS.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
<div class="col-6 col-md-3">
|
<div class="col-6 col-md-3">
|
||||||
<label for="size">Size (px)</label>
|
<label for="size">Size (px)</label>
|
||||||
<select name="size" class="form-control">
|
<select name="size" id="size" class="form-control">
|
||||||
<option value="100">100</option>
|
<option value="100">100</option>
|
||||||
<option value="200">200</option>
|
<option value="200">200</option>
|
||||||
<option value="300">300</option>
|
<option value="300">300</option>
|
||||||
@@ -55,6 +55,40 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 mb-2">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<button type="button" id="random_style_btn" class="btn btn-outline-secondary btn-block">
|
||||||
|
<i class="fa fa-dice"></i> Random style
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<label for="preset_select">Load preset</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<select id="preset_select" class="form-control">
|
||||||
|
<option value="">-- Select --</option>
|
||||||
|
</select>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button type="button" id="preset_delete_btn" class="btn btn-outline-secondary" title="Delete selected preset"><i class="fa fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<label for="preset_name">Save as preset</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="preset_name" class="form-control" placeholder="Preset name" maxlength="50">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button type="button" id="preset_save_btn" class="btn btn-outline-secondary"><i class="fa fa-save"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="dist/js/qrcode-style-tools.js?nocache=<?php print rand();?>"></script>
|
||||||
|
|
||||||
<!-- Its use is not recommended. Read the documentation
|
<!-- Its use is not recommended. Read the documentation
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="logo">Logo</label>
|
<label for="logo">Logo</label>
|
||||||
|
|||||||
@@ -29,3 +29,9 @@
|
|||||||
<script src="plugins/daterangepicker/daterangepicker.js"></script>
|
<script src="plugins/daterangepicker/daterangepicker.js"></script>
|
||||||
<!-- Overlay scrollbar -->
|
<!-- Overlay scrollbar -->
|
||||||
<script type="text/javascript" src="plugins/overlayScrollbars/js/OverlayScrollbars.js"></script>
|
<script type="text/javascript" src="plugins/overlayScrollbars/js/OverlayScrollbars.js"></script>
|
||||||
|
<!-- PWA service worker -->
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register('service-worker.js');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
<meta name="csrf-token" content="<?php echo csrf_token(); ?>">
|
<meta name="csrf-token" content="<?php echo csrf_token(); ?>">
|
||||||
|
<meta name="theme-color" content="#007bff">
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
<link rel="apple-touch-icon" href="dist/img/icon-192.png">
|
||||||
|
|
||||||
<!-- Font Awesome Icons -->
|
<!-- Font Awesome Icons -->
|
||||||
<link rel="stylesheet" href="plugins/fontawesome-free/css/all.min.css">
|
<link rel="stylesheet" href="plugins/fontawesome-free/css/all.min.css">
|
||||||
|
|||||||
@@ -32,6 +32,14 @@
|
|||||||
Dashboard
|
Dashboard
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="./scan_qrcode.php" <?php echo (CURRENT_PAGE == 'scan_qrcode.php') ? ' class="nav-link active"' : ' class="nav-link"'; ?>>
|
||||||
|
<i class="nav-icon fas fa-camera"></i>
|
||||||
|
<p>
|
||||||
|
Scan qr code
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<?php if ($_SESSION['type'] !== 'user' || !empty($_SESSION['can_view_dynamic'])): ?>
|
<?php if ($_SESSION['type'] !== 'user' || !empty($_SESSION['can_view_dynamic'])): ?>
|
||||||
<li <?php echo ((substr(CURRENT_PAGE, 0, 19) == 'dynamic_qrcodes.php') || (substr(CURRENT_PAGE, 0, 18) == 'dynamic_qrcode.php')) ? ' class="nav-item has-treeview menu-open"' : ' class="nav-item has-treeview"'; ?>>
|
<li <?php echo ((substr(CURRENT_PAGE, 0, 19) == 'dynamic_qrcodes.php') || (substr(CURRENT_PAGE, 0, 18) == 'dynamic_qrcode.php')) ? ' class="nav-item has-treeview menu-open"' : ' class="nav-item has-treeview"'; ?>>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "Qrcode Generator",
|
||||||
|
"short_name": "QRcode",
|
||||||
|
"description": "Self-hosted static and dynamic qr code generator",
|
||||||
|
"start_url": "index.php",
|
||||||
|
"scope": "./",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"theme_color": "#007bff",
|
||||||
|
"icons": [
|
||||||
|
{ "src": "dist/img/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||||
|
{ "src": "dist/img/icon-512.png", "sizes": "512x512", "type": "image/png" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AJAX endpoint for saved color/style presets (Fase 3, priority 2). Presets are
|
||||||
|
* personal: scoped to the logged-in user's own id, never shared across accounts.
|
||||||
|
*/
|
||||||
|
require_once 'includes/bootstrap.php';
|
||||||
|
require_once BASE_PATH . '/includes/auth_validate.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$action = $_GET['action'] ?? $_POST['action'] ?? '';
|
||||||
|
|
||||||
|
if ($action === 'list') {
|
||||||
|
$db = getDbInstance();
|
||||||
|
$db->where('user_id', $_SESSION['user_id']);
|
||||||
|
$db->orderBy('name', 'ASC');
|
||||||
|
$presets = $db->get('qr_presets', null, ['id', 'name', 'foreground', 'background', 'level', 'size']);
|
||||||
|
|
||||||
|
echo json_encode(['status' => 200, 'data' => $presets]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'save') {
|
||||||
|
csrf_verify_header_or_die();
|
||||||
|
|
||||||
|
$name = trim((string) ($_POST['name'] ?? ''));
|
||||||
|
$foreground = trim((string) ($_POST['foreground'] ?? ''));
|
||||||
|
$background = trim((string) ($_POST['background'] ?? ''));
|
||||||
|
$level = $_POST['level'] ?? 'L';
|
||||||
|
$size = filter_var($_POST['size'] ?? 200, FILTER_VALIDATE_INT);
|
||||||
|
|
||||||
|
if ($name === '' || strlen($name) > 50) {
|
||||||
|
echo json_encode(['status' => 400, 'data' => 'Preset name must be between 1 and 50 characters.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match('/^#?[0-9a-fA-F]{6}$/', $foreground) || !preg_match('/^#?[0-9a-fA-F]{6}$/', $background)) {
|
||||||
|
echo json_encode(['status' => 400, 'data' => 'Foreground/background must be valid hex colors.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($level, ['L', 'M', 'Q', 'H'], true)) {
|
||||||
|
$level = 'L';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($size === false) {
|
||||||
|
$size = 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = getDbInstance();
|
||||||
|
$last_id = $db->insert('qr_presets', [
|
||||||
|
'user_id' => $_SESSION['user_id'],
|
||||||
|
'name' => $name,
|
||||||
|
'foreground' => $foreground,
|
||||||
|
'background' => $background,
|
||||||
|
'level' => $level,
|
||||||
|
'size' => $size,
|
||||||
|
'created_at' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$last_id) {
|
||||||
|
echo json_encode(['status' => 500, 'data' => 'Could not save preset.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['status' => 200, 'data' => ['id' => $last_id, 'name' => $name]]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'delete') {
|
||||||
|
csrf_verify_header_or_die();
|
||||||
|
|
||||||
|
$id = filter_var($_POST['id'] ?? null, FILTER_VALIDATE_INT);
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
echo json_encode(['status' => 400, 'data' => 'Invalid preset id.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = getDbInstance();
|
||||||
|
$db->where('id', $id);
|
||||||
|
$db->where('user_id', $_SESSION['user_id']);
|
||||||
|
$deleted = $db->delete('qr_presets');
|
||||||
|
|
||||||
|
echo json_encode(['status' => $deleted ? 200 : 404, 'data' => $deleted ? 'Deleted' : 'Preset not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['status' => 400, 'data' => 'Unknown action']);
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'includes/bootstrap.php';
|
||||||
|
require_once BASE_PATH . '/includes/auth_validate.php';
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<title>Qrcode Generator</title>
|
||||||
|
<head>
|
||||||
|
<?php include './includes/head.php'; ?>
|
||||||
|
</head>
|
||||||
|
<body class="hold-transition sidebar-mini layout-fixed layout-navbar-fixed layout-footer-fixed">
|
||||||
|
<div class="wrapper">
|
||||||
|
<!-- Navbar -->
|
||||||
|
<?php include './includes/navbar.php'; ?>
|
||||||
|
<!-- /.navbar -->
|
||||||
|
|
||||||
|
<!-- Main Sidebar Container -->
|
||||||
|
<?php include './includes/sidebar.php'; ?>
|
||||||
|
<!-- /.Main Sidebar Container -->
|
||||||
|
|
||||||
|
<!-- Content Wrapper. Contains page content -->
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<!-- Content Header (Page header) -->
|
||||||
|
<div class="content-header">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h1 class="m-0 text-dark">Scan a qr code</h1>
|
||||||
|
</div><!-- /.col -->
|
||||||
|
</div><!-- /.row -->
|
||||||
|
</div><!-- /.container-fluid -->
|
||||||
|
</div>
|
||||||
|
<!-- /.content-header -->
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="card card-primary">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Scan from camera or upload an image</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted">Decoding happens entirely in your browser - no image is uploaded to the server.</p>
|
||||||
|
|
||||||
|
<button type="button" id="start_camera_btn" class="btn btn-primary mb-3">
|
||||||
|
<i class="fa fa-camera"></i> Start camera
|
||||||
|
</button>
|
||||||
|
<button type="button" id="stop_camera_btn" class="btn btn-secondary mb-3" style="display:none;">
|
||||||
|
<i class="fa fa-stop"></i> Stop camera
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="camera_reader" style="max-width: 500px;"></div>
|
||||||
|
|
||||||
|
<div class="form-group mt-3">
|
||||||
|
<label for="qr_image_input">...or upload an image</label>
|
||||||
|
<input type="file" id="qr_image_input" accept="image/*" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="scan_result_wrapper" class="mt-3" style="display:none;">
|
||||||
|
<label>Decoded content</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="scan_result" class="form-control" readonly>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button type="button" id="copy_result_btn" class="btn btn-outline-secondary"><i class="fa fa-copy"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!--/. container-fluid -->
|
||||||
|
</section><!-- /.content -->
|
||||||
|
</div><!-- /.content-wrapper -->
|
||||||
|
|
||||||
|
<!-- Footer and scripts -->
|
||||||
|
<?php include './includes/footer.php'; ?>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var resultInput = document.getElementById('scan_result');
|
||||||
|
var resultWrapper = document.getElementById('scan_result_wrapper');
|
||||||
|
var html5QrCode = new Html5Qrcode('camera_reader');
|
||||||
|
var cameraRunning = false;
|
||||||
|
|
||||||
|
function showResult(text) {
|
||||||
|
resultInput.value = text;
|
||||||
|
resultWrapper.style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('start_camera_btn').addEventListener('click', function () {
|
||||||
|
var startBtn = this;
|
||||||
|
var stopBtn = document.getElementById('stop_camera_btn');
|
||||||
|
|
||||||
|
html5QrCode.start(
|
||||||
|
{ facingMode: 'environment' },
|
||||||
|
{ fps: 10, qrbox: 250 },
|
||||||
|
function (decodedText) {
|
||||||
|
showResult(decodedText);
|
||||||
|
}
|
||||||
|
).then(function () {
|
||||||
|
cameraRunning = true;
|
||||||
|
startBtn.style.display = 'none';
|
||||||
|
stopBtn.style.display = '';
|
||||||
|
}).catch(function (err) {
|
||||||
|
alert('Could not start the camera: ' + err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('stop_camera_btn').addEventListener('click', function () {
|
||||||
|
var startBtn = document.getElementById('start_camera_btn');
|
||||||
|
var stopBtn = this;
|
||||||
|
|
||||||
|
if (!cameraRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
html5QrCode.stop().then(function () {
|
||||||
|
cameraRunning = false;
|
||||||
|
startBtn.style.display = '';
|
||||||
|
stopBtn.style.display = 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('qr_image_input').addEventListener('change', function (event) {
|
||||||
|
var file = event.target.files[0];
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
html5QrCode.scanFile(file, false)
|
||||||
|
.then(function (decodedText) {
|
||||||
|
showResult(decodedText);
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
alert('Could not find a qr code in this image: ' + err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('copy_result_btn').addEventListener('click', function () {
|
||||||
|
navigator.clipboard.writeText(resultInput.value).catch(function () {
|
||||||
|
resultInput.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Minimal service worker: only makes the app installable and caches truly static
|
||||||
|
// assets. Deliberately never caches PHP pages or the presets/bulk_action/qrcode_image
|
||||||
|
// endpoints - those carry session-specific and CSRF-sensitive content.
|
||||||
|
const CACHE_NAME = 'qrcode-static-v1';
|
||||||
|
const STATIC_ASSET_PATTERN = /\.(css|js|png|jpg|jpeg|svg|gif|woff2?|ttf)$/;
|
||||||
|
|
||||||
|
self.addEventListener('install', function (event) {
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', function (event) {
|
||||||
|
event.waitUntil(self.clients.claim());
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', function (event) {
|
||||||
|
const url = new URL(event.request.url);
|
||||||
|
|
||||||
|
if (event.request.method !== 'GET' || url.origin !== self.location.origin || !STATIC_ASSET_PATTERN.test(url.pathname)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
caches.open(CACHE_NAME).then(function (cache) {
|
||||||
|
return cache.match(event.request).then(function (cached) {
|
||||||
|
const fetchPromise = fetch(event.request).then(function (response) {
|
||||||
|
cache.put(event.request, response.clone());
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
return cached || fetchPromise;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user