225 lines
6.8 KiB
Dart
225 lines
6.8 KiB
Dart
import 'dart:async';
|
|
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: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<MyHomePage> createState() => _MyHomePageState();
|
|
}
|
|
|
|
class _MyHomePageState extends State<MyHomePage> {
|
|
final _barcodeScanner = BarcodeScanner(formats: [BarcodeFormat.qrCode]);
|
|
final _rxTextController = BehaviorSubject<String>();
|
|
late final Stream<String> _rxTextStream = _rxTextController.stream;
|
|
final _scrollController = ScrollController();
|
|
|
|
TxConfig? _txConfig;
|
|
var _cuttleState = CuttleState.unitialized;
|
|
final List<RaptorPacket> _rxData = [];
|
|
String _rxText = '';
|
|
|
|
@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: 1024,
|
|
),
|
|
maxFramesPerSecond: 30,
|
|
autoStart: false,
|
|
),
|
|
builder: (cameraModeState, previewSize, previewRect) {
|
|
return _RxTextDisplayWidget(
|
|
rxTextStream: _rxTextStream,
|
|
scrollController: _scrollController,
|
|
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, ${_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 || barcode.rawValue != null) {
|
|
continue;
|
|
}
|
|
final packet = RaptorPacket(field0: dbytes);
|
|
_rxData.add(packet);
|
|
final content =
|
|
await api.decodePackets(packets: _rxData, 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;
|
|
}
|
|
_rxTextController
|
|
.add("$_rxText -- ${_rxData.length} bytes so far");
|
|
}
|
|
|
|
case CuttleState.received:
|
|
continue;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
debugPrint("sending image resulted error $error");
|
|
}
|
|
}
|
|
|
|
Future<String> _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<String> rxTextStream;
|
|
final ScrollController scrollController;
|
|
|
|
final AnalysisController analysisController;
|
|
|
|
const _RxTextDisplayWidget({
|
|
// ignore: unused_element
|
|
super.key,
|
|
required this.rxTextStream,
|
|
required this.scrollController,
|
|
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: BoxDecoration(
|
|
color: Colors.tealAccent.withOpacity(0.7),
|
|
),
|
|
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<String>(
|
|
stream: widget.rxTextStream,
|
|
builder: (context, value) =>
|
|
!value.hasData ? const SizedBox.expand() : Text(value.data!),
|
|
)),
|
|
),
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
}
|