Compare commits

..

27 Commits

Author SHA1 Message Date
f8ff892104 Bump tag 2025-05-19 23:16:33 +02:00
121d0ec236 Auto-fix 2025-05-19 22:01:17 +02:00
71993c2686 Implement account deletion 2025-05-19 21:55:49 +02:00
cbe2d5a556 Bump tag 2025-05-18 22:43:26 +02:00
c4f7d5745a Ignore Sentry artifacts 2025-05-18 22:19:37 +02:00
567f5070e9 Allow sending errors to Sentry 2025-05-18 22:19:10 +02:00
baf0dfa99d Format 2025-05-18 15:09:05 +02:00
facbc16bb2 Fix budgets appearing multiple times 2025-05-18 14:46:48 +02:00
42b1bbd438 Make budgets work again 2025-05-18 14:42:42 +02:00
5dc474407c Start migration to sqlite using drift 2025-05-17 23:51:51 +02:00
4d267eff88 Bump version 2025-05-12 22:22:35 +02:00
ba78685bfd Fix adding a new beneficiary in the loan screen 2025-05-12 22:22:01 +02:00
e0c16031ef Implement a nicer account switcher 2025-05-12 22:16:14 +02:00
88c9991e0d Make the loan details work on mobile 2025-05-12 22:01:53 +02:00
c5aa165424 Add a loan feature 2025-05-12 21:02:51 +02:00
e0fba11f25 Add i18n via slang 2025-05-12 00:02:19 +02:00
99ab2f006d Add the button to delete beneficiaries 2025-05-11 22:55:06 +02:00
1957c0711c Make the account screen a bit nicer 2025-05-11 22:53:10 +02:00
e7e671f4cd Implement deleting an account 2025-05-11 22:47:14 +02:00
93d152f7e4 Indicate the selected color scheme 2025-05-11 21:59:00 +02:00
407626b535 Fix ink splash for the account card 2025-05-11 20:46:31 +02:00
f4a232883f Add settings page 2025-05-11 20:32:34 +02:00
c38e76490a Add the beneficiary page 2025-05-11 17:52:53 +02:00
384aa4eb6f Allow deleting templates 2025-05-11 15:40:12 +02:00
058291fa80 Fix the template page 2025-05-10 22:33:20 +02:00
60bfd9481f UI fixes 2025-05-09 20:23:45 +02:00
5a2dbf8962 Fix InkWell on the beneficiary image 2025-05-07 22:53:26 +02:00
62 changed files with 12135 additions and 6647 deletions

4
.gitignore vendored
View File

