Add i18n via slang
This commit is contained in:
parent
99ab2f006d
commit
e0fba11f25
3
.gitignore
vendored
3
.gitignore
vendored
@ -43,3 +43,6 @@ app.*.map.json
|
|||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
lib/i18n/*
|
137
assets/i18n/en.i18n.json
Normal file
137
assets/i18n/en.i18n.json
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"beneficiary": {
|
||||||
|
"addBeneficiary": {
|
||||||
|
"title": "Add beneficiary",
|
||||||
|
"body": "The beneficiary '$name' does not exist. Do you want to add it?"
|
||||||
|
},
|
||||||
|
"nameWithAccount": "$name (Account)"
|
||||||
|
},
|
||||||
|
"transaction": {
|
||||||
|
"directionSend": "Send",
|
||||||
|
"directionReceive": "Receive",
|
||||||
|
"beneficiaryTextfieldHintSend": "Payee",
|
||||||
|
"beneficiaryTextfieldHintReceive": "Payer"
|
||||||
|
},
|
||||||
|
"amount": "Amount",
|
||||||
|
"date": "Date",
|
||||||
|
"expenseCategory": {
|
||||||
|
"name": "Expense category",
|
||||||
|
"none": "None"
|
||||||
|
},
|
||||||
|
"templateName": "Template name",
|
||||||
|
"period": {
|
||||||
|
"days": "Days",
|
||||||
|
"weeks": "Weeks",
|
||||||
|
"months": "Months",
|
||||||
|
"years": "Years",
|
||||||
|
"daysNumber": "$number days",
|
||||||
|
"weeksNumber": "$number weeks",
|
||||||
|
"monthsNumber": "$number months",
|
||||||
|
"yearsNumber": "$number years"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"accounts": {
|
||||||
|
"title": "Accounts",
|
||||||
|
"accountSelector": {
|
||||||
|
"none": "None"
|
||||||
|
},
|
||||||
|
"addAccount": {
|
||||||
|
"accountName": "Acount name"
|
||||||
|
},
|
||||||
|
"expenseBreakdown": {
|
||||||
|
"title": "Expense Breakdown",
|
||||||
|
"noActiveAccount": "No account active",
|
||||||
|
"noExpensesAvailable": "No expenses available",
|
||||||
|
"availableFunds": "Available funds: $amount"
|
||||||
|
},
|
||||||
|
"totalBalance": {
|
||||||
|
"title": "Total Balance",
|
||||||
|
"loading": "..."
|
||||||
|
},
|
||||||
|
"upcomingTransactions": {
|
||||||
|
"title": "Upcoming Transactions",
|
||||||
|
"noUpcomingTransactions": "No upcoming transactions",
|
||||||
|
"items": {
|
||||||
|
"title": "$name ($amount)",
|
||||||
|
"dueIn": "Due in $number days"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deleteAccount": {
|
||||||
|
"title": "Delete Account",
|
||||||
|
"content": "Are you sure you want to delete the account '$name'? This will delete all related data as well!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"budgets": {
|
||||||
|
"addBudget": {
|
||||||
|
"budgetNameHint": "Budget name",
|
||||||
|
"income": "Income",
|
||||||
|
"includeOtherSpendings": "Include other spendings"
|
||||||
|
},
|
||||||
|
"addBudgetItem": {
|
||||||
|
"amountHint": "Amount"
|
||||||
|
},
|
||||||
|
"noBudgets": "No budgets",
|
||||||
|
"details": {
|
||||||
|
"noBudgetSelected": "No budget selected",
|
||||||
|
"noBudgetItems": "No budget items",
|
||||||
|
"budgetItems": "Budget items",
|
||||||
|
"items": {
|
||||||
|
"title": "$name ($amount)",
|
||||||
|
"loading": "...",
|
||||||
|
"over": "$amount over",
|
||||||
|
"remaining": "$amount left"
|
||||||
|
},
|
||||||
|
"daysLeft": "Days left",
|
||||||
|
"budgetLeft": "Budget left",
|
||||||
|
"totalBudget": "Budget total",
|
||||||
|
"budgetBreakdown": {
|
||||||
|
"title": "Budget breakdown",
|
||||||
|
"noSpendingAvailable": "No spending available"
|
||||||
|
},
|
||||||
|
"spendingBreakdown": {
|
||||||
|
"title": "Spending Breakdown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"transactions": {
|
||||||
|
"balance": "Account Balance",
|
||||||
|
"details": {
|
||||||
|
"noTransactionSelected": "No transaction selected"
|
||||||
|
},
|
||||||
|
"addTransaction": {
|
||||||
|
"useTemplate": "Use template"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"templates": {
|
||||||
|
"removeTemplate": {
|
||||||
|
"title": "Remove Template",
|
||||||
|
"body": "Are you sure you want to remove the template '$name'?"
|
||||||
|
},
|
||||||
|
"addTemplate": {
|
||||||
|
"isRecurring": "Is recurring"
|
||||||
|
},
|
||||||
|
"nonRecurring": {
|
||||||
|
"title": "Non-recurring"
|
||||||
|
},
|
||||||
|
"recurring": {
|
||||||
|
"title": "Recurring"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"colorSchemes": {
|
||||||
|
"title": "Color Scheme",
|
||||||
|
"dark": "Dark",
|
||||||
|
"light": "Light",
|
||||||
|
"system": "System"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modals": {
|
||||||
|
"add": "Add",
|
||||||
|
"delete": "Delete",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"save": "Save"
|
||||||
|
}
|
||||||
|
}
|
9
build.yaml
Normal file
9
build.yaml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
targets:
|
||||||
|
$default:
|
||||||
|
builders:
|
||||||
|
slang_build_runner:
|
||||||
|
options:
|
||||||
|
input_directory: assets/i18n
|
||||||
|
output_directory: lib/i18n
|
||||||
|
fallback_strategy: base_locale
|
||||||
|
base_locale: en
|
@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:okane/database/database.dart';
|
import 'package:okane/database/database.dart';
|
||||||
|
import 'package:okane/i18n/strings.g.dart';
|
||||||
import 'package:okane/screen.dart';
|
import 'package:okane/screen.dart';
|
||||||
import 'package:okane/ui/navigation.dart';
|
import 'package:okane/ui/navigation.dart';
|
||||||
import 'package:okane/ui/pages/budgets/budget_details.dart';
|
import 'package:okane/ui/pages/budgets/budget_details.dart';
|
||||||
@ -15,6 +16,7 @@ import 'package:okane/ui/state/settings.dart';
|
|||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
LocaleSettings.useDeviceLocale();
|
||||||
|
|
||||||
final settings = SettingsCubit();
|
final settings = SettingsCubit();
|
||||||
await settings.loadSettings();
|
await settings.loadSettings();
|
||||||
|
@ -10,6 +10,7 @@ import 'package:okane/ui/pages/account/total_balance_card.dart';
|
|||||||
import 'package:okane/ui/pages/account/upcoming_transactions_card.dart';
|
import 'package:okane/ui/pages/account/upcoming_transactions_card.dart';
|
||||||
import 'package:okane/ui/state/core.dart';
|
import 'package:okane/ui/state/core.dart';
|
||||||
import 'package:okane/ui/utils.dart';
|
import 'package:okane/ui/utils.dart';
|
||||||
|
import 'package:okane/i18n/strings.g.dart';
|
||||||
|
|
||||||
class AccountListPage extends StatefulWidget {
|
class AccountListPage extends StatefulWidget {
|
||||||
final bool isPage;
|
final bool isPage;
|
||||||
@ -33,7 +34,7 @@ class AccountListPageState extends State<AccountListPage> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
"Accounts",
|
t.pages.accounts.title,
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -104,7 +105,10 @@ class AccountListPageState extends State<AccountListPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Text(bloc.activeAccount?.name ?? "None"),
|
child: Text(
|
||||||
|
bloc.activeAccount?.name ??
|
||||||
|
t.pages.accounts.accountSelector.none,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -207,7 +211,8 @@ class AccountListPageState extends State<AccountListPage> {
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _accountNameController,
|
controller: _accountNameController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: "Account name",
|
hintText:
|
||||||
|
t.pages.accounts.addAccount.accountName,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -229,7 +234,7 @@ class AccountListPageState extends State<AccountListPage> {
|
|||||||
_accountNameController.text = "";
|
_accountNameController.text = "";
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: Text("Add"),
|
child: Text(t.modals.add),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -3,6 +3,7 @@ import 'package:fl_chart/fl_chart.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:okane/database/database.dart';
|
import 'package:okane/database/database.dart';
|
||||||
|
import 'package:okane/i18n/strings.g.dart';
|
||||||
import 'package:okane/ui/state/core.dart';
|
import 'package:okane/ui/state/core.dart';
|
||||||
import 'package:okane/ui/utils.dart';
|
import 'package:okane/ui/utils.dart';
|
||||||
|
|
||||||
@ -67,7 +68,7 @@ class AccountBalanceGraphCard extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text("Account balance"),
|
Text(t.pages.transactions.balance),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 150,
|
height: 150,
|
||||||
child: FutureBuilder(
|
child: FutureBuilder(
|
||||||
|
@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:okane/database/collections/transaction.dart';
|
import 'package:okane/database/collections/transaction.dart';
|
||||||
import 'package:okane/database/database.dart';
|
import 'package:okane/database/database.dart';
|
||||||
|
import 'package:okane/i18n/strings.g.dart';
|
||||||
import 'package:okane/ui/state/core.dart';
|
import 'package:okane/ui/state/core.dart';
|
||||||
import 'package:okane/ui/utils.dart';
|
import 'package:okane/ui/utils.dart';
|
||||||
import 'package:okane/ui/widgets/piechart.dart';
|
import 'package:okane/ui/widgets/piechart.dart';
|
||||||
@ -75,7 +76,7 @@ class BreakdownCard extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildCard(Widget child, String? subtitle) {
|
Widget _buildCard(Widget child, String? subtitle) {
|
||||||
return ResponsiveCard(
|
return ResponsiveCard(
|
||||||
titleText: "Expense Breakdown",
|
titleText: t.pages.accounts.expenseBreakdown.title,
|
||||||
subtitleText: subtitle,
|
subtitleText: subtitle,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
@ -91,7 +92,9 @@ class BreakdownCard extends StatelessWidget {
|
|||||||
return BlocBuilder<CoreCubit, CoreState>(
|
return BlocBuilder<CoreCubit, CoreState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (bloc.activeAccount == null) {
|
if (bloc.activeAccount == null) {
|
||||||
return _buildCenterText("No account active");
|
return _buildCenterText(
|
||||||
|
t.pages.accounts.expenseBreakdown.noActiveAccount,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return FutureBuilder(
|
return FutureBuilder(
|
||||||
@ -125,7 +128,9 @@ class BreakdownCard extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
if (sectionData.isEmpty) {
|
if (sectionData.isEmpty) {
|
||||||
return _buildCenterText("No expenses available");
|
return _buildCenterText(
|
||||||
|
t.pages.accounts.expenseBreakdown.noExpensesAvailable,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return _buildCard(
|
return _buildCard(
|
||||||
OkanePieChart(
|
OkanePieChart(
|
||||||
@ -141,7 +146,9 @@ class BreakdownCard extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
"Available money: ${formatCurrency(data.usable)}",
|
t.pages.accounts.expenseBreakdown.availableFunds(
|
||||||
|
amount: formatCurrency(data.usable),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:okane/database/collections/account.dart';
|
import 'package:okane/database/collections/account.dart';
|
||||||
|
import 'package:okane/i18n/strings.g.dart';
|
||||||
import 'package:okane/ui/state/core.dart';
|
import 'package:okane/ui/state/core.dart';
|
||||||
|
|
||||||
class DeleteAccountPopup extends StatelessWidget {
|
class DeleteAccountPopup extends StatelessWidget {
|
||||||
@ -22,7 +23,7 @@ class DeleteAccountPopup extends StatelessWidget {
|
|||||||
return BlocBuilder<CoreCubit, CoreState>(
|
return BlocBuilder<CoreCubit, CoreState>(
|
||||||
builder:
|
builder:
|
||||||
(context, state) => AlertDialog(
|
(context, state) => AlertDialog(
|
||||||
title: Text("Delete Account"),
|
title: Text(t.pages.accounts.deleteAccount.title),
|
||||||
content:
|
content:
|
||||||
state.isDeletingAccount
|
state.isDeletingAccount
|
||||||
? Row(
|
? Row(
|
||||||
@ -37,7 +38,9 @@ class DeleteAccountPopup extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
"Are you sure you want to delete the account '${account.name!}'? This will delete all transactions as well.",
|
t.pages.accounts.deleteAccount.content(
|
||||||
|
name: account.name!,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
@ -48,7 +51,10 @@ class DeleteAccountPopup extends StatelessWidget {
|
|||||||
await GetIt.I.get<CoreCubit>().deleteAccount(account);
|
await GetIt.I.get<CoreCubit>().deleteAccount(account);
|
||||||
afterDelete();
|
afterDelete();
|
||||||
},
|
},
|
||||||
child: Text("Delete", style: TextStyle(color: Colors.red)),
|
child: Text(
|
||||||
|
t.modals.delete,
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed:
|
onPressed:
|
||||||
@ -57,7 +63,7 @@ class DeleteAccountPopup extends StatelessWidget {
|
|||||||
: () {
|
: () {
|
||||||
onCancel();
|
onCancel();
|
||||||
},
|
},
|
||||||
child: Text("Cancel"),
|
child: Text(t.modals.cancel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:okane/database/collections/account.dart';
|
import 'package:okane/database/collections/account.dart';
|
||||||
import 'package:okane/database/database.dart';
|
import 'package:okane/database/database.dart';
|
||||||
|
import 'package:okane/i18n/strings.g.dart';
|
||||||
import 'package:okane/ui/state/core.dart';
|
import 'package:okane/ui/state/core.dart';
|
||||||
import 'package:okane/ui/utils.dart';
|
import 'package:okane/ui/utils.dart';
|
||||||
import 'package:okane/ui/widgets/piechart_card.dart';
|
import 'package:okane/ui/widgets/piechart_card.dart';
|
||||||
@ -24,14 +25,16 @@ class TotalBalanceCard extends StatelessWidget {
|
|||||||
return BlocBuilder<CoreCubit, CoreState>(
|
return BlocBuilder<CoreCubit, CoreState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return ResponsiveCard(
|
return ResponsiveCard(
|
||||||
titleText: "Total Balance",
|
titleText: t.pages.accounts.totalBalance.title,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(16),
|
padding: EdgeInsets.all(16),
|
||||||
child: FutureBuilder(
|
child: FutureBuilder(
|
||||||
future: _getTotalBalance(state.accounts),
|
future: _getTotalBalance(state.accounts),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return Text(
|
return Text(
|
||||||
snapshot.hasData ? formatCurrency(snapshot.data!) : "...",
|
snapshot.hasData
|
||||||
|
? formatCurrency(snapshot.data!)
|
||||||
|
: t.pages.accounts.totalBalance.loading,
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:okane/database/collections/recurrent.dart';
|
import 'package:okane/database/collections/recurrent.dart';
|
||||||
import 'package:okane/database/database.dart';
|
import 'package:okane/database/database.dart';
|
||||||
|
import 'package:okane/i18n/strings.g.dart';
|
||||||
import 'package:okane/ui/state/core.dart';
|
import 'package:okane/ui/state/core.dart';
|
||||||
import 'package:okane/ui/utils.dart';
|
import 'package:okane/ui/utils.dart';
|
||||||
import 'package:okane/ui/widgets/add_transaction.dart';
|
import 'package:okane/ui/widgets/add_transaction.dart';
|
||||||
@ -28,25 +29,40 @@ class UpcomingTransactionsCard extends StatelessWidget {
|
|||||||
upcoming.isEmpty
|
upcoming.isEmpty
|
||||||
? [
|
? [
|
||||||
Text(
|
Text(
|
||||||
"No upcoming transactions",
|
t
|
||||||
|
.pages
|
||||||
|
.accounts
|
||||||
|
.upcomingTransactions
|
||||||
|
.noUpcomingTransactions,
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
: upcoming
|
: upcoming
|
||||||
.map(
|
.map(
|
||||||
(t) => ListTile(
|
(transaction) => ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
"${t.template.value!.name} (${t.template.value!.amount}€)",
|
t.pages.accounts.upcomingTransactions.items.title(
|
||||||
|
name: transaction.template.value!.name,
|
||||||
|
amount:
|
||||||
|
"${formatCurrency(transaction.template.value!.amount)}€",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
"Due in ${today.difference(t.lastExecution ?? today).inDays} days",
|
t.pages.accounts.upcomingTransactions.items.dueIn(
|
||||||
|
number:
|
||||||
|
today
|
||||||
|
.difference(
|
||||||
|
transaction.lastExecution ?? today,
|
||||||
|
)
|
||||||
|
.inDays,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
t.template.value!.amount < 0
|
transaction.template.value!.amount < 0
|
||||||
? Icons.remove
|
? Icons.remove
|
||||||
: Icons.add,
|
: Icons.add,
|
||||||
color:
|
color:
|
||||||
t.template.value!.amount < 0
|
transaction.template.value!.amount < 0
|
||||||
? Colors.red
|
? Colors.red
|
||||||
: Colors.green,
|
: Colors.green,
|
||||||
),
|
),
|
||||||
@ -58,12 +74,14 @@ class UpcomingTransactionsCard extends StatelessWidget {
|
|||||||
builder:
|
builder:
|
||||||
(context) => AddTransactionWidget(
|
(context) => AddTransactionWidget(
|
||||||
activeAccountItem: bloc.activeAccount!,
|
activeAccountItem: bloc.activeAccount!,
|
||||||
template: t.template.value!,
|
template: transaction.template.value!,
|
||||||
onAdd: (transaction) async {
|
onAdd: (newTransaction) async {
|
||||||
// Update the recurring template
|
// Update the recurring template
|
||||||
print(transaction.date);
|
transaction.lastExecution =
|
||||||
t.lastExecution = transaction.date;
|
newTransaction.date;
|
||||||
await upsertRecurringTransaction(t);
|
await upsertRecurringTransaction(
|
||||||
|
transaction,
|
||||||
|
);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -74,7 +92,7 @@ class UpcomingTransactionsCard extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
return ResponsiveCard(
|
return ResponsiveCard(
|
||||||
titleText: "Upcoming Transactions",
|
titleText: t.pages.accounts.upcomingTransactions.title,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(children: transactions),
|
child: Column(children: transactions),
|
||||||
|
@ -20,11 +20,8 @@ class BeneficiaryListPage extends StatelessWidget {
|
|||||||
leading: ImageWrapper(title: item.name, path: item.imagePath),
|
leading: ImageWrapper(title: item.name, path: item.imagePath),
|
||||||
// TODO: Allow deleting beneficiaries
|
// TODO: Allow deleting beneficiaries
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
onPressed: null,
|
onPressed: null,
|
||||||
icon: Icon(
|
icon: Icon(Icons.delete, color: Colors.grey),
|
||||||
Icons.delete,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
title: Text(item.name),
|
title: Text(item.name),
|
||||||
);
|
);
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:okane/database/collections/budget.dart';
|
import 'package:okane/database/collections/budget.dart';
|
||||||
import 'package:okane/database/database.dart';
|
import 'package:okane/database/database.dart';
|
||||||
|
import 'package:okane/i18n/strings.g.dart';
|
||||||
import 'package:okane/ui/state/core.dart';
|
import 'package:okane/ui/state/core.dart';
|
||||||
|
|
||||||
class AddBudgetPopup extends StatefulWidget {
|
class AddBudgetPopup extends StatefulWidget {
|
||||||
@ -23,12 +24,16 @@ class AddBudgetState extends State<AddBudgetPopup> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
TextField(
|
||||||
decoration: InputDecoration(hintText: "Budget name"),
|
decoration: InputDecoration(
|
||||||
|
hintText: t.pages.budgets.addBudget.budgetNameHint,
|
||||||
|
),
|
||||||
controller: _budgetNameEditController,
|
controller: _budgetNameEditController,
|
||||||
),
|
),
|
||||||
|
|
||||||
TextField(
|
TextField(
|
||||||
decoration: InputDecoration(hintText: "Income"),
|
decoration: InputDecoration(
|
||||||
|
hintText: t.pages.budgets.addBudget.income,
|
||||||
|
),
|
||||||
controller: _budgetIncomeEditController,
|
controller: _budgetIncomeEditController,
|
||||||
keyboardType: TextInputType.numberWithOptions(
|
keyboardType: TextInputType.numberWithOptions(
|
||||||
signed: false,
|
signed: false,
|
||||||
@ -57,7 +62,7 @@ class AddBudgetState extends State<AddBudgetPopup> {
|
|||||||
await upsertBudget(budget);
|
await upsertBudget(budget);
|
||||||
widget.onDone();
|
widget.onDone();
|
||||||
},
|
},
|
||||||
child: Text("Add"),
|
child: Text(t.modals.add),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -3,6 +3,7 @@ import 'package:get_it/get_it.dart';
|
|||||||
import 'package:okane/database/collections/budget.dart';
|
import 'package:okane/database/collections/budget.dart';
|
||||||
import 'package:okane/database/collections/expense_category.dart';
|
import 'package:okane/database/collections/expense_category.dart';
|
||||||
import 'package:okane/database/database.dart';
|
import 'package:okane/database/database.dart';
|
||||||
|
import 'package:okane/i18n/strings.g.dart';
|
||||||
import 'package:okane/ui/state/core.dart';
|
import 'package:okane/ui/state/core.dart';
|
||||||
import 'package:okane/ui/utils.dart';
|
import 'package:okane/ui/utils.dart';
|
||||||
import 'package:okane/ui/widgets/add_expense_category.dart';
|
import 'package:okane/ui/widgets/add_expense_category.dart';
|
||||||
@ -32,7 +33,7 @@ class AddBudgetItemState extends State<AddBudgetItemPopup> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text("Expense category"),
|
Text(t.common.expenseCategory.name),
|
||||||
|
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@ -46,13 +47,17 @@ class AddBudgetItemState extends State<AddBudgetItemPopup> {
|
|||||||
|
|
||||||
setState(() => _expenseCategory = category);
|
setState(() => _expenseCategory = category);
|
||||||
},
|
},
|
||||||
child: Text(_expenseCategory?.name ?? "None"),
|
child: Text(
|
||||||
|
_expenseCategory?.name ?? t.common.expenseCategory.none,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
TextField(
|
TextField(
|
||||||
decoration: InputDecoration(hintText: "Amount"),
|
decoration: InputDecoration(
|
||||||
|
hintText: t.pages.budgets.addBudgetItem.amountHint,
|
||||||
|
),
|
||||||
controller: _budgetItemAmountEditController,
|
controller: _budgetItemAmountEditController,
|
||||||
keyboardType: TextInputType.numberWithOptions(
|
keyboardType: TextInputType.numberWithOptions(
|
||||||
signed: false,
|
signed: false,
|
||||||
@ -91,7 +96,7 @@ class AddBudgetItemState extends State<AddBudgetItemPopup> {
|
|||||||
await upsertBudget(widget.budget);
|
await upsertBudget(widget.budget);
|
||||||
widget.onDone();
|
widget.onDone();
|
||||||
},
|
},
|
||||||
child: Text("Add"),
|
child: Text(t.modals.add),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import 'package:fl_chart/fl_chart.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:okane/database/collections/budget.dart';
|
import 'package:okane/database/collections/budget.dart';
|
||||||
import 'package:okane/database/database.dart';
|
import 'package:okane/database/database.dart';
|
||||||
import 'package:okane/ui/pages/account/breakdown_card.dart';
|
import 'package:okane/i18n/strings.g.dart';
|
||||||
import 'package:okane/ui/pages/budgets/add_budget_item.dart';
|
import 'package:okane/ui/pages/budgets/add_budget_item.dart';
|
||||||
import 'package:okane/ui/state/core.dart';
|
import 'package:okane/ui/state/core.dart';
|
||||||
import 'package:okane/ui/utils.dart';
|
import 'package:okane/ui/utils.dart';
|
||||||
import 'package:okane/ui/widgets/piechart.dart';
|
|
||||||
import 'package:okane/ui/widgets/piechart_card.dart';
|
import 'package:okane/ui/widgets/piechart_card.dart';
|
||||||
|
|
||||||
class BudgetDetailsPage extends StatelessWidget {
|
class BudgetDetailsPage extends StatelessWidget {
|
||||||
@ -55,13 +53,13 @@ class BudgetDetailsPage extends StatelessWidget {
|
|||||||
BlocBuilder<CoreCubit, CoreState>(
|
BlocBuilder<CoreCubit, CoreState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.activeBudget == null) {
|
if (state.activeBudget == null) {
|
||||||
return Text("No budget selected");
|
return Text(t.pages.budgets.details.noBudgetSelected);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.activeBudget!.items.isEmpty) {
|
if (state.activeBudget!.items.isEmpty) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Text("No budget items added"),
|
Text(t.pages.budgets.details.noBudgetItems),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(left: 16),
|
padding: EdgeInsets.only(left: 16),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
@ -92,7 +90,7 @@ class BudgetDetailsPage extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Budget items",
|
t.pages.budgets.details.budgetItems,
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
ListView.builder(
|
ListView.builder(
|
||||||
@ -105,9 +103,14 @@ class BudgetDetailsPage extends StatelessWidget {
|
|||||||
final amount = formatCurrency(item.amount);
|
final amount = formatCurrency(item.amount);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
"${item.expenseCategory.value!.name} ($amount)",
|
t.pages.budgets.details.items.title(
|
||||||
|
name: item.expenseCategory.value!.name,
|
||||||
|
amount: amount,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
t.pages.budgets.details.items.loading,
|
||||||
),
|
),
|
||||||
subtitle: Text("..."),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -167,7 +170,7 @@ class BudgetDetailsPage extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Days left",
|
t.pages.budgets.details.daysLeft,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style:
|
style:
|
||||||
Theme.of(
|
Theme.of(
|
||||||
@ -199,7 +202,7 @@ class BudgetDetailsPage extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Budget left",
|
t.pages.budgets.details.budgetLeft,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style:
|
style:
|
||||||
Theme.of(
|
Theme.of(
|
||||||
@ -233,7 +236,7 @@ class BudgetDetailsPage extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Budget total",
|
t.pages.budgets.details.totalBudget,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style:
|
style:
|
||||||
Theme.of(
|
Theme.of(
|
||||||
@ -276,14 +279,21 @@ class BudgetDetailsPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
titleText: "Budget breakdown",
|
titleText:
|
||||||
|
t.pages.budgets.details.budgetBreakdown.title,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: PieChartCard(
|
child: PieChartCard(
|
||||||
fallbackText: "No spending available",
|
fallbackText:
|
||||||
|
t
|
||||||
|
.pages
|
||||||
|
.budgets
|
||||||
|
.details
|
||||||
|
.budgetBreakdown
|
||||||
|
.noSpendingAvailable,
|
||||||
valueConverter: formatCurrency,
|
valueConverter: formatCurrency,
|
||||||
items:
|
items:
|
||||||
spending.entries
|
spending.entries
|
||||||
@ -295,7 +305,13 @@ class BudgetDetailsPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
titleText: "Spending Breakdown",
|
titleText:
|
||||||
|
t
|
||||||
|
.pages
|
||||||
|
.budgets
|
||||||
|
.details
|
||||||
|
.spendingBreakdown
|
||||||
|
.title,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -306,7 +322,7 @@ class BudgetDetailsPage extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Budget items",
|
t.pages.budgets.details.budgetItems,
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
@ -337,8 +353,12 @@ class BudgetDetailsPage extends StatelessWidget {
|
|||||||
: item.amount + spent;
|
: item.amount + spent;
|
||||||
final subtitleText =
|
final subtitleText =
|
||||||
left < 0
|
left < 0
|
||||||
? "${formatCurrency(left)} over"
|
? t.pages.budgets.details.items.over(
|
||||||
: "${formatCurrency(left)} left";
|
amount: formatCurrency(left),
|
||||||
|
)
|
||||||
|
: t.pages.budgets.details.items.remaining(
|
||||||
|
amount: formatCurrency(left),
|
||||||
|
);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
"${item.expenseCategory.value!.name} ($amount)",
|
"${item.expenseCategory.value!.name} ($amount)",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:okane/i18n/strings.g.dart';
|
||||||
import 'package:okane/screen.dart';
|
import 'package:okane/screen.dart';
|
||||||
import 'package:okane/ui/pages/budgets/add_budget.dart';
|
import 'package:okane/ui/pages/budgets/add_budget.dart';
|
||||||
import 'package:okane/ui/pages/budgets/edit_budget.dart';
|
import 'package:okane/ui/pages/budgets/edit_budget.dart';
|
||||||
@ -19,7 +20,7 @@ class BudgetListPage extends StatelessWidget {
|
|||||||
if (state.budgets.isEmpty) {
|
if (state.budgets.isEmpty) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [Text("No budgets")],
|
children: [Text(t.pages.budgets.noBudgets)],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:okane/database/collections/budget.dart';
|
import 'package:okane/database/collections/budget.dart';
|
||||||
import 'package:okane/database/database.dart';
|
import 'package:okane/database/database.dart';
|
||||||
|
import 'package:okane/i18n/strings.g.dart';
|
||||||
|
|
||||||
class EditBudgetPopup extends StatefulWidget {
|
class EditBudgetPopup extends StatefulWidget {
|
||||||
final Budget budget;
|
final Budget budget;
|
||||||
@ -36,12 +37,14 @@ class EditBudgetState extends State<EditBudgetPopup> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
TextField(
|
||||||
decoration: InputDecoration(hintText: "Name"),
|
decoration: InputDecoration(
|
||||||
|
hintText: t.pages.budgets.addBudget.budgetNameHint,
|
||||||
|
),
|
||||||
controller: _budgetNameEditController,
|
controller: _budgetNameEditController,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text("Include other spendings"),
|
Text(t.pages.budgets.addBudget.includeOtherSpendings),
|
||||||
Switch(
|
Switch(
|
||||||
value: _includeOtherSpendings,
|
value: _includeOtherSpendings,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -71,7 +74,7 @@ class EditBudgetState extends State<EditBudgetPopup> {
|
|||||||
await upsertBudget(widget.budget);
|
await upsertBudget(widget.budget);
|
||||||
widget.onDone();
|
widget.onDone();
|
||||||
},
|
},
|
||||||
child: Text("Save"),
|
child: Text(t.modals.save),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:okane/i18n/strings.g.dart';
|
||||||
import 'package:okane/ui/state/settings.dart';
|
import 'package:okane/ui/state/settings.dart';
|
||||||
import 'package:okane/ui/utils.dart';
|
import 'package:okane/ui/utils.dart';
|
||||||
|
|
||||||
@ -14,11 +15,17 @@ class SettingsPage extends StatelessWidget {
|
|||||||
BlocBuilder<SettingsCubit, SettingsWrapper>(
|
BlocBuilder<SettingsCubit, SettingsWrapper>(
|
||||||
builder:
|
builder:
|
||||||
(context, state) => ListTile(
|
(context, state) => ListTile(
|
||||||
title: Text("Color Scheme"),
|
title: Text(t.pages.settings.colorSchemes.title),
|
||||||
subtitle: switch (state.settings.colorScheme) {
|
subtitle: switch (state.settings.colorScheme) {
|
||||||
ColorSchemeSettings.dark => Text("Dark"),
|
ColorSchemeSettings.dark => Text(
|
||||||
ColorSchemeSettings.light => Text("Light"),
|
t.pages.settings.colorSchemes.dark,
|
||||||
ColorSchemeSettings.system => Text("System"),
|
),
|
||||||
|
ColorSchemeSettings.light => Text(
|
||||||
|
t.pages.settings.colorSchemes.light,
|
||||||
|
),
|
||||||
|
ColorSchemeSettings.system => Text(
|
||||||
|
t.pages.settings.colorSchemes.system,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final colorScheme = await showDialogOrModal(
|
final colorScheme = await showDialogOrModal(
|
||||||
@ -35,9 +42,12 @@ class SettingsPage extends StatelessWidget {
|
|||||||
? Icon(Icons.check)
|
? Icon(Icons.check)
|
||||||
: null,
|
: null,
|
||||||
title: Text(switch (s) {
|
title: Text(switch (s) {
|
||||||
ColorSchemeSettings.dark => "Dark",
|
ColorSchemeSettings.dark =>
|
||||||
ColorSchemeSettings.light => "Light",
|
t.pages.settings.colorSchemes.dark,
|
||||||
ColorSchemeSettings.system => "System",
|
ColorSchemeSettings.light =>
|
||||||
|
t.pages.settings.colorSchemes.light,
|
||||||
|
ColorSchemeSettings.system =>
|
||||||
|
t.pages.settings.colorSchemes.system,
|
||||||
}),
|
}),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop(s);
|
Navigator.of(context).pop(s);
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:okane/database/database.dart';
|
import 'package:okane/database/database.dart';
|
||||||
|
import 'package:okane/i18n/strings.g.dart';
|
||||||
import 'package:okane/ui/state/core.dart';
|
import 'package:okane/ui/state/core.dart';
|
||||||
import 'package:okane/ui/utils.dart';
|
import 'package:okane/ui/utils.dart';
|
||||||
import 'package:okane/ui/widgets/add_template.dart';
|
import 'package:okane/ui/widgets/add_template.dart';
|
||||||
@ -25,7 +26,9 @@ class TemplateListState extends State<TemplateListPage> {
|
|||||||
children: [
|
children: [
|
||||||
CustomScrollView(
|
CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverToBoxAdapter(child: Text("Non-recurring")),
|
SliverToBoxAdapter(
|
||||||
|
child: Text(t.pages.templates.nonRecurring.title),
|
||||||
|
),
|
||||||
SliverList.builder(
|
SliverList.builder(
|
||||||
itemCount: nonRecurringTemplates.length,
|
itemCount: nonRecurringTemplates.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
@ -38,8 +41,10 @@ class TemplateListState extends State<TemplateListPage> {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final result = await confirm(
|
final result = await confirm(
|
||||||
context,
|
context,
|
||||||
"Remove Template",
|
t.pages.templates.removeTemplate.title,
|
||||||
"Are you sure you want to remove the template '${template.name}'",
|
t.pages.templates.removeTemplate.body(
|
||||||
|
name: template.name,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return;
|
return;
|
||||||
@ -51,7 +56,9 @@ class TemplateListState extends State<TemplateListPage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(child: Text("Recurring")),
|
SliverToBoxAdapter(
|
||||||
|
child: Text(t.pages.templates.recurring.title),
|
||||||
|
),
|
||||||
SliverList.builder(
|
SliverList.builder(
|
||||||
itemCount: state.recurringTransactions.length,
|
itemCount: state.recurringTransactions.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
@ -63,8 +70,10 @@ class TemplateListState extends State<TemplateListPage> {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final result = await confirm(
|
final result = await confirm(
|
||||||
context,
|
context,
|
||||||
"Remove Template",
|
t.pages.templates.removeTemplate.title,
|
||||||
"Are you sure you want to remove the template '${template.template.value!.name}'",
|
t.pages.templates.removeTemplate.body(
|
||||||
|
name: template.template.value!.name,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return;
|
return;
|
||||||
|
@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:okane/database/collections/beneficiary.dart';
|
import 'package:okane/database/collections/beneficiary.dart';
|
||||||
import 'package:okane/database/database.dart';
|
import 'package:okane/database/database.dart';
|
||||||
|
import 'package:okane/i18n/strings.g.dart';
|
||||||
import 'package:okane/ui/state/core.dart';
|
import 'package:okane/ui/state/core.dart';
|
||||||
import 'package:okane/ui/utils.dart';
|
import 'package:okane/ui/utils.dart';
|
||||||
import 'package:okane/ui/widgets/image_wrapper.dart';
|
import 'package:okane/ui/widgets/image_wrapper.dart';
|
||||||
@ -69,7 +70,9 @@ class TransactionDetailsPage extends StatelessWidget {
|
|||||||
child: BlocBuilder<CoreCubit, CoreState>(
|
child: BlocBuilder<CoreCubit, CoreState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.activeTransaction == null) {
|
if (state.activeTransaction == null) {
|
||||||
return Text("No transaction selected");
|
return Text(
|
||||||
|
t.pages.transactions.details.noTransactionSelected,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
@ -162,7 +165,7 @@ class TransactionDetailsPage extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text("Expense category"),
|
Text(t.common.expenseCategory.name),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(left: 16),
|
padding: EdgeInsets.only(left: 16),
|
||||||
child: Chip(
|
child: Chip(
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:okane/database/collections/expense_category.dart';
|
import 'package:okane/database/collections/expense_category.dart';
|
||||||
import 'package:okane/database/database.dart';
|
import 'package:okane/database/database.dart';
|
||||||
|
import 'package:okane/i18n/strings.g.dart';
|
||||||
import 'package:okane/ui/state/core.dart';
|
import 'package:okane/ui/state/core.dart';
|
||||||
|
|
||||||
class AddExpenseCategory extends StatefulWidget {
|
class AddExpenseCategory extends StatefulWidget {
|
||||||
@ -39,7 +40,9 @@ class AddExpenseCategoryState extends State<AddExpenseCategory> {
|
|||||||
),
|
),
|
||||||
|
|
||||||
TextField(
|
TextField(
|
||||||
decoration: InputDecoration(hintText: "Category name"),
|
decoration: InputDecoration(
|
||||||
|
hintText: t.common.expenseCategory.name,
|
||||||
|
),
|
||||||
controller: _categoryNameController,
|
controller: _categoryNameController,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
@ -54,7 +57,7 @@ class AddExpenseCategoryState extends State<AddExpenseCategory> {
|
|||||||
_categoryNameController.text = "";
|
_categoryNameController.text = "";
|
||||||
Navigator.of(context).pop(category);
|
Navigator.of(context).pop(category);
|
||||||
},
|
},
|
||||||
child: Text("Add"),
|
child: Text(t.modals.add),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -1,291 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_picker_plus/picker.dart';
|
|
||||||
import 'package:get_it/get_it.dart';
|
|
||||||
import 'package:okane/database/collections/account.dart';
|
|
||||||
import 'package:okane/database/collections/beneficiary.dart';
|
|
||||||
import 'package:okane/database/collections/recurrent.dart';
|
|
||||||
import 'package:okane/database/collections/template.dart';
|
|
||||||
import 'package:okane/database/database.dart';
|
|
||||||
import 'package:okane/ui/state/core.dart';
|
|
||||||
import 'package:okane/ui/transaction.dart';
|
|
||||||
import 'package:okane/ui/utils.dart';
|
|
||||||
import 'package:searchfield/searchfield.dart';
|
|
||||||
|
|
||||||
enum Period { days, weeks, months, years }
|
|
||||||
|
|
||||||
class AddRecurringTransactionTemplateWidget extends StatefulWidget {
|
|
||||||
final VoidCallback onAdd;
|
|
||||||
|
|
||||||
final Account activeAccountItem;
|
|
||||||
|
|
||||||
const AddRecurringTransactionTemplateWidget({
|
|
||||||
super.key,
|
|
||||||
required this.activeAccountItem,
|
|
||||||
required this.onAdd,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AddRecurringTransactionTemplateWidget> createState() =>
|
|
||||||
_AddRecurringTransactionTemplateWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AddRecurringTransactionTemplateWidgetState
|
|
||||||
extends State<AddRecurringTransactionTemplateWidget> {
|
|
||||||
final TextEditingController _beneficiaryTextController =
|
|
||||||
TextEditingController();
|
|
||||||
final TextEditingController _amountTextController = TextEditingController();
|
|
||||||
final TextEditingController _templateNameController = TextEditingController();
|
|
||||||
|
|
||||||
List<Beneficiary> beneficiaries = [];
|
|
||||||
|
|
||||||
SearchFieldListItem<Beneficiary>? _selectedBeneficiary;
|
|
||||||
|
|
||||||
TransactionDirection _selectedDirection = TransactionDirection.send;
|
|
||||||
|
|
||||||
Period _selectedPeriod = Period.months;
|
|
||||||
int _periodSize = 1;
|
|
||||||
|
|
||||||
String getBeneficiaryName(Beneficiary item) {
|
|
||||||
return switch (item.type) {
|
|
||||||
BeneficiaryType.account => "${item.name} (Account)",
|
|
||||||
BeneficiaryType.other => item.name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _submit(BuildContext context) async {
|
|
||||||
final beneficiaryName = _beneficiaryTextController.text;
|
|
||||||
if (_selectedBeneficiary == null && beneficiaryName.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_templateNameController.text.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Beneficiary? beneficiary = _selectedBeneficiary?.item;
|
|
||||||
if (beneficiary == null ||
|
|
||||||
getBeneficiaryName(beneficiary) != beneficiaryName) {
|
|
||||||
// Add a new beneficiary, if none was selected
|
|
||||||
final result = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder:
|
|
||||||
(context) => AlertDialog(
|
|
||||||
title: const Text("Add Beneficiary"),
|
|
||||||
content: Text(
|
|
||||||
"The beneficiary '$beneficiaryName' does not exist. Do you want to add it?",
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
|
||||||
),
|
|
||||||
child: const Text('Add'),
|
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
|
||||||
),
|
|
||||||
child: const Text('Cancel'),
|
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (result == null || !result) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
beneficiary =
|
|
||||||
Beneficiary()
|
|
||||||
..name = beneficiaryName
|
|
||||||
..type = BeneficiaryType.other;
|
|
||||||
await upsertBeneficiary(beneficiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
final days = switch (_selectedPeriod) {
|
|
||||||
Period.days => _periodSize,
|
|
||||||
Period.weeks => _periodSize * 7,
|
|
||||||
Period.months => _periodSize * 31,
|
|
||||||
Period.years => _periodSize * 365,
|
|
||||||
};
|
|
||||||
final factor = switch (_selectedDirection) {
|
|
||||||
TransactionDirection.send => -1,
|
|
||||||
TransactionDirection.receive => 1,
|
|
||||||
};
|
|
||||||
final amount = factor * double.parse(_amountTextController.text).abs();
|
|
||||||
final template =
|
|
||||||
TransactionTemplate()
|
|
||||||
..name = _templateNameController.text
|
|
||||||
..beneficiary.value = beneficiary
|
|
||||||
..account.value = widget.activeAccountItem
|
|
||||||
..recurring = true
|
|
||||||
..amount = amount;
|
|
||||||
await upsertTransactionTemplate(template);
|
|
||||||
|
|
||||||
final transaction =
|
|
||||||
RecurringTransaction()
|
|
||||||
..lastExecution = null
|
|
||||||
..template.value = template
|
|
||||||
..account.value = widget.activeAccountItem
|
|
||||||
..days = days;
|
|
||||||
await upsertRecurringTransaction(transaction);
|
|
||||||
|
|
||||||
_periodSize = 1;
|
|
||||||
_selectedPeriod = Period.weeks;
|
|
||||||
_amountTextController.text = "";
|
|
||||||
_templateNameController.text = "";
|
|
||||||
widget.onAdd();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: ListView(
|
|
||||||
shrinkWrap: true,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: TextField(
|
|
||||||
controller: _templateNameController,
|
|
||||||
decoration: InputDecoration(label: Text("Template name")),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: SegmentedButton<TransactionDirection>(
|
|
||||||
segments: [
|
|
||||||
ButtonSegment(
|
|
||||||
value: TransactionDirection.send,
|
|
||||||
label: Text("Send"),
|
|
||||||
icon: Icon(Icons.remove),
|
|
||||||
),
|
|
||||||
ButtonSegment(
|
|
||||||
value: TransactionDirection.receive,
|
|
||||||
label: Text("Receive"),
|
|
||||||
icon: Icon(Icons.add),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
selected: <TransactionDirection>{_selectedDirection},
|
|
||||||
multiSelectionEnabled: false,
|
|
||||||
onSelectionChanged: (selection) {
|
|
||||||
setState(() => _selectedDirection = selection.first);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: SearchField<Beneficiary>(
|
|
||||||
suggestions:
|
|
||||||
beneficiaries
|
|
||||||
.where((el) {
|
|
||||||
final bloc = GetIt.I.get<CoreCubit>();
|
|
||||||
if (el.type == BeneficiaryType.account) {
|
|
||||||
return el.account.value?.id != bloc.activeAccount?.id;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.map((el) {
|
|
||||||
return SearchFieldListItem(
|
|
||||||
getBeneficiaryName(el),
|
|
||||||
item: el,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.toList(),
|
|
||||||
hint: switch (_selectedDirection) {
|
|
||||||
TransactionDirection.send => "Payee",
|
|
||||||
TransactionDirection.receive => "Payer",
|
|
||||||
},
|
|
||||||
controller: _beneficiaryTextController,
|
|
||||||
selectedValue: _selectedBeneficiary,
|
|
||||||
onSuggestionTap: (beneficiary) {
|
|
||||||
setState(() => _selectedBeneficiary = beneficiary);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: TextField(
|
|
||||||
controller: _amountTextController,
|
|
||||||
keyboardType: TextInputType.numberWithOptions(
|
|
||||||
signed: false,
|
|
||||||
decimal: false,
|
|
||||||
),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: "Amount",
|
|
||||||
icon: Icon(Icons.euro),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
|
|
||||||
child: SegmentedButton<Period>(
|
|
||||||
segments: [
|
|
||||||
ButtonSegment(value: Period.days, label: Text("Days")),
|
|
||||||
ButtonSegment(value: Period.weeks, label: Text("Weeks")),
|
|
||||||
ButtonSegment(value: Period.months, label: Text("Months")),
|
|
||||||
ButtonSegment(value: Period.years, label: Text("Years")),
|
|
||||||
],
|
|
||||||
selected: <Period>{_selectedPeriod},
|
|
||||||
multiSelectionEnabled: false,
|
|
||||||
onSelectionChanged: (selection) {
|
|
||||||
setState(() => _selectedPeriod = selection.first);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text.rich(
|
|
||||||
TextSpan(
|
|
||||||
text: "Repeat every ",
|
|
||||||
children: [
|
|
||||||
WidgetSpan(
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Picker(
|
|
||||||
adapter: NumberPickerAdapter(
|
|
||||||
data: [
|
|
||||||
NumberPickerColumn(
|
|
||||||
begin: 1,
|
|
||||||
end: 999,
|
|
||||||
initValue: _periodSize,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
hideHeader: true,
|
|
||||||
selectedTextStyle: TextStyle(color: Colors.blue),
|
|
||||||
onConfirm: (Picker picker, List value) {
|
|
||||||
setState(() {
|
|
||||||
_periodSize = (value.first as int) + 1;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
).showDialog(context);
|
|
||||||
},
|
|
||||||
child: Text(_periodSize.toString()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: switch (_selectedPeriod) {
|
|
||||||
Period.days => " days",
|
|
||||||
Period.weeks => " weeks",
|
|
||||||
Period.months => " months",
|
|
||||||
Period.years => " years",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: OutlinedButton(
|
|
||||||
onPressed: () => _submit(context),
|
|
||||||
child: Text("Add"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,6 +7,7 @@ import 'package:okane/database/collections/expense_category.dart';
|
|||||||
import 'package:okane/database/collections/recurrent.dart';
|
import 'package:okane/database/collections/recurrent.dart';
|
||||||
import 'package:okane/database/collections/template.dart';
|
import 'package:okane/database/collections/template.dart';
|
||||||
import 'package:okane/database/database.dart';
|
import 'package:okane/database/database.dart';
|
||||||
|
import 'package:okane/i18n/strings.g.dart';
|
||||||
import 'package:okane/ui/state/core.dart';
|
import 'package:okane/ui/state/core.dart';
|
||||||
import 'package:okane/ui/transaction.dart';
|
import 'package:okane/ui/transaction.dart';
|
||||||
import 'package:okane/ui/utils.dart';
|
import 'package:okane/ui/utils.dart';
|
||||||
@ -51,7 +52,9 @@ class _AddTransactionTemplateWidgetState
|
|||||||
|
|
||||||
String getBeneficiaryName(Beneficiary item) {
|
String getBeneficiaryName(Beneficiary item) {
|
||||||
return switch (item.type) {
|
return switch (item.type) {
|
||||||
BeneficiaryType.account => "${item.name} (Account)",
|
BeneficiaryType.account => t.common.beneficiary.nameWithAccount(
|
||||||
|
name: item.name,
|
||||||
|
),
|
||||||
BeneficiaryType.other => item.name,
|
BeneficiaryType.other => item.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -59,7 +62,6 @@ class _AddTransactionTemplateWidgetState
|
|||||||
Future<void> _submit(BuildContext context) async {
|
Future<void> _submit(BuildContext context) async {
|
||||||
final beneficiaryName = _beneficiaryTextController.text;
|
final beneficiaryName = _beneficiaryTextController.text;
|
||||||
if (_selectedBeneficiary == null && beneficiaryName.isEmpty) {
|
if (_selectedBeneficiary == null && beneficiaryName.isEmpty) {
|
||||||
print("No beneficiary");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_templateNameController.text.isEmpty) {
|
if (_templateNameController.text.isEmpty) {
|
||||||
@ -74,23 +76,23 @@ class _AddTransactionTemplateWidgetState
|
|||||||
context: context,
|
context: context,
|
||||||
builder:
|
builder:
|
||||||
(context) => AlertDialog(
|
(context) => AlertDialog(
|
||||||
title: const Text("Add Beneficiary"),
|
title: Text(t.common.beneficiary.addBeneficiary.title),
|
||||||
content: Text(
|
content: Text(
|
||||||
"The beneficiary '$beneficiaryName' does not exist. Do you want to add it?",
|
t.common.beneficiary.addBeneficiary.body(name: beneficiaryName),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||||
),
|
),
|
||||||
child: const Text('Add'),
|
child: Text(t.modals.add),
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||||
),
|
),
|
||||||
child: const Text('Cancel'),
|
child: Text(t.modals.cancel),
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -151,7 +153,7 @@ class _AddTransactionTemplateWidgetState
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _templateNameController,
|
controller: _templateNameController,
|
||||||
decoration: InputDecoration(label: Text("Template name")),
|
decoration: InputDecoration(label: Text(t.common.templateName)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@ -161,12 +163,12 @@ class _AddTransactionTemplateWidgetState
|
|||||||
segments: [
|
segments: [
|
||||||
ButtonSegment(
|
ButtonSegment(
|
||||||
value: TransactionDirection.send,
|
value: TransactionDirection.send,
|
||||||
label: Text("Send"),
|
label: Text(t.common.transaction.directionSend),
|
||||||
icon: Icon(Icons.remove),
|
icon: Icon(Icons.remove),
|
||||||
),
|
),
|
||||||
ButtonSegment(
|
ButtonSegment(
|
||||||
value: TransactionDirection.receive,
|
value: TransactionDirection.receive,
|
||||||
label: Text("Receive"),
|
label: Text(t.common.transaction.directionReceive),
|
||||||
icon: Icon(Icons.add),
|
icon: Icon(Icons.add),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -201,8 +203,10 @@ class _AddTransactionTemplateWidgetState
|
|||||||
})
|
})
|
||||||
.toList(),
|
.toList(),
|
||||||
hint: switch (_selectedDirection) {
|
hint: switch (_selectedDirection) {
|
||||||
TransactionDirection.send => "Payee",
|
TransactionDirection.send =>
|
||||||
TransactionDirection.receive => "Payer",
|
t.common.transaction.beneficiaryTextfieldHintSend,
|
||||||
|
TransactionDirection.receive =>
|
||||||
|
t.common.transaction.beneficiaryTextfieldHintReceive,
|
||||||
},
|
},
|
||||||
controller: _beneficiaryTextController,
|
controller: _beneficiaryTextController,
|
||||||
selectedValue: _selectedBeneficiary,
|
selectedValue: _selectedBeneficiary,
|
||||||
@ -222,7 +226,7 @@ class _AddTransactionTemplateWidgetState
|
|||||||
decimal: false,
|
decimal: false,
|
||||||
),
|
),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: "Amount",
|
hintText: t.common.amount,
|
||||||
icon: Icon(Icons.euro),
|
icon: Icon(Icons.euro),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -230,7 +234,7 @@ class _AddTransactionTemplateWidgetState
|
|||||||
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text("Expense category"),
|
Text(t.common.expenseCategory.name),
|
||||||
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(left: 16),
|
padding: EdgeInsets.only(left: 16),
|
||||||
@ -246,7 +250,9 @@ class _AddTransactionTemplateWidgetState
|
|||||||
|
|
||||||
setState(() => _expenseCategory = category);
|
setState(() => _expenseCategory = category);
|
||||||
},
|
},
|
||||||
child: Text(_expenseCategory?.name ?? "None"),
|
child: Text(
|
||||||
|
_expenseCategory?.name ?? t.common.expenseCategory.none,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -254,7 +260,7 @@ class _AddTransactionTemplateWidgetState
|
|||||||
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text("Is recurring"),
|
Text(t.pages.templates.addTemplate.isRecurring),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(left: 16),
|
padding: EdgeInsets.only(left: 16),
|
||||||
child: Switch(
|
child: Switch(
|
||||||
@ -272,10 +278,22 @@ class _AddTransactionTemplateWidgetState
|
|||||||
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
|
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
|
||||||
child: SegmentedButton<Period>(
|
child: SegmentedButton<Period>(
|
||||||
segments: [
|
segments: [
|
||||||
ButtonSegment(value: Period.days, label: Text("Days")),
|
ButtonSegment(
|
||||||
ButtonSegment(value: Period.weeks, label: Text("Weeks")),
|
value: Period.days,
|
||||||
ButtonSegment(value: Period.months, label: Text("Months")),
|
label: Text(t.common.period.days),
|
||||||
ButtonSegment(value: Period.years, label: Text("Years")),
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: Period.weeks,
|
||||||
|
label: Text(t.common.period.weeks),
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: Period.months,
|
||||||
|
label: Text(t.common.period.months),
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: Period.years,
|
||||||
|
label: Text(t.common.period.years),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
selected: <Period>{_selectedPeriod},
|
selected: <Period>{_selectedPeriod},
|
||||||
multiSelectionEnabled: false,
|
multiSelectionEnabled: false,
|
||||||
@ -311,10 +329,18 @@ class _AddTransactionTemplateWidgetState
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
switch (_selectedPeriod) {
|
switch (_selectedPeriod) {
|
||||||
Period.days => "$_periodSize days",
|
Period.days => t.common.period.daysNumber(
|
||||||
Period.weeks => "$_periodSize weeks",
|
number: _periodSize,
|
||||||
Period.months => "$_periodSize months",
|
),
|
||||||
Period.years => "$_periodSize years",
|
Period.weeks => t.common.period.weeksNumber(
|
||||||
|
number: _periodSize,
|
||||||
|
),
|
||||||
|
Period.months => t.common.period.monthsNumber(
|
||||||
|
number: _periodSize,
|
||||||
|
),
|
||||||
|
Period.years => t.common.period.yearsNumber(
|
||||||
|
number: _periodSize,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: _isRecurring ? Colors.black : Colors.grey,
|
color: _isRecurring ? Colors.black : Colors.grey,
|
||||||
@ -341,7 +367,7 @@ class _AddTransactionTemplateWidgetState
|
|||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: () => _submit(context),
|
onPressed: () => _submit(context),
|
||||||
child: Text("Add"),
|
child: Text(t.modals.add),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -7,6 +7,7 @@ import 'package:okane/database/collections/expense_category.dart';
|
|||||||
import 'package:okane/database/collections/template.dart';
|
import 'package:okane/database/collections/template.dart';
|
||||||
import 'package:okane/database/collections/transaction.dart';
|
import 'package:okane/database/collections/transaction.dart';
|
||||||
import 'package:okane/database/database.dart';
|
import 'package:okane/database/database.dart';
|
||||||
|
import 'package:okane/i18n/strings.g.dart';
|
||||||
import 'package:okane/ui/state/core.dart';
|
import 'package:okane/ui/state/core.dart';
|
||||||
import 'package:okane/ui/transaction.dart';
|
import 'package:okane/ui/transaction.dart';
|
||||||
import 'package:okane/ui/utils.dart';
|
import 'package:okane/ui/utils.dart';
|
||||||
@ -67,7 +68,9 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
|
|||||||
|
|
||||||
String getBeneficiaryName(Beneficiary item) {
|
String getBeneficiaryName(Beneficiary item) {
|
||||||
return switch (item.type) {
|
return switch (item.type) {
|
||||||
BeneficiaryType.account => "${item.name} (Account)",
|
BeneficiaryType.account => t.common.beneficiary.nameWithAccount(
|
||||||
|
name: item.name,
|
||||||
|
),
|
||||||
BeneficiaryType.other => item.name,
|
BeneficiaryType.other => item.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -86,23 +89,23 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
|
|||||||
context: context,
|
context: context,
|
||||||
builder:
|
builder:
|
||||||
(context) => AlertDialog(
|
(context) => AlertDialog(
|
||||||
title: const Text("Add Beneficiary"),
|
title: Text(t.common.beneficiary.addBeneficiary.title),
|
||||||
content: Text(
|
content: Text(
|
||||||
"The beneficiary '$beneficiaryName' does not exist. Do you want to add it?",
|
t.common.beneficiary.addBeneficiary.body(name: beneficiaryName),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||||
),
|
),
|
||||||
child: const Text('Add'),
|
child: Text(t.modals.add),
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||||
),
|
),
|
||||||
child: const Text('Cancel'),
|
child: Text(t.modals.cancel),
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -177,7 +180,7 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
|
|||||||
template.beneficiary.value!,
|
template.beneficiary.value!,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Text("Use template"),
|
child: Text(t.pages.transactions.addTransaction.useTemplate),
|
||||||
),
|
),
|
||||||
|
|
||||||
Padding(
|
Padding(
|
||||||
@ -186,12 +189,12 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
|
|||||||
segments: [
|
segments: [
|
||||||
ButtonSegment(
|
ButtonSegment(
|
||||||
value: TransactionDirection.send,
|
value: TransactionDirection.send,
|
||||||
label: Text("Send"),
|
label: Text(t.common.transaction.directionSend),
|
||||||
icon: Icon(Icons.remove),
|
icon: Icon(Icons.remove),
|
||||||
),
|
),
|
||||||
ButtonSegment(
|
ButtonSegment(
|
||||||
value: TransactionDirection.receive,
|
value: TransactionDirection.receive,
|
||||||
label: Text("Receive"),
|
label: Text(t.common.transaction.directionReceive),
|
||||||
icon: Icon(Icons.add),
|
icon: Icon(Icons.add),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -226,8 +229,10 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
|
|||||||
})
|
})
|
||||||
.toList(),
|
.toList(),
|
||||||
hint: switch (_selectedDirection) {
|
hint: switch (_selectedDirection) {
|
||||||
TransactionDirection.send => "Payee",
|
TransactionDirection.send =>
|
||||||
TransactionDirection.receive => "Payer",
|
t.common.transaction.beneficiaryTextfieldHintSend,
|
||||||
|
TransactionDirection.receive =>
|
||||||
|
t.common.transaction.beneficiaryTextfieldHintReceive,
|
||||||
},
|
},
|
||||||
controller: _beneficiaryTextController,
|
controller: _beneficiaryTextController,
|
||||||
selectedValue: _selectedBeneficiary,
|
selectedValue: _selectedBeneficiary,
|
||||||
@ -247,7 +252,7 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
|
|||||||
decimal: false,
|
decimal: false,
|
||||||
),
|
),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: "Amount",
|
hintText: t.common.amount,
|
||||||
icon: Icon(Icons.euro),
|
icon: Icon(Icons.euro),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -258,7 +263,7 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text("Date"),
|
Text(t.common.date),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(left: 16),
|
padding: EdgeInsets.only(left: 16),
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
@ -289,7 +294,7 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
|
|||||||
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text("Expense category"),
|
Text(t.common.expenseCategory.name),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(left: 16),
|
padding: EdgeInsets.only(left: 16),
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
@ -304,7 +309,9 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
|
|||||||
|
|
||||||
setState(() => _expenseCategory = category);
|
setState(() => _expenseCategory = category);
|
||||||
},
|
},
|
||||||
child: Text(_expenseCategory?.name ?? "None"),
|
child: Text(
|
||||||
|
_expenseCategory?.name ?? t.common.expenseCategory.none,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -314,7 +321,7 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
|
|||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: () => _submit(context),
|
onPressed: () => _submit(context),
|
||||||
child: Text("Add"),
|
child: Text(t.modals.add),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
56
pubspec.lock
56
pubspec.lock
@ -177,6 +177,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.6"
|
version: "3.0.6"
|
||||||
|
csv:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csv
|
||||||
|
sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.0"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -237,10 +245,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: "8986dec4581b4bcd4b6df5d75a2ea0bede3db802f500635d05fa8be298f9467f"
|
sha256: a222f231db4f822fc49e3b753674bda630e981873c84bf8604bceeb77fce0b24
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.1.2"
|
version: "10.1.7"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -416,6 +424,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.7"
|
version: "0.6.7"
|
||||||
|
json2yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: json2yaml
|
||||||
|
sha256: da94630fbc56079426fdd167ae58373286f603371075b69bf46d848d63ba3e51
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.1"
|
||||||
json_annotation:
|
json_annotation:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -636,10 +652,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: searchfield
|
name: searchfield
|
||||||
sha256: "223fca0828ec95f45501db93feac7b120b93600760c0d8c04039fb2eeed9cc20"
|
sha256: "98fa29165366ec178e86a370918b084c9830cdf6663126fbd11b8c6f77cdcd0f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.7"
|
version: "1.2.9"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -717,6 +733,30 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
slang:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: slang
|
||||||
|
sha256: a466773de768eb95bdf681e0a92e7c8010d44bb247b62130426c83ece33aeaed
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.32.0"
|
||||||
|
slang_build_runner:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: slang_build_runner
|
||||||
|
sha256: b2e0c63f3c801a4aa70b4ca43173893d6eb7d5a421fc9d97ad983527397631b3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.32.0"
|
||||||
|
slang_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: slang_flutter
|
||||||
|
sha256: "1a98e878673996902fa5ef0b61ce5c245e41e4d25640d18af061c6aab917b0c7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.32.0"
|
||||||
source_gen:
|
source_gen:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -849,10 +889,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: web_socket
|
name: web_socket
|
||||||
sha256: bfe6f435f6ec49cb6c01da1e275ae4228719e59a6b067048c51e72d9d63bcc4b
|
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.1"
|
||||||
web_socket_channel:
|
web_socket_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -865,10 +905,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f
|
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.12.0"
|
version: "5.13.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -26,6 +26,8 @@ dependencies:
|
|||||||
shared_preferences: ^2.5.3
|
shared_preferences: ^2.5.3
|
||||||
json_annotation: ^4.9.0
|
json_annotation: ^4.9.0
|
||||||
more: 4.5.0
|
more: 4.5.0
|
||||||
|
slang: ^3.0.0
|
||||||
|
slang_flutter: ^3.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -35,6 +37,7 @@ dev_dependencies:
|
|||||||
freezed: 2.5.0
|
freezed: 2.5.0
|
||||||
isar_generator: ^3.1.0+1
|
isar_generator: ^3.1.0+1
|
||||||
json_serializable: ^6.4.0
|
json_serializable: ^6.4.0
|
||||||
|
slang_build_runner: ^3.0.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
Loading…
Reference in New Issue
Block a user