parser logic added

This commit is contained in:
davmar
2026-08-30 12:24:29 +02:00
parent 35d5c90d81
commit 3b698a3667
28 changed files with 3018 additions and 346 deletions
+181
View File
@@ -0,0 +1,181 @@
/// Drag a crop box over the receipt photo before OCR runs.
library;
import 'dart:io';
import 'dart:typed_data';
import 'package:crop_your_image/crop_your_image.dart';
import 'package:flutter/material.dart';
class CropReceiptScreen extends StatefulWidget {
const CropReceiptScreen({super.key, required this.imagePath});
final String imagePath;
@override
State<CropReceiptScreen> createState() => _CropReceiptScreenState();
}
class _CropReceiptScreenState extends State<CropReceiptScreen> {
final CropController _controller = CropController();
Uint8List? _image;
String? _error;
bool _ready = false;
bool _cropping = false;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
try {
final bytes = await File(widget.imagePath).readAsBytes();
if (!mounted) return;
setState(() => _image = bytes);
} catch (_) {
if (!mounted) return;
setState(() => _error = 'Could not open this photo.');
}
}
void _onCropped(CropResult result) {
switch (result) {
case CropSuccess(:final croppedImage):
_saveAndPop(croppedImage);
case CropFailure():
if (!mounted) return;
setState(() => _cropping = false);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not crop this photo.')),
);
}
}
Future<void> _saveAndPop(Uint8List bytes) async {
try {
final file = File(
'${Directory.systemTemp.path}/receipity_crop_${DateTime.now().millisecondsSinceEpoch}.jpg',
);
await file.writeAsBytes(bytes, flush: true);
if (!mounted) return;
Navigator.of(context).pop(file.path);
} catch (_) {
if (!mounted) return;
setState(() => _cropping = false);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not save the cropped photo.')),
);
}
}
void _confirm() {
if (!_ready || _cropping) return;
setState(() => _cropping = true);
_controller.crop();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return PopScope(
canPop: !_cropping,
child: Scaffold(
appBar: AppBar(title: const Text('Crop receipt')),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(child: _editor(theme)),
SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Drag the corners over the text you want to read. Pinch to zoom.',
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
FilledButton.icon(
onPressed: _ready && !_cropping ? _confirm : null,
icon: const Icon(Icons.check),
label: Text(_cropping ? 'Cropping…' : 'Use this area'),
),
],
),
),
),
],
),
),
);
}
Widget _editor(ThemeData theme) {
if (_error != null) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
_error!,
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge,
),
),
);
}
final image = _image;
if (image == null) {
return const Center(child: CircularProgressIndicator());
}
return Stack(
fit: StackFit.expand,
children: [
Crop(
image: image,
controller: _controller,
onCropped: _onCropped,
interactive: true,
filterQuality: FilterQuality.medium,
baseColor: theme.colorScheme.surface,
maskColor: Colors.black.withValues(alpha: 0.55),
radius: 8,
progressIndicator: const Center(child: CircularProgressIndicator()),
initialRectBuilder: InitialRectBuilder.withBuilder((
viewportRect,
imageRect,
) {
final dx = imageRect.width * 0.06;
final dy = imageRect.height * 0.06;
return Rect.fromLTRB(
imageRect.left + dx,
imageRect.top + dy,
imageRect.right - dx,
imageRect.bottom - dy,
);
}),
cornerDotBuilder: (size, _) =>
DotControl(color: theme.colorScheme.primary),
onStatusChanged: (status) {
final ready = status == CropStatus.ready;
if (ready == _ready) return;
setState(() => _ready = ready);
},
),
if (_cropping)
const ColoredBox(
color: Color(0x66000000),
child: Center(child: CircularProgressIndicator()),
),
],
);
}
}