221 lines
6.8 KiB
Dart
221 lines
6.8 KiB
Dart
import 'dart:async';
|
|
import 'dart:math';
|
|
import 'package:fl_clash/state.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
|
|
|
class ScanPage extends StatefulWidget {
|
|
const ScanPage({super.key});
|
|
|
|
@override
|
|
State<ScanPage> createState() => _ScanPageState();
|
|
}
|
|
|
|
class _ScanPageState extends State<ScanPage> with WidgetsBindingObserver {
|
|
MobileScannerController controller = MobileScannerController(
|
|
detectionSpeed: DetectionSpeed.noDuplicates,
|
|
formats: const [BarcodeFormat.qrCode],
|
|
);
|
|
|
|
StreamSubscription<Object?>? _subscription;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addObserver(this);
|
|
_subscription = controller.barcodes.listen(_handleBarcode);
|
|
unawaited(controller.start());
|
|
}
|
|
|
|
_handleBarcode(BarcodeCapture barcodeCapture) {
|
|
final barcode = barcodeCapture.barcodes.first;
|
|
if (barcode.type == BarcodeType.url) {
|
|
Navigator.pop<String>(
|
|
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: const ButtonStyle(
|
|
iconSize: WidgetStatePropertyAll(32),
|
|
foregroundColor: WidgetStatePropertyAll(Colors.white),
|
|
),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
icon: const Icon(Icons.close),
|
|
),
|
|
actions: [
|
|
ValueListenableBuilder<MobileScannerState>(
|
|
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: AbsorbPointer(
|
|
absorbing: state.torchState == TorchState.unavailable,
|
|
child: IconButton(
|
|
color: Colors.white,
|
|
icon: icon,
|
|
style: ButtonStyle(
|
|
foregroundColor: const WidgetStatePropertyAll(Colors.white),
|
|
backgroundColor: WidgetStatePropertyAll(backgroundColor),
|
|
),
|
|
onPressed: () => controller.toggleTorch(),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
)
|
|
],
|
|
),
|
|
Container(
|
|
margin: const EdgeInsets.only(bottom: 32),
|
|
alignment: Alignment.bottomCenter,
|
|
child: IconButton(
|
|
color: Colors.white,
|
|
style: const ButtonStyle(
|
|
foregroundColor: WidgetStatePropertyAll(Colors.white),
|
|
backgroundColor: WidgetStatePropertyAll(Colors.grey),
|
|
),
|
|
padding: const EdgeInsets.all(16),
|
|
iconSize: 32.0,
|
|
onPressed: globalState.appController.addProfileFormQrCode,
|
|
icon: const Icon(Icons.photo_camera_back),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> dispose() async {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
unawaited(_subscription?.cancel());
|
|
_subscription = null;
|
|
super.dispose();
|
|
await controller.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()
|
|
..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 = 4.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(ScannerOverlay oldDelegate) {
|
|
return scanWindow != oldDelegate.scanWindow ||
|
|
borderRadius != oldDelegate.borderRadius;
|
|
}
|
|
}
|