import 'dart:async'; import 'dart:collection'; import 'dart:io'; import 'dart:typed_data'; import 'package:camerawesome/camerawesome_plugin.dart'; import 'package:flutter/material.dart'; import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; import 'package:intl/intl.dart'; import 'package:rxdart/rxdart.dart'; import 'ffi.dart'; import 'utils/mlkit_utils.dart'; void main() { runApp(const MyApp()); } enum CuttleState { unitialized, receiving, received, } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Cuttle', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { final _barcodeScanner = BarcodeScanner(formats: [BarcodeFormat.qrCode]); final _rxTextController = BehaviorSubject(); late final Stream _rxTextStream = _rxTextController.stream; TxConfig? _txConfig; var _cuttleState = CuttleState.unitialized; final HashSet _rxData = HashSet(); String _rxText = ''; int _rxCount = 1; final _formatter = NumberFormat('###,###,###'); @override void dispose() { _rxTextController.close(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: CameraAwesomeBuilder.previewOnly( onImageForAnalysis: (img) => _processImageBarcode(img), imageAnalysisConfig: AnalysisConfig( androidOptions: const AndroidAnalysisOptions.nv21( width: 600, ), maxFramesPerSecond: 20, autoStart: false, ), builder: (cameraModeState, previewSize, previewRect) { return _RxTextDisplayWidget( rxTextStream: _rxTextStream, analysisController: cameraModeState.analysisController!, ); }, ), ); } Future _processImageBarcode(AnalysisImage img) async { final inputImage = toInputImage(img as Nv21Image); try { var recognizedBarCodes = await _barcodeScanner.processImage(inputImage); for (Barcode barcode in recognizedBarCodes) { var bytes = barcode.rawBytes; if (bytes == null) { continue; } final Uint8List dbytes = bytes; switch (_cuttleState) { case CuttleState.unitialized: { var txconf = await api.getTxConfig(bytes: dbytes); if (txconf != null) { _txConfig = txconf; _cuttleState = CuttleState.receiving; final fname = _txConfig!.filename ?? "large text on the command line"; final desc = _txConfig!.description; final text = 'Receiving $fname, ${_formatter.format(_txConfig!.len)} bytes ($desc)'; _rxText = text; _rxTextController.add(text); continue; } // implicit else here; txconf was null var text = barcode.rawValue; if (text != null) { // it's not a txconfig, and it's not a raptor packet, so it must be a regular qr code _rxText = text; _rxTextController.add(text); _cuttleState = CuttleState.received; } } case CuttleState.receiving: { var txconf = await api.getTxConfig(bytes: dbytes); if (txconf != null) { await api.dropTxConfig(txc: txconf); continue; } final packet = RaptorPacket(field0: dbytes); _rxData.add(packet); _rxCount += 1; if (_rxCount % 40 == 0) { _rxCount = 1; final content = await api.decodePackets( packets: _rxData.toList(), txconf: _txConfig!); if (content != null) { _rxData.clear(); _barcodeScanner.close(); _cuttleState = CuttleState.received; _rxTextController.add("DONE RECEIVING $_rxText"); final f = await _saveReceivedFile(_txConfig!.filename, content); _rxTextController.add("Saved content to $f"); continue; } } final bytesTotal = _rxData.length * dbytes.length; final pct = (100.0 * bytesTotal / _txConfig!.len).floor(); _rxTextController.add( "$_rxText -- $pct% received (${_formatter.format(bytesTotal)} bytes)"); } case CuttleState.received: continue; } } } catch (error) { debugPrint("sending image resulted error $error"); } } Future _saveReceivedFile(String? filename, Uint8List bytes) async { final Directory downloadDir = Directory('/storage/emulated/0/Download'); final String fname = filename ?? "cuttle_${DateTime.now().millisecondsSinceEpoch}.txt"; final String path = "${downloadDir.path}/$fname"; final file = await File(path).create(); await file.writeAsBytes(bytes, flush: true); return path; } } class _RxTextDisplayWidget extends StatefulWidget { final Stream rxTextStream; final AnalysisController analysisController; const _RxTextDisplayWidget({ // ignore: unused_element super.key, required this.rxTextStream, required this.analysisController, }); @override State<_RxTextDisplayWidget> createState() => _RxTextDisplayWidgetState(); } class _RxTextDisplayWidgetState extends State<_RxTextDisplayWidget> { @override Widget build(BuildContext context) { return Align( alignment: Alignment.bottomCenter, child: Container( decoration: const BoxDecoration( color: Colors.white, ), child: Column(mainAxisSize: MainAxisSize.min, children: [ Material( color: Colors.transparent, child: CheckboxListTile( value: widget.analysisController.enabled, onChanged: (newValue) async { if (widget.analysisController.enabled == true) { await widget.analysisController.stop(); } else { await widget.analysisController.start(); } setState(() {}); }, title: const Text( "Enable barcode scan", style: TextStyle(fontWeight: FontWeight.bold), ), ), ), Container( height: 120, padding: const EdgeInsets.symmetric(horizontal: 16), child: SelectionArea( child: StreamBuilder( stream: widget.rxTextStream, builder: (context, value) => !value.hasData ? const SizedBox.expand() : Text(value.data!), )), ), ]), ), ); } }