@@ -11,6 +11,7 @@
.svn/ .svn/
.swiftpm/ .swiftpm/
migrate_working_dir/ migrate_working_dir/
.sentry-native/
# IntelliJ related # IntelliJ related
*.iml *.iml
@@ -43,3 +44,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/*

View File

@@ -26,3 +26,8 @@ linter:
# Additional information about this file can be found at # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options
analyzer:
exclude:
- "**/*.g.dart"
- build/**
- .dart_tool/**

137
assets/i18n/en.i18n.json Normal file
View 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
View 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

View File

@@ -1,10 +0,0 @@
import 'package:isar/isar.dart';
part 'account.g.dart';
@collection
class Account {
Id id = Isar.autoIncrement;
String? name;
}

View File

@@ -1,469 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'account.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetAccountCollection on Isar {
IsarCollection<Account> get accounts => this.collection();
}
const AccountSchema = CollectionSchema(
name: r'Account',
id: -6646797162501847804,
properties: {
r'name': PropertySchema(id: 0, name: r'name', type: IsarType.string),
},
estimateSize: _accountEstimateSize,
serialize: _accountSerialize,
deserialize: _accountDeserialize,
deserializeProp: _accountDeserializeProp,
idName: r'id',
indexes: {},
links: {},
embeddedSchemas: {},
getId: _accountGetId,
getLinks: _accountGetLinks,
attach: _accountAttach,
version: '3.1.0+1',
);
int _accountEstimateSize(
Account object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
{
final value = object.name;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
}
return bytesCount;
}
void _accountSerialize(
Account object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeString(offsets[0], object.name);
}
Account _accountDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = Account();
object.id = id;
object.name = reader.readStringOrNull(offsets[0]);
return object;
}
P _accountDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readStringOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _accountGetId(Account object) {
return object.id;
}
List<IsarLinkBase<dynamic>> _accountGetLinks(Account object) {
return [];
}
void _accountAttach(IsarCollection<dynamic> col, Id id, Account object) {
object.id = id;
}
extension AccountQueryWhereSort on QueryBuilder<Account, Account, QWhere> {
QueryBuilder<Account, Account, QAfterWhere> anyId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension AccountQueryWhere on QueryBuilder<Account, Account, QWhereClause> {
QueryBuilder<Account, Account, QAfterWhereClause> idEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(lower: id, upper: id));
});
}
QueryBuilder<Account, Account, QAfterWhereClause> idNotEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
);
}
});
}
QueryBuilder<Account, Account, QAfterWhereClause> idGreaterThan(
Id id, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: include),
);
});
}
QueryBuilder<Account, Account, QAfterWhereClause> idLessThan(
Id id, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: include),
);
});
}
QueryBuilder<Account, Account, QAfterWhereClause> idBetween(
Id lowerId,
Id upperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(
lower: lowerId,
includeLower: includeLower,
upper: upperId,
includeUpper: includeUpper,
),
);
});
}
}
extension AccountQueryFilter
on QueryBuilder<Account, Account, QFilterCondition> {
QueryBuilder<Account, Account, QAfterFilterCondition> idEqualTo(Id value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'id', value: value),
);
});
}
QueryBuilder<Account, Account, QAfterFilterCondition> idGreaterThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<Account, Account, QAfterFilterCondition> idLessThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<Account, Account, QAfterFilterCondition> idBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<Account, Account, QAfterFilterCondition> nameIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNull(property: r'name'),
);
});
}
QueryBuilder<Account, Account, QAfterFilterCondition> nameIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNotNull(property: r'name'),
);
});
}
QueryBuilder<Account, Account, QAfterFilterCondition> nameEqualTo(
String? value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Account, Account, QAfterFilterCondition> nameGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Account, Account, QAfterFilterCondition> nameLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Account, Account, QAfterFilterCondition> nameBetween(
String? lower,
String? upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'name',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Account, Account, QAfterFilterCondition> nameStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.startsWith(
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Account, Account, QAfterFilterCondition> nameEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.endsWith(
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Account, Account, QAfterFilterCondition> nameContains(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.contains(
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Account, Account, QAfterFilterCondition> nameMatches(
String pattern, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.matches(
property: r'name',
wildcard: pattern,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Account, Account, QAfterFilterCondition> nameIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'name', value: ''),
);
});
}
QueryBuilder<Account, Account, QAfterFilterCondition> nameIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(property: r'name', value: ''),
);
});
}
}
extension AccountQueryObject
on QueryBuilder<Account, Account, QFilterCondition> {}
extension AccountQueryLinks
on QueryBuilder<Account, Account, QFilterCondition> {}
extension AccountQuerySortBy on QueryBuilder<Account, Account, QSortBy> {
QueryBuilder<Account, Account, QAfterSortBy> sortByName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.asc);
});
}
QueryBuilder<Account, Account, QAfterSortBy> sortByNameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.desc);
});
}
}
extension AccountQuerySortThenBy
on QueryBuilder<Account, Account, QSortThenBy> {
QueryBuilder<Account, Account, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<Account, Account, QAfterSortBy> thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<Account, Account, QAfterSortBy> thenByName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.asc);
});
}
QueryBuilder<Account, Account, QAfterSortBy> thenByNameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.desc);
});
}
}
extension AccountQueryWhereDistinct
on QueryBuilder<Account, Account, QDistinct> {
QueryBuilder<Account, Account, QDistinct> distinctByName({
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'name', caseSensitive: caseSensitive);
});
}
}
extension AccountQueryProperty
on QueryBuilder<Account, Account, QQueryProperty> {
QueryBuilder<Account, int, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
QueryBuilder<Account, String?, QQueryOperations> nameProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'name');
});
}
}

View File

@@ -1,20 +0,0 @@
import 'package:isar/isar.dart';
import 'package:okane/database/collections/account.dart';
part 'beneficiary.g.dart';
enum BeneficiaryType { account, other }
@collection
class Beneficiary {
Id id = Isar.autoIncrement;
late String name;
@Enumerated(EnumType.ordinal)
late BeneficiaryType type;
final account = IsarLink<Account>();
String? imagePath;
}

View File

@@ -1,810 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'beneficiary.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetBeneficiaryCollection on Isar {
IsarCollection<Beneficiary> get beneficiarys => this.collection();
}
const BeneficiarySchema = CollectionSchema(
name: r'Beneficiary',
id: -7106369371336791482,
properties: {
r'imagePath': PropertySchema(
id: 0,
name: r'imagePath',
type: IsarType.string,
),
r'name': PropertySchema(id: 1, name: r'name', type: IsarType.string),
r'type': PropertySchema(
id: 2,
name: r'type',
type: IsarType.byte,
enumMap: _BeneficiarytypeEnumValueMap,
),
},
estimateSize: _beneficiaryEstimateSize,
serialize: _beneficiarySerialize,
deserialize: _beneficiaryDeserialize,
deserializeProp: _beneficiaryDeserializeProp,
idName: r'id',
indexes: {},
links: {
r'account': LinkSchema(
id: -725531860126526319,
name: r'account',
target: r'Account',
single: true,
),
},
embeddedSchemas: {},
getId: _beneficiaryGetId,
getLinks: _beneficiaryGetLinks,
attach: _beneficiaryAttach,
version: '3.1.0+1',
);
int _beneficiaryEstimateSize(
Beneficiary object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
{
final value = object.imagePath;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
}
bytesCount += 3 + object.name.length * 3;
return bytesCount;
}
void _beneficiarySerialize(
Beneficiary object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeString(offsets[0], object.imagePath);
writer.writeString(offsets[1], object.name);
writer.writeByte(offsets[2], object.type.index);
}
Beneficiary _beneficiaryDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = Beneficiary();
object.id = id;
object.imagePath = reader.readStringOrNull(offsets[0]);
object.name = reader.readString(offsets[1]);
object.type =
_BeneficiarytypeValueEnumMap[reader.readByteOrNull(offsets[2])] ??
BeneficiaryType.account;
return object;
}
P _beneficiaryDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readStringOrNull(offset)) as P;
case 1:
return (reader.readString(offset)) as P;
case 2:
return (_BeneficiarytypeValueEnumMap[reader.readByteOrNull(offset)] ??
BeneficiaryType.account)
as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
const _BeneficiarytypeEnumValueMap = {'account': 0, 'other': 1};
const _BeneficiarytypeValueEnumMap = {
0: BeneficiaryType.account,
1: BeneficiaryType.other,
};
Id _beneficiaryGetId(Beneficiary object) {
return object.id;
}
List<IsarLinkBase<dynamic>> _beneficiaryGetLinks(Beneficiary object) {
return [object.account];
}
void _beneficiaryAttach(
IsarCollection<dynamic> col,
Id id,
Beneficiary object,
) {
object.id = id;
object.account.attach(col, col.isar.collection<Account>(), r'account', id);
}
extension BeneficiaryQueryWhereSort
on QueryBuilder<Beneficiary, Beneficiary, QWhere> {
QueryBuilder<Beneficiary, Beneficiary, QAfterWhere> anyId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension BeneficiaryQueryWhere
on QueryBuilder<Beneficiary, Beneficiary, QWhereClause> {
QueryBuilder<Beneficiary, Beneficiary, QAfterWhereClause> idEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(lower: id, upper: id));
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterWhereClause> idNotEqualTo(
Id id,
) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
);
}
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterWhereClause> idGreaterThan(
Id id, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: include),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterWhereClause> idLessThan(
Id id, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: include),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterWhereClause> idBetween(
Id lowerId,
Id upperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(
lower: lowerId,
includeLower: includeLower,
upper: upperId,
includeUpper: includeUpper,
),
);
});
}
}
extension BeneficiaryQueryFilter
on QueryBuilder<Beneficiary, Beneficiary, QFilterCondition> {
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition> idEqualTo(
Id value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'id', value: value),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition> idGreaterThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition> idLessThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition> idBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition>
imagePathIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNull(property: r'imagePath'),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition>
imagePathIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNotNull(property: r'imagePath'),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition>
imagePathEqualTo(String? value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(
property: r'imagePath',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition>
imagePathGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'imagePath',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition>
imagePathLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'imagePath',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition>
imagePathBetween(
String? lower,
String? upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'imagePath',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition>
imagePathStartsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.startsWith(
property: r'imagePath',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition>
imagePathEndsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.endsWith(
property: r'imagePath',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition>
imagePathContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.contains(
property: r'imagePath',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition>
imagePathMatches(String pattern, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.matches(
property: r'imagePath',
wildcard: pattern,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition>
imagePathIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'imagePath', value: ''),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition>
imagePathIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(property: r'imagePath', value: ''),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition> nameEqualTo(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition> nameGreaterThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition> nameLessThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition> nameBetween(
String lower,
String upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'name',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition> nameStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.startsWith(
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition> nameEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.endsWith(
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition> nameContains(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.contains(
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition> nameMatches(
String pattern, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.matches(
property: r'name',
wildcard: pattern,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition> nameIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'name', value: ''),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition>
nameIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(property: r'name', value: ''),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition> typeEqualTo(
BeneficiaryType value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'type', value: value),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition> typeGreaterThan(
BeneficiaryType value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'type',
value: value,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition> typeLessThan(
BeneficiaryType value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'type',
value: value,
),
);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition> typeBetween(
BeneficiaryType lower,
BeneficiaryType upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'type',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
}
extension BeneficiaryQueryObject
on QueryBuilder<Beneficiary, Beneficiary, QFilterCondition> {}
extension BeneficiaryQueryLinks
on QueryBuilder<Beneficiary, Beneficiary, QFilterCondition> {
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition> account(
FilterQuery<Account> q,
) {
return QueryBuilder.apply(this, (query) {
return query.link(q, r'account');
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterFilterCondition>
accountIsNull() {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'account', 0, true, 0, true);
});
}
}
extension BeneficiaryQuerySortBy
on QueryBuilder<Beneficiary, Beneficiary, QSortBy> {
QueryBuilder<Beneficiary, Beneficiary, QAfterSortBy> sortByImagePath() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'imagePath', Sort.asc);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterSortBy> sortByImagePathDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'imagePath', Sort.desc);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterSortBy> sortByName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.asc);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterSortBy> sortByNameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.desc);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterSortBy> sortByType() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'type', Sort.asc);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterSortBy> sortByTypeDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'type', Sort.desc);
});
}
}
extension BeneficiaryQuerySortThenBy
on QueryBuilder<Beneficiary, Beneficiary, QSortThenBy> {
QueryBuilder<Beneficiary, Beneficiary, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterSortBy> thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterSortBy> thenByImagePath() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'imagePath', Sort.asc);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterSortBy> thenByImagePathDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'imagePath', Sort.desc);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterSortBy> thenByName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.asc);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterSortBy> thenByNameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.desc);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterSortBy> thenByType() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'type', Sort.asc);
});
}
QueryBuilder<Beneficiary, Beneficiary, QAfterSortBy> thenByTypeDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'type', Sort.desc);
});
}
}
extension BeneficiaryQueryWhereDistinct
on QueryBuilder<Beneficiary, Beneficiary, QDistinct> {
QueryBuilder<Beneficiary, Beneficiary, QDistinct> distinctByImagePath({
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'imagePath', caseSensitive: caseSensitive);
});
}
QueryBuilder<Beneficiary, Beneficiary, QDistinct> distinctByName({
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'name', caseSensitive: caseSensitive);
});
}
QueryBuilder<Beneficiary, Beneficiary, QDistinct> distinctByType() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'type');
});
}
}
extension BeneficiaryQueryProperty
on QueryBuilder<Beneficiary, Beneficiary, QQueryProperty> {
QueryBuilder<Beneficiary, int, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
QueryBuilder<Beneficiary, String?, QQueryOperations> imagePathProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'imagePath');
});
}
QueryBuilder<Beneficiary, String, QQueryOperations> nameProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'name');
});
}
QueryBuilder<Beneficiary, BeneficiaryType, QQueryOperations> typeProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'type');
});
}
}

View File

@@ -1,35 +0,0 @@
import 'package:isar/isar.dart';
import 'package:okane/database/collections/expense_category.dart';
import 'account.dart';
part 'budget.g.dart';
enum BudgetPeriod { month }
@collection
class BudgetItem {
Id id = Isar.autoIncrement;
late double amount;
final expenseCategory = IsarLink<ExpenseCategory>();
}
@collection
class Budget {
Id id = Isar.autoIncrement;
@Enumerated(EnumType.ordinal)
late BudgetPeriod period;
late String name;
late double income;
late bool includeOtherSpendings;
final account = IsarLink<Account>();
final items = IsarLinks<BudgetItem>();
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +0,0 @@
import 'package:isar/isar.dart';
part 'expense_category.g.dart';
@collection
class ExpenseCategory {
Id id = Isar.autoIncrement;
late String name;
}

View File

@@ -1,451 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'expense_category.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetExpenseCategoryCollection on Isar {
IsarCollection<ExpenseCategory> get expenseCategorys => this.collection();
}
const ExpenseCategorySchema = CollectionSchema(
name: r'ExpenseCategory',
id: -6352499903118634,
properties: {
r'name': PropertySchema(id: 0, name: r'name', type: IsarType.string),
},
estimateSize: _expenseCategoryEstimateSize,
serialize: _expenseCategorySerialize,
deserialize: _expenseCategoryDeserialize,
deserializeProp: _expenseCategoryDeserializeProp,
idName: r'id',
indexes: {},
links: {},
embeddedSchemas: {},
getId: _expenseCategoryGetId,
getLinks: _expenseCategoryGetLinks,
attach: _expenseCategoryAttach,
version: '3.1.0+1',
);
int _expenseCategoryEstimateSize(
ExpenseCategory object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
bytesCount += 3 + object.name.length * 3;
return bytesCount;
}
void _expenseCategorySerialize(
ExpenseCategory object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeString(offsets[0], object.name);
}
ExpenseCategory _expenseCategoryDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = ExpenseCategory();
object.id = id;
object.name = reader.readString(offsets[0]);
return object;
}
P _expenseCategoryDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readString(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _expenseCategoryGetId(ExpenseCategory object) {
return object.id;
}
List<IsarLinkBase<dynamic>> _expenseCategoryGetLinks(ExpenseCategory object) {
return [];
}
void _expenseCategoryAttach(
IsarCollection<dynamic> col,
Id id,
ExpenseCategory object,
) {
object.id = id;
}
extension ExpenseCategoryQueryWhereSort
on QueryBuilder<ExpenseCategory, ExpenseCategory, QWhere> {
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterWhere> anyId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension ExpenseCategoryQueryWhere
on QueryBuilder<ExpenseCategory, ExpenseCategory, QWhereClause> {
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterWhereClause> idEqualTo(
Id id,
) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(lower: id, upper: id));
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterWhereClause>
idNotEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
);
}
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterWhereClause>
idGreaterThan(Id id, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: include),
);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterWhereClause> idLessThan(
Id id, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: include),
);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterWhereClause> idBetween(
Id lowerId,
Id upperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(
lower: lowerId,
includeLower: includeLower,
upper: upperId,
includeUpper: includeUpper,
),
);
});
}
}
extension ExpenseCategoryQueryFilter
on QueryBuilder<ExpenseCategory, ExpenseCategory, QFilterCondition> {
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterFilterCondition>
idEqualTo(Id value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'id', value: value),
);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterFilterCondition>
idGreaterThan(Id value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterFilterCondition>
idLessThan(Id value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterFilterCondition>
idBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterFilterCondition>
nameEqualTo(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterFilterCondition>
nameGreaterThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterFilterCondition>
nameLessThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterFilterCondition>
nameBetween(
String lower,
String upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'name',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterFilterCondition>
nameStartsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.startsWith(
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterFilterCondition>
nameEndsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.endsWith(
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterFilterCondition>
nameContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.contains(
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterFilterCondition>
nameMatches(String pattern, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.matches(
property: r'name',
wildcard: pattern,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterFilterCondition>
nameIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'name', value: ''),
);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterFilterCondition>
nameIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(property: r'name', value: ''),
);
});
}
}
extension ExpenseCategoryQueryObject
on QueryBuilder<ExpenseCategory, ExpenseCategory, QFilterCondition> {}
extension ExpenseCategoryQueryLinks
on QueryBuilder<ExpenseCategory, ExpenseCategory, QFilterCondition> {}
extension ExpenseCategoryQuerySortBy
on QueryBuilder<ExpenseCategory, ExpenseCategory, QSortBy> {
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterSortBy> sortByName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.asc);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterSortBy>
sortByNameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.desc);
});
}
}
extension ExpenseCategoryQuerySortThenBy
on QueryBuilder<ExpenseCategory, ExpenseCategory, QSortThenBy> {
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterSortBy> thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterSortBy> thenByName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.asc);
});
}
QueryBuilder<ExpenseCategory, ExpenseCategory, QAfterSortBy>
thenByNameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.desc);
});
}
}
extension ExpenseCategoryQueryWhereDistinct
on QueryBuilder<ExpenseCategory, ExpenseCategory, QDistinct> {
QueryBuilder<ExpenseCategory, ExpenseCategory, QDistinct> distinctByName({
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'name', caseSensitive: caseSensitive);
});
}
}
extension ExpenseCategoryQueryProperty
on QueryBuilder<ExpenseCategory, ExpenseCategory, QQueryProperty> {
QueryBuilder<ExpenseCategory, int, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
QueryBuilder<ExpenseCategory, String, QQueryOperations> nameProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'name');
});
}
}

View File

@@ -1,18 +0,0 @@
import 'package:isar/isar.dart';
import 'package:okane/database/collections/account.dart';
import 'package:okane/database/collections/template.dart';
part 'recurrent.g.dart';
@collection
class RecurringTransaction {
Id id = Isar.autoIncrement;
late int days;
DateTime? lastExecution;
final template = IsarLink<TransactionTemplate>();
final account = IsarLink<Account>();
}

View File

@@ -1,633 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'recurrent.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetRecurringTransactionCollection on Isar {
IsarCollection<RecurringTransaction> get recurringTransactions =>
this.collection();
}
const RecurringTransactionSchema = CollectionSchema(
name: r'RecurringTransaction',
id: 969840479390105118,
properties: {
r'days': PropertySchema(id: 0, name: r'days', type: IsarType.long),
r'lastExecution': PropertySchema(
id: 1,
name: r'lastExecution',
type: IsarType.dateTime,
),
},
estimateSize: _recurringTransactionEstimateSize,
serialize: _recurringTransactionSerialize,
deserialize: _recurringTransactionDeserialize,
deserializeProp: _recurringTransactionDeserializeProp,
idName: r'id',
indexes: {},
links: {
r'template': LinkSchema(
id: -8891369755965227865,
name: r'template',
target: r'TransactionTemplate',
single: true,
),
r'account': LinkSchema(
id: -6028551496614242115,
name: r'account',
target: r'Account',
single: true,
),
},
embeddedSchemas: {},
getId: _recurringTransactionGetId,
getLinks: _recurringTransactionGetLinks,
attach: _recurringTransactionAttach,
version: '3.1.0+1',
);
int _recurringTransactionEstimateSize(
RecurringTransaction object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
return bytesCount;
}
void _recurringTransactionSerialize(
RecurringTransaction object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeLong(offsets[0], object.days);
writer.writeDateTime(offsets[1], object.lastExecution);
}
RecurringTransaction _recurringTransactionDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = RecurringTransaction();
object.days = reader.readLong(offsets[0]);
object.id = id;
object.lastExecution = reader.readDateTimeOrNull(offsets[1]);
return object;
}
P _recurringTransactionDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readLong(offset)) as P;
case 1:
return (reader.readDateTimeOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _recurringTransactionGetId(RecurringTransaction object) {
return object.id;
}
List<IsarLinkBase<dynamic>> _recurringTransactionGetLinks(
RecurringTransaction object,
) {
return [object.template, object.account];
}
void _recurringTransactionAttach(
IsarCollection<dynamic> col,
Id id,
RecurringTransaction object,
) {
object.id = id;
object.template.attach(
col,
col.isar.collection<TransactionTemplate>(),
r'template',
id,
);
object.account.attach(col, col.isar.collection<Account>(), r'account', id);
}
extension RecurringTransactionQueryWhereSort
on QueryBuilder<RecurringTransaction, RecurringTransaction, QWhere> {
QueryBuilder<RecurringTransaction, RecurringTransaction, QAfterWhere>
anyId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension RecurringTransactionQueryWhere
on QueryBuilder<RecurringTransaction, RecurringTransaction, QWhereClause> {
QueryBuilder<RecurringTransaction, RecurringTransaction, QAfterWhereClause>
idEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(lower: id, upper: id));
});
}
QueryBuilder<RecurringTransaction, RecurringTransaction, QAfterWhereClause>
idNotEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
);
}
});
}
QueryBuilder<RecurringTransaction, RecurringTransaction, QAfterWhereClause>
idGreaterThan(Id id, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: include),
);
});
}
QueryBuilder<RecurringTransaction, RecurringTransaction, QAfterWhereClause>
idLessThan(Id id, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: include),
);
});
}
QueryBuilder<RecurringTransaction, RecurringTransaction, QAfterWhereClause>
idBetween(
Id lowerId,
Id upperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(
lower: lowerId,
includeLower: includeLower,
upper: upperId,
includeUpper: includeUpper,
),
);
});
}
}
extension RecurringTransactionQueryFilter
on
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QFilterCondition
> {
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QAfterFilterCondition
>
daysEqualTo(int value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'days', value: value),
);
});
}
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QAfterFilterCondition
>
daysGreaterThan(int value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'days',
value: value,
),
);
});
}
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QAfterFilterCondition
>
daysLessThan(int value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'days',
value: value,
),
);
});
}
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QAfterFilterCondition
>
daysBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'days',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QAfterFilterCondition
>
idEqualTo(Id value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'id', value: value),
);
});
}
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QAfterFilterCondition
>
idGreaterThan(Id value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QAfterFilterCondition
>
idLessThan(Id value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QAfterFilterCondition
>
idBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QAfterFilterCondition
>
lastExecutionIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNull(property: r'lastExecution'),
);
});
}
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QAfterFilterCondition
>
lastExecutionIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNotNull(property: r'lastExecution'),
);
});
}
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QAfterFilterCondition
>
lastExecutionEqualTo(DateTime? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'lastExecution', value: value),
);
});
}
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QAfterFilterCondition
>
lastExecutionGreaterThan(DateTime? value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'lastExecution',
value: value,
),
);
});
}
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QAfterFilterCondition
>
lastExecutionLessThan(DateTime? value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'lastExecution',
value: value,
),
);
});
}
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QAfterFilterCondition
>
lastExecutionBetween(
DateTime? lower,
DateTime? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'lastExecution',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
}
extension RecurringTransactionQueryObject
on
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QFilterCondition
> {}
extension RecurringTransactionQueryLinks
on
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QFilterCondition
> {
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QAfterFilterCondition
>
template(FilterQuery<TransactionTemplate> q) {
return QueryBuilder.apply(this, (query) {
return query.link(q, r'template');
});
}
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QAfterFilterCondition
>
templateIsNull() {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'template', 0, true, 0, true);
});
}
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QAfterFilterCondition
>
account(FilterQuery<Account> q) {
return QueryBuilder.apply(this, (query) {
return query.link(q, r'account');
});
}
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QAfterFilterCondition
>
accountIsNull() {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'account', 0, true, 0, true);
});
}
}
extension RecurringTransactionQuerySortBy
on QueryBuilder<RecurringTransaction, RecurringTransaction, QSortBy> {
QueryBuilder<RecurringTransaction, RecurringTransaction, QAfterSortBy>
sortByDays() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'days', Sort.asc);
});
}
QueryBuilder<RecurringTransaction, RecurringTransaction, QAfterSortBy>
sortByDaysDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'days', Sort.desc);
});
}
QueryBuilder<RecurringTransaction, RecurringTransaction, QAfterSortBy>
sortByLastExecution() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'lastExecution', Sort.asc);
});
}
QueryBuilder<RecurringTransaction, RecurringTransaction, QAfterSortBy>
sortByLastExecutionDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'lastExecution', Sort.desc);
});
}
}
extension RecurringTransactionQuerySortThenBy
on QueryBuilder<RecurringTransaction, RecurringTransaction, QSortThenBy> {
QueryBuilder<RecurringTransaction, RecurringTransaction, QAfterSortBy>
thenByDays() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'days', Sort.asc);
});
}
QueryBuilder<RecurringTransaction, RecurringTransaction, QAfterSortBy>
thenByDaysDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'days', Sort.desc);
});
}
QueryBuilder<RecurringTransaction, RecurringTransaction, QAfterSortBy>
thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<RecurringTransaction, RecurringTransaction, QAfterSortBy>
thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<RecurringTransaction, RecurringTransaction, QAfterSortBy>
thenByLastExecution() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'lastExecution', Sort.asc);
});
}
QueryBuilder<RecurringTransaction, RecurringTransaction, QAfterSortBy>
thenByLastExecutionDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'lastExecution', Sort.desc);
});
}
}
extension RecurringTransactionQueryWhereDistinct
on QueryBuilder<RecurringTransaction, RecurringTransaction, QDistinct> {
QueryBuilder<RecurringTransaction, RecurringTransaction, QDistinct>
distinctByDays() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'days');
});
}
QueryBuilder<RecurringTransaction, RecurringTransaction, QDistinct>
distinctByLastExecution() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'lastExecution');
});
}
}
extension RecurringTransactionQueryProperty
on
QueryBuilder<
RecurringTransaction,
RecurringTransaction,
QQueryProperty
> {
QueryBuilder<RecurringTransaction, int, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
QueryBuilder<RecurringTransaction, int, QQueryOperations> daysProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'days');
});
}
QueryBuilder<RecurringTransaction, DateTime?, QQueryOperations>
lastExecutionProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'lastExecution');
});
}
}

View File

@@ -1,23 +0,0 @@
import 'package:isar/isar.dart';
import 'package:okane/database/collections/account.dart';
import 'package:okane/database/collections/beneficiary.dart';
import 'package:okane/database/collections/expense_category.dart';
part 'template.g.dart';
@collection
class TransactionTemplate {
Id id = Isar.autoIncrement;
late String name;
late double amount;
late bool recurring;
final expenseCategory = IsarLink<ExpenseCategory>();
final beneficiary = IsarLink<Beneficiary>();
final account = IsarLink<Account>();
}

View File

@@ -1,721 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'template.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetTransactionTemplateCollection on Isar {
IsarCollection<TransactionTemplate> get transactionTemplates =>
this.collection();
}
const TransactionTemplateSchema = CollectionSchema(
name: r'TransactionTemplate',
id: -2324989530163310644,
properties: {
r'amount': PropertySchema(id: 0, name: r'amount', type: IsarType.double),
r'name': PropertySchema(id: 1, name: r'name', type: IsarType.string),
r'recurring': PropertySchema(
id: 2,
name: r'recurring',
type: IsarType.bool,
),
},
estimateSize: _transactionTemplateEstimateSize,
serialize: _transactionTemplateSerialize,
deserialize: _transactionTemplateDeserialize,
deserializeProp: _transactionTemplateDeserializeProp,
idName: r'id',
indexes: {},
links: {
r'expenseCategory': LinkSchema(
id: 3013186211408715712,
name: r'expenseCategory',
target: r'ExpenseCategory',
single: true,
),
r'beneficiary': LinkSchema(
id: -7565656011019083791,
name: r'beneficiary',
target: r'Beneficiary',
single: true,
),
r'account': LinkSchema(
id: 2465433941426054606,
name: r'account',
target: r'Account',
single: true,
),
},
embeddedSchemas: {},
getId: _transactionTemplateGetId,
getLinks: _transactionTemplateGetLinks,
attach: _transactionTemplateAttach,
version: '3.1.0+1',
);
int _transactionTemplateEstimateSize(
TransactionTemplate object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
bytesCount += 3 + object.name.length * 3;
return bytesCount;
}
void _transactionTemplateSerialize(
TransactionTemplate object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeDouble(offsets[0], object.amount);
writer.writeString(offsets[1], object.name);
writer.writeBool(offsets[2], object.recurring);
}
TransactionTemplate _transactionTemplateDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = TransactionTemplate();
object.amount = reader.readDouble(offsets[0]);
object.id = id;
object.name = reader.readString(offsets[1]);
object.recurring = reader.readBool(offsets[2]);
return object;
}
P _transactionTemplateDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readDouble(offset)) as P;
case 1:
return (reader.readString(offset)) as P;
case 2:
return (reader.readBool(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _transactionTemplateGetId(TransactionTemplate object) {
return object.id;
}
List<IsarLinkBase<dynamic>> _transactionTemplateGetLinks(
TransactionTemplate object,
) {
return [object.expenseCategory, object.beneficiary, object.account];
}
void _transactionTemplateAttach(
IsarCollection<dynamic> col,
Id id,
TransactionTemplate object,
) {
object.id = id;
object.expenseCategory.attach(
col,
col.isar.collection<ExpenseCategory>(),
r'expenseCategory',
id,
);
object.beneficiary.attach(
col,
col.isar.collection<Beneficiary>(),
r'beneficiary',
id,
);
object.account.attach(col, col.isar.collection<Account>(), r'account', id);
}
extension TransactionTemplateQueryWhereSort
on QueryBuilder<TransactionTemplate, TransactionTemplate, QWhere> {
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterWhere> anyId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension TransactionTemplateQueryWhere
on QueryBuilder<TransactionTemplate, TransactionTemplate, QWhereClause> {
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterWhereClause>
idEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(lower: id, upper: id));
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterWhereClause>
idNotEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
);
}
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterWhereClause>
idGreaterThan(Id id, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: include),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterWhereClause>
idLessThan(Id id, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: include),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterWhereClause>
idBetween(
Id lowerId,
Id upperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(
lower: lowerId,
includeLower: includeLower,
upper: upperId,
includeUpper: includeUpper,
),
);
});
}
}
extension TransactionTemplateQueryFilter
on
QueryBuilder<
TransactionTemplate,
TransactionTemplate,
QFilterCondition
> {
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
amountEqualTo(double value, {double epsilon = Query.epsilon}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(
property: r'amount',
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
amountGreaterThan(
double value, {
bool include = false,
double epsilon = Query.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'amount',
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
amountLessThan(
double value, {
bool include = false,
double epsilon = Query.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'amount',
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
amountBetween(
double lower,
double upper, {
bool includeLower = true,
bool includeUpper = true,
double epsilon = Query.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'amount',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
epsilon: epsilon,
),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
idEqualTo(Id value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'id', value: value),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
idGreaterThan(Id value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
idLessThan(Id value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
idBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
nameEqualTo(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
nameGreaterThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
nameLessThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
nameBetween(
String lower,
String upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'name',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
nameStartsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.startsWith(
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
nameEndsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.endsWith(
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
nameContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.contains(
property: r'name',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
nameMatches(String pattern, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.matches(
property: r'name',
wildcard: pattern,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
nameIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'name', value: ''),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
nameIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(property: r'name', value: ''),
);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
recurringEqualTo(bool value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'recurring', value: value),
);
});
}
}
extension TransactionTemplateQueryObject
on
QueryBuilder<
TransactionTemplate,
TransactionTemplate,
QFilterCondition
> {}
extension TransactionTemplateQueryLinks
on
QueryBuilder<
TransactionTemplate,
TransactionTemplate,
QFilterCondition
> {
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
expenseCategory(FilterQuery<ExpenseCategory> q) {
return QueryBuilder.apply(this, (query) {
return query.link(q, r'expenseCategory');
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
expenseCategoryIsNull() {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'expenseCategory', 0, true, 0, true);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
beneficiary(FilterQuery<Beneficiary> q) {
return QueryBuilder.apply(this, (query) {
return query.link(q, r'beneficiary');
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
beneficiaryIsNull() {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'beneficiary', 0, true, 0, true);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
account(FilterQuery<Account> q) {
return QueryBuilder.apply(this, (query) {
return query.link(q, r'account');
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterFilterCondition>
accountIsNull() {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'account', 0, true, 0, true);
});
}
}
extension TransactionTemplateQuerySortBy
on QueryBuilder<TransactionTemplate, TransactionTemplate, QSortBy> {
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterSortBy>
sortByAmount() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'amount', Sort.asc);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterSortBy>
sortByAmountDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'amount', Sort.desc);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterSortBy>
sortByName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.asc);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterSortBy>
sortByNameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.desc);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterSortBy>
sortByRecurring() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'recurring', Sort.asc);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterSortBy>
sortByRecurringDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'recurring', Sort.desc);
});
}
}
extension TransactionTemplateQuerySortThenBy
on QueryBuilder<TransactionTemplate, TransactionTemplate, QSortThenBy> {
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterSortBy>
thenByAmount() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'amount', Sort.asc);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterSortBy>
thenByAmountDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'amount', Sort.desc);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterSortBy>
thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterSortBy>
thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterSortBy>
thenByName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.asc);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterSortBy>
thenByNameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'name', Sort.desc);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterSortBy>
thenByRecurring() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'recurring', Sort.asc);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QAfterSortBy>
thenByRecurringDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'recurring', Sort.desc);
});
}
}
extension TransactionTemplateQueryWhereDistinct
on QueryBuilder<TransactionTemplate, TransactionTemplate, QDistinct> {
QueryBuilder<TransactionTemplate, TransactionTemplate, QDistinct>
distinctByAmount() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'amount');
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QDistinct>
distinctByName({bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'name', caseSensitive: caseSensitive);
});
}
QueryBuilder<TransactionTemplate, TransactionTemplate, QDistinct>
distinctByRecurring() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'recurring');
});
}
}
extension TransactionTemplateQueryProperty
on QueryBuilder<TransactionTemplate, TransactionTemplate, QQueryProperty> {
QueryBuilder<TransactionTemplate, int, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
QueryBuilder<TransactionTemplate, double, QQueryOperations> amountProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'amount');
});
}
QueryBuilder<TransactionTemplate, String, QQueryOperations> nameProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'name');
});
}
QueryBuilder<TransactionTemplate, bool, QQueryOperations>
recurringProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'recurring');
});
}
}

View File

@@ -1,23 +0,0 @@
import 'package:isar/isar.dart';
import 'package:okane/database/collections/account.dart';
import 'package:okane/database/collections/beneficiary.dart';
import 'package:okane/database/collections/expense_category.dart';
part 'transaction.g.dart';
@collection
class Transaction {
Id id = Isar.autoIncrement;
late double amount;
late List<String> tags;
late DateTime date;
final expenseCategory = IsarLink<ExpenseCategory>();
final account = IsarLink<Account>();
final beneficiary = IsarLink<Beneficiary>();
}

View File

@@ -1,775 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'transaction.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetTransactionCollection on Isar {
IsarCollection<Transaction> get transactions => this.collection();
}
const TransactionSchema = CollectionSchema(
name: r'Transaction',
id: 5320225499417954855,
properties: {
r'amount': PropertySchema(id: 0, name: r'amount', type: IsarType.double),
r'date': PropertySchema(id: 1, name: r'date', type: IsarType.dateTime),
r'tags': PropertySchema(id: 2, name: r'tags', type: IsarType.stringList),
},
estimateSize: _transactionEstimateSize,
serialize: _transactionSerialize,
deserialize: _transactionDeserialize,
deserializeProp: _transactionDeserializeProp,
idName: r'id',
indexes: {},
links: {
r'expenseCategory': LinkSchema(
id: 490804775908778298,
name: r'expenseCategory',
target: r'ExpenseCategory',
single: true,
),
r'account': LinkSchema(
id: -8467990729867616553,
name: r'account',
target: r'Account',
single: true,
),
r'beneficiary': LinkSchema(
id: -1184196133247909686,
name: r'beneficiary',
target: r'Beneficiary',
single: true,
),
},
embeddedSchemas: {},
getId: _transactionGetId,
getLinks: _transactionGetLinks,
attach: _transactionAttach,
version: '3.1.0+1',
);
int _transactionEstimateSize(
Transaction object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
bytesCount += 3 + object.tags.length * 3;
{
for (var i = 0; i < object.tags.length; i++) {
final value = object.tags[i];
bytesCount += value.length * 3;
}
}
return bytesCount;
}
void _transactionSerialize(
Transaction object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeDouble(offsets[0], object.amount);
writer.writeDateTime(offsets[1], object.date);
writer.writeStringList(offsets[2], object.tags);
}
Transaction _transactionDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = Transaction();
object.amount = reader.readDouble(offsets[0]);
object.date = reader.readDateTime(offsets[1]);
object.id = id;
object.tags = reader.readStringList(offsets[2]) ?? [];
return object;
}
P _transactionDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readDouble(offset)) as P;
case 1:
return (reader.readDateTime(offset)) as P;
case 2:
return (reader.readStringList(offset) ?? []) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _transactionGetId(Transaction object) {
return object.id;
}
List<IsarLinkBase<dynamic>> _transactionGetLinks(Transaction object) {
return [object.expenseCategory, object.account, object.beneficiary];
}
void _transactionAttach(
IsarCollection<dynamic> col,
Id id,
Transaction object,
) {
object.id = id;
object.expenseCategory.attach(
col,
col.isar.collection<ExpenseCategory>(),
r'expenseCategory',
id,
);
object.account.attach(col, col.isar.collection<Account>(), r'account', id);
object.beneficiary.attach(
col,
col.isar.collection<Beneficiary>(),
r'beneficiary',
id,
);
}
extension TransactionQueryWhereSort
on QueryBuilder<Transaction, Transaction, QWhere> {
QueryBuilder<Transaction, Transaction, QAfterWhere> anyId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension TransactionQueryWhere
on QueryBuilder<Transaction, Transaction, QWhereClause> {
QueryBuilder<Transaction, Transaction, QAfterWhereClause> idEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(lower: id, upper: id));
});
}
QueryBuilder<Transaction, Transaction, QAfterWhereClause> idNotEqualTo(
Id id,
) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
);
}
});
}
QueryBuilder<Transaction, Transaction, QAfterWhereClause> idGreaterThan(
Id id, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: include),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterWhereClause> idLessThan(
Id id, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: include),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterWhereClause> idBetween(
Id lowerId,
Id upperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(
lower: lowerId,
includeLower: includeLower,
upper: upperId,
includeUpper: includeUpper,
),
);
});
}
}
extension TransactionQueryFilter
on QueryBuilder<Transaction, Transaction, QFilterCondition> {
QueryBuilder<Transaction, Transaction, QAfterFilterCondition> amountEqualTo(
double value, {
double epsilon = Query.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(
property: r'amount',
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
amountGreaterThan(
double value, {
bool include = false,
double epsilon = Query.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'amount',
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition> amountLessThan(
double value, {
bool include = false,
double epsilon = Query.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'amount',
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition> amountBetween(
double lower,
double upper, {
bool includeLower = true,
bool includeUpper = true,
double epsilon = Query.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'amount',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
epsilon: epsilon,
),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition> dateEqualTo(
DateTime value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'date', value: value),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition> dateGreaterThan(
DateTime value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'date',
value: value,
),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition> dateLessThan(
DateTime value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'date',
value: value,
),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition> dateBetween(
DateTime lower,
DateTime upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'date',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition> idEqualTo(
Id value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'id', value: value),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition> idGreaterThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition> idLessThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition> idBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
tagsElementEqualTo(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(
property: r'tags',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
tagsElementGreaterThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'tags',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
tagsElementLessThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'tags',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
tagsElementBetween(
String lower,
String upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'tags',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
tagsElementStartsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.startsWith(
property: r'tags',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
tagsElementEndsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.endsWith(
property: r'tags',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
tagsElementContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.contains(
property: r'tags',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
tagsElementMatches(String pattern, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.matches(
property: r'tags',
wildcard: pattern,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
tagsElementIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'tags', value: ''),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
tagsElementIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(property: r'tags', value: ''),
);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
tagsLengthEqualTo(int length) {
return QueryBuilder.apply(this, (query) {
return query.listLength(r'tags', length, true, length, true);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition> tagsIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.listLength(r'tags', 0, true, 0, true);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
tagsIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.listLength(r'tags', 0, false, 999999, true);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
tagsLengthLessThan(int length, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(r'tags', 0, true, length, include);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
tagsLengthGreaterThan(int length, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(r'tags', length, include, 999999, true);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
tagsLengthBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'tags',
lower,
includeLower,
upper,
includeUpper,
);
});
}
}
extension TransactionQueryObject
on QueryBuilder<Transaction, Transaction, QFilterCondition> {}
extension TransactionQueryLinks
on QueryBuilder<Transaction, Transaction, QFilterCondition> {
QueryBuilder<Transaction, Transaction, QAfterFilterCondition> expenseCategory(
FilterQuery<ExpenseCategory> q,
) {
return QueryBuilder.apply(this, (query) {
return query.link(q, r'expenseCategory');
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
expenseCategoryIsNull() {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'expenseCategory', 0, true, 0, true);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition> account(
FilterQuery<Account> q,
) {
return QueryBuilder.apply(this, (query) {
return query.link(q, r'account');
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
accountIsNull() {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'account', 0, true, 0, true);
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition> beneficiary(
FilterQuery<Beneficiary> q,
) {
return QueryBuilder.apply(this, (query) {
return query.link(q, r'beneficiary');
});
}
QueryBuilder<Transaction, Transaction, QAfterFilterCondition>
beneficiaryIsNull() {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'beneficiary', 0, true, 0, true);
});
}
}
extension TransactionQuerySortBy
on QueryBuilder<Transaction, Transaction, QSortBy> {
QueryBuilder<Transaction, Transaction, QAfterSortBy> sortByAmount() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'amount', Sort.asc);
});
}
QueryBuilder<Transaction, Transaction, QAfterSortBy> sortByAmountDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'amount', Sort.desc);
});
}
QueryBuilder<Transaction, Transaction, QAfterSortBy> sortByDate() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'date', Sort.asc);
});
}
QueryBuilder<Transaction, Transaction, QAfterSortBy> sortByDateDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'date', Sort.desc);
});
}
}
extension TransactionQuerySortThenBy
on QueryBuilder<Transaction, Transaction, QSortThenBy> {
QueryBuilder<Transaction, Transaction, QAfterSortBy> thenByAmount() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'amount', Sort.asc);
});
}
QueryBuilder<Transaction, Transaction, QAfterSortBy> thenByAmountDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'amount', Sort.desc);
});
}
QueryBuilder<Transaction, Transaction, QAfterSortBy> thenByDate() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'date', Sort.asc);
});
}
QueryBuilder<Transaction, Transaction, QAfterSortBy> thenByDateDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'date', Sort.desc);
});
}
QueryBuilder<Transaction, Transaction, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<Transaction, Transaction, QAfterSortBy> thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
}
extension TransactionQueryWhereDistinct
on QueryBuilder<Transaction, Transaction, QDistinct> {
QueryBuilder<Transaction, Transaction, QDistinct> distinctByAmount() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'amount');
});
}
QueryBuilder<Transaction, Transaction, QDistinct> distinctByDate() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'date');
});
}
QueryBuilder<Transaction, Transaction, QDistinct> distinctByTags() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'tags');
});
}
}
extension TransactionQueryProperty
on QueryBuilder<Transaction, Transaction, QQueryProperty> {
QueryBuilder<Transaction, int, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
QueryBuilder<Transaction, double, QQueryOperations> amountProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'amount');
});
}
QueryBuilder<Transaction, DateTime, QQueryOperations> dateProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'date');
});
}
QueryBuilder<Transaction, List<String>, QQueryOperations> tagsProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'tags');
});
}
}

View File

@@ -1,277 +0,0 @@
import 'dart:async';
import 'package:isar/isar.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/expense_category.dart';
import 'package:okane/database/collections/recurrent.dart';
import 'package:okane/database/collections/template.dart';
import 'package:okane/database/collections/transaction.dart';
import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/utils.dart';
import 'package:path_provider/path_provider.dart';
import 'collections/budget.dart';
Future<Isar> openDatabase() async {
final dir = await getApplicationDocumentsDirectory();
return Isar.open([
AccountSchema,
BeneficiarySchema,
TransactionSchema,
TransactionTemplateSchema,
RecurringTransactionSchema,
ExpenseCategorySchema,
BudgetSchema,
BudgetItemSchema,
], directory: dir.path);
}
Future<List<Account>> getAccounts() {
return GetIt.I.get<Isar>().accounts.where().findAll();
}
Future<double> getTotalBalance(Account account) async {
return GetIt.I
.get<Isar>()
.transactions
.filter()
.account((q) => q.idEqualTo(account.id))
.amountProperty()
.sum();
}
Future<List<Transaction>> getLastTransactions(
Account account,
DateTime today,
int days,
) async {
return GetIt.I
.get<Isar>()
.transactions
.filter()
.account((q) => q.idEqualTo(account.id))
.dateGreaterThan(toMidnight(today.subtract(Duration(days: days))))
.findAll();
}
Future<List<RecurringTransaction>> getRecurringTransactions(Account? account) {
if (account == null) {
return Future.value([]);
}
return GetIt.I
.get<Isar>()
.recurringTransactions
.filter()
.account((q) => q.idEqualTo(account.id))
.findAll();
}
Stream<void> watchRecurringTransactions(Account account) {
final account = GetIt.I.get<CoreCubit>().activeAccount!;
return GetIt.I
.get<Isar>()
.recurringTransactions
.filter()
.account((q) => q.idEqualTo(account.id))
.build()
.watchLazy(fireImmediately: true);
}
Future<void> upsertAccount(Account account) async {
final db = GetIt.I.get<Isar>();
return db.writeTxn(() async {
print("Before account insert");
final id = await db.accounts.put(account);
print("After account insert: $id");
});
}
Future<void> upsertBeneficiary(Beneficiary beneficiary) async {
final db = GetIt.I.get<Isar>();
return db.writeTxn(() async {
await db.beneficiarys.put(beneficiary);
await beneficiary.account.save();
});
}
Future<Beneficiary?> getAccountBeneficiary(Account account) {
return GetIt.I
.get<Isar>()
.beneficiarys
.filter()
.account((q) => q.idEqualTo(account.id))
.findFirst();
}
Future<void> upsertTransactionTemplate(TransactionTemplate template) async {
final db = GetIt.I.get<Isar>();
return db.writeTxn(() async {
await db.transactionTemplates.put(template);
await template.beneficiary.save();
await template.account.save();
await template.expenseCategory.save();
});
}
Future<void> upsertRecurringTransaction(RecurringTransaction template) async {
final db = GetIt.I.get<Isar>();
return db.writeTxn(() async {
await db.recurringTransactions.put(template);
await template.template.save();
await template.account.save();
});
}
Future<void> upsertTransaction(Transaction transaction) async {
final db = GetIt.I.get<Isar>();
return db.writeTxn(() async {
await db.transactions.put(transaction);
await transaction.beneficiary.save();
await transaction.account.save();
await transaction.expenseCategory.save();
});
}
Stream<void> watchAccounts() {
return GetIt.I.get<Isar>().accounts.watchLazy();
}
Stream<void> watchTransactionTemplates(Account account) {
return GetIt.I
.get<Isar>()
.transactionTemplates
.filter()
.account((q) => q.idEqualTo(account.id))
.recurringEqualTo(false)
.watchLazy(fireImmediately: true);
}
Future<List<TransactionTemplate>> getTransactionTemplates(Account? account) {
if (account == null) {
return Future.value([]);
}
return GetIt.I
.get<Isar>()
.transactionTemplates
.filter()
.account((q) => q.idEqualTo(account.id))
.recurringEqualTo(false)
.findAll();
}
Stream<void> watchTransactions(Account account) {
return GetIt.I
.get<Isar>()
.transactions
.filter()
.account((q) => q.idEqualTo(account.id))
.watchLazy(fireImmediately: true);
}
Future<List<Transaction>> getTransactions(Account? account) {
if (account == null) {
return Future.value([]);
}
return GetIt.I
.get<Isar>()
.transactions
.filter()
.account((q) => q.idEqualTo(account.id))
.findAll();
}
Stream<void> watchBeneficiaries() {
return GetIt.I.get<Isar>().beneficiarys.watchLazy(fireImmediately: true);
}
Future<List<Beneficiary>> getBeneficiaries() {
return GetIt.I.get<Isar>().beneficiarys.where().findAll();
}
Stream<Beneficiary?> watchBeneficiaryObject(Id id) {
return GetIt.I.get<Isar>().beneficiarys.watchObject(id);
}
Future<void> upsertExpenseCategory(ExpenseCategory category) {
final db = GetIt.I.get<Isar>();
return db.writeTxn(() => db.expenseCategorys.put(category));
}
Future<List<ExpenseCategory>> getExpenseCategories() {
return GetIt.I.get<Isar>().expenseCategorys.where().findAll();
}
Stream<void> watchExpenseCategory() {
return GetIt.I.get<Isar>().expenseCategorys.watchLazy(fireImmediately: true);
}
Stream<void> watchBudgets(Account account) {
return GetIt.I
.get<Isar>()
.budgets
.filter()
.account((q) => q.idEqualTo(account.id))
.watchLazy(fireImmediately: true);
}
Future<List<Budget>> getBudgets(Account? account) {
if (account == null) {
return Future.value([]);
}
return GetIt.I
.get<Isar>()
.budgets
.filter()
.account((q) => q.idEqualTo(account.id))
.findAll();
}
Future<void> upsertBudget(Budget budget) {
final db = GetIt.I.get<Isar>();
return db.writeTxn(() async {
await db.budgets.put(budget);
await budget.items.save();
await budget.account.save();
});
}
Future<void> upsertBudgetItem(BudgetItem item) {
final db = GetIt.I.get<Isar>();
return db.writeTxn(() async {
await db.budgetItems.put(item);
await item.expenseCategory.save();
});
}
enum TransactionQueryDateOption { thisMonth }
Future<List<Transaction>> getTransactionsInTimeframe(
Account account,
DateTime today,
TransactionQueryDateOption option,
) async {
final lower = switch (option) {
TransactionQueryDateOption.thisMonth => DateTime(
today.year,
today.month,
0,
),
};
final upper = switch (option) {
TransactionQueryDateOption.thisMonth => monthEnding(today),
};
return GetIt.I
.get<Isar>()
.transactions
.filter()
.account((q) => q.idEqualTo(account.id))
.dateBetween(lower, upper)
.findAll();
}

690
lib/database/sqlite.dart Normal file
View File

@@ -0,0 +1,690 @@
import 'dart:async';
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:okane/ui/utils.dart';
import 'package:path_provider/path_provider.dart';
part 'sqlite.g.dart';
class Accounts extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
}
enum BeneficiaryType { account, other }
class Beneficiaries extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().unique()();
TextColumn get type => textEnum<BeneficiaryType>()();
IntColumn get accountId => integer().nullable().references(Accounts, #id)();
TextColumn get imagePath => text().nullable()();
}
class ExpenseCategories extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
}
enum BudgetPeriod { month }
class BudgetItems extends Table {
IntColumn get id => integer().autoIncrement()();
RealColumn get amount => real()();
IntColumn get expenseCategoryId =>
integer().references(ExpenseCategories, #id)();
IntColumn get budgetId => integer().references(Budgets, #id)();
}
class BudgetItemDto {
final BudgetItem item;
final ExpenseCategory expenseCategory;
BudgetItemDto({required this.item, required this.expenseCategory});
}
class Budgets extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get period => textEnum<BudgetPeriod>()();
TextColumn get name => text()();
RealColumn get income => real()();
BoolColumn get includeOtherSpendings => boolean()();
IntColumn get accountId => integer().references(Accounts, #id)();
}
class BudgetsDto {
final Budget budget;
BudgetsDto({required this.budget});
}
class Loans extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get beneficiaryId => integer().references(Beneficiaries, #id)();
}
class LoanChanges extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get loanId => integer().references(Loans, #id)();
RealColumn get amount => real()();
DateTimeColumn get date => dateTime()();
}
class LoanDto {
final Loan loan;
final Beneficiary beneficiary;
LoanDto({required this.loan, required this.beneficiary});
}
class RecurringTransactions extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get days => integer()();
DateTimeColumn get lastExecution => dateTime().nullable()();
IntColumn get templateId => integer().references(TransactionTemplates, #id)();
IntColumn get accountId => integer().references(Accounts, #id)();
}
typedef RecurringTransactionDto =
({
RecurringTransaction recurring,
Beneficiary beneficiary,
TransactionTemplate template,
});
class TransactionTemplates extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
RealColumn get amount => real()();
BoolColumn get recurring => boolean()();
IntColumn get expenseCategoryId =>
integer().nullable().references(ExpenseCategories, #id)();
IntColumn get beneficiaryId => integer().references(Beneficiaries, #id)();
IntColumn get accountId => integer().references(Accounts, #id)();
}
typedef TransactionTemplateDto =
({
TransactionTemplate template,
Beneficiary beneficiary,
ExpenseCategory? expenseCategory,
});
class Transactions extends Table {
IntColumn get id => integer().autoIncrement()();
RealColumn get amount => real()();
// TODO: tags
DateTimeColumn get date => dateTime()();
IntColumn get expenseCategoryId =>
integer().nullable().references(ExpenseCategories, #id)();
IntColumn get accountId => integer().references(Accounts, #id)();
IntColumn get beneficiaryId => integer().references(Beneficiaries, #id)();
}
class TransactionDto {
final Transaction transaction;
final Beneficiary beneficiary;
final ExpenseCategory? expenseCategory;
TransactionDto({
required this.transaction,
required this.beneficiary,
required this.expenseCategory,
});
}
@DriftDatabase(
tables: [
Accounts,
Beneficiaries,
Budgets,
BudgetItems,
ExpenseCategories,
Loans,
LoanChanges,
RecurringTransactions,
TransactionTemplates,
Transactions,
],
daos: [
AccountsDao,
BeneficiariesDao,
BudgetsDao,
ExpenseCategoriesDao,
LoansDao,
RecurringTransactionsDao,
TransactionTemplatesDao,
TransactionsDao,
],
)
class OkaneDatabase extends _$OkaneDatabase {
OkaneDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
static QueryExecutor _openConnection() {
return driftDatabase(
name: "okane",
native: const DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory,
),
);
}
}
@DriftAccessor(
tables: [
Accounts,
Transactions,
TransactionTemplates,
RecurringTransactions,
Budgets,
Beneficiaries,
],
)
class AccountsDao extends DatabaseAccessor<OkaneDatabase>
with _$AccountsDaoMixin {
AccountsDao(super.db);
Stream<List<Account>> accountsStream() {
return select(accounts).watch();
}
Future<List<Account>> getAccounts() {
return select(accounts).get();
}
Future<int> upsertAccount(AccountsCompanion account) {
return into(accounts).insertOnConflictUpdate(account);
}
Future<void> removeAccount(Account account) async {
// Delete dependent data
await (delete(transactions)
..where((t) => t.accountId.equals(account.id))).go();
await (delete(recurringTransactions)
..where((r) => r.accountId.equals(account.id))).go();
await (delete(transactionTemplates)
..where((t) => t.accountId.equals(account.id))).go();
await (delete(budgets)..where((b) => b.accountId.equals(account.id))).go();
await (delete(beneficiaries)
..where((b) => b.accountId.equals(account.id))).go();
// Delete the account
await (delete(accounts)..where((a) => a.id.equals(account.id))).go();
}
}
enum TransactionQueryDateOption { thisMonth }
@DriftAccessor(tables: [Transactions, Beneficiaries, ExpenseCategories])
class TransactionsDao extends DatabaseAccessor<OkaneDatabase>
with _$TransactionsDaoMixin {
TransactionsDao(super.db);
JoinedSelectStatement _transactionQuery(Account account) {
return (select(transactions)
..where((t) => t.accountId.equals(account.id))).join([
leftOuterJoin(
beneficiaries,
beneficiaries.id.equalsExp(transactions.beneficiaryId),
),
leftOuterJoin(
expenseCategories,
expenseCategories.id.equalsExp(transactions.expenseCategoryId),
),
]);
}
TransactionDto _mapToDto(TypedResult row) {
return TransactionDto(
transaction: row.readTable(transactions),
beneficiary: row.readTable(beneficiaries),
expenseCategory: row.readTableOrNull(expenseCategories),
);
}
Stream<List<TransactionDto>> transactionsStream(Account account) {
return _transactionQuery(account).watch().map((rows) {
return rows.map(_mapToDto).toList();
});
}
Future<List<TransactionDto>> getTransactions(Account? account) {
if (account == null) {
return Future.value(List.empty());
}
return _transactionQuery(
account,
).get().then((rows) => rows.map(_mapToDto).toList());
}
Future<List<TransactionDto>> getLastTransactions(
Account account,
DateTime today,
int days,
) async {
return (select(transactions)..where(
(t) =>
t.accountId.equals(account.id) &
t.date.isBiggerThanValue(
toMidnight(today.subtract(Duration(days: days))),
),
))
.join([
leftOuterJoin(
beneficiaries,
beneficiaries.id.equalsExp(transactions.beneficiaryId),
),
leftOuterJoin(
expenseCategories,
expenseCategories.id.equalsExp(transactions.expenseCategoryId),
),
])
.get()
.then((rows) => rows.map(_mapToDto).toList());
}
Future<double> getTotalBalance(Iterable<int> accountIds) async {
final sum = transactions.amount.sum();
final query =
selectOnly(transactions)
..where(transactions.accountId.isIn(accountIds))
..addColumns([sum]);
return query
.map((row) => row.read(sum))
.getSingleOrNull()
.then((v) => v ?? 0);
}
Future<List<TransactionDto>> getTransactionsInTimeframe(
Account account,
DateTime today,
TransactionQueryDateOption option,
) {
final lower = switch (option) {
TransactionQueryDateOption.thisMonth => DateTime(
today.year,
today.month,
0,
),
};
final upper = switch (option) {
TransactionQueryDateOption.thisMonth => monthEnding(today),
};
return (select(transactions)..where(
(t) =>
t.accountId.equals(account.id) &
t.date.isBetweenValues(lower, upper),
))
.join([
leftOuterJoin(
beneficiaries,
beneficiaries.id.equalsExp(transactions.beneficiaryId),
),
leftOuterJoin(
expenseCategories,
expenseCategories.id.equalsExp(transactions.expenseCategoryId),
),
])
.get()
.then((rows) => rows.map(_mapToDto).toList());
}
Future<Transaction> upsertTransaction(TransactionsCompanion t) {
return into(
transactions,
).insertReturning(t, mode: InsertMode.insertOrReplace);
}
}
@DriftAccessor(tables: [Beneficiaries])
class BeneficiariesDao extends DatabaseAccessor<OkaneDatabase>
with _$BeneficiariesDaoMixin {
BeneficiariesDao(super.db);
Stream<List<Beneficiary>> beneficiariesStream() {
return select(beneficiaries).watch();
}
Future<List<Beneficiary>> getBeneficiaries() {
return select(beneficiaries).get();
}
Future<Beneficiary> upsertBeneficiary(BeneficiariesCompanion beneficiary) {
return into(
beneficiaries,
).insertReturning(beneficiary, mode: InsertMode.insertOrReplace);
}
Future<Beneficiary> getAccountBeneficiary(Account account) {
return (select(beneficiaries)
..where((b) => b.accountId.equals(account.id))).getSingle();
}
Stream<Beneficiary> watchBeneficiary(int id) {
return (select(beneficiaries)..where((b) => b.id.equals(id))).watchSingle();
}
}
@DriftAccessor(tables: [ExpenseCategories])
class ExpenseCategoriesDao extends DatabaseAccessor<OkaneDatabase>
with _$ExpenseCategoriesDaoMixin {
ExpenseCategoriesDao(super.db);
Stream<List<ExpenseCategory>> expenseCategoriesStream(Account account) {
return select(expenseCategories).watch();
}
Future<List<ExpenseCategory>> getExpenseCategories(Account? account) {
if (account == null) {
return Future.value(List.empty());
}
return select(expenseCategories).get();
}
Future<ExpenseCategory> upsertCategory(ExpenseCategoriesCompanion category) {
return into(
expenseCategories,
).insertReturning(category, mode: InsertMode.insertOrReplace);
}
}
@DriftAccessor(tables: [Budgets, BudgetItems])
class BudgetsDao extends DatabaseAccessor<OkaneDatabase>
with _$BudgetsDaoMixin {
BudgetsDao(super.db);
Stream<List<BudgetsDto>> budgetsStream(Account account) {
return (select(budgets)
..where((b) => b.accountId.equals(account.id))).watch().map((rows) {
return rows.map((row) {
return BudgetsDto(budget: row);
}).toList();
});
}
Future<List<BudgetsDto>> getBudgets(Account? account) {
if (account == null) {
return Future.value(List.empty());
}
return (select(budgets)
..where((b) => b.accountId.equals(account.id))).get().then((rows) {
return rows.map((row) {
return BudgetsDto(budget: row);
}).toList();
});
}
Stream<List<BudgetItemDto>> watchBudgetItems(Budget budget) {
return (select(budgetItems)..where((b) => b.budgetId.equals(budget.id)))
.join([
leftOuterJoin(
expenseCategories,
expenseCategories.id.equalsExp(budgetItems.expenseCategoryId),
),
])
.watch()
.map((rows) {
return rows.map((row) {
return BudgetItemDto(
expenseCategory: row.readTable(expenseCategories),
item: row.readTable(budgetItems),
);
}).toList();
});
}
Future<Budget> upsertBudget(BudgetsCompanion budget) {
return into(
budgets,
).insertReturning(budget, mode: InsertMode.insertOrReplace);
}
Future<BudgetItem> upsertBudgetItem(BudgetItemsCompanion item) {
return into(
budgetItems,
).insertReturning(item, mode: InsertMode.insertOrReplace);
}
}
@DriftAccessor(tables: [Loans, LoanChanges, Beneficiaries])
class LoansDao extends DatabaseAccessor<OkaneDatabase> with _$LoansDaoMixin {
LoansDao(super.db);
Stream<List<LoanDto>> loansStream(Account account) {
return select(loans)
.join([
leftOuterJoin(
beneficiaries,
beneficiaries.id.equalsExp(loans.beneficiaryId),
),
])
.watch()
.map((rows) {
return rows.map((row) {
return LoanDto(
loan: row.readTable(loans),
beneficiary: row.readTable(beneficiaries),
);
}).toList();
});
}
Future<List<LoanDto>> getLoans(Account? account) {
if (account == null) {
return Future.value(List.empty());
}
return select(loans)
.join([
leftOuterJoin(
beneficiaries,
beneficiaries.id.equalsExp(loans.beneficiaryId),
),
])
.get()
.then((rows) {
return rows.map((row) {
return LoanDto(
loan: row.readTable(loans),
beneficiary: row.readTable(beneficiaries),
);
}).toList();
});
}
Future<double> getTotalLoanSum() async {
final count = loanChanges.amount.sum();
final query = selectOnly(loanChanges)..addColumns([count]);
return query
.map((row) => row.read(count))
.getSingleOrNull()
.then((v) => v ?? 0);
}
Future<Loan> upsertLoan(LoansCompanion loan) {
return into(loans).insertReturning(loan, mode: InsertMode.insertOrReplace);
}
Future<LoanChange> upsertLoanChange(LoanChangesCompanion loanChange) {
return into(
loanChanges,
).insertReturning(loanChange, mode: InsertMode.insertOrReplace);
}
Stream<List<LoanChange>> watchLoanChanges(Loan loan) {
return (select(loanChanges)
..where((c) => c.loanId.equals(loan.id))).watch();
}
Future<void> deleteLoanChange(int id) {
return (delete(loanChanges)..where((c) => c.id.equals(id))).go();
}
}
@DriftAccessor(tables: [TransactionTemplates, ExpenseCategories, Beneficiaries])
class TransactionTemplatesDao extends DatabaseAccessor<OkaneDatabase>
with _$TransactionTemplatesDaoMixin {
TransactionTemplatesDao(super.db);
Stream<List<TransactionTemplateDto>> transactionTemplatesStream(
Account account,
) {
return (select(transactionTemplates)
..where((b) => b.accountId.equals(account.id)))
.join([
leftOuterJoin(
beneficiaries,
beneficiaries.id.equalsExp(transactionTemplates.beneficiaryId),
),
leftOuterJoin(
expenseCategories,
expenseCategories.id.equalsExp(
transactionTemplates.expenseCategoryId,
),
),
])
.watch()
.map((rows) {
return rows.map((row) {
return (
template: row.readTable(transactionTemplates),
beneficiary: row.readTable(beneficiaries),
expenseCategory: row.readTable(expenseCategories),
);
}).toList();
});
}
Future<List<TransactionTemplateDto>> getTransactionTemplates(
Account? account,
) {
if (account == null) {
return Future.value(List.empty());
}
return (select(transactionTemplates)
..where((b) => b.accountId.equals(account.id)))
.join([
leftOuterJoin(
beneficiaries,
beneficiaries.id.equalsExp(transactionTemplates.beneficiaryId),
),
leftOuterJoin(
expenseCategories,
expenseCategories.id.equalsExp(
transactionTemplates.expenseCategoryId,
),
),
])
.get()
.then((rows) {
return rows.map((row) {
return (
template: row.readTable(transactionTemplates),
beneficiary: row.readTable(beneficiaries),
expenseCategory: row.readTable(expenseCategories),
);
}).toList();
});
}
Future<TransactionTemplate> upsertTemplate(
TransactionTemplatesCompanion template,
) {
return into(
transactionTemplates,
).insertReturning(template, mode: InsertMode.insertOrReplace);
}
Future<void> deleteTemplate(TransactionTemplate template) {
return (delete(transactionTemplates)
..where((t) => t.id.equals(template.id))).go();
}
}
@DriftAccessor(tables: [TransactionTemplates, RecurringTransactions])
class RecurringTransactionsDao extends DatabaseAccessor<OkaneDatabase>
with _$RecurringTransactionsDaoMixin {
RecurringTransactionsDao(super.db);
Stream<List<RecurringTransactionDto>> recurringTransactionsStream(
Account account,
) {
return (select(recurringTransactions)
..where((b) => b.accountId.equals(account.id)))
.join([
leftOuterJoin(
transactionTemplates,
transactionTemplates.id.equalsExp(recurringTransactions.templateId),
),
leftOuterJoin(
beneficiaries,
beneficiaries.id.equalsExp(transactionTemplates.beneficiaryId),
),
])
.watch()
.map((rows) {
return rows.map((row) {
return (
recurring: row.readTable(recurringTransactions),
beneficiary: row.readTable(beneficiaries),
template: row.readTable(transactionTemplates),
);
}).toList();
});
}
Future<List<RecurringTransactionDto>> getRecurringTransactions(
Account? account,
) {
if (account == null) {
return Future.value(List.empty());
}
return (select(recurringTransactions)
..where((b) => b.accountId.equals(account.id)))
.join([
leftOuterJoin(
transactionTemplates,
transactionTemplates.id.equalsExp(recurringTransactions.templateId),
),
leftOuterJoin(
beneficiaries,
beneficiaries.id.equalsExp(transactionTemplates.beneficiaryId),
),
])
.get()
.then((rows) {
return rows.map((row) {
return (
recurring: row.readTable(recurringTransactions),
beneficiary: row.readTable(beneficiaries),
template: row.readTable(transactionTemplates),
);
}).toList();
});
}
Future<int> upsertRecurringTransaction(RecurringTransactionsCompanion r) {
return into(recurringTransactions).insertOnConflictUpdate(r);
}
Future<void> deleteTemplate(RecurringTransactionDto dto) async {
await db.transactionTemplatesDao.deleteTemplate(dto.template);
await (delete(recurringTransactions)
..where((t) => t.id.equals(dto.recurring.id))).go();
}
}

8248
lib/database/sqlite.g.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,21 +4,38 @@ import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.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:isar/isar.dart'; import 'package:okane/database/sqlite.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';
import 'package:okane/ui/pages/transaction_details.dart'; import 'package:okane/ui/pages/transaction_details.dart';
import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/state/settings.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'ui/pages/loans/loan_details.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
LocaleSettings.useDeviceLocale();
final settings = SettingsCubit();
await settings.loadSettings();
GetIt.I.registerSingleton<SettingsCubit>(settings);
GetIt.I.registerSingleton<CoreCubit>(CoreCubit()); GetIt.I.registerSingleton<CoreCubit>(CoreCubit());
GetIt.I.registerSingleton<Isar>(await openDatabase()); GetIt.I.registerSingleton<OkaneDatabase>(OkaneDatabase());
runApp(const MyApp()); appRunner() => runApp(const MyApp());
if (settings.sentryDsn != null) {
print("Setting up Sentry!");
await SentryFlutter.init((options) {
options.dsn = settings.sentryDsn!;
}, appRunner: appRunner);
} else {
appRunner();
}
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
@@ -31,21 +48,43 @@ class MyApp extends StatelessWidget {
child: MultiBlocProvider( child: MultiBlocProvider(
providers: [ providers: [
BlocProvider<CoreCubit>(create: (_) => GetIt.I.get<CoreCubit>()), BlocProvider<CoreCubit>(create: (_) => GetIt.I.get<CoreCubit>()),
BlocProvider<SettingsCubit>(
create: (_) => GetIt.I.get<SettingsCubit>(),
),
], ],
child: MaterialApp( child: BlocBuilder<SettingsCubit, SettingsWrapper>(
title: 'Flutter Demo', builder:
(context, state) => MaterialApp(
title: 'Okane',
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), colorSchemeSeed: Colors.deepPurple,
brightness: switch (state.settings.colorScheme) {
ColorSchemeSettings.dark => Brightness.dark,
ColorSchemeSettings.light => Brightness.light,
ColorSchemeSettings.system =>
View.of(context).platformDispatcher.platformBrightness,
},
pageTransitionsTheme: PageTransitionsTheme(
builders: Map.fromIterable(
TargetPlatform.values,
value: (_) => const FadeForwardsPageTransitionsBuilder(),
),
),
), ),
home: const MyHomePage(), home: const MyHomePage(),
onGenerateRoute: onGenerateRoute:
(settings) => switch (settings.name) { (settings) => switch (settings.name) {
"/transactions/details" => TransactionDetailsPage.mobileRoute, "/transactions/details" =>
TransactionDetailsPage.mobileRoute,
"/budgets/details" => BudgetDetailsPage.mobileRoute, "/budgets/details" => BudgetDetailsPage.mobileRoute,
_ => MaterialPageRoute<void>(builder: (_) => Text("Unknown!!")), "/loans/details" => LoanDetailsPage.mobileRoute,
_ => MaterialPageRoute<void>(
builder: (_) => Text("Unknown!!"),
),
}, },
), ),
), ),
),
); );
} }
} }

View File

@@ -2,15 +2,27 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:okane/screen.dart'; import 'package:okane/screen.dart';
import 'package:okane/ui/pages/account/account.dart'; import 'package:okane/ui/pages/account/account.dart';
import 'package:okane/ui/pages/beneficiary_list.dart';
import 'package:okane/ui/pages/budgets/budget_details.dart'; import 'package:okane/ui/pages/budgets/budget_details.dart';
import 'package:okane/ui/pages/budgets/budgets.dart'; import 'package:okane/ui/pages/budgets/budgets.dart';
import 'package:okane/ui/pages/loans/loan_details.dart';
import 'package:okane/ui/pages/loans/loan_list.dart';
import 'package:okane/ui/pages/settings.dart';
import 'package:okane/ui/pages/template_list.dart'; import 'package:okane/ui/pages/template_list.dart';
import 'package:okane/ui/pages/transaction_details.dart'; import 'package:okane/ui/pages/transaction_details.dart';
import 'package:okane/ui/pages/transaction_list.dart'; import 'package:okane/ui/pages/transaction_list.dart';
import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/widgets/account_indicator.dart'; import 'package:okane/ui/widgets/account_indicator.dart';
enum OkanePage { accounts, transactions, beneficiaries, templates, budgets } enum OkanePage {
accounts,
transactions,
beneficiaries,
templates,
budgets,
loans,
settings,
}
typedef OkanePageBuilder = Widget Function(bool); typedef OkanePageBuilder = Widget Function(bool);
@@ -61,7 +73,7 @@ final _pages = <OkanePageItem>[
"Accounts", "Accounts",
AccountListPage(isPage: false), AccountListPage(isPage: false),
null, null,
false, true,
), ),
OkanePageItem( OkanePageItem(
OkanePage.transactions, OkanePage.transactions,
@@ -75,7 +87,7 @@ final _pages = <OkanePageItem>[
OkanePage.beneficiaries, OkanePage.beneficiaries,
Icons.person, Icons.person,
"Beneficiaries", "Beneficiaries",
Container(), BeneficiaryListPage(),
null, null,
true, true,
), ),
@@ -95,6 +107,22 @@ final _pages = <OkanePageItem>[
(_) => BudgetDetailsPage(), (_) => BudgetDetailsPage(),
true, true,
), ),
OkanePageItem(
OkanePage.loans,
Icons.money_outlined,
"Loans",
LoanListPage(),
(_) => LoanDetailsPage(isPage: false),
false,
),
OkanePageItem(
OkanePage.settings,
Icons.settings,
"Settings",
SettingsPage(),
null,
false,
),
]; ];
class OkaneNavigationRail extends StatelessWidget { class OkaneNavigationRail extends StatelessWidget {
@@ -198,17 +226,10 @@ class OkaneNavigationLayout extends StatelessWidget {
if (p.showAccountName && if (p.showAccountName &&
state.activeAccountIndex != null) state.activeAccountIndex != null)
Padding( Padding(
padding: EdgeInsets.only(left: 8), padding: EdgeInsets.symmetric(
child: Text( horizontal: 8,
state
.accounts[state
.activeAccountIndex!]
.name!,
style:
Theme.of(
context,
).textTheme.titleLarge,
), ),
child: AccountSwitcher(),
), ),
], ],
), ),
@@ -221,12 +242,7 @@ class OkaneNavigationLayout extends StatelessWidget {
children: [ children: [
if (p.showAccountName && if (p.showAccountName &&
state.activeAccountIndex != null) state.activeAccountIndex != null)
AccountIndicator( AccountIndicator(),
accountName:
state
.accounts[state.activeAccountIndex!]
.name!,
),
Expanded( Expanded(
child: child:
p.details != null p.details != null

View File

@@ -1,16 +1,15 @@
import 'package:drift/drift.dart' show Value;
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/account.dart'; import 'package:okane/database/sqlite.dart';
import 'package:okane/database/collections/beneficiary.dart';
import 'package:okane/database/database.dart';
import 'package:okane/ui/pages/account/breakdown_card.dart'; import 'package:okane/ui/pages/account/breakdown_card.dart';
import 'package:okane/ui/pages/account/loan_card.dart';
import 'package:okane/ui/pages/account/total_balance_card.dart'; 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/ui/widgets/piechart.dart'; import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/widgets/piechart_card.dart';
class AccountListPage extends StatefulWidget { class AccountListPage extends StatefulWidget {
final bool isPage; final bool isPage;
@@ -30,14 +29,6 @@ class AccountListPageState extends State<AccountListPage> {
children: [ children: [
ListView( ListView(
children: [ children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
"Accounts",
style: Theme.of(context).textTheme.titleLarge,
),
),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: BlocBuilder<CoreCubit, CoreState>( child: BlocBuilder<CoreCubit, CoreState>(
@@ -53,7 +44,7 @@ class AccountListPageState extends State<AccountListPage> {
width: 150, width: 150,
height: 100, height: 100,
child: Card( child: Card(
color: colorHash(state.accounts[index].name!), color: colorHash(state.accounts[index].name),
shape: shape:
index == state.activeAccountIndex index == state.activeAccountIndex
? RoundedRectangleBorder( ? RoundedRectangleBorder(
@@ -66,21 +57,18 @@ class AccountListPageState extends State<AccountListPage> {
), ),
) )
: null, : null,
child: InkWell(
onTap: () {
GetIt.I
.get<CoreCubit>()
.setActiveAccountIndex(index);
},
child: Center( child: Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(state.accounts[index].name!), Text(state.accounts[index].name),
FutureBuilder( FutureBuilder(
future: getTotalBalance( future: GetIt.I
state.accounts[index], .get<OkaneDatabase>()
), .transactionsDao
.getTotalBalance([
state.accounts[index].id,
]),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return Container(); return Container();
@@ -101,8 +89,9 @@ class AccountListPageState extends State<AccountListPage> {
), ),
), ),
), ),
),
Wrap(
children: [
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8), padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: TotalBalanceCard(), child: TotalBalanceCard(),
@@ -112,9 +101,10 @@ class AccountListPageState extends State<AccountListPage> {
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8), padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: UpcomingTransactionsCard(), child: UpcomingTransactionsCard(),
), ),
Wrap(
children: [
Padding(padding: EdgeInsets.all(8), child: BreakdownCard()), Padding(padding: EdgeInsets.all(8), child: BreakdownCard()),
Padding(padding: EdgeInsets.all(8), child: TotalLoanCard()),
], ],
), ),
], ],
@@ -141,7 +131,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,
), ),
), ),
), ),
@@ -150,20 +141,25 @@ class AccountListPageState extends State<AccountListPage> {
onPressed: () async { onPressed: () async {
if (_accountNameController.text.isEmpty) return; if (_accountNameController.text.isEmpty) return;
final a = final db = GetIt.I.get<OkaneDatabase>();
Account()..name = _accountNameController.text; print("Adding account");
final b = final accountId = await db.accountsDao
Beneficiary() .upsertAccount(
..name = _accountNameController.text AccountsCompanion(
..account.value = name: Value(_accountNameController.text),
GetIt.I.get<CoreCubit>().activeAccount ),
..type = BeneficiaryType.account; );
await upsertAccount(a); print("Adding beneficiary");
await upsertBeneficiary(b); final b = BeneficiariesCompanion(
name: Value(_accountNameController.text),
accountId: Value(accountId),
type: Value(BeneficiaryType.account),
);
await db.beneficiariesDao.upsertBeneficiary(b);
_accountNameController.text = ""; _accountNameController.text = "";
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text("Add"), child: Text(t.modals.add),
), ),
], ],
), ),

View File

@@ -2,7 +2,8 @@ import 'dart:collection';
import 'package:fl_chart/fl_chart.dart'; 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/sqlite.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';
@@ -25,21 +26,27 @@ class AccountBalanceGraphCard extends StatelessWidget {
Future<List<FlSpot>> getAccountBalance() async { Future<List<FlSpot>> getAccountBalance() async {
final coreCubit = GetIt.I.get<CoreCubit>(); final coreCubit = GetIt.I.get<CoreCubit>();
final today = toMidnight(DateTime.now()); final today = toMidnight(DateTime.now());
final transactions = await getLastTransactions( final db = GetIt.I.get<OkaneDatabase>();
print("Getting transactions");
final transactions = await db.transactionsDao.getLastTransactions(
coreCubit.activeAccount!, coreCubit.activeAccount!,
today, today,
30, 30,
); );
final totalBalance = await getTotalBalance(coreCubit.activeAccount!); print("Got transactions. Getting balance");
final totalBalance = await db.transactionsDao.getTotalBalance([
coreCubit.activeAccount!.id,
]);
print("Got balance");
// Compute the differences per day // Compute the differences per day
Map<int, double> differences = Map.fromEntries( Map<int, double> differences = Map.fromEntries(
List.generate(30, (i) => i).map((i) => MapEntry(i, 0)), List.generate(30, (i) => i).map((i) => MapEntry(i, 0)),
); );
for (final item in transactions) { for (final item in transactions) {
final diff = today.difference(toMidnight(item.date)).inDays; final diff = today.difference(toMidnight(item.transaction.date)).inDays;
final balance = differences[diff]!; final balance = differences[diff]!;
differences[diff] = balance + item.amount; differences[diff] = balance + item.transaction.amount;
} }
// Compute the balance // Compute the balance
@@ -67,7 +74,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(

View File

@@ -1,11 +1,9 @@
import 'dart:math';
import 'package:fl_chart/fl_chart.dart'; 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/transaction.dart'; import 'package:okane/database/sqlite.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';
@@ -44,38 +42,38 @@ class LegendItem extends StatelessWidget {
class BreakdownCard extends StatelessWidget { class BreakdownCard extends StatelessWidget {
const BreakdownCard({super.key}); const BreakdownCard({super.key});
LegendData _computeSections(List<Transaction> transactions) { LegendData _computeSections(List<TransactionDto> transactions) {
Map<String, double> expenses = {}; Map<String, double> expenses = {};
Map<String, Color> colors = {}; Map<String, Color> colors = {};
double usableMoney = 0; double usableMoney = 0;
transactions.forEach((t) { for (var t in transactions) {
String category; String category;
if (t.amount > 0) { if (t.transaction.amount > 0) {
category = CATEGORY_INCOME; category = CATEGORY_INCOME;
colors[CATEGORY_INCOME] = Colors.green; colors[CATEGORY_INCOME] = Colors.green;
} else { } else {
if (t.expenseCategory.value?.name == null) { if (t.expenseCategory?.name == null) {
category = CATEGORY_OTHER; category = CATEGORY_OTHER;
colors[category] = Colors.red; colors[category] = Colors.red;
} else { } else {
category = t.expenseCategory.value!.name; category = t.expenseCategory!.name;
colors[category] = colorHash(t.expenseCategory.value!.name); colors[category] = colorHash(t.expenseCategory!.name);
} }
} }
expenses.update( expenses.update(
category, category,
(value) => value + t.amount.abs(), (value) => value + t.transaction.amount.abs(),
ifAbsent: () => t.amount.abs(), ifAbsent: () => t.transaction.amount.abs().toDouble(),
); );
usableMoney += t.amount; usableMoney += t.transaction.amount;
}); }
return (expenses: expenses, colors: colors, usable: usableMoney); return (expenses: expenses, colors: colors, usable: usableMoney);
} }
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,11 +89,18 @@ 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,
);
} }
final db = GetIt.I.get<OkaneDatabase>();
return FutureBuilder( return FutureBuilder(
future: getLastTransactions(bloc.activeAccount!, DateTime.now(), 30), future: db.transactionsDao.getLastTransactions(
bloc.activeAccount!,
DateTime.now(),
30,
),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return _buildCard( return _buildCard(
@@ -125,7 +130,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 +148,9 @@ class BreakdownCard extends StatelessWidget {
) )
.toList(), .toList(),
), ),
"Available money: ${formatCurrency(data.usable)}", t.pages.accounts.expenseBreakdown.availableFunds(
amount: formatCurrency(data.usable),
),
); );
}, },
); );

View File

@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:okane/database/sqlite.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/state/core.dart';
class DeleteAccountPopup extends StatelessWidget {
final Account account;
final VoidCallback onCancel;
final VoidCallback afterDelete;
const DeleteAccountPopup({
super.key,
required this.account,
required this.onCancel,
required this.afterDelete,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<CoreCubit, CoreState>(
builder:
(context, state) => AlertDialog(
title: Text(t.pages.accounts.deleteAccount.title),
content:
state.isDeletingAccount
? Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 80,
height: 80,
child: CircularProgressIndicator(),
),
],
)
: Text(
t.pages.accounts.deleteAccount.content(
name: account.name,
),
),
actions: [
TextButton(
onPressed:
state.isDeletingAccount
? null
: () async {
await GetIt.I.get<CoreCubit>().deleteAccount(account);
afterDelete();
},
child: Text(
t.modals.delete,
style: TextStyle(color: Colors.red),
),
),
TextButton(
onPressed:
state.isDeletingAccount
? null
: () {
onCancel();
},
child: Text(t.modals.cancel),
),
],
),
);
}
}

View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:okane/database/sqlite.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/utils.dart';
import 'package:okane/ui/widgets/piechart_card.dart';
class TotalLoanCard extends StatelessWidget {
const TotalLoanCard({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) {
return ResponsiveCard(
titleText: "Loan Sum",
child: Padding(
padding: EdgeInsets.all(16),
child: FutureBuilder(
future: GetIt.I.get<OkaneDatabase>().loansDao.getTotalLoanSum(),
builder: (context, snapshot) {
return Text(
snapshot.hasData
? formatCurrency(snapshot.data!)
: t.pages.accounts.totalBalance.loading,
style: Theme.of(context).textTheme.bodyLarge,
);
},
),
),
);
},
);
}
}

View File

@@ -1,9 +1,11 @@
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:okane/database/collections/account.dart'; import 'package:get_it/get_it.dart';
import 'package:okane/database/database.dart'; import 'package:okane/database/sqlite.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';
class TotalBalanceCard extends StatelessWidget { class TotalBalanceCard extends StatelessWidget {
const TotalBalanceCard({super.key}); const TotalBalanceCard({super.key});
@@ -13,36 +15,35 @@ class TotalBalanceCard extends StatelessWidget {
return 0; return 0;
} }
final results = await Future.wait(accounts.map(getTotalBalance).toList()); final db = GetIt.I.get<OkaneDatabase>();
final totalBalance = await db.transactionsDao.getTotalBalance(
accounts.map((a) => a.id),
);
return results.reduce((acc, val) => acc + val); final loanSum = await db.loansDao.getTotalLoanSum();
return totalBalance + loanSum;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<CoreCubit, CoreState>( return BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) { builder: (context, state) {
return Card( return ResponsiveCard(
titleText: t.pages.accounts.totalBalance.title,
child: Padding( child: Padding(
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),
child: Column( child: FutureBuilder(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Total balance",
style: Theme.of(context).textTheme.titleLarge,
),
FutureBuilder(
future: _getTotalBalance(state.accounts), future: _getTotalBalance(state.accounts),
builder: (context, snapshot) { builder: (context, snapshot) {
print("SNAPSHOT: ${snapshot.data}");
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,
); );
}, },
), ),
],
),
), ),
); );
}, },

View File

@@ -1,27 +1,29 @@
import 'dart:math'; import 'dart:math';
import 'package:drift/drift.dart' show Value;
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:okane/database/collections/recurrent.dart'; import 'package:get_it/get_it.dart';
import 'package:okane/database/sqlite.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/widgets/add_transaction.dart';
import 'package:okane/ui/widgets/piechart_card.dart';
class UpcomingTransactionsCard extends StatelessWidget { class UpcomingTransactionsCard extends StatelessWidget {
const UpcomingTransactionsCard({super.key}); const UpcomingTransactionsCard({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bloc = GetIt.I.get<CoreCubit>();
return BlocBuilder<CoreCubit, CoreState>( return BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) { builder: (context, state) {
final today = DateTime.now(); final today = DateTime.now();
final upcomingRaw = final upcomingRaw =
state.recurringTransactions.where((t) { state.recurringTransactions
if (t.lastExecution == null) { .where((t) => isTransactionDue(t.recurring, today))
return true; .toList();
} final List<RecurringTransactionDto> upcoming =
return today.difference(t.lastExecution!).inDays <=
(t.days * 1.5).toInt();
}).toList();
final List<RecurringTransaction> upcoming =
upcomingRaw.isEmpty upcomingRaw.isEmpty
? List.empty() ? List.empty()
: upcomingRaw.sublist(0, min(upcomingRaw.length, 3)); : upcomingRaw.sublist(0, min(upcomingRaw.length, 3));
@@ -29,48 +31,87 @@ 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.name,
amount:
"${formatCurrency(transaction.template.amount)}",
),
), ),
subtitle: Text( subtitle: Text(
"Due in ${today.difference(t.lastExecution ?? today).inDays} days", t.pages.accounts.upcomingTransactions.items.dueIn(
number:
today
.difference(
transaction.recurring.lastExecution ??
today,
)
.inDays,
),
), ),
leading: Icon( leading: Icon(
t.template.value!.amount < 0 transaction.template.amount < 0
? Icons.remove ? Icons.remove
: Icons.add, : Icons.add,
color: color:
t.template.value!.amount < 0 transaction.template.amount < 0
? Colors.red ? Colors.red
: Colors.green, : Colors.green,
), ),
trailing: IconButton( trailing: IconButton(
icon: Icon(Icons.play_arrow), icon: Icon(Icons.play_arrow),
onPressed: () {}, onPressed: () {
showDialogOrModal(
context: context,
builder:
(context) => AddTransactionWidget(
activeAccountItem: bloc.activeAccount!,
template: (
template: transaction.template,
beneficiary: transaction.beneficiary,
expenseCategory: null,
),
onAdd: (newTransaction) async {
// Update the recurring template
await GetIt.I
.get<OkaneDatabase>()
.recurringTransactionsDao
.upsertRecurringTransaction(
transaction.recurring
.copyWith(
lastExecution: Value(
newTransaction
.transaction
.date,
),
)
.toCompanion(true),
);
Navigator.of(context).pop();
},
),
);
},
), ),
), ),
) )
.toList(); .toList();
return Card( return ResponsiveCard(
titleText: t.pages.accounts.upcomingTransactions.title,
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(children: transactions),
children:
<Widget>[
Text(
"Upcoming Transactions",
style: Theme.of(context).textTheme.titleLarge,
),
] +
transactions,
),
), ),
); );
}, },

View File

@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/widgets/image_wrapper.dart';
class BeneficiaryListPage extends StatelessWidget {
const BeneficiaryListPage({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) {
return Stack(
children: [
ListView.builder(
itemCount: state.beneficiaries.length,
itemBuilder: (context, index) {
final item = state.beneficiaries[index];
return ListTile(
leading: ImageWrapper(title: item.name, path: item.imagePath),
// TODO: Allow deleting beneficiaries
trailing: IconButton(
onPressed: null,
icon: Icon(Icons.delete, color: Colors.grey),
),
title: Text(item.name),
);
},
),
],
);
},
);
}
}

View File

@@ -1,7 +1,8 @@
import 'package:drift/drift.dart' show Value;
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/collections/budget.dart'; import 'package:okane/database/sqlite.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,
@@ -47,17 +52,20 @@ class AddBudgetState extends State<AddBudgetPopup> {
} }
final bloc = GetIt.I.get<CoreCubit>(); final bloc = GetIt.I.get<CoreCubit>();
final budget = await GetIt.I.get<OkaneDatabase>().budgetsDao.upsertBudget(
Budget() BudgetsCompanion(
..name = _budgetNameEditController.text name: Value(_budgetNameEditController.text),
..period = BudgetPeriod.month period: Value(BudgetPeriod.month),
..includeOtherSpendings = false includeOtherSpendings: Value(false),
..income = double.parse(_budgetIncomeEditController.text) income: Value(
..account.value = bloc.activeAccount!; double.parse(_budgetIncomeEditController.text),
await upsertBudget(budget); ),
accountId: Value(bloc.activeAccount!.id),
),
);
widget.onDone(); widget.onDone();
}, },
child: Text("Add"), child: Text(t.modals.add),
), ),
], ],
), ),

View File

@@ -1,20 +1,21 @@
import 'package:drift/drift.dart' show Value;
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/collections/budget.dart'; import 'package:okane/database/sqlite.dart';
import 'package:okane/database/collections/expense_category.dart'; import 'package:okane/i18n/strings.g.dart';
import 'package:okane/database/database.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';
class AddBudgetItemPopup extends StatefulWidget { class AddBudgetItemPopup extends StatefulWidget {
final VoidCallback onDone; final VoidCallback onDone;
final Budget budget; final BudgetsDto budget;
final List<BudgetItemDto> items;
const AddBudgetItemPopup({ const AddBudgetItemPopup({
super.key, super.key,
required this.onDone, required this.onDone,
required this.budget, required this.budget,
required this.items,
}); });
@override @override
@@ -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,
@@ -69,29 +74,28 @@ class AddBudgetItemState extends State<AddBudgetItemPopup> {
_expenseCategory == null) { _expenseCategory == null) {
return; return;
} }
if (widget.budget.items if (widget.items
.where( .where(
(i) => (i) =>
i.expenseCategory.value!.name == i.expenseCategory.name == _expenseCategory!.name,
_expenseCategory!.name,
) )
.firstOrNull != .firstOrNull !=
null) { null) {
return; return;
} }
final item = await GetIt.I.get<OkaneDatabase>().budgetsDao.upsertBudgetItem(
BudgetItem() BudgetItemsCompanion(
..expenseCategory.value = _expenseCategory amount: Value(
..amount = double.parse( double.parse(_budgetItemAmountEditController.text),
_budgetItemAmountEditController.text, ),
expenseCategoryId: Value(_expenseCategory!.id),
budgetId: Value(widget.budget.budget.id),
),
); );
await upsertBudgetItem(item);
widget.budget.items.add(item);
await upsertBudget(widget.budget);
widget.onDone(); widget.onDone();
}, },
child: Text("Add"), child: Text(t.modals.add),
), ),
], ],
), ),

View File

@@ -1,14 +1,11 @@
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/sqlite.dart';
import 'package:okane/database/database.dart'; import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/pages/account/breakdown_card.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 {
@@ -25,6 +22,7 @@ class BudgetDetailsPage extends StatelessWidget {
builder: builder:
(_) => AddBudgetItemPopup( (_) => AddBudgetItemPopup(
budget: state.activeBudget!, budget: state.activeBudget!,
items: state.budgetItems,
onDone: () { onDone: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
@@ -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.budgetItems.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(
@@ -75,14 +73,15 @@ class BudgetDetailsPage extends StatelessWidget {
final bloc = GetIt.I.get<CoreCubit>(); final bloc = GetIt.I.get<CoreCubit>();
final today = DateTime.now(); final today = DateTime.now();
final db = GetIt.I.get<OkaneDatabase>();
return FutureBuilder( return FutureBuilder(
future: getTransactionsInTimeframe( future: db.transactionsDao.getTransactionsInTimeframe(
bloc.activeAccount!, bloc.activeAccount!,
today, today,
TransactionQueryDateOption.thisMonth, TransactionQueryDateOption.thisMonth,
), ),
builder: (context, snapshot) { builder: (context, snapshot) {
final daysLeft = switch (state.activeBudget!.period) { final daysLeft = switch (state.activeBudget!.budget.period) {
BudgetPeriod.month => BudgetPeriod.month =>
monthEnding(today).difference(today).inDays, monthEnding(today).difference(today).inDays,
}; };
@@ -92,22 +91,27 @@ 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(
shrinkWrap: true, shrinkWrap: true,
itemCount: state.activeBudget!.items.length, itemCount: state.budgetItems.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = state.activeBudget!.items.elementAt( final item = state.budgetItems.elementAt(index);
index, final amount = formatCurrency(item.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(
// TODO
name: "lol",
//name: item.expenseCategory.value!.name,
amount: amount,
),
),
subtitle: Text(
t.pages.budgets.details.items.loading,
), ),
subtitle: Text("..."),
); );
}, },
), ),
@@ -116,26 +120,28 @@ class BudgetDetailsPage extends StatelessWidget {
} }
final categories = final categories =
state.activeBudget!.items state.budgetItems
.map((i) => i.expenseCategory.value!.name) // TODO
//.map((i) => i.expenseCategory.value!.name)
.map((i) => "lol")
.toList(); .toList();
final spending = <String, double>{}; final spending = <String, double>{};
for (final t in snapshot.data!) { for (final t in snapshot.data!) {
String categoryName; String categoryName;
if (!categories.contains(t.expenseCategory.value?.name)) { if (!categories.contains(t.expenseCategory?.name)) {
if (!state.activeBudget!.includeOtherSpendings) { if (!state.activeBudget!.budget.includeOtherSpendings) {
continue; continue;
} }
categoryName = "Other"; categoryName = "Other";
} else { } else {
categoryName = t.expenseCategory.value!.name; categoryName = t.expenseCategory!.name;
} }
spending.update( spending.update(
categoryName, categoryName,
(value) => value + t.amount, (value) => value + t.transaction.amount,
ifAbsent: () => t.amount, ifAbsent: () => t.transaction.amount,
); );
} }
@@ -143,8 +149,8 @@ class BudgetDetailsPage extends StatelessWidget {
spending.isEmpty spending.isEmpty
? 0 ? 0
: spending.values.reduce((acc, val) => acc + val); : spending.values.reduce((acc, val) => acc + val);
final budgetTotal = state.activeBudget!.items final budgetTotal = state.budgetItems
.map((i) => i.amount) .map((i) => i.item.amount)
.reduce((acc, val) => acc + val); .reduce((acc, val) => acc + val);
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -167,7 +173,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 +205,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 +239,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(
@@ -265,25 +271,32 @@ class BudgetDetailsPage extends StatelessWidget {
fallbackText: "", fallbackText: "",
valueConverter: formatCurrency, valueConverter: formatCurrency,
items: items:
state.activeBudget!.items state.budgetItems
.map( .map(
(i) => ( (i) => (
title: i.expenseCategory.value!.name, title: i.expenseCategory.name,
value: i.amount, value: i.item.amount,
color: colorHash( color: colorHash(
i.expenseCategory.value!.name, i.expenseCategory.name,
), ),
), ),
) )
.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 +308,13 @@ class BudgetDetailsPage extends StatelessWidget {
), ),
) )
.toList(), .toList(),
titleText: "Spending Breakdown", titleText:
t
.pages
.budgets
.details
.spendingBreakdown
.title,
), ),
), ),
], ],
@@ -306,7 +325,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(
@@ -323,25 +342,26 @@ class BudgetDetailsPage extends StatelessWidget {
padding: EdgeInsets.all(8), padding: EdgeInsets.all(8),
child: ListView.builder( child: ListView.builder(
shrinkWrap: true, shrinkWrap: true,
itemCount: state.activeBudget!.items.length, itemCount: state.budgetItems.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = state.activeBudget!.items.elementAt( final item = state.budgetItems.elementAt(index);
index, final amount = formatCurrency(item.item.amount);
); final spent = spending[item.expenseCategory.name];
final amount = formatCurrency(item.amount);
final spent =
spending[item.expenseCategory.value!.name];
final left = final left =
spent == null spent == null
? item.amount ? item.item.amount
: item.amount + spent; : item.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.name} ($amount)",
), ),
subtitle: Text( subtitle: Text(
subtitleText, subtitleText,

View File

@@ -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)],
); );
} }
@@ -27,7 +28,7 @@ class BudgetListPage extends StatelessWidget {
itemCount: state.budgets.length, itemCount: state.budgets.length,
itemBuilder: itemBuilder:
(context, index) => ListTile( (context, index) => ListTile(
title: Text(state.budgets[index].name), title: Text(state.budgets[index].budget.name),
selected: state.budgets[index] == state.activeBudget, selected: state.budgets[index] == state.activeBudget,
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -67,7 +68,6 @@ class BudgetListPage extends StatelessWidget {
); );
}, },
), ),
Positioned( Positioned(
right: 16, right: 16,
bottom: 16, bottom: 16,

View File

@@ -1,9 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:okane/database/collections/budget.dart'; import 'package:get_it/get_it.dart';
import 'package:okane/database/database.dart'; import 'package:okane/database/sqlite.dart';
import 'package:okane/i18n/strings.g.dart';
class EditBudgetPopup extends StatefulWidget { class EditBudgetPopup extends StatefulWidget {
final Budget budget; final BudgetsDto budget;
final VoidCallback onDone; final VoidCallback onDone;
@@ -26,8 +27,8 @@ class EditBudgetState extends State<EditBudgetPopup> {
void initState() { void initState() {
super.initState(); super.initState();
_budgetNameEditController.text = widget.budget.name; _budgetNameEditController.text = widget.budget.budget.name;
_includeOtherSpendings = widget.budget.includeOtherSpendings; _includeOtherSpendings = widget.budget.budget.includeOtherSpendings;
} }
@override @override
@@ -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) {
@@ -58,20 +61,25 @@ class EditBudgetState extends State<EditBudgetPopup> {
if (_budgetNameEditController.text.isEmpty) { if (_budgetNameEditController.text.isEmpty) {
return; return;
} }
if (_budgetNameEditController.text == widget.budget.name && if (_budgetNameEditController.text ==
widget.budget.budget.name &&
_includeOtherSpendings == _includeOtherSpendings ==
widget.budget.includeOtherSpendings) { widget.budget.budget.includeOtherSpendings) {
widget.onDone(); widget.onDone();
return; return;
} }
widget.budget await GetIt.I.get<OkaneDatabase>().budgetsDao.upsertBudget(
..name = _budgetNameEditController.text widget.budget.budget
..includeOtherSpendings = _includeOtherSpendings; .copyWith(
await upsertBudget(widget.budget); name: _budgetNameEditController.text,
includeOtherSpendings: _includeOtherSpendings,
)
.toCompanion(false),
);
widget.onDone(); widget.onDone();
}, },
child: Text("Save"), child: Text(t.modals.save),
), ),
], ],
), ),

View File

@@ -0,0 +1,141 @@
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:okane/database/sqlite.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/state/core.dart';
import 'package:searchfield/searchfield.dart';
class AddLoanPopup extends StatefulWidget {
final VoidCallback onDone;
const AddLoanPopup({super.key, required this.onDone});
@override
AddBudgetState createState() => AddBudgetState();
}
class AddBudgetState extends State<AddLoanPopup> {
final TextEditingController _beneficiaryTextController =
TextEditingController();
SearchFieldListItem<Beneficiary>? _selectedBeneficiary;
String getBeneficiaryName(Beneficiary item) {
return switch (item.type) {
BeneficiaryType.account => t.common.beneficiary.nameWithAccount(
name: item.name,
),
BeneficiaryType.other => item.name,
};
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: BlocBuilder<CoreCubit, CoreState>(
builder:
(context, state) => SearchField<Beneficiary>(
suggestions:
state.beneficiaries
.where((el) {
final bloc = GetIt.I.get<CoreCubit>();
if (el.type == BeneficiaryType.account) {
return el.accountId ==
bloc.activeAccount?.id.toInt();
}
return true;
})
.map((el) {
return SearchFieldListItem(
getBeneficiaryName(el),
item: el,
);
})
.toList(),
hint: "Beneficiary",
controller: _beneficiaryTextController,
selectedValue: _selectedBeneficiary,
onSuggestionTap: (beneficiary) {
setState(() => _selectedBeneficiary = beneficiary);
},
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton(
onPressed: () async {
final beneficiaryName = _beneficiaryTextController.text;
if (_selectedBeneficiary == null && beneficiaryName.isEmpty) {
return;
}
final db = GetIt.I.get<OkaneDatabase>();
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: Text(
t.common.beneficiary.addBeneficiary.title,
),
content: Text(
t.common.beneficiary.addBeneficiary.body(
name: beneficiaryName,
),
),
actions: [
TextButton(
style: TextButton.styleFrom(
textStyle:
Theme.of(context).textTheme.labelLarge,
),
child: Text(t.modals.add),
onPressed: () => Navigator.of(context).pop(true),
),
TextButton(
style: TextButton.styleFrom(
textStyle:
Theme.of(context).textTheme.labelLarge,
),
child: Text(t.modals.cancel),
onPressed: () => Navigator.of(context).pop(false),
),
],
),
);
if (result == null || !result) {
return;
}
beneficiary = await db.beneficiariesDao.upsertBeneficiary(
BeneficiariesCompanion(
name: Value(beneficiaryName),
type: Value(BeneficiaryType.other),
),
);
}
await db.loansDao.upsertLoan(
LoansCompanion(beneficiaryId: Value(beneficiary.id)),
);
widget.onDone();
},
child: Text(t.modals.add),
),
],
),
],
);
}
}

View File

@@ -0,0 +1,105 @@
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:okane/database/sqlite.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/utils.dart';
enum LoanChangeType { owe, loan }
class AddLoanChangePopup extends StatefulWidget {
final VoidCallback onDone;
final LoanDto loan;
const AddLoanChangePopup({
super.key,
required this.onDone,
required this.loan,
});
@override
AddLoanPopupState createState() => AddLoanPopupState();
}
class AddLoanPopupState extends State<AddLoanChangePopup> {
LoanChangeType _loanChangeType = LoanChangeType.loan;
final TextEditingController _amountController = TextEditingController(
text: "0.00",
);
DateTime _selectedDate = DateTime.now();
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SegmentedButton(
segments: [
ButtonSegment(value: LoanChangeType.loan, label: Text("Loan")),
ButtonSegment(value: LoanChangeType.owe, label: Text("Owe")),
],
selected: {_loanChangeType},
onSelectionChanged: (values) {
setState(() {
_loanChangeType = values.first;
});
},
),
TextField(
decoration: InputDecoration(
icon: Icon(Icons.euro),
hintText: "Amount",
),
controller: _amountController,
keyboardType: TextInputType.numberWithOptions(
signed: false,
decimal: true,
),
),
Row(
children: [
Text("Date"),
OutlinedButton(
onPressed: () async {
final dt = await showDatePicker(
context: context,
initialDate: _selectedDate,
firstDate: DateTime(1),
lastDate: DateTime(9999),
);
if (dt == null) return;
setState(() => _selectedDate = dt);
},
child: Text(formatDateTime(_selectedDate)),
),
],
),
Align(
alignment: Alignment.centerRight,
child: OutlinedButton(
onPressed: () async {
final sign = switch (_loanChangeType) {
LoanChangeType.owe => -1,
LoanChangeType.loan => 1,
};
await GetIt.I.get<OkaneDatabase>().loansDao.upsertLoanChange(
LoanChangesCompanion(
amount: Value(
sign * double.parse(_amountController.text).abs(),
),
date: Value(DateTime.now()),
loanId: Value(widget.loan.loan.id),
),
);
widget.onDone();
},
child: Text(t.modals.add),
),
),
],
);
}
}

View File

@@ -0,0 +1,166 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:grouped_list/grouped_list.dart';
import 'package:okane/database/sqlite.dart';
import 'package:okane/ui/pages/loans/add_loan_change.dart';
import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/utils.dart';
import 'package:okane/ui/widgets/image_wrapper.dart';
class LoanDetailsPage extends StatelessWidget {
final bool isPage;
const LoanDetailsPage({super.key, required this.isPage});
static MaterialPageRoute<void> get mobileRoute =>
MaterialPageRoute(builder: (_) => LoanDetailsPage(isPage: true));
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
if (isPage)
SizedBox(
height: 50,
child: Row(
children: [
IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
),
Expanded(
child: BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) {
if (state.activeLoan == null) {
return Text("No loan selected");
}
final loanSum =
state.loanChanges.isNotEmpty
? state.loanChanges
.map((c) => c.amount)
.reduce((acc, val) => acc + val)
: 0.0;
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Row(
children: [
ImageWrapper(
title: state.activeLoan!.beneficiary.name,
path: state.activeLoan!.beneficiary.imagePath,
),
Text(state.activeLoan!.beneficiary.name),
],
),
),
SliverToBoxAdapter(
child: Text("Total: ${formatCurrency(loanSum)}"),
),
SliverToBoxAdapter(
child: Row(
children: [
Text("Loan Transactions"),
IconButton(
onPressed: () {
showDialogOrModal(
context: context,
builder:
(_) => AddLoanChangePopup(
loan: state.activeLoan!,
onDone: () {
Navigator.of(context).pop();
},
),
);
},
icon: Icon(Icons.add),
),
],
),
),
SliverToBoxAdapter(
child:
state.loanChanges.isNotEmpty
? GroupedListView(
elements: state.loanChanges,
shrinkWrap: true,
reverse: true,
groupBy:
(LoanChange loanChange) =>
formatDateTime(loanChange.date),
groupHeaderBuilder:
(item) => Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
DecoratedBox(
decoration: BoxDecoration(
color: Colors.black.withAlpha(170),
borderRadius: BorderRadius.circular(
8,
),
),
child: Padding(
padding: EdgeInsets.all(4),
child: Text(
formatDateTime(item.date),
style: TextStyle(
color: Colors.white,
),
),
),
),
],
),
indexedItemBuilder:
(ctx, item, idx) => ListTile(
leading:
item.amount > 0
? Icon(
Icons.add,
color: Colors.green,
)
: Icon(
Icons.remove,
color: Colors.red,
),
title: Text(formatCurrency(item.amount)),
trailing: IconButton(
icon: Icon(
Icons.delete,
color: Colors.red,
),
onPressed: () async {
await GetIt.I
.get<OkaneDatabase>()
.loansDao
.deleteLoanChange(item.id);
},
),
),
)
: Text("No transactions available"),
),
],
);
},
),
),
],
),
);
}
}

View File

@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:okane/screen.dart';
import 'package:okane/ui/pages/loans/add_loan.dart';
import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/utils.dart';
import 'package:okane/ui/widgets/image_wrapper.dart';
class LoanListPage extends StatelessWidget {
const LoanListPage({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) {
return Stack(
children: [
ListView.builder(
itemCount: state.loans.length,
itemBuilder: (context, index) {
final item = state.loans[index];
final beneficiary = item.beneficiary;
return ListTile(
leading: ImageWrapper(
title: beneficiary.name,
path: beneficiary.imagePath,
),
onTap: () {
GetIt.I.get<CoreCubit>().setActiveLoan(item);
if (getScreenSize(context) == ScreenSize.small) {
Navigator.of(context).pushNamed("/loans/details");
}
},
trailing: IconButton(
onPressed: () async {
final result = await confirm(
context,
"Delete Loan",
"Are you sure you want to delete the loan?",
);
if (!result) {
return;
}
// TODO
// await deleteLoan(item);
},
icon: Icon(Icons.delete, color: Colors.red),
),
title: Text(beneficiary.name),
);
},
),
Positioned(
right: 16,
bottom: 16,
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
showDialogOrModal(
context: context,
builder:
(_) => AddLoanPopup(
onDone: () {
Navigator.of(context).pop();
},
),
);
},
),
),
],
);
},
);
}
}

141
lib/ui/pages/settings.dart Normal file
View File

@@ -0,0 +1,141 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/utils.dart';
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context) {
return ListView(
children: [
BlocBuilder<SettingsCubit, SettingsWrapper>(
builder:
(context, state) => ListTile(
title: Text(t.pages.settings.colorSchemes.title),
subtitle: switch (state.settings.colorScheme) {
ColorSchemeSettings.dark => Text(
t.pages.settings.colorSchemes.dark,
),
ColorSchemeSettings.light => Text(
t.pages.settings.colorSchemes.light,
),
ColorSchemeSettings.system => Text(
t.pages.settings.colorSchemes.system,
),
},
onTap: () async {
final colorScheme = await showDialogOrModal(
context: context,
builder:
(context) => ListView(
shrinkWrap: true,
children:
ColorSchemeSettings.values
.map(
(s) => ListTile(
leading:
state.settings.colorScheme == s
? Icon(Icons.check)
: null,
title: Text(switch (s) {
ColorSchemeSettings.dark =>
t.pages.settings.colorSchemes.dark,
ColorSchemeSettings.light =>
t.pages.settings.colorSchemes.light,
ColorSchemeSettings.system =>
t.pages.settings.colorSchemes.system,
}),
onTap: () {
Navigator.of(context).pop(s);
},
),
)
.toList(),
),
);
if (colorScheme == null) {
return;
}
await GetIt.I.get<SettingsCubit>().setSettings(
state.settings.copyWith(colorScheme: colorScheme),
);
},
),
),
BlocBuilder<SettingsCubit, SettingsWrapper>(
builder:
(context, state) => ListTile(
title: Text("Sentry"),
subtitle: Text(
"When enabled sends errors to the configured Sentry DSN.",
),
trailing: IconButton(
icon: Icon(Icons.clear),
onPressed:
state.settings.sentryDsn == null
? null
: () async {
final result = await confirm(
context,
"Clear Sentry DSN",
"Are you sure you want to clear the Sentry DSN?",
);
if (!result) {
return;
}
await GetIt.I.get<SettingsCubit>().setSettings(
state.settings.copyWith(sentryDsn: null),
);
},
),
onTap: () async {
final controller = TextEditingController(
text: state.settings.sentryDsn ?? '',
);
final result = await showDialogOrModal<String>(
context: context,
builder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: controller,
decoration: InputDecoration(hintText: "Sentry DSN"),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton(
child: Text("Apply"),
onPressed: () {
Navigator.of(context).pop(controller.text);
},
),
],
),
],
);
},
);
if (result == null) {
return;
}
await GetIt.I.get<SettingsCubit>().setSettings(
state.settings.copyWith(sentryDsn: result),
);
},
),
),
],
);
}
}

View File

@@ -1,6 +1,8 @@
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/sqlite.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';
@@ -18,28 +20,78 @@ class TemplateListState extends State<TemplateListPage> {
return BlocBuilder<CoreCubit, CoreState>( return BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) { builder: (context, state) {
final account = GetIt.I.get<CoreCubit>().activeAccount; final account = GetIt.I.get<CoreCubit>().activeAccount;
final nonRecurringTemplates =
state.transactionTemplates
.where((t) => !t.template.recurring)
.toList();
return Stack( return Stack(
children: [ children: [
Column( CustomScrollView(
children: [ slivers: [
Padding( SliverToBoxAdapter(
padding: EdgeInsets.only(top: 16), child: Text(t.pages.templates.nonRecurring.title),
child: ListView.builder( ),
SliverList.builder(
itemCount: nonRecurringTemplates.length,
itemBuilder: (context, index) {
final template = nonRecurringTemplates[index];
return ListTile(
title: Text(template.template.name),
trailing: IconButton(
icon: Icon(Icons.delete),
color: Colors.red,
onPressed: () async {
final result = await confirm(
context,
t.pages.templates.removeTemplate.title,
t.pages.templates.removeTemplate.body(
name: template.template.name,
),
);
if (!result) {
return;
}
await GetIt.I
.get<OkaneDatabase>()
.transactionTemplatesDao
.deleteTemplate(template.template);
},
),
);
},
),
SliverToBoxAdapter(
child: Text(t.pages.templates.recurring.title),
),
SliverList.builder(
itemCount: state.recurringTransactions.length, itemCount: state.recurringTransactions.length,
shrinkWrap: true, itemBuilder: (context, index) {
itemBuilder: final template = state.recurringTransactions[index];
(ctx, idx) => Card( return ListTile(
child: ListTile( title: Text(template.template.name),
title: Text( trailing: IconButton(
state icon: Icon(Icons.delete, color: Colors.red),
.recurringTransactions[idx] onPressed: () async {
.template final result = await confirm(
.value! context,
.name, t.pages.templates.removeTemplate.title,
), t.pages.templates.removeTemplate.body(
), name: template.template.name,
), ),
);
if (!result) {
return;
}
await GetIt.I
.get<OkaneDatabase>()
.recurringTransactionsDao
.deleteTemplate(template);
},
), ),
);
},
), ),
], ],
), ),
@@ -47,7 +99,6 @@ class TemplateListState extends State<TemplateListPage> {
right: 16, right: 16,
bottom: 16, bottom: 16,
child: FloatingActionButton( child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: onPressed:
account == null account == null
? () {} ? () {}
@@ -65,6 +116,7 @@ class TemplateListState extends State<TemplateListPage> {
showDragHandle: true, showDragHandle: true,
); );
}, },
child: Icon(Icons.add),
), ),
), ),
], ],

View File

@@ -1,10 +1,12 @@
import 'dart:io'; import 'dart:io';
import 'package:drift/drift.dart' show Value;
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.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:okane/database/collections/beneficiary.dart'; import 'package:get_it/get_it.dart';
import 'package:okane/database/database.dart'; import 'package:okane/database/sqlite.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';
@@ -41,8 +43,9 @@ class TransactionDetailsPage extends StatelessWidget {
await File(file.path!).copy(imagePath); await File(file.path!).copy(imagePath);
print("Updating DB"); print("Updating DB");
beneficiary.imagePath = imagePath; await GetIt.I.get<OkaneDatabase>().beneficiariesDao.upsertBeneficiary(
await upsertBeneficiary(beneficiary); beneficiary.copyWith(imagePath: Value(imagePath)).toCompanion(false),
);
} }
@override @override
@@ -68,7 +71,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(
@@ -76,26 +81,66 @@ class TransactionDetailsPage extends StatelessWidget {
child: ListView( child: ListView(
children: [ children: [
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
StreamBuilder( StreamBuilder(
stream: watchBeneficiaryObject( stream: GetIt.I
state.activeTransaction!.beneficiary.value!.id, .get<OkaneDatabase>()
.beneficiariesDao
.watchBeneficiary(
state.activeTransaction!.beneficiary.id,
), ),
builder: (context, snapshot) { builder: (context, snapshot) {
final obj = final obj =
snapshot.data ?? snapshot.data ??
state.activeTransaction!.beneficiary.value!; state.activeTransaction!.beneficiary;
return ImageWrapper( return ImageWrapper(
title: obj.name, title: obj.name,
path: obj.imagePath, path: obj.imagePath,
onTap: () => _updateBeneficiaryIcon(obj), onTap: () => _updateBeneficiaryIcon(obj),
width: 90,
height: 90,
); );
}, },
), ),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.arrow_forward_rounded),
Padding( Padding(
padding: EdgeInsets.only(left: 8), padding: EdgeInsets.only(left: 8),
child: Text( child: Text(
state.activeTransaction!.beneficiary.value!.name, state
.activeTransaction!
.beneficiary
.name,
),
),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.arrow_back_rounded),
Padding(
padding: EdgeInsets.only(left: 8),
child: Text(
GetIt.I
.get<CoreCubit>()
.activeAccount!
.name,
),
),
],
),
],
), ),
), ),
Spacer(), Spacer(),
@@ -107,21 +152,53 @@ class TransactionDetailsPage extends StatelessWidget {
), ),
], ],
), ),
// TODO
/*
Wrap( Wrap(
spacing: 8, spacing: 8,
children: children:
state.activeTransaction!.tags state.activeTransaction!.tags
.map((tag) => Chip(label: Text(tag))) .map((tag) => Chip(label: Text(tag)))
.toList(), .toList(),
), ),*/
Row( if (state.activeTransaction!.expenseCategory != null)
Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
state.activeTransaction!.amount > 0 Text(t.common.expenseCategory.name),
Padding(
padding: EdgeInsets.only(left: 16),
child: Chip(
label: Text(
state
.activeTransaction!
.expenseCategory!
.name,
),
),
),
],
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
state.activeTransaction!.transaction.amount > 0
? Icon(Icons.add) ? Icon(Icons.add)
: Icon(Icons.remove), : Icon(Icons.remove),
Text(formatCurrency(state.activeTransaction!.amount)), Text(
formatCurrency(
state.activeTransaction!.transaction.amount,
),
),
], ],
), ),
),
], ],
), ),
); );

View File

@@ -2,7 +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:grouped_list/grouped_list.dart'; import 'package:grouped_list/grouped_list.dart';
import 'package:okane/database/collections/transaction.dart'; import 'package:okane/database/sqlite.dart';
import 'package:okane/screen.dart'; import 'package:okane/screen.dart';
import 'package:okane/ui/pages/account/balance_graph_card.dart'; import 'package:okane/ui/pages/account/balance_graph_card.dart';
import 'package:okane/ui/state/core.dart'; import 'package:okane/ui/state/core.dart';
@@ -41,7 +41,9 @@ class TransactionListState extends State<TransactionListPage> {
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
elements: state.transactions, elements: state.transactions,
reverse: true, reverse: true,
groupBy: (Transaction item) => formatDateTime(item.date), groupBy:
(TransactionDto item) =>
formatDateTime(item.transaction.date),
groupHeaderBuilder: groupHeaderBuilder:
(item) => Row( (item) => Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -54,7 +56,7 @@ class TransactionListState extends State<TransactionListPage> {
child: Padding( child: Padding(
padding: EdgeInsets.all(4), padding: EdgeInsets.all(4),
child: Text( child: Text(
formatDateTime(item.date), formatDateTime(item.transaction.date),
style: TextStyle(color: Colors.white), style: TextStyle(color: Colors.white),
), ),
), ),
@@ -83,58 +85,10 @@ class TransactionListState extends State<TransactionListPage> {
), ),
], ],
), ),
/*Column(
children: [
Padding(
padding: EdgeInsets.only(top: 16),
child: GroupedListView(
elements: state.transactions,
reverse: true,
groupBy:
(Transaction item) => formatDateTime(item.date),
groupHeaderBuilder:
(item) => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DecoratedBox(
decoration: BoxDecoration(
color: Colors.black.withAlpha(170),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: EdgeInsets.all(4),
child: Text(
formatDateTime(item.date),
style: TextStyle(color: Colors.white),
),
),
),
],
),
shrinkWrap: true,
indexedItemBuilder:
(ctx, item, idx) => TransactionCard(
transaction: item,
onTap: () {
GetIt.I.get<CoreCubit>().setActiveTransaction(
item,
);
if (getScreenSize(ctx) == ScreenSize.small) {
Navigator.of(
context,
).pushNamed("/transactions/details");
}
},
),
),
),
],
),*/
Positioned( Positioned(
right: 16, right: 16,
bottom: 16, bottom: 16,
child: FloatingActionButton( child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: onPressed:
account == null account == null
? () {} ? () {}
@@ -144,7 +98,7 @@ class TransactionListState extends State<TransactionListPage> {
builder: builder:
(ctx) => AddTransactionWidget( (ctx) => AddTransactionWidget(
activeAccountItem: account, activeAccountItem: account,
onAdd: () { onAdd: (_) {
setState(() {}); setState(() {});
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
@@ -152,6 +106,7 @@ class TransactionListState extends State<TransactionListPage> {
showDragHandle: true, showDragHandle: true,
); );
}, },
child: Icon(Icons.add),
), ),
), ),
], ],

