v9.1: OCR-Feature Bug-Fix - zusammenhängende Zahlenblöcke
extractDigitSequences() Fix: - VORHER: Alle Ziffern zusammengefasst → Datensalat bei Subsequenzen - JETZT: Nur isolierte N-stellige Zahlenblöcke (filter auf length === N) - Beispiel: "Artikel 1234567 Datum 20250311" → nur "1234567" (nicht "2025031" etc.) Artikelnummer-Suche: - findproduct.php sucht in: barcode, ref_fourn (Lieferanten-Artikelnummer!), ref - OCR-erkannte Nummern funktionieren wenn ref_fourn in Dolibarr gepflegt ist Dokumentation: - README.md aktualisiert: ZXing-JS, OCR-Feature, Versionierung - Barcode-Unterstützung erweitert: Code 128, QR, DataMatrix, OCR - Bibliotheken: ZXing-JS v0.21.3, Tesseract.js v5.1.0 Versionen synchron erhöht: - pwa.php: ?v=91 (CSS + JS) - sw.js: scanner-v9.1 - modHandyBarcodeScanner.class.php: 9.1 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8586b568e8
commit
c97540e6f6
6 changed files with 843 additions and 115 deletions
45
README.md
45
README.md
|
|
@ -50,18 +50,32 @@ Das Modul bietet drei Modi für die mobile Barcode-Erfassung:
|
|||
|
||||
## Barcode-Unterstützung
|
||||
|
||||
Das Modul sucht Barcodes in folgender Reihenfolge:
|
||||
Das Modul sucht Barcodes/Artikelnummern in folgender Reihenfolge:
|
||||
1. Produkt-Barcode (`llx_product.barcode`)
|
||||
2. Lieferanten-Barcode (`llx_product_fournisseur_price.barcode`)
|
||||
3. Produkt-Referenz (`llx_product.ref`)
|
||||
3. **Lieferanten-Artikelnummer** (`llx_product_fournisseur_price.ref_fourn`) ← **NEU ab v9.0**
|
||||
4. Produkt-Referenz (`llx_product.ref`)
|
||||
|
||||
Unterstützte Barcode-Formate (Quagga2):
|
||||
- **Code 128** (empfohlen für alphanumerische Codes, höchste Priorität)
|
||||
- **Code 39** (alphanumerisch, zweite Priorität)
|
||||
- **EAN-13** (nur Ziffern, dritte Priorität)
|
||||
- **EAN-8** (nur Ziffern, niedrigste Priorität)
|
||||
### Barcode-Formate (ZXing-JS, ab v8.3)
|
||||
|
||||
**WICHTIG:** Reader-Reihenfolge optimiert für alphanumerische Codes! CODE128/CODE39 haben Vorrang vor EAN-Readern, um Fehlerkennungen bei Herstellercodes wie "P20260030" zu vermeiden.
|
||||
**Unterstützte Formate - wählbar per Dropdown:**
|
||||
- **Code 128** - alphanumerisch (1D-Barcode)
|
||||
- **QR-Code** - 2D-Matrix-Code
|
||||
- **DataMatrix** - 2D-Matrix-Code
|
||||
|
||||
**Typ-Umschaltung ohne Kamera-Neustart:**
|
||||
Der Barcode-Typ kann während des Scannens gewechselt werden. Die Kamera bleibt aktiv, nur der Decoder wird neu initialisiert. Die Auswahl wird persistent in localStorage gespeichert.
|
||||
|
||||
### OCR-Modus für Artikelnummern (ab v8.5, optimiert in v9.1)
|
||||
|
||||
**Für Etiketten OHNE Barcode:**
|
||||
- Foto-basierte Texterkennung mit Tesseract.js
|
||||
- Variable Ziffern-Anzahl (5-12 stellig) per Dropdown wählbar
|
||||
- Erkennt **zusammenhängende** N-stellige Zahlen (z.B. 7-stellige Artikelnummer)
|
||||
- Mehrere Treffer → Auswahl-Dialog
|
||||
- Keine Treffer → Fallback auf manuelle Produktsuche
|
||||
|
||||
**Wichtig:** OCR erkennt nur **isolierte** Zahlenblöcke mit exakt N Ziffern. Keine Subsequenzen oder Zusammensetzungen aus längeren Zahlen!
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -186,8 +200,9 @@ handybarcodescanner/
|
|||
```
|
||||
|
||||
### Verwendete Bibliotheken
|
||||
- [Quagga2](https://github.com/ericblade/quagga2) - Browser-basierte Barcode-Erkennung
|
||||
- [JsBarcode](https://github.com/lindell/JsBarcode) - Barcode-Generierung für Druck
|
||||
- [ZXing-JS](https://github.com/zxing-js/library) v0.21.3 - Browser-basierte Barcode-Erkennung (1D + 2D)
|
||||
- [Tesseract.js](https://github.com/naptha/tesseract.js) v5.1.0 - OCR (Optical Character Recognition) für Artikelnummern
|
||||
- [JsBarcode](https://github.com/lindell/JsBarcode) v3.11.6 - Barcode-Generierung für Druck
|
||||
- [ZXing](https://github.com/zxing/zxing) - Barcode-Generierung in Android App
|
||||
|
||||
### PWA (Progressive Web App)
|
||||
|
|
@ -203,7 +218,15 @@ Das Modul ist als PWA konzipiert mit:
|
|||
- Firefox auf Android: Nur Shortcuts, kein echter Standalone-Modus ⚠️
|
||||
- Safari auf iOS: PWA-Unterstützung mit Einschränkungen
|
||||
|
||||
**Versionsverwaltung:** Bei Updates CACHE_NAME in `sw.js` + `?v=` in `pwa.php` synchron erhöhen!
|
||||
**Versionsverwaltung:** Bei Updates IMMER synchron erhöhen:
|
||||
- `CACHE_NAME` in `sw.js` (z.B. `scanner-v9.1`)
|
||||
- `?v=` Parameter in `pwa.php` für CSS und JS (z.B. `?v=91`)
|
||||
- `$this->version` in `modHandyBarcodeScanner.class.php` (z.B. `9.1`)
|
||||
|
||||
**Aktuelle Version: 9.1**
|
||||
- v9.1: OCR-Feature Bug-Fix (extractDigitSequences: nur zusammenhängende Zahlenblöcke)
|
||||
- v9.0: OCR-Feature für Artikelnummern-Erkennung
|
||||
- v8.3: ZXing-JS Integration (Code 128, QR, DataMatrix)
|
||||
|
||||
### Brother PT-E560BT Android App
|
||||
|
||||
|
|
|
|||
BIN
android-app/BrotherPrintHelper-v2.apk
Executable file
BIN
android-app/BrotherPrintHelper-v2.apk
Executable file
Binary file not shown.
|
|
@ -76,7 +76,7 @@ class modHandyBarcodeScanner extends DolibarrModules
|
|||
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@handybarcodescanner'
|
||||
|
||||
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
|
||||
$this->version = '8.2';
|
||||
$this->version = '9.1';
|
||||
// Url to the file with your last numberversion of this module
|
||||
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
||||
|
||||
|
|
|
|||
716
js/scanner.js
716
js/scanner.js
|
|
@ -44,9 +44,18 @@
|
|||
let currentProduct = null;
|
||||
let selectedSupplier = null;
|
||||
let allSuppliers = [];
|
||||
let quaggaInitialized = false;
|
||||
let scannerInitialized = false;
|
||||
let openDialogCount = 0; // Zaehlt offene Dialoge
|
||||
|
||||
// ZXing Scanner State
|
||||
let currentBarcodeType = 'CODE_128'; // Aktuell gewahlter Barcode-Typ
|
||||
let currentScanner = null; // ZXing BrowserMultiFormatReader instance
|
||||
|
||||
// OCR State (Tesseract.js)
|
||||
let ocrWorker = null; // Singleton Worker
|
||||
let ocrInitialized = false;
|
||||
let currentDigitCount = 8; // Anzahl Ziffern für OCR-Erkennung
|
||||
|
||||
// Persistente Einstellungen - eine zentrale Config statt einzelner Keys
|
||||
const STORAGE_KEY = 'hbs_config';
|
||||
function loadConfig() {
|
||||
|
|
@ -133,6 +142,49 @@
|
|||
window._showProductSearchModal = showProductSearchModal;
|
||||
window._showFreetextModal = showFreetextModal;
|
||||
window._showOrdersPanel = showOrdersPanel;
|
||||
|
||||
// History-Button Event-Handler
|
||||
const historyBtn = document.getElementById('ctrl-history');
|
||||
if (historyBtn) {
|
||||
historyBtn.addEventListener('click', showBarcodeHistory);
|
||||
}
|
||||
|
||||
// Barcode-Typ-Dropdown Event-Handler
|
||||
const typeSelect = document.getElementById('barcode-type-select');
|
||||
if (typeSelect) {
|
||||
// Gespeicherten Typ laden
|
||||
const savedType = loadConfig().barcodeType || 'CODE_128';
|
||||
typeSelect.value = savedType;
|
||||
currentBarcodeType = savedType;
|
||||
|
||||
// Initiale Scan-Region-Farbe setzen
|
||||
updateScanRegionColor(savedType);
|
||||
|
||||
// OCR-Digit-Selector initial ein/ausblenden
|
||||
const digitSelector = document.getElementById('ocr-digit-selector');
|
||||
if (digitSelector) {
|
||||
digitSelector.classList.toggle('hidden', savedType !== 'OCR');
|
||||
}
|
||||
|
||||
// Event-Listener fuer Typ-Wechsel
|
||||
typeSelect.addEventListener('change', function() {
|
||||
switchBarcodeType(this.value);
|
||||
});
|
||||
}
|
||||
|
||||
// OCR Digit-Count Dropdown Handler
|
||||
const digitSelect = document.getElementById('ocr-digit-count');
|
||||
if (digitSelect) {
|
||||
const savedCount = loadConfig().ocrDigitCount || 8;
|
||||
digitSelect.value = savedCount;
|
||||
currentDigitCount = savedCount;
|
||||
|
||||
digitSelect.addEventListener('change', function() {
|
||||
currentDigitCount = parseInt(this.value);
|
||||
saveConfig('ocrDigitCount', currentDigitCount);
|
||||
showToast('OCR: ' + currentDigitCount + ' Ziffern', 'success');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check camera support - just log warnings, never disable button
|
||||
|
|
@ -143,9 +195,9 @@
|
|||
console.warn('HandyBarcodeScanner: HTTPS empfohlen fuer Kamerazugriff');
|
||||
}
|
||||
|
||||
// Check if Quagga is loaded
|
||||
if (typeof Quagga === 'undefined') {
|
||||
console.error('HandyBarcodeScanner: Quagga nicht geladen');
|
||||
// Check if ZXing is loaded
|
||||
if (typeof ZXing === 'undefined') {
|
||||
console.error('HandyBarcodeScanner: ZXing nicht geladen');
|
||||
}
|
||||
|
||||
// Check mediaDevices
|
||||
|
|
@ -224,10 +276,17 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if Quagga is available
|
||||
if (typeof Quagga === 'undefined') {
|
||||
showToast('Quagga Bibliothek nicht geladen', 'error');
|
||||
return;
|
||||
// Check if libraries are available
|
||||
if (currentBarcodeType === 'OCR') {
|
||||
if (typeof Tesseract === 'undefined') {
|
||||
showToast('Tesseract.js Bibliothek nicht geladen', 'error');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (typeof ZXing === 'undefined') {
|
||||
showToast('ZXing Bibliothek nicht geladen', 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Disable button while initializing
|
||||
|
|
@ -240,8 +299,14 @@
|
|||
// Permission granted - stop the test stream
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
|
||||
// Now start Quagga
|
||||
initQuagga();
|
||||
// OCR-Modus oder Barcode-Modus?
|
||||
if (currentBarcodeType === 'OCR') {
|
||||
// OCR-Modus: Kamera-Preview + "Foto aufnehmen" Button
|
||||
startCameraPreview();
|
||||
} else {
|
||||
// Barcode-Modus: ZXing Video-Stream
|
||||
currentScanner = initZXing(currentBarcodeType);
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error('Camera permission error:', err);
|
||||
|
|
@ -260,68 +325,63 @@
|
|||
});
|
||||
}
|
||||
|
||||
function initQuagga() {
|
||||
Quagga.init({
|
||||
inputStream: {
|
||||
name: "Live",
|
||||
type: "LiveStream",
|
||||
target: elements.videoContainer,
|
||||
constraints: {
|
||||
facingMode: "environment",
|
||||
width: { min: 640, ideal: 1280, max: 1920 },
|
||||
height: { min: 480, ideal: 720, max: 1080 }
|
||||
},
|
||||
area: { // Scan-Bereich: groesserer Bereich fuer kleine Barcodes
|
||||
top: "10%",
|
||||
right: "5%",
|
||||
left: "5%",
|
||||
bottom: "10%"
|
||||
function getBarcodeFormat(type) {
|
||||
const formats = {
|
||||
'CODE_128': ZXing.BarcodeFormat.CODE_128,
|
||||
'QR_CODE': ZXing.BarcodeFormat.QR_CODE,
|
||||
'DATA_MATRIX': ZXing.BarcodeFormat.DATA_MATRIX,
|
||||
'CODE_39': ZXing.BarcodeFormat.CODE_39,
|
||||
'EAN_13': ZXing.BarcodeFormat.EAN_13,
|
||||
'EAN_8': ZXing.BarcodeFormat.EAN_8
|
||||
};
|
||||
return formats[type] || ZXing.BarcodeFormat.CODE_128;
|
||||
}
|
||||
|
||||
function initZXing(format) {
|
||||
format = format || currentBarcodeType;
|
||||
|
||||
try {
|
||||
const hints = new Map();
|
||||
const possibleFormats = [getBarcodeFormat(format)];
|
||||
hints.set(ZXing.DecodeHintType.POSSIBLE_FORMATS, possibleFormats);
|
||||
|
||||
const codeReader = new ZXing.BrowserMultiFormatReader(hints);
|
||||
|
||||
codeReader.decodeFromVideoDevice(
|
||||
undefined, // Auto-select Kamera
|
||||
elements.video,
|
||||
(result, err) => {
|
||||
if (result) {
|
||||
onBarcodeDetected(result);
|
||||
}
|
||||
// Errors werden ignoriert (passiert bei jedem Frame ohne Code)
|
||||
}
|
||||
},
|
||||
decoder: {
|
||||
readers: [
|
||||
"code_128_reader", // Best for alphanumeric codes
|
||||
"code_39_reader", // Also good for alphanumeric (e.g. P20260030)
|
||||
"ean_reader", // Numeric only - lower priority
|
||||
"ean_8_reader" // Numeric only - lower priority
|
||||
],
|
||||
multiple: false
|
||||
},
|
||||
locate: true,
|
||||
locator: {
|
||||
patchSize: "medium", // medium = besser fuer kleine Barcodes
|
||||
halfSample: false // volle Aufloesung = bessere Genauigkeit
|
||||
},
|
||||
numOfWorkers: navigator.hardwareConcurrency || 4,
|
||||
frequency: 20 // haeufiger scannen fuer bessere Erkennung
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
console.error('Quagga init error:', err);
|
||||
).then(() => {
|
||||
// Scanner erfolgreich gestartet
|
||||
scannerInitialized = true;
|
||||
isScanning = true;
|
||||
elements.startBtn.disabled = false;
|
||||
elements.startBtn.classList.add('hidden');
|
||||
elements.stopBtn.classList.remove('hidden');
|
||||
elements.videoContainer.classList.add('scanning');
|
||||
|
||||
// Timeout-Timer starten
|
||||
startScanTimeout();
|
||||
}).catch((err) => {
|
||||
console.error('ZXing init error:', err);
|
||||
elements.startBtn.disabled = false;
|
||||
elements.startBtn.textContent = CONFIG.lang.startScan || 'Scan starten';
|
||||
showToast(CONFIG.lang.cameraError || 'Kamerafehler', 'error');
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Success - start scanning
|
||||
Quagga.start();
|
||||
quaggaInitialized = true;
|
||||
isScanning = true;
|
||||
return codeReader;
|
||||
} catch (e) {
|
||||
console.error('ZXing initialization error:', e);
|
||||
elements.startBtn.disabled = false;
|
||||
elements.startBtn.classList.add('hidden');
|
||||
elements.stopBtn.classList.remove('hidden');
|
||||
elements.videoContainer.classList.add('scanning');
|
||||
|
||||
// Quagga-Canvas darf keine Klicks abfangen
|
||||
const canvases = elements.videoContainer.querySelectorAll('canvas');
|
||||
canvases.forEach(function(c) { c.style.pointerEvents = 'none'; });
|
||||
|
||||
// Register detection handler
|
||||
Quagga.onDetected(onBarcodeDetected);
|
||||
|
||||
// Timeout-Timer starten
|
||||
startScanTimeout();
|
||||
});
|
||||
elements.startBtn.textContent = CONFIG.lang.startScan || 'Scan starten';
|
||||
showToast('Scanner konnte nicht initialisiert werden', 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Timeout-Hinweis wenn nichts erkannt wird
|
||||
|
|
@ -339,18 +399,29 @@
|
|||
function stopScanner() {
|
||||
clearTimeout(scanTimeoutTimer);
|
||||
|
||||
if (quaggaInitialized) {
|
||||
// OCR-Modus: Video-Stream manuell stoppen
|
||||
if (currentBarcodeType === 'OCR') {
|
||||
const video = elements.video;
|
||||
if (video && video.srcObject) {
|
||||
video.srcObject.getTracks().forEach(track => track.stop());
|
||||
video.srcObject = null;
|
||||
}
|
||||
// Button-Click-Handler zurücksetzen
|
||||
elements.startBtn.onclick = null;
|
||||
}
|
||||
|
||||
if (currentScanner && scannerInitialized) {
|
||||
try {
|
||||
Quagga.offDetected(onBarcodeDetected);
|
||||
Quagga.stop();
|
||||
currentScanner.reset();
|
||||
} catch (e) {
|
||||
console.log('Quagga stop error:', e);
|
||||
console.log('Scanner stop error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
isScanning = false;
|
||||
isPaused = false;
|
||||
quaggaInitialized = false;
|
||||
scannerInitialized = false;
|
||||
currentScanner = null;
|
||||
elements.startBtn.classList.remove('hidden');
|
||||
elements.startBtn.disabled = false;
|
||||
elements.startBtn.textContent = CONFIG.lang.startScan || 'Scannen';
|
||||
|
|
@ -359,19 +430,12 @@
|
|||
}
|
||||
|
||||
// Scanner pausieren (bei Dialog-Oeffnung)
|
||||
// ZXing hat kein natives pause() - wir nutzen ein Flag und ignorieren Detections
|
||||
function pauseScanner() {
|
||||
if (!isScanning || isPaused) return;
|
||||
openDialogCount++;
|
||||
|
||||
if (quaggaInitialized) {
|
||||
try {
|
||||
Quagga.pause();
|
||||
isPaused = true;
|
||||
console.log('Scanner paused, dialogs:', openDialogCount);
|
||||
} catch (e) {
|
||||
console.log('Quagga pause error:', e);
|
||||
}
|
||||
}
|
||||
isPaused = true;
|
||||
console.log('Scanner paused, dialogs:', openDialogCount);
|
||||
}
|
||||
|
||||
// Scanner fortsetzen (wenn alle Dialoge geschlossen)
|
||||
|
|
@ -385,29 +449,376 @@
|
|||
|
||||
if (!isScanning || !isPaused) return;
|
||||
|
||||
if (quaggaInitialized) {
|
||||
try {
|
||||
Quagga.start();
|
||||
isPaused = false;
|
||||
console.log('Scanner resumed');
|
||||
startScanTimeout();
|
||||
} catch (e) {
|
||||
console.log('Quagga resume error:', e);
|
||||
}
|
||||
}
|
||||
isPaused = false;
|
||||
console.log('Scanner resumed');
|
||||
startScanTimeout();
|
||||
}
|
||||
|
||||
// Globale Funktionen fuer PWA
|
||||
window._pauseScanner = pauseScanner;
|
||||
window._resumeScanner = resumeScanner;
|
||||
|
||||
// ===== OCR-Funktionen (Tesseract.js) =====
|
||||
|
||||
// Kamera-Preview starten (ohne Barcode-Detection)
|
||||
function startCameraPreview() {
|
||||
const video = elements.video;
|
||||
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: 'environment' }
|
||||
})
|
||||
.then(function(stream) {
|
||||
video.srcObject = stream;
|
||||
video.play();
|
||||
|
||||
isScanning = true;
|
||||
scannerInitialized = true;
|
||||
// OCR: Start-Button NICHT verstecken, sondern als "Foto aufnehmen" nutzen
|
||||
elements.startBtn.classList.remove('hidden');
|
||||
elements.startBtn.textContent = 'Foto aufnehmen';
|
||||
elements.stopBtn.classList.remove('hidden');
|
||||
elements.videoContainer.classList.add('scanning');
|
||||
elements.startBtn.disabled = false;
|
||||
|
||||
// Button-Click auf Snapshot-Capture umstellen
|
||||
elements.startBtn.onclick = captureOCRSnapshot;
|
||||
|
||||
console.log('OCR Kamera-Preview gestartet');
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error('OCR Camera error:', err);
|
||||
showToast('Kamerafehler: ' + err.message, 'error');
|
||||
elements.startBtn.disabled = false;
|
||||
elements.startBtn.textContent = 'Scannen';
|
||||
});
|
||||
}
|
||||
|
||||
// Foto vom Video-Stream aufnehmen und OCR durchführen
|
||||
function captureOCRSnapshot() {
|
||||
const video = elements.video;
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
// Canvas-Größe = Video-Größe
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
|
||||
// Video-Frame auf Canvas zeichnen
|
||||
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Canvas zu Blob konvertieren
|
||||
canvas.toBlob(function(blob) {
|
||||
if (blob) {
|
||||
performOCR(blob);
|
||||
} else {
|
||||
showToast('Fehler beim Erstellen des Fotos', 'error');
|
||||
}
|
||||
}, 'image/jpeg', 0.9);
|
||||
}
|
||||
|
||||
// Tesseract Worker initialisieren (Singleton, Lazy)
|
||||
async function initOCRWorker() {
|
||||
if (ocrInitialized && ocrWorker) {
|
||||
return ocrWorker;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Initialisiere Tesseract Worker...');
|
||||
ocrWorker = await Tesseract.createWorker('eng');
|
||||
|
||||
// Nur Ziffern erkennen, einfache Einstellungen
|
||||
await ocrWorker.setParameters({
|
||||
tessedit_char_whitelist: '0123456789'
|
||||
// Keine PSM/OEM - Tesseract-Defaults verwenden
|
||||
});
|
||||
|
||||
ocrInitialized = true;
|
||||
console.log('Tesseract Worker bereit');
|
||||
return ocrWorker;
|
||||
} catch (e) {
|
||||
console.error('OCR Worker Init Error:', e);
|
||||
showToast('OCR-Fehler: ' + e.message, 'error');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// OCR durchführen mit Timeout
|
||||
async function performOCR(imageBlob) {
|
||||
// Loading-Overlay anzeigen
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'ocr-loading-overlay';
|
||||
overlay.innerHTML = '<div class="ocr-loading-spinner"></div><div class="ocr-loading-text">Erkenne Text...</div>';
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
try {
|
||||
// Worker initialisieren (falls noch nicht geschehen)
|
||||
const worker = await initOCRWorker();
|
||||
|
||||
// Bild vorverarbeiten für bessere Erkennung
|
||||
const processedBlob = await preprocessImage(imageBlob);
|
||||
|
||||
// OCR mit 10s Timeout
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('OCR Timeout')), 10000)
|
||||
);
|
||||
|
||||
const ocrPromise = worker.recognize(processedBlob);
|
||||
const result = await Promise.race([ocrPromise, timeoutPromise]);
|
||||
|
||||
overlay.remove();
|
||||
|
||||
const recognizedText = result.data.text;
|
||||
console.log('OCR Result:', recognizedText);
|
||||
console.log('OCR Confidence:', result.data.confidence);
|
||||
|
||||
// DEBUG: Erkannten Text anzeigen
|
||||
showToast('OCR erkannte: "' + recognizedText.trim().substring(0, 50) + '"', 'info', 5000);
|
||||
|
||||
// Ziffern extrahieren
|
||||
const matches = extractDigitSequences(recognizedText, currentDigitCount);
|
||||
console.log('Gefundene ' + currentDigitCount + '-stellige Zahlen:', matches);
|
||||
|
||||
if (matches.length === 0) {
|
||||
// Keine Treffer
|
||||
showToast('Keine ' + currentDigitCount + '-stellige Zahl gefunden', 'error');
|
||||
playErrorBeep();
|
||||
} else if (matches.length === 1) {
|
||||
// Ein Treffer - direkt verwenden
|
||||
onOCRDetected(matches[0]);
|
||||
} else {
|
||||
// Mehrere Treffer - Auswahl anzeigen
|
||||
showOCRSelectionModal(matches);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
overlay.remove();
|
||||
console.error('OCR Error:', e);
|
||||
if (e.message === 'OCR Timeout') {
|
||||
showToast('OCR-Timeout - bitte erneut versuchen', 'error');
|
||||
} else {
|
||||
showToast('OCR-Fehler: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bild vorverarbeiten: MINIMAL - nur Grayscale
|
||||
async function preprocessImage(blob) {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
img.onload = function() {
|
||||
// Original-Größe beibehalten - keine Skalierung
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
|
||||
// Bild zeichnen
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// NUR Grayscale - kein Kontrast, kein Binary!
|
||||
// Tesseract macht das selbst besser
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
// Grayscale
|
||||
const gray = 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2];
|
||||
data[i] = data[i+1] = data[i+2] = gray;
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
// Zurück zu Blob
|
||||
canvas.toBlob(function(processedBlob) {
|
||||
resolve(processedBlob);
|
||||
}, 'image/jpeg', 0.95);
|
||||
};
|
||||
|
||||
img.src = URL.createObjectURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
// N-stellige Zahlensequenzen extrahieren - NUR zusammenhängende Zahlenblöcke mit EXAKT N Ziffern!
|
||||
function extractDigitSequences(text, digitCount) {
|
||||
// Alle zusammenhängenden Zahlenblöcke finden
|
||||
const numberBlocks = text.match(/\d+/g) || [];
|
||||
|
||||
// Nur Blöcke mit EXAKT digitCount Ziffern (keine Subsequenzen, keine Zusammensetzungen!)
|
||||
const matches = numberBlocks.filter(function(block) {
|
||||
return block.length === digitCount;
|
||||
});
|
||||
|
||||
// Duplikate entfernen
|
||||
return [...new Set(matches)];
|
||||
}
|
||||
|
||||
// Auswahl-Modal bei mehreren OCR-Treffern
|
||||
function showOCRSelectionModal(matches) {
|
||||
pauseScanner();
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'ocr-select-modal';
|
||||
modal.innerHTML = '<div class="ocr-select-content">' +
|
||||
'<div class="ocr-select-header">' +
|
||||
'<h3 style="margin:0;font-size:16px;color:var(--colortext);">Mehrere Nummern gefunden</h3>' +
|
||||
'<button type="button" class="ocr-select-close">×</button>' +
|
||||
'</div>' +
|
||||
'<div class="ocr-select-list">' +
|
||||
matches.map(function(code) {
|
||||
return '<div class="ocr-select-item" data-code="' + code + '">' + code + '</div>';
|
||||
}).join('') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
function closeModal() {
|
||||
modal.remove();
|
||||
resumeScanner();
|
||||
}
|
||||
|
||||
// Close button
|
||||
modal.querySelector('.ocr-select-close').addEventListener('click', closeModal);
|
||||
|
||||
// Item selection
|
||||
modal.querySelectorAll('.ocr-select-item').forEach(function(item) {
|
||||
item.addEventListener('click', function() {
|
||||
const code = this.getAttribute('data-code');
|
||||
modal.remove();
|
||||
onOCRDetected(code);
|
||||
resumeScanner();
|
||||
});
|
||||
});
|
||||
|
||||
// Backdrop click
|
||||
modal.addEventListener('click', function(e) {
|
||||
if (e.target === modal) closeModal();
|
||||
});
|
||||
}
|
||||
|
||||
// OCR-Ergebnis verarbeiten (analog zu onBarcodeDetected)
|
||||
function onOCRDetected(code) {
|
||||
// Ignore detections wenn Scanner pausiert ist
|
||||
if (isPaused) return;
|
||||
|
||||
const format = 'OCR';
|
||||
|
||||
// Duplicate Check
|
||||
if (lastScannedCode === code) {
|
||||
showToast('Code bereits gescannt: ' + code, 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
lastScannedCode = code;
|
||||
console.log('OCR detected:', code);
|
||||
|
||||
// Success Vibration
|
||||
if ('vibrate' in navigator) {
|
||||
navigator.vibrate([100, 50, 100]);
|
||||
}
|
||||
|
||||
// In Historie speichern
|
||||
addToHistory(format, code);
|
||||
|
||||
// Last Scan anzeigen
|
||||
const formatLabels = {
|
||||
'OCR': 'OCR'
|
||||
};
|
||||
const label = formatLabels[format] || format;
|
||||
elements.lastScanCode.innerHTML = '<span class="scan-format-badge">' + label + '</span>' +
|
||||
'<span class="scan-code-value">' + escapeHtml(code) + '</span>';
|
||||
elements.lastScanArea.classList.remove('hidden');
|
||||
|
||||
// Toast
|
||||
showToast('OCR: ' + code, 'success');
|
||||
|
||||
// Produktsuche starten
|
||||
searchProduct(code);
|
||||
}
|
||||
|
||||
// Barcode-Typ wechseln (ohne Kamera-Neustart)
|
||||
function switchBarcodeType(newType) {
|
||||
if (newType === currentBarcodeType) return;
|
||||
|
||||
console.log('Switching barcode type from', currentBarcodeType, 'to', newType);
|
||||
|
||||
// OCR-Digit-Selector ein/ausblenden
|
||||
const digitSelector = document.getElementById('ocr-digit-selector');
|
||||
if (digitSelector) {
|
||||
digitSelector.classList.toggle('hidden', newType !== 'OCR');
|
||||
}
|
||||
|
||||
// 1. Aktuellen Scanner stoppen (NICHT Kamera-Stream!)
|
||||
if (currentScanner && isScanning) {
|
||||
try {
|
||||
currentScanner.reset();
|
||||
console.log('Scanner reset for type switch');
|
||||
// Stream bleibt aktiv!
|
||||
} catch (e) {
|
||||
console.log('Scanner reset error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Format wechseln
|
||||
currentBarcodeType = newType;
|
||||
saveConfig('barcodeType', newType);
|
||||
|
||||
// 3. Scanner mit neuem Format neu starten (falls er lief)
|
||||
if (isScanning && newType !== 'OCR') {
|
||||
// Barcode-Modus: ZXing neu starten, onclick zurücksetzen
|
||||
elements.startBtn.onclick = null;
|
||||
currentScanner = initZXing(newType);
|
||||
} else if (isScanning && newType === 'OCR') {
|
||||
// OCR-Modus: Stream läuft bereits, nur Button umstellen
|
||||
elements.startBtn.classList.remove('hidden');
|
||||
elements.startBtn.textContent = 'Foto aufnehmen';
|
||||
elements.startBtn.disabled = false;
|
||||
elements.startBtn.onclick = captureOCRSnapshot;
|
||||
console.log('OCR-Modus aktiviert (Stream läuft bereits)');
|
||||
}
|
||||
|
||||
// 4. UI-Feedback
|
||||
const formatNames = {
|
||||
'CODE_128': 'Code 128',
|
||||
'QR_CODE': 'QR-Code',
|
||||
'DATA_MATRIX': 'DataMatrix',
|
||||
'OCR': 'OCR'
|
||||
};
|
||||
showToast('Barcode-Typ: ' + (formatNames[newType] || newType), 'success');
|
||||
|
||||
// Scan-Region-Farbe andern
|
||||
updateScanRegionColor(newType);
|
||||
}
|
||||
|
||||
// Scan-Region-Farbe je nach Barcode-Typ andern
|
||||
function updateScanRegionColor(type) {
|
||||
const colors = {
|
||||
'CODE_128': 'var(--primary)', // Blau (Standard)
|
||||
'QR_CODE': '#25a580', // Grun
|
||||
'DATA_MATRIX': '#bc9526', // Orange/Gelb
|
||||
'OCR': '#e74c3c' // Rot
|
||||
};
|
||||
|
||||
const region = document.querySelector('.scan-region-highlight');
|
||||
if (region) {
|
||||
region.style.borderColor = colors[type] || colors['CODE_128'];
|
||||
}
|
||||
}
|
||||
|
||||
function onBarcodeDetected(result) {
|
||||
let code = result.codeResult.code;
|
||||
const format = result.codeResult.format;
|
||||
// Ignore detections wenn Scanner pausiert ist
|
||||
if (isPaused) return;
|
||||
|
||||
// ZXing Result-Format
|
||||
let code = result.getText();
|
||||
let formatObj = result.getBarcodeFormat();
|
||||
// Format normalisieren: CODE_128, QR_CODE, DATA_MATRIX, EAN_13, EAN_8
|
||||
const format = formatObj ? formatObj.toString().toUpperCase() : 'UNKNOWN';
|
||||
|
||||
// EAN-13 detected but might be internal code with added check digit
|
||||
// If it's 13 digits and doesn't validate, try removing the last digit
|
||||
if (format === 'ean_13' || (code.length === 13 && /^\d{13}$/.test(code))) {
|
||||
if (format.includes('EAN') && format.includes('13') || (code.length === 13 && /^\d{13}$/.test(code))) {
|
||||
if (!validateEAN13(code)) {
|
||||
// Try without last digit (might be internal 12-digit code)
|
||||
const shortened = code.substring(0, 12);
|
||||
|
|
@ -417,7 +828,7 @@
|
|||
}
|
||||
|
||||
// Validate EAN8 checksum
|
||||
if (format === 'ean_8' && code.length === 8 && /^\d{8}$/.test(code)) {
|
||||
if (format.includes('EAN') && format.includes('8') && code.length === 8 && /^\d{8}$/.test(code)) {
|
||||
if (!validateEAN8(code)) {
|
||||
// Try without last digit
|
||||
code = code.substring(0, 7);
|
||||
|
|
@ -455,8 +866,27 @@
|
|||
}
|
||||
playBeep();
|
||||
|
||||
// Format-Label fuer Anzeige
|
||||
const formatLabels = {
|
||||
'CODE_128': 'Code 128',
|
||||
'CODE_39': 'Code 39',
|
||||
'QR_CODE': 'QR',
|
||||
'DATA_MATRIX': 'DataMatrix',
|
||||
'EAN_13': 'EAN-13',
|
||||
'EAN_8': 'EAN-8'
|
||||
};
|
||||
const label = formatLabels[format] || format;
|
||||
|
||||
showToast('Barcode: ' + code, 'success');
|
||||
elements.lastScanCode.textContent = code;
|
||||
|
||||
// Last Scan mit Format-Badge anzeigen
|
||||
elements.lastScanCode.innerHTML = `
|
||||
<span class="scan-format-badge">${escapeHtml(label)}</span>
|
||||
<span class="scan-code-value">${escapeHtml(code)}</span>
|
||||
`;
|
||||
|
||||
// In Historie speichern
|
||||
addToHistory(format, code);
|
||||
|
||||
// Search product
|
||||
searchProduct(code);
|
||||
|
|
@ -467,6 +897,100 @@
|
|||
}, 3000);
|
||||
}
|
||||
|
||||
// Barcode-Scan in Historie speichern (max. 10 Einträge)
|
||||
function addToHistory(format, code) {
|
||||
const cfg = loadConfig();
|
||||
let history = cfg.barcodeHistory || [];
|
||||
|
||||
// Neuen Eintrag hinzufügen
|
||||
history.unshift({
|
||||
type: format,
|
||||
code: code,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
});
|
||||
|
||||
// Maximal 10 Einträge behalten
|
||||
if (history.length > 10) {
|
||||
history = history.slice(0, 10);
|
||||
}
|
||||
|
||||
saveConfig('barcodeHistory', history);
|
||||
}
|
||||
|
||||
// Scan-Historie-Modal anzeigen
|
||||
function showBarcodeHistory() {
|
||||
pauseScanner();
|
||||
|
||||
const cfg = loadConfig();
|
||||
const history = cfg.barcodeHistory || [];
|
||||
|
||||
if (history.length === 0) {
|
||||
showToast('Noch keine Scans vorhanden', 'error');
|
||||
resumeScanner();
|
||||
return;
|
||||
}
|
||||
|
||||
const formatLabels = {
|
||||
'CODE_128': 'Code 128',
|
||||
'CODE_39': 'Code 39',
|
||||
'QR_CODE': 'QR',
|
||||
'DATA_MATRIX': 'DataMatrix',
|
||||
'EAN_13': 'EAN-13',
|
||||
'EAN_8': 'EAN-8'
|
||||
};
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'search-modal'; // Bestehende Modal-Styles wiederverwenden
|
||||
modal.id = 'history-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="search-modal-content">
|
||||
<div class="search-modal-header">
|
||||
<h3 style="margin:0;font-size:16px;color:var(--colortext);">Scan-Historie (${history.length})</h3>
|
||||
<button type="button" class="search-close-btn" id="history-close">×</button>
|
||||
</div>
|
||||
<div class="search-results" style="max-height:60vh;">
|
||||
${history.map((item, i) => {
|
||||
const relativeTime = getRelativeTime(item.timestamp);
|
||||
const label = formatLabels[item.type] || item.type;
|
||||
return `
|
||||
<div class="search-result-item" style="cursor:default;">
|
||||
<div class="search-result-label">
|
||||
<span class="scan-format-badge" style="display:inline-block;margin-right:8px;padding:2px 6px;font-size:10px;background:var(--butactionbg);color:#fff;border-radius:3px;">${escapeHtml(label)}</span>
|
||||
${escapeHtml(item.code)}
|
||||
</div>
|
||||
<div class="search-result-info">
|
||||
<span>${relativeTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
function closeModal() {
|
||||
modal.remove();
|
||||
resumeScanner();
|
||||
}
|
||||
|
||||
document.getElementById('history-close').addEventListener('click', closeModal);
|
||||
modal.addEventListener('click', function(e) {
|
||||
if (e.target === modal) closeModal();
|
||||
});
|
||||
}
|
||||
|
||||
// Relative Zeit-Anzeige (vor X Sekunden/Minuten/Stunden/Tagen)
|
||||
function getRelativeTime(timestamp) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const diff = now - timestamp;
|
||||
|
||||
if (diff < 60) return 'vor ' + diff + ' Sek';
|
||||
if (diff < 3600) return 'vor ' + Math.floor(diff / 60) + ' Min';
|
||||
if (diff < 86400) return 'vor ' + Math.floor(diff / 3600) + ' Std';
|
||||
return 'vor ' + Math.floor(diff / 86400) + ' Tagen';
|
||||
}
|
||||
|
||||
// Validate EAN13 checksum
|
||||
function validateEAN13(code) {
|
||||
if (!/^\d{13}$/.test(code)) return false;
|
||||
|
|
|
|||
190
pwa.php
190
pwa.php
|
|
@ -53,7 +53,7 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
|
|||
<title>Barcode Scanner</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="apple-touch-icon" href="img/icon-192.png">
|
||||
<link rel="stylesheet" href="css/scanner.css?v=81">
|
||||
<link rel="stylesheet" href="css/scanner.css?v=91">
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
|
|
@ -409,8 +409,158 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
|
|||
.scan-tool-btn:active { transform: scale(0.95); opacity: 0.8; }
|
||||
.scan-tool-btn svg { width: 22px; height: 22px; }
|
||||
.scan-tool-btn.search-btn { order: 1; }
|
||||
.scan-tool-btn.freetext-btn { order: 3; }
|
||||
.scan-buttons-group { order: 2; display: flex; gap: 8px; }
|
||||
.barcode-type-selector { order: 2; padding: 5px 10px; width: 100%; }
|
||||
.scan-buttons-group { order: 3; display: flex; gap: 8px; }
|
||||
.scan-tool-btn.freetext-btn { order: 4; }
|
||||
.scan-tool-btn.history-btn { order: 5; }
|
||||
|
||||
.barcode-type-dropdown {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-size: 15px;
|
||||
background: var(--colorbackinput);
|
||||
color: var(--colortext);
|
||||
border: 1px solid var(--colorborder);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.barcode-type-dropdown:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* OCR Digit Selector */
|
||||
.ocr-digit-selector {
|
||||
padding: 5px 10px 10px 10px;
|
||||
background: #444;
|
||||
}
|
||||
.ocr-digit-selector.hidden {
|
||||
display: none;
|
||||
}
|
||||
.ocr-digit-dropdown {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
padding: 14px 20px;
|
||||
}
|
||||
|
||||
/* OCR Loading Overlay */
|
||||
.ocr-loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
.ocr-loading-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid rgba(255,255,255,0.2);
|
||||
border-top-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.ocr-loading-text {
|
||||
color: #fff;
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* OCR Selection Modal */
|
||||
.ocr-select-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
}
|
||||
.ocr-select-content {
|
||||
background: var(--colorbackbody);
|
||||
border-radius: 8px;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ocr-select-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--colorborder);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.ocr-select-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: var(--colortext);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.ocr-select-list {
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
.ocr-select-item {
|
||||
padding: 16px;
|
||||
background: var(--colorbackinput);
|
||||
border: 1px solid var(--colorborder);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
font-family: monospace;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.ocr-select-item:hover {
|
||||
background: var(--butactionbg);
|
||||
color: var(--textbutaction);
|
||||
border-color: var(--butactionbg);
|
||||
}
|
||||
.ocr-select-item:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Format Badge (im Last Scan) */
|
||||
.scan-format-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
background: var(--butactionbg);
|
||||
color: var(--textbutaction);
|
||||
margin-right: 8px;
|
||||
}
|
||||
.scan-code-value {
|
||||
font-family: monospace;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Swipe hint */
|
||||
.swipe-hint {
|
||||
|
|
@ -786,6 +936,28 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
|
|||
<button type="button" class="scan-tool-btn search-btn" id="ctrl-search" style="display:none;" title="Produktsuche">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||
</button>
|
||||
<!-- Barcode Type Selector -->
|
||||
<div class="barcode-type-selector">
|
||||
<select id="barcode-type-select" class="barcode-type-dropdown">
|
||||
<option value="CODE_128">Code 128</option>
|
||||
<option value="QR_CODE">QR-Code</option>
|
||||
<option value="DATA_MATRIX">DataMatrix</option>
|
||||
<option value="OCR">OCR</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- OCR Digit Count Selector (nur sichtbar bei OCR-Modus) -->
|
||||
<div class="ocr-digit-selector hidden" id="ocr-digit-selector">
|
||||
<select id="ocr-digit-count" class="barcode-type-dropdown ocr-digit-dropdown">
|
||||
<option value="5">5</option>
|
||||
<option value="6">6</option>
|
||||
<option value="7">7</option>
|
||||
<option value="8" selected>8</option>
|
||||
<option value="9">9</option>
|
||||
<option value="10">10</option>
|
||||
<option value="11">11</option>
|
||||
<option value="12">12</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Scan buttons (center) -->
|
||||
<div class="scan-buttons-group">
|
||||
<button type="button" id="start-scan-btn" class="scan-btn start">Scannen</button>
|
||||
|
|
@ -795,6 +967,13 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
|
|||
<button type="button" class="scan-tool-btn freetext-btn" id="ctrl-freetext" style="display:none;" title="Freitext">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>
|
||||
</button>
|
||||
<!-- History button -->
|
||||
<button type="button" class="scan-tool-btn history-btn" id="ctrl-history" title="Scan-Historie">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -1312,8 +1491,9 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
|
|||
// App starten
|
||||
initApp();
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@zxing/library@0.21.3/umd/index.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/tesseract.js@5.1.0/dist/tesseract.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js"></script>
|
||||
<script src="<?php echo dol_buildpath('/handybarcodescanner/js/scanner.js', 1); ?>?v=81"></script>
|
||||
<script src="<?php echo dol_buildpath('/handybarcodescanner/js/scanner.js', 1); ?>?v=91"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
5
sw.js
5
sw.js
|
|
@ -1,12 +1,13 @@
|
|||
// Service Worker for HandyBarcodeScanner PWA
|
||||
const CACHE_NAME = 'scanner-v8.1';
|
||||
const CACHE_NAME = 'scanner-v9.1';
|
||||
const ASSETS = [
|
||||
'pwa.php',
|
||||
'css/scanner.css',
|
||||
'js/scanner.js',
|
||||
'img/icon-192.png',
|
||||
'img/icon-512.png',
|
||||
'https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/@zxing/library@0.21.3/umd/index.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/tesseract.js@5.1.0/dist/tesseract.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js'
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue