import 'dart:io' show Platform; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:zxing_barcode_scanner/zxing_barcode_scanner.dart'; import '../data/species_repository.dart'; import '../data/variety_repository.dart'; import '../i18n/strings.g.dart'; import '../services/camera_availability.dart'; import '../services/seed_label_scan.dart'; /// Whether this platform can open the camera scanner. Mobile-only for now /// (pure-ZXing platform views; no Google Play Services) — elsewhere the scan /// button simply isn't offered, same pattern as the OCR capture. On Android it /// also requires an actual camera, so camera-less devices (Chromebooks, /// Automotive, some TVs) don't get a scan button that can't open — see /// [deviceHasCamera]. bool get qrScanSupported => !kIsWeb && ((Platform.isAndroid && deviceHasCamera) || Platform.isIOS); /// Opens the full-screen camera scanner and returns the first decoded QR /// payload, or null if the person backed out. (Ğ1nkgo's scanner pattern on /// the same pure-ZXing stack.) Future scanSeedLabelQr(BuildContext context) { return Navigator.of(context).push( MaterialPageRoute(builder: (_) => const _ScannerScreen()), ); } /// Acts on a scanned payload: a known seed opens its record (via /// [openVariety]); an unknown seed label asks before adding anything; anything /// else says so and moves on. Navigation is injected so the flow is /// widget-testable without a camera or a router. Future handleScannedPayload( BuildContext context, { required VarietyRepository repository, required SpeciesRepository species, required String payload, required void Function(String varietyId) openVariety, }) async { final t = context.t; final messenger = ScaffoldMessenger.of(context); final result = await resolveScannedLabel(repository, payload); if (!context.mounted) return; if (result.varietyId case final id?) { openVariety(id); return; } final data = result.data; if (data == null) { messenger.showSnackBar(SnackBar(content: Text(t.scan.notALabel))); return; } final add = await showDialog( context: context, builder: (context) => AlertDialog( key: const Key('scan.addPrompt'), title: Text(t.scan.addTitle), content: Text(t.scan.addBody(label: data.varietyLabel)), actions: [ TextButton( key: const Key('scan.cancel'), onPressed: () => Navigator.of(context).pop(false), child: Text(t.common.cancel), ), FilledButton( key: const Key('scan.add'), onPressed: () => Navigator.of(context).pop(true), child: Text(t.scan.add), ), ], ), ); if (add != true) return; final varietyId = await importScannedLabel( repository: repository, species: species, data: data, ); messenger.showSnackBar(SnackBar(content: Text(t.scan.added))); openVariety(varietyId); } class _ScannerScreen extends StatefulWidget { const _ScannerScreen(); @override State<_ScannerScreen> createState() => _ScannerScreenState(); } class _ScannerScreenState extends State<_ScannerScreen> { bool _done = false; void _onScan(List results) { if (_done || !mounted || results.isEmpty) return; final text = results.first.text; if (text == null || text.isEmpty) return; _done = true; // A beat so the camera view settles before popping (Ğ1nkgo does the same). Future.delayed(const Duration(milliseconds: 150), () { if (mounted) Navigator.of(context).pop(text); }); } @override Widget build(BuildContext context) { final t = context.t; return Scaffold( appBar: AppBar(title: Text(t.scan.title)), body: ZxingBarcodeScanner( onScan: _onScan, onError: (error) => Center( child: Padding( padding: const EdgeInsets.all(24), child: Text(error.message ?? t.common.offline), ), ), ), ); } }