Files
sidak/admin/scanner_modal.php

815 lines
32 KiB
PHP

<!-- Modal Scanner -->
<div class="modal fade" id="modalScanner" tabindex="-1" role="dialog" aria-labelledby="modalScannerLabel" aria-hidden="true" data-backdrop="static">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalScannerLabel"><i class="fas fa-expand"></i> Smart Scanner</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body text-center bg-dark p-0" style="position: relative; overflow: hidden; height: 80vh;">
<div id="scanner-container" style="position: relative; margin: auto; display: inline-block;">
<canvas id="canvas-image" style="position: absolute; left: 0; top: 0; z-index: 1;"></canvas>
<canvas id="canvas-overlay" style="position: absolute; left: 0; top: 0; z-index: 2; cursor: crosshair;"></canvas>
</div>
<div id="scanner-loading" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); color: white; display: none;">
<i class="fas fa-spinner fa-spin fa-3x"></i><br>Detecting Document...
</div>
<!-- Mobile Help Tips -->
<div id="mobile-help" class="d-none d-md-none d-lg-none" style="position: absolute; bottom: 10px; left: 0; right: 0; text-align: center; color: white; background: rgba(0,0,0,0.7); padding: 5px; font-size: 12px;">
<span>📍 Sentuh & geser titik biru untuk atur sudut dokumen</span>
</div>
</div>
<div class="modal-footer justify-content-between">
<div>
<button type="button" class="btn btn-secondary" id="btnScanRotateLeft" title="Putar Kiri (-90°)"><i class="fas fa-undo"></i></button>
<button type="button" class="btn btn-secondary" id="btnScanRotateRight" title="Putar Kanan (+90°)"><i class="fas fa-redo"></i></button>
<button type="button" class="btn btn-warning" id="btnScanReset"><i class="fas fa-sync"></i> Reset Sudut</button>
</div>
<div>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
<button type="button" class="btn btn-primary" id="btnScanSave"><i class="fas fa-check"></i> Simpan Hasil Scan</button>
</div>
</div>
</div>
</div>
</div>
<style>
/* Disable all transitions and hover effects for scanner modal */
#modalScanner .card,
#modalScanner .modal-content,
#modalScanner .modal-body,
#modalScanner .modal-header,
#modalScanner .modal-footer,
#modalScanner .modal-dialog,
#modalScanner #scanner-container,
#modalScanner #scanner-container * {
transition: none !important;
}
#modalScanner .card:hover,
#modalScanner .modal-content:hover,
#modalScanner .modal-body:hover,
#modalScanner .modal-header:hover,
#modalScanner .modal-footer:hover,
#modalScanner .modal-dialog:hover,
#modalScanner #scanner-container:hover,
#modalScanner #scanner-container *:hover {
transform: none !important;
}
/* Ensure canvas is fully visible */
#canvas-image {
opacity: 1 !important;
filter: none !important;
background-color: white !important;
}
#canvas-overlay {
opacity: 1 !important;
filter: none !important;
}
/* Prevent canvas from moving on hover */
#canvas-image:hover,
#canvas-overlay:hover {
transform: none !important;
}
/* Prevent modal dragging and improve touch handling on mobile */
#modalScanner .modal-dialog,
#modalScanner .modal-content,
#modalScanner .modal-header,
#modalScanner .modal-footer {
touch-action: none !important; /* Prevent browser touch gestures (pan, zoom, swipe) */
user-select: none !important; /* Prevent text selection during drag */
-webkit-user-select: none !important;
-webkit-touch-callout: none !important;
}
/* Allow touch interaction only on canvas and buttons */
#modalScanner .modal-body,
#canvas-image,
#canvas-overlay {
touch-action: manipulation !important; /* Allow pinch-zoom and pan on canvas only */
}
/* Prevent modal backdrop from responding to touch */
.modal-backdrop {
touch-action: none !important;
}
/* Lock modal position on mobile */
@media (max-width: 768px) {
#modalScanner {
padding-right: 0 !important; /* Prevent shift from scrollbar */
}
#modalScanner .modal-dialog {
margin: 0 !important;
max-height: 100vh !important;
height: 100vh !important;
width: 100vw !important;
max-width: 100vw !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
transform: none !important;
transition: none !important;
}
#modalScanner .modal-content {
border-radius: 0 !important;
height: 100vh !important;
max-height: 100vh !important;
width: 100vw !important;
max-width: 100vw !important;
overflow: hidden !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
transform: none !important;
transition: none !important;
}
#modalScanner .modal-body {
height: calc(100vh - 120px) !important; /* Account for header and footer */
overflow: hidden !important;
}
/* Prevent any modal movement */
.modal-open #modalScanner {
overflow: hidden !important;
}
/* Larger touch target for close button on mobile */
#modalScanner .modal-header .close {
padding: 20px !important;
font-size: 2rem !important;
line-height: 1 !important;
margin: -10px -10px -10px auto !important;
}
/* Larger touch targets for all buttons */
#modalScanner .modal-footer .btn {
min-height: 44px !important;
min-width: 44px !important;
padding: 10px 15px !important;
font-size: 16px !important; /* Prevent zoom on iOS */
}
/* Larger corner points for touch */
#canvas-overlay {
touch-action: manipulation;
}
/* Help text */
#mobile-help {
font-size: 14px !important;
padding: 10px !important;
}
}
/* Larger hit area for corner points for touch devices */
#canvas-overlay {
touch-action: pinch-zoom;
}
</style>
<script>
window.addEventListener('load', function() {
// Function to load scanner libraries dynamically
function loadScannerLibs() {
return new Promise(function(resolve, reject) {
// Check if both libraries are already loaded AND OpenCV runtime is ready
if (typeof cv !== 'undefined' && cv.Mat && typeof jscanify !== 'undefined') {
resolve();
return;
}
// Load OpenCV first (jscanify depends on it)
function loadOpenCV() {
return new Promise(function(resolveOpenCV, rejectOpenCV) {
if (typeof cv !== 'undefined') {
// OpenCV already loaded, check if runtime ready
if (cv.Mat) {
resolveOpenCV();
} else if (cv.onRuntimeInitialized) {
// Wait for runtime initialization
var existingCallback = cv.onRuntimeInitialized;
cv.onRuntimeInitialized = function() {
if (typeof existingCallback === 'function') {
existingCallback();
}
resolveOpenCV();
};
} else {
// OpenCV loaded but no runtime callback mechanism
// Poll for cv.Mat
var checkInterval = setInterval(function() {
if (cv.Mat) {
clearInterval(checkInterval);
resolveOpenCV();
}
}, 100);
// Timeout after 10 seconds
setTimeout(function() {
clearInterval(checkInterval);
rejectOpenCV(new Error('OpenCV runtime initialization timeout'));
}, 10000);
}
} else {
// Load OpenCV script
var opencvScript = document.createElement('script');
opencvScript.src = '../../plugins/vendor/opencv/opencv.js';
opencvScript.onload = function() {
console.log('OpenCV.js loaded dynamically');
// Wait for runtime initialization
if (cv.onRuntimeInitialized) {
var existingCallback = cv.onRuntimeInitialized;
cv.onRuntimeInitialized = function() {
if (typeof existingCallback === 'function') {
existingCallback();
}
resolveOpenCV();
};
} else {
// Fallback: wait a bit then resolve
setTimeout(resolveOpenCV, 5000);
}
};
opencvScript.onerror = function() {
console.error('Failed to load OpenCV.js');
rejectOpenCV(new Error('Failed to load OpenCV.js'));
};
document.head.appendChild(opencvScript);
}
});
}
// Load jscanify after OpenCV is ready
function loadJscanify() {
return new Promise(function(resolveJscanify, rejectJscanify) {
if (typeof jscanify !== 'undefined') {
resolveJscanify();
return;
}
var jscanifyScript = document.createElement('script');
jscanifyScript.src = '../../plugins/vendor/jscanify/jscanify.min.js';
jscanifyScript.onload = function() {
console.log('jscanify loaded dynamically');
resolveJscanify();
};
jscanifyScript.onerror = function() {
console.error('Failed to load jscanify');
rejectJscanify(new Error('Failed to load jscanify'));
};
document.head.appendChild(jscanifyScript);
});
}
// Load all libraries in sequence
loadOpenCV()
.then(function() {
console.log('OpenCV ready, loading jscanify...');
return loadJscanify();
})
.then(function() {
console.log('All scanner libraries loaded');
resolve();
})
.catch(function(error) {
console.error('Failed to load scanner libraries:', error);
reject(error);
});
});
}
// Load libraries and then initialize scanner
loadScannerLibs().then(function() {
// Give OpenCV a moment to initialize runtime
setTimeout(function() {
// Scanner Variables
var scannerModal = $('#modalScanner');
var canvasImage = document.getElementById('canvas-image');
var canvasOverlay = document.getElementById('canvas-overlay');
var ctxImg = canvasImage.getContext('2d');
var ctxOver = canvasOverlay.getContext('2d');
const IS_TOUCH_DEVICE = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
var scanner = null;
function initScanner() {
// Check if OpenCV is loaded
if (typeof cv === 'undefined') {
console.warn('OpenCV not loaded yet');
return false;
}
// Check if OpenCV runtime is already initialized
if (cv.Mat) {
try {
scanner = new jscanify();
console.log('Scanner initialized successfully (runtime ready)');
return true;
} catch (e) {
console.error('Failed to initialize jscanify:', e);
return false;
}
}
// Runtime not initialized yet, set up callback
if (cv.onRuntimeInitialized) {
// Save any existing callback
var existingCallback = cv.onRuntimeInitialized;
cv.onRuntimeInitialized = function() {
// Call existing callback first if it's a function
if (typeof existingCallback === 'function') {
existingCallback();
}
// Now initialize scanner
try {
scanner = new jscanify();
console.log('Scanner initialized after runtime ready (callback)');
} catch (e) {
console.error('Failed to initialize jscanify after runtime:', e);
}
};
console.log('Waiting for OpenCV runtime initialization...');
return false;
}
// Should not reach here
console.warn('OpenCV loaded but no runtime initialization mechanism');
return false;
}
// Try to initialize scanner immediately
initScanner();
var originalImg = new Image();
// Mode is always smart scan now
var currentMode = 'smart';
// Corner Points (tl, tr, bl, br)
var corners = [];
var activePoint = null;
var isDragging = false;
var touchStartPos = null;
var isTouchInteraction = false;
var touchOffset = null;
// Config - Extra large circles for easy mobile touch, thin lines
const POINT_RADIUS = 50; // Extra large circle radius for easy mobile touch
const POINT_COLOR = 'rgba(0, 123, 255, 0.15)'; // Transparent blue (15% opacity)
const POINT_OUTLINE_COLOR = '#007bff'; // Blue outline
const POINT_OUTLINE_WIDTH = 2; // Thin outline
const LINE_COLOR = 'rgba(0, 255, 0, 0.8)'; // Semi-transparent green lines
const LINE_WIDTH = 1.5; // Very thin lines
const TOUCH_RADIUS_MULTIPLIER = 3.5; // Extra large hit area for touch devices
const TOUCH_SLOP = 5; // pixels threshold before dragging starts
// Mode is always smart scan (no toggle needed)
// --- Public Function to Open Scanner ---
window.openScanner = function(file) {
if (!file) return;
// Reset State
corners = [];
activePoint = null;
isDragging = false;
// Show Loading
$('#scanner-loading').show();
// Logic: Wait for BOTH Image Load AND Modal Shown
var imgLoaded = false;
var modalShown = false;
function checkReady() {
if (imgLoaded && modalShown) {
setupScannerCanvas();
}
}
// 1. Load Image
var reader = new FileReader();
reader.onload = function(e) {
originalImg.onload = function() {
imgLoaded = true;
checkReady();
};
originalImg.src = e.target.result;
};
reader.readAsDataURL(file);
// 2. Show Modal & Listen
scannerModal.off('shown.bs.modal'); // Remove old listeners
scannerModal.on('shown.bs.modal', function() {
modalShown = true;
checkReady();
// Prevent body scrolling on mobile
document.body.style.overflow = 'hidden';
document.body.style.position = 'fixed';
document.body.style.width = '100%';
// Prevent modal dragging on mobile
var modalDialog = document.querySelector('#modalScanner .modal-dialog');
var modalContent = document.querySelector('#modalScanner .modal-content');
var modalHeader = document.querySelector('#modalScanner .modal-header');
var modalFooter = document.querySelector('#modalScanner .modal-footer');
var preventTouch = function(e) {
// Allow touch on canvas elements and buttons
var target = e.target;
var isCanvas = target.id === 'canvas-overlay' || target.id === 'canvas-image' ||
target.closest('#scanner-container');
var isButton = target.tagName === 'BUTTON' || target.closest('button');
if (!isCanvas && !isButton) {
e.preventDefault();
e.stopPropagation();
}
};
if (modalDialog) {
modalDialog.addEventListener('touchstart', preventTouch, { passive: false });
modalDialog.addEventListener('touchmove', preventTouch, { passive: false });
}
if (modalContent) {
modalContent.addEventListener('touchstart', preventTouch, { passive: false });
modalContent.addEventListener('touchmove', preventTouch, { passive: false });
}
if (modalHeader) {
modalHeader.addEventListener('touchstart', preventTouch, { passive: false });
modalHeader.addEventListener('touchmove', preventTouch, { passive: false });
}
if (modalFooter) {
modalFooter.addEventListener('touchstart', preventTouch, { passive: false });
modalFooter.addEventListener('touchmove', preventTouch, { passive: false });
}
});
// Restore scrolling when modal is hidden
scannerModal.on('hidden.bs.modal', function() {
document.body.style.overflow = '';
document.body.style.position = '';
document.body.style.width = '';
});
scannerModal.modal('show');
};
function detectDocument(scale, w, h, showAlert = false) {
// Detect Contour using jscanify
try {
// jscanify expects an image element, we can pass originalImg but we need to map coordinates
// Wait, jscanify uses OpenCV which might not be ready.
if (typeof cv !== 'undefined' && cv.Mat) {
// We need to work on the original image for detection, then scale points
var contour = scanner ? scanner.findPaper(originalImg) : null;
// contour returns { topLeftCorner, topRightCorner, bottomLeftCorner, bottomRightCorner } each {x, y}
if (contour) {
// Order: TL, TR, BR, BL (clockwise)
corners = [
{ x: contour.topLeftCorner.x * scale, y: contour.topLeftCorner.y * scale },
{ x: contour.topRightCorner.x * scale, y: contour.topRightCorner.y * scale },
{ x: contour.bottomRightCorner.x * scale, y: contour.bottomRightCorner.y * scale },
{ x: contour.bottomLeftCorner.x * scale, y: contour.bottomLeftCorner.y * scale }
];
} else {
defaultCorners(w, h);
if (showAlert) {
Swal.fire({
icon: 'warning',
title: 'Deteksi Gagal',
text: 'Dokumen tidak terdeteksi. Silakan atur sudut secara manual.',
confirmButtonText: 'OK'
});
}
}
} else {
console.warn("OpenCV not ready yet");
defaultCorners(w, h);
if (showAlert) {
Swal.fire({
icon: 'warning',
title: 'Scanner Tidak Siap',
text: 'OpenCV belum siap. Silakan atur sudut secara manual.',
confirmButtonText: 'OK'
});
}
}
} catch(e) {
console.error("Scanner Error:", e);
defaultCorners(w, h); // Fallback
if (showAlert) {
Swal.fire({
icon: 'error',
title: 'Error Scanner',
text: 'Terjadi kesalahan saat mendeteksi dokumen: ' + e.message,
confirmButtonText: 'OK'
});
}
}
drawOverlay();
}
function setupScannerCanvas() {
// Resize Canvas to fit screen but keep aspect ratio
var maxWidth = $('#modalScanner .modal-body').width() - 20;
var maxHeight = $('#modalScanner .modal-body').height() - 20;
var scale = Math.min(maxWidth / originalImg.width, maxHeight / originalImg.height);
var w = originalImg.width * scale;
var h = originalImg.height * scale;
canvasImage.width = w;
canvasImage.height = h;
canvasOverlay.width = w;
canvasOverlay.height = h;
// Resize container
$('#scanner-container').css({ width: w, height: h, marginTop: '10px' });
// Draw Image
ctxImg.drawImage(originalImg, 0, 0, w, h);
detectDocument(scale, w, h, false);
$('#scanner-loading').hide();
// No cropper needed - smart scan only
}
function defaultCorners(w, h) {
// Default 20% margin
var mX = w * 0.1;
var mY = h * 0.1;
corners = [
{ x: mX, y: mY }, // TL
{ x: w - mX, y: mY }, // TR
{ x: w - mX, y: h - mY }, // BR
{ x: mX, y: h - mY } // BL
];
}
function drawOverlay() {
ctxOver.clearRect(0, 0, canvasOverlay.width, canvasOverlay.height);
if (corners.length < 4) return;
// Draw thin cropping lines
ctxOver.beginPath();
ctxOver.lineWidth = LINE_WIDTH;
ctxOver.strokeStyle = LINE_COLOR;
ctxOver.moveTo(corners[0].x, corners[0].y);
ctxOver.lineTo(corners[1].x, corners[1].y);
ctxOver.lineTo(corners[2].x, corners[2].y);
ctxOver.lineTo(corners[3].x, corners[3].y);
ctxOver.closePath();
ctxOver.stroke();
// Draw transparent circles with thin outline
corners.forEach(p => {
// Draw transparent inner circle
ctxOver.beginPath();
ctxOver.arc(p.x, p.y, POINT_RADIUS, 0, Math.PI * 2);
ctxOver.fillStyle = POINT_COLOR;
ctxOver.fill();
// Draw thin blue outline
ctxOver.beginPath();
ctxOver.arc(p.x, p.y, POINT_RADIUS, 0, Math.PI * 2);
ctxOver.lineWidth = POINT_OUTLINE_WIDTH;
ctxOver.strokeStyle = POINT_OUTLINE_COLOR;
ctxOver.stroke();
});
}
// --- Mouse / Touch Interactivity ---
function getMousePos(evt) {
var rect = canvasOverlay.getBoundingClientRect();
return {
x: (evt.clientX || evt.touches[0].clientX) - rect.left,
y: (evt.clientY || evt.touches[0].clientY) - rect.top
};
}
function isInside(pos, point) {
var dx = pos.x - point.x;
var dy = pos.y - point.y;
return dx * dx + dy * dy <= POINT_RADIUS * POINT_RADIUS * 2; // Bigger hit area
}
function getClosestCorner(pos, isTouch) {
var closestIdx = -1;
var closestDist = Infinity;
var radius = POINT_RADIUS * (isTouch ? TOUCH_RADIUS_MULTIPLIER : 1.5); // Larger radius for touch
var radiusSq = radius * radius;
corners.forEach((p, i) => {
var dx = pos.x - p.x;
var dy = pos.y - p.y;
var distSq = dx * dx + dy * dy;
if (distSq < radiusSq && distSq < closestDist) {
closestDist = distSq;
closestIdx = i;
}
});
return closestIdx;
}
canvasOverlay.addEventListener('mousedown', function(e) {
handleStart(getMousePos(e), false);
e.stopPropagation();
});
canvasOverlay.addEventListener('touchstart', function(e) {
var pos = getMousePos(e);
handleStart(pos, true);
e.preventDefault();
e.stopPropagation();
}, {passive: false});
window.addEventListener('mousemove', function(e) { if(isDragging) handleMove(getMousePos(e)); }); // Window to catch drag out
canvasOverlay.addEventListener('touchmove', function(e) {
if(isDragging) {
handleMove(getMousePos(e));
e.preventDefault();
e.stopPropagation();
}
}, {passive: false});
window.addEventListener('mouseup', function() { handleEnd(); });
window.addEventListener('touchend', function() { handleEnd(); });
function handleStart(pos, isTouch = false) {
activePoint = getClosestCorner(pos, isTouch);
if (activePoint !== -1) {
isTouchInteraction = isTouch;
touchOffset = { x: pos.x - corners[activePoint].x, y: pos.y - corners[activePoint].y };
if (isTouch) {
touchStartPos = pos;
// Start dragging immediately but handle slop in handleMove
isDragging = true;
} else {
isDragging = true;
touchStartPos = null;
}
} else {
activePoint = null;
isDragging = false;
isTouchInteraction = false;
touchStartPos = null;
touchOffset = null;
}
}
function handleMove(pos) {
if (activePoint !== null) {
// Touch slop detection
if (isTouchInteraction && touchStartPos) {
var dx = pos.x - touchStartPos.x;
var dy = pos.y - touchStartPos.y;
var distSq = dx * dx + dy * dy;
if (distSq < TOUCH_SLOP * TOUCH_SLOP) {
return; // Ignore small movements until slop exceeded
}
// Slop exceeded, clear touchStartPos so we don't check again
touchStartPos = null;
}
// Apply offset to maintain relative position
if (touchOffset) {
corners[activePoint].x = pos.x - touchOffset.x;
corners[activePoint].y = pos.y - touchOffset.y;
} else {
corners[activePoint].x = pos.x;
corners[activePoint].y = pos.y;
}
// Optional: constrain to canvas bounds
var w = canvasOverlay.width;
var h = canvasOverlay.height;
corners[activePoint].x = Math.max(0, Math.min(w, corners[activePoint].x));
corners[activePoint].y = Math.max(0, Math.min(h, corners[activePoint].y));
drawOverlay();
}
}
function handleEnd() {
isDragging = false;
activePoint = null;
isTouchInteraction = false;
touchStartPos = null;
touchOffset = null;
}
// --- Rotate Functions ---
function rotateImage(degree) {
var offCanvas = document.createElement('canvas');
var offCtx = offCanvas.getContext('2d');
// Swap Width/Height for 90 degree rotation
offCanvas.width = originalImg.height;
offCanvas.height = originalImg.width;
offCtx.translate(offCanvas.width / 2, offCanvas.height / 2);
offCtx.rotate(degree * Math.PI / 180);
offCtx.drawImage(originalImg, -originalImg.width / 2, -originalImg.height / 2);
// Update originalImg
var rotatedUrl = offCanvas.toDataURL();
originalImg.onload = function() {
// Re-init scanner
setupScannerCanvas();
}
originalImg.src = rotatedUrl;
}
$('#btnScanRotateLeft').click(function() { rotateImage(-90); });
$('#btnScanRotateRight').click(function() { rotateImage(90); });
// --- Reset Button ---
$('#btnScanReset').click(function() {
setupScannerCanvas();
});
// --- Save / Extract Button ---
$('#btnScanSave').click(function() {
try {
var base64;
// Smart Scan Mode - Use jscanify
// 1. Get raw points relative to Original Image
var scaleX = originalImg.width / canvasImage.width;
var scaleY = originalImg.height / canvasImage.height;
var tl = { x: corners[0].x * scaleX, y: corners[0].y * scaleY };
var tr = { x: corners[1].x * scaleX, y: corners[1].y * scaleY };
var br = { x: corners[2].x * scaleX, y: corners[2].y * scaleY };
var bl = { x: corners[3].x * scaleX, y: corners[3].y * scaleY };
// 2. Calculate dimensions of the crop area
var widthTop = Math.hypot(tr.x - tl.x, tr.y - tl.y);
var widthBottom = Math.hypot(br.x - bl.x, br.y - bl.y);
var outputWidth = Math.max(widthTop, widthBottom);
var heightLeft = Math.hypot(bl.x - tl.x, bl.y - tl.y);
var heightRight = Math.hypot(br.x - tr.x, br.y - tr.y);
var outputHeight = Math.max(heightLeft, heightRight);
var extractPoints = {
topLeftCorner: tl,
topRightCorner: tr,
bottomRightCorner: br,
bottomLeftCorner: bl
};
// 3. Extract with dynamic dimensions
var resultCanvas = null;
if (!scanner || !scanner.extractPaper) {
// Try to initialize scanner if not ready
initScanner();
}
if (scanner && scanner.extractPaper) {
resultCanvas = scanner.extractPaper(originalImg, outputWidth, outputHeight, extractPoints);
} else {
alert("Scanner library not loaded. Please refresh the page.");
return;
}
base64 = resultCanvas.toDataURL('image/jpeg');
if (window.handleScannerResult) {
window.handleScannerResult(base64);
}
scannerModal.modal('hide');
} catch (e) {
alert("Gagal memproses gambar: " + e.message);
console.error(e);
}
});
// Show mobile help if touch device
if (IS_TOUCH_DEVICE) {
$('#mobile-help').removeClass('d-none');
}
// Mode is always smart scan now
}, 500); }); });
</script>