Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 042afc19e2 | |||
| 28d049b52a | |||
| 8e478aeba4 | |||
| 1b38d1f0c8 | |||
| e7e24b3fae | |||
| fc80857fbe | |||
| f763ad064d | |||
| bc94ffdc18 | |||
| 76e9670cee | |||
| 577c6c4bed | |||
| 3aa19b10d1 |
-60
@@ -3484,66 +3484,6 @@ def api_library_scan_action():
|
||||
client.close()
|
||||
|
||||
|
||||
@app.route('/api/scan_upload', methods=['POST'])
|
||||
def api_scan_upload():
|
||||
"""
|
||||
Server-side barcode/QR decoding endpoint.
|
||||
Accepts image upload and returns detected barcodes (fallback for client-side scanner).
|
||||
"""
|
||||
if 'username' not in session:
|
||||
return jsonify({'ok': False, 'message': 'Nicht angemeldet.'}), 401
|
||||
|
||||
try:
|
||||
from pyzbar.pyzbar import decode
|
||||
from PIL import Image
|
||||
import io
|
||||
except ImportError:
|
||||
return jsonify({'ok': False, 'message': 'pyzbar library not installed on server.'}), 500
|
||||
|
||||
if 'image' not in request.files:
|
||||
return jsonify({'ok': False, 'message': 'Keine Bilddatei bereitgestellt.'}), 400
|
||||
|
||||
file = request.files['image']
|
||||
if file.filename == '':
|
||||
return jsonify({'ok': False, 'message': 'Datei ist leer.'}), 400
|
||||
|
||||
try:
|
||||
# Decode image
|
||||
image = Image.open(io.BytesIO(file.read()))
|
||||
image = image.convert('RGB')
|
||||
|
||||
# Detect barcodes using pyzbar
|
||||
results = decode(image)
|
||||
|
||||
if not results:
|
||||
return jsonify({'ok': False, 'message': 'Kein Barcode im Bild erkannt.'}), 400
|
||||
|
||||
# Return all detected barcodes, sorted by confidence (quality)
|
||||
barcodes = [
|
||||
{
|
||||
'type': result.type,
|
||||
'value': result.data.decode('utf-8'),
|
||||
'rect': {
|
||||
'x': result.rect.left,
|
||||
'y': result.rect.top,
|
||||
'width': result.rect.width,
|
||||
'height': result.rect.height,
|
||||
}
|
||||
}
|
||||
for result in results
|
||||
]
|
||||
|
||||
return jsonify({
|
||||
'ok': True,
|
||||
'barcodes': barcodes,
|
||||
'message': f'{len(barcodes)} Barcode(s) erkannt.'
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error decoding barcode image: {e}")
|
||||
return jsonify({'ok': False, 'message': f'Fehler beim Dekodieren: {str(e)}'}), 500
|
||||
|
||||
|
||||
@app.route('/api/item_detail/<item_id>')
|
||||
def api_item_detail(item_id):
|
||||
"""
|
||||
|
||||
@@ -15,4 +15,3 @@ openpyxl
|
||||
cryptography>=42.0.0
|
||||
pywebpush
|
||||
py-vapid>=1.9.0
|
||||
pyzbar
|
||||
|
||||
@@ -1,353 +0,0 @@
|
||||
/**
|
||||
* Hybrid Barcode Scanner
|
||||
* Supports: Real-time camera scanning (ZXing.js), image upload fallback (server-side pyzbar),
|
||||
* and USB/keyboard scanner emulation.
|
||||
*/
|
||||
|
||||
class HybridScanner {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
videoId: options.videoId || 'scanner-video',
|
||||
canvasId: options.canvasId || 'scanner-canvas',
|
||||
formats: options.formats || ['QR_CODE', 'CODE_128', 'EAN_13', 'ISBN', 'CODE_39', 'UPC_A'],
|
||||
fps: options.fps || 10,
|
||||
onSuccess: options.onSuccess || (() => {}),
|
||||
onError: options.onError || (() => {}),
|
||||
facingMode: options.facingMode || 'environment', // 'environment' for rear, 'user' for front
|
||||
};
|
||||
|
||||
this.state = {
|
||||
isRunning: false,
|
||||
stream: null,
|
||||
reader: null,
|
||||
canvas: null,
|
||||
videoElement: null,
|
||||
zxingReady: false,
|
||||
lastScanTime: 0,
|
||||
scanDebounceMs: 300, // Prevent rapid duplicate scans
|
||||
};
|
||||
|
||||
this.keyboardInputBuffer = '';
|
||||
this.keyboardInputTimeout = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize ZXing library from CDN
|
||||
*/
|
||||
async initZXing() {
|
||||
if (this.state.zxingReady) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// Load ZXing library from CDN
|
||||
if (typeof ZXing !== 'undefined') {
|
||||
this.state.zxingReady = true;
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use fallback URL for better mobile compatibility
|
||||
let scriptUrl = 'https://unpkg.com/@zxing/library@latest/umd/index.min.js';
|
||||
|
||||
// Check if already loading
|
||||
const existingScript = document.querySelector(`script[src="${scriptUrl}"]`);
|
||||
if (existingScript) {
|
||||
existingScript.addEventListener('load', () => {
|
||||
this.state.zxingReady = typeof ZXing !== 'undefined';
|
||||
resolve(this.state.zxingReady);
|
||||
}, { once: true });
|
||||
existingScript.addEventListener('error', () => {
|
||||
console.error('Failed to load ZXing library');
|
||||
resolve(false);
|
||||
}, { once: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = scriptUrl;
|
||||
script.async = true;
|
||||
script.onload = () => {
|
||||
this.state.zxingReady = typeof ZXing !== 'undefined';
|
||||
resolve(this.state.zxingReady);
|
||||
};
|
||||
script.onerror = () => {
|
||||
console.error('Failed to load ZXing library');
|
||||
resolve(false);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start real-time barcode scanning via camera
|
||||
*/
|
||||
async start() {
|
||||
if (this.state.isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure ZXing is loaded
|
||||
const zxingReady = await this.initZXing();
|
||||
if (!zxingReady) {
|
||||
this.options.onError('ZXing library failed to load. Please use image upload fallback.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get DOM elements
|
||||
this.state.videoElement = document.getElementById(this.options.videoId);
|
||||
this.state.canvas = document.getElementById(this.options.canvasId);
|
||||
|
||||
if (!this.state.videoElement || !this.state.canvas) {
|
||||
this.options.onError('Scanner video or canvas element not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Request camera access
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
facingMode: this.options.facingMode,
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 },
|
||||
},
|
||||
audio: false,
|
||||
});
|
||||
|
||||
this.state.stream = stream;
|
||||
this.state.videoElement.srcObject = stream;
|
||||
|
||||
// Make video element visible (ensure it's not hidden by display: none)
|
||||
this.state.videoElement.style.display = 'block';
|
||||
// Ensure autoplay works on mobile by muting and setting playsInline
|
||||
try {
|
||||
this.state.videoElement.muted = true;
|
||||
this.state.videoElement.autoplay = true;
|
||||
this.state.videoElement.playsInline = true;
|
||||
// Some browsers require explicit play() after setting srcObject
|
||||
const p = this.state.videoElement.play();
|
||||
if (p && p.then) {
|
||||
p.catch(() => {
|
||||
// ignore play promise rejection (user gesture required on some browsers)
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Initialize ZXing reader
|
||||
const hints = new Map();
|
||||
hints.set(ZXing.DecodeHintType.POSSIBLE_FORMATS, this.options.formats);
|
||||
hints.set(ZXing.DecodeHintType.TRY_HARDER, true);
|
||||
this.state.reader = new ZXing.BrowserMultiFormatReader(hints);
|
||||
|
||||
this.state.isRunning = true;
|
||||
|
||||
// Start scanning loop
|
||||
this.scanLoop();
|
||||
|
||||
// Setup keyboard input fallback for USB scanners
|
||||
this.setupKeyboardInput();
|
||||
} catch (err) {
|
||||
this.options.onError(`Camera error: ${err.message || err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop barcode scanning
|
||||
*/
|
||||
stop() {
|
||||
if (!this.state.isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.isRunning = false;
|
||||
|
||||
if (this.state.stream) {
|
||||
this.state.stream.getTracks().forEach((track) => track.stop());
|
||||
this.state.stream = null;
|
||||
}
|
||||
|
||||
if (this.state.videoElement) {
|
||||
this.state.videoElement.srcObject = null;
|
||||
this.state.videoElement.style.display = 'none';
|
||||
}
|
||||
|
||||
// Reset ZXing reader to stop any internal loops
|
||||
try {
|
||||
if (this.state.reader && typeof this.state.reader.reset === 'function') {
|
||||
this.state.reader.reset();
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
this.state.reader = null;
|
||||
|
||||
this.removeKeyboardInput();
|
||||
}
|
||||
|
||||
/**
|
||||
* Main scanning loop for real-time frame processing
|
||||
*/
|
||||
scanLoop() {
|
||||
if (!this.state.isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = this.state.canvas;
|
||||
const video = this.state.videoElement;
|
||||
|
||||
if (!video || !canvas) {
|
||||
setTimeout(() => this.scanLoop(), 100);
|
||||
return;
|
||||
}
|
||||
|
||||
if (video.readyState === video.HAVE_ENOUGH_DATA) {
|
||||
try {
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
|
||||
if (canvas.width === 0 || canvas.height === 0) {
|
||||
setTimeout(() => this.scanLoop(), 100);
|
||||
return;
|
||||
}
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (context) {
|
||||
context.drawImage(video, 0, 0);
|
||||
|
||||
try {
|
||||
const luminanceSource = new ZXing.HTMLCanvasElementLuminanceSource(canvas);
|
||||
const binaryBitmap = new ZXing.BinaryBitmap(new ZXing.HybridBinarizer(luminanceSource));
|
||||
|
||||
try {
|
||||
const result = this.state.reader.decodeFromBitmap(binaryBitmap);
|
||||
this.handleScanResult(result.text);
|
||||
} catch (e) {
|
||||
// No barcode found in frame; continue scanning
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently ignore canvas errors
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Scan loop error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Throttle scan loop based on FPS setting
|
||||
setTimeout(() => this.scanLoop(), 1000 / this.options.fps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle successful barcode scan with debouncing
|
||||
*/
|
||||
handleScanResult(scannedText) {
|
||||
const now = Date.now();
|
||||
if (now - this.state.lastScanTime < this.state.scanDebounceMs) {
|
||||
return; // Ignore rapid duplicates
|
||||
}
|
||||
|
||||
this.state.lastScanTime = now;
|
||||
this.options.onSuccess(scannedText.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup keyboard input listener for USB scanner emulation
|
||||
* Most USB scanners emulate keyboard input (barcode followed by Enter)
|
||||
*/
|
||||
setupKeyboardInput() {
|
||||
this.keyboardListener = (event) => {
|
||||
// Allow only alphanumeric, dash, colon (common in barcodes)
|
||||
if (/^[a-zA-Z0-9:_\-]$/.test(event.key)) {
|
||||
event.preventDefault();
|
||||
this.keyboardInputBuffer += event.key;
|
||||
|
||||
// Reset timeout for multi-part barcodes
|
||||
if (this.keyboardInputTimeout) {
|
||||
clearTimeout(this.keyboardInputTimeout);
|
||||
}
|
||||
} else if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
if (this.keyboardInputBuffer.length > 0) {
|
||||
this.handleScanResult(this.keyboardInputBuffer);
|
||||
this.keyboardInputBuffer = '';
|
||||
}
|
||||
} else if (event.key === 'Escape') {
|
||||
this.keyboardInputBuffer = '';
|
||||
this.stop();
|
||||
}
|
||||
|
||||
// Auto-trigger if buffer looks complete (common barcode lengths)
|
||||
if ([8, 12, 13, 17].includes(this.keyboardInputBuffer.length)) {
|
||||
this.keyboardInputTimeout = setTimeout(() => {
|
||||
if (this.keyboardInputBuffer.length > 0) {
|
||||
this.handleScanResult(this.keyboardInputBuffer);
|
||||
this.keyboardInputBuffer = '';
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', this.keyboardListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove keyboard input listener
|
||||
*/
|
||||
removeKeyboardInput() {
|
||||
if (this.keyboardListener) {
|
||||
document.removeEventListener('keydown', this.keyboardListener);
|
||||
this.keyboardListener = null;
|
||||
}
|
||||
this.keyboardInputBuffer = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload image for server-side decoding (fallback)
|
||||
* Requires /api/scan_upload endpoint
|
||||
*/
|
||||
static async uploadImageForDecoding(file, onSuccess, onError) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
const response = await fetch('/api/scan_upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.ok && data.barcodes && data.barcodes.length > 0) {
|
||||
// Return first detected barcode (highest confidence)
|
||||
onSuccess(data.barcodes[0].value);
|
||||
} else {
|
||||
onError(data.message || 'No barcode detected in image.');
|
||||
}
|
||||
} catch (err) {
|
||||
onError(`Upload error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle camera (front/rear on mobile)
|
||||
*/
|
||||
toggleCamera() {
|
||||
if (this.options.facingMode === 'environment') {
|
||||
this.options.facingMode = 'user';
|
||||
} else {
|
||||
this.options.facingMode = 'environment';
|
||||
}
|
||||
|
||||
// Restart scanning with new camera
|
||||
this.stop();
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in templates
|
||||
if (typeof window !== 'undefined') {
|
||||
window.HybridScanner = HybridScanner;
|
||||
}
|
||||
+60
-349
@@ -10,24 +10,6 @@
|
||||
<h1>📚 Bibliothek</h1>
|
||||
<p class="subtitle">Bücher, CDs und weitere Medien</p>
|
||||
</div>
|
||||
|
||||
<div class="header-controls">
|
||||
<!-- Favorites Toggle -->
|
||||
<div class="view-switch">
|
||||
<button id="favoriteToggle" class="toggle-btn" aria-label="Favoriten anzeigen/verbergen">
|
||||
<span class="toggle-icon">⭐</span>
|
||||
<span class="toggle-label">Favoriten</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- View Mode Toggle -->
|
||||
<div class="view-switch">
|
||||
<button id="viewModeToggle" class="toggle-btn" aria-label="Ansichtsmodus wechseln">
|
||||
<span class="toggle-icon">ㄷ</span>
|
||||
<span class="toggle-label">Tabelle</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search & Filter Section -->
|
||||
@@ -67,22 +49,8 @@
|
||||
|
||||
<!-- Scanner Panel -->
|
||||
<div id="qrContainer" class="qr-container" style="display: none;">
|
||||
<div class="scanner-header">
|
||||
<h3>Barcode/QR-Code Scanner</h3>
|
||||
<div class="scanner-controls">
|
||||
<button type="button" id="cameraToggle" class="scanner-button" title="Kamera wechseln">📷 Kamera</button>
|
||||
<label for="uploadScanImage" class="scanner-button" title="Bild hochladen zum Dekodieren">📤 Upload</label>
|
||||
<input type="file" id="uploadScanImage" accept="image/*" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="scanner-video-wrapper">
|
||||
<video id="qr-reader" playsinline style="width: 100%; max-width: 100%; height: auto; aspect-ratio: 4/3; object-fit: cover; border-radius: 8px;"></video>
|
||||
<canvas id="scanner-canvas" style="display: none;"></canvas>
|
||||
</div>
|
||||
<div id="scanMessage" class="scanner-message" style="margin-top: 10px; text-align: center; font-weight: bold; color: #666;"></div>
|
||||
<p style="font-size: 0.85em; color: #999; margin-top: 10px; margin-bottom: 0; text-align: center;">
|
||||
💡 Hinweis: Scan-Ergebnis wird automatisch in das Feld oben übernommen. USB-Scanner werden als Tastatureingabe erkannt.
|
||||
</p>
|
||||
<div id="qr-reader" style="width: auto; height: 300px;"></div>
|
||||
<span id="qr-result" style="display: none;"></span>
|
||||
</div>
|
||||
|
||||
<!-- Table Header (only in table mode) -->
|
||||
@@ -219,9 +187,7 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
|
||||
<script src="/static/js/scanner.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
|
||||
<script>
|
||||
// View mode persistence
|
||||
const LIBRARY_VIEW_MODE_KEY = 'inventarLibraryViewMode';
|
||||
@@ -564,29 +530,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
|
||||
initMobileWindowedLibraryLoading();
|
||||
// On mobile, make tapping an item open details for easier access (tap target larger)
|
||||
function enableMobileTapToDetails() {
|
||||
if (!isMobileViewport()) return;
|
||||
document.querySelectorAll('.library-item').forEach(item => {
|
||||
item.addEventListener('click', function(e) {
|
||||
// Ignore clicks on buttons/links inside the card
|
||||
const tag = e.target.tagName.toLowerCase();
|
||||
if (tag === 'button' || tag === 'a' || e.target.closest('.action-button')) return;
|
||||
const itemId = this.dataset.itemId;
|
||||
if (!itemId) return;
|
||||
fetch(`/get_item_details/${itemId}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
displayLibraryItemDetail(data.item);
|
||||
document.getElementById('detailModal').style.display = 'flex';
|
||||
}
|
||||
});
|
||||
}, { passive: true });
|
||||
});
|
||||
}
|
||||
|
||||
enableMobileTapToDetails();
|
||||
});
|
||||
|
||||
function displayLibraryItemDetail(item) {
|
||||
@@ -597,7 +540,7 @@ function displayLibraryItemDetail(item) {
|
||||
modalBody.innerHTML = `
|
||||
<h2>${item.Name}</h2>
|
||||
<div class="detail-content">
|
||||
${item.Image ? `<img src="/uploads/${item.Image}" alt="${item.Name}" class="detail-image">` : ''}
|
||||
${item.Image ? `<img src="/uploads/${item.Image}" alt="${item.Name}" style="max-width: 200px; margin-bottom: 20px;">` : ''}
|
||||
<p><strong>Autor:</strong> ${item.Author || '—'}</p>
|
||||
<p><strong>ISBN:</strong> ${item.ISBN || '—'}</p>
|
||||
<p><strong>Typ:</strong> ${item.ItemType || 'Medium'}</p>
|
||||
@@ -663,103 +606,80 @@ document.getElementById('clearFiltersBtn').addEventListener('click', function()
|
||||
closeAllFilters();
|
||||
});
|
||||
|
||||
// Favorites toggle
|
||||
document.getElementById('favoriteToggle').addEventListener('click', function() {
|
||||
this.classList.toggle('open');
|
||||
// TODO: Implement favorites filtering
|
||||
});
|
||||
|
||||
// Scanner instance (hybrid: real-time + upload + keyboard)
|
||||
let scanner = null;
|
||||
// Scanner toggle
|
||||
// Scanner state tracking
|
||||
let isScanning = false;
|
||||
|
||||
document.getElementById('scannerBtn').addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
const container = document.getElementById('qrContainer');
|
||||
const isOpen = container.style.display !== 'none';
|
||||
const btn = this;
|
||||
|
||||
if (isOpen) {
|
||||
// Close the scanner
|
||||
container.style.display = 'none';
|
||||
if (scanner) {
|
||||
scanner.stop();
|
||||
scanner = null;
|
||||
}
|
||||
this.classList.remove('open');
|
||||
this.setAttribute('aria-expanded', 'false');
|
||||
Quagga.stop();
|
||||
isScanning = false;
|
||||
|
||||
btn.classList.remove('open');
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
} else {
|
||||
// Open the scanner
|
||||
container.style.display = 'block';
|
||||
this.classList.add('open');
|
||||
this.setAttribute('aria-expanded', 'true');
|
||||
btn.classList.add('open');
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
|
||||
if (!scanner) {
|
||||
initScanner();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function initScanner() {
|
||||
scanner = new HybridScanner({
|
||||
videoId: 'qr-reader',
|
||||
canvasId: 'scanner-canvas',
|
||||
formats: ['QR_CODE', 'CODE_128', 'EAN_13', 'ISBN', 'CODE_39', 'UPC_A', 'EAN_8'],
|
||||
fps: 10,
|
||||
facingMode: 'environment',
|
||||
onSuccess: function(decodedText) {
|
||||
const studentId = decodedText.trim();
|
||||
document.getElementById('studentIdInput').value = studentId;
|
||||
document.getElementById('scanMessage').textContent = '✓ Erkannt: ' + studentId;
|
||||
document.getElementById('scanMessage').style.color = '#4CAF50';
|
||||
},
|
||||
onError: function(error) {
|
||||
document.getElementById('scanMessage').textContent = '✗ ' + error;
|
||||
document.getElementById('scanMessage').style.color = '#f44336';
|
||||
}
|
||||
});
|
||||
|
||||
scanner.start();
|
||||
}
|
||||
|
||||
// Camera toggle
|
||||
document.getElementById('cameraToggle')?.addEventListener('click', function() {
|
||||
if (scanner) {
|
||||
scanner.toggleCamera();
|
||||
this.textContent = scanner.options.facingMode === 'user' ? '📷 Rückkamera' : '🤳 Frontkamera';
|
||||
}
|
||||
});
|
||||
|
||||
// Image upload fallback
|
||||
document.getElementById('uploadScanImage')?.addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const messageEl = document.getElementById('scanMessage');
|
||||
messageEl.textContent = 'Wird dekodiert...';
|
||||
messageEl.style.color = '#2196F3';
|
||||
|
||||
HybridScanner.uploadImageForDecoding(
|
||||
file,
|
||||
(decodedText) => {
|
||||
document.getElementById('studentIdInput').value = decodedText;
|
||||
messageEl.textContent = '✓ Erkannt (Upload): ' + decodedText;
|
||||
messageEl.style.color = '#4CAF50';
|
||||
e.target.value = ''; // Reset file input
|
||||
Quagga.init({
|
||||
inputStream: {
|
||||
name: "Live",
|
||||
type: "LiveStream",
|
||||
// Targets the element where the video stream will inject
|
||||
target: document.querySelector('#qr-reader'),
|
||||
constraints: {
|
||||
width: 640,
|
||||
height: 480,
|
||||
facingMode: "environment" // Forces back camera
|
||||
},
|
||||
},
|
||||
(error) => {
|
||||
messageEl.textContent = '✗ ' + error;
|
||||
messageEl.style.color = '#f44336';
|
||||
e.target.value = '';
|
||||
decoder: {
|
||||
// Optimized for 1D barcodes (e.g., student IDs, member cards)
|
||||
readers: ["code_128_reader", "ean_reader", "code_39_reader", "upc_reader"]
|
||||
}
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
console.error("Initialization error:", err);
|
||||
alert("Kamera konnte nicht gestartet werden.");
|
||||
return;
|
||||
}
|
||||
);
|
||||
Quagga.start();
|
||||
isScanning = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function onScanSuccess(decodedText, decodedResult) {
|
||||
const studentId = decodedText.trim();
|
||||
// Single Quagga event listener for successful scans
|
||||
Quagga.onDetected(function(data) {
|
||||
const decodedText = data.codeResult.code;
|
||||
const studentId = String(decodedText || '').trim();
|
||||
|
||||
// Stop scanning immediately to prevent multiple triggers
|
||||
Quagga.stop();
|
||||
isScanning = false;
|
||||
|
||||
// Reset UI states to closed
|
||||
const container = document.getElementById('qrContainer');
|
||||
const btn = document.getElementById('scannerBtn');
|
||||
if (container) container.style.display = 'none';
|
||||
if (btn) {
|
||||
btn.classList.remove('open');
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
// Route the scanned data to your input
|
||||
document.getElementById('studentIdInput').value = studentId;
|
||||
alert('Ausweis gescannt: ' + studentId);
|
||||
}
|
||||
|
||||
function onScanError(error) {
|
||||
// Silently ignore scan errors
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -814,46 +734,12 @@ function onScanError(error) {
|
||||
color: #b3261e;
|
||||
}
|
||||
|
||||
/* Mobile: simplify list entries - show only title */
|
||||
@media (max-width: 900px) {
|
||||
.library-item .item-author,
|
||||
.library-item .item-isbn,
|
||||
.table-cell.author-cell,
|
||||
.table-cell.isbn-cell {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Make title more prominent and full-width */
|
||||
.library-item .item-details {
|
||||
display: block;
|
||||
width: calc(100% - 140px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.library-item .item-name {
|
||||
font-size: 1.05rem;
|
||||
margin: 0 0 6px 0;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.library-item .item-image-wrapper {
|
||||
width: 100px;
|
||||
height: 130px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile: allow horizontal scrolling for table-mode views */
|
||||
@media (max-width: 900px) {
|
||||
.library-table-wrapper {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0 -12px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.library-table-wrapper .table-row {
|
||||
min-width: 720px; /* allow table to have intrinsic width and be scrolled */
|
||||
@@ -862,180 +748,5 @@ function onScanError(error) {
|
||||
display: block; /* keep rows stacked but allow horizontal scroll */
|
||||
}
|
||||
}
|
||||
|
||||
/* Hybrid Scanner Styles */
|
||||
.qr-container {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin: 12px 0;
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.scanner-header {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.scanner-header h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 1em;
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.scanner-controls {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.scanner-button {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85em;
|
||||
white-space: nowrap;
|
||||
flex: 0 1 auto;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.scanner-button:hover {
|
||||
background: #1976D2;
|
||||
}
|
||||
|
||||
.scanner-button:active {
|
||||
background: #1565C0;
|
||||
}
|
||||
|
||||
.scanner-video-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 12px 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#qr-reader {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 2px solid #2196F3;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.scanner-message {
|
||||
min-height: 20px;
|
||||
font-size: 0.9em;
|
||||
text-align: center;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.qr-container {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.scanner-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.scanner-header h3 {
|
||||
width: 100%;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.scanner-controls {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.scanner-button {
|
||||
flex: 1 1 auto;
|
||||
padding: 8px 6px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
#qr-reader {
|
||||
max-width: 100%;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.scanner-message {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal styles - responsive for mobile */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 2000;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 1.1rem;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-body img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.modal-body p { margin: 6px 0; }
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.modal-content { padding: 12px; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
+195
-115
@@ -445,7 +445,7 @@
|
||||
<!-- Customizable Filter Panel -->
|
||||
|
||||
<!-- Student card / quick scan workflow -->
|
||||
<div class="library-scan-panel">
|
||||
<div class="library-scan-panel">
|
||||
<div class="library-scan-controls">
|
||||
<select id="scanModeSelect" aria-label="Scan-Modus">
|
||||
<option value="card_only">Nur Ausweis erfassen</option>
|
||||
@@ -458,10 +458,9 @@
|
||||
<div id="scanStatus" class="library-scan-status">
|
||||
Hinweis: Im Schnellmodus zuerst den Schülerausweis scannen, danach den Buch-/Mediencode.
|
||||
</div>
|
||||
<div id="scanReaderWrap" class="library-scan-reader-wrap">
|
||||
<div id="scanReaderWrap" class="library-scan-reader-wrap" style="display: none;">
|
||||
<div id="libraryQrReader" class="library-scan-reader">
|
||||
<video id="libraryQrReaderVideo" playsinline style="width: 100%; border-radius: 8px;"></video>
|
||||
<canvas id="libraryQrReaderCanvas" style="display: none;"></canvas>
|
||||
<div id="library-scanner-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -550,8 +549,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
|
||||
<script src="/static/js/scanner.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
|
||||
<script>
|
||||
// State
|
||||
let libraryItems = [];
|
||||
@@ -851,22 +849,186 @@
|
||||
document.getElementById('detailModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// Keep track of the scanner state globally/outer scope
|
||||
let scannerRunning = false; // Maps directly to your 'isScanning' flag style
|
||||
let activeScannerCallback = null; // Dynamically routes the scanned barcode to the caller
|
||||
let lastScanValue = null;
|
||||
let lastScanAt = 0;
|
||||
let activeStudentCardId = ''; // Managed globally by your system
|
||||
|
||||
// =========================================================================
|
||||
// 1. CORE SCANNER ROUTING ENGINE (QUAGGA2)
|
||||
// =========================================================================
|
||||
|
||||
function startScanner(targetCallback) {
|
||||
// if (scannerRunning) return;
|
||||
|
||||
const readerWrap = document.getElementById('scanReaderWrap');
|
||||
const toggleBtn = document.getElementById('toggleScannerBtn');
|
||||
|
||||
// Store the custom action that should happen when a barcode is found
|
||||
activeScannerCallback = targetCallback;
|
||||
|
||||
if (readerWrap) readerWrap.style.display = 'block';
|
||||
setScanStatus('Initializing camera...', 'warn');
|
||||
|
||||
Quagga.init({
|
||||
inputStream: {
|
||||
name: "Live",
|
||||
type: "LiveStream",
|
||||
// Targets your interior relative viewport box
|
||||
target: document.querySelector('#library-scanner-container'),
|
||||
constraints: {
|
||||
width: 640,
|
||||
height: 480,
|
||||
facingMode: "environment" // Force rear camera
|
||||
},
|
||||
},
|
||||
decoder: {
|
||||
readers: [
|
||||
"code_128_reader",
|
||||
"ean_reader",
|
||||
"code_39_reader",
|
||||
"upc_reader",
|
||||
"codabar_reader",
|
||||
"i2of5_reader"
|
||||
]
|
||||
}
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
console.error('Scanner start failed:', err);
|
||||
if (readerWrap) readerWrap.style.display = 'none';
|
||||
const detail = (err && (err.message || err.name)) ? ` (${err.message || err.name})` : '';
|
||||
setScanStatus(`Scanner konnte nicht gestartet werden${detail}`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
Quagga.start();
|
||||
scannerRunning = true;
|
||||
|
||||
// Update the main toggle button text if no specific edit callback is provided
|
||||
if (!targetCallback && toggleBtn) {
|
||||
toggleBtn.textContent = 'Scanner stoppen';
|
||||
}
|
||||
setScanStatus('Scanner aktiv. Jetzt Code scannen.', 'warn');
|
||||
});
|
||||
}
|
||||
|
||||
function stopScanner() {
|
||||
if (!scannerRunning) return;
|
||||
|
||||
const readerWrap = document.getElementById('scanReaderWrap');
|
||||
const toggleBtn = document.getElementById('toggleScannerBtn');
|
||||
|
||||
Quagga.stop();
|
||||
|
||||
scannerRunning = false;
|
||||
activeScannerCallback = null; // Clear out callback routing
|
||||
|
||||
if (readerWrap) readerWrap.style.display = 'none';
|
||||
if (toggleBtn) toggleBtn.textContent = 'Scanner starten';
|
||||
setScanStatus('Scanner gestoppt.', 'warn');
|
||||
}
|
||||
|
||||
// Unified global reader callback routing data based on who opened the scanner
|
||||
Quagga.onDetected(function(data) {
|
||||
if (!data || !data.codeResult || !data.codeResult.code) return;
|
||||
|
||||
const barcode = String(data.codeResult.code || '').trim();
|
||||
console.log("Barcode detected:", barcode);
|
||||
|
||||
// Keep a local reference to the active callback before stopping the engine
|
||||
const currentCallback = activeScannerCallback;
|
||||
|
||||
// Critical: Stop scanning immediately to prevent multiple triggers
|
||||
stopScanner();
|
||||
|
||||
// 1. If an explicit modal/edit function asked for the scan, route to it directly
|
||||
if (typeof currentCallback === "function") {
|
||||
currentCallback(barcode);
|
||||
} else {
|
||||
// 2. Fallback Default: Pass it to your library dashboard's primary business processing
|
||||
handleScanSuccess(barcode);
|
||||
}
|
||||
});
|
||||
|
||||
// Event Listener for the main standalone library processing scanner button
|
||||
document.getElementById('toggleScannerBtn')?.addEventListener('click', async function() {
|
||||
if (scannerRunning) {
|
||||
await stopScanner();
|
||||
} else {
|
||||
await startScanner(null); // Passing null flags it to run default library processing
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// 2. LIBRARY WORKFLOW AND MODAL IMPLEMENTATIONS
|
||||
// =========================================================================
|
||||
|
||||
function handleScanSuccess(decodedText) {
|
||||
const scannedCode = normalizeScannedCode(decodedText);
|
||||
if (!scannedCode) return;
|
||||
|
||||
// Debounce checks
|
||||
const now = Date.now();
|
||||
if (scannedCode === lastScanValue && (now - lastScanAt) < 1500) {
|
||||
return;
|
||||
}
|
||||
lastScanValue = scannedCode;
|
||||
lastScanAt = now;
|
||||
|
||||
const mode = (document.getElementById('scanModeSelect') || {}).value || 'card_only';
|
||||
if (mode === 'card_only') {
|
||||
setActiveStudentCard(scannedCode);
|
||||
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
|
||||
return;
|
||||
}
|
||||
|
||||
processQuickToggleScan(scannedCode);
|
||||
}
|
||||
|
||||
function scanIntoEditCode() {
|
||||
const scanReaderWrap = document.getElementById('scanReaderWrap');
|
||||
const editCodeInput = document.getElementById('edit-code4');
|
||||
const scanEditBtn = document.getElementById('scan-edit-code-btn');
|
||||
if (!scanReaderWrap || !editCodeInput || !scanEditBtn) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle close if it's already running
|
||||
if (scannerRunning && scanReaderWrap.style.display !== 'none') {
|
||||
stopScanner();
|
||||
scanEditBtn.textContent = 'Barcode scannen';
|
||||
return;
|
||||
}
|
||||
|
||||
scanEditBtn.textContent = 'Scanner schließen';
|
||||
|
||||
// Launch the scanner with custom callback routing to the item edit fields
|
||||
startScanner(function(decodedText) {
|
||||
editCodeInput.value = decodedText;
|
||||
validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null);
|
||||
scanEditBtn.textContent = 'Barcode scannen';
|
||||
});
|
||||
}
|
||||
|
||||
function borrowItem(itemId) {
|
||||
const selectedItem = (libraryItems || []).find(item => item._id === itemId);
|
||||
if (selectedItem && selectedItem.LibraryDisplayStatus === 'damaged') {
|
||||
alert('Dieses Medium ist als defekt/zerstört markiert und kann nicht ausgeliehen werden.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const defaultCardId = activeStudentCardId || '';
|
||||
const cardId = (window.prompt('Bitte Schülerausweis-ID eingeben:', defaultCardId) || '').trim().toUpperCase();
|
||||
if (!cardId) {
|
||||
alert('Ausleihe abgebrochen: Für Bibliotheksmedien ist eine gültige Schülerausweis-ID erforderlich.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setActiveStudentCard(cardId);
|
||||
|
||||
|
||||
const durationInput = (window.prompt('Ausleihdauer in Tagen (optional):') || '').trim();
|
||||
const maxAvailable = Math.max(1, parseInt(selectedItem?.AvailableGroupedCount || selectedItem?.Quantity || 1, 10) || 1);
|
||||
const countPrompt = (window.prompt(`Anzahl ausleihen? (Standard: 1, verfügbar: ${maxAvailable})`, '1') || '').trim();
|
||||
@@ -878,29 +1040,29 @@
|
||||
alert(`Es sind nur ${maxAvailable} Exemplar(e) verfügbar.`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = `/ausleihen/${itemId}`;
|
||||
|
||||
|
||||
const csrfField = document.createElement('input');
|
||||
csrfField.type = 'hidden';
|
||||
csrfField.name = 'csrf_token';
|
||||
csrfField.value = '{{ csrf_token }}';
|
||||
form.appendChild(csrfField);
|
||||
|
||||
|
||||
const cardField = document.createElement('input');
|
||||
cardField.type = 'hidden';
|
||||
cardField.name = 'borrower_card_id';
|
||||
cardField.value = cardId;
|
||||
form.appendChild(cardField);
|
||||
|
||||
|
||||
const returnTargetField = document.createElement('input');
|
||||
returnTargetField.type = 'hidden';
|
||||
returnTargetField.name = 'return_to';
|
||||
returnTargetField.value = 'library';
|
||||
form.appendChild(returnTargetField);
|
||||
|
||||
|
||||
if (durationInput) {
|
||||
const durationField = document.createElement('input');
|
||||
durationField.type = 'hidden';
|
||||
@@ -908,17 +1070,17 @@
|
||||
durationField.value = durationInput;
|
||||
form.appendChild(durationField);
|
||||
}
|
||||
|
||||
|
||||
const countField = document.createElement('input');
|
||||
countField.type = 'hidden';
|
||||
countField.name = 'exemplare_count';
|
||||
countField.value = String(borrowCount || 1);
|
||||
form.appendChild(countField);
|
||||
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
|
||||
function setScanStatus(message, kind = '') {
|
||||
const el = document.getElementById('scanStatus');
|
||||
if (!el) return;
|
||||
@@ -928,7 +1090,7 @@
|
||||
el.classList.add(kind);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function setActiveStudentCard(cardId) {
|
||||
activeStudentCardId = (cardId || '').trim().toUpperCase();
|
||||
const input = document.getElementById('activeStudentCard');
|
||||
@@ -936,18 +1098,18 @@
|
||||
input.value = activeStudentCardId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function normalizeScannedCode(code) {
|
||||
return (code || '').trim();
|
||||
}
|
||||
|
||||
|
||||
async function processQuickToggleScan(scannedCode) {
|
||||
if (!activeStudentCardId) {
|
||||
setActiveStudentCard(scannedCode);
|
||||
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
setScanStatus('Verarbeite Mediencode...', 'warn');
|
||||
const response = await fetch('/api/library_scan_action', {
|
||||
@@ -958,13 +1120,13 @@
|
||||
item_code: scannedCode
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
const result = await response.json();
|
||||
if (!response.ok || !result.ok) {
|
||||
setScanStatus(result.message || 'Scan-Aktion fehlgeschlagen.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (result.action === 'borrowed') {
|
||||
setScanStatus(`Ausgeliehen: ${result.item_name}`, 'ok');
|
||||
} else if (result.action === 'returned') {
|
||||
@@ -972,118 +1134,36 @@
|
||||
} else {
|
||||
setScanStatus(result.message || 'Aktion durchgeführt.', 'ok');
|
||||
}
|
||||
|
||||
|
||||
await loadLibraryItems();
|
||||
} catch (err) {
|
||||
console.error('Quick scan action failed:', err);
|
||||
setScanStatus('Fehler beim Verarbeiten des Scans.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function handleScanSuccess(decodedText) {
|
||||
const scannedCode = normalizeScannedCode(decodedText);
|
||||
if (!scannedCode) return;
|
||||
|
||||
const now = Date.now();
|
||||
if (scannedCode === lastScanValue && (now - lastScanAt) < 1500) {
|
||||
return;
|
||||
}
|
||||
lastScanValue = scannedCode;
|
||||
lastScanAt = now;
|
||||
|
||||
const mode = (document.getElementById('scanModeSelect') || {}).value || 'card_only';
|
||||
if (mode === 'card_only') {
|
||||
setActiveStudentCard(scannedCode);
|
||||
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
|
||||
return;
|
||||
}
|
||||
|
||||
processQuickToggleScan(scannedCode);
|
||||
}
|
||||
|
||||
function handleScanError() {
|
||||
// Intentionally silent to avoid UI spam while camera searches codes.
|
||||
}
|
||||
|
||||
async function ensureScannerLibraryLoaded() {
|
||||
// HybridScanner is loaded globally via script tag
|
||||
return typeof HybridScanner !== 'undefined';
|
||||
}
|
||||
|
||||
async function startScanner() {
|
||||
if (scannerRunning) return;
|
||||
|
||||
const scannerLoaded = await ensureScannerLibraryLoaded();
|
||||
if (!scannerLoaded) {
|
||||
setScanStatus('Scanner-Bibliothek konnte nicht geladen werden.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const readerWrap = document.getElementById('scanReaderWrap');
|
||||
const toggleBtn = document.getElementById('toggleScannerBtn');
|
||||
readerWrap.style.display = 'block';
|
||||
|
||||
try {
|
||||
scannerInstance = scannerInstance || new HybridScanner({
|
||||
videoId: 'libraryQrReaderVideo',
|
||||
canvasId: 'libraryQrReaderCanvas',
|
||||
formats: ['QR_CODE', 'EAN_13', 'EAN_8', 'CODE_128', 'CODE_39', 'UPC_A', 'UPC_E', 'ITF', 'CODABAR'],
|
||||
fps: 10,
|
||||
facingMode: 'environment',
|
||||
onSuccess: handleScanSuccess,
|
||||
onError: handleScanError
|
||||
});
|
||||
|
||||
scannerInstance.start();
|
||||
scannerRunning = true;
|
||||
toggleBtn.textContent = 'Scanner stoppen';
|
||||
setScanStatus('Scanner aktiv. Jetzt Code scannen.', 'warn');
|
||||
} catch (err) {
|
||||
console.error('Scanner start failed:', err);
|
||||
readerWrap.style.display = 'none';
|
||||
const detail = (err && (err.message || err.name)) ? ` (${err.message || err.name})` : '';
|
||||
setScanStatus(`Scanner konnte nicht gestartet werden${detail}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function stopScanner() {
|
||||
if (!scannerRunning || !scannerInstance) return;
|
||||
const readerWrap = document.getElementById('scanReaderWrap');
|
||||
const toggleBtn = document.getElementById('toggleScannerBtn');
|
||||
try {
|
||||
scannerInstance.stop();
|
||||
} catch (err) {
|
||||
console.error('Scanner stop failed:', err);
|
||||
}
|
||||
scannerRunning = false;
|
||||
scannerInstance = null;
|
||||
readerWrap.style.display = 'none';
|
||||
toggleBtn.textContent = 'Scanner starten';
|
||||
setScanStatus('Scanner gestoppt.', 'warn');
|
||||
}
|
||||
|
||||
|
||||
function wireScannerUi() {
|
||||
const toggleBtn = document.getElementById('toggleScannerBtn');
|
||||
const resetBtn = document.getElementById('resetCardBtn');
|
||||
const modeSelect = document.getElementById('scanModeSelect');
|
||||
|
||||
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener('click', async () => {
|
||||
if (scannerRunning) {
|
||||
await stopScanner();
|
||||
} else {
|
||||
await startScanner();
|
||||
await startScanner(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', () => {
|
||||
setActiveStudentCard('');
|
||||
setScanStatus('Ausweis zurückgesetzt. Bitte neu scannen.', 'warn');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (modeSelect) {
|
||||
modeSelect.addEventListener('change', () => {
|
||||
if (modeSelect.value === 'quick_toggle' && !activeStudentCardId) {
|
||||
@@ -1096,18 +1176,18 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getLibraryItemById(itemId) {
|
||||
return libraryItems.find(i => String(i._id) === String(itemId));
|
||||
}
|
||||
|
||||
|
||||
function openEditLibraryItem(itemId) {
|
||||
const item = getLibraryItemById(itemId);
|
||||
if (!item) {
|
||||
alert('Element nicht gefunden.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const modal = document.getElementById('editLibraryModal');
|
||||
document.getElementById('editLibraryItemId').value = item._id || '';
|
||||
document.getElementById('editLibraryName').value = item.Name || '';
|
||||
@@ -1119,7 +1199,7 @@
|
||||
document.getElementById('editLibraryType').value = (item.ItemType || 'book');
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
|
||||
function closeEditLibraryModal() {
|
||||
const modal = document.getElementById('editLibraryModal');
|
||||
modal.style.display = 'none';
|
||||
|
||||
+104
-54
@@ -413,10 +413,7 @@
|
||||
</div>
|
||||
<div class="qr-container">
|
||||
<button id="scanButton" class="scan-button" aria-controls="qr-reader" aria-expanded="false">Barcode scannen</button>
|
||||
<div id="qr-reader" style="display: none;">
|
||||
<video id="qr-reader-video" playsinline style="width: 100%; max-width: 500px; border-radius: 8px;"></video>
|
||||
<canvas id="qr-reader-canvas" style="display: none;"></canvas>
|
||||
</div>
|
||||
<div id="qr-reader"></div>
|
||||
</div>
|
||||
<div id="table-view-header" class="table-view-header" aria-hidden="true">
|
||||
<span>Name</span>
|
||||
@@ -528,9 +525,7 @@
|
||||
|
||||
window.isDebug = false; // Set to true only for development environment
|
||||
</script>
|
||||
|
||||
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
|
||||
<script src="/static/js/scanner.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
|
||||
<script>
|
||||
// Global state
|
||||
const highlightItemId = (window.serverVars && window.serverVars.highlightItemId && window.serverVars.highlightItemId !== 'null')
|
||||
@@ -539,59 +534,114 @@
|
||||
let codeSearchTerm = '';
|
||||
let descSearchIds = null; // Set of matching IDs or null when disabled
|
||||
|
||||
// QR scanner toggle setup
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const scanButton = document.getElementById('scanButton');
|
||||
// Keep track of the scanner state globally/outer scope
|
||||
let isScanning = false;
|
||||
let activeScannerCallback = null; // Tracks which function currently owns the scanner output
|
||||
|
||||
function startScanner(targetCallback) {
|
||||
const qrReader = document.getElementById('qr-reader');
|
||||
let hybridScanner = null;
|
||||
const statusText = document.getElementById('status');
|
||||
|
||||
if (!scanButton || !qrReader) return;
|
||||
// Store the custom action that should happen when a barcode is found
|
||||
activeScannerCallback = targetCallback;
|
||||
|
||||
const setScannerUi = (isOpen) => {
|
||||
scanButton.classList.toggle('is-active', isOpen);
|
||||
scanButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
scanButton.textContent = isOpen ? 'Scanner schliessen' : 'Barcode scannen';
|
||||
};
|
||||
// Show the container
|
||||
if (qrReader) qrReader.style.display = 'block';
|
||||
if (statusText) statusText.innerText = "Initializing camera...";
|
||||
|
||||
scanButton.addEventListener('click', function() {
|
||||
if (qrReader.style.display !== 'block') {
|
||||
qrReader.style.display = 'block';
|
||||
|
||||
hybridScanner = new HybridScanner({
|
||||
videoId: 'qr-reader-video',
|
||||
canvasId: 'qr-reader-canvas',
|
||||
formats: ['QR_CODE', 'CODE_128', 'EAN_13', 'EAN_8', 'CODE_39'],
|
||||
fps: 10,
|
||||
facingMode: 'environment',
|
||||
onSuccess: function(decodedText) {
|
||||
hybridScanner.stop();
|
||||
qrReader.style.display = 'none';
|
||||
|
||||
// Put scanned code into the search box and trigger search
|
||||
const searchInput = document.getElementById('code-search');
|
||||
if (searchInput) {
|
||||
searchInput.value = decodedText;
|
||||
searchByCode();
|
||||
}
|
||||
|
||||
setScannerUi(false);
|
||||
},
|
||||
onError: function(error) {
|
||||
console.error('Scanner error:', error);
|
||||
}
|
||||
});
|
||||
|
||||
hybridScanner.start();
|
||||
setScannerUi(true);
|
||||
} else {
|
||||
if (hybridScanner) {
|
||||
hybridScanner.stop();
|
||||
hybridScanner = null;
|
||||
}
|
||||
qrReader.style.display = 'none';
|
||||
setScannerUi(false);
|
||||
Quagga.init({
|
||||
inputStream: {
|
||||
name: "Live",
|
||||
type: "LiveStream",
|
||||
// Make sure this targets your correct HTML container element
|
||||
target: document.querySelector('#scanner-container'),
|
||||
constraints: {
|
||||
width: 640,
|
||||
height: 480,
|
||||
facingMode: "environment" // Force rear camera
|
||||
},
|
||||
},
|
||||
decoder: {
|
||||
// Your required formats mapped to Quagga2 reader strings
|
||||
readers: ["code_128_reader", "ean_reader", "code_39_reader", "upc_reader"]
|
||||
}
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
console.error("Initialization error:", err);
|
||||
if (statusText) statusText.innerText = "Camera Error";
|
||||
return;
|
||||
}
|
||||
|
||||
Quagga.start();
|
||||
isScanning = true;
|
||||
|
||||
// If we are using the main search button, manage its UI state
|
||||
if (!targetCallback) {
|
||||
setScannerUi(true);
|
||||
}
|
||||
|
||||
if (statusText) statusText.innerText = "Scanning...";
|
||||
});
|
||||
}
|
||||
|
||||
function stopScanner() {
|
||||
Quagga.stop();
|
||||
isScanning = false;
|
||||
activeScannerCallback = null; // Reset the callback routing
|
||||
|
||||
const qrReader = document.getElementById('qr-reader');
|
||||
if (qrReader) qrReader.style.display = 'none';
|
||||
|
||||
setScannerUi(false);
|
||||
}
|
||||
|
||||
function setScannerUi(isOpen) {
|
||||
const scanBtn = document.getElementById('scanButton');
|
||||
if (!scanBtn) return;
|
||||
|
||||
scanBtn.classList.toggle('is-active', isOpen);
|
||||
scanBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
scanBtn.textContent = isOpen ? 'Scanner schliessen' : 'Barcode scannen';
|
||||
}
|
||||
|
||||
// Unified global reader callback
|
||||
Quagga.onDetected(function(data) {
|
||||
const barcode = String(data.codeResult.code || '').trim();
|
||||
console.log("Barcode detected:", barcode);
|
||||
|
||||
// Keep a local reference to the active callback before shutting down the engine
|
||||
const currentCallback = activeScannerCallback;
|
||||
|
||||
// Critical: Stop scanning immediately to prevent multiple triggers
|
||||
stopScanner();
|
||||
|
||||
// Route data based on who opened the scanner
|
||||
if (typeof currentCallback === "function") {
|
||||
// Send data directly to the specific edit field's logic
|
||||
currentCallback(barcode);
|
||||
} else {
|
||||
// Fallback Default: Main search box logic
|
||||
const resultText = document.getElementById('barcode-result');
|
||||
if (resultText) resultText.innerText = barcode;
|
||||
|
||||
const searchInput = document.getElementById('code-search');
|
||||
if (searchInput) {
|
||||
searchInput.value = barcode;
|
||||
if (typeof searchByCode === "function") {
|
||||
searchByCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Event Listener for the main standalone search button
|
||||
document.getElementById('scanButton')?.addEventListener('click', function() {
|
||||
if (!isScanning) {
|
||||
// Pass null so it defaults to filling the standard search box
|
||||
startScanner(null);
|
||||
} else {
|
||||
stopScanner();
|
||||
}
|
||||
});
|
||||
|
||||
// Return preferred primary and fallback image URLs based on extension
|
||||
|
||||
+125
-100
@@ -2436,8 +2436,8 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
<div class="qr-container">
|
||||
<button id="scanButton" class="scan-button" aria-controls="qr-reader" aria-expanded="false">Barcode scannen</button>
|
||||
<div id="qr-reader" style="display: none;">
|
||||
<video id="qr-reader-video" playsinline style="width: 100%; max-width: 100%; height: auto; aspect-ratio: 4/3; object-fit: cover; border-radius: 8px;"></video>
|
||||
<canvas id="qr-reader-canvas" style="display: none;"></canvas>
|
||||
<h3>Status: <span id="status">Bereit</span></h3>
|
||||
<div id="scanner-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2732,8 +2732,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
|
||||
window.isDebug = false; // Set to true only for development environment
|
||||
</script>
|
||||
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
|
||||
<script src="/static/js/scanner.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
|
||||
<script>
|
||||
// Function to check if a file is a video
|
||||
function isVideoFile(filename) {
|
||||
@@ -2743,7 +2742,6 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
}
|
||||
|
||||
// Initialize QR Code scanner and global variables
|
||||
let hybridScanner = null;
|
||||
let codeSearchTerm = '';
|
||||
let descSearchIds = null; // Set of matching IDs or null when disabled
|
||||
let currentUsername = '';
|
||||
@@ -2772,55 +2770,114 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
// No-op in production
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('scanButton').addEventListener('click', function() {
|
||||
|
||||
// Keep track of the scanner state globally/outer scope
|
||||
let isScanning = false;
|
||||
let activeScannerCallback = null; // Tracks which function currently owns the scanner output
|
||||
|
||||
function startScanner(targetCallback) {
|
||||
const qrReader = document.getElementById('qr-reader');
|
||||
const scanBtn = document.getElementById('scanButton');
|
||||
const statusText = document.getElementById('status');
|
||||
|
||||
const setScannerUi = (isOpen) => {
|
||||
if (!scanBtn) return;
|
||||
scanBtn.classList.toggle('is-active', isOpen);
|
||||
scanBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
scanBtn.textContent = isOpen ? 'Scanner schliessen' : 'Barcode scannen';
|
||||
};
|
||||
|
||||
if (qrReader.style.display === 'none') {
|
||||
qrReader.style.display = 'block';
|
||||
|
||||
hybridScanner = new HybridScanner({
|
||||
videoId: 'qr-reader-video',
|
||||
canvasId: 'qr-reader-canvas',
|
||||
formats: ['QR_CODE', 'CODE_128', 'EAN_13', 'EAN_8', 'CODE_39', 'UPC_A'],
|
||||
fps: 10,
|
||||
facingMode: 'environment',
|
||||
onSuccess: function(decodedText) {
|
||||
hybridScanner.stop();
|
||||
qrReader.style.display = 'none';
|
||||
|
||||
// Put the scanned code in the search box
|
||||
const searchInput = document.getElementById('code-search');
|
||||
if (searchInput) {
|
||||
searchInput.value = decodedText;
|
||||
// Trigger search automatically
|
||||
searchByCode();
|
||||
}
|
||||
// Store the custom action that should happen when a barcode is found
|
||||
activeScannerCallback = targetCallback;
|
||||
|
||||
setScannerUi(false);
|
||||
// Show the container
|
||||
if (qrReader) qrReader.style.display = 'block';
|
||||
if (statusText) statusText.innerText = "Initializing camera...";
|
||||
|
||||
Quagga.init({
|
||||
inputStream: {
|
||||
name: "Live",
|
||||
type: "LiveStream",
|
||||
// Make sure this targets your correct HTML container element
|
||||
target: document.querySelector('#scanner-container'),
|
||||
constraints: {
|
||||
width: 640,
|
||||
height: 480,
|
||||
facingMode: "environment" // Force rear camera
|
||||
},
|
||||
onError: function(error) {
|
||||
console.error('Scanner error:', error);
|
||||
}
|
||||
});
|
||||
|
||||
hybridScanner.start();
|
||||
setScannerUi(true);
|
||||
} else {
|
||||
if (hybridScanner) {
|
||||
hybridScanner.stop();
|
||||
hybridScanner = null;
|
||||
},
|
||||
decoder: {
|
||||
// Your required formats mapped to Quagga2 reader strings
|
||||
readers: ["code_128_reader", "ean_reader", "code_39_reader", "upc_reader"]
|
||||
}
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
console.error("Initialization error:", err);
|
||||
if (statusText) statusText.innerText = "Camera Error";
|
||||
return;
|
||||
}
|
||||
qrReader.style.display = 'none';
|
||||
setScannerUi(false);
|
||||
|
||||
Quagga.start();
|
||||
isScanning = true;
|
||||
|
||||
// If we are using the main search button, manage its UI state
|
||||
if (!targetCallback) {
|
||||
setScannerUi(true);
|
||||
}
|
||||
|
||||
if (statusText) statusText.innerText = "Scanning...";
|
||||
});
|
||||
}
|
||||
|
||||
function stopScanner() {
|
||||
Quagga.stop();
|
||||
isScanning = false;
|
||||
activeScannerCallback = null; // Reset the callback routing
|
||||
|
||||
const qrReader = document.getElementById('qr-reader');
|
||||
if (qrReader) qrReader.style.display = 'none';
|
||||
|
||||
setScannerUi(false);
|
||||
}
|
||||
|
||||
function setScannerUi(isOpen) {
|
||||
const scanBtn = document.getElementById('scanButton');
|
||||
if (!scanBtn) return;
|
||||
|
||||
scanBtn.classList.toggle('is-active', isOpen);
|
||||
scanBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
scanBtn.textContent = isOpen ? 'Scanner schliessen' : 'Barcode scannen';
|
||||
}
|
||||
|
||||
// Unified global reader callback
|
||||
Quagga.onDetected(function(data) {
|
||||
const barcode = String(data.codeResult.code || '').trim();
|
||||
console.log("Barcode detected:", barcode);
|
||||
|
||||
// Keep a local reference to the active callback before shutting down the engine
|
||||
const currentCallback = activeScannerCallback;
|
||||
|
||||
// Critical: Stop scanning immediately to prevent multiple triggers
|
||||
stopScanner();
|
||||
|
||||
// Route data based on who opened the scanner
|
||||
if (typeof currentCallback === "function") {
|
||||
// Send data directly to the specific edit field's logic
|
||||
currentCallback(barcode);
|
||||
} else {
|
||||
// Fallback Default: Main search box logic
|
||||
const resultText = document.getElementById('barcode-result');
|
||||
if (resultText) resultText.innerText = barcode;
|
||||
|
||||
const searchInput = document.getElementById('code-search');
|
||||
if (searchInput) {
|
||||
searchInput.value = barcode;
|
||||
if (typeof searchByCode === "function") {
|
||||
searchByCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Event Listener for the main standalone search button
|
||||
document.getElementById('scanButton')?.addEventListener('click', function() {
|
||||
if (!isScanning) {
|
||||
// Pass null so it defaults to filling the standard search box
|
||||
startScanner(null);
|
||||
} else {
|
||||
stopScanner();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2832,37 +2889,21 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
return;
|
||||
}
|
||||
|
||||
if (qrReader.style.display !== 'none') {
|
||||
if (hybridScanner) {
|
||||
hybridScanner.stop();
|
||||
hybridScanner = null;
|
||||
}
|
||||
qrReader.style.display = 'none';
|
||||
// Toggle close if it's already running
|
||||
if (isScanning && qrReader.style.display !== 'none') {
|
||||
stopScanner();
|
||||
scanEditBtn.textContent = 'Barcode scannen';
|
||||
return;
|
||||
}
|
||||
|
||||
qrReader.style.display = 'block';
|
||||
scanEditBtn.textContent = 'Scanner schließen';
|
||||
hybridScanner = new HybridScanner({
|
||||
videoId: 'qr-reader-video',
|
||||
canvasId: 'qr-reader-canvas',
|
||||
formats: ['CODE_128', 'CODE_39', 'QR_CODE'],
|
||||
fps: 10,
|
||||
facingMode: 'environment',
|
||||
onSuccess: function(decodedText) {
|
||||
hybridScanner.stop();
|
||||
qrReader.style.display = 'none';
|
||||
editCodeInput.value = String(decodedText || '').trim();
|
||||
validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null);
|
||||
scanEditBtn.textContent = 'Barcode scannen';
|
||||
},
|
||||
onError: function(error) {
|
||||
console.error('Scanner error:', error);
|
||||
}
|
||||
|
||||
// Start scanner with custom logic mapping to the Code input field
|
||||
startScanner(function(decodedText) {
|
||||
editCodeInput.value = decodedText;
|
||||
validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null);
|
||||
scanEditBtn.textContent = 'Barcode scannen';
|
||||
});
|
||||
|
||||
hybridScanner.start();
|
||||
}
|
||||
|
||||
function scanIntoEditIsbn() {
|
||||
@@ -2873,39 +2914,23 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
return;
|
||||
}
|
||||
|
||||
if (qrReader.style.display !== 'none') {
|
||||
if (hybridScanner) {
|
||||
hybridScanner.stop();
|
||||
hybridScanner = null;
|
||||
}
|
||||
qrReader.style.display = 'none';
|
||||
// Toggle close if it's already running
|
||||
if (isScanning && qrReader.style.display !== 'none') {
|
||||
stopScanner();
|
||||
scanIsbnBtn.textContent = 'ISBN scannen';
|
||||
return;
|
||||
}
|
||||
|
||||
qrReader.style.display = 'block';
|
||||
scanIsbnBtn.textContent = 'Scanner schließen';
|
||||
hybridScanner = new HybridScanner({
|
||||
videoId: 'qr-reader-video',
|
||||
canvasId: 'qr-reader-canvas',
|
||||
formats: ['ISBN', 'EAN_13', 'EAN_8', 'QR_CODE'],
|
||||
fps: 10,
|
||||
facingMode: 'environment',
|
||||
onSuccess: function(decodedText) {
|
||||
hybridScanner.stop();
|
||||
qrReader.style.display = 'none';
|
||||
editIsbnInput.value = String(decodedText || '').trim();
|
||||
scanIsbnBtn.textContent = 'ISBN scannen';
|
||||
if (typeof fetchBookInfo === 'function') {
|
||||
fetchBookInfo('edit');
|
||||
}
|
||||
},
|
||||
onError: function(error) {
|
||||
console.error('Scanner error:', error);
|
||||
|
||||
// Start scanner with custom logic mapping to the ISBN input field
|
||||
startScanner(function(decodedText) {
|
||||
editIsbnInput.value = decodedText;
|
||||
scanIsbnBtn.textContent = 'ISBN scannen';
|
||||
if (typeof fetchBookInfo === 'function') {
|
||||
fetchBookInfo('edit');
|
||||
}
|
||||
});
|
||||
|
||||
hybridScanner.start();
|
||||
}
|
||||
|
||||
function rebuildFilter3Options() {
|
||||
|
||||
@@ -876,10 +876,7 @@
|
||||
<button type="button" id="scan-code4-btn" class="fetch-isbn-button">Barcode scannen</button>
|
||||
</div>
|
||||
<small style="display:block; color:#666;">Einzelcode manuell setzen oder per Scanner erfassen.</small>
|
||||
<div id="code4-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;">
|
||||
<video id="code4-scanner-video" playsinline style="width: 100%; border-radius: 8px;"></video>
|
||||
<canvas id="code4-scanner-canvas" style="display: none;"></canvas>
|
||||
</div>
|
||||
<div id="code4-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;"></div>
|
||||
<small id="code4-scan-status" style="display:block; color:#666; margin-top:6px;"></small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -910,10 +907,7 @@
|
||||
<button type="button" id="scan-isbn-btn" class="fetch-isbn-button">Barcode scannen</button>
|
||||
<button type="button" class="fetch-isbn-button" onclick="fetchBookInfo('upload')">Bild abrufen</button>
|
||||
</div>
|
||||
<div id="isbn-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;">
|
||||
<video id="isbn-scanner-video" playsinline style="width: 100%; border-radius: 8px;"></video>
|
||||
<canvas id="isbn-scanner-canvas" style="display: none;"></canvas>
|
||||
</div>
|
||||
<div id="isbn-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;"></div>
|
||||
<small id="isbn-scan-status" style="display:block; color:#666; margin-top:6px;">Scannen oder manuell eingeben. Gültige ISBNs helfen beim Abruf von Buchdaten, andere Codes werden trotzdem akzeptiert.</small>
|
||||
<div id="book-info-container" class="book-info-container"></div>
|
||||
</div>
|
||||
@@ -977,8 +971,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
|
||||
<script src="/static/js/scanner.js"></script>
|
||||
<script src="https://unpkg.com/html5-qrcode@2.0.9/dist/html5-qrcode.min.js"></script>
|
||||
<script>
|
||||
const libraryModuleEnabled = {{ 'true' if library_module_enabled else 'false' }};
|
||||
|
||||
@@ -1057,19 +1050,16 @@
|
||||
|
||||
// Stop ISBN scanner if currently running to avoid camera conflicts.
|
||||
if (isbnScannerRunning && isbnScannerInstance) {
|
||||
isbnScannerInstance.stop();
|
||||
isbnScannerInstance = null;
|
||||
isbnScannerInstance.clear().catch(() => {});
|
||||
const isbnScannerBox = document.getElementById('isbn-scanner');
|
||||
const isbnScanButton = document.getElementById('scan-isbn-btn');
|
||||
if (isbnScannerBox) isbnScannerBox.style.display = 'none';
|
||||
if (isbnScanButton) isbnScanButton.textContent = 'ISBN scannen';
|
||||
isbnScannerRunning = false;
|
||||
setIsbnScanStatus('Scanner gestoppt.');
|
||||
}
|
||||
|
||||
if (code4ScannerRunning && code4ScannerInstance) {
|
||||
code4ScannerInstance.stop();
|
||||
code4ScannerInstance = null;
|
||||
code4ScannerInstance.clear().catch(() => {});
|
||||
scannerBox.style.display = 'none';
|
||||
scanButton.textContent = 'Barcode scannen';
|
||||
code4ScannerRunning = false;
|
||||
@@ -1079,36 +1069,29 @@
|
||||
|
||||
scannerBox.style.display = 'block';
|
||||
scanButton.textContent = 'Scanner stoppen';
|
||||
code4ScannerInstance = new Html5QrcodeScanner('code4-scanner', {
|
||||
fps: 10,
|
||||
qrbox: 250,
|
||||
rememberLastUsedCamera: true
|
||||
});
|
||||
code4ScannerRunning = true;
|
||||
setCode4ScanStatus('Scanner läuft. Der Scan wird im Basis-Codefeld übernommen.');
|
||||
|
||||
code4ScannerInstance = new HybridScanner({
|
||||
videoId: 'code4-scanner-video',
|
||||
canvasId: 'code4-scanner-canvas',
|
||||
formats: ['CODE_128', 'CODE_39', 'QR_CODE'],
|
||||
fps: 10,
|
||||
facingMode: 'environment',
|
||||
onSuccess: function(decodedText) {
|
||||
const scannedCode = String(decodedText || '').trim();
|
||||
if (!scannedCode) return;
|
||||
code4ScannerInstance.render((decodedText) => {
|
||||
const scannedCode = String(decodedText || '').trim();
|
||||
if (!scannedCode) return;
|
||||
|
||||
const now = Date.now();
|
||||
if (scannedCode === code4LastScanned && (now - code4LastScannedAt) < 1500) {
|
||||
return;
|
||||
}
|
||||
code4LastScanned = scannedCode;
|
||||
code4LastScannedAt = now;
|
||||
|
||||
codeField.value = scannedCode;
|
||||
validateCodeField(codeField);
|
||||
setCode4ScanStatus(`Code_4 gesetzt: ${scannedCode}`);
|
||||
},
|
||||
onError: function(error) {
|
||||
setCode4ScanStatus(`Fehler: ${error}`, true);
|
||||
const now = Date.now();
|
||||
if (scannedCode === code4LastScanned && (now - code4LastScannedAt) < 1500) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
code4LastScanned = scannedCode;
|
||||
code4LastScannedAt = now;
|
||||
|
||||
code4ScannerInstance.start();
|
||||
codeField.value = scannedCode;
|
||||
validateCodeField(codeField);
|
||||
setCode4ScanStatus(`Code_4 gesetzt: ${scannedCode}`);
|
||||
}, () => {});
|
||||
}
|
||||
|
||||
function startIsbnScanner() {
|
||||
@@ -1123,8 +1106,7 @@
|
||||
|
||||
// Stop Code_4 scanner if currently running to avoid camera conflicts.
|
||||
if (code4ScannerRunning && code4ScannerInstance) {
|
||||
code4ScannerInstance.stop();
|
||||
code4ScannerInstance = null;
|
||||
code4ScannerInstance.clear().catch(() => {});
|
||||
const codeScannerBox = document.getElementById('code4-scanner');
|
||||
const codeScanButton = document.getElementById('scan-code4-btn');
|
||||
if (codeScannerBox) codeScannerBox.style.display = 'none';
|
||||
@@ -1134,8 +1116,7 @@
|
||||
}
|
||||
|
||||
if (isbnScannerRunning && isbnScannerInstance) {
|
||||
isbnScannerInstance.stop();
|
||||
isbnScannerInstance = null;
|
||||
isbnScannerInstance.clear().catch(() => {});
|
||||
scannerBox.style.display = 'none';
|
||||
scanButton.textContent = 'ISBN scannen';
|
||||
isbnScannerRunning = false;
|
||||
@@ -1145,16 +1126,15 @@
|
||||
|
||||
scannerBox.style.display = 'block';
|
||||
scanButton.textContent = 'Scanner stoppen';
|
||||
isbnScannerInstance = new Html5QrcodeScanner('isbn-scanner', {
|
||||
fps: 10,
|
||||
qrbox: 250,
|
||||
rememberLastUsedCamera: true
|
||||
});
|
||||
isbnScannerRunning = true;
|
||||
setIsbnScanStatus('Scanner läuft. Bitte ISBN-Code erfassen.');
|
||||
|
||||
isbnScannerInstance = new HybridScanner({
|
||||
videoId: 'isbn-scanner-video',
|
||||
canvasId: 'isbn-scanner-canvas',
|
||||
formats: ['ISBN', 'EAN_13', 'EAN_8', 'QR_CODE'],
|
||||
fps: 10,
|
||||
facingMode: 'environment',
|
||||
onSuccess: function(decodedText) {
|
||||
isbnScannerInstance.render((decodedText) => {
|
||||
const scannedCode = String(decodedText || '').trim();
|
||||
if (!scannedCode) return;
|
||||
|
||||
@@ -1167,11 +1147,7 @@
|
||||
setIsbnScanStatus('Kein gültiges ISBN-Format erkannt, der gescannte Wert bleibt aber im Feld.', true);
|
||||
}
|
||||
|
||||
// Stop scanner after successful scan
|
||||
if (isbnScannerInstance) {
|
||||
isbnScannerInstance.stop();
|
||||
isbnScannerInstance = null;
|
||||
}
|
||||
isbnScannerInstance.clear().catch(() => {});
|
||||
scannerBox.style.display = 'none';
|
||||
scanButton.textContent = 'ISBN scannen';
|
||||
isbnScannerRunning = false;
|
||||
@@ -1180,13 +1156,7 @@
|
||||
// Automatically load book metadata after a valid ISBN scan.
|
||||
fetchBookInfo('upload');
|
||||
}
|
||||
},
|
||||
onError: function(error) {
|
||||
setIsbnScanStatus(`Fehler: ${error}`, true);
|
||||
}
|
||||
});
|
||||
|
||||
isbnScannerInstance.start();
|
||||
}, () => {});
|
||||
}
|
||||
|
||||
// Load predefined filter values for dropdowns
|
||||
|
||||
Reference in New Issue
Block a user