View File

@@ -1,15 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:okane/database/collections/account.dart'; import 'package:get_it/get_it.dart';
import 'package:okane/database/collections/beneficiary.dart'; import 'package:okane/database/sqlite.dart';
import 'package:okane/database/collections/budget.dart';
import 'package:okane/database/collections/expense_category.dart';
import 'package:okane/database/collections/recurrent.dart';
import 'package:okane/database/collections/template.dart';
import 'package:okane/database/collections/transaction.dart';
import 'package:okane/database/database.dart';
import 'package:okane/ui/navigation.dart'; import 'package:okane/ui/navigation.dart';
part 'core.freezed.dart'; part 'core.freezed.dart';
@@ -19,15 +12,20 @@ abstract class CoreState with _$CoreState {
const factory CoreState({ const factory CoreState({
@Default(OkanePage.accounts) OkanePage activePage, @Default(OkanePage.accounts) OkanePage activePage,
int? activeAccountIndex, int? activeAccountIndex,
@Default(null) Transaction? activeTransaction, @Default(null) TransactionDto? activeTransaction,
@Default([]) List<Account> accounts, @Default([]) List<Account> accounts,
@Default([]) List<RecurringTransaction> recurringTransactions, @Default([]) List<RecurringTransactionDto> recurringTransactions,
@Default([]) List<Transaction> transactions, @Default([]) List<TransactionDto> transactions,
@Default([]) List<TransactionTemplate> transactionTemplates, @Default([]) List<TransactionTemplateDto> transactionTemplates,
@Default([]) List<Beneficiary> beneficiaries, @Default([]) List<Beneficiary> beneficiaries,
@Default([]) List<ExpenseCategory> expenseCategories, @Default([]) List<ExpenseCategory> expenseCategories,
@Default([]) List<Budget> budgets, @Default([]) List<BudgetsDto> budgets,
@Default(null) Budget? activeBudget, @Default([]) List<BudgetItemDto> budgetItems,
@Default(null) BudgetsDto? activeBudget,
@Default([]) List<LoanDto> loans,
@Default([]) List<LoanChange> loanChanges,
@Default(null) LoanDto? activeLoan,
@Default(false) bool isDeletingAccount,
}) = _CoreState; }) = _CoreState;
} }
@@ -41,86 +39,98 @@ class CoreCubit extends Cubit<CoreState> {
StreamSubscription<void>? _beneficiariesStreamSubscription; StreamSubscription<void>? _beneficiariesStreamSubscription;
StreamSubscription<void>? _expenseCategoryStreamSubscription; StreamSubscription<void>? _expenseCategoryStreamSubscription;
StreamSubscription<void>? _budgetsStreamSubscription; StreamSubscription<void>? _budgetsStreamSubscription;
StreamSubscription<void>? _budgetItemsStreamSubscription;
StreamSubscription<void>? _loanStreamSubscription;
StreamSubscription<void>? _loanChangesSubscription;
void setupAccountStream() { void setupAccountStream() {
_accountsStreamSubscription?.cancel(); _accountsStreamSubscription?.cancel();
_accountsStreamSubscription = watchAccounts().listen((_) async { _accountsStreamSubscription = GetIt.I
final resetStreams = state.activeAccountIndex == null; .get<OkaneDatabase>()
final accounts = await getAccounts(); .accountsDao
.accountsStream()
.listen((accounts) {
emit( emit(
state.copyWith( state.copyWith(
accounts: accounts, accounts: accounts,
activeAccountIndex: state.activeAccountIndex ?? 0, activeAccountIndex:
accounts.isNotEmpty ? state.activeAccountIndex ?? 0 : null,
), ),
); );
if (resetStreams) {
setupStreams(accounts[0]);
}
}); });
} }
void setupStreams(Account account) { void setupStreams(Account account) {
final db = GetIt.I.get<OkaneDatabase>();
setupAccountStream(); setupAccountStream();
_recurringTransactionStreamSubscription?.cancel(); _recurringTransactionStreamSubscription?.cancel();
_recurringTransactionStreamSubscription = watchRecurringTransactions( _recurringTransactionStreamSubscription = db.recurringTransactionsDao
activeAccount!, .recurringTransactionsStream(account)
).listen((_) async { .listen((recurring) async {
emit( emit(state.copyWith(recurringTransactions: recurring));
state.copyWith(
recurringTransactions: await getRecurringTransactions(activeAccount!),
),
);
}); });
_transactionTemplatesStreamSubcription?.cancel(); _transactionTemplatesStreamSubcription?.cancel();
_transactionTemplatesStreamSubcription = watchTransactionTemplates( _transactionTemplatesStreamSubcription = db.transactionTemplatesDao
activeAccount!, .transactionTemplatesStream(account)
).listen((_) async { .listen((templates) async {
emit( emit(state.copyWith(transactionTemplates: templates));
state.copyWith(
transactionTemplates: await getTransactionTemplates(activeAccount!),
),
);
}); });
_transactionsStreamSubscription?.cancel(); _transactionsStreamSubscription?.cancel();
_transactionsStreamSubscription = watchTransactions(activeAccount!).listen(( _transactionsStreamSubscription = db.transactionsDao
_, .transactionsStream(activeAccount!)
) async { .listen((transactions) async {
emit(state.copyWith(transactions: await getTransactions(activeAccount!))); emit(state.copyWith(transactions: transactions));
}); });
_beneficiariesStreamSubscription?.cancel(); _beneficiariesStreamSubscription?.cancel();
_beneficiariesStreamSubscription = watchBeneficiaries().listen((_) async { _beneficiariesStreamSubscription = db.beneficiariesDao
emit(state.copyWith(beneficiaries: await getBeneficiaries())); .beneficiariesStream()
.listen((beneficiaries) async {
emit(state.copyWith(beneficiaries: beneficiaries));
}); });
_expenseCategoryStreamSubscription?.cancel(); _expenseCategoryStreamSubscription?.cancel();
_expenseCategoryStreamSubscription = watchExpenseCategory().listen(( _expenseCategoryStreamSubscription = db.expenseCategoriesDao
_, .expenseCategoriesStream(account)
) async { .listen((expenseCategories) async {
emit(state.copyWith(expenseCategories: await getExpenseCategories())); emit(state.copyWith(expenseCategories: expenseCategories));
}); });
_budgetsStreamSubscription?.cancel(); _budgetsStreamSubscription?.cancel();
_budgetsStreamSubscription = watchBudgets(activeAccount!).listen((_) async { _budgetsStreamSubscription = db.budgetsDao
emit(state.copyWith(budgets: await getBudgets(activeAccount!))); .budgetsStream(activeAccount!)
.listen((budgets) async {
emit(state.copyWith(budgets: budgets));
});
_loanStreamSubscription?.cancel();
_loanStreamSubscription = db.loansDao.loansStream(account).listen((
loans,
) async {
emit(state.copyWith(loans: loans));
}); });
} }
void cancelStreams() {
_recurringTransactionStreamSubscription?.cancel();
_accountsStreamSubscription?.cancel();
_beneficiariesStreamSubscription?.cancel();
_budgetsStreamSubscription?.cancel();
_expenseCategoryStreamSubscription?.cancel();
_transactionsStreamSubscription?.cancel();
_transactionTemplatesStreamSubcription?.cancel();
_loanStreamSubscription?.cancel();
}
Future<void> init() async { Future<void> init() async {
final accounts = await getAccounts(); final db = GetIt.I.get<OkaneDatabase>();
final accounts = await db.accountsDao.getAccounts();
final account = accounts.isEmpty ? null : accounts[0]; final account = accounts.isEmpty ? null : accounts[0];
emit( emit(
state.copyWith( state.copyWith(
accounts: accounts, accounts: accounts,
activeAccountIndex: accounts.isEmpty ? null : 0, activeAccountIndex: accounts.isEmpty ? null : 0,
transactions: await getTransactions(account),
beneficiaries: await getBeneficiaries(),
transactionTemplates: await getTransactionTemplates(account),
recurringTransactions: await getRecurringTransactions(account),
expenseCategories: await getExpenseCategories(),
budgets: await getBudgets(account),
), ),
); );
if (account != null) { if (account != null) {
setupAccountStream();
setupStreams(account); setupStreams(account);
} else { } else {
setupAccountStream(); setupAccountStream();
@@ -133,23 +143,27 @@ class CoreCubit extends Cubit<CoreState> {
} }
Future<void> setActiveAccountIndex(int index) async { Future<void> setActiveAccountIndex(int index) async {
final db = GetIt.I.get<OkaneDatabase>();
final account = state.accounts[index]; final account = state.accounts[index];
emit( emit(
state.copyWith( state.copyWith(
activeAccountIndex: index, activeAccountIndex: index,
transactions: await getTransactions(account), transactions: await db.transactionsDao.getTransactions(account),
beneficiaries: await getBeneficiaries(), beneficiaries: await db.beneficiariesDao.getBeneficiaries(),
transactionTemplates: await getTransactionTemplates(account), transactionTemplates: await db.transactionTemplatesDao
recurringTransactions: await getRecurringTransactions(account), .getTransactionTemplates(account),
budgets: await getBudgets(account), recurringTransactions: await db.recurringTransactionsDao
.getRecurringTransactions(account),
budgets: await db.budgetsDao.getBudgets(account),
activeBudget: null, activeBudget: null,
activeTransaction: null, activeTransaction: null,
activeLoan: null,
), ),
); );
setupStreams(account); setupStreams(account);
} }
void setActiveTransaction(Transaction? item) { void setActiveTransaction(TransactionDto? item) {
emit(state.copyWith(activeTransaction: item)); emit(state.copyWith(activeTransaction: item));
} }
@@ -157,8 +171,47 @@ class CoreCubit extends Cubit<CoreState> {
emit(state.copyWith(accounts: accounts)); emit(state.copyWith(accounts: accounts));
} }
void setActiveBudget(Budget? budget) { void setActiveBudget(BudgetsDto? budget) {
emit(state.copyWith(activeBudget: budget)); emit(state.copyWith(activeBudget: budget, budgetItems: []));
_budgetItemsStreamSubscription?.cancel();
if (budget != null) {
_budgetItemsStreamSubscription = GetIt.I
.get<OkaneDatabase>()
.budgetsDao
.watchBudgetItems(budget.budget)
.listen((items) {
emit(state.copyWith(budgetItems: items));
});
}
}
Future<void> deleteAccount(Account account) async {
final l = List.of(state.accounts);
l.removeWhere((a) => a.id == account.id);
final newIndex = l.isEmpty ? null : 0;
emit(state.copyWith(activeAccountIndex: newIndex, isDeletingAccount: true));
cancelStreams();
try {
await GetIt.I.get<OkaneDatabase>().accountsDao.removeAccount(account);
} finally {
emit(state.copyWith(isDeletingAccount: false));
}
await init();
}
void setActiveLoan(LoanDto? loan) {
emit(state.copyWith(activeLoan: loan, loanChanges: []));
_loanChangesSubscription?.cancel();
if (loan != null) {
_loanChangesSubscription = GetIt.I
.get<OkaneDatabase>()
.loansDao
.watchLoanChanges(loan.loan)
.listen((changes) {
emit(state.copyWith(loanChanges: changes));
});
}
} }
Account? get activeAccount => Account? get activeAccount =>

View File

@@ -19,18 +19,23 @@ final _privateConstructorUsedError = UnsupportedError(
mixin _$CoreState { mixin _$CoreState {
OkanePage get activePage => throw _privateConstructorUsedError; OkanePage get activePage => throw _privateConstructorUsedError;
int? get activeAccountIndex => throw _privateConstructorUsedError; int? get activeAccountIndex => throw _privateConstructorUsedError;
Transaction? get activeTransaction => throw _privateConstructorUsedError; TransactionDto? get activeTransaction => throw _privateConstructorUsedError;
List<Account> get accounts => throw _privateConstructorUsedError; List<Account> get accounts => throw _privateConstructorUsedError;
List<RecurringTransaction> get recurringTransactions => List<RecurringTransactionDto> get recurringTransactions =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
List<Transaction> get transactions => throw _privateConstructorUsedError; List<TransactionDto> get transactions => throw _privateConstructorUsedError;
List<TransactionTemplate> get transactionTemplates => List<TransactionTemplateDto> get transactionTemplates =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
List<Beneficiary> get beneficiaries => throw _privateConstructorUsedError; List<Beneficiary> get beneficiaries => throw _privateConstructorUsedError;
List<ExpenseCategory> get expenseCategories => List<ExpenseCategory> get expenseCategories =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
List<Budget> get budgets => throw _privateConstructorUsedError; List<BudgetsDto> get budgets => throw _privateConstructorUsedError;
Budget? get activeBudget => throw _privateConstructorUsedError; List<BudgetItemDto> get budgetItems => throw _privateConstructorUsedError;
BudgetsDto? get activeBudget => throw _privateConstructorUsedError;
List<LoanDto> get loans => throw _privateConstructorUsedError;
List<LoanChange> get loanChanges => throw _privateConstructorUsedError;
LoanDto? get activeLoan => throw _privateConstructorUsedError;
bool get isDeletingAccount => throw _privateConstructorUsedError;
@JsonKey(ignore: true) @JsonKey(ignore: true)
$CoreStateCopyWith<CoreState> get copyWith => $CoreStateCopyWith<CoreState> get copyWith =>
@@ -45,15 +50,20 @@ abstract class $CoreStateCopyWith<$Res> {
$Res call({ $Res call({
OkanePage activePage, OkanePage activePage,
int? activeAccountIndex, int? activeAccountIndex,
Transaction? activeTransaction, TransactionDto? activeTransaction,
List<Account> accounts, List<Account> accounts,
List<RecurringTransaction> recurringTransactions, List<RecurringTransactionDto> recurringTransactions,
List<Transaction> transactions, List<TransactionDto> transactions,
List<TransactionTemplate> transactionTemplates, List<TransactionTemplateDto> transactionTemplates,
List<Beneficiary> beneficiaries, List<Beneficiary> beneficiaries,
List<ExpenseCategory> expenseCategories, List<ExpenseCategory> expenseCategories,
List<Budget> budgets, List<BudgetsDto> budgets,
Budget? activeBudget, List<BudgetItemDto> budgetItems,
BudgetsDto? activeBudget,
List<LoanDto> loans,
List<LoanChange> loanChanges,
LoanDto? activeLoan,
bool isDeletingAccount,
}); });
} }
@@ -80,7 +90,12 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
Object? beneficiaries = null, Object? beneficiaries = null,
Object? expenseCategories = null, Object? expenseCategories = null,
Object? budgets = null, Object? budgets = null,
Object? budgetItems = null,
Object? activeBudget = freezed, Object? activeBudget = freezed,
Object? loans = null,
Object? loanChanges = null,
Object? activeLoan = freezed,
Object? isDeletingAccount = null,
}) { }) {
return _then( return _then(
_value.copyWith( _value.copyWith(
@@ -98,7 +113,7 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
freezed == activeTransaction freezed == activeTransaction
? _value.activeTransaction ? _value.activeTransaction
: activeTransaction // ignore: cast_nullable_to_non_nullable : activeTransaction // ignore: cast_nullable_to_non_nullable
as Transaction?, as TransactionDto?,
accounts: accounts:
null == accounts null == accounts
? _value.accounts ? _value.accounts
@@ -108,17 +123,17 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
null == recurringTransactions null == recurringTransactions
? _value.recurringTransactions ? _value.recurringTransactions
: recurringTransactions // ignore: cast_nullable_to_non_nullable : recurringTransactions // ignore: cast_nullable_to_non_nullable
as List<RecurringTransaction>, as List<RecurringTransactionDto>,
transactions: transactions:
null == transactions null == transactions
? _value.transactions ? _value.transactions
: transactions // ignore: cast_nullable_to_non_nullable : transactions // ignore: cast_nullable_to_non_nullable
as List<Transaction>, as List<TransactionDto>,
transactionTemplates: transactionTemplates:
null == transactionTemplates null == transactionTemplates
? _value.transactionTemplates ? _value.transactionTemplates
: transactionTemplates // ignore: cast_nullable_to_non_nullable : transactionTemplates // ignore: cast_nullable_to_non_nullable
as List<TransactionTemplate>, as List<TransactionTemplateDto>,
beneficiaries: beneficiaries:
null == beneficiaries null == beneficiaries
? _value.beneficiaries ? _value.beneficiaries
@@ -133,12 +148,37 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
null == budgets null == budgets
? _value.budgets ? _value.budgets
: budgets // ignore: cast_nullable_to_non_nullable : budgets // ignore: cast_nullable_to_non_nullable
as List<Budget>, as List<BudgetsDto>,
budgetItems:
null == budgetItems
? _value.budgetItems
: budgetItems // ignore: cast_nullable_to_non_nullable
as List<BudgetItemDto>,
activeBudget: activeBudget:
freezed == activeBudget freezed == activeBudget
? _value.activeBudget ? _value.activeBudget
: activeBudget // ignore: cast_nullable_to_non_nullable : activeBudget // ignore: cast_nullable_to_non_nullable
as Budget?, as BudgetsDto?,
loans:
null == loans
? _value.loans
: loans // ignore: cast_nullable_to_non_nullable
as List<LoanDto>,
loanChanges:
null == loanChanges
? _value.loanChanges
: loanChanges // ignore: cast_nullable_to_non_nullable
as List<LoanChange>,
activeLoan:
freezed == activeLoan
? _value.activeLoan
: activeLoan // ignore: cast_nullable_to_non_nullable
as LoanDto?,
isDeletingAccount:
null == isDeletingAccount
? _value.isDeletingAccount
: isDeletingAccount // ignore: cast_nullable_to_non_nullable
as bool,
) )
as $Val, as $Val,
); );
@@ -157,15 +197,20 @@ abstract class _$$CoreStateImplCopyWith<$Res>
$Res call({ $Res call({
OkanePage activePage, OkanePage activePage,
int? activeAccountIndex, int? activeAccountIndex,
Transaction? activeTransaction, TransactionDto? activeTransaction,
List<Account> accounts, List<Account> accounts,
List<RecurringTransaction> recurringTransactions, List<RecurringTransactionDto> recurringTransactions,
List<Transaction> transactions, List<TransactionDto> transactions,
List<TransactionTemplate> transactionTemplates, List<TransactionTemplateDto> transactionTemplates,
List<Beneficiary> beneficiaries, List<Beneficiary> beneficiaries,
List<ExpenseCategory> expenseCategories, List<ExpenseCategory> expenseCategories,
List<Budget> budgets, List<BudgetsDto> budgets,
Budget? activeBudget, List<BudgetItemDto> budgetItems,
BudgetsDto? activeBudget,
List<LoanDto> loans,
List<LoanChange> loanChanges,
LoanDto? activeLoan,
bool isDeletingAccount,
}); });
} }
@@ -191,7 +236,12 @@ class __$$CoreStateImplCopyWithImpl<$Res>
Object? beneficiaries = null, Object? beneficiaries = null,
Object? expenseCategories = null, Object? expenseCategories = null,
Object? budgets = null, Object? budgets = null,
Object? budgetItems = null,
Object? activeBudget = freezed, Object? activeBudget = freezed,
Object? loans = null,
Object? loanChanges = null,
Object? activeLoan = freezed,
Object? isDeletingAccount = null,
}) { }) {
return _then( return _then(
_$CoreStateImpl( _$CoreStateImpl(
@@ -209,7 +259,7 @@ class __$$CoreStateImplCopyWithImpl<$Res>
freezed == activeTransaction freezed == activeTransaction
? _value.activeTransaction ? _value.activeTransaction
: activeTransaction // ignore: cast_nullable_to_non_nullable : activeTransaction // ignore: cast_nullable_to_non_nullable
as Transaction?, as TransactionDto?,
accounts: accounts:
null == accounts null == accounts
? _value._accounts ? _value._accounts
@@ -219,17 +269,17 @@ class __$$CoreStateImplCopyWithImpl<$Res>
null == recurringTransactions null == recurringTransactions
? _value._recurringTransactions ? _value._recurringTransactions
: recurringTransactions // ignore: cast_nullable_to_non_nullable : recurringTransactions // ignore: cast_nullable_to_non_nullable
as List<RecurringTransaction>, as List<RecurringTransactionDto>,
transactions: transactions:
null == transactions null == transactions
? _value._transactions ? _value._transactions
: transactions // ignore: cast_nullable_to_non_nullable : transactions // ignore: cast_nullable_to_non_nullable
as List<Transaction>, as List<TransactionDto>,
transactionTemplates: transactionTemplates:
null == transactionTemplates null == transactionTemplates
? _value._transactionTemplates ? _value._transactionTemplates
: transactionTemplates // ignore: cast_nullable_to_non_nullable : transactionTemplates // ignore: cast_nullable_to_non_nullable
as List<TransactionTemplate>, as List<TransactionTemplateDto>,
beneficiaries: beneficiaries:
null == beneficiaries null == beneficiaries
? _value._beneficiaries ? _value._beneficiaries
@@ -244,12 +294,37 @@ class __$$CoreStateImplCopyWithImpl<$Res>
null == budgets null == budgets
? _value._budgets ? _value._budgets
: budgets // ignore: cast_nullable_to_non_nullable : budgets // ignore: cast_nullable_to_non_nullable
as List<Budget>, as List<BudgetsDto>,
budgetItems:
null == budgetItems
? _value._budgetItems
: budgetItems // ignore: cast_nullable_to_non_nullable
as List<BudgetItemDto>,
activeBudget: activeBudget:
freezed == activeBudget freezed == activeBudget
? _value.activeBudget ? _value.activeBudget
: activeBudget // ignore: cast_nullable_to_non_nullable : activeBudget // ignore: cast_nullable_to_non_nullable
as Budget?, as BudgetsDto?,
loans:
null == loans
? _value._loans
: loans // ignore: cast_nullable_to_non_nullable
as List<LoanDto>,
loanChanges:
null == loanChanges
? _value._loanChanges
: loanChanges // ignore: cast_nullable_to_non_nullable
as List<LoanChange>,
activeLoan:
freezed == activeLoan
? _value.activeLoan
: activeLoan // ignore: cast_nullable_to_non_nullable
as LoanDto?,
isDeletingAccount:
null == isDeletingAccount
? _value.isDeletingAccount
: isDeletingAccount // ignore: cast_nullable_to_non_nullable
as bool,
), ),
); );
} }
@@ -263,20 +338,28 @@ class _$CoreStateImpl implements _CoreState {
this.activeAccountIndex, this.activeAccountIndex,
this.activeTransaction = null, this.activeTransaction = null,
final List<Account> accounts = const [], final List<Account> accounts = const [],
final List<RecurringTransaction> recurringTransactions = const [], final List<RecurringTransactionDto> recurringTransactions = const [],
final List<Transaction> transactions = const [], final List<TransactionDto> transactions = const [],
final List<TransactionTemplate> transactionTemplates = const [], final List<TransactionTemplateDto> transactionTemplates = const [],
final List<Beneficiary> beneficiaries = const [], final List<Beneficiary> beneficiaries = const [],
final List<ExpenseCategory> expenseCategories = const [], final List<ExpenseCategory> expenseCategories = const [],
final List<Budget> budgets = const [], final List<BudgetsDto> budgets = const [],
final List<BudgetItemDto> budgetItems = const [],
this.activeBudget = null, this.activeBudget = null,
final List<LoanDto> loans = const [],
final List<LoanChange> loanChanges = const [],
this.activeLoan = null,
this.isDeletingAccount = false,
}) : _accounts = accounts, }) : _accounts = accounts,
_recurringTransactions = recurringTransactions, _recurringTransactions = recurringTransactions,
_transactions = transactions, _transactions = transactions,
_transactionTemplates = transactionTemplates, _transactionTemplates = transactionTemplates,
_beneficiaries = beneficiaries, _beneficiaries = beneficiaries,
_expenseCategories = expenseCategories, _expenseCategories = expenseCategories,
_budgets = budgets; _budgets = budgets,
_budgetItems = budgetItems,
_loans = loans,
_loanChanges = loanChanges;
@override @override
@JsonKey() @JsonKey()
@@ -285,7 +368,7 @@ class _$CoreStateImpl implements _CoreState {
final int? activeAccountIndex; final int? activeAccountIndex;
@override @override
@JsonKey() @JsonKey()
final Transaction? activeTransaction; final TransactionDto? activeTransaction;
final List<Account> _accounts; final List<Account> _accounts;
@override @override
@JsonKey() @JsonKey()
@@ -295,29 +378,29 @@ class _$CoreStateImpl implements _CoreState {
return EqualUnmodifiableListView(_accounts); return EqualUnmodifiableListView(_accounts);
} }
final List<RecurringTransaction> _recurringTransactions; final List<RecurringTransactionDto> _recurringTransactions;
@override @override
@JsonKey() @JsonKey()
List<RecurringTransaction> get recurringTransactions { List<RecurringTransactionDto> get recurringTransactions {
if (_recurringTransactions is EqualUnmodifiableListView) if (_recurringTransactions is EqualUnmodifiableListView)
return _recurringTransactions; return _recurringTransactions;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_recurringTransactions); return EqualUnmodifiableListView(_recurringTransactions);
} }
final List<Transaction> _transactions; final List<TransactionDto> _transactions;
@override @override
@JsonKey() @JsonKey()
List<Transaction> get transactions { List<TransactionDto> get transactions {
if (_transactions is EqualUnmodifiableListView) return _transactions; if (_transactions is EqualUnmodifiableListView) return _transactions;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_transactions); return EqualUnmodifiableListView(_transactions);
} }
final List<TransactionTemplate> _transactionTemplates; final List<TransactionTemplateDto> _transactionTemplates;
@override @override
@JsonKey() @JsonKey()
List<TransactionTemplate> get transactionTemplates { List<TransactionTemplateDto> get transactionTemplates {
if (_transactionTemplates is EqualUnmodifiableListView) if (_transactionTemplates is EqualUnmodifiableListView)
return _transactionTemplates; return _transactionTemplates;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
@@ -343,22 +426,55 @@ class _$CoreStateImpl implements _CoreState {
return EqualUnmodifiableListView(_expenseCategories); return EqualUnmodifiableListView(_expenseCategories);
} }
final List<Budget> _budgets; final List<BudgetsDto> _budgets;
@override @override
@JsonKey() @JsonKey()
List<Budget> get budgets { List<BudgetsDto> get budgets {
if (_budgets is EqualUnmodifiableListView) return _budgets; if (_budgets is EqualUnmodifiableListView) return _budgets;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_budgets); return EqualUnmodifiableListView(_budgets);
} }
final List<BudgetItemDto> _budgetItems;
@override @override
@JsonKey() @JsonKey()
final Budget? activeBudget; List<BudgetItemDto> get budgetItems {
if (_budgetItems is EqualUnmodifiableListView) return _budgetItems;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_budgetItems);
}
@override
@JsonKey()
final BudgetsDto? activeBudget;
final List<LoanDto> _loans;
@override
@JsonKey()
List<LoanDto> get loans {
if (_loans is EqualUnmodifiableListView) return _loans;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_loans);
}
final List<LoanChange> _loanChanges;
@override
@JsonKey()
List<LoanChange> get loanChanges {
if (_loanChanges is EqualUnmodifiableListView) return _loanChanges;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_loanChanges);
}
@override
@JsonKey()
final LoanDto? activeLoan;
@override
@JsonKey()
final bool isDeletingAccount;
@override @override
String toString() { String toString() {
return 'CoreState(activePage: $activePage, activeAccountIndex: $activeAccountIndex, activeTransaction: $activeTransaction, accounts: $accounts, recurringTransactions: $recurringTransactions, transactions: $transactions, transactionTemplates: $transactionTemplates, beneficiaries: $beneficiaries, expenseCategories: $expenseCategories, budgets: $budgets, activeBudget: $activeBudget)'; return 'CoreState(activePage: $activePage, activeAccountIndex: $activeAccountIndex, activeTransaction: $activeTransaction, accounts: $accounts, recurringTransactions: $recurringTransactions, transactions: $transactions, transactionTemplates: $transactionTemplates, beneficiaries: $beneficiaries, expenseCategories: $expenseCategories, budgets: $budgets, budgetItems: $budgetItems, activeBudget: $activeBudget, loans: $loans, loanChanges: $loanChanges, activeLoan: $activeLoan, isDeletingAccount: $isDeletingAccount)';
} }
@override @override
@@ -394,8 +510,21 @@ class _$CoreStateImpl implements _CoreState {
_expenseCategories, _expenseCategories,
) && ) &&
const DeepCollectionEquality().equals(other._budgets, _budgets) && const DeepCollectionEquality().equals(other._budgets, _budgets) &&
const DeepCollectionEquality().equals(
other._budgetItems,
_budgetItems,
) &&
(identical(other.activeBudget, activeBudget) || (identical(other.activeBudget, activeBudget) ||
other.activeBudget == activeBudget)); other.activeBudget == activeBudget) &&
const DeepCollectionEquality().equals(other._loans, _loans) &&
const DeepCollectionEquality().equals(
other._loanChanges,
_loanChanges,
) &&
(identical(other.activeLoan, activeLoan) ||
other.activeLoan == activeLoan) &&
(identical(other.isDeletingAccount, isDeletingAccount) ||
other.isDeletingAccount == isDeletingAccount));
} }
@override @override
@@ -411,7 +540,12 @@ class _$CoreStateImpl implements _CoreState {
const DeepCollectionEquality().hash(_beneficiaries), const DeepCollectionEquality().hash(_beneficiaries),
const DeepCollectionEquality().hash(_expenseCategories), const DeepCollectionEquality().hash(_expenseCategories),
const DeepCollectionEquality().hash(_budgets), const DeepCollectionEquality().hash(_budgets),
const DeepCollectionEquality().hash(_budgetItems),
activeBudget, activeBudget,
const DeepCollectionEquality().hash(_loans),
const DeepCollectionEquality().hash(_loanChanges),
activeLoan,
isDeletingAccount,
); );
@JsonKey(ignore: true) @JsonKey(ignore: true)
@@ -425,15 +559,20 @@ abstract class _CoreState implements CoreState {
const factory _CoreState({ const factory _CoreState({
final OkanePage activePage, final OkanePage activePage,
final int? activeAccountIndex, final int? activeAccountIndex,
final Transaction? activeTransaction, final TransactionDto? activeTransaction,
final List<Account> accounts, final List<Account> accounts,
final List<RecurringTransaction> recurringTransactions, final List<RecurringTransactionDto> recurringTransactions,
final List<Transaction> transactions, final List<TransactionDto> transactions,
final List<TransactionTemplate> transactionTemplates, final List<TransactionTemplateDto> transactionTemplates,
final List<Beneficiary> beneficiaries, final List<Beneficiary> beneficiaries,
final List<ExpenseCategory> expenseCategories, final List<ExpenseCategory> expenseCategories,
final List<Budget> budgets, final List<BudgetsDto> budgets,
final Budget? activeBudget, final List<BudgetItemDto> budgetItems,
final BudgetsDto? activeBudget,
final List<LoanDto> loans,
final List<LoanChange> loanChanges,
final LoanDto? activeLoan,
final bool isDeletingAccount,
}) = _$CoreStateImpl; }) = _$CoreStateImpl;
@override @override
@@ -441,23 +580,33 @@ abstract class _CoreState implements CoreState {
@override @override
int? get activeAccountIndex; int? get activeAccountIndex;
@override @override
Transaction? get activeTransaction; TransactionDto? get activeTransaction;
@override @override
List<Account> get accounts; List<Account> get accounts;
@override @override
List<RecurringTransaction> get recurringTransactions; List<RecurringTransactionDto> get recurringTransactions;
@override @override
List<Transaction> get transactions; List<TransactionDto> get transactions;
@override @override
List<TransactionTemplate> get transactionTemplates; List<TransactionTemplateDto> get transactionTemplates;
@override @override
List<Beneficiary> get beneficiaries; List<Beneficiary> get beneficiaries;
@override @override
List<ExpenseCategory> get expenseCategories; List<ExpenseCategory> get expenseCategories;
@override @override
List<Budget> get budgets; List<BudgetsDto> get budgets;
@override @override
Budget? get activeBudget; List<BudgetItemDto> get budgetItems;
@override
BudgetsDto? get activeBudget;
@override
List<LoanDto> get loans;
@override
List<LoanChange> get loanChanges;
@override
LoanDto? get activeLoan;
@override
bool get isDeletingAccount;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$CoreStateImplCopyWith<_$CoreStateImpl> get copyWith => _$$CoreStateImplCopyWith<_$CoreStateImpl> get copyWith =>

View File

@@ -0,0 +1,51 @@
import 'dart:convert';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'settings.freezed.dart';
part 'settings.g.dart';
enum ColorSchemeSettings { light, dark, system }
@freezed
abstract class Settings with _$Settings {
const factory Settings({
@Default(ColorSchemeSettings.system) ColorSchemeSettings colorScheme,
@Default(null) String? sentryDsn,
}) = _Settings;
factory Settings.fromJson(Map<String, Object?> json) =>
_$SettingsFromJson(json);
}
@freezed
abstract class SettingsWrapper with _$SettingsWrapper {
const factory SettingsWrapper({@Default(Settings()) Settings settings}) =
_SettingsWrapper;
}
class SettingsCubit extends Cubit<SettingsWrapper> {
final SharedPreferencesAsync _prefs = SharedPreferencesAsync();
SettingsCubit() : super(SettingsWrapper());
Future<void> loadSettings() async {
final value = await _prefs.getString("settings");
if (value == null) {
await _prefs.setString("settings", jsonEncode(Settings().toJson()));
return;
}
emit(state.copyWith(settings: Settings.fromJson(jsonDecode(value))));
}
Future<void> setSettings(Settings settings) async {
emit(state.copyWith(settings: settings));
await _prefs.setString("settings", jsonEncode(settings.toJson()));
}
String? get sentryDsn => state.settings.sentryDsn;
}

View File

@@ -0,0 +1,322 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'settings.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
Settings _$SettingsFromJson(Map<String, dynamic> json) {
return _Settings.fromJson(json);
}
/// @nodoc
mixin _$Settings {
ColorSchemeSettings get colorScheme => throw _privateConstructorUsedError;
String? get sentryDsn => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$SettingsCopyWith<Settings> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SettingsCopyWith<$Res> {
factory $SettingsCopyWith(Settings value, $Res Function(Settings) then) =
_$SettingsCopyWithImpl<$Res, Settings>;
@useResult
$Res call({ColorSchemeSettings colorScheme, String? sentryDsn});
}
/// @nodoc
class _$SettingsCopyWithImpl<$Res, $Val extends Settings>
implements $SettingsCopyWith<$Res> {
_$SettingsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({Object? colorScheme = null, Object? sentryDsn = freezed}) {
return _then(
_value.copyWith(
colorScheme:
null == colorScheme
? _value.colorScheme
: colorScheme // ignore: cast_nullable_to_non_nullable
as ColorSchemeSettings,
sentryDsn:
freezed == sentryDsn
? _value.sentryDsn
: sentryDsn // ignore: cast_nullable_to_non_nullable
as String?,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$SettingsImplCopyWith<$Res>
implements $SettingsCopyWith<$Res> {
factory _$$SettingsImplCopyWith(
_$SettingsImpl value,
$Res Function(_$SettingsImpl) then,
) = __$$SettingsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({ColorSchemeSettings colorScheme, String? sentryDsn});
}
/// @nodoc
class __$$SettingsImplCopyWithImpl<$Res>
extends _$SettingsCopyWithImpl<$Res, _$SettingsImpl>
implements _$$SettingsImplCopyWith<$Res> {
__$$SettingsImplCopyWithImpl(
_$SettingsImpl _value,
$Res Function(_$SettingsImpl) _then,
) : super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({Object? colorScheme = null, Object? sentryDsn = freezed}) {
return _then(
_$SettingsImpl(
colorScheme:
null == colorScheme
? _value.colorScheme
: colorScheme // ignore: cast_nullable_to_non_nullable
as ColorSchemeSettings,
sentryDsn:
freezed == sentryDsn
? _value.sentryDsn
: sentryDsn // ignore: cast_nullable_to_non_nullable
as String?,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$SettingsImpl implements _Settings {
const _$SettingsImpl({
this.colorScheme = ColorSchemeSettings.system,
this.sentryDsn = null,
});
factory _$SettingsImpl.fromJson(Map<String, dynamic> json) =>
_$$SettingsImplFromJson(json);
@override
@JsonKey()
final ColorSchemeSettings colorScheme;
@override
@JsonKey()
final String? sentryDsn;
@override
String toString() {
return 'Settings(colorScheme: $colorScheme, sentryDsn: $sentryDsn)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SettingsImpl &&
(identical(other.colorScheme, colorScheme) ||
other.colorScheme == colorScheme) &&
(identical(other.sentryDsn, sentryDsn) ||
other.sentryDsn == sentryDsn));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, colorScheme, sentryDsn);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$SettingsImplCopyWith<_$SettingsImpl> get copyWith =>
__$$SettingsImplCopyWithImpl<_$SettingsImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SettingsImplToJson(this);
}
}
abstract class _Settings implements Settings {
const factory _Settings({
final ColorSchemeSettings colorScheme,
final String? sentryDsn,
}) = _$SettingsImpl;
factory _Settings.fromJson(Map<String, dynamic> json) =
_$SettingsImpl.fromJson;
@override
ColorSchemeSettings get colorScheme;
@override
String? get sentryDsn;
@override
@JsonKey(ignore: true)
_$$SettingsImplCopyWith<_$SettingsImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$SettingsWrapper {
Settings get settings => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$SettingsWrapperCopyWith<SettingsWrapper> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SettingsWrapperCopyWith<$Res> {
factory $SettingsWrapperCopyWith(
SettingsWrapper value,
$Res Function(SettingsWrapper) then,
) = _$SettingsWrapperCopyWithImpl<$Res, SettingsWrapper>;
@useResult
$Res call({Settings settings});
$SettingsCopyWith<$Res> get settings;
}
/// @nodoc
class _$SettingsWrapperCopyWithImpl<$Res, $Val extends SettingsWrapper>
implements $SettingsWrapperCopyWith<$Res> {
_$SettingsWrapperCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({Object? settings = null}) {
return _then(
_value.copyWith(
settings:
null == settings
? _value.settings
: settings // ignore: cast_nullable_to_non_nullable
as Settings,
)
as $Val,
);
}
@override
@pragma('vm:prefer-inline')
$SettingsCopyWith<$Res> get settings {
return $SettingsCopyWith<$Res>(_value.settings, (value) {
return _then(_value.copyWith(settings: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$SettingsWrapperImplCopyWith<$Res>
implements $SettingsWrapperCopyWith<$Res> {
factory _$$SettingsWrapperImplCopyWith(
_$SettingsWrapperImpl value,
$Res Function(_$SettingsWrapperImpl) then,
) = __$$SettingsWrapperImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({Settings settings});
@override
$SettingsCopyWith<$Res> get settings;
}
/// @nodoc
class __$$SettingsWrapperImplCopyWithImpl<$Res>
extends _$SettingsWrapperCopyWithImpl<$Res, _$SettingsWrapperImpl>
implements _$$SettingsWrapperImplCopyWith<$Res> {
__$$SettingsWrapperImplCopyWithImpl(
_$SettingsWrapperImpl _value,
$Res Function(_$SettingsWrapperImpl) _then,
) : super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({Object? settings = null}) {
return _then(
_$SettingsWrapperImpl(
settings:
null == settings
? _value.settings
: settings // ignore: cast_nullable_to_non_nullable
as Settings,
),
);
}
}
/// @nodoc
class _$SettingsWrapperImpl implements _SettingsWrapper {
const _$SettingsWrapperImpl({this.settings = const Settings()});
@override
@JsonKey()
final Settings settings;
@override
String toString() {
return 'SettingsWrapper(settings: $settings)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SettingsWrapperImpl &&
(identical(other.settings, settings) ||
other.settings == settings));
}
@override
int get hashCode => Object.hash(runtimeType, settings);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$SettingsWrapperImplCopyWith<_$SettingsWrapperImpl> get copyWith =>
__$$SettingsWrapperImplCopyWithImpl<_$SettingsWrapperImpl>(
this,
_$identity,
);
}
abstract class _SettingsWrapper implements SettingsWrapper {
const factory _SettingsWrapper({final Settings settings}) =
_$SettingsWrapperImpl;
@override
Settings get settings;
@override
@JsonKey(ignore: true)
_$$SettingsWrapperImplCopyWith<_$SettingsWrapperImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,30 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'settings.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$SettingsImpl _$$SettingsImplFromJson(Map<String, dynamic> json) =>
_$SettingsImpl(
colorScheme:
$enumDecodeNullable(
_$ColorSchemeSettingsEnumMap,
json['colorScheme'],
) ??
ColorSchemeSettings.system,
sentryDsn: json['sentryDsn'] as String? ?? null,
);
Map<String, dynamic> _$$SettingsImplToJson(_$SettingsImpl instance) =>
<String, dynamic>{
'colorScheme': _$ColorSchemeSettingsEnumMap[instance.colorScheme]!,
'sentryDsn': instance.sentryDsn,
};
const _$ColorSchemeSettingsEnumMap = {
ColorSchemeSettings.light: 'light',
ColorSchemeSettings.dark: 'dark',
ColorSchemeSettings.system: 'system',
};

View File

@@ -1,10 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:okane/database/sqlite.dart';
import 'package:okane/screen.dart'; import 'package:okane/screen.dart';
import 'package:okane/ui/state/core.dart';
Future<T?> showDialogOrModal<T>({ Future<T?> showDialogOrModal<T>({
required BuildContext context, required BuildContext context,
required WidgetBuilder builder, required WidgetBuilder builder,
bool showDragHandle = true, bool showDragHandle = true,
bool horizontalPaddingOnMobile = true,
}) { }) {
final screenSize = getScreenSize(context); final screenSize = getScreenSize(context);
final width = MediaQuery.sizeOf(context).shortestSide; final width = MediaQuery.sizeOf(context).shortestSide;
@@ -18,6 +22,8 @@ Future<T?> showDialogOrModal<T>({
(context) => Padding( (context) => Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: 32 + MediaQuery.of(context).viewInsets.bottom, bottom: 32 + MediaQuery.of(context).viewInsets.bottom,
left: horizontalPaddingOnMobile ? 16 : 0,
right: horizontalPaddingOnMobile ? 16 : 0,
), ),
child: builder(context), child: builder(context),
), ),
@@ -26,6 +32,8 @@ Future<T?> showDialogOrModal<T>({
context: context, context: context,
builder: builder:
(context) => Dialog( (context) => Dialog(
child: Padding(
padding: EdgeInsets.only(top: 16),
child: Container( child: Container(
constraints: BoxConstraints(maxWidth: width * 0.7), constraints: BoxConstraints(maxWidth: width * 0.7),
child: Padding( child: Padding(
@@ -35,9 +43,43 @@ Future<T?> showDialogOrModal<T>({
), ),
), ),
), ),
),
}; };
} }
Future<TransactionTemplateDto?> selectTransactionTemplate(
BuildContext context,
) {
return showDialogOrModal<TransactionTemplateDto>(
context: context,
builder: (context) {
return BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) {
if (state.transactionTemplates.isEmpty) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text("No templates defined"),
);
}
return ListView.builder(
itemCount: state.transactionTemplates.length,
itemBuilder:
(context, index) => ListTile(
title: Text(state.transactionTemplates[index].template.name),
onTap: () {
Navigator.of(
context,
).pop(state.transactionTemplates[index]);
},
),
);
},
);
},
);
}
DateTime toMidnight(DateTime t) { DateTime toMidnight(DateTime t) {
return DateTime(t.year, t.month, t.day); return DateTime(t.year, t.month, t.day);
} }
@@ -76,3 +118,43 @@ String formatCurrency(double amount, {bool precise = true}) {
DateTime monthEnding(DateTime now) { DateTime monthEnding(DateTime now) {
return DateTime(now.year, now.month, 32, 23, 59, 59); return DateTime(now.year, now.month, 32, 23, 59, 59);
} }
Future<bool> confirm(BuildContext context, String title, String body) async {
final result = await showDialog<bool>(
context: context,
builder:
(context) => AlertDialog(
title: Text(title),
content: Text(body),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text("Delete", style: TextStyle(color: Colors.red)),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text("Cancel"),
),
],
),
);
return result ?? false;
}
bool isTransactionDue(RecurringTransaction r, DateTime now) {
if (r.lastExecution == null) {
return true;
}
final expectedNextExecution = r.lastExecution!.add(Duration(days: r.days));
if (now.isAfter(expectedNextExecution)) {
return true;
}
return now.difference(expectedNextExecution).inDays.abs() <=
(r.days * 0.5).toInt();
}

View File

@@ -1,11 +1,86 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:okane/ui/pages/account/delete_account.dart';
import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/utils.dart';
class AccountSwitcher extends StatelessWidget {
const AccountSwitcher({super.key});
@override
Widget build(BuildContext context) {
final bloc = GetIt.I.get<CoreCubit>();
return BlocBuilder<CoreCubit, CoreState>(
builder:
(context, state) => InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {
showDialogOrModal(
context: context,
builder:
(context) => ListView.builder(
shrinkWrap: true,
itemCount: state.accounts.length,
itemBuilder: (context, index) {
final item = state.accounts[index];
return ListTile(
title: Text(item.name),
trailing: IconButton(
icon: Icon(Icons.delete),
color: Colors.red,
onPressed: () async {
await showDialog(
context: context,
barrierDismissible: false,
builder:
(context) => DeleteAccountPopup(
account: item,
onCancel: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
},
afterDelete: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
},
),
);
},
),
onTap: () {
GetIt.I.get<CoreCubit>().setActiveAccountIndex(
index,
);
Navigator.of(context).pop();
},
);
},
),
);
},
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
bloc.activeAccount!.name,
style: Theme.of(context).textTheme.titleLarge,
),
Icon(Icons.arrow_drop_down),
],
),
),
),
);
}
}
class AccountIndicator extends StatelessWidget { class AccountIndicator extends StatelessWidget {
final String accountName;
final Widget? trailing; final Widget? trailing;
const AccountIndicator({super.key, this.trailing, required this.accountName}); const AccountIndicator({super.key, this.trailing});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -15,11 +90,8 @@ class AccountIndicator extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Padding( Padding(
padding: EdgeInsets.all(8), padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Text( child: AccountSwitcher(),
accountName,
style: Theme.of(context).textTheme.titleLarge,
),
), ),
const Spacer(), const Spacer(),
if (trailing != null) trailing!, if (trailing != null) trailing!,

View File

@@ -1,7 +1,9 @@
import 'package:drift/drift.dart' show Value;
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:okane/database/collections/expense_category.dart'; import 'package:get_it/get_it.dart';
import 'package:okane/database/database.dart'; import 'package:okane/database/sqlite.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 +41,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(
@@ -47,14 +51,18 @@ class AddExpenseCategoryState extends State<AddExpenseCategory> {
Spacer(), Spacer(),
OutlinedButton( OutlinedButton(
onPressed: () async { onPressed: () async {
final category = final category = await GetIt.I
ExpenseCategory() .get<OkaneDatabase>()
..name = _categoryNameController.text; .expenseCategoriesDao
await upsertExpenseCategory(category); .upsertCategory(
ExpenseCategoriesCompanion(
name: Value(_categoryNameController.text),
),
);
_categoryNameController.text = ""; _categoryNameController.text = "";
Navigator.of(context).pop(category); Navigator.of(context).pop(category);
}, },
child: Text("Add"), child: Text(t.modals.add),
), ),
], ],
), ),

View File

@@ -1,284 +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: 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: "Beneficiary",
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"),
),
),
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: 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"),
),
),
],
),
);
}
}

