Compare commits

..

No commits in common. "d1f37282f0a44cd9ebaee2a5cc6dd9a0f27374f9" and "de6c5fe31524511e5724979e8e50df5b60c2bedd" have entirely different histories.

19 changed files with 225 additions and 464 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) [![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) [![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)
Expense manager Expense manager

View file

@ -1,3 +0,0 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

View file

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

View file

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

View file

@ -1,17 +0,0 @@
// 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,
};

View file

@ -1,25 +0,0 @@
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);
}

View file

@ -1,32 +0,0 @@
// 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/walletentry.dart'; import 'package:prasule/api/entry.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<WalletSingleEntry> entries; final List<WalletEntry> entries;
double availableAmount; double availableAmount;
@JsonKey(fromJson: _currencyFromJson) @JsonKey(fromJson: _currencyFromJson)
final Currency currency; final Currency currency;

View file

@ -14,8 +14,7 @@ Wallet _$WalletFromJson(Map<String, dynamic> json) => Wallet(
.toList() ?? .toList() ??
const [], const [],
entries: (json['entries'] as List<dynamic>?) entries: (json['entries'] as List<dynamic>?)
?.map( ?.map((e) => WalletEntry.fromJson(e as Map<String, dynamic>))
(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,92 +0,0 @@
{
"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"
}

View file

@ -3,11 +3,9 @@ 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());
@ -24,12 +22,6 @@ 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,

View file

@ -1,17 +1,15 @@
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_data.dart'; import 'package:prasule/api/entry.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 WalletSingleEntry? editEntry; final WalletEntry? editEntry;
const CreateEntryView({super.key, required this.w, this.editEntry}); const CreateEntryView({super.key, required this.w, this.editEntry});
@override @override
@ -19,7 +17,7 @@ class CreateEntryView extends StatefulWidget {
} }
class _CreateEntryViewState extends State<CreateEntryView> { class _CreateEntryViewState extends State<CreateEntryView> {
late WalletSingleEntry newEntry; late WalletEntry newEntry;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -30,9 +28,10 @@ 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 = WalletSingleEntry( newEntry = WalletEntry(
id: id, id: id,
data: EntryData(amount: 0, name: ""), 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);
@ -44,7 +43,7 @@ class _CreateEntryViewState extends State<CreateEntryView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context)!.createEntry), title: const Text("Create new entry"),
), ),
body: SizedBox( body: SizedBox(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
@ -57,10 +56,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: AppLocalizations.of(context)!.name, labelText: "Name",
controller: TextEditingController(text: newEntry.data.name), controller: TextEditingController(text: newEntry.name),
onChanged: (v) { onChanged: (v) {
newEntry.data.name = v; newEntry.name = v;
}, },
), ),
), ),
@ -70,9 +69,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: AppLocalizations.of(context)!.amount, labelText: "Amount",
controller: TextEditingController( controller:
text: newEntry.data.amount.toString()), TextEditingController(text: newEntry.amount.toString()),
keyboardType: keyboardType:
const TextInputType.numberWithOptions(decimal: true), const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [ inputFormatters: [
@ -80,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.data.amount = double.parse(v); newEntry.amount = double.parse(v);
}, },
), ),
), ),
@ -100,8 +99,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: Text( child: const Text(
AppLocalizations.of(context)!.expense, "Expense",
), ),
), ),
), ),
@ -109,7 +108,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: Text(AppLocalizations.of(context)!.income), child: const Text("Income"),
), ),
), ),
], ],
@ -123,7 +122,7 @@ class _CreateEntryViewState extends State<CreateEntryView> {
const SizedBox( const SizedBox(
height: 20, height: 20,
), ),
Text(AppLocalizations.of(context)!.category), const Text("Category"),
const SizedBox( const SizedBox(
height: 10, height: 10,
), ),
@ -156,14 +155,13 @@ class _CreateEntryViewState extends State<CreateEntryView> {
height: 15, height: 15,
), ),
PlatformButton( PlatformButton(
text: AppLocalizations.of(context)!.save, text: "Save",
onPressed: () { onPressed: () {
if (newEntry.data.name.isEmpty) { if (newEntry.name.isEmpty) {
ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( const SnackBar(
content: Text( content: Text("Name cannot be empty"),
AppLocalizations.of(context)!.errorEmptyName),
), ),
); );
return; return;

View file

@ -6,8 +6,7 @@ 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_data.dart'; import 'package:prasule/api/entry.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';
@ -18,7 +17,6 @@ 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});
@ -64,7 +62,7 @@ class _HomeViewState extends State<HomeView> {
children: [ children: [
SpeedDialChild( SpeedDialChild(
child: const Icon(Icons.edit), child: const Icon(Icons.edit),
label: AppLocalizations.of(context)!.addNew, label: "Add new",
onTap: () async { onTap: () async {
var sw = await Navigator.of(context).push<Wallet>( var sw = await Navigator.of(context).push<Wallet>(
MaterialPageRoute( MaterialPageRoute(
@ -78,7 +76,7 @@ class _HomeViewState extends State<HomeView> {
}), }),
SpeedDialChild( SpeedDialChild(
child: const Icon(Icons.camera_alt), child: const Icon(Icons.camera_alt),
label: AppLocalizations.of(context)!.addCamera, label: "Add through camera",
onTap: () async { onTap: () async {
final ImagePicker picker = ImagePicker(); final ImagePicker picker = ImagePicker();
final XFile? media = final XFile? media =
@ -88,7 +86,7 @@ class _HomeViewState extends State<HomeView> {
), ),
SpeedDialChild( SpeedDialChild(
child: const Icon(Icons.image), child: const Icon(Icons.image),
label: AppLocalizations.of(context)!.addGallery, label: "Add through saved image",
onTap: () { onTap: () {
startOcr(ImageSource.gallery); startOcr(ImageSource.gallery);
}, },
@ -96,24 +94,24 @@ class _HomeViewState extends State<HomeView> {
], ],
), ),
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context)!.home), title: const Text("Home"),
actions: [ actions: [
PopupMenuButton( PopupMenuButton(
itemBuilder: (context) => [ itemBuilder: (context) => ["Settings", "About"]
AppLocalizations.of(context)!.settings, .map((e) => PopupMenuItem(value: e, child: Text(e)))
AppLocalizations.of(context)!.about .toList(),
].map((e) => PopupMenuItem(value: e, child: Text(e))).toList(),
onSelected: (value) { onSelected: (value) {
if (value == AppLocalizations.of(context)!.settings) { if (value == "Settings") {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const SettingsView(), builder: (context) => const SettingsView(),
), ),
); );
} else if (value == AppLocalizations.of(context)!.about) { } else if (value == "About") {
showAboutDialog( showAboutDialog(
context: context, context: context,
applicationLegalese: AppLocalizations.of(context)!.license, applicationLegalese:
"©️ 2023 Matyáš Caras\nReleased under the GNU AGPL license version 3",
applicationName: "Prašule"); applicationName: "Prašule");
} }
}, },
@ -135,17 +133,17 @@ class _HomeViewState extends State<HomeView> {
], ],
) )
: (selectedWallet!.entries.isEmpty) : (selectedWallet!.entries.isEmpty)
? Column( ? const Column(
children: [ children: [
Text( Text(
AppLocalizations.of(context)!.noEntries, "No entries :(",
style: const TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
Text( Text(
AppLocalizations.of(context)!.noEntriesSub, "Add one using the floating action button.",
) )
], ],
) )
@ -164,7 +162,7 @@ class _HomeViewState extends State<HomeView> {
SlidableAction( SlidableAction(
onPressed: (c) { onPressed: (c) {
Navigator.of(context) Navigator.of(context)
.push<WalletSingleEntry>( .push<WalletEntry>(
MaterialPageRoute( MaterialPageRoute(
builder: (c) => CreateEntryView( builder: (c) => CreateEntryView(
w: selectedWallet!, w: selectedWallet!,
@ -198,10 +196,9 @@ class _HomeViewState extends State<HomeView> {
showDialog( showDialog(
context: context, context: context,
builder: (cx) => PlatformDialog( builder: (cx) => PlatformDialog(
title: title: "Are you sure",
AppLocalizations.of(context)!.sureDialog, content: const Text(
content: Text( "Do you really want to delete this entry?"),
AppLocalizations.of(context)!.deleteSure),
actions: [ actions: [
PlatformButton( PlatformButton(
text: "Yes", text: "Yes",
@ -240,9 +237,9 @@ class _HomeViewState extends State<HomeView> {
), ),
), ),
), ),
title: Text(element.data.name), title: Text(element.name),
subtitle: Text( subtitle: Text(
"${element.data.amount} ${selectedWallet!.currency.symbol}"), "${element.amount} ${selectedWallet!.currency.symbol}"),
), ),
), ),
), ),
@ -257,9 +254,10 @@ class _HomeViewState extends State<HomeView> {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(AppLocalizations.of(context)!.missingOcr), content:
const Text("You do not have any OCR language data downloaded"),
action: SnackBarAction( action: SnackBarAction(
label: AppLocalizations.of(context)!.download, label: "Download",
onPressed: () { onPressed: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
@ -274,98 +272,73 @@ 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);
selectedLanguages[0] = true; if (selectedLanguages.length == 1) {
selectedLanguages[0] = true;
}
showDialog( showDialog(
context: context, context: context,
builder: (c) => StatefulBuilder( builder: (c) => PlatformDialog(
builder: (ctx, setState) => PlatformDialog( actions: [
actions: [ TextButton(
TextButton( onPressed: () async {
onPressed: () async { final ImagePicker picker = ImagePicker();
final ImagePicker picker = ImagePicker(); final XFile? media = await picker.pickImage(source: imgSrc);
final XFile? media = await picker.pickImage(source: imgSrc); if (media == null) {
if (media == null) { if (mounted) Navigator.of(context).pop();
if (mounted) Navigator.of(context).pop(); return;
return; }
} // 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); var string = await FlutterTesseractOcr.extractText(media.path,
if (!mounted) return; language: selected,
showDialog( args: {
context: context, //"psm": "4",
builder: (c) => PlatformDialog( "preserve_interword_spaces": "1",
title: AppLocalizations.of(context)!.ocrLoading),
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);
var data = <EntryData>[]; if (mounted) Navigator.of(context).pop();
for (var line in lines) { return;
var regex = RegExp(r'\d+(?:\.|,)\d+'); },
var price = 0.0; child: const Text("Ok")),
for (var match in regex.allMatches(line)) { TextButton(
price += double.tryParse(match.group(0).toString()) ?? 0; onPressed: () {
} Navigator.of(c).pop();
data.add(EntryData(name: "Idk", amount: price)); },
} child: const Text("Cancel")),
Navigator.of(context).pop(); ],
// TODO: send to create title: "Select languages for OCR",
}, content: Column(
child: const Text("Ok")), children: [
TextButton( ...List.generate(
onPressed: () { availableLanguages.length,
Navigator.of(c).pop(); (index) => Row(
}, children: [
child: const Text("Cancel")), 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)
],
),
)
], ],
title: AppLocalizations.of(context)!.ocrSelect,
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)
],
),
)
],
),
), ),
), ),
); );
@ -379,7 +352,6 @@ 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

@ -1,7 +1,6 @@
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});
@ -14,7 +13,7 @@ class _SettingsViewState extends State<SettingsView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context)!.settings)), appBar: AppBar(title: const Text("Settings")),
body: SettingsList( body: SettingsList(
applicationType: ApplicationType.both, applicationType: ApplicationType.both,
darkTheme: SettingsThemeData( darkTheme: SettingsThemeData(
@ -24,15 +23,15 @@ class _SettingsViewState extends State<SettingsView> {
SettingsSection( SettingsSection(
tiles: [ tiles: [
SettingsTile.navigation( SettingsTile.navigation(
title: Text(AppLocalizations.of(context)!.downloadedOcr), title: const Text("View downloaded OCR data"),
description: description: const Text(
Text(AppLocalizations.of(context)!.downloadedOcrDesc), "This data is used by the OCR to recognise text from pictures"),
onPressed: (context) => Navigator.of(context).push( onPressed: (context) => Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (c) => const TessdataListView())), builder: (c) => const TessdataListView())),
) )
], ],
title: Text(AppLocalizations.of(context)!.ocr), title: const Text("OCR"),
), ),
], ],
), ),

View file

@ -7,7 +7,6 @@ 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});
@ -29,7 +28,7 @@ class _TessdataListViewState extends State<TessdataListView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context)!.ocrData)), appBar: AppBar(title: const Text("OCR data")),
body: Center( body: Center(
child: SizedBox( child: SizedBox(
width: MediaQuery.of(context).size.width * 0.9, width: MediaQuery.of(context).size.width * 0.9,
@ -50,8 +49,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]!
? AppLocalizations.of(context)!.downloaded ? "Downloaded"
: AppLocalizations.of(context)!.download), : "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]!) {
@ -59,12 +58,12 @@ class _TessdataListViewState extends State<TessdataListView> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => PlatformDialog( builder: (context) => PlatformDialog(
title: AppLocalizations.of(context)!.sureDialog, title: "Warning",
content: Text(AppLocalizations.of(context)! content: Text(
.deleteOcr(lang)), "Do you want to delete '$lang' OCR data?\nYou will not be able to utilize this language when using the OCR functions."),
actions: [ actions: [
PlatformButton( PlatformButton(
text: AppLocalizations.of(context)!.yes, text: "Yes",
onPressed: () async { onPressed: () async {
await TessdataApi.deleteData(lang); await TessdataApi.deleteData(lang);
_tessdata[i][lang] = true; _tessdata[i][lang] = true;
@ -72,7 +71,7 @@ class _TessdataListViewState extends State<TessdataListView> {
}, },
), ),
PlatformButton( PlatformButton(
text: AppLocalizations.of(context)!.no, text: "No",
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
@ -91,8 +90,7 @@ class _TessdataListViewState extends State<TessdataListView> {
showDialog( showDialog(
context: context, context: context,
builder: (c) => PlatformDialog( builder: (c) => PlatformDialog(
title: AppLocalizations.of(context)! title: "Downloading $lang, please wait...",
.langDownloadDialog(lang),
content: StreamBuilder( content: StreamBuilder(
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == if (snapshot.connectionState ==
@ -102,8 +100,8 @@ class _TessdataListViewState extends State<TessdataListView> {
if (snapshot.hasError) { if (snapshot.hasError) {
return const Text("Error"); return const Text("Error");
} }
return Text(AppLocalizations.of(context)! return Text(
.langDownloadProgress(snapshot.data!)); "Download progress: ${snapshot.data} %");
}, },
stream: progressStream.stream, stream: progressStream.stream,
), ),

View file

@ -9,7 +9,6 @@ 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});
@ -32,42 +31,34 @@ 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>[
WalletCategory(
name: "Health",
type: EntryType.expense,
id: 1,
icon: IconData(Icons.medical_information.codePoint,
fontFamily: 'MaterialIcons'),
),
WalletCategory(
name: "Car",
type: EntryType.expense,
id: 2,
icon: IconData(Icons.car_repair.codePoint, fontFamily: 'MaterialIcons'),
),
WalletCategory(
name: "Food",
type: EntryType.expense,
id: 3,
icon: IconData(Icons.restaurant.codePoint, fontFamily: 'MaterialIcons'),
),
WalletCategory(
name: "Travel",
type: EntryType.expense,
id: 4,
icon: IconData(Icons.train.codePoint, fontFamily: 'MaterialIcons'),
),
];
var name = ""; var name = "";
@override
void initState() {
super.initState();
categories = [
WalletCategory(
name: AppLocalizations.of(context)!.categoryHealth,
type: EntryType.expense,
id: 1,
icon: IconData(Icons.medical_information.codePoint,
fontFamily: 'MaterialIcons'),
),
WalletCategory(
name: AppLocalizations.of(context)!.categoryCar,
type: EntryType.expense,
id: 2,
icon: IconData(Icons.car_repair.codePoint, fontFamily: 'MaterialIcons'),
),
WalletCategory(
name: AppLocalizations.of(context)!.categoryFood,
type: EntryType.expense,
id: 3,
icon: IconData(Icons.restaurant.codePoint, fontFamily: 'MaterialIcons'),
),
WalletCategory(
name: AppLocalizations.of(context)!.categoryTravel,
type: EntryType.expense,
id: 4,
icon: IconData(Icons.train.codePoint, fontFamily: 'MaterialIcons'),
),
];
setState(() {});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -82,16 +73,15 @@ class _SetupViewState extends State<SetupView> {
showNextButton: true, showNextButton: true,
showBackButton: true, showBackButton: true,
showDoneButton: true, showDoneButton: true,
next: Text(AppLocalizations.of(context)!.next), next: const Text("Next"),
back: Text(AppLocalizations.of(context)!.back), back: const Text("Back"),
done: Text(AppLocalizations.of(context)!.finish), done: const Text("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(SnackBar( ScaffoldMessenger.of(context).showSnackBar(
content: const SnackBar(content: Text("Name cannot be empty")));
Text(AppLocalizations.of(context)!.errorEmptyName)));
return; return;
} }
var wallet = Wallet( var wallet = Wallet(
@ -110,43 +100,39 @@ class _SetupViewState extends State<SetupView> {
PageViewModel( PageViewModel(
decoration: decoration:
const PageDecoration(bodyAlignment: Alignment.center), const PageDecoration(bodyAlignment: Alignment.center),
titleWidget: Padding( titleWidget: const Padding(
padding: const EdgeInsets.all(8), padding: EdgeInsets.all(8),
child: Text( child: Text(
AppLocalizations.of(context)!.welcome, "Welcome!",
style: const TextStyle( style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
fontSize: 24, fontWeight: FontWeight.bold),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
bodyWidget: Column( bodyWidget: const Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Flexible( Flexible(
child: Text( child: Text(
AppLocalizations.of(context)!.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.")),
), SizedBox(
const SizedBox(
height: 5, height: 5,
), ),
Flexible( Flexible(
child: Text( child: Text(
AppLocalizations.of(context)!.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.")),
),
], ],
), ),
), ),
PageViewModel( PageViewModel(
decoration: decoration:
const PageDecoration(bodyAlignment: Alignment.center), const PageDecoration(bodyAlignment: Alignment.center),
titleWidget: Padding( titleWidget: const Padding(
padding: const EdgeInsets.all(8), padding: EdgeInsets.all(8),
child: Text( child: Text(
AppLocalizations.of(context)!.setupWalletNameCurrency, "Set your wallet's name and currency",
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
fontSize: 24, fontWeight: FontWeight.bold),
), ),
), ),
bodyWidget: Column( bodyWidget: Column(
@ -155,8 +141,7 @@ 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: labelText: "Your awesome name here...",
AppLocalizations.of(context)!.setupNamePlaceholder,
onChanged: (t) { onChanged: (t) {
name = t; name = t;
}, },
@ -166,8 +151,7 @@ class _SetupViewState extends State<SetupView> {
height: 5, height: 5,
), ),
PlatformButton( PlatformButton(
text: AppLocalizations.of(context)! text: "Currency: ${_selectedCurrency.code}",
.setupCurrency(_selectedCurrency.code),
onPressed: () { onPressed: () {
showCurrencyPicker( showCurrencyPicker(
context: context, context: context,
@ -184,20 +168,19 @@ class _SetupViewState extends State<SetupView> {
PageViewModel( PageViewModel(
decoration: decoration:
const PageDecoration(bodyAlignment: Alignment.center), const PageDecoration(bodyAlignment: Alignment.center),
titleWidget: Padding( titleWidget: const Padding(
padding: const EdgeInsets.all(8), padding: EdgeInsets.all(8),
child: Text( child: Text(
AppLocalizations.of(context)!.setupCategoriesHeading, "Create categories",
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
fontSize: 24, fontWeight: FontWeight.bold),
), ),
), ),
bodyWidget: Column( bodyWidget: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( const Text(
AppLocalizations.of(context)!.setupCategoriesEditHint, "Tap on the icon or name to edit it",
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
SizedBox( SizedBox(
@ -249,19 +232,16 @@ class _SetupViewState extends State<SetupView> {
categories[i].name = controller.text; categories[i].name = controller.text;
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text( child: const Text("Ok"),
AppLocalizations.of(context)!.ok),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text( child: const Text("Cancel"),
AppLocalizations.of(context)!.cancel),
), ),
], ],
title: AppLocalizations.of(context)! title: "Editing name",
.setupCategoriesEditingName,
content: SizedBox( content: SizedBox(
width: 400, width: 400,
child: child:
@ -290,8 +270,7 @@ class _SetupViewState extends State<SetupView> {
} }
categories.add( categories.add(
WalletCategory( WalletCategory(
name: AppLocalizations.of(context)! name: "Edit me",
.setupWalletNamePlaceholder,
type: EntryType.expense, type: EntryType.expense,
id: id, id: id,
icon: IconData(Icons.question_mark.codePoint, icon: IconData(Icons.question_mark.codePoint,

View file

@ -387,11 +387,6 @@ 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:
@ -539,10 +534,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_ios name: image_picker_ios
sha256: "76ec722aeea419d03aa915c2c96bf5b47214b053899088c9abb4086ceecf97a7" sha256: c5538cacefacac733c724be7484377923b476216ad1ead35a0d2eadcdc0fc497
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.8+4" version: "0.8.8+2"
image_picker_linux: image_picker_linux:
dependency: transitive dependency: transitive
description: description:

View file

@ -1,6 +1,21 @@
name: prasule name: prasule
description: Open-source private expense tracker description: A new Flutter project.
# 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:
@ -30,14 +45,12 @@ 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: any intl: ^0.18.1
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:
@ -78,7 +91,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.