Compare commits
4 commits
de6c5fe315
...
d1f37282f0
Author | SHA1 | Date | |
---|---|---|---|
|
d1f37282f0 | ||
|
d82a5f4b1f | ||
|
f8f40f6db6 | ||
|
de8f57fcc8 |
19 changed files with 464 additions and 225 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
3
l10n.yaml
Normal file
3
l10n.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
arb-dir: lib/l10n
|
||||||
|
template-arb-file: app_en.arb
|
||||||
|
output-localization-file: app_localizations.dart
|
15
lib/api/entry_data.dart
Normal file
15
lib/api/entry_data.dart
Normal 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
17
lib/api/entry_data.g.dart
Normal 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
25
lib/api/multientry.dart
Normal 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
32
lib/api/multientry.g.dart
Normal 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',
|
||||||
|
};
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
|
@ -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,
|
92
lib/l10n/app_en.arb
Normal file
92
lib/l10n/app_en.arb
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
{
|
||||||
|
"categoryHealth": "Health",
|
||||||
|
"categoryCar": "Car",
|
||||||
|
"categoryFood": "Food",
|
||||||
|
"categoryTravel": "Travel",
|
||||||
|
"next": "Next",
|
||||||
|
"back": "Back",
|
||||||
|
"finish": "Finish",
|
||||||
|
"errorEmptyName": "Name cannot be empty",
|
||||||
|
"welcome": "Welcome!",
|
||||||
|
"welcomeAboutPrasule": "Prašule is an expense tracker tool designed for people, who don't want to spend too much time filling in all the little details.",
|
||||||
|
"welcomeInstruction": "On this screen you will set up your 'wallet', in which you will track your expenses categorized under categories, which you can later set in the settings menu.",
|
||||||
|
"setupWalletNameCurrency": "Set your wallet's name and currency",
|
||||||
|
"setupNamePlaceholder": "Your awesome name here...",
|
||||||
|
"setupCurrency": "Currency: {currency}",
|
||||||
|
"@setupCurrency": {
|
||||||
|
"description": "Shows the currently selected currency on the setup screen",
|
||||||
|
"placeholders": {
|
||||||
|
"currency": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "CZK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"setupCategoriesHeading": "Create categories",
|
||||||
|
"setupCategoriesEditHint": "Tap on the icon or name to edit it",
|
||||||
|
"ok": "Ok",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"setupCategoriesEditingName": "Editing name",
|
||||||
|
"setupWalletNamePlaceholder": "Edit me",
|
||||||
|
"addNew": "Add new",
|
||||||
|
"addCamera": "Add through camera",
|
||||||
|
"addGallery": "Add through saved image",
|
||||||
|
"home": "Home",
|
||||||
|
"settings": "Settings",
|
||||||
|
"about": "About",
|
||||||
|
"noEntries": "No entries :(",
|
||||||
|
"noEntriesSub": "Add one using the floating action button.",
|
||||||
|
"sureDialog": "Are you sure?",
|
||||||
|
"deleteSure": "Do you really want to delete this entry?",
|
||||||
|
"missingOcr": "You don't have any OCR language data downloaded!",
|
||||||
|
"download": "Download",
|
||||||
|
"ocrLoading": "Loading text from image, please wait a moment...",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No",
|
||||||
|
"ocrSelect": "Select languages for OCR",
|
||||||
|
"createEntry": "Create new entry",
|
||||||
|
"name": "Name",
|
||||||
|
"amount": "Amount",
|
||||||
|
"type": "Type",
|
||||||
|
"expense": "Expense",
|
||||||
|
"income": "Income",
|
||||||
|
"category": "Category",
|
||||||
|
"save": "Save",
|
||||||
|
"downloadedOcr": "View downloaded OCR data",
|
||||||
|
"downloadedOcrDesc": "This data is used by the OCR engine to recognize text from pictues",
|
||||||
|
"ocr": "OCR",
|
||||||
|
"ocrData": "OCR Data",
|
||||||
|
"downloaded": "Downloaded",
|
||||||
|
"deleteOcr": "Do you really want to delete '$lang' OCR data?\nYou will not be able to use these language data when scanning pictures.",
|
||||||
|
"@deleteOcr": {
|
||||||
|
"description": "Shown when a user wants to delete OCR data through settings",
|
||||||
|
"placeholders": {
|
||||||
|
"lang": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "ces"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"langDownloadDialog": "Downloading $lang, please wait...",
|
||||||
|
"@langDownloadDialog": {
|
||||||
|
"description": "Shown as a title of a dialog while downloading new OCR data",
|
||||||
|
"placeholders": {
|
||||||
|
"lang": {
|
||||||
|
"type": "String",
|
||||||
|
"example": "ces"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"langDownloadProgress": "Download progress: $progress %",
|
||||||
|
"@langDownloadProgress": {
|
||||||
|
"description": "Progress percentage shown while downloading OCR data",
|
||||||
|
"placeholders": {
|
||||||
|
"progress":{
|
||||||
|
"type":"num",
|
||||||
|
"example":"99.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"addingFromOcr": "Add from OCR",
|
||||||
|
"license":"©️ 2023 Matyáš Caras\nReleased under the GNU AGPL license version 3"
|
||||||
|
}
|
|
@ -3,9 +3,11 @@ import 'dart:io';
|
||||||
import 'package:dynamic_color/dynamic_color.dart';
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:prasule/util/color_schemes.g.dart';
|
import 'package:prasule/util/color_schemes.g.dart';
|
||||||
import 'package:prasule/views/home.dart';
|
import 'package:prasule/views/home.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
|
@ -22,6 +24,12 @@ class MyApp extends StatelessWidget {
|
||||||
return (Platform.isAndroid)
|
return (Platform.isAndroid)
|
||||||
? DynamicColorBuilder(
|
? DynamicColorBuilder(
|
||||||
builder: (light, dark) => MaterialApp(
|
builder: (light, dark) => MaterialApp(
|
||||||
|
localizationsDelegates: const [
|
||||||
|
AppLocalizations.delegate,
|
||||||
|
...GlobalMaterialLocalizations.delegates,
|
||||||
|
...GlobalCupertinoLocalizations.delegates
|
||||||
|
],
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
title: 'Prašule',
|
title: 'Prašule',
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme: light ?? lightColorScheme,
|
colorScheme: light ?? lightColorScheme,
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
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';
|
||||||
import 'package:prasule/pw/platformfield.dart';
|
import 'package:prasule/pw/platformfield.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.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 +19,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 +30,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);
|
||||||
|
@ -43,7 +44,7 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("Create new entry"),
|
title: Text(AppLocalizations.of(context)!.createEntry),
|
||||||
),
|
),
|
||||||
body: SizedBox(
|
body: SizedBox(
|
||||||
width: MediaQuery.of(context).size.width,
|
width: MediaQuery.of(context).size.width,
|
||||||
|
@ -56,10 +57,10 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: MediaQuery.of(context).size.width * 0.8,
|
width: MediaQuery.of(context).size.width * 0.8,
|
||||||
child: PlatformField(
|
child: PlatformField(
|
||||||
labelText: "Name",
|
labelText: AppLocalizations.of(context)!.name,
|
||||||
controller: TextEditingController(text: newEntry.name),
|
controller: TextEditingController(text: newEntry.data.name),
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
newEntry.name = v;
|
newEntry.data.name = v;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -69,9 +70,9 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: MediaQuery.of(context).size.width * 0.8,
|
width: MediaQuery.of(context).size.width * 0.8,
|
||||||
child: PlatformField(
|
child: PlatformField(
|
||||||
labelText: "Amount",
|
labelText: AppLocalizations.of(context)!.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 +80,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);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -99,8 +100,8 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
||||||
value: EntryType.expense,
|
value: EntryType.expense,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: MediaQuery.of(context).size.width * 0.8 - 24,
|
width: MediaQuery.of(context).size.width * 0.8 - 24,
|
||||||
child: const Text(
|
child: Text(
|
||||||
"Expense",
|
AppLocalizations.of(context)!.expense,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -108,7 +109,7 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
||||||
value: EntryType.income,
|
value: EntryType.income,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: MediaQuery.of(context).size.width * 0.8 - 24,
|
width: MediaQuery.of(context).size.width * 0.8 - 24,
|
||||||
child: const Text("Income"),
|
child: Text(AppLocalizations.of(context)!.income),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -122,7 +123,7 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 20,
|
height: 20,
|
||||||
),
|
),
|
||||||
const Text("Category"),
|
Text(AppLocalizations.of(context)!.category),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 10,
|
height: 10,
|
||||||
),
|
),
|
||||||
|
@ -155,13 +156,14 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
||||||
height: 15,
|
height: 15,
|
||||||
),
|
),
|
||||||
PlatformButton(
|
PlatformButton(
|
||||||
text: "Save",
|
text: AppLocalizations.of(context)!.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(
|
SnackBar(
|
||||||
content: Text("Name cannot be empty"),
|
content: Text(
|
||||||
|
AppLocalizations.of(context)!.errorEmptyName),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -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';
|
||||||
|
@ -17,6 +18,7 @@ import 'package:prasule/views/create_entry.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';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
class HomeView extends StatefulWidget {
|
class HomeView extends StatefulWidget {
|
||||||
const HomeView({super.key});
|
const HomeView({super.key});
|
||||||
|
@ -62,7 +64,7 @@ class _HomeViewState extends State<HomeView> {
|
||||||
children: [
|
children: [
|
||||||
SpeedDialChild(
|
SpeedDialChild(
|
||||||
child: const Icon(Icons.edit),
|
child: const Icon(Icons.edit),
|
||||||
label: "Add new",
|
label: AppLocalizations.of(context)!.addNew,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
var sw = await Navigator.of(context).push<Wallet>(
|
var sw = await Navigator.of(context).push<Wallet>(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
@ -76,7 +78,7 @@ class _HomeViewState extends State<HomeView> {
|
||||||
}),
|
}),
|
||||||
SpeedDialChild(
|
SpeedDialChild(
|
||||||
child: const Icon(Icons.camera_alt),
|
child: const Icon(Icons.camera_alt),
|
||||||
label: "Add through camera",
|
label: AppLocalizations.of(context)!.addCamera,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final ImagePicker picker = ImagePicker();
|
final ImagePicker picker = ImagePicker();
|
||||||
final XFile? media =
|
final XFile? media =
|
||||||
|
@ -86,7 +88,7 @@ class _HomeViewState extends State<HomeView> {
|
||||||
),
|
),
|
||||||
SpeedDialChild(
|
SpeedDialChild(
|
||||||
child: const Icon(Icons.image),
|
child: const Icon(Icons.image),
|
||||||
label: "Add through saved image",
|
label: AppLocalizations.of(context)!.addGallery,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
startOcr(ImageSource.gallery);
|
startOcr(ImageSource.gallery);
|
||||||
},
|
},
|
||||||
|
@ -94,24 +96,24 @@ class _HomeViewState extends State<HomeView> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("Home"),
|
title: Text(AppLocalizations.of(context)!.home),
|
||||||
actions: [
|
actions: [
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
itemBuilder: (context) => ["Settings", "About"]
|
itemBuilder: (context) => [
|
||||||
.map((e) => PopupMenuItem(value: e, child: Text(e)))
|
AppLocalizations.of(context)!.settings,
|
||||||
.toList(),
|
AppLocalizations.of(context)!.about
|
||||||
|
].map((e) => PopupMenuItem(value: e, child: Text(e))).toList(),
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
if (value == "Settings") {
|
if (value == AppLocalizations.of(context)!.settings) {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const SettingsView(),
|
builder: (context) => const SettingsView(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (value == "About") {
|
} else if (value == AppLocalizations.of(context)!.about) {
|
||||||
showAboutDialog(
|
showAboutDialog(
|
||||||
context: context,
|
context: context,
|
||||||
applicationLegalese:
|
applicationLegalese: AppLocalizations.of(context)!.license,
|
||||||
"©️ 2023 Matyáš Caras\nReleased under the GNU AGPL license version 3",
|
|
||||||
applicationName: "Prašule");
|
applicationName: "Prašule");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -133,17 +135,17 @@ class _HomeViewState extends State<HomeView> {
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: (selectedWallet!.entries.isEmpty)
|
: (selectedWallet!.entries.isEmpty)
|
||||||
? const Column(
|
? Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"No entries :(",
|
AppLocalizations.of(context)!.noEntries,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"Add one using the floating action button.",
|
AppLocalizations.of(context)!.noEntriesSub,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -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!,
|
||||||
|
@ -196,9 +198,10 @@ class _HomeViewState extends State<HomeView> {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (cx) => PlatformDialog(
|
builder: (cx) => PlatformDialog(
|
||||||
title: "Are you sure",
|
title:
|
||||||
content: const Text(
|
AppLocalizations.of(context)!.sureDialog,
|
||||||
"Do you really want to delete this entry?"),
|
content: Text(
|
||||||
|
AppLocalizations.of(context)!.deleteSure),
|
||||||
actions: [
|
actions: [
|
||||||
PlatformButton(
|
PlatformButton(
|
||||||
text: "Yes",
|
text: "Yes",
|
||||||
|
@ -237,9 +240,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}"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -254,10 +257,9 @@ class _HomeViewState extends State<HomeView> {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content:
|
content: Text(AppLocalizations.of(context)!.missingOcr),
|
||||||
const Text("You do not have any OCR language data downloaded"),
|
|
||||||
action: SnackBarAction(
|
action: SnackBarAction(
|
||||||
label: "Download",
|
label: AppLocalizations.of(context)!.download,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
@ -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,44 @@ 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) => PlatformDialog(
|
||||||
|
title: AppLocalizations.of(context)!.ocrLoading),
|
||||||
|
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'\d+(?:\.|,)\d+');
|
||||||
|
var price = 0.0;
|
||||||
|
for (var match in regex.allMatches(line)) {
|
||||||
|
price += double.tryParse(match.group(0).toString()) ?? 0;
|
||||||
|
}
|
||||||
|
data.add(EntryData(name: "Idk", amount: price));
|
||||||
|
}
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
// TODO: send to create
|
||||||
},
|
},
|
||||||
child: const Text("Ok")),
|
child: const Text("Ok")),
|
||||||
TextButton(
|
TextButton(
|
||||||
|
@ -311,7 +337,7 @@ class _HomeViewState extends State<HomeView> {
|
||||||
},
|
},
|
||||||
child: const Text("Cancel")),
|
child: const Text("Cancel")),
|
||||||
],
|
],
|
||||||
title: "Select languages for OCR",
|
title: AppLocalizations.of(context)!.ocrSelect,
|
||||||
content: Column(
|
content: Column(
|
||||||
children: [
|
children: [
|
||||||
...List.generate(
|
...List.generate(
|
||||||
|
@ -328,7 +354,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 +367,7 @@ class _HomeViewState extends State<HomeView> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -352,6 +379,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);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:prasule/views/settings/tessdata_list.dart';
|
import 'package:prasule/views/settings/tessdata_list.dart';
|
||||||
import 'package:settings_ui/settings_ui.dart';
|
import 'package:settings_ui/settings_ui.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
class SettingsView extends StatefulWidget {
|
class SettingsView extends StatefulWidget {
|
||||||
const SettingsView({super.key});
|
const SettingsView({super.key});
|
||||||
|
@ -13,7 +14,7 @@ class _SettingsViewState extends State<SettingsView> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text("Settings")),
|
appBar: AppBar(title: Text(AppLocalizations.of(context)!.settings)),
|
||||||
body: SettingsList(
|
body: SettingsList(
|
||||||
applicationType: ApplicationType.both,
|
applicationType: ApplicationType.both,
|
||||||
darkTheme: SettingsThemeData(
|
darkTheme: SettingsThemeData(
|
||||||
|
@ -23,15 +24,15 @@ class _SettingsViewState extends State<SettingsView> {
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
tiles: [
|
tiles: [
|
||||||
SettingsTile.navigation(
|
SettingsTile.navigation(
|
||||||
title: const Text("View downloaded OCR data"),
|
title: Text(AppLocalizations.of(context)!.downloadedOcr),
|
||||||
description: const Text(
|
description:
|
||||||
"This data is used by the OCR to recognise text from pictures"),
|
Text(AppLocalizations.of(context)!.downloadedOcrDesc),
|
||||||
onPressed: (context) => Navigator.of(context).push(
|
onPressed: (context) => Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (c) => const TessdataListView())),
|
builder: (c) => const TessdataListView())),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
title: const Text("OCR"),
|
title: Text(AppLocalizations.of(context)!.ocr),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:prasule/main.dart';
|
||||||
import 'package:prasule/network/tessdata.dart';
|
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:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
class TessdataListView extends StatefulWidget {
|
class TessdataListView extends StatefulWidget {
|
||||||
const TessdataListView({super.key});
|
const TessdataListView({super.key});
|
||||||
|
@ -28,7 +29,7 @@ class _TessdataListViewState extends State<TessdataListView> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text("OCR data")),
|
appBar: AppBar(title: Text(AppLocalizations.of(context)!.ocrData)),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: MediaQuery.of(context).size.width * 0.9,
|
width: MediaQuery.of(context).size.width * 0.9,
|
||||||
|
@ -49,8 +50,8 @@ class _TessdataListViewState extends State<TessdataListView> {
|
||||||
title: Text(_tessdata[i].keys.first),
|
title: Text(_tessdata[i].keys.first),
|
||||||
trailing: TextButton(
|
trailing: TextButton(
|
||||||
child: Text(_tessdata[i][_tessdata[i].keys.first]!
|
child: Text(_tessdata[i][_tessdata[i].keys.first]!
|
||||||
? "Downloaded"
|
? AppLocalizations.of(context)!.downloaded
|
||||||
: "Download"),
|
: AppLocalizations.of(context)!.download),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
var lang = _tessdata[i].keys.first;
|
var lang = _tessdata[i].keys.first;
|
||||||
if (_tessdata[i][lang]!) {
|
if (_tessdata[i][lang]!) {
|
||||||
|
@ -58,12 +59,12 @@ class _TessdataListViewState extends State<TessdataListView> {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => PlatformDialog(
|
builder: (context) => PlatformDialog(
|
||||||
title: "Warning",
|
title: AppLocalizations.of(context)!.sureDialog,
|
||||||
content: Text(
|
content: Text(AppLocalizations.of(context)!
|
||||||
"Do you want to delete '$lang' OCR data?\nYou will not be able to utilize this language when using the OCR functions."),
|
.deleteOcr(lang)),
|
||||||
actions: [
|
actions: [
|
||||||
PlatformButton(
|
PlatformButton(
|
||||||
text: "Yes",
|
text: AppLocalizations.of(context)!.yes,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await TessdataApi.deleteData(lang);
|
await TessdataApi.deleteData(lang);
|
||||||
_tessdata[i][lang] = true;
|
_tessdata[i][lang] = true;
|
||||||
|
@ -71,7 +72,7 @@ class _TessdataListViewState extends State<TessdataListView> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
PlatformButton(
|
PlatformButton(
|
||||||
text: "No",
|
text: AppLocalizations.of(context)!.no,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
|
@ -90,7 +91,8 @@ class _TessdataListViewState extends State<TessdataListView> {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (c) => PlatformDialog(
|
builder: (c) => PlatformDialog(
|
||||||
title: "Downloading $lang, please wait...",
|
title: AppLocalizations.of(context)!
|
||||||
|
.langDownloadDialog(lang),
|
||||||
content: StreamBuilder(
|
content: StreamBuilder(
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState ==
|
if (snapshot.connectionState ==
|
||||||
|
@ -100,8 +102,8 @@ class _TessdataListViewState extends State<TessdataListView> {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
return const Text("Error");
|
return const Text("Error");
|
||||||
}
|
}
|
||||||
return Text(
|
return Text(AppLocalizations.of(context)!
|
||||||
"Download progress: ${snapshot.data} %");
|
.langDownloadProgress(snapshot.data!));
|
||||||
},
|
},
|
||||||
stream: progressStream.stream,
|
stream: progressStream.stream,
|
||||||
),
|
),
|
||||||
|
|
|
@ -9,6 +9,7 @@ import 'package:prasule/pw/platformbutton.dart';
|
||||||
import 'package:prasule/pw/platformdialog.dart';
|
import 'package:prasule/pw/platformdialog.dart';
|
||||||
import 'package:prasule/pw/platformfield.dart';
|
import 'package:prasule/pw/platformfield.dart';
|
||||||
import 'package:prasule/views/home.dart';
|
import 'package:prasule/views/home.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
class SetupView extends StatefulWidget {
|
class SetupView extends StatefulWidget {
|
||||||
const SetupView({super.key});
|
const SetupView({super.key});
|
||||||
|
@ -31,34 +32,42 @@ class _SetupViewState extends State<SetupView> {
|
||||||
"space_between_amount_and_symbol": false,
|
"space_between_amount_and_symbol": false,
|
||||||
"symbol_on_left": true,
|
"symbol_on_left": true,
|
||||||
});
|
});
|
||||||
var categories = <WalletCategory>[
|
var categories = <WalletCategory>[];
|
||||||
|
var name = "";
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
categories = [
|
||||||
WalletCategory(
|
WalletCategory(
|
||||||
name: "Health",
|
name: AppLocalizations.of(context)!.categoryHealth,
|
||||||
type: EntryType.expense,
|
type: EntryType.expense,
|
||||||
id: 1,
|
id: 1,
|
||||||
icon: IconData(Icons.medical_information.codePoint,
|
icon: IconData(Icons.medical_information.codePoint,
|
||||||
fontFamily: 'MaterialIcons'),
|
fontFamily: 'MaterialIcons'),
|
||||||
),
|
),
|
||||||
WalletCategory(
|
WalletCategory(
|
||||||
name: "Car",
|
name: AppLocalizations.of(context)!.categoryCar,
|
||||||
type: EntryType.expense,
|
type: EntryType.expense,
|
||||||
id: 2,
|
id: 2,
|
||||||
icon: IconData(Icons.car_repair.codePoint, fontFamily: 'MaterialIcons'),
|
icon: IconData(Icons.car_repair.codePoint, fontFamily: 'MaterialIcons'),
|
||||||
),
|
),
|
||||||
WalletCategory(
|
WalletCategory(
|
||||||
name: "Food",
|
name: AppLocalizations.of(context)!.categoryFood,
|
||||||
type: EntryType.expense,
|
type: EntryType.expense,
|
||||||
id: 3,
|
id: 3,
|
||||||
icon: IconData(Icons.restaurant.codePoint, fontFamily: 'MaterialIcons'),
|
icon: IconData(Icons.restaurant.codePoint, fontFamily: 'MaterialIcons'),
|
||||||
),
|
),
|
||||||
WalletCategory(
|
WalletCategory(
|
||||||
name: "Travel",
|
name: AppLocalizations.of(context)!.categoryTravel,
|
||||||
type: EntryType.expense,
|
type: EntryType.expense,
|
||||||
id: 4,
|
id: 4,
|
||||||
icon: IconData(Icons.train.codePoint, fontFamily: 'MaterialIcons'),
|
icon: IconData(Icons.train.codePoint, fontFamily: 'MaterialIcons'),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
var name = "";
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
@ -73,15 +82,16 @@ class _SetupViewState extends State<SetupView> {
|
||||||
showNextButton: true,
|
showNextButton: true,
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
showDoneButton: true,
|
showDoneButton: true,
|
||||||
next: const Text("Next"),
|
next: Text(AppLocalizations.of(context)!.next),
|
||||||
back: const Text("Back"),
|
back: Text(AppLocalizations.of(context)!.back),
|
||||||
done: const Text("Finish"),
|
done: Text(AppLocalizations.of(context)!.finish),
|
||||||
onDone: () {
|
onDone: () {
|
||||||
if (name.isEmpty) {
|
if (name.isEmpty) {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
.clearSnackBars(); // TODO: iOS replacement
|
.clearSnackBars(); // TODO: iOS replacement
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
const SnackBar(content: Text("Name cannot be empty")));
|
content:
|
||||||
|
Text(AppLocalizations.of(context)!.errorEmptyName)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var wallet = Wallet(
|
var wallet = Wallet(
|
||||||
|
@ -100,39 +110,43 @@ class _SetupViewState extends State<SetupView> {
|
||||||
PageViewModel(
|
PageViewModel(
|
||||||
decoration:
|
decoration:
|
||||||
const PageDecoration(bodyAlignment: Alignment.center),
|
const PageDecoration(bodyAlignment: Alignment.center),
|
||||||
titleWidget: const Padding(
|
titleWidget: Padding(
|
||||||
padding: EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Text(
|
child: Text(
|
||||||
"Welcome!",
|
AppLocalizations.of(context)!.welcome,
|
||||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
bodyWidget: const Column(
|
bodyWidget: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
"Prašule is an expense tracker tool designed for people, who don't want to spend too much time filling in all the little details.")),
|
AppLocalizations.of(context)!.welcomeAboutPrasule),
|
||||||
SizedBox(
|
),
|
||||||
|
const SizedBox(
|
||||||
height: 5,
|
height: 5,
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
"On this screen you will set up your 'wallet', in which you will track your expenses categorized under categories, which you can later set in the settings menu.")),
|
AppLocalizations.of(context)!.welcomeInstruction),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PageViewModel(
|
PageViewModel(
|
||||||
decoration:
|
decoration:
|
||||||
const PageDecoration(bodyAlignment: Alignment.center),
|
const PageDecoration(bodyAlignment: Alignment.center),
|
||||||
titleWidget: const Padding(
|
titleWidget: Padding(
|
||||||
padding: EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Text(
|
child: Text(
|
||||||
"Set your wallet's name and currency",
|
AppLocalizations.of(context)!.setupWalletNameCurrency,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
bodyWidget: Column(
|
bodyWidget: Column(
|
||||||
|
@ -141,7 +155,8 @@ class _SetupViewState extends State<SetupView> {
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: MediaQuery.of(context).size.width * 0.7,
|
width: MediaQuery.of(context).size.width * 0.7,
|
||||||
child: PlatformField(
|
child: PlatformField(
|
||||||
labelText: "Your awesome name here...",
|
labelText:
|
||||||
|
AppLocalizations.of(context)!.setupNamePlaceholder,
|
||||||
onChanged: (t) {
|
onChanged: (t) {
|
||||||
name = t;
|
name = t;
|
||||||
},
|
},
|
||||||
|
@ -151,7 +166,8 @@ class _SetupViewState extends State<SetupView> {
|
||||||
height: 5,
|
height: 5,
|
||||||
),
|
),
|
||||||
PlatformButton(
|
PlatformButton(
|
||||||
text: "Currency: ${_selectedCurrency.code}",
|
text: AppLocalizations.of(context)!
|
||||||
|
.setupCurrency(_selectedCurrency.code),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showCurrencyPicker(
|
showCurrencyPicker(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -168,19 +184,20 @@ class _SetupViewState extends State<SetupView> {
|
||||||
PageViewModel(
|
PageViewModel(
|
||||||
decoration:
|
decoration:
|
||||||
const PageDecoration(bodyAlignment: Alignment.center),
|
const PageDecoration(bodyAlignment: Alignment.center),
|
||||||
titleWidget: const Padding(
|
titleWidget: Padding(
|
||||||
padding: EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Text(
|
child: Text(
|
||||||
"Create categories",
|
AppLocalizations.of(context)!.setupCategoriesHeading,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
bodyWidget: Column(
|
bodyWidget: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
"Tap on the icon or name to edit it",
|
AppLocalizations.of(context)!.setupCategoriesEditHint,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
@ -232,16 +249,19 @@ class _SetupViewState extends State<SetupView> {
|
||||||
categories[i].name = controller.text;
|
categories[i].name = controller.text;
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: const Text("Ok"),
|
child: Text(
|
||||||
|
AppLocalizations.of(context)!.ok),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: const Text("Cancel"),
|
child: Text(
|
||||||
|
AppLocalizations.of(context)!.cancel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
title: "Editing name",
|
title: AppLocalizations.of(context)!
|
||||||
|
.setupCategoriesEditingName,
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: 400,
|
width: 400,
|
||||||
child:
|
child:
|
||||||
|
@ -270,7 +290,8 @@ class _SetupViewState extends State<SetupView> {
|
||||||
}
|
}
|
||||||
categories.add(
|
categories.add(
|
||||||
WalletCategory(
|
WalletCategory(
|
||||||
name: "Edit me",
|
name: AppLocalizations.of(context)!
|
||||||
|
.setupWalletNamePlaceholder,
|
||||||
type: EntryType.expense,
|
type: EntryType.expense,
|
||||||
id: id,
|
id: id,
|
||||||
icon: IconData(Icons.question_mark.codePoint,
|
icon: IconData(Icons.question_mark.codePoint,
|
||||||
|
|
|
@ -387,6 +387,11 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
version: "2.0.3"
|
||||||
|
flutter_localizations:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -534,10 +539,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_ios
|
name: image_picker_ios
|
||||||
sha256: c5538cacefacac733c724be7484377923b476216ad1ead35a0d2eadcdc0fc497
|
sha256: "76ec722aeea419d03aa915c2c96bf5b47214b053899088c9abb4086ceecf97a7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.8+2"
|
version: "0.8.8+4"
|
||||||
image_picker_linux:
|
image_picker_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
23
pubspec.yaml
23
pubspec.yaml
|
@ -1,21 +1,6 @@
|
||||||
name: prasule
|
name: prasule
|
||||||
description: A new Flutter project.
|
description: Open-source private expense tracker
|
||||||
# The following line prevents the package from being accidentally published to
|
|
||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|
||||||
|
|
||||||
# The following defines the version and build number for your application.
|
|
||||||
# A version number is three numbers separated by dots, like 1.2.43
|
|
||||||
# followed by an optional build number separated by a +.
|
|
||||||
# Both the version and the builder number may be overridden in flutter
|
|
||||||
# build by specifying --build-name and --build-number, respectively.
|
|
||||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
|
||||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
|
||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
|
||||||
# Read more about iOS versioning at
|
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
|
||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
@ -45,12 +30,14 @@ dependencies:
|
||||||
flutter_iconpicker: ^3.2.4
|
flutter_iconpicker: ^3.2.4
|
||||||
dynamic_color: ^1.6.6
|
dynamic_color: ^1.6.6
|
||||||
introduction_screen: ^3.1.11
|
introduction_screen: ^3.1.11
|
||||||
intl: ^0.18.1
|
intl: any
|
||||||
grouped_list: ^5.1.2
|
grouped_list: ^5.1.2
|
||||||
flutter_speed_dial: ^7.0.0
|
flutter_speed_dial: ^7.0.0
|
||||||
image_picker: ^1.0.1
|
image_picker: ^1.0.1
|
||||||
flutter_tesseract_ocr: ^0.4.23
|
flutter_tesseract_ocr: ^0.4.23
|
||||||
flutter_slidable: ^3.0.0
|
flutter_slidable: ^3.0.0
|
||||||
|
flutter_localizations:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -91,7 +78,7 @@ flutter_launcher_icons:
|
||||||
|
|
||||||
# The following section is specific to Flutter packages.
|
# The following section is specific to Flutter packages.
|
||||||
flutter:
|
flutter:
|
||||||
|
generate: true
|
||||||
# The following line ensures that the Material Icons font is
|
# The following line ensures that the Material Icons font is
|
||||||
# included with your application, so that you can use the icons in
|
# included with your application, so that you can use the icons in
|
||||||
# the material Icons class.
|
# the material Icons class.
|
||||||
|
|
Loading…
Reference in a new issue