parser logic added
This commit is contained in:
@@ -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()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user