From de8f57fcc8c41ae9033ee2a7017428aa62333c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Mon, 9 Oct 2023 17:28:05 +0200 Subject: [PATCH] feat(ocr): :construction: work on adding entries through OCR --- README.md | 2 +- lib/api/entry_data.dart | 15 ++ lib/api/entry_data.g.dart | 17 ++ lib/api/multientry.dart | 25 +++ lib/api/multientry.g.dart | 32 ++++ lib/api/wallet.dart | 4 +- lib/api/wallet.g.dart | 3 +- lib/api/{entry.dart => walletentry.dart} | 21 +-- lib/api/{entry.g.dart => walletentry.g.dart} | 13 +- lib/views/create_entry.dart | 24 +-- lib/views/home.dart | 164 +++++++++++-------- lib/views/multientry_creator.dart | 46 ++++++ 12 files changed, 266 insertions(+), 100 deletions(-) create mode 100644 lib/api/entry_data.dart create mode 100644 lib/api/entry_data.g.dart create mode 100644 lib/api/multientry.dart create mode 100644 lib/api/multientry.g.dart rename lib/api/{entry.dart => walletentry.dart} (54%) rename lib/api/{entry.g.dart => walletentry.g.dart} (70%) create mode 100644 lib/views/multientry_creator.dart diff --git a/README.md b/README.md index cb6c766..1476323 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 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 diff --git a/lib/api/entry_data.dart b/lib/api/entry_data.dart new file mode 100644 index 0000000..5044bfc --- /dev/null +++ b/lib/api/entry_data.dart @@ -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 json) => + _$EntryDataFromJson(json); + + Map toJson() => _$EntryDataToJson(this); +} diff --git a/lib/api/entry_data.g.dart b/lib/api/entry_data.g.dart new file mode 100644 index 0000000..2be8617 --- /dev/null +++ b/lib/api/entry_data.g.dart @@ -0,0 +1,17 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'entry_data.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +EntryData _$EntryDataFromJson(Map json) => EntryData( + name: json['name'] as String, + amount: (json['amount'] as num).toDouble(), + ); + +Map _$EntryDataToJson(EntryData instance) => { + 'name': instance.name, + 'amount': instance.amount, + }; diff --git a/lib/api/multientry.dart b/lib/api/multientry.dart new file mode 100644 index 0000000..b53cdc8 --- /dev/null +++ b/lib/api/multientry.dart @@ -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 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 json) => + _$MultiEntryFromJson(json); + + Map toJson() => _$MultiEntryToJson(this); +} diff --git a/lib/api/multientry.g.dart b/lib/api/multientry.g.dart new file mode 100644 index 0000000..3afafa6 --- /dev/null +++ b/lib/api/multientry.g.dart @@ -0,0 +1,32 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'multientry.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +MultiEntry _$MultiEntryFromJson(Map json) => MultiEntry( + data: (json['data'] as List) + .map((e) => EntryData.fromJson(e as Map)) + .toList(), + type: $enumDecode(_$EntryTypeEnumMap, json['type']), + date: DateTime.parse(json['date'] as String), + category: + WalletCategory.fromJson(json['category'] as Map), + id: json['id'] as int, + ); + +Map _$MultiEntryToJson(MultiEntry instance) => + { + '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', +}; diff --git a/lib/api/wallet.dart b/lib/api/wallet.dart index 90d89a3..d4aa60b 100644 --- a/lib/api/wallet.dart +++ b/lib/api/wallet.dart @@ -1,7 +1,7 @@ import 'package:currency_picker/currency_picker.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:prasule/api/category.dart'; -import 'package:prasule/api/entry.dart'; +import 'package:prasule/api/walletentry.dart'; part 'wallet.g.dart'; Currency _currencyFromJson(Map data) => @@ -11,7 +11,7 @@ Currency _currencyFromJson(Map data) => class Wallet { final String name; final List categories; - final List entries; + final List entries; double availableAmount; @JsonKey(fromJson: _currencyFromJson) final Currency currency; diff --git a/lib/api/wallet.g.dart b/lib/api/wallet.g.dart index 0636f95..f3f7243 100644 --- a/lib/api/wallet.g.dart +++ b/lib/api/wallet.g.dart @@ -14,7 +14,8 @@ Wallet _$WalletFromJson(Map json) => Wallet( .toList() ?? const [], entries: (json['entries'] as List?) - ?.map((e) => WalletEntry.fromJson(e as Map)) + ?.map( + (e) => WalletSingleEntry.fromJson(e as Map)) .toList() ?? const [], availableAmount: (json['availableAmount'] as num?)?.toDouble() ?? 0, diff --git a/lib/api/entry.dart b/lib/api/walletentry.dart similarity index 54% rename from lib/api/entry.dart rename to lib/api/walletentry.dart index cf0e027..48c6e0f 100644 --- a/lib/api/entry.dart +++ b/lib/api/walletentry.dart @@ -1,19 +1,20 @@ import 'package:prasule/api/category.dart'; import 'package:json_annotation/json_annotation.dart'; -part 'entry.g.dart'; +import 'package:prasule/api/entry_data.dart'; +part 'walletentry.g.dart'; @JsonSerializable() -class WalletEntry { + +/// This is an entry containing a single item +class WalletSingleEntry { EntryType type; - String name; - double amount; + EntryData data; DateTime date; WalletCategory category; int id; - WalletEntry( - {required this.name, - required this.amount, + WalletSingleEntry( + {required this.data, required this.type, required this.date, required this.category, @@ -21,9 +22,9 @@ class WalletEntry { /// Connect the generated [_$WalletEntry] function to the `fromJson` /// factory. - factory WalletEntry.fromJson(Map json) => - _$WalletEntryFromJson(json); + factory WalletSingleEntry.fromJson(Map json) => + _$WalletSingleEntryFromJson(json); /// Connect the generated [_$WalletEntryToJson] function to the `toJson` method. - Map toJson() => _$WalletEntryToJson(this); + Map toJson() => _$WalletSingleEntryToJson(this); } diff --git a/lib/api/entry.g.dart b/lib/api/walletentry.g.dart similarity index 70% rename from lib/api/entry.g.dart rename to lib/api/walletentry.g.dart index 80ada6e..fcdd640 100644 --- a/lib/api/entry.g.dart +++ b/lib/api/walletentry.g.dart @@ -1,14 +1,14 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'entry.dart'; +part of 'walletentry.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** -WalletEntry _$WalletEntryFromJson(Map json) => WalletEntry( - name: json['name'] as String, - amount: (json['amount'] as num).toDouble(), +WalletSingleEntry _$WalletSingleEntryFromJson(Map json) => + WalletSingleEntry( + data: EntryData.fromJson(json['data'] as Map), type: $enumDecode(_$EntryTypeEnumMap, json['type']), date: DateTime.parse(json['date'] as String), category: @@ -16,11 +16,10 @@ WalletEntry _$WalletEntryFromJson(Map json) => WalletEntry( id: json['id'] as int, ); -Map _$WalletEntryToJson(WalletEntry instance) => +Map _$WalletSingleEntryToJson(WalletSingleEntry instance) => { 'type': _$EntryTypeEnumMap[instance.type]!, - 'name': instance.name, - 'amount': instance.amount, + 'data': instance.data, 'date': instance.date.toIso8601String(), 'category': instance.category, 'id': instance.id, diff --git a/lib/views/create_entry.dart b/lib/views/create_entry.dart index 43d4d75..bef5781 100644 --- a/lib/views/create_entry.dart +++ b/lib/views/create_entry.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.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/walletmanager.dart'; import 'package:prasule/pw/platformbutton.dart'; @@ -9,7 +10,7 @@ import 'package:prasule/pw/platformfield.dart'; class CreateEntryView extends StatefulWidget { final Wallet w; - final WalletEntry? editEntry; + final WalletSingleEntry? editEntry; const CreateEntryView({super.key, required this.w, this.editEntry}); @override @@ -17,7 +18,7 @@ class CreateEntryView extends StatefulWidget { } class _CreateEntryViewState extends State { - late WalletEntry newEntry; + late WalletSingleEntry newEntry; @override void initState() { super.initState(); @@ -28,10 +29,9 @@ class _CreateEntryViewState extends State { while (widget.w.entries.where((element) => element.id == id).isNotEmpty) { id++; // create unique ID } - newEntry = WalletEntry( + newEntry = WalletSingleEntry( id: id, - name: "", - amount: 0, + data: EntryData(amount: 0, name: ""), type: EntryType.expense, date: DateTime.now(), category: widget.w.categories.first); @@ -57,9 +57,9 @@ class _CreateEntryViewState extends State { width: MediaQuery.of(context).size.width * 0.8, child: PlatformField( labelText: "Name", - controller: TextEditingController(text: newEntry.name), + controller: TextEditingController(text: newEntry.data.name), onChanged: (v) { - newEntry.name = v; + newEntry.data.name = v; }, ), ), @@ -70,8 +70,8 @@ class _CreateEntryViewState extends State { width: MediaQuery.of(context).size.width * 0.8, child: PlatformField( labelText: "Amount", - controller: - TextEditingController(text: newEntry.amount.toString()), + controller: TextEditingController( + text: newEntry.data.amount.toString()), keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [ @@ -79,7 +79,7 @@ class _CreateEntryViewState extends State { RegExp(r'\d+[\.,]{0,1}\d{0,}')) ], onChanged: (v) { - newEntry.amount = double.parse(v); + newEntry.data.amount = double.parse(v); }, ), ), @@ -157,7 +157,7 @@ class _CreateEntryViewState extends State { PlatformButton( text: "Save", onPressed: () { - if (newEntry.name.isEmpty) { + if (newEntry.data.name.isEmpty) { ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( diff --git a/lib/views/home.dart b/lib/views/home.dart index 3bda87f..51d49ef 100644 --- a/lib/views/home.dart +++ b/lib/views/home.dart @@ -6,7 +6,8 @@ import 'package:grouped_list/grouped_list.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/date_symbol_data_local.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/walletmanager.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/platformdialog.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/tessdata_list.dart'; import 'package:prasule/views/setup.dart'; @@ -162,7 +164,7 @@ class _HomeViewState extends State { SlidableAction( onPressed: (c) { Navigator.of(context) - .push( + .push( MaterialPageRoute( builder: (c) => CreateEntryView( w: selectedWallet!, @@ -237,9 +239,9 @@ class _HomeViewState extends State { ), ), ), - title: Text(element.name), + title: Text(element.data.name), subtitle: Text( - "${element.amount} ${selectedWallet!.currency.symbol}"), + "${element.data.amount} ${selectedWallet!.currency.symbol}"), ), ), ), @@ -272,73 +274,100 @@ class _HomeViewState extends State { } if (!mounted) return; var selectedLanguages = List.filled(availableLanguages.length, false); - if (selectedLanguages.length == 1) { - selectedLanguages[0] = true; - } + selectedLanguages[0] = true; + showDialog( context: context, - builder: (c) => PlatformDialog( - actions: [ - TextButton( - onPressed: () async { - final ImagePicker picker = ImagePicker(); - final XFile? media = await picker.pickImage(source: imgSrc); - if (media == null) { - if (mounted) Navigator.of(context).pop(); - return; - } - // get selected languages - var selected = availableLanguages - .where((element) => - selectedLanguages[availableLanguages.indexOf(element)]) - .join("+") - .replaceAll(".traineddata", ""); - logger.i(selected); - var string = await FlutterTesseractOcr.extractText(media.path, - language: selected, - args: { - //"psm": "4", - "preserve_interword_spaces": "1", + builder: (c) => StatefulBuilder( + builder: (ctx, setState) => PlatformDialog( + actions: [ + TextButton( + onPressed: () async { + final ImagePicker picker = ImagePicker(); + final XFile? media = await picker.pickImage(source: imgSrc); + if (media == null) { + if (mounted) Navigator.of(context).pop(); + return; + } + // get selected languages + var selected = availableLanguages + .where((element) => selectedLanguages[ + availableLanguages.indexOf(element)]) + .join("+") + .replaceAll(".traineddata", ""); + 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, + language: selected, + args: { + "psm": "4", + "preserve_interword_spaces": "1", + }); + if (!mounted) return; + Navigator.of(context).pop(); + logger.i(string); + if (!mounted) return; + var lines = string.split("\n") + ..removeWhere((element) { + element.trim(); + return element.isEmpty; }); - logger.i(string); - if (mounted) Navigator.of(context).pop(); - return; - }, - child: const Text("Ok")), - TextButton( - onPressed: () { - Navigator.of(c).pop(); - }, - child: const Text("Cancel")), - ], - title: "Select languages for OCR", - content: Column( - children: [ - ...List.generate( - availableLanguages.length, - (index) => Row( - children: [ - Checkbox( - value: selectedLanguages[index], - onChanged: (value) { - if (value == null || - (selectedLanguages - .where((element) => element) - .length <= - 1 && - !value)) return; - selectedLanguages[index] = value; - setState(() {}); // todo: builder - }, - ), - const SizedBox( - width: 10, - ), - Text(availableLanguages[index].split(".").first) - ], - ), - ) + + var data = []; + 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")), + TextButton( + onPressed: () { + Navigator.of(c).pop(); + }, + child: const Text("Cancel")), ], + title: "Select languages for OCR", + content: Column( + children: [ + ...List.generate( + availableLanguages.length, + (index) => Row( + children: [ + Checkbox( + value: selectedLanguages[index], + onChanged: (value) { + if (value == null || + (selectedLanguages + .where((element) => element) + .length <= + 1 && + !value)) return; + selectedLanguages[index] = value; + setState(() {}); + }, + ), + const SizedBox( + width: 10, + ), + Text(availableLanguages[index].split(".").first) + ], + ), + ) + ], + ), ), ), ); @@ -352,6 +381,7 @@ class _HomeViewState extends State { } final List? files = response.files; if (files != null) { + logger.i("Found lost files"); _handleLostFiles(files); } else { logger.e(response.exception); diff --git a/lib/views/multientry_creator.dart b/lib/views/multientry_creator.dart new file mode 100644 index 0000000..57af283 --- /dev/null +++ b/lib/views/multientry_creator.dart @@ -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 linesToAdd; + @override + State createState() => _MultientryCreateViewState(); +} + +class _MultientryCreateViewState extends State { + final _isOpen = []; + @override + void initState() { + super.initState(); + _isOpen.clear(); + _isOpen.addAll(List.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]), + ), + ), + ), + ); + } +}