import 'package:flutter/material.dart'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:manager_api_new/api.dart'; import 'package:mymuseum_visitapp/Components/SlideFromRouteRight.dart'; import 'package:mymuseum_visitapp/Helpers/DatabaseHelper.dart'; import 'package:mymuseum_visitapp/Helpers/translationHelper.dart'; import 'package:mymuseum_visitapp/Models/visitContext.dart'; import 'package:mymuseum_visitapp/Screens/ConfigurationPage/configuration_page.dart'; import 'package:mymuseum_visitapp/Screens/section_page.dart'; import 'package:mymuseum_visitapp/app_context.dart'; import 'package:mymuseum_visitapp/constants.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; class ScannerDialog extends StatefulWidget { const ScannerDialog({Key? key, required this.appContext}) : super(key: key); final AppContext? appContext; @override State createState() => _ScannerDialogState(); } class _ScannerDialogState extends State { final MobileScannerController controller = MobileScannerController(); bool isProcessing = false; @override void reassemble() { super.reassemble(); if (Platform.isAndroid) { controller.stop(); } controller.start(); } @override Widget build(BuildContext context) { Size size = MediaQuery.of(context).size; return Container( height: size.height * 0.5, width: size.width * 0.9, child: Stack( children: [ Center( child: ClipRRect( borderRadius: BorderRadius.circular(10.0), child: MobileScanner( controller: controller, //allowDuplicates: false, onDetect: (barcodes) => _onDetect(barcodes), ), ), ), _buildControlButton( icon: Icons.flash_on, onTap: () => controller.toggleTorch(), alignment: Alignment.topRight, ), _buildControlButton( icon: Icons.flip_camera_android, onTap: () => controller.switchCamera(), alignment: Alignment.bottomRight, ), ], ), ); } Widget _buildControlButton({ required IconData icon, required VoidCallback onTap, required Alignment alignment, }) { return Align( alignment: alignment, child: Container( width: 45, height: 45, margin: const EdgeInsets.all(8), decoration: BoxDecoration( shape: BoxShape.rectangle, color: kMainColor1, borderRadius: BorderRadius.circular(20.0), ), child: InkWell( onTap: onTap, child: Icon(icon, color: Colors.white), ), ), ); } Future _onDetect(BarcodeCapture capture) async { if (isProcessing) return; final barcode = capture.barcodes.first; final code = barcode.rawValue ?? ""; if (barcode.format == BarcodeFormat.qrCode && code.isNotEmpty) { isProcessing = true; RegExp regExp = RegExp(r'^(?:https:\/\/web\.myinfomate\.be\/([^\/]+)\/([^\/]+)\/([^\/]+)|([^\/]+))$'); RegExp regExp2 = RegExp(r'^(?:https:\/\/web\.mymuseum\.be\/([^\/]+)\/([^\/]+)\/([^\/]+)|([^\/]+))$'); var match = regExp.firstMatch(code); var match2 = regExp2.firstMatch(code); String? instanceId; String? configurationId; String? sectionId; if(match == null) { instanceId = match2?.group(1); configurationId = match2?.group(2); sectionId = match2?.group(3) ?? match2?.group(4); } else { instanceId = match.group(1); configurationId = match.group(2); sectionId = match.group(3) ?? match.group(4); } if ((match == null && match2 == null) || sectionId == null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("L'URL ne correspond pas au format attendu."), backgroundColor: kMainColor2), ); Navigator.of(context).pop(); return; } VisitAppContext visitAppContext = widget.appContext!.getContext(); if (visitAppContext.sectionIds == null || !visitAppContext.sectionIds!.contains(sectionId)) { visitAppContext.statisticsService?.track(VisitEventType.qrScan, metadata: {'valid': false, 'sectionId': sectionId}); // Le QR porte l'id de sa configuration, jusqu'ici parsé puis jeté. Un code // d'une autre visite du site tombait donc dans « QR code invalide », qui // laisse croire à un code abîmé alors qu'il est parfaitement lisible. final other = await _findOtherConfiguration(visitAppContext, configurationId); if (!mounted) return; // Capturés AVANT le pop : fermer la boîte du scanner démonte ce State, et // tout ce qui repasse ensuite par son `context` — la navigation depuis le // bouton « Ouvrir », touché quelques secondes plus tard — ne fait rien. final navigator = Navigator.of(context); final messenger = ScaffoldMessenger.of(context); navigator.pop(); if (other != null) { _proposeOtherVisit(navigator, visitAppContext, other, sectionId); } else { messenger.showSnackBar( SnackBar(content: Text(TranslationHelper.getFromLocale('invalidQRCode', visitAppContext)), backgroundColor: kMainColor2), ); } } else { visitAppContext.statisticsService?.track(VisitEventType.qrScan, sectionId: sectionId, metadata: {'valid': true}); // `orElse` : sans lui, le moindre écart entre `sectionIds` et // `currentSections` levait un StateError non rattrapé, en plein scan. dynamic rawSection = visitAppContext.currentSections ?.firstWhere((cs) => cs?['id'] == sectionId, orElse: () => null); Navigator.of(context).pop(); if (rawSection == null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(TranslationHelper.getFromLocale('invalidQRCode', visitAppContext)), backgroundColor: kMainColor2), ); return; } Navigator.push( context, SlideFromRightRoute(page: SectionPage( configuration: visitAppContext.configuration!, rawSection: rawSection, visitAppContextIn: visitAppContext, sectionId: rawSection['id'], )), ); } } } /// La configuration du QR, si elle existe et n'est pas celle qu'on lit déjà. /// La base locale d'abord — c'est la seule source hors ligne — puis l'API. Future _findOtherConfiguration( VisitAppContext visitAppContext, String? configurationId) async { if (configurationId == null || configurationId == visitAppContext.configuration?.id) { return null; } final local = await DatabaseHelper.instance .queryWithColumnId(DatabaseTableType.configurations, configurationId); if (local.isNotEmpty) { return DatabaseHelper.instance.getConfigurationFromDB(local.first); } try { return await visitAppContext.clientAPI.configurationApi! .configurationGetDetail(configurationId); } catch (e) { debugPrint("[scan] configuration $configurationId introuvable : $e"); return null; } } Future _proposeOtherVisit(NavigatorState navigator, VisitAppContext visitAppContext, ConfigurationDTO other, String sectionId) async { final name = TranslationHelper.getPlain(other.title, visitAppContext); // Une visite hors ligne non téléchargée n'a rien à afficher : on nomme la // visite sans proposer de l'ouvrir sur du vide. final sections = await DatabaseHelper.instance .queryWithConfigurationId(DatabaseTableType.sections, other.id!); final canOpen = other.isOffline != true || sections.isNotEmpty; showDialog( context: navigator.context, builder: (dialogContext) => AlertDialog( shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20))), title: Text(name, textAlign: TextAlign.center), content: Text( TranslationHelper.getFromLocale('qrOtherVisit', visitAppContext), textAlign: TextAlign.center, ), actionsAlignment: MainAxisAlignment.center, actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), child: Text(TranslationHelper.getFromLocale('close', visitAppContext)), ), if (canOpen) TextButton( onPressed: () { Navigator.of(dialogContext).pop(); visitAppContext.configuration = other; visitAppContext.sectionIds = null; widget.appContext!.setContext(visitAppContext); // `pushAndRemoveUntil` jusqu'à l'accueil, et non `push` : une visite // est une destination de premier niveau, pas quelque chose qui // s'empile. En scannant des QR de trois visites différentes, le // visiteur se retrouvait avec trois ConfigurationPage superposées et // autant de retours à faire pour revenir à l'accueil — en gardant en // mémoire des visites qu'il a quittées. Ici la pile reste // « accueil → visite en cours ». navigator.pushAndRemoveUntil( SlideFromRightRoute( page: ConfigurationPage( configuration: other, isAlreadyAllowed: visitAppContext.isScanBeaconAlreadyAllowed, openSectionId: sectionId, ), ), (route) => route.isFirst, ); }, child: Text(TranslationHelper.getFromLocale('open', visitAppContext)), ), ], ), ); } @override void dispose() { controller.dispose(); super.dispose(); } } showScannerDialog(BuildContext context, AppContext appContext) { showDialog( context: context, builder: (BuildContext context) => AlertDialog( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10.0)), ), content: ScannerDialog(appContext: appContext), contentPadding: EdgeInsets.zero, ), ); }