cuttle/mobile/lib/main.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!),
)),
),
]),
),
);
}
}