dev
dart 391 lines 10.9 KB
Raw
1 import 'dart:math';
2
3 import 'package:cake_wallet/generated/i18n.dart';
4 import 'package:cake_wallet/new-ui/pages/scan_page.dart';
5 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
6 import 'package:cake_wallet/utils/show_pop_up.dart';
7 import 'package:cw_core/utils/print_verbose.dart';
8 import 'package:fast_scanner/fast_scanner.dart';
9 import 'package:flutter/cupertino.dart';
10 import 'package:flutter/material.dart';
11 import 'package:flutter/scheduler.dart';
12 import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
13 import 'package:ur/ur_decoder.dart';
14
15 var isQrScannerShown = false;
16
17 Future<String?> presentQRScanner(BuildContext context,
18 {bool showHelp = false, bool showManualInput = true, bool useModal = false}) async {
19 isQrScannerShown = true;
20 try {
21 final result = useModal
22 ? await CupertinoScaffold.showCupertinoModalBottomSheet<String?>(
23 context: context,
24 builder: (context) => ScanPage(
25 showHelp: showHelp,
26 showManualInput: showManualInput,
27 ))
28 : await Navigator.of(context).push<String>(
29 CupertinoPageRoute(
30 builder: (context) {
31 return ScanPage(
32 showHelp: showHelp,
33 showManualInput: showManualInput,
34 );
35 },
36 ),
37 );
38 isQrScannerShown = false;
39 return result;
40 } catch (e) {
41 isQrScannerShown = false;
42 rethrow;
43 }
44 }
45
46 // https://github.com/MrCyjaneK/fast_scanner/blob/master/example/lib/barcode_scanner_simple.dart
47 class BarcodeScannerSimple extends StatefulWidget {
48 const BarcodeScannerSimple({super.key});
49
50 @override
51 State<BarcodeScannerSimple> createState() => _BarcodeScannerSimpleState();
52 }
53
54 class _BarcodeScannerSimpleState extends State<BarcodeScannerSimple> {
55 Barcode? _barcode;
56 bool popped = false;
57
58 List<String> urCodes = [];
59 late var ur = URQRToURQRData(urCodes);
60 final decoder = URDecoder();
61
62 void _handleBarcode(BarcodeCapture barcodes) {
63 try {
64 _handleBarcodeInternal(barcodes);
65 } catch (e) {
66 showPopUp<void>(
67 context: context,
68 builder: (context) {
69 return AlertWithOneAction(
70 alertTitle: S.of(context).error,
71 alertContent: S.of(context).error_dialog_content,
72 buttonText: S.of(context).ok,
73 buttonAction: () {
74 Navigator.of(context).pop();
75 },
76 );
77 },
78 );
79 printV(e);
80 }
81 }
82
83 void _handleBarcodeInternal(BarcodeCapture barcodes) {
84 for (final barcode in barcodes.barcodes) {
85 // don't handle unknown QR codes
86 if (barcode.rawValue?.trim().isEmpty ?? false == false) continue;
87 if (barcode.rawValue!.startsWith("ur:")) {
88 if (urCodes.contains(barcode.rawValue)) continue;
89 decoder.receivePart(barcode.rawValue!);
90 setState(() {
91 urCodes.add(barcode.rawValue!);
92 ur = URQRToURQRData(urCodes);
93 });
94 if (decoder.estimatedPercentComplete() == 1) {
95 setState(() {
96 popped = true;
97 });
98 SchedulerBinding.instance.addPostFrameCallback((_) {
99 Navigator.of(context).pop(ur.inputs.join("\n"));
100 });
101 }
102 ;
103 }
104 }
105 if (urCodes.isNotEmpty) return;
106 if (mounted) {
107 setState(() {
108 _barcode = barcodes.barcodes.firstOrNull;
109 });
110 if (_barcode != null && popped != true) {
111 setState(() {
112 popped = true;
113 });
114 Navigator.of(context).pop(_barcode!.rawValue ?? _barcode!.rawBytes);
115 }
116 }
117 }
118
119 final MobileScannerController ctrl = MobileScannerController();
120
121 @override
122 Widget build(BuildContext context) {
123 return Scaffold(
124 appBar: AppBar(
125 title: const Text('Scan'),
126 actions: [
127 SwitchCameraButton(controller: ctrl),
128 ToggleFlashlightButton(controller: ctrl),
129 ],
130 ),
131 backgroundColor: Theme.of(context).colorScheme.surface,
132 body: Stack(
133 children: [
134 MobileScanner(
135 onDetect: _handleBarcode,
136 controller: ctrl,
137 ),
138 if (decoder.expectedPartCount() != null)
139 Center(
140 child: Text(
141 "${decoder.processedPartsCount()}/${decoder.expectedPartCount()!}",
142 style: Theme.of(context)
143 .textTheme
144 .displayLarge
145 ?.copyWith(color: Theme.of(context).colorScheme.onSurface),
146 ),
147 ),
148 SizedBox(
149 child: Center(
150 child: SizedBox(
151 width: 250,
152 height: 250,
153 child: CustomPaint(
154 painter: ProgressPainter(
155 urQrProgress: URQrProgress(
156 expectedPartCount: decoder.expectedPartCount() ?? 0,
157 processedPartsCount: decoder.processedPartsCount(),
158 receivedPartIndexes: decoder.receivedPartIndexes().toList(),
159 percentage: decoder.estimatedPercentComplete(),
160 ),
161 ),
162 ),
163 ),
164 ),
165 ),
166 ],
167 ),
168 );
169 }
170
171 List<int> _urParts() {
172 List<int> l = [];
173 for (var inp in ur.inputs) {
174 try {
175 l.add(int.parse(inp.split("/")[1].split("-")[0]));
176 } catch (e) {}
177 }
178 return l;
179 }
180 }
181
182 class ToggleFlashlightButton extends StatelessWidget {
183 const ToggleFlashlightButton({required this.controller, super.key});
184
185 final MobileScannerController controller;
186
187 @override
188 Widget build(BuildContext context) {
189 return ValueListenableBuilder(
190 valueListenable: controller,
191 builder: (context, state, child) {
192 if (!state.isInitialized || !state.isRunning) {
193 return const SizedBox.shrink();
194 }
195
196 switch (state.torchState) {
197 case TorchState.auto:
198 return IconButton(
199 iconSize: 32.0,
200 icon: const Icon(Icons.flash_auto),
201 onPressed: () async {
202 await controller.toggleTorch();
203 },
204 );
205 case TorchState.off:
206 return IconButton(
207 iconSize: 32.0,
208 icon: const Icon(Icons.flash_off),
209 onPressed: () async {
210 await controller.toggleTorch();
211 },
212 );
213 case TorchState.on:
214 return IconButton(
215 iconSize: 32.0,
216 icon: const Icon(Icons.flash_on),
217 onPressed: () async {
218 await controller.toggleTorch();
219 },
220 );
221 case TorchState.unavailable:
222 return Icon(
223 Icons.no_flash,
224 color: Theme.of(context).colorScheme.onSurfaceVariant,
225 );
226 }
227 },
228 );
229 }
230 }
231
232 class SwitchCameraButton extends StatelessWidget {
233 const SwitchCameraButton({required this.controller, super.key});
234
235 final MobileScannerController controller;
236
237 @override
238 Widget build(BuildContext context) {
239 return ValueListenableBuilder(
240 valueListenable: controller,
241 builder: (context, state, child) {
242 if (!state.isInitialized || !state.isRunning) {
243 return const SizedBox.shrink();
244 }
245
246 final int? availableCameras = state.availableCameras;
247
248 if (availableCameras != null && availableCameras < 2) {
249 return const SizedBox.shrink();
250 }
251
252 final Widget icon;
253
254 switch (state.cameraDirection) {
255 case CameraFacing.front:
256 icon = const Icon(Icons.camera_front);
257 case CameraFacing.back:
258 icon = const Icon(Icons.camera_rear);
259 }
260
261 return IconButton(
262 iconSize: 32.0,
263 icon: icon,
264 onPressed: () async {
265 await controller.switchCamera();
266 },
267 );
268 },
269 );
270 }
271 }
272
273 class URQRData {
274 URQRData(
275 {required this.tag,
276 required this.str,
277 required this.progress,
278 required this.count,
279 required this.error,
280 required this.inputs});
281 final String tag;
282 final String str;
283 final double progress;
284 final int count;
285 final String error;
286 final List<String> inputs;
287 Map<String, dynamic> toJson() {
288 return {
289 "tag": tag,
290 "str": str,
291 "progress": progress,
292 "count": count,
293 "error": error,
294 "inputs": inputs,
295 };
296 }
297 }
298
299 URQRData URQRToURQRData(List<String> urqr_) {
300 final urqr = urqr_.toSet().toList();
301 urqr.sort((s1, s2) {
302 final s1s = s1.split("/");
303 final s1frameStr = s1s[1].split("-");
304 final s1curFrame = int.parse(s1frameStr[0]);
305 final s2s = s2.split("/");
306 final s2frameStr = s2s[1].split("-");
307 final s2curFrame = int.parse(s2frameStr[0]);
308 return s1curFrame - s2curFrame;
309 });
310
311 String tag = '';
312 int count = 0;
313 String bw = '';
314 for (var elm in urqr) {
315 final s = elm.substring(elm.indexOf(":") + 1); // strip down ur: prefix
316 final s2 = s.split("/");
317 tag = s2[0];
318 final frameStr = s2[1].split("-");
319 // final curFrame = int.parse(frameStr[0]);
320 count = int.parse(frameStr[1]);
321 final byteWords = s2[2];
322 bw += byteWords;
323 }
324 String? error;
325
326 return URQRData(
327 tag: tag,
328 str: bw,
329 progress: count == 0 ? 0 : (urqr.length / count),
330 count: count,
331 error: error ?? "",
332 inputs: urqr,
333 );
334 }
335
336 class ProgressPainter extends CustomPainter {
337 final URQrProgress urQrProgress;
338
339 ProgressPainter({required this.urQrProgress});
340
341 @override
342 void paint(Canvas canvas, Size size) {
343 final c = Offset(size.width / 2.0, size.height / 2.0);
344 final radius = size.width * 0.9;
345 final rect = Rect.fromCenter(center: c, width: radius, height: radius);
346 const fullAngle = 360.0;
347 var startAngle = 0.0;
348 for (int i = 0; i < urQrProgress.expectedPartCount.toInt(); i++) {
349 var sweepAngle = (1 / urQrProgress.expectedPartCount) * fullAngle * pi / 180.0;
350 drawSector(
351 canvas, urQrProgress.receivedPartIndexes.contains(i), rect, startAngle, sweepAngle);
352 startAngle += sweepAngle;
353 }
354 }
355
356 void drawSector(Canvas canvas, bool isActive, Rect rect, double startAngle, double sweepAngle) {
357 final paint = Paint()
358 ..style = PaintingStyle.stroke
359 ..strokeWidth = 8
360 ..strokeCap = StrokeCap.round
361 ..strokeJoin = StrokeJoin.round
362 ..color = isActive ? const Color(0xffff6600) : Colors.white70;
363 canvas.drawArc(rect, startAngle, sweepAngle, false, paint);
364 }
365
366 @override
367 bool shouldRepaint(covariant ProgressPainter oldDelegate) {
368 return urQrProgress != oldDelegate.urQrProgress;
369 }
370 }
371
372 class URQrProgress {
373 int expectedPartCount;
374 int processedPartsCount;
375 List<int> receivedPartIndexes;
376 double percentage;
377
378 URQrProgress({
379 required this.expectedPartCount,
380 required this.processedPartsCount,
381 required this.receivedPartIndexes,
382 required this.percentage,
383 });
384
385 bool equals(URQrProgress? progress) {
386 if (progress == null) {
387 return false;
388 }
389 return processedPartsCount == progress.processedPartsCount;
390 }
391 }