feat(ocr): 🚧 work on adding entries through OCR

This commit is contained in:
Matyáš Caras 2023-10-09 17:28:05 +02:00
parent de6c5fe315
commit de8f57fcc8
Signed by untrusted user who does not match committer: hernik
GPG key ID: 2A3175F98820C5C6
12 changed files with 266 additions and 100 deletions

View file

@ -1,5 +1,5 @@
# prasule # prasule
[![Codemagic build status](https://api.codemagic.io/apps/64faee78aae8c48abc70dbc6/64faee78aae8c48abc70dbc5/status_badge.svg)](https://codemagic.io/apps/64faee78aae8c48abc70dbc6/64faee78aae8c48abc70dbc5/latest_build) [![Bug issue count](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.mnau.xyz%2Fapi%2Fv1%2Frepos%2Fhernik%2Fprasule%2Fissues%3Flabels%3DKind%2FBug&query=%24.length&logo=forgejo&label=bug%20issues&color=red)](https://git.mnau.xyz/hernik/prasule/issues?q=&type=all&sort=&state=open&labels=144&milestone=0&project=0&assignee=0&poster=0) [![Codemagic build status](https://api.codemagic.io/apps/64faee78aae8c48abc70dbc6/64faee78aae8c48abc70dbc5/status_badge.svg)](https://codemagic.io/apps/64faee78aae8c48abc70dbc6/64faee78aae8c48abc70dbc5/latest_build) [![Bug issue count](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.mnau.xyz%2Fapi%2Fv1%2Frepos%2Fhernik%2Fprasule%2Fissues%3Flabels%3DKind%2FBug&query=%24.length&logo=forgejo&label=bug%20issues&color=red)](https://git.mnau.xyz/hernik/prasule/issues?q=&type=all&sort=&state=open&labels=144&milestone=0&project=0&assignee=0&poster=0) [![wakatime](https://wakatime.com/badge/user/17178fab-a33c-430f-a764-7b3f26c7b966/project/bf1f40b0-c8c0-4f72-8ad6-c861ecdcc90c.svg)](https://wakatime.com/badge/user/17178fab-a33c-430f-a764-7b3f26c7b966/project/bf1f40b0-c8c0-4f72-8ad6-c861ecdcc90c)
Expense manager Expense manager

15
lib/api/entry_data.dart Normal file
View file

@ -0,0 +1,15 @@
import 'package:json_annotation/json_annotation.dart';
part 'entry_data.g.dart';
@JsonSerializable()
class EntryData {
String name;
double amount;
EntryData({required this.name, required this.amount});
factory EntryData.fromJson(Map<String, dynamic> json) =>
_$EntryDataFromJson(json);
Map<String, dynamic> toJson() => _$EntryDataToJson(this);
}

17
lib/api/entry_data.g.dart Normal file
View file

@ -0,0 +1,17 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'entry_data.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
EntryData _$EntryDataFromJson(Map<String, dynamic> json) => EntryData(
name: json['name'] as String,
amount: (json['amount'] as num).toDouble(),
);
Map<String, dynamic> _$EntryDataToJson(EntryData instance) => <String, dynamic>{
'name': instance.name,
'amount': instance.amount,
};

25
lib/api/multientry.dart Normal file
View file

@ -0,0 +1,25 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:prasule/api/category.dart';
import 'package:prasule/api/entry_data.dart';
part 'multientry.g.dart';
@JsonSerializable()
class MultiEntry {
EntryType type;
List<EntryData> data;
DateTime date;
WalletCategory category;
int id;
MultiEntry(
{required this.data,
required this.type,
required this.date,
required this.category,
required this.id});
factory MultiEntry.fromJson(Map<String, dynamic> json) =>
_$MultiEntryFromJson(json);
Map<String, dynamic> toJson() => _$MultiEntryToJson(this);
}

32
lib/api/multientry.g.dart Normal file
View file

@ -0,0 +1,32 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'multientry.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
MultiEntry _$MultiEntryFromJson(Map<String, dynamic> json) => MultiEntry(
data: (json['data'] as List<dynamic>)
.map((e) => EntryData.fromJson(e as Map<String, dynamic>))
.toList(),
type: $enumDecode(_$EntryTypeEnumMap, json['type']),
date: DateTime.parse(json['date'] as String),
category:
WalletCategory.fromJson(json['category'] as Map<String, dynamic>),
id: json['id'] as int,
);
Map<String, dynamic> _$MultiEntryToJson(MultiEntry instance) =>
<String, dynamic>{
'type': _$EntryTypeEnumMap[instance.type]!,
'data': instance.data,
'date': instance.date.toIso8601String(),
'category': instance.category,
'id': instance.id,
};
const _$EntryTypeEnumMap = {
EntryType.expense: 'expense',
EntryType.income: 'income',
};

View file

@ -1,7 +1,7 @@
import 'package:currency_picker/currency_picker.dart'; import 'package:currency_picker/currency_picker.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:prasule/api/category.dart'; import 'package:prasule/api/category.dart';
import 'package:prasule/api/entry.dart'; import 'package:prasule/api/walletentry.dart';
part 'wallet.g.dart'; part 'wallet.g.dart';
Currency _currencyFromJson(Map<String, dynamic> data) => Currency _currencyFromJson(Map<String, dynamic> data) =>
@ -11,7 +11,7 @@ Currency _currencyFromJson(Map<String, dynamic> data) =>
class Wallet { class Wallet {
final String name; final String name;
final List<WalletCategory> categories; final List<WalletCategory> categories;
final List<WalletEntry> entries; final List<WalletSingleEntry> entries;
double availableAmount; double availableAmount;
@JsonKey(fromJson: _currencyFromJson) @JsonKey(fromJson: _currencyFromJson)
final Currency currency; final Currency currency;

View file

@ -14,7 +14,8 @@ Wallet _$WalletFromJson(Map<String, dynamic> json) => Wallet(
.toList() ?? .toList() ??
const [], const [],
entries: (json['entries'] as List<dynamic>?) entries: (json['entries'] as List<dynamic>?)
?.map((e) => WalletEntry.fromJson(e as Map<String, dynamic>)) ?.map(
(e) => WalletSingleEntry.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
const [], const [],
availableAmount: (json['availableAmount'] as num?)?.toDouble() ?? 0, availableAmount: (json['availableAmount'] as num?)?.toDouble() ?? 0,

View file

@ -1,19 +1,20 @@
import 'package:prasule/api/category.dart'; import 'package:prasule/api/category.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'entry.g.dart'; import 'package:prasule/api/entry_data.dart';
part 'walletentry.g.dart';
@JsonSerializable() @JsonSerializable()
class WalletEntry {
/// This is an entry containing a single item
class WalletSingleEntry {
EntryType type; EntryType type;
String name; EntryData data;
double amount;
DateTime date; DateTime date;
WalletCategory category; WalletCategory category;
int id; int id;
WalletEntry( WalletSingleEntry(
{required this.name, {required this.data,
required this.amount,
required this.type, required this.type,
required this.date, required this.date,
required this.category, required this.category,
@ -21,9 +22,9 @@ class WalletEntry {
/// Connect the generated [_$WalletEntry] function to the `fromJson` /// Connect the generated [_$WalletEntry] function to the `fromJson`
/// factory. /// factory.
factory WalletEntry.fromJson(Map<String, dynamic> json) => factory WalletSingleEntry.fromJson(Map<String, dynamic> json) =>
_$WalletEntryFromJson(json); _$WalletSingleEntryFromJson(json);
/// Connect the generated [_$WalletEntryToJson] function to the `toJson` method. /// Connect the generated [_$WalletEntryToJson] function to the `toJson` method.
Map<String, dynamic> toJson() => _$WalletEntryToJson(this); Map<String, dynamic> toJson() => _$WalletSingleEntryToJson(this);
} }

View file

@ -1,14 +1,14 @@
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
part of 'entry.dart'; part of 'walletentry.dart';
// ************************************************************************** // **************************************************************************
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
WalletEntry _$WalletEntryFromJson(Map<String, dynamic> json) => WalletEntry( WalletSingleEntry _$WalletSingleEntryFromJson(Map<String, dynamic> json) =>
name: json['name'] as String, WalletSingleEntry(
amount: (json['amount'] as num).toDouble(), data: EntryData.fromJson(json['data'] as Map<String, dynamic>),
type: $enumDecode(_$EntryTypeEnumMap, json['type']), type: $enumDecode(_$EntryTypeEnumMap, json['type']),
date: DateTime.parse(json['date'] as String), date: DateTime.parse(json['date'] as String),
category: category:
@ -16,11 +16,10 @@ WalletEntry _$WalletEntryFromJson(Map<String, dynamic> json) => WalletEntry(
id: json['id'] as int, id: json['id'] as int,
); );
Map<String, dynamic> _$WalletEntryToJson(WalletEntry instance) => Map<String, dynamic> _$WalletSingleEntryToJson(WalletSingleEntry instance) =>
<String, dynamic>{ <String, dynamic>{
'type': _$EntryTypeEnumMap[instance.type]!, 'type': _$EntryTypeEnumMap[instance.type]!,
'name': instance.name, 'data': instance.data,
'amount': instance.amount,
'date': instance.date.toIso8601String(), 'date': instance.date.toIso8601String(),
'category': instance.category, 'category': instance.category,
'id': instance.id, 'id': instance.id,

View file

@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:prasule/api/category.dart'; import 'package:prasule/api/category.dart';
import 'package:prasule/api/entry.dart'; import 'package:prasule/api/entry_data.dart';
import 'package:prasule/api/walletentry.dart';
import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/wallet.dart';
import 'package:prasule/api/walletmanager.dart'; import 'package:prasule/api/walletmanager.dart';
import 'package:prasule/pw/platformbutton.dart'; import 'package:prasule/pw/platformbutton.dart';
@ -9,7 +10,7 @@ import 'package:prasule/pw/platformfield.dart';
class CreateEntryView extends StatefulWidget { class CreateEntryView extends StatefulWidget {
final Wallet w; final Wallet w;
final WalletEntry? editEntry; final WalletSingleEntry? editEntry;
const CreateEntryView({super.key, required this.w, this.editEntry}); const CreateEntryView({super.key, required this.w, this.editEntry});
@override @override
@ -17,7 +18,7 @@ class CreateEntryView extends StatefulWidget {
} }
class _CreateEntryViewState extends State<CreateEntryView> { class _CreateEntryViewState extends State<CreateEntryView> {
late WalletEntry newEntry; late WalletSingleEntry newEntry;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -28,10 +29,9 @@ class _CreateEntryViewState extends State<CreateEntryView> {
while (widget.w.entries.where((element) => element.id == id).isNotEmpty) { while (widget.w.entries.where((element) => element.id == id).isNotEmpty) {
id++; // create unique ID id++; // create unique ID
} }
newEntry = WalletEntry( newEntry = WalletSingleEntry(
id: id, id: id,
name: "", data: EntryData(amount: 0, name: ""),
amount: 0,
type: EntryType.expense, type: EntryType.expense,
date: DateTime.now(), date: DateTime.now(),
category: widget.w.categories.first); category: widget.w.categories.first);
@ -57,9 +57,9 @@ class _CreateEntryViewState extends State<CreateEntryView> {
width: MediaQuery.of(context).size.width * 0.8, width: MediaQuery.of(context).size.width * 0.8,
child: PlatformField( child: PlatformField(
labelText: "Name", labelText: "Name",
controller: TextEditingController(text: newEntry.name), controller: TextEditingController(text: newEntry.data.name),
onChanged: (v) { onChanged: (v) {
newEntry.name = v; newEntry.data.name = v;
}, },
), ),
), ),
@ -70,8 +70,8 @@ class _CreateEntryViewState extends State<CreateEntryView> {
width: MediaQuery.of(context).size.width * 0.8, width: MediaQuery.of(context).size.width * 0.8,
child: PlatformField( child: PlatformField(
labelText: "Amount", labelText: "Amount",
controller: controller: TextEditingController(
TextEditingController(text: newEntry.amount.toString()), text: newEntry.data.amount.toString()),
keyboardType: keyboardType:
const TextInputType.numberWithOptions(decimal: true), const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [ inputFormatters: [
@ -79,7 +79,7 @@ class _CreateEntryViewState extends State<CreateEntryView> {
RegExp(r'\d+[\.,]{0,1}\d{0,}')) RegExp(r'\d+[\.,]{0,1}\d{0,}'))
], ],
onChanged: (v) { onChanged: (v) {
newEntry.amount = double.parse(v); newEntry.data.amount = double.parse(v);
}, },
), ),
), ),
@ -157,7 +157,7 @@ class _CreateEntryViewState extends State<CreateEntryView> {
PlatformButton( PlatformButton(
text: "Save", text: "Save",
onPressed: () { onPressed: () {
if (newEntry.name.isEmpty) { if (newEntry.data.name.isEmpty) {
ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(

View file

@ -6,7 +6,8 @@ import 'package:grouped_list/grouped_list.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:prasule/api/entry.dart'; import 'package:prasule/api/entry_data.dart';
import 'package:prasule/api/walletentry.dart';
import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/wallet.dart';
import 'package:prasule/api/walletmanager.dart'; import 'package:prasule/api/walletmanager.dart';
import 'package:prasule/main.dart'; import 'package:prasule/main.dart';
@ -14,6 +15,7 @@ import 'package:prasule/network/tessdata.dart';
import 'package:prasule/pw/platformbutton.dart'; import 'package:prasule/pw/platformbutton.dart';
import 'package:prasule/pw/platformdialog.dart'; import 'package:prasule/pw/platformdialog.dart';
import 'package:prasule/views/create_entry.dart'; import 'package:prasule/views/create_entry.dart';
import 'package:prasule/views/multientry_creator.dart';
import 'package:prasule/views/settings/settings.dart'; import 'package:prasule/views/settings/settings.dart';
import 'package:prasule/views/settings/tessdata_list.dart'; import 'package:prasule/views/settings/tessdata_list.dart';
import 'package:prasule/views/setup.dart'; import 'package:prasule/views/setup.dart';
@ -162,7 +164,7 @@ class _HomeViewState extends State<HomeView> {
SlidableAction( SlidableAction(
onPressed: (c) { onPressed: (c) {
Navigator.of(context) Navigator.of(context)
.push<WalletEntry>( .push<WalletSingleEntry>(
MaterialPageRoute( MaterialPageRoute(
builder: (c) => CreateEntryView( builder: (c) => CreateEntryView(
w: selectedWallet!, w: selectedWallet!,
@ -237,9 +239,9 @@ class _HomeViewState extends State<HomeView> {
), ),
), ),
), ),
title: Text(element.name), title: Text(element.data.name),
subtitle: Text( subtitle: Text(
"${element.amount} ${selectedWallet!.currency.symbol}"), "${element.data.amount} ${selectedWallet!.currency.symbol}"),
), ),
), ),
), ),
@ -272,12 +274,12 @@ class _HomeViewState extends State<HomeView> {
} }
if (!mounted) return; if (!mounted) return;
var selectedLanguages = List<bool>.filled(availableLanguages.length, false); var selectedLanguages = List<bool>.filled(availableLanguages.length, false);
if (selectedLanguages.length == 1) {
selectedLanguages[0] = true; selectedLanguages[0] = true;
}
showDialog( showDialog(
context: context, context: context,
builder: (c) => PlatformDialog( builder: (c) => StatefulBuilder(
builder: (ctx, setState) => PlatformDialog(
actions: [ actions: [
TextButton( TextButton(
onPressed: () async { onPressed: () async {
@ -289,20 +291,46 @@ class _HomeViewState extends State<HomeView> {
} }
// get selected languages // get selected languages
var selected = availableLanguages var selected = availableLanguages
.where((element) => .where((element) => selectedLanguages[
selectedLanguages[availableLanguages.indexOf(element)]) availableLanguages.indexOf(element)])
.join("+") .join("+")
.replaceAll(".traineddata", ""); .replaceAll(".traineddata", "");
logger.i(selected); logger.i(selected);
if (!mounted) return;
showDialog(
context: context,
builder: (c) => const PlatformDialog(
title:
"Loading text from image, please wait a moment..."),
barrierDismissible: false);
var string = await FlutterTesseractOcr.extractText(media.path, var string = await FlutterTesseractOcr.extractText(media.path,
language: selected, language: selected,
args: { args: {
//"psm": "4", "psm": "4",
"preserve_interword_spaces": "1", "preserve_interword_spaces": "1",
}); });
if (!mounted) return;
Navigator.of(context).pop();
logger.i(string); logger.i(string);
if (mounted) Navigator.of(context).pop(); if (!mounted) return;
return; var lines = string.split("\n")
..removeWhere((element) {
element.trim();
return element.isEmpty;
});
var data = <EntryData>[];
for (var line in lines) {
var regex = RegExp(r'');
}
Navigator.of(context).pop();
Navigator.of(context).push(
MaterialPageRoute(
builder: (c) => MultientryCreateView(
linesToAdd: data,
),
),
);
}, },
child: const Text("Ok")), child: const Text("Ok")),
TextButton( TextButton(
@ -328,7 +356,7 @@ class _HomeViewState extends State<HomeView> {
1 && 1 &&
!value)) return; !value)) return;
selectedLanguages[index] = value; selectedLanguages[index] = value;
setState(() {}); // todo: builder setState(() {});
}, },
), ),
const SizedBox( const SizedBox(
@ -341,6 +369,7 @@ class _HomeViewState extends State<HomeView> {
], ],
), ),
), ),
),
); );
} }
@ -352,6 +381,7 @@ class _HomeViewState extends State<HomeView> {
} }
final List<XFile>? files = response.files; final List<XFile>? files = response.files;
if (files != null) { if (files != null) {
logger.i("Found lost files");
_handleLostFiles(files); _handleLostFiles(files);
} else { } else {
logger.e(response.exception); logger.e(response.exception);

View file

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:prasule/api/entry_data.dart';
class MultientryCreateView extends StatefulWidget {
const MultientryCreateView({super.key, required this.linesToAdd});
final List<EntryData> linesToAdd;
@override
State<MultientryCreateView> createState() => _MultientryCreateViewState();
}
class _MultientryCreateViewState extends State<MultientryCreateView> {
final _isOpen = <bool>[];
@override
void initState() {
super.initState();
_isOpen.clear();
_isOpen.addAll(List<bool>.filled(widget.linesToAdd.length, false));
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Add from OCR"),
),
body: SingleChildScrollView(
child: ExpansionPanelList(
expansionCallback: (i, expanded) {
_isOpen[i] = !_isOpen[i];
setState(() {});
},
children: List.generate(
widget.linesToAdd.length,
(index) => ExpansionPanel(
headerBuilder: (c, expanded) => const Text(""),
body: Text(
widget.linesToAdd[index].name,
),
isExpanded: _isOpen[index]),
),
),
),
);
}
}