import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; class QRCodeScannerScreen extends HookConsumerWidget with PresLogger { const QRCodeScannerScreen({super.key}); Future open(BuildContext context) async { return Navigator.of(context, rootNavigator: true).push( MaterialPageRoute( fullscreenDialog: true, builder: (context) => const QRCodeScannerScreen(), ), ); } @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); final controller = useMemoized( () => MobileScannerController(detectionTimeoutMs: 500), ); useEffect(() => controller.dispose, []); final size = MediaQuery.sizeOf(context); final overlaySize = (size.shortestSide - 12).coerceAtMost(248); return Scaffold( extendBodyBehindAppBar: true, appBar: AppBar( backgroundColor: Colors.transparent, iconTheme: Theme.of(context).iconTheme.copyWith( color: Colors.white, size: 32, ), actions: [ IconButton( icon: ValueListenableBuilder( valueListenable: controller.torchState, builder: (context, state, child) { switch (state) { case TorchState.off: return const Icon(Icons.flash_off, color: Colors.grey); case TorchState.on: return const Icon(Icons.flash_on, color: Colors.yellow); } }, ), tooltip: t.profile.add.qrScanner.torchSemanticLabel, onPressed: () => controller.toggleTorch(), ), IconButton( icon: ValueListenableBuilder( valueListenable: controller.cameraFacingState, builder: (context, state, child) { switch (state) { case CameraFacing.front: return const Icon(Icons.camera_front); case CameraFacing.back: return const Icon(Icons.camera_rear); } }, ), tooltip: t.profile.add.qrScanner.facingSemanticLabel, onPressed: () => controller.switchCamera(), ), ], ), body: Stack( children: [ MobileScanner( controller: controller, onDetect: (capture) { final data = capture.barcodes.first; if (context.mounted && data.type == BarcodeType.url) { loggy.debug('captured raw: [${data.rawValue}]'); loggy.debug('captured url: [${data.url?.url}]'); Navigator.of(context, rootNavigator: true).pop(data.url?.url); } }, errorBuilder: (_, error, __) { final message = switch (error.errorCode) { MobileScannerErrorCode.permissionDenied => t.profile.add.qrScanner.permissionDeniedError, _ => t.profile.add.qrScanner.unexpectedError, }; return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Padding( padding: EdgeInsets.only(bottom: 8), child: Icon(Icons.error, color: Colors.white), ), Text(message), Text(error.errorDetails?.message ?? ''), ], ), ); }, ), CustomPaint( painter: ScannerOverlay( Rect.fromCenter( center: size.center(Offset.zero), width: overlaySize, height: overlaySize, ), ), ), ], ), ); } } class ScannerOverlay extends CustomPainter { ScannerOverlay(this.scanWindow); final Rect scanWindow; final double borderRadius = 12.0; @override void paint(Canvas canvas, Size size) { final backgroundPath = Path()..addRect(Rect.largest); final cutoutPath = Path() ..addRRect( RRect.fromRectAndCorners( scanWindow, topLeft: Radius.circular(borderRadius), topRight: Radius.circular(borderRadius), bottomLeft: Radius.circular(borderRadius), bottomRight: Radius.circular(borderRadius), ), ); final backgroundPaint = Paint() ..color = Colors.black.withOpacity(0.5) ..style = PaintingStyle.fill ..blendMode = BlendMode.dstOut; final backgroundWithCutout = Path.combine( PathOperation.difference, backgroundPath, cutoutPath, ); final borderPaint = Paint() ..color = Colors.white ..style = PaintingStyle.stroke ..strokeWidth = 3.0; final borderRect = RRect.fromRectAndCorners( scanWindow, topLeft: Radius.circular(borderRadius), topRight: Radius.circular(borderRadius), bottomLeft: Radius.circular(borderRadius), bottomRight: Radius.circular(borderRadius), ); canvas.drawPath(backgroundWithCutout, backgroundPaint); canvas.drawRRect(borderRect, borderPaint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return false; } }