Files
MWClash/lib/pages/scan.dart
chen08209 ddc07e9f29 Add android separates the core process
Support core status check and force restart

Optimize proxies page and access page

Update flutter and pub dependencies

Optimize more details
2025-09-17 17:12:11 +08:00

216 lines
6.7 KiB
Dart

import 'dart:async';
import 'dart:math';
import 'package:fl_clash/common/color.dart';
import 'package:fl_clash/state.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<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());
}
void _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: IconButton.styleFrom(
iconSize: 32,
foregroundColor: 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: 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: globalState.appController.addProfileFormQrCode,
icon: const Icon(Icons.photo_camera_back),
),
),
],
),
);
}
@override
Future<void> 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()
..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.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 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;
}
}