View File

@@ -1,17 +1,17 @@
import 'package:drift/drift.dart' show Value;
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/account.dart'; import 'package:okane/database/sqlite.dart';
import 'package:okane/database/collections/beneficiary.dart'; import 'package:okane/i18n/strings.g.dart';
import 'package:okane/database/collections/expense_category.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/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';
import 'package:okane/ui/widgets/add_expense_category.dart'; import 'package:okane/ui/widgets/add_expense_category.dart';
import 'package:searchfield/searchfield.dart'; import 'package:searchfield/searchfield.dart';
enum Period { days, weeks, months, years }
class AddTransactionTemplateWidget extends StatefulWidget { class AddTransactionTemplateWidget extends StatefulWidget {
final VoidCallback onAdd; final VoidCallback onAdd;
@@ -39,11 +39,18 @@ class _AddTransactionTemplateWidgetState
TransactionDirection _selectedDirection = TransactionDirection.send; TransactionDirection _selectedDirection = TransactionDirection.send;
ExpenseCategory? _expenseCategory = null; ExpenseCategory? _expenseCategory;
bool _isRecurring = false;
Period _selectedPeriod = Period.weeks;
int _periodSize = 1;
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,
}; };
} }
@@ -57,6 +64,7 @@ class _AddTransactionTemplateWidgetState
return; return;
} }
final db = GetIt.I.get<OkaneDatabase>();
Beneficiary? beneficiary = _selectedBeneficiary?.item; Beneficiary? beneficiary = _selectedBeneficiary?.item;
if (beneficiary == null || if (beneficiary == null ||
getBeneficiaryName(beneficiary) != beneficiaryName) { getBeneficiaryName(beneficiary) != beneficiaryName) {
@@ -65,23 +73,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),
), ),
], ],
@@ -90,12 +98,12 @@ class _AddTransactionTemplateWidgetState
if (result == null || !result) { if (result == null || !result) {
return; return;
} }
beneficiary = await db.beneficiariesDao.upsertBeneficiary(
beneficiary = BeneficiariesCompanion(
Beneficiary() name: Value(beneficiaryName),
..name = beneficiaryName type: Value(BeneficiaryType.other),
..type = BeneficiaryType.other; ),
await upsertBeneficiary(beneficiary); );
} }
final factor = switch (_selectedDirection) { final factor = switch (_selectedDirection) {
@@ -103,15 +111,33 @@ class _AddTransactionTemplateWidgetState
TransactionDirection.receive => 1, TransactionDirection.receive => 1,
}; };
final amount = factor * double.parse(_amountTextController.text).abs(); final amount = factor * double.parse(_amountTextController.text).abs();
final transaction = final template = await db.transactionTemplatesDao.upsertTemplate(
TransactionTemplate() TransactionTemplatesCompanion(
..name = _templateNameController.text name: Value(_templateNameController.text),
..account.value = widget.activeAccountItem accountId: Value(widget.activeAccountItem.id),
..beneficiary.value = beneficiary beneficiaryId: Value(beneficiary.id),
..expenseCategory.value = _expenseCategory expenseCategoryId: Value(_expenseCategory?.id),
..recurring = false recurring: Value(_isRecurring),
..amount = amount; amount: Value(amount),
await upsertTransactionTemplate(transaction); ),
);
if (_isRecurring) {
final days = switch (_selectedPeriod) {
Period.days => _periodSize,
Period.weeks => _periodSize * 7,
Period.months => _periodSize * 31,
Period.years => _periodSize * 365,
};
await db.recurringTransactionsDao.upsertRecurringTransaction(
RecurringTransactionsCompanion(
accountId: Value(widget.activeAccountItem.id),
templateId: Value(template.id),
lastExecution: Value(null),
days: Value(days),
),
);
}
widget.onAdd(); widget.onAdd();
} }
@@ -126,71 +152,9 @@ 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)),
), ),
), ),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: BlocBuilder<CoreCubit, CoreState>(
builder:
(context, state) => SearchField<Beneficiary>(
suggestions:
state.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: "Beneficiary",
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"),
),
),
Row(
children: [
Text("Expense category"),
OutlinedButton(
onPressed: () async {
final category = await showDialogOrModal(
context: context,
builder: (_) => AddExpenseCategory(),
);
if (category == null) {
return;
}
setState(() => _expenseCategory = category);
},
child: Text(_expenseCategory?.name ?? "None"),
),
],
),
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
@@ -198,12 +162,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),
), ),
], ],
@@ -215,11 +179,193 @@ class _AddTransactionTemplateWidgetState
), ),
), ),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: BlocBuilder<CoreCubit, CoreState>(
builder:
(context, state) => SearchField<Beneficiary>(
suggestions:
state.beneficiaries
.where((el) {
final bloc = GetIt.I.get<CoreCubit>();
if (el.type == BeneficiaryType.account) {
return el.accountId != bloc.activeAccount?.id;
}
return true;
})
.map((el) {
return SearchFieldListItem(
getBeneficiaryName(el),
item: el,
);
})
.toList(),
hint: switch (_selectedDirection) {
TransactionDirection.send =>
t.common.transaction.beneficiaryTextfieldHintSend,
TransactionDirection.receive =>
t.common.transaction.beneficiaryTextfieldHintReceive,
},
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: t.common.amount,
icon: Icon(Icons.euro),
),
),
),
Row(
children: [
Text(t.common.expenseCategory.name),
Padding(
padding: EdgeInsets.only(left: 16),
child: OutlinedButton(
onPressed: () async {
final category = await showDialogOrModal(
context: context,
builder: (_) => AddExpenseCategory(),
);
if (category == null) {
return;
}
setState(() => _expenseCategory = category);
},
child: Text(
_expenseCategory?.name ?? t.common.expenseCategory.none,
),
),
),
],
),
Row(
children: [
Text(t.pages.templates.addTemplate.isRecurring),
Padding(
padding: EdgeInsets.only(left: 16),
child: Switch(
value: _isRecurring,
onChanged: (value) {
setState(() {
_isRecurring = value;
});
},
),
),
],
),
Padding(
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
child: SegmentedButton<Period>(
segments: [
ButtonSegment(
value: Period.days,
label: Text(t.common.period.days),
),
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},
multiSelectionEnabled: false,
onSelectionChanged:
_isRecurring
? (selection) {
setState(() => _selectedPeriod = selection.first);
}
: null,
),
),
Row(
children: [
IconButton(
icon: Icon(Icons.remove),
onPressed:
_isRecurring
? () {
if (_periodSize <= 1) {
return;
}
setState(() {
_periodSize--;
});
}
: null,
),
SizedBox(
width: 100,
child: Center(
child: Text(
switch (_selectedPeriod) {
Period.days => t.common.period.daysNumber(
number: _periodSize,
),
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(
color: _isRecurring ? Colors.black : Colors.grey,
),
),
),
),
IconButton(
icon: Icon(Icons.add),
onPressed:
_isRecurring
? () {
setState(() {
_periodSize++;
});
}
: null,
),
],
),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: OutlinedButton( child: OutlinedButton(
onPressed: () => _submit(context), onPressed: () => _submit(context),
child: Text("Add"), child: Text(t.modals.add),
), ),
), ),
], ],

View File

@@ -1,24 +1,27 @@
import 'package:drift/drift.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/account.dart'; import 'package:okane/database/sqlite.dart';
import 'package:okane/database/collections/beneficiary.dart'; import 'package:okane/i18n/strings.g.dart';
import 'package:okane/database/collections/expense_category.dart';
import 'package:okane/database/collections/transaction.dart';
import 'package:okane/database/database.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';
import 'package:okane/ui/widgets/add_expense_category.dart'; import 'package:okane/ui/widgets/add_expense_category.dart';
import 'package:searchfield/searchfield.dart'; import 'package:searchfield/searchfield.dart';
typedef AddTransactionCallback = void Function(TransactionDto);
class AddTransactionWidget extends StatefulWidget { class AddTransactionWidget extends StatefulWidget {
final VoidCallback onAdd; final AddTransactionCallback onAdd;
final Account activeAccountItem; final Account activeAccountItem;
final TransactionTemplateDto? template;
const AddTransactionWidget({ const AddTransactionWidget({
super.key, super.key,
this.template,
required this.activeAccountItem, required this.activeAccountItem,
required this.onAdd, required this.onAdd,
}); });
@@ -38,11 +41,34 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
TransactionDirection _selectedDirection = TransactionDirection.send; TransactionDirection _selectedDirection = TransactionDirection.send;
ExpenseCategory? _expenseCategory = null; ExpenseCategory? _expenseCategory;
@override
void initState() {
super.initState();
// TODO
/*
if (widget.template != null) {
_selectedDirection =
widget.template!.amount > 0
? TransactionDirection.receive
: TransactionDirection.send;
_amountTextController.text = widget.template!.amount.toString();
_beneficiaryTextController.text =
widget.template!;
_selectedBeneficiary = SearchFieldListItem(
getBeneficiaryName(widget.template!.beneficiary.value!),
item: widget.template!.beneficiary.value!,
);
}*/
}
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,
}; };
} }
@@ -53,6 +79,7 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
return; return;
} }
final db = GetIt.I.get<OkaneDatabase>();
Beneficiary? beneficiary = _selectedBeneficiary?.item; Beneficiary? beneficiary = _selectedBeneficiary?.item;
if (beneficiary == null || if (beneficiary == null ||
getBeneficiaryName(beneficiary) != beneficiaryName) { getBeneficiaryName(beneficiary) != beneficiaryName) {
@@ -61,23 +88,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),
), ),
], ],
@@ -87,11 +114,12 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
return; return;
} }
beneficiary = beneficiary = await db.beneficiariesDao.upsertBeneficiary(
Beneficiary() BeneficiariesCompanion(
..name = beneficiaryName name: Value(beneficiaryName),
..type = BeneficiaryType.other; type: Value(BeneficiaryType.other),
await upsertBeneficiary(beneficiary); ),
);
} }
final factor = switch (_selectedDirection) { final factor = switch (_selectedDirection) {
@@ -99,30 +127,38 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
TransactionDirection.receive => 1, TransactionDirection.receive => 1,
}; };
final amount = factor * double.parse(_amountTextController.text).abs(); final amount = factor * double.parse(_amountTextController.text).abs();
final transaction = final rawTransaction = TransactionsCompanion(
Transaction() accountId: Value(widget.activeAccountItem.id),
..account.value = widget.activeAccountItem beneficiaryId: Value(beneficiary.id),
..beneficiary.value = beneficiary amount: Value(amount),
..amount = amount // tags: [],
..tags = [] expenseCategoryId: Value(_expenseCategory?.id),
..expenseCategory.value = _expenseCategory date: Value(_selectedDate),
..date = _selectedDate; );
await upsertTransaction(transaction); final transaction = await db.transactionsDao.upsertTransaction(
rawTransaction,
);
if (beneficiary.type == BeneficiaryType.account) { if (beneficiary.type == BeneficiaryType.account) {
final otherTransaction = final otherTransaction = rawTransaction.copyWith(
Transaction() accountId: Value(beneficiary.accountId!),
..account.value = beneficiary.account.value! beneficiaryId: Value(
..beneficiary.value = await getAccountBeneficiary( (await db.beneficiariesDao.getAccountBeneficiary(
widget.activeAccountItem, widget.activeAccountItem,
) )).id,
..date = _selectedDate ),
..expenseCategory.value = _expenseCategory amount: Value(-1 * rawTransaction.amount.value),
..amount = -1 * amount; );
await upsertTransaction(otherTransaction); await db.transactionsDao.upsertTransaction(otherTransaction);
} }
widget.onAdd(); widget.onAdd(
TransactionDto(
transaction: transaction,
beneficiary: beneficiary,
expenseCategory: _expenseCategory,
),
);
} }
@override @override
@@ -134,85 +170,25 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
children: [ children: [
OutlinedButton( OutlinedButton(
onPressed: () async { onPressed: () async {
final template = await showDialogOrModal<Transaction>( final template = await selectTransactionTemplate(context);
context: context,
builder: (context) {
return BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) {
if (state.transactionTemplates.isEmpty) {
return Text("No templates defined");
}
return ListView.builder(
itemCount: state.transactionTemplates.length,
itemBuilder:
(context, index) => ListTile(
title: Text(
state.transactionTemplates[index].name,
),
onTap: () {
Navigator.of(
context,
).pop(state.transactionTemplates[index]);
},
),
);
},
);
},
);
if (template == null) { if (template == null) {
return; return;
} }
_amountTextController.text = template.amount.toString(); _amountTextController.text = template.template.amount.toString();
_selectedDirection = _selectedDirection =
template.amount > 0 template.template.amount > 0
? TransactionDirection.receive ? TransactionDirection.receive
: TransactionDirection.send; : TransactionDirection.send;
_selectedBeneficiary = SearchFieldListItem( _selectedBeneficiary = SearchFieldListItem(
getBeneficiaryName(template.beneficiary.value!), getBeneficiaryName(template.beneficiary),
item: template.beneficiary.value!, item: template.beneficiary,
); );
_beneficiaryTextController.text = getBeneficiaryName( _beneficiaryTextController.text = getBeneficiaryName(
template.beneficiary.value!, template.beneficiary,
); );
}, },
child: Text("Use template"), child: Text(t.pages.transactions.addTransaction.useTemplate),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: BlocBuilder<CoreCubit, CoreState>(
builder:
(context, state) => SearchField<Beneficiary>(
suggestions:
state.beneficiaries
.where((el) {
final bloc = GetIt.I.get<CoreCubit>();
if (el.type == BeneficiaryType.account) {
return el.account.value?.id.toInt() ==
bloc.activeAccount?.id.toInt();
}
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(
@@ -221,12 +197,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),
), ),
], ],
@@ -238,6 +214,43 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
), ),
), ),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: BlocBuilder<CoreCubit, CoreState>(
builder:
(context, state) => SearchField<Beneficiary>(
suggestions:
state.beneficiaries
.where((el) {
final bloc = GetIt.I.get<CoreCubit>();
if (el.type == BeneficiaryType.account) {
return el.accountId ==
bloc.activeAccount?.id.toInt();
}
return true;
})
.map((el) {
return SearchFieldListItem(
getBeneficiaryName(el),
item: el,
);
})
.toList(),
hint: switch (_selectedDirection) {
TransactionDirection.send =>
t.common.transaction.beneficiaryTextfieldHintSend,
TransactionDirection.receive =>
t.common.transaction.beneficiaryTextfieldHintReceive,
},
controller: _beneficiaryTextController,
selectedValue: _selectedBeneficiary,
onSuggestionTap: (beneficiary) {
setState(() => _selectedBeneficiary = beneficiary);
},
),
),
),
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: TextField( child: TextField(
@@ -246,7 +259,10 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
signed: false, signed: false,
decimal: false, decimal: false,
), ),
decoration: InputDecoration(hintText: "Amount"), decoration: InputDecoration(
hintText: t.common.amount,
icon: Icon(Icons.euro),
),
), ),
), ),
@@ -255,8 +271,10 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Text("Date"), Text(t.common.date),
OutlinedButton( Padding(
padding: EdgeInsets.only(left: 16),
child: OutlinedButton(
onPressed: () async { onPressed: () async {
final dt = await showDatePicker( final dt = await showDatePicker(
context: context, context: context,
@@ -277,14 +295,17 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
], ],
), ),
), ),
),
], ],
), ),
), ),
Row( Row(
children: [ children: [
Text("Expense category"), Text(t.common.expenseCategory.name),
OutlinedButton( Padding(
padding: EdgeInsets.only(left: 16),
child: OutlinedButton(
onPressed: () async { onPressed: () async {
final category = await showDialogOrModal( final category = await showDialogOrModal(
context: context, context: context,
@@ -296,7 +317,10 @@ class _AddTransactionWidgetState extends State<AddTransactionWidget> {
setState(() => _expenseCategory = category); setState(() => _expenseCategory = category);
}, },
child: Text(_expenseCategory?.name ?? "None"), child: Text(
_expenseCategory?.name ?? t.common.expenseCategory.none,
),
),
), ),
], ],
), ),
@@ -305,7 +329,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),
), ),
), ),
], ],

View File

@@ -1,40 +1,57 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
const _BORDER_RADIUS = 8.0;
class ImageWrapper extends StatelessWidget { class ImageWrapper extends StatelessWidget {
final String title; final String title;
final String? path; final String? path;
final VoidCallback onTap; final VoidCallback? onTap;
final double width;
final double height;
const ImageWrapper({ const ImageWrapper({
super.key, super.key,
required this.title, required this.title,
required this.onTap, this.onTap,
this.path, this.path,
this.width = 45,
this.height = 45,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget widget;
if (path == null) { if (path == null) {
widget = SizedBox( return InkWell(
width: 45, onTap: onTap,
height: 45, borderRadius: BorderRadius.circular(_BORDER_RADIUS),
child: SizedBox(
width: width,
height: height,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey, color: Colors.grey,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(_BORDER_RADIUS),
), ),
child: Center(child: Text(title[0])), child: Center(child: Text(title[0])),
), ),
),
); );
} else { } else {
widget = ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(_BORDER_RADIUS),
child: Image.file(File(path!), width: 45, height: 45), child: Material(
child: InkWell(
onTap: onTap,
radius: _BORDER_RADIUS,
child: Ink.image(
width: width,
height: height,
image: FileImage(File(path!)),
),
),
),
); );
} }
return InkWell(onTap: onTap, child: widget);
} }
} }

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:okane/database/collections/transaction.dart'; import 'package:okane/database/sqlite.dart';
import 'package:okane/database/database.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';
@@ -14,30 +13,33 @@ class TransactionCard extends StatelessWidget {
this.subtitle, this.subtitle,
}); });
final Transaction transaction; final TransactionDto transaction;
final VoidCallback onTap; final VoidCallback onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
clipBehavior: Clip.hardEdge,
child: ListTile( child: ListTile(
onTap: onTap, onTap: onTap,
leading: ImageWrapper( leading: ImageWrapper(
title: transaction.beneficiary.value!.name, title: transaction.beneficiary.name,
path: transaction.beneficiary.value!.imagePath, path: transaction.beneficiary.imagePath,
onTap: () {},
), ),
trailing: Text(formatDateTime(transaction.date)), trailing: Text(formatDateTime(transaction.transaction.date)),
title: Text(transaction.beneficiary.value!.name), title: Text(transaction.beneficiary.name),
subtitle: Column( subtitle: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
formatCurrency(transaction.amount), formatCurrency(transaction.transaction.amount),
style: TextStyle( style: TextStyle(
color: transaction.amount < 0 ? Colors.red : Colors.green, color:
transaction.transaction.amount < 0
? Colors.red
: Colors.green,
), ),
), ),
if (subtitle != null) subtitle!, if (subtitle != null) subtitle!,

View File

@@ -6,10 +6,14 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <isar_flutter_libs/isar_flutter_libs_plugin.h> #include <sentry_flutter/sentry_flutter_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar = g_autoptr(FlPluginRegistrar) sentry_flutter_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "SentryFlutterPlugin");
isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar); sentry_flutter_plugin_register_with_registrar(sentry_flutter_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
} }

View File

@@ -3,7 +3,8 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
isar_flutter_libs sentry_flutter
sqlite3_flutter_libs
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@@ -5,18 +5,23 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "61.0.0" version: "76.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.3"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.13.0" version: "6.11.0"
args: args:
dependency: transitive dependency: transitive
description: description:
@@ -121,6 +126,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
url: "https://pub.dev"
source: hosted
version: "1.4.0"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@@ -129,6 +142,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.3" version: "2.0.3"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@@ -177,6 +198,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:
@@ -189,18 +218,34 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dart_style name: dart_style
sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.8"
dartx: drift:
dependency: transitive dependency: "direct main"
description: description:
name: dartx name: drift
sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" sha256: b584ddeb2b74436735dd2cf746d2d021e19a9a6770f409212fd5cbc2814ada85
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "2.26.1"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: "0d3f8b33b76cf1c6a82ee34d9511c40957549c4674b8f1688609e6d6c7306588"
url: "https://pub.dev"
source: hosted
version: "2.26.0"
drift_flutter:
dependency: "direct main"
description:
name: drift_flutter
sha256: "0cadbf3b8733409a6cf61d18ba2e94e149df81df7de26f48ae0695b48fd71922"
url: "https://pub.dev"
source: hosted
version: "0.2.4"
equatable: equatable:
dependency: transitive dependency: transitive
description: description:
@@ -237,10 +282,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:
@@ -360,6 +405,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.0.0"
http:
dependency: transitive
description:
name: http
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@@ -384,30 +437,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
isar:
dependency: "direct main"
description:
name: isar
sha256: "99165dadb2cf2329d3140198363a7e7bff9bbd441871898a87e26914d25cf1ea"
url: "https://pub.dev"
source: hosted
version: "3.1.0+1"
isar_flutter_libs:
dependency: "direct main"
description:
name: isar_flutter_libs
sha256: bc6768cc4b9c61aabff77152e7f33b4b17d2fc93134f7af1c3dd51500fe8d5e8
url: "https://pub.dev"
source: hosted
version: "3.1.0+1"
isar_generator:
dependency: "direct dev"
description:
name: isar_generator
sha256: "76c121e1295a30423604f2f819bc255bc79f852f3bc8743a24017df6068ad133"
url: "https://pub.dev"
source: hosted
version: "3.1.0+1"
js: js:
dependency: transitive dependency: transitive
description: description:
@@ -416,14 +445,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.7" version: "0.6.7"
json_annotation: json2yaml:
dependency: transitive dependency: transitive
description:
name: json2yaml
sha256: da94630fbc56079426fdd167ae58373286f603371075b69bf46d848d63ba3e51
url: "https://pub.dev"
source: hosted
version: "3.0.1"
json_annotation:
dependency: "direct main"
description: description:
name: json_annotation name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.9.0" version: "4.9.0"
json_serializable:
dependency: "direct dev"
description:
name: json_serializable
sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
url: "https://pub.dev"
source: hosted
version: "6.8.0"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@@ -464,6 +509,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
macros:
dependency: transitive
description:
name: macros
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev"
source: hosted
version: "0.1.3-main.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -496,6 +549,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
more:
dependency: "direct main"
description:
name: more
sha256: d3908d710f78ee5470d2ae9d7599a11aeb00a17909cc36cbd0f23a0b659ca375
url: "https://pub.dev"
source: hosted
version: "4.5.0"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@@ -512,6 +573,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
package_info_plus:
dependency: transitive
description:
name: package_info_plus
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
url: "https://pub.dev"
source: hosted
version: "8.3.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
path: path:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -616,14 +693,94 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.0" version: "1.5.0"
recase:
dependency: transitive
description:
name: recase
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
url: "https://pub.dev"
source: hosted
version: "4.1.0"
searchfield: searchfield:
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"
sentry:
dependency: transitive
description:
name: sentry
sha256: "599701ca0693a74da361bc780b0752e1abc98226cf5095f6b069648116c896bb"
url: "https://pub.dev"
source: hosted
version: "8.14.2"
sentry_flutter:
dependency: "direct main"
description:
name: sentry_flutter
sha256: "5ba2cf40646a77d113b37a07bd69f61bb3ec8a73cbabe5537b05a7c89d2656f8"
url: "https://pub.dev"
source: hosted
version: "8.14.2"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
url: "https://pub.dev"
source: hosted
version: "2.5.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac"
url: "https://pub.dev"
source: hosted
version: "2.4.10"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
@@ -645,6 +802,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:
@@ -653,6 +834,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.0" version: "1.5.0"
source_helper:
dependency: transitive
description:
name: source_helper
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
url: "https://pub.dev"
source: hosted
version: "1.3.5"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@@ -661,6 +850,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.1" version: "1.10.1"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e"
url: "https://pub.dev"
source: hosted
version: "2.7.5"
sqlite3_flutter_libs:
dependency: transitive
description:
name: sqlite3_flutter_libs
sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14"
url: "https://pub.dev"
source: hosted
version: "0.5.32"
sqlparser:
dependency: transitive
description:
name: sqlparser
sha256: "27dd0a9f0c02e22ac0eb42a23df9ea079ce69b52bb4a3b478d64e0ef34a263ee"
url: "https://pub.dev"
source: hosted
version: "0.41.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -709,14 +930,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.4" version: "0.7.4"
time:
dependency: transitive
description:
name: time
sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
timing: timing:
dependency: transitive dependency: transitive
description: description:
@@ -733,6 +946,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
source: hosted
version: "4.5.1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -769,10 +990,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:
@@ -785,10 +1006,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:
@@ -797,14 +1018,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
xxh3:
dependency: transitive
description:
name: xxh3
sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:

View File

@@ -1,8 +1,8 @@
name: okane name: okane
description: "A cross-platform finance tracker." description: "A cross-platform finance tracker."
publish_to: 'none' publish_to: "none"
version: 1.0.0+1 version: 1.0.0+6
environment: environment:
sdk: ^3.7.0 sdk: ^3.7.0
@@ -21,8 +21,16 @@ dependencies:
path: ^1.9.1 path: ^1.9.1
fl_chart: ^0.71.0 fl_chart: ^0.71.0
flutter_picker_plus: ^1.5.1 flutter_picker_plus: ^1.5.1
isar: ^3.1.0+1 shared_preferences: ^2.5.3
isar_flutter_libs: ^3.1.0+1 json_annotation: ^4.9.0
more: 4.5.0
slang: ^3.0.0
slang_flutter: ^3.0.0
drift: ^2.26.1
drift_flutter: ^0.2.4
# For optional error tracking
sentry_flutter: ^8.14.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -30,7 +38,9 @@ dev_dependencies:
flutter_lints: ^5.0.0 flutter_lints: ^5.0.0
build_runner: ^2.4.13 build_runner: ^2.4.13
freezed: 2.5.0 freezed: 2.5.0
isar_generator: ^3.1.0+1 json_serializable: ^6.4.0
slang_build_runner: ^3.0.0
drift_dev: ^2.20.1
flutter: flutter:
uses-material-design: true uses-material-design: true

View File

@@ -1,30 +0,0 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:okane/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}