Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 04b074454a | |||
| 4bda2e044b | |||
| b83b6c0ba2 | |||
| ab718d6ac2 | |||
| 112d9b6aa0 | |||
| 5139a50a43 | |||
| 997c7b42d5 | |||
| 20e5d3bb9a | |||
| f62b22c2f7 |
+1
-1
@@ -1 +1 @@
|
|||||||
v0.8.26
|
v0.8.31
|
||||||
|
|||||||
+113
-65
@@ -3484,6 +3484,66 @@ def api_library_scan_action():
|
|||||||
client.close()
|
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>')
|
@app.route('/api/item_detail/<item_id>')
|
||||||
def api_item_detail(item_id):
|
def api_item_detail(item_id):
|
||||||
"""
|
"""
|
||||||
@@ -4823,6 +4883,22 @@ def get_user_appointments():
|
|||||||
|
|
||||||
result = []
|
result = []
|
||||||
import re as _re
|
import re as _re
|
||||||
|
|
||||||
|
def _date_iter(start_value: str, end_value: str):
|
||||||
|
try:
|
||||||
|
start_date = datetime.datetime.strptime(start_value, '%Y-%m-%d').date()
|
||||||
|
end_date = datetime.datetime.strptime(end_value, '%Y-%m-%d').date()
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
if end_date < start_date:
|
||||||
|
end_date = start_date
|
||||||
|
cursor = start_date
|
||||||
|
days = []
|
||||||
|
while cursor <= end_date:
|
||||||
|
days.append(cursor.strftime('%Y-%m-%d'))
|
||||||
|
cursor += datetime.timedelta(days=1)
|
||||||
|
return days
|
||||||
|
|
||||||
for appt in appointments:
|
for appt in appointments:
|
||||||
appt_id = str(appt.get('_id') or '')
|
appt_id = str(appt.get('_id') or '')
|
||||||
if not appt_id:
|
if not appt_id:
|
||||||
@@ -4831,72 +4907,44 @@ def get_user_appointments():
|
|||||||
date_start = str(appt.get('date_start') or '')
|
date_start = str(appt.get('date_start') or '')
|
||||||
date_end = str(appt.get('date_end') or date_start)
|
date_end = str(appt.get('date_end') or date_start)
|
||||||
time_span = appt.get('time_span', []) or []
|
time_span = appt.get('time_span', []) or []
|
||||||
|
|
||||||
# Determine a sensible start and end datetime for the event.
|
|
||||||
# Prefer a time from the time_span for the first day and a time for the last day when multi-day.
|
|
||||||
start_dt_str = date_start
|
|
||||||
end_dt_str = date_end
|
|
||||||
first_time = None
|
|
||||||
last_time = None
|
|
||||||
generic_first = None
|
|
||||||
generic_last = None
|
|
||||||
try:
|
|
||||||
# collect times from spans
|
|
||||||
for entry in time_span:
|
|
||||||
s = str(entry or '').strip()
|
|
||||||
if not s:
|
|
||||||
continue
|
|
||||||
m_date = _re.match(r"^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})-(\d{2}:\d{2})$", s)
|
|
||||||
if m_date:
|
|
||||||
d = m_date.group(1)
|
|
||||||
if d == date_start and not first_time:
|
|
||||||
first_time = m_date.group(2)
|
|
||||||
if d == date_end:
|
|
||||||
last_time = m_date.group(3)
|
|
||||||
continue
|
|
||||||
m = _re.match(r"^(\d{2}:\d{2})-(\d{2}:\d{2})$", s)
|
|
||||||
if m:
|
|
||||||
if generic_first is None:
|
|
||||||
generic_first = m.group(1)
|
|
||||||
generic_last = m.group(2)
|
|
||||||
|
|
||||||
if not first_time and generic_first:
|
|
||||||
first_time = generic_first
|
|
||||||
if not last_time and generic_last:
|
|
||||||
last_time = generic_last
|
|
||||||
|
|
||||||
if first_time:
|
|
||||||
start_dt_str = f"{date_start}T{first_time}"
|
|
||||||
|
|
||||||
# If appointment spans multiple days and we have a last_time for the final day, use it
|
|
||||||
if date_end != date_start:
|
|
||||||
if last_time:
|
|
||||||
end_dt_str = f"{date_end}T{last_time}"
|
|
||||||
else:
|
|
||||||
# span until end of final day
|
|
||||||
end_dt_str = f"{date_end}T23:59:59"
|
|
||||||
else:
|
|
||||||
if last_time:
|
|
||||||
end_dt_str = f"{date_start}T{last_time}"
|
|
||||||
except Exception:
|
|
||||||
# fallback: use whole-day span
|
|
||||||
if date_end != date_start:
|
|
||||||
end_dt_str = f"{date_end}T23:59:59"
|
|
||||||
|
|
||||||
title = appt.get('note') or f"Termin von {appt.get('user') or ''}"
|
title = appt.get('note') or f"Termin von {appt.get('user') or ''}"
|
||||||
result.append({
|
days_in_range = _date_iter(date_start, date_end) or ([date_start] if date_start else [])
|
||||||
'id': appt_id,
|
|
||||||
'title': title,
|
span_entries = []
|
||||||
'start': start_dt_str,
|
for entry in time_span:
|
||||||
'end': end_dt_str,
|
s = str(entry or '').strip()
|
||||||
'status': 'planned',
|
if not s:
|
||||||
'itemId': appt_id,
|
continue
|
||||||
'userName': str(appt.get('user') or ''),
|
m_date = _re.match(r"^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})-(\d{2}:\d{2})$", s)
|
||||||
'notes': str(appt.get('note') or ''),
|
if m_date:
|
||||||
'period': None,
|
span_entries.append((m_date.group(1), m_date.group(2), m_date.group(3)))
|
||||||
'isCurrentUser': True,
|
continue
|
||||||
'itemBorrower': '',
|
m = _re.match(r"^(\d{2}:\d{2})-(\d{2}:\d{2})$", s)
|
||||||
})
|
if m:
|
||||||
|
for day in days_in_range:
|
||||||
|
span_entries.append((day, m.group(1), m.group(2)))
|
||||||
|
|
||||||
|
# If the stored span is already day-specific, use it as-is.
|
||||||
|
# If it was generic, we duplicated it to each day in the range above.
|
||||||
|
if not span_entries and days_in_range:
|
||||||
|
# Fallback: create a simple all-day marker for each date so the appointment is visible.
|
||||||
|
for day in days_in_range:
|
||||||
|
span_entries.append((day, '08:00', '16:45'))
|
||||||
|
|
||||||
|
for day, start_time, end_time in span_entries:
|
||||||
|
result.append({
|
||||||
|
'id': f"{appt_id}-{day}-{start_time}",
|
||||||
|
'title': title,
|
||||||
|
'start': f"{day}T{start_time}",
|
||||||
|
'end': f"{day}T{end_time}",
|
||||||
|
'status': 'planned',
|
||||||
|
'itemId': appt_id,
|
||||||
|
'userName': str(appt.get('user') or ''),
|
||||||
|
'notes': str(appt.get('note') or ''),
|
||||||
|
'period': None,
|
||||||
|
'isCurrentUser': True,
|
||||||
|
'itemBorrower': '',
|
||||||
|
})
|
||||||
|
|
||||||
return jsonify({'ok': True, 'bookings': result})
|
return jsonify({'ok': True, 'bookings': result})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -15,3 +15,4 @@ openpyxl
|
|||||||
cryptography>=42.0.0
|
cryptography>=42.0.0
|
||||||
pywebpush
|
pywebpush
|
||||||
py-vapid>=1.9.0
|
py-vapid>=1.9.0
|
||||||
|
pyzbar
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
// 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';
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
+207
-12
@@ -67,8 +67,22 @@
|
|||||||
|
|
||||||
<!-- Scanner Panel -->
|
<!-- Scanner Panel -->
|
||||||
<div id="qrContainer" class="qr-container" style="display: none;">
|
<div id="qrContainer" class="qr-container" style="display: none;">
|
||||||
<div id="qr-reader" style="width: auto; height: 300px;"></div>
|
<div class="scanner-header">
|
||||||
<span id="qr-result" style="display: none;"></span>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Table Header (only in table mode) -->
|
<!-- Table Header (only in table mode) -->
|
||||||
@@ -206,7 +220,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/html5-qrcode/minified/html5-qrcode.min.js"></script>
|
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
|
||||||
|
<script src="/static/js/scanner.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// View mode persistence
|
// View mode persistence
|
||||||
const LIBRARY_VIEW_MODE_KEY = 'inventarLibraryViewMode';
|
const LIBRARY_VIEW_MODE_KEY = 'inventarLibraryViewMode';
|
||||||
@@ -631,8 +646,9 @@ document.getElementById('favoriteToggle').addEventListener('click', function() {
|
|||||||
// TODO: Implement favorites filtering
|
// TODO: Implement favorites filtering
|
||||||
});
|
});
|
||||||
|
|
||||||
// Scanner toggle
|
// Scanner instance (hybrid: real-time + upload + keyboard)
|
||||||
let scanner = null;
|
let scanner = null;
|
||||||
|
|
||||||
document.getElementById('scannerBtn').addEventListener('click', function(e) {
|
document.getElementById('scannerBtn').addEventListener('click', function(e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const container = document.getElementById('qrContainer');
|
const container = document.getElementById('qrContainer');
|
||||||
@@ -640,7 +656,10 @@ document.getElementById('scannerBtn').addEventListener('click', function(e) {
|
|||||||
|
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
container.style.display = 'none';
|
container.style.display = 'none';
|
||||||
if (scanner) scanner.clear();
|
if (scanner) {
|
||||||
|
scanner.stop();
|
||||||
|
scanner = null;
|
||||||
|
}
|
||||||
this.classList.remove('open');
|
this.classList.remove('open');
|
||||||
this.setAttribute('aria-expanded', 'false');
|
this.setAttribute('aria-expanded', 'false');
|
||||||
} else {
|
} else {
|
||||||
@@ -649,17 +668,66 @@ document.getElementById('scannerBtn').addEventListener('click', function(e) {
|
|||||||
this.setAttribute('aria-expanded', 'true');
|
this.setAttribute('aria-expanded', 'true');
|
||||||
|
|
||||||
if (!scanner) {
|
if (!scanner) {
|
||||||
scanner = new Html5Qrcode('qr-reader');
|
initScanner();
|
||||||
scanner.start(
|
|
||||||
{ facingMode: "environment" },
|
|
||||||
{ fps: 10, qrbox: 250 },
|
|
||||||
onScanSuccess,
|
|
||||||
onScanError
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
messageEl.textContent = '✗ ' + error;
|
||||||
|
messageEl.style.color = '#f44336';
|
||||||
|
e.target.value = '';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function onScanSuccess(decodedText, decodedResult) {
|
function onScanSuccess(decodedText, decodedResult) {
|
||||||
const studentId = decodedText.trim();
|
const studentId = decodedText.trim();
|
||||||
document.getElementById('studentIdInput').value = studentId;
|
document.getElementById('studentIdInput').value = studentId;
|
||||||
@@ -729,6 +797,9 @@ function onScanError(error) {
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0 -12px;
|
||||||
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
.library-table-wrapper .table-row {
|
.library-table-wrapper .table-row {
|
||||||
min-width: 720px; /* allow table to have intrinsic width and be scrolled */
|
min-width: 720px; /* allow table to have intrinsic width and be scrolled */
|
||||||
@@ -737,5 +808,129 @@ function onScanError(error) {
|
|||||||
display: block; /* keep rows stacked but allow horizontal scroll */
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -459,7 +459,10 @@
|
|||||||
Hinweis: Im Schnellmodus zuerst den Schülerausweis scannen, danach den Buch-/Mediencode.
|
Hinweis: Im Schnellmodus zuerst den Schülerausweis scannen, danach den Buch-/Mediencode.
|
||||||
</div>
|
</div>
|
||||||
<div id="scanReaderWrap" class="library-scan-reader-wrap">
|
<div id="scanReaderWrap" class="library-scan-reader-wrap">
|
||||||
<div id="libraryQrReader" class="library-scan-reader"></div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="filterPanel" class="library-filter-panel">
|
<div id="filterPanel" class="library-filter-panel">
|
||||||
@@ -547,7 +550,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/html5-qrcode/minified/html5-qrcode.min.js"></script>
|
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
|
||||||
|
<script src="/static/js/scanner.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// State
|
// State
|
||||||
let libraryItems = [];
|
let libraryItems = [];
|
||||||
@@ -1002,54 +1006,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensureScannerLibraryLoaded() {
|
async function ensureScannerLibraryLoaded() {
|
||||||
if (typeof Html5QrcodeScanner !== 'undefined') {
|
// HybridScanner is loaded globally via script tag
|
||||||
return true;
|
return typeof HybridScanner !== 'undefined';
|
||||||
}
|
|
||||||
|
|
||||||
const sources = [
|
|
||||||
'https://cdn.jsdelivr.net/npm/html5-qrcode/minified/html5-qrcode.min.js'
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const src of sources) {
|
|
||||||
try {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const existing = document.querySelector(`script[data-scanner-src="${src}"]`);
|
|
||||||
if (existing) {
|
|
||||||
const onLoad = () => resolve();
|
|
||||||
const onError = () => reject(new Error('Script load failed'));
|
|
||||||
existing.addEventListener('load', onLoad, { once: true });
|
|
||||||
existing.addEventListener('error', onError, { once: true });
|
|
||||||
setTimeout(() => {
|
|
||||||
existing.removeEventListener('load', onLoad);
|
|
||||||
existing.removeEventListener('error', onError);
|
|
||||||
if (typeof Html5QrcodeScanner !== 'undefined') {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject(new Error('Script not available'));
|
|
||||||
}
|
|
||||||
}, 1200);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.src = src;
|
|
||||||
script.async = true;
|
|
||||||
script.defer = true;
|
|
||||||
script.dataset.scannerSrc = src;
|
|
||||||
script.onload = () => resolve();
|
|
||||||
script.onerror = () => reject(new Error('Script load failed'));
|
|
||||||
document.head.appendChild(script);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (typeof Html5QrcodeScanner !== 'undefined') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('Scanner library load failed from', src, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startScanner() {
|
async function startScanner() {
|
||||||
@@ -1066,37 +1024,17 @@
|
|||||||
readerWrap.style.display = 'block';
|
readerWrap.style.display = 'block';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formats = [];
|
scannerInstance = scannerInstance || new HybridScanner({
|
||||||
if (typeof Html5QrcodeSupportedFormats !== 'undefined') {
|
videoId: 'libraryQrReaderVideo',
|
||||||
formats.push(
|
canvasId: 'libraryQrReaderCanvas',
|
||||||
Html5QrcodeSupportedFormats.QR_CODE,
|
formats: ['QR_CODE', 'EAN_13', 'EAN_8', 'CODE_128', 'CODE_39', 'UPC_A', 'UPC_E', 'ITF', 'CODABAR'],
|
||||||
Html5QrcodeSupportedFormats.EAN_13,
|
|
||||||
Html5QrcodeSupportedFormats.EAN_8,
|
|
||||||
Html5QrcodeSupportedFormats.CODE_128,
|
|
||||||
Html5QrcodeSupportedFormats.CODE_39,
|
|
||||||
Html5QrcodeSupportedFormats.UPC_A,
|
|
||||||
Html5QrcodeSupportedFormats.UPC_E,
|
|
||||||
Html5QrcodeSupportedFormats.ITF,
|
|
||||||
Html5QrcodeSupportedFormats.CODABAR
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const scannerConfig = {
|
|
||||||
fps: 10,
|
fps: 10,
|
||||||
rememberLastUsedCamera: true,
|
facingMode: 'environment',
|
||||||
aspectRatio: 1.333334
|
onSuccess: handleScanSuccess,
|
||||||
};
|
onError: handleScanError
|
||||||
if (formats.length > 0) {
|
});
|
||||||
scannerConfig.formatsToSupport = formats;
|
|
||||||
}
|
|
||||||
|
|
||||||
scannerInstance = scannerInstance || new Html5QrcodeScanner(
|
scannerInstance.start();
|
||||||
'libraryQrReader',
|
|
||||||
scannerConfig,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
scannerInstance.render(handleScanSuccess, handleScanError);
|
|
||||||
scannerRunning = true;
|
scannerRunning = true;
|
||||||
toggleBtn.textContent = 'Scanner stoppen';
|
toggleBtn.textContent = 'Scanner stoppen';
|
||||||
setScanStatus('Scanner aktiv. Jetzt Code scannen.', 'warn');
|
setScanStatus('Scanner aktiv. Jetzt Code scannen.', 'warn');
|
||||||
@@ -1113,7 +1051,7 @@
|
|||||||
const readerWrap = document.getElementById('scanReaderWrap');
|
const readerWrap = document.getElementById('scanReaderWrap');
|
||||||
const toggleBtn = document.getElementById('toggleScannerBtn');
|
const toggleBtn = document.getElementById('toggleScannerBtn');
|
||||||
try {
|
try {
|
||||||
await scannerInstance.clear();
|
scannerInstance.stop();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Scanner stop failed:', err);
|
console.error('Scanner stop failed:', err);
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-20
@@ -413,7 +413,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="qr-container">
|
<div class="qr-container">
|
||||||
<button id="scanButton" class="scan-button" aria-controls="qr-reader" aria-expanded="false">Barcode scannen</button>
|
<button id="scanButton" class="scan-button" aria-controls="qr-reader" aria-expanded="false">Barcode scannen</button>
|
||||||
<div id="qr-reader"></div>
|
<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>
|
</div>
|
||||||
<div id="table-view-header" class="table-view-header" aria-hidden="true">
|
<div id="table-view-header" class="table-view-header" aria-hidden="true">
|
||||||
<span>Name</span>
|
<span>Name</span>
|
||||||
@@ -526,7 +529,8 @@
|
|||||||
window.isDebug = false; // Set to true only for development environment
|
window.isDebug = false; // Set to true only for development environment
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="https://unpkg.com/html5-qrcode@2.0.9/dist/html5-qrcode.min.js"></script>
|
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
|
||||||
|
<script src="/static/js/scanner.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Global state
|
// Global state
|
||||||
const highlightItemId = (window.serverVars && window.serverVars.highlightItemId && window.serverVars.highlightItemId !== 'null')
|
const highlightItemId = (window.serverVars && window.serverVars.highlightItemId && window.serverVars.highlightItemId !== 'null')
|
||||||
@@ -539,7 +543,7 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const scanButton = document.getElementById('scanButton');
|
const scanButton = document.getElementById('scanButton');
|
||||||
const qrReader = document.getElementById('qr-reader');
|
const qrReader = document.getElementById('qr-reader');
|
||||||
let html5QrcodeScanner = null;
|
let hybridScanner = null;
|
||||||
|
|
||||||
if (!scanButton || !qrReader) return;
|
if (!scanButton || !qrReader) return;
|
||||||
|
|
||||||
@@ -553,28 +557,36 @@
|
|||||||
if (qrReader.style.display !== 'block') {
|
if (qrReader.style.display !== 'block') {
|
||||||
qrReader.style.display = 'block';
|
qrReader.style.display = 'block';
|
||||||
|
|
||||||
html5QrcodeScanner = new Html5QrcodeScanner(
|
hybridScanner = new HybridScanner({
|
||||||
'qr-reader', { fps: 10, qrbox: 250 }
|
videoId: 'qr-reader-video',
|
||||||
);
|
canvasId: 'qr-reader-canvas',
|
||||||
|
formats: ['QR_CODE', 'CODE_128', 'EAN_13', 'EAN_8', 'CODE_39'],
|
||||||
html5QrcodeScanner.render((decodedText) => {
|
fps: 10,
|
||||||
html5QrcodeScanner.clear();
|
facingMode: 'environment',
|
||||||
qrReader.style.display = 'none';
|
onSuccess: function(decodedText) {
|
||||||
|
hybridScanner.stop();
|
||||||
// Put scanned code into the search box and trigger search
|
qrReader.style.display = 'none';
|
||||||
const searchInput = document.getElementById('code-search');
|
|
||||||
if (searchInput) {
|
// Put scanned code into the search box and trigger search
|
||||||
searchInput.value = decodedText;
|
const searchInput = document.getElementById('code-search');
|
||||||
searchByCode();
|
if (searchInput) {
|
||||||
}
|
searchInput.value = decodedText;
|
||||||
|
searchByCode();
|
||||||
|
}
|
||||||
|
|
||||||
setScannerUi(false);
|
setScannerUi(false);
|
||||||
|
},
|
||||||
|
onError: function(error) {
|
||||||
|
console.error('Scanner error:', error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
hybridScanner.start();
|
||||||
setScannerUi(true);
|
setScannerUi(true);
|
||||||
} else {
|
} else {
|
||||||
if (html5QrcodeScanner) {
|
if (hybridScanner) {
|
||||||
html5QrcodeScanner.clear();
|
hybridScanner.stop();
|
||||||
|
hybridScanner = null;
|
||||||
}
|
}
|
||||||
qrReader.style.display = 'none';
|
qrReader.style.display = 'none';
|
||||||
setScannerUi(false);
|
setScannerUi(false);
|
||||||
|
|||||||
@@ -2435,7 +2435,10 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
</div>
|
</div>
|
||||||
<div class="qr-container">
|
<div class="qr-container">
|
||||||
<button id="scanButton" class="scan-button" aria-controls="qr-reader" aria-expanded="false">Barcode scannen</button>
|
<button id="scanButton" class="scan-button" aria-controls="qr-reader" aria-expanded="false">Barcode scannen</button>
|
||||||
<div id="qr-reader"></div>
|
<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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="table-view-header" class="table-view-header" aria-hidden="true">
|
<div id="table-view-header" class="table-view-header" aria-hidden="true">
|
||||||
@@ -2729,7 +2732,8 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
|
|
||||||
window.isDebug = false; // Set to true only for development environment
|
window.isDebug = false; // Set to true only for development environment
|
||||||
</script>
|
</script>
|
||||||
<script src="https://unpkg.com/html5-qrcode@2.0.9/dist/html5-qrcode.min.js"></script>
|
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
|
||||||
|
<script src="/static/js/scanner.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Function to check if a file is a video
|
// Function to check if a file is a video
|
||||||
function isVideoFile(filename) {
|
function isVideoFile(filename) {
|
||||||
@@ -2739,7 +2743,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize QR Code scanner and global variables
|
// Initialize QR Code scanner and global variables
|
||||||
let html5QrcodeScanner = null;
|
let hybridScanner = null;
|
||||||
let codeSearchTerm = '';
|
let codeSearchTerm = '';
|
||||||
let descSearchIds = null; // Set of matching IDs or null when disabled
|
let descSearchIds = null; // Set of matching IDs or null when disabled
|
||||||
let currentUsername = '';
|
let currentUsername = '';
|
||||||
@@ -2783,33 +2787,37 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
if (qrReader.style.display === 'none') {
|
if (qrReader.style.display === 'none') {
|
||||||
qrReader.style.display = 'block';
|
qrReader.style.display = 'block';
|
||||||
|
|
||||||
html5QrcodeScanner = new Html5QrcodeScanner(
|
hybridScanner = new HybridScanner({
|
||||||
"qr-reader", {
|
videoId: 'qr-reader-video',
|
||||||
fps: 10,
|
canvasId: 'qr-reader-canvas',
|
||||||
qrbox: 250,
|
formats: ['QR_CODE', 'CODE_128', 'EAN_13', 'EAN_8', 'CODE_39', 'UPC_A'],
|
||||||
rememberLastUsedCamera: true
|
fps: 10,
|
||||||
}
|
facingMode: 'environment',
|
||||||
);
|
onSuccess: function(decodedText) {
|
||||||
|
hybridScanner.stop();
|
||||||
html5QrcodeScanner.render((decodedText) => {
|
qrReader.style.display = 'none';
|
||||||
html5QrcodeScanner.clear();
|
|
||||||
qrReader.style.display = 'none';
|
// Put the scanned code in the search box
|
||||||
|
const searchInput = document.getElementById('code-search');
|
||||||
// Instead of navigating to the URL, put the scanned code in the search box
|
if (searchInput) {
|
||||||
const searchInput = document.getElementById('code-search');
|
searchInput.value = decodedText;
|
||||||
if (searchInput) {
|
// Trigger search automatically
|
||||||
searchInput.value = decodedText;
|
searchByCode();
|
||||||
// Trigger search automatically
|
}
|
||||||
searchByCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
setScannerUi(false);
|
setScannerUi(false);
|
||||||
|
},
|
||||||
|
onError: function(error) {
|
||||||
|
console.error('Scanner error:', error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
hybridScanner.start();
|
||||||
setScannerUi(true);
|
setScannerUi(true);
|
||||||
} else {
|
} else {
|
||||||
if (html5QrcodeScanner) {
|
if (hybridScanner) {
|
||||||
html5QrcodeScanner.clear();
|
hybridScanner.stop();
|
||||||
|
hybridScanner = null;
|
||||||
}
|
}
|
||||||
qrReader.style.display = 'none';
|
qrReader.style.display = 'none';
|
||||||
setScannerUi(false);
|
setScannerUi(false);
|
||||||
@@ -2825,8 +2833,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (qrReader.style.display !== 'none') {
|
if (qrReader.style.display !== 'none') {
|
||||||
if (html5QrcodeScanner) {
|
if (hybridScanner) {
|
||||||
html5QrcodeScanner.clear();
|
hybridScanner.stop();
|
||||||
|
hybridScanner = null;
|
||||||
}
|
}
|
||||||
qrReader.style.display = 'none';
|
qrReader.style.display = 'none';
|
||||||
scanEditBtn.textContent = 'Barcode scannen';
|
scanEditBtn.textContent = 'Barcode scannen';
|
||||||
@@ -2835,18 +2844,25 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
|
|
||||||
qrReader.style.display = 'block';
|
qrReader.style.display = 'block';
|
||||||
scanEditBtn.textContent = 'Scanner schließen';
|
scanEditBtn.textContent = 'Scanner schließen';
|
||||||
html5QrcodeScanner = new Html5QrcodeScanner('qr-reader', {
|
hybridScanner = new HybridScanner({
|
||||||
fps: 10,
|
videoId: 'qr-reader-video',
|
||||||
qrbox: 250,
|
canvasId: 'qr-reader-canvas',
|
||||||
rememberLastUsedCamera: true
|
formats: ['CODE_128', 'CODE_39', 'QR_CODE'],
|
||||||
});
|
fps: 10,
|
||||||
html5QrcodeScanner.render((decodedText) => {
|
facingMode: 'environment',
|
||||||
html5QrcodeScanner.clear();
|
onSuccess: function(decodedText) {
|
||||||
qrReader.style.display = 'none';
|
hybridScanner.stop();
|
||||||
editCodeInput.value = String(decodedText || '').trim();
|
qrReader.style.display = 'none';
|
||||||
validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null);
|
editCodeInput.value = String(decodedText || '').trim();
|
||||||
scanEditBtn.textContent = 'Barcode scannen';
|
validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null);
|
||||||
|
scanEditBtn.textContent = 'Barcode scannen';
|
||||||
|
},
|
||||||
|
onError: function(error) {
|
||||||
|
console.error('Scanner error:', error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
hybridScanner.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
function scanIntoEditIsbn() {
|
function scanIntoEditIsbn() {
|
||||||
@@ -2858,8 +2874,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (qrReader.style.display !== 'none') {
|
if (qrReader.style.display !== 'none') {
|
||||||
if (html5QrcodeScanner) {
|
if (hybridScanner) {
|
||||||
html5QrcodeScanner.clear();
|
hybridScanner.stop();
|
||||||
|
hybridScanner = null;
|
||||||
}
|
}
|
||||||
qrReader.style.display = 'none';
|
qrReader.style.display = 'none';
|
||||||
scanIsbnBtn.textContent = 'ISBN scannen';
|
scanIsbnBtn.textContent = 'ISBN scannen';
|
||||||
@@ -2868,20 +2885,27 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
|
|
||||||
qrReader.style.display = 'block';
|
qrReader.style.display = 'block';
|
||||||
scanIsbnBtn.textContent = 'Scanner schließen';
|
scanIsbnBtn.textContent = 'Scanner schließen';
|
||||||
html5QrcodeScanner = new Html5QrcodeScanner('qr-reader', {
|
hybridScanner = new HybridScanner({
|
||||||
|
videoId: 'qr-reader-video',
|
||||||
|
canvasId: 'qr-reader-canvas',
|
||||||
|
formats: ['ISBN', 'EAN_13', 'EAN_8', 'QR_CODE'],
|
||||||
fps: 10,
|
fps: 10,
|
||||||
qrbox: 250,
|
facingMode: 'environment',
|
||||||
rememberLastUsedCamera: true
|
onSuccess: function(decodedText) {
|
||||||
});
|
hybridScanner.stop();
|
||||||
html5QrcodeScanner.render((decodedText) => {
|
qrReader.style.display = 'none';
|
||||||
html5QrcodeScanner.clear();
|
editIsbnInput.value = String(decodedText || '').trim();
|
||||||
qrReader.style.display = 'none';
|
scanIsbnBtn.textContent = 'ISBN scannen';
|
||||||
editIsbnInput.value = String(decodedText || '').trim();
|
if (typeof fetchBookInfo === 'function') {
|
||||||
scanIsbnBtn.textContent = 'ISBN scannen';
|
fetchBookInfo('edit');
|
||||||
if (typeof fetchBookInfo === 'function') {
|
}
|
||||||
fetchBookInfo('edit');
|
},
|
||||||
|
onError: function(error) {
|
||||||
|
console.error('Scanner error:', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
hybridScanner.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
function rebuildFilter3Options() {
|
function rebuildFilter3Options() {
|
||||||
|
|||||||
@@ -288,22 +288,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
? processedEvents
|
? processedEvents
|
||||||
: processedEvents.filter(event => event.extendedProps.status !== 'completed');
|
: processedEvents.filter(event => event.extendedProps.status !== 'completed');
|
||||||
|
|
||||||
// If any event spans multiple days, switch to week view for better visibility
|
|
||||||
try {
|
|
||||||
const multiDay = processedEvents.some(ev => {
|
|
||||||
try {
|
|
||||||
const s = new Date(ev.start);
|
|
||||||
const e = new Date(ev.end);
|
|
||||||
return s && e && s.toDateString() !== e.toDateString();
|
|
||||||
} catch (e) { return false; }
|
|
||||||
});
|
|
||||||
if (multiDay && calendar && calendar.view && calendar.view.type === 'timeGridDay') {
|
|
||||||
calendar.changeView('timeGridWeek');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provide events to calendar
|
// Provide events to calendar
|
||||||
successCallback(filteredEvents);
|
successCallback(filteredEvents);
|
||||||
})
|
})
|
||||||
@@ -330,22 +314,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
periodDiv.style.display = 'inline-block';
|
periodDiv.style.display = 'inline-block';
|
||||||
periodDiv.style.fontSize = '10px';
|
periodDiv.style.fontSize = '10px';
|
||||||
info.el.appendChild(periodDiv);
|
info.el.appendChild(periodDiv);
|
||||||
} else {
|
|
||||||
// Check if it is a multi-day event
|
|
||||||
const start = info.event.start;
|
|
||||||
const end = info.event.end;
|
|
||||||
if (start && end && start.toDateString() !== end.toDateString()) {
|
|
||||||
const periodDiv = document.createElement('div');
|
|
||||||
periodDiv.className = 'fc-event-period-marker';
|
|
||||||
periodDiv.textContent = 'Mehrtägig';
|
|
||||||
periodDiv.style.backgroundColor = 'rgba(255,255,255,0.3)';
|
|
||||||
periodDiv.style.borderRadius = '3px';
|
|
||||||
periodDiv.style.padding = '1px 3px';
|
|
||||||
periodDiv.style.marginTop = '2px';
|
|
||||||
periodDiv.style.display = 'inline-block';
|
|
||||||
periodDiv.style.fontSize = '10px';
|
|
||||||
info.el.appendChild(periodDiv);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add borrowed indicator if relevant
|
// Add borrowed indicator if relevant
|
||||||
|
|||||||
@@ -876,7 +876,10 @@
|
|||||||
<button type="button" id="scan-code4-btn" class="fetch-isbn-button">Barcode scannen</button>
|
<button type="button" id="scan-code4-btn" class="fetch-isbn-button">Barcode scannen</button>
|
||||||
</div>
|
</div>
|
||||||
<small style="display:block; color:#666;">Einzelcode manuell setzen oder per Scanner erfassen.</small>
|
<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;"></div>
|
<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>
|
||||||
<small id="code4-scan-status" style="display:block; color:#666; margin-top:6px;"></small>
|
<small id="code4-scan-status" style="display:block; color:#666; margin-top:6px;"></small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -907,7 +910,10 @@
|
|||||||
<button type="button" id="scan-isbn-btn" class="fetch-isbn-button">Barcode scannen</button>
|
<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>
|
<button type="button" class="fetch-isbn-button" onclick="fetchBookInfo('upload')">Bild abrufen</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="isbn-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;"></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>
|
||||||
<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>
|
<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 id="book-info-container" class="book-info-container"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -971,7 +977,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="https://unpkg.com/html5-qrcode@2.0.9/dist/html5-qrcode.min.js"></script>
|
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
|
||||||
|
<script src="/static/js/scanner.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const libraryModuleEnabled = {{ 'true' if library_module_enabled else 'false' }};
|
const libraryModuleEnabled = {{ 'true' if library_module_enabled else 'false' }};
|
||||||
|
|
||||||
@@ -1050,16 +1057,19 @@
|
|||||||
|
|
||||||
// Stop ISBN scanner if currently running to avoid camera conflicts.
|
// Stop ISBN scanner if currently running to avoid camera conflicts.
|
||||||
if (isbnScannerRunning && isbnScannerInstance) {
|
if (isbnScannerRunning && isbnScannerInstance) {
|
||||||
isbnScannerInstance.clear().catch(() => {});
|
isbnScannerInstance.stop();
|
||||||
|
isbnScannerInstance = null;
|
||||||
const isbnScannerBox = document.getElementById('isbn-scanner');
|
const isbnScannerBox = document.getElementById('isbn-scanner');
|
||||||
const isbnScanButton = document.getElementById('scan-isbn-btn');
|
const isbnScanButton = document.getElementById('scan-isbn-btn');
|
||||||
if (isbnScannerBox) isbnScannerBox.style.display = 'none';
|
if (isbnScannerBox) isbnScannerBox.style.display = 'none';
|
||||||
if (isbnScanButton) isbnScanButton.textContent = 'ISBN scannen';
|
if (isbnScanButton) isbnScanButton.textContent = 'ISBN scannen';
|
||||||
isbnScannerRunning = false;
|
isbnScannerRunning = false;
|
||||||
|
setIsbnScanStatus('Scanner gestoppt.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code4ScannerRunning && code4ScannerInstance) {
|
if (code4ScannerRunning && code4ScannerInstance) {
|
||||||
code4ScannerInstance.clear().catch(() => {});
|
code4ScannerInstance.stop();
|
||||||
|
code4ScannerInstance = null;
|
||||||
scannerBox.style.display = 'none';
|
scannerBox.style.display = 'none';
|
||||||
scanButton.textContent = 'Barcode scannen';
|
scanButton.textContent = 'Barcode scannen';
|
||||||
code4ScannerRunning = false;
|
code4ScannerRunning = false;
|
||||||
@@ -1069,29 +1079,36 @@
|
|||||||
|
|
||||||
scannerBox.style.display = 'block';
|
scannerBox.style.display = 'block';
|
||||||
scanButton.textContent = 'Scanner stoppen';
|
scanButton.textContent = 'Scanner stoppen';
|
||||||
code4ScannerInstance = new Html5QrcodeScanner('code4-scanner', {
|
|
||||||
fps: 10,
|
|
||||||
qrbox: 250,
|
|
||||||
rememberLastUsedCamera: true
|
|
||||||
});
|
|
||||||
code4ScannerRunning = true;
|
code4ScannerRunning = true;
|
||||||
setCode4ScanStatus('Scanner läuft. Der Scan wird im Basis-Codefeld übernommen.');
|
setCode4ScanStatus('Scanner läuft. Der Scan wird im Basis-Codefeld übernommen.');
|
||||||
|
|
||||||
code4ScannerInstance.render((decodedText) => {
|
code4ScannerInstance = new HybridScanner({
|
||||||
const scannedCode = String(decodedText || '').trim();
|
videoId: 'code4-scanner-video',
|
||||||
if (!scannedCode) return;
|
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;
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (scannedCode === code4LastScanned && (now - code4LastScannedAt) < 1500) {
|
if (scannedCode === code4LastScanned && (now - code4LastScannedAt) < 1500) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
code4LastScanned = scannedCode;
|
||||||
|
code4LastScannedAt = now;
|
||||||
|
|
||||||
|
codeField.value = scannedCode;
|
||||||
|
validateCodeField(codeField);
|
||||||
|
setCode4ScanStatus(`Code_4 gesetzt: ${scannedCode}`);
|
||||||
|
},
|
||||||
|
onError: function(error) {
|
||||||
|
setCode4ScanStatus(`Fehler: ${error}`, true);
|
||||||
}
|
}
|
||||||
code4LastScanned = scannedCode;
|
});
|
||||||
code4LastScannedAt = now;
|
|
||||||
|
|
||||||
codeField.value = scannedCode;
|
code4ScannerInstance.start();
|
||||||
validateCodeField(codeField);
|
|
||||||
setCode4ScanStatus(`Code_4 gesetzt: ${scannedCode}`);
|
|
||||||
}, () => {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startIsbnScanner() {
|
function startIsbnScanner() {
|
||||||
@@ -1106,7 +1123,8 @@
|
|||||||
|
|
||||||
// Stop Code_4 scanner if currently running to avoid camera conflicts.
|
// Stop Code_4 scanner if currently running to avoid camera conflicts.
|
||||||
if (code4ScannerRunning && code4ScannerInstance) {
|
if (code4ScannerRunning && code4ScannerInstance) {
|
||||||
code4ScannerInstance.clear().catch(() => {});
|
code4ScannerInstance.stop();
|
||||||
|
code4ScannerInstance = null;
|
||||||
const codeScannerBox = document.getElementById('code4-scanner');
|
const codeScannerBox = document.getElementById('code4-scanner');
|
||||||
const codeScanButton = document.getElementById('scan-code4-btn');
|
const codeScanButton = document.getElementById('scan-code4-btn');
|
||||||
if (codeScannerBox) codeScannerBox.style.display = 'none';
|
if (codeScannerBox) codeScannerBox.style.display = 'none';
|
||||||
@@ -1116,7 +1134,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isbnScannerRunning && isbnScannerInstance) {
|
if (isbnScannerRunning && isbnScannerInstance) {
|
||||||
isbnScannerInstance.clear().catch(() => {});
|
isbnScannerInstance.stop();
|
||||||
|
isbnScannerInstance = null;
|
||||||
scannerBox.style.display = 'none';
|
scannerBox.style.display = 'none';
|
||||||
scanButton.textContent = 'ISBN scannen';
|
scanButton.textContent = 'ISBN scannen';
|
||||||
isbnScannerRunning = false;
|
isbnScannerRunning = false;
|
||||||
@@ -1126,15 +1145,16 @@
|
|||||||
|
|
||||||
scannerBox.style.display = 'block';
|
scannerBox.style.display = 'block';
|
||||||
scanButton.textContent = 'Scanner stoppen';
|
scanButton.textContent = 'Scanner stoppen';
|
||||||
isbnScannerInstance = new Html5QrcodeScanner('isbn-scanner', {
|
|
||||||
fps: 10,
|
|
||||||
qrbox: 250,
|
|
||||||
rememberLastUsedCamera: true
|
|
||||||
});
|
|
||||||
isbnScannerRunning = true;
|
isbnScannerRunning = true;
|
||||||
setIsbnScanStatus('Scanner läuft. Bitte ISBN-Code erfassen.');
|
setIsbnScanStatus('Scanner läuft. Bitte ISBN-Code erfassen.');
|
||||||
|
|
||||||
isbnScannerInstance.render((decodedText) => {
|
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) {
|
||||||
const scannedCode = String(decodedText || '').trim();
|
const scannedCode = String(decodedText || '').trim();
|
||||||
if (!scannedCode) return;
|
if (!scannedCode) return;
|
||||||
|
|
||||||
@@ -1147,7 +1167,11 @@
|
|||||||
setIsbnScanStatus('Kein gültiges ISBN-Format erkannt, der gescannte Wert bleibt aber im Feld.', true);
|
setIsbnScanStatus('Kein gültiges ISBN-Format erkannt, der gescannte Wert bleibt aber im Feld.', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
isbnScannerInstance.clear().catch(() => {});
|
// Stop scanner after successful scan
|
||||||
|
if (isbnScannerInstance) {
|
||||||
|
isbnScannerInstance.stop();
|
||||||
|
isbnScannerInstance = null;
|
||||||
|
}
|
||||||
scannerBox.style.display = 'none';
|
scannerBox.style.display = 'none';
|
||||||
scanButton.textContent = 'ISBN scannen';
|
scanButton.textContent = 'ISBN scannen';
|
||||||
isbnScannerRunning = false;
|
isbnScannerRunning = false;
|
||||||
@@ -1156,7 +1180,13 @@
|
|||||||
// Automatically load book metadata after a valid ISBN scan.
|
// Automatically load book metadata after a valid ISBN scan.
|
||||||
fetchBookInfo('upload');
|
fetchBookInfo('upload');
|
||||||
}
|
}
|
||||||
}, () => {});
|
},
|
||||||
|
onError: function(error) {
|
||||||
|
setIsbnScanStatus(`Fehler: ${error}`, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
isbnScannerInstance.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load predefined filter values for dropdowns
|
// Load predefined filter values for dropdowns
|
||||||
|
|||||||
Reference in New Issue
Block a user