import 'dart:async'; import 'dart:math'; import 'package:fl_clash/common/color.dart'; import 'package:fl_clash/controller.dart'; import 'package:fl_clash/widgets/activate_box.dart'; import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; class ScanPage extends StatefulWidget { const ScanPage({super.key}); @override State createState() => _ScanPageState(); } class _ScanPageState extends State with WidgetsBindingObserver { MobileScannerController controller = MobileScannerController( detectionSpeed: DetectionSpeed.noDuplicates, formats: const [BarcodeFormat.qrCode], ); StreamSubscription? _subscription; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _subscription = controller.barcodes.listen(_handleBarcode); unawaited(controller.start()); } void _handleBarcode(BarcodeCapture barcodeCapture) { final barcode = barcodeCapture.barcodes.first; if (barcode.type == BarcodeType.url) { Navigator.pop(context, barcode.rawValue); } else { Navigator.pop(context); } } @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); switch (state) { case AppLifecycleState.detached: case AppLifecycleState.hidden: case AppLifecycleState.paused: return; case AppLifecycleState.resumed: _subscription = controller.barcodes.listen(_handleBarcode); unawaited(controller.start()); case AppLifecycleState.inactive: unawaited(_subscription?.cancel()); _subscription = null; unawaited(controller.stop()); } } @override Widget build(BuildContext context) { double sideLength = min(400, MediaQuery.of(context).size.width * 0.67); final scanWindow = Rect.fromCenter( center: MediaQuery.sizeOf(context).center(Offset.zero), width: sideLength, height: sideLength, ); return Scaffold( body: Stack( children: [ Center( child: MobileScanner( controller: controller, scanWindow: scanWindow, ), ), CustomPaint(painter: ScannerOverlay(scanWindow: scanWindow)), AppBar( backgroundColor: Colors.transparent, automaticallyImplyLeading: false, leading: IconButton( style: IconButton.styleFrom( iconSize: 32, foregroundColor: Colors.white, ), onPressed: () { Navigator.of(context).pop(); }, icon: const Icon(Icons.close), ), actions: [ ValueListenableBuilder( valueListenable: controller, builder: (context, state, _) { var icon = const Icon(Icons.flash_off); var backgroundColor = Colors.black12; switch (state.torchState) { case TorchState.off: icon = const Icon(Icons.flash_off); backgroundColor = Colors.black12; case TorchState.on: icon = const Icon(Icons.flash_on); backgroundColor = Colors.orange; case TorchState.unavailable: icon = const Icon(Icons.flash_off); backgroundColor = Colors.transparent; case TorchState.auto: icon = const Icon(Icons.flash_auto); backgroundColor = Colors.orange; } return Container( margin: const EdgeInsets.symmetric(horizontal: 8), child: ActivateBox( active: state.torchState != TorchState.unavailable, child: IconButton( color: Colors.white, icon: icon, style: IconButton.styleFrom( foregroundColor: Colors.white, backgroundColor: backgroundColor, ), onPressed: () => controller.toggleTorch(), ), ), ); }, ), ], ), Container( margin: const EdgeInsets.only(bottom: 32), alignment: Alignment.bottomCenter, child: IconButton( color: Colors.white, style: IconButton.styleFrom( foregroundColor: Colors.white, backgroundColor: Colors.grey, ), padding: const EdgeInsets.all(16), iconSize: 32.0, onPressed: appController.addProfileFormQrCode, icon: const Icon(Icons.photo_camera_back), ), ), ], ), ); } @override Future dispose() async { WidgetsBinding.instance.removeObserver(this); unawaited(_subscription?.cancel()); _subscription = null; await controller.dispose(); super.dispose(); } } class ScannerOverlay extends CustomPainter { const ScannerOverlay({required this.scanWindow, this.borderRadius = 12.0}); final Rect scanWindow; final double borderRadius; @override void paint(Canvas canvas, Size size) { final backgroundPath = Path()..addRect(Rect.largest); final cutoutPath = Path() ..addRSuperellipse( RSuperellipse.fromRectAndCorners( scanWindow, topLeft: Radius.circular(borderRadius), topRight: Radius.circular(borderRadius), bottomLeft: Radius.circular(borderRadius), bottomRight: Radius.circular(borderRadius), ), ); final backgroundPaint = Paint() ..color = Colors.black.opacity50 ..style = PaintingStyle.fill ..blendMode = BlendMode.dstOut; final backgroundWithCutout = Path.combine( PathOperation.difference, backgroundPath, cutoutPath, ); final borderPaint = Paint() ..color = Colors.white ..style = PaintingStyle.stroke ..strokeWidth = 4.0; final border = RSuperellipse.fromRectAndCorners( scanWindow, topLeft: Radius.circular(borderRadius), topRight: Radius.circular(borderRadius), bottomLeft: Radius.circular(borderRadius), bottomRight: Radius.circular(borderRadius), ); canvas.drawPath(backgroundWithCutout, backgroundPaint); canvas.drawRSuperellipse(border, borderPaint); } @override bool shouldRepaint(ScannerOverlay oldDelegate) { return scanWindow != oldDelegate.scanWindow || borderRadius != oldDelegate.borderRadius; } }