import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; /// Schmaler Kamera-Stripe mit Torch- und Manual-Entry-Button sowie /// Zoom-Steuerung. Geteilt zwischen Beladen-Phase und Filial-Abholung. /// /// Wichtig: sollte **einmal** und außerhalb eines `PageView` gemountet /// werden, damit der Kamera-Stream beim Wischen nicht abreißt (eine einzige /// Kamera-Instanz pro Page-Lebenszyklus). class ArticleScannerStripe extends StatefulWidget { const ArticleScannerStripe({ super.key, required this.onBarcode, required this.onManualEntry, }); /// Roh-Wert eines erkannten Barcodes (bereits getrimmt, dedupliziert). final void Function(String code) onBarcode; /// Tap auf das Tastatur-Icon → Aufrufer öffnet den Manual-Entry-Dialog. final VoidCallback onManualEntry; @override State createState() => _ArticleScannerStripeState(); } class _ArticleScannerStripeState extends State { late final MobileScannerController _scanner = MobileScannerController( detectionSpeed: DetectionSpeed.normal, formats: const [ BarcodeFormat.ean13, BarcodeFormat.ean8, BarcodeFormat.code128, BarcodeFormat.code39, BarcodeFormat.qrCode, ], ); /// Dedupliziert Detections derselben Tastendruck-Geste: der Scanner liest /// dasselbe Code-Bild viele Frames lang. Nur wenn der Code für /// `_minRepeatGap` lang nicht gesehen wurde, lassen wir ihn erneut zu. String? _lastCode; DateTime _lastEmit = DateTime.fromMillisecondsSinceEpoch(0); static const Duration _minRepeatGap = Duration(milliseconds: 1200); /// Schrittweite der `-` / `+`-Buttons im normalisierten Zoom-Bereich /// [0.0, 1.0]. 5% pro Tastendruck — fühlt sich auf einem 6"-Display /// natürlich an, ohne dass der Fahrer zehnmal tappen muss. static const double _zoomStep = 0.05; @override void dispose() { _scanner.dispose(); super.dispose(); } void _onDetect(BarcodeCapture capture) { if (capture.barcodes.isEmpty) return; final raw = capture.barcodes.first.rawValue?.trim(); if (raw == null || raw.isEmpty) return; final now = DateTime.now(); if (raw == _lastCode && now.difference(_lastEmit) < _minRepeatGap) { return; } _lastCode = raw; _lastEmit = now; widget.onBarcode(raw); } Future _setZoom(double normalized) async { final clamped = normalized.clamp(0.0, 1.0); await _scanner.setZoomScale(clamped); } @override Widget build(BuildContext context) { return Column( children: [ SizedBox( height: 140, child: Stack( children: [ Positioned.fill( child: MobileScanner( controller: _scanner, onDetect: _onDetect, errorBuilder: (context, error) { return Container( color: Colors.black, child: Center( child: Padding( padding: const EdgeInsets.all(12), child: Text( 'Kamera nicht verfügbar: ${error.errorCode.name}', style: const TextStyle(color: Colors.white), textAlign: TextAlign.center, ), ), ), ); }, ), ), Positioned( right: 8, top: 8, child: Material( color: Colors.black54, borderRadius: BorderRadius.circular(20), child: IconButton( tooltip: 'Manuelle Eingabe', icon: const Icon(Icons.keyboard, color: Colors.white), onPressed: widget.onManualEntry, ), ), ), Positioned( left: 8, top: 8, child: Material( color: Colors.black54, borderRadius: BorderRadius.circular(20), child: IconButton( tooltip: 'Taschenlampe', icon: const Icon(Icons.flash_on, color: Colors.white), onPressed: () => _scanner.toggleTorch(), ), ), ), ], ), ), _ZoomBar(controller: _scanner, onChange: _setZoom, step: _zoomStep), ], ); } } /// Zoom-Steuerung: `-` Button links, Slider in der Mitte, `+` rechts. /// Bindet an `MobileScannerController.value.zoomScale` via /// ValueListenableBuilder — damit reagiert der Slider sofort, wenn das /// Plugin den Zoom selbst clampt (z. B. bei Geräten, die nicht den vollen /// Bereich unterstützen). class _ZoomBar extends StatelessWidget { const _ZoomBar({ required this.controller, required this.onChange, required this.step, }); final MobileScannerController controller; final Future Function(double normalized) onChange; final double step; @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: controller, builder: (context, state, _) { final zoom = state.zoomScale; return Material( color: Colors.black87, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), child: Row( children: [ IconButton( tooltip: 'Auszoomen', icon: const Icon(Icons.remove, color: Colors.white), onPressed: zoom <= 0.0 ? null : () => onChange(zoom - step), ), Expanded( child: SliderTheme( data: SliderTheme.of(context).copyWith( activeTrackColor: Colors.white, inactiveTrackColor: Colors.white24, thumbColor: Colors.white, overlayColor: Colors.white24, trackHeight: 3, ), child: Slider( min: 0.0, max: 1.0, value: zoom.clamp(0.0, 1.0), onChanged: (v) => onChange(v), ), ), ), IconButton( tooltip: 'Reinzoomen', icon: const Icon(Icons.add, color: Colors.white), onPressed: zoom >= 1.0 ? null : () => onChange(zoom + step), ), ], ), ), ); }, ); } }