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
|
## Barcode-Unterstützung
|
||||||
|
|
||||||
Das Modul sucht Barcodes in folgender Reihenfolge:
|
Das Modul sucht Barcodes/Artikelnummern in folgender Reihenfolge:
|
||||||
1. Produkt-Barcode (`llx_product.barcode`)
|
1. Produkt-Barcode (`llx_product.barcode`)
|
||||||
2. Lieferanten-Barcode (`llx_product_fournisseur_price.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):
|
### Barcode-Formate (ZXing-JS, ab v8.3)
|
||||||
- **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)
|
|
||||||
|
|
||||||
**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
|
## Installation
|
||||||
|
|
||||||
|
|
@ -186,8 +200,9 @@ handybarcodescanner/
|
||||||
```
|
```
|
||||||
|
|
||||||
### Verwendete Bibliotheken
|
### Verwendete Bibliotheken
|
||||||
- [Quagga2](https://github.com/ericblade/quagga2) - Browser-basierte Barcode-Erkennung
|
- [ZXing-JS](https://github.com/zxing-js/library) v0.21.3 - Browser-basierte Barcode-Erkennung (1D + 2D)
|
||||||
- [JsBarcode](https://github.com/lindell/JsBarcode) - Barcode-Generierung für Druck
|
- [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
|
- [ZXing](https://github.com/zxing/zxing) - Barcode-Generierung in Android App
|
||||||
|
|
||||||
### PWA (Progressive Web 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 ⚠️
|
- Firefox auf Android: Nur Shortcuts, kein echter Standalone-Modus ⚠️
|
||||||
- Safari auf iOS: PWA-Unterstützung mit Einschränkungen
|
- 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
|
### 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'
|
$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'
|
// 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
|
// Url to the file with your last numberversion of this module
|
||||||
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
//$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 currentProduct = null;
|
||||||
let selectedSupplier = null;
|
let selectedSupplier = null;
|
||||||
let allSuppliers = [];
|
let allSuppliers = [];
|
||||||
let quaggaInitialized = false;
|
let scannerInitialized = false;
|
||||||
let openDialogCount = 0; // Zaehlt offene Dialoge
|
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
|
// Persistente Einstellungen - eine zentrale Config statt einzelner Keys
|
||||||
const STORAGE_KEY = 'hbs_config';
|
const STORAGE_KEY = 'hbs_config';
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
|
|
@ -133,6 +142,49 @@
|
||||||
window._showProductSearchModal = showProductSearchModal;
|
window._showProductSearchModal = showProductSearchModal;
|
||||||
window._showFreetextModal = showFreetextModal;
|
window._showFreetextModal = showFreetextModal;
|
||||||
window._showOrdersPanel = showOrdersPanel;
|
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
|
// Check camera support - just log warnings, never disable button
|
||||||
|
|
@ -143,9 +195,9 @@
|
||||||
console.warn('HandyBarcodeScanner: HTTPS empfohlen fuer Kamerazugriff');
|
console.warn('HandyBarcodeScanner: HTTPS empfohlen fuer Kamerazugriff');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Quagga is loaded
|
// Check if ZXing is loaded
|
||||||
if (typeof Quagga === 'undefined') {
|
if (typeof ZXing === 'undefined') {
|
||||||
console.error('HandyBarcodeScanner: Quagga nicht geladen');
|
console.error('HandyBarcodeScanner: ZXing nicht geladen');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check mediaDevices
|
// Check mediaDevices
|
||||||
|
|
@ -224,10 +276,17 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Quagga is available
|
// Check if libraries are available
|
||||||
if (typeof Quagga === 'undefined') {
|
if (currentBarcodeType === 'OCR') {
|
||||||
showToast('Quagga Bibliothek nicht geladen', 'error');
|
if (typeof Tesseract === 'undefined') {
|
||||||
return;
|
showToast('Tesseract.js Bibliothek nicht geladen', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (typeof ZXing === 'undefined') {
|
||||||
|
showToast('ZXing Bibliothek nicht geladen', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable button while initializing
|
// Disable button while initializing
|
||||||
|
|
@ -240,8 +299,14 @@
|
||||||
// Permission granted - stop the test stream
|
// Permission granted - stop the test stream
|
||||||
stream.getTracks().forEach(track => track.stop());
|
stream.getTracks().forEach(track => track.stop());
|
||||||
|
|
||||||
// Now start Quagga
|
// OCR-Modus oder Barcode-Modus?
|
||||||
initQuagga();
|
if (currentBarcodeType === 'OCR') {
|
||||||
|
// OCR-Modus: Kamera-Preview + "Foto aufnehmen" Button
|
||||||
|
startCameraPreview();
|
||||||
|
} else {
|
||||||
|
// Barcode-Modus: ZXing Video-Stream
|
||||||
|
currentScanner = initZXing(currentBarcodeType);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
console.error('Camera permission error:', err);
|
console.error('Camera permission error:', err);
|
||||||
|
|
@ -260,68 +325,63 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initQuagga() {
|
function getBarcodeFormat(type) {
|
||||||
Quagga.init({
|
const formats = {
|
||||||
inputStream: {
|
'CODE_128': ZXing.BarcodeFormat.CODE_128,
|
||||||
name: "Live",
|
'QR_CODE': ZXing.BarcodeFormat.QR_CODE,
|
||||||
type: "LiveStream",
|
'DATA_MATRIX': ZXing.BarcodeFormat.DATA_MATRIX,
|
||||||
target: elements.videoContainer,
|
'CODE_39': ZXing.BarcodeFormat.CODE_39,
|
||||||
constraints: {
|
'EAN_13': ZXing.BarcodeFormat.EAN_13,
|
||||||
facingMode: "environment",
|
'EAN_8': ZXing.BarcodeFormat.EAN_8
|
||||||
width: { min: 640, ideal: 1280, max: 1920 },
|
};
|
||||||
height: { min: 480, ideal: 720, max: 1080 }
|
return formats[type] || ZXing.BarcodeFormat.CODE_128;
|
||||||
},
|
}
|
||||||
area: { // Scan-Bereich: groesserer Bereich fuer kleine Barcodes
|
|
||||||
top: "10%",
|
function initZXing(format) {
|
||||||
right: "5%",
|
format = format || currentBarcodeType;
|
||||||
left: "5%",
|
|
||||||
bottom: "10%"
|
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)
|
||||||
}
|
}
|
||||||
},
|
).then(() => {
|
||||||
decoder: {
|
// Scanner erfolgreich gestartet
|
||||||
readers: [
|
scannerInitialized = true;
|
||||||
"code_128_reader", // Best for alphanumeric codes
|
isScanning = true;
|
||||||
"code_39_reader", // Also good for alphanumeric (e.g. P20260030)
|
elements.startBtn.disabled = false;
|
||||||
"ean_reader", // Numeric only - lower priority
|
elements.startBtn.classList.add('hidden');
|
||||||
"ean_8_reader" // Numeric only - lower priority
|
elements.stopBtn.classList.remove('hidden');
|
||||||
],
|
elements.videoContainer.classList.add('scanning');
|
||||||
multiple: false
|
|
||||||
},
|
// Timeout-Timer starten
|
||||||
locate: true,
|
startScanTimeout();
|
||||||
locator: {
|
}).catch((err) => {
|
||||||
patchSize: "medium", // medium = besser fuer kleine Barcodes
|
console.error('ZXing init error:', err);
|
||||||
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);
|
|
||||||
elements.startBtn.disabled = false;
|
elements.startBtn.disabled = false;
|
||||||
elements.startBtn.textContent = CONFIG.lang.startScan || 'Scan starten';
|
elements.startBtn.textContent = CONFIG.lang.startScan || 'Scan starten';
|
||||||
showToast(CONFIG.lang.cameraError || 'Kamerafehler', 'error');
|
showToast(CONFIG.lang.cameraError || 'Kamerafehler', 'error');
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Success - start scanning
|
return codeReader;
|
||||||
Quagga.start();
|
} catch (e) {
|
||||||
quaggaInitialized = true;
|
console.error('ZXing initialization error:', e);
|
||||||
isScanning = true;
|
|
||||||
elements.startBtn.disabled = false;
|
elements.startBtn.disabled = false;
|
||||||
elements.startBtn.classList.add('hidden');
|
elements.startBtn.textContent = CONFIG.lang.startScan || 'Scan starten';
|
||||||
elements.stopBtn.classList.remove('hidden');
|
showToast('Scanner konnte nicht initialisiert werden', 'error');
|
||||||
elements.videoContainer.classList.add('scanning');
|
return null;
|
||||||
|
}
|
||||||
// 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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeout-Hinweis wenn nichts erkannt wird
|
// Timeout-Hinweis wenn nichts erkannt wird
|
||||||
|
|
@ -339,18 +399,29 @@
|
||||||
function stopScanner() {
|
function stopScanner() {
|
||||||
clearTimeout(scanTimeoutTimer);
|
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 {
|
try {
|
||||||
Quagga.offDetected(onBarcodeDetected);
|
currentScanner.reset();
|
||||||
Quagga.stop();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Quagga stop error:', e);
|
console.log('Scanner stop error:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isScanning = false;
|
isScanning = false;
|
||||||
isPaused = false;
|
isPaused = false;
|
||||||
quaggaInitialized = false;
|
scannerInitialized = false;
|
||||||
|
currentScanner = null;
|
||||||
elements.startBtn.classList.remove('hidden');
|
elements.startBtn.classList.remove('hidden');
|
||||||
elements.startBtn.disabled = false;
|
elements.startBtn.disabled = false;
|
||||||
elements.startBtn.textContent = CONFIG.lang.startScan || 'Scannen';
|
elements.startBtn.textContent = CONFIG.lang.startScan || 'Scannen';
|
||||||
|
|
@ -359,19 +430,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scanner pausieren (bei Dialog-Oeffnung)
|
// Scanner pausieren (bei Dialog-Oeffnung)
|
||||||
|
// ZXing hat kein natives pause() - wir nutzen ein Flag und ignorieren Detections
|
||||||
function pauseScanner() {
|
function pauseScanner() {
|
||||||
if (!isScanning || isPaused) return;
|
if (!isScanning || isPaused) return;
|
||||||
openDialogCount++;
|
openDialogCount++;
|
||||||
|
isPaused = true;
|
||||||
if (quaggaInitialized) {
|
console.log('Scanner paused, dialogs:', openDialogCount);
|
||||||
try {
|
|
||||||
Quagga.pause();
|
|
||||||
isPaused = true;
|
|
||||||
console.log('Scanner paused, dialogs:', openDialogCount);
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Quagga pause error:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scanner fortsetzen (wenn alle Dialoge geschlossen)
|
// Scanner fortsetzen (wenn alle Dialoge geschlossen)
|
||||||
|
|
@ -385,29 +449,376 @@
|
||||||
|
|
||||||
if (!isScanning || !isPaused) return;
|
if (!isScanning || !isPaused) return;
|
||||||
|
|
||||||
if (quaggaInitialized) {
|
isPaused = false;
|
||||||
try {
|
console.log('Scanner resumed');
|
||||||
Quagga.start();
|
startScanTimeout();
|
||||||
isPaused = false;
|
|
||||||
console.log('Scanner resumed');
|
|
||||||
startScanTimeout();
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Quagga resume error:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Globale Funktionen fuer PWA
|
// Globale Funktionen fuer PWA
|
||||||
window._pauseScanner = pauseScanner;
|
window._pauseScanner = pauseScanner;
|
||||||
window._resumeScanner = resumeScanner;
|
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) {
|
function onBarcodeDetected(result) {
|
||||||
let code = result.codeResult.code;
|
// Ignore detections wenn Scanner pausiert ist
|
||||||
const format = result.codeResult.format;
|
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
|
// 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 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)) {
|
if (!validateEAN13(code)) {
|
||||||
// Try without last digit (might be internal 12-digit code)
|
// Try without last digit (might be internal 12-digit code)
|
||||||
const shortened = code.substring(0, 12);
|
const shortened = code.substring(0, 12);
|
||||||
|
|
@ -417,7 +828,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate EAN8 checksum
|
// 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)) {
|
if (!validateEAN8(code)) {
|
||||||
// Try without last digit
|
// Try without last digit
|
||||||
code = code.substring(0, 7);
|
code = code.substring(0, 7);
|
||||||
|
|
@ -455,8 +866,27 @@
|
||||||
}
|
}
|
||||||
playBeep();
|
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');
|
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
|
// Search product
|
||||||
searchProduct(code);
|
searchProduct(code);
|
||||||
|
|
@ -467,6 +897,100 @@
|
||||||
}, 3000);
|
}, 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
|
// Validate EAN13 checksum
|
||||||
function validateEAN13(code) {
|
function validateEAN13(code) {
|
||||||
if (!/^\d{13}$/.test(code)) return false;
|
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>
|
<title>Barcode Scanner</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
<link rel="apple-touch-icon" href="img/icon-192.png">
|
<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>
|
<style>
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
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:active { transform: scale(0.95); opacity: 0.8; }
|
||||||
.scan-tool-btn svg { width: 22px; height: 22px; }
|
.scan-tool-btn svg { width: 22px; height: 22px; }
|
||||||
.scan-tool-btn.search-btn { order: 1; }
|
.scan-tool-btn.search-btn { order: 1; }
|
||||||
.scan-tool-btn.freetext-btn { order: 3; }
|
.barcode-type-selector { order: 2; padding: 5px 10px; width: 100%; }
|
||||||
.scan-buttons-group { order: 2; display: flex; gap: 8px; }
|
.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 */
|
||||||
.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">
|
<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>
|
<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>
|
</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) -->
|
<!-- Scan buttons (center) -->
|
||||||
<div class="scan-buttons-group">
|
<div class="scan-buttons-group">
|
||||||
<button type="button" id="start-scan-btn" class="scan-btn start">Scannen</button>
|
<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">
|
<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>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1312,8 +1491,9 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
|
||||||
// App starten
|
// App starten
|
||||||
initApp();
|
initApp();
|
||||||
</script>
|
</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="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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
5
sw.js
5
sw.js
|
|
@ -1,12 +1,13 @@
|
||||||
// Service Worker for HandyBarcodeScanner PWA
|
// Service Worker for HandyBarcodeScanner PWA
|
||||||
const CACHE_NAME = 'scanner-v8.1';
|
const CACHE_NAME = 'scanner-v9.1';
|
||||||
const ASSETS = [
|
const ASSETS = [
|
||||||
'pwa.php',
|
'pwa.php',
|
||||||
'css/scanner.css',
|
'css/scanner.css',
|
||||||
'js/scanner.js',
|
'js/scanner.js',
|
||||||
'img/icon-192.png',
|
'img/icon-192.png',
|
||||||
'img/icon-512.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'
|
'https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue