commit 5cc0bba09af917556fd542e0de0a3f16489f7910 Author: Alexander "PapaTutuWawa Date: Sun May 4 02:54:07 2025 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79c113f --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..6ac59d7 --- /dev/null +++ b/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 + base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 + - platform: android + create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 + base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md new file mode 100644 index 0000000..158ba84 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# okane + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..5d438a8 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.okane" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.okane" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..35659b7 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/okane/MainActivity.kt b/android/app/src/main/kotlin/com/example/okane/MainActivity.kt new file mode 100644 index 0000000..faf4f81 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/okane/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.okane + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..89176ef --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..afa1e8e --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..a439442 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,25 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.0" apply false + id("org.jetbrains.kotlin.android") version "1.8.22" apply false +} + +include(":app") diff --git a/lib/database/collections/account.dart b/lib/database/collections/account.dart new file mode 100644 index 0000000..dbacb16 --- /dev/null +++ b/lib/database/collections/account.dart @@ -0,0 +1,10 @@ +import 'package:isar/isar.dart'; + +part 'account.g.dart'; + +@collection +class Account { + Id id = Isar.autoIncrement; + + String? name; +} diff --git a/lib/database/collections/account.g.dart b/lib/database/collections/account.g.dart new file mode 100644 index 0000000..c7f17ee --- /dev/null +++ b/lib/database/collections/account.g.dart @@ -0,0 +1,469 @@ +// 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 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 offsets, + Map> 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 offsets, + Map> allOffsets, +) { + writer.writeString(offsets[0], object.name); +} + +Account _accountDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = Account(); + object.id = id; + object.name = reader.readStringOrNull(offsets[0]); + return object; +} + +P _accountDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> 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> _accountGetLinks(Account object) { + return []; +} + +void _accountAttach(IsarCollection col, Id id, Account object) { + object.id = id; +} + +extension AccountQueryWhereSort on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension AccountQueryWhere on QueryBuilder { + QueryBuilder idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); + }); + } + + QueryBuilder 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 idGreaterThan( + Id id, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder idLessThan( + Id id, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder 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 { + QueryBuilder idEqualTo(Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'id', value: value), + ); + }); + } + + QueryBuilder idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + ), + ); + }); + } + + QueryBuilder idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + ), + ); + }); + } + + QueryBuilder 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 nameIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNull(property: r'name'), + ); + }); + } + + QueryBuilder nameIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'name'), + ); + }); + } + + QueryBuilder nameEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'name', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder 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 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 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 nameStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.startsWith( + property: r'name', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder nameEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.endsWith( + property: r'name', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder nameContains( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.contains( + property: r'name', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder nameMatches( + String pattern, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.matches( + property: r'name', + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder nameIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'name', value: ''), + ); + }); + } + + QueryBuilder nameIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan(property: r'name', value: ''), + ); + }); + } +} + +extension AccountQueryObject + on QueryBuilder {} + +extension AccountQueryLinks + on QueryBuilder {} + +extension AccountQuerySortBy on QueryBuilder { + QueryBuilder sortByName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.asc); + }); + } + + QueryBuilder sortByNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.desc); + }); + } +} + +extension AccountQuerySortThenBy + on QueryBuilder { + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder thenByName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.asc); + }); + } + + QueryBuilder thenByNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.desc); + }); + } +} + +extension AccountQueryWhereDistinct + on QueryBuilder { + QueryBuilder distinctByName({ + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'name', caseSensitive: caseSensitive); + }); + } +} + +extension AccountQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder nameProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'name'); + }); + } +} diff --git a/lib/database/collections/beneficiary.dart b/lib/database/collections/beneficiary.dart new file mode 100644 index 0000000..2bbeffb --- /dev/null +++ b/lib/database/collections/beneficiary.dart @@ -0,0 +1,20 @@ +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(); + + String? imagePath; +} diff --git a/lib/database/collections/beneficiary.g.dart b/lib/database/collections/beneficiary.g.dart new file mode 100644 index 0000000..7943e15 --- /dev/null +++ b/lib/database/collections/beneficiary.g.dart @@ -0,0 +1,810 @@ +// 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 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 offsets, + Map> 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 offsets, + Map> 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 offsets, + Map> 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

( + IsarReader reader, + int propertyId, + int offset, + Map> 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> _beneficiaryGetLinks(Beneficiary object) { + return [object.account]; +} + +void _beneficiaryAttach( + IsarCollection col, + Id id, + Beneficiary object, +) { + object.id = id; + object.account.attach(col, col.isar.collection(), r'account', id); +} + +extension BeneficiaryQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension BeneficiaryQueryWhere + on QueryBuilder { + QueryBuilder idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); + }); + } + + QueryBuilder 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 idGreaterThan( + Id id, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder idLessThan( + Id id, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder 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 { + QueryBuilder idEqualTo( + Id value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'id', value: value), + ); + }); + } + + QueryBuilder idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + ), + ); + }); + } + + QueryBuilder idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + ), + ); + }); + } + + QueryBuilder 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 + imagePathIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNull(property: r'imagePath'), + ); + }); + } + + QueryBuilder + imagePathIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'imagePath'), + ); + }); + } + + QueryBuilder + imagePathEqualTo(String? value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'imagePath', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + 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 + 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 + 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 + imagePathStartsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.startsWith( + property: r'imagePath', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + imagePathEndsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.endsWith( + property: r'imagePath', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + imagePathContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.contains( + property: r'imagePath', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + imagePathMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.matches( + property: r'imagePath', + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + imagePathIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'imagePath', value: ''), + ); + }); + } + + QueryBuilder + imagePathIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan(property: r'imagePath', value: ''), + ); + }); + } + + QueryBuilder nameEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'name', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder 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 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 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 nameStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.startsWith( + property: r'name', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder nameEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.endsWith( + property: r'name', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder nameContains( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.contains( + property: r'name', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder nameMatches( + String pattern, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.matches( + property: r'name', + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder nameIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'name', value: ''), + ); + }); + } + + QueryBuilder + nameIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan(property: r'name', value: ''), + ); + }); + } + + QueryBuilder typeEqualTo( + BeneficiaryType value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'type', value: value), + ); + }); + } + + QueryBuilder typeGreaterThan( + BeneficiaryType value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'type', + value: value, + ), + ); + }); + } + + QueryBuilder typeLessThan( + BeneficiaryType value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'type', + value: value, + ), + ); + }); + } + + QueryBuilder 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 {} + +extension BeneficiaryQueryLinks + on QueryBuilder { + QueryBuilder account( + FilterQuery q, + ) { + return QueryBuilder.apply(this, (query) { + return query.link(q, r'account'); + }); + } + + QueryBuilder + accountIsNull() { + return QueryBuilder.apply(this, (query) { + return query.linkLength(r'account', 0, true, 0, true); + }); + } +} + +extension BeneficiaryQuerySortBy + on QueryBuilder { + QueryBuilder sortByImagePath() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'imagePath', Sort.asc); + }); + } + + QueryBuilder sortByImagePathDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'imagePath', Sort.desc); + }); + } + + QueryBuilder sortByName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.asc); + }); + } + + QueryBuilder sortByNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.desc); + }); + } + + QueryBuilder sortByType() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.asc); + }); + } + + QueryBuilder sortByTypeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.desc); + }); + } +} + +extension BeneficiaryQuerySortThenBy + on QueryBuilder { + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder thenByImagePath() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'imagePath', Sort.asc); + }); + } + + QueryBuilder thenByImagePathDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'imagePath', Sort.desc); + }); + } + + QueryBuilder thenByName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.asc); + }); + } + + QueryBuilder thenByNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.desc); + }); + } + + QueryBuilder thenByType() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.asc); + }); + } + + QueryBuilder thenByTypeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.desc); + }); + } +} + +extension BeneficiaryQueryWhereDistinct + on QueryBuilder { + QueryBuilder distinctByImagePath({ + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'imagePath', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByName({ + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'name', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByType() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'type'); + }); + } +} + +extension BeneficiaryQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder imagePathProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'imagePath'); + }); + } + + QueryBuilder nameProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'name'); + }); + } + + QueryBuilder typeProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'type'); + }); + } +} diff --git a/lib/database/collections/expense_category.dart b/lib/database/collections/expense_category.dart new file mode 100644 index 0000000..11e157b --- /dev/null +++ b/lib/database/collections/expense_category.dart @@ -0,0 +1,10 @@ +import 'package:isar/isar.dart'; + +part 'expense_category.g.dart'; + +@collection +class ExpenseCategory { + Id id = Isar.autoIncrement; + + late String name; +} diff --git a/lib/database/collections/expense_category.g.dart b/lib/database/collections/expense_category.g.dart new file mode 100644 index 0000000..13012af --- /dev/null +++ b/lib/database/collections/expense_category.g.dart @@ -0,0 +1,451 @@ +// 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 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 offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + bytesCount += 3 + object.name.length * 3; + return bytesCount; +} + +void _expenseCategorySerialize( + ExpenseCategory object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeString(offsets[0], object.name); +} + +ExpenseCategory _expenseCategoryDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = ExpenseCategory(); + object.id = id; + object.name = reader.readString(offsets[0]); + return object; +} + +P _expenseCategoryDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> 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> _expenseCategoryGetLinks(ExpenseCategory object) { + return []; +} + +void _expenseCategoryAttach( + IsarCollection col, + Id id, + ExpenseCategory object, +) { + object.id = id; +} + +extension ExpenseCategoryQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension ExpenseCategoryQueryWhere + on QueryBuilder { + QueryBuilder idEqualTo( + Id id, + ) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); + }); + } + + QueryBuilder + 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 + idGreaterThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder idLessThan( + Id id, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder 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 { + QueryBuilder + idEqualTo(Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'id', value: value), + ); + }); + } + + QueryBuilder + idGreaterThan(Id value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + ), + ); + }); + } + + QueryBuilder + idLessThan(Id value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + ), + ); + }); + } + + QueryBuilder + 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 + nameEqualTo(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'name', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + 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 + 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 + 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 + nameStartsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.startsWith( + property: r'name', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + nameEndsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.endsWith( + property: r'name', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + nameContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.contains( + property: r'name', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + nameMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.matches( + property: r'name', + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + nameIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'name', value: ''), + ); + }); + } + + QueryBuilder + nameIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan(property: r'name', value: ''), + ); + }); + } +} + +extension ExpenseCategoryQueryObject + on QueryBuilder {} + +extension ExpenseCategoryQueryLinks + on QueryBuilder {} + +extension ExpenseCategoryQuerySortBy + on QueryBuilder { + QueryBuilder sortByName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.asc); + }); + } + + QueryBuilder + sortByNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.desc); + }); + } +} + +extension ExpenseCategoryQuerySortThenBy + on QueryBuilder { + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder thenByName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.asc); + }); + } + + QueryBuilder + thenByNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.desc); + }); + } +} + +extension ExpenseCategoryQueryWhereDistinct + on QueryBuilder { + QueryBuilder distinctByName({ + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'name', caseSensitive: caseSensitive); + }); + } +} + +extension ExpenseCategoryQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder nameProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'name'); + }); + } +} diff --git a/lib/database/collections/recurrent.dart b/lib/database/collections/recurrent.dart new file mode 100644 index 0000000..0576243 --- /dev/null +++ b/lib/database/collections/recurrent.dart @@ -0,0 +1,18 @@ +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(); + + final account = IsarLink(); +} diff --git a/lib/database/collections/recurrent.g.dart b/lib/database/collections/recurrent.g.dart new file mode 100644 index 0000000..c587bc5 --- /dev/null +++ b/lib/database/collections/recurrent.g.dart @@ -0,0 +1,633 @@ +// 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 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 offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + return bytesCount; +} + +void _recurringTransactionSerialize( + RecurringTransaction object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeLong(offsets[0], object.days); + writer.writeDateTime(offsets[1], object.lastExecution); +} + +RecurringTransaction _recurringTransactionDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = RecurringTransaction(); + object.days = reader.readLong(offsets[0]); + object.id = id; + object.lastExecution = reader.readDateTimeOrNull(offsets[1]); + return object; +} + +P _recurringTransactionDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> 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> _recurringTransactionGetLinks( + RecurringTransaction object, +) { + return [object.template, object.account]; +} + +void _recurringTransactionAttach( + IsarCollection col, + Id id, + RecurringTransaction object, +) { + object.id = id; + object.template.attach( + col, + col.isar.collection(), + r'template', + id, + ); + object.account.attach(col, col.isar.collection(), r'account', id); +} + +extension RecurringTransactionQueryWhereSort + on QueryBuilder { + QueryBuilder + anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension RecurringTransactionQueryWhere + on QueryBuilder { + QueryBuilder + idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); + }); + } + + QueryBuilder + 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 + idGreaterThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder + idLessThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder + 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 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 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 { + QueryBuilder + sortByDays() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'days', Sort.asc); + }); + } + + QueryBuilder + sortByDaysDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'days', Sort.desc); + }); + } + + QueryBuilder + sortByLastExecution() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastExecution', Sort.asc); + }); + } + + QueryBuilder + sortByLastExecutionDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastExecution', Sort.desc); + }); + } +} + +extension RecurringTransactionQuerySortThenBy + on QueryBuilder { + QueryBuilder + thenByDays() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'days', Sort.asc); + }); + } + + QueryBuilder + thenByDaysDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'days', Sort.desc); + }); + } + + QueryBuilder + thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder + thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder + thenByLastExecution() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastExecution', Sort.asc); + }); + } + + QueryBuilder + thenByLastExecutionDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastExecution', Sort.desc); + }); + } +} + +extension RecurringTransactionQueryWhereDistinct + on QueryBuilder { + QueryBuilder + distinctByDays() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'days'); + }); + } + + QueryBuilder + distinctByLastExecution() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'lastExecution'); + }); + } +} + +extension RecurringTransactionQueryProperty + on + QueryBuilder< + RecurringTransaction, + RecurringTransaction, + QQueryProperty + > { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder daysProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'days'); + }); + } + + QueryBuilder + lastExecutionProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'lastExecution'); + }); + } +} diff --git a/lib/database/collections/template.dart b/lib/database/collections/template.dart new file mode 100644 index 0000000..4fd62ae --- /dev/null +++ b/lib/database/collections/template.dart @@ -0,0 +1,23 @@ +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(); + + final beneficiary = IsarLink(); + + final account = IsarLink(); +} diff --git a/lib/database/collections/template.g.dart b/lib/database/collections/template.g.dart new file mode 100644 index 0000000..99b6524 --- /dev/null +++ b/lib/database/collections/template.g.dart @@ -0,0 +1,721 @@ +// 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 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 offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + bytesCount += 3 + object.name.length * 3; + return bytesCount; +} + +void _transactionTemplateSerialize( + TransactionTemplate object, + IsarWriter writer, + List offsets, + Map> 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 offsets, + Map> 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

( + IsarReader reader, + int propertyId, + int offset, + Map> 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> _transactionTemplateGetLinks( + TransactionTemplate object, +) { + return [object.expenseCategory, object.beneficiary, object.account]; +} + +void _transactionTemplateAttach( + IsarCollection col, + Id id, + TransactionTemplate object, +) { + object.id = id; + object.expenseCategory.attach( + col, + col.isar.collection(), + r'expenseCategory', + id, + ); + object.beneficiary.attach( + col, + col.isar.collection(), + r'beneficiary', + id, + ); + object.account.attach(col, col.isar.collection(), r'account', id); +} + +extension TransactionTemplateQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension TransactionTemplateQueryWhere + on QueryBuilder { + QueryBuilder + idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); + }); + } + + QueryBuilder + 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 + idGreaterThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder + idLessThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder + 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 + 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 + 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 + 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 + 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 + idEqualTo(Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'id', value: value), + ); + }); + } + + QueryBuilder + idGreaterThan(Id value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + ), + ); + }); + } + + QueryBuilder + idLessThan(Id value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + ), + ); + }); + } + + QueryBuilder + 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 + nameEqualTo(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'name', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + 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 + 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 + 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 + nameStartsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.startsWith( + property: r'name', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + nameEndsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.endsWith( + property: r'name', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + nameContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.contains( + property: r'name', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + nameMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.matches( + property: r'name', + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + nameIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'name', value: ''), + ); + }); + } + + QueryBuilder + nameIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan(property: r'name', value: ''), + ); + }); + } + + QueryBuilder + 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 + expenseCategory(FilterQuery q) { + return QueryBuilder.apply(this, (query) { + return query.link(q, r'expenseCategory'); + }); + } + + QueryBuilder + expenseCategoryIsNull() { + return QueryBuilder.apply(this, (query) { + return query.linkLength(r'expenseCategory', 0, true, 0, true); + }); + } + + QueryBuilder + beneficiary(FilterQuery q) { + return QueryBuilder.apply(this, (query) { + return query.link(q, r'beneficiary'); + }); + } + + QueryBuilder + beneficiaryIsNull() { + return QueryBuilder.apply(this, (query) { + return query.linkLength(r'beneficiary', 0, true, 0, true); + }); + } + + QueryBuilder + account(FilterQuery q) { + return QueryBuilder.apply(this, (query) { + return query.link(q, r'account'); + }); + } + + QueryBuilder + accountIsNull() { + return QueryBuilder.apply(this, (query) { + return query.linkLength(r'account', 0, true, 0, true); + }); + } +} + +extension TransactionTemplateQuerySortBy + on QueryBuilder { + QueryBuilder + sortByAmount() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'amount', Sort.asc); + }); + } + + QueryBuilder + sortByAmountDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'amount', Sort.desc); + }); + } + + QueryBuilder + sortByName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.asc); + }); + } + + QueryBuilder + sortByNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.desc); + }); + } + + QueryBuilder + sortByRecurring() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'recurring', Sort.asc); + }); + } + + QueryBuilder + sortByRecurringDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'recurring', Sort.desc); + }); + } +} + +extension TransactionTemplateQuerySortThenBy + on QueryBuilder { + QueryBuilder + thenByAmount() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'amount', Sort.asc); + }); + } + + QueryBuilder + thenByAmountDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'amount', Sort.desc); + }); + } + + QueryBuilder + thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder + thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder + thenByName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.asc); + }); + } + + QueryBuilder + thenByNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.desc); + }); + } + + QueryBuilder + thenByRecurring() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'recurring', Sort.asc); + }); + } + + QueryBuilder + thenByRecurringDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'recurring', Sort.desc); + }); + } +} + +extension TransactionTemplateQueryWhereDistinct + on QueryBuilder { + QueryBuilder + distinctByAmount() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'amount'); + }); + } + + QueryBuilder + distinctByName({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'name', caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByRecurring() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'recurring'); + }); + } +} + +extension TransactionTemplateQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder amountProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'amount'); + }); + } + + QueryBuilder nameProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'name'); + }); + } + + QueryBuilder + recurringProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'recurring'); + }); + } +} diff --git a/lib/database/collections/transaction.dart b/lib/database/collections/transaction.dart new file mode 100644 index 0000000..38e4eef --- /dev/null +++ b/lib/database/collections/transaction.dart @@ -0,0 +1,23 @@ +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 tags; + + late DateTime date; + + final expenseCategory = IsarLink(); + + final account = IsarLink(); + + final beneficiary = IsarLink(); +} diff --git a/lib/database/collections/transaction.g.dart b/lib/database/collections/transaction.g.dart new file mode 100644 index 0000000..491ba9c --- /dev/null +++ b/lib/database/collections/transaction.g.dart @@ -0,0 +1,775 @@ +// 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 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 offsets, + Map> 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 offsets, + Map> 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 offsets, + Map> 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

( + IsarReader reader, + int propertyId, + int offset, + Map> 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> _transactionGetLinks(Transaction object) { + return [object.expenseCategory, object.account, object.beneficiary]; +} + +void _transactionAttach( + IsarCollection col, + Id id, + Transaction object, +) { + object.id = id; + object.expenseCategory.attach( + col, + col.isar.collection(), + r'expenseCategory', + id, + ); + object.account.attach(col, col.isar.collection(), r'account', id); + object.beneficiary.attach( + col, + col.isar.collection(), + r'beneficiary', + id, + ); +} + +extension TransactionQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension TransactionQueryWhere + on QueryBuilder { + QueryBuilder idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); + }); + } + + QueryBuilder 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 idGreaterThan( + Id id, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder idLessThan( + Id id, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder 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 { + QueryBuilder 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 + 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 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 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 dateEqualTo( + DateTime value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'date', value: value), + ); + }); + } + + QueryBuilder dateGreaterThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'date', + value: value, + ), + ); + }); + } + + QueryBuilder dateLessThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'date', + value: value, + ), + ); + }); + } + + QueryBuilder 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 idEqualTo( + Id value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'id', value: value), + ); + }); + } + + QueryBuilder idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + ), + ); + }); + } + + QueryBuilder idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + ), + ); + }); + } + + QueryBuilder 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 + tagsElementEqualTo(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'tags', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + 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 + 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 + 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 + tagsElementStartsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.startsWith( + property: r'tags', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + tagsElementEndsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.endsWith( + property: r'tags', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + tagsElementContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.contains( + property: r'tags', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + tagsElementMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.matches( + property: r'tags', + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + tagsElementIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'tags', value: ''), + ); + }); + } + + QueryBuilder + tagsElementIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan(property: r'tags', value: ''), + ); + }); + } + + QueryBuilder + tagsLengthEqualTo(int length) { + return QueryBuilder.apply(this, (query) { + return query.listLength(r'tags', length, true, length, true); + }); + } + + QueryBuilder tagsIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength(r'tags', 0, true, 0, true); + }); + } + + QueryBuilder + tagsIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength(r'tags', 0, false, 999999, true); + }); + } + + QueryBuilder + tagsLengthLessThan(int length, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.listLength(r'tags', 0, true, length, include); + }); + } + + QueryBuilder + tagsLengthGreaterThan(int length, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.listLength(r'tags', length, include, 999999, true); + }); + } + + QueryBuilder + 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 {} + +extension TransactionQueryLinks + on QueryBuilder { + QueryBuilder expenseCategory( + FilterQuery q, + ) { + return QueryBuilder.apply(this, (query) { + return query.link(q, r'expenseCategory'); + }); + } + + QueryBuilder + expenseCategoryIsNull() { + return QueryBuilder.apply(this, (query) { + return query.linkLength(r'expenseCategory', 0, true, 0, true); + }); + } + + QueryBuilder account( + FilterQuery q, + ) { + return QueryBuilder.apply(this, (query) { + return query.link(q, r'account'); + }); + } + + QueryBuilder + accountIsNull() { + return QueryBuilder.apply(this, (query) { + return query.linkLength(r'account', 0, true, 0, true); + }); + } + + QueryBuilder beneficiary( + FilterQuery q, + ) { + return QueryBuilder.apply(this, (query) { + return query.link(q, r'beneficiary'); + }); + } + + QueryBuilder + beneficiaryIsNull() { + return QueryBuilder.apply(this, (query) { + return query.linkLength(r'beneficiary', 0, true, 0, true); + }); + } +} + +extension TransactionQuerySortBy + on QueryBuilder { + QueryBuilder sortByAmount() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'amount', Sort.asc); + }); + } + + QueryBuilder sortByAmountDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'amount', Sort.desc); + }); + } + + QueryBuilder sortByDate() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'date', Sort.asc); + }); + } + + QueryBuilder sortByDateDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'date', Sort.desc); + }); + } +} + +extension TransactionQuerySortThenBy + on QueryBuilder { + QueryBuilder thenByAmount() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'amount', Sort.asc); + }); + } + + QueryBuilder thenByAmountDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'amount', Sort.desc); + }); + } + + QueryBuilder thenByDate() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'date', Sort.asc); + }); + } + + QueryBuilder thenByDateDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'date', Sort.desc); + }); + } + + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } +} + +extension TransactionQueryWhereDistinct + on QueryBuilder { + QueryBuilder distinctByAmount() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'amount'); + }); + } + + QueryBuilder distinctByDate() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'date'); + }); + } + + QueryBuilder distinctByTags() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'tags'); + }); + } +} + +extension TransactionQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder amountProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'amount'); + }); + } + + QueryBuilder dateProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'date'); + }); + } + + QueryBuilder, QQueryOperations> tagsProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'tags'); + }); + } +} diff --git a/lib/database/database.dart b/lib/database/database.dart new file mode 100644 index 0000000..95e916c --- /dev/null +++ b/lib/database/database.dart @@ -0,0 +1,207 @@ +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'; + +Future openDatabase() async { + final dir = await getApplicationDocumentsDirectory(); + return Isar.open([ + AccountSchema, + BeneficiarySchema, + TransactionSchema, + TransactionTemplateSchema, + RecurringTransactionSchema, + ExpenseCategorySchema, + ], directory: dir.path); +} + +Future> getAccounts() { + return GetIt.I.get().accounts.where().findAll(); +} + +Future getTotalBalance(Account account) async { + return GetIt.I + .get() + .transactions + .filter() + .account((q) => q.idEqualTo(account.id)) + .amountProperty() + .sum(); +} + +Future> getLastTransactions( + Account account, + DateTime today, + int days, +) async { + return GetIt.I + .get() + .transactions + .filter() + .account((q) => q.idEqualTo(account.id)) + .dateGreaterThan(toMidnight(today.subtract(Duration(days: days)))) + .findAll(); +} + +Future> getRecurringTransactions(Account? account) { + if (account == null) { + return Future.value([]); + } + + return GetIt.I + .get() + .recurringTransactions + .filter() + .account((q) => q.idEqualTo(account.id)) + .findAll(); +} + +Stream watchRecurringTransactions(Account account) { + final account = GetIt.I.get().activeAccount!; + return GetIt.I + .get() + .recurringTransactions + .filter() + .account((q) => q.idEqualTo(account.id)) + .build() + .watchLazy(fireImmediately: true); +} + +Future upsertAccount(Account account) async { + final db = GetIt.I.get(); + return db.writeTxn(() async { + print("Before account insert"); + final id = await db.accounts.put(account); + print("After account insert: $id"); + }); +} + +Future upsertBeneficiary(Beneficiary beneficiary) async { + final db = GetIt.I.get(); + return db.writeTxn(() async { + await db.beneficiarys.put(beneficiary); + await beneficiary.account.save(); + }); +} + +Future getAccountBeneficiary(Account account) { + return GetIt.I + .get() + .beneficiarys + .filter() + .account((q) => q.idEqualTo(account.id)) + .findFirst(); +} + +Future upsertTransactionTemplate(TransactionTemplate template) async { + final db = GetIt.I.get(); + return db.writeTxn(() async { + await db.transactionTemplates.put(template); + await template.beneficiary.save(); + await template.account.save(); + await template.expenseCategory.save(); + }); +} + +Future upsertRecurringTransaction(RecurringTransaction template) async { + final db = GetIt.I.get(); + return db.writeTxn(() async { + await db.recurringTransactions.put(template); + await template.template.save(); + await template.account.save(); + }); +} + +Future upsertTransaction(Transaction transaction) async { + final db = GetIt.I.get(); + return db.writeTxn(() async { + await db.transactions.put(transaction); + await transaction.beneficiary.save(); + await transaction.account.save(); + await transaction.expenseCategory.save(); + }); +} + +Stream watchAccounts() { + return GetIt.I.get().accounts.watchLazy(); +} + +Stream watchTransactionTemplates(Account account) { + return GetIt.I + .get() + .transactionTemplates + .filter() + .account((q) => q.idEqualTo(account.id)) + .recurringEqualTo(false) + .watchLazy(fireImmediately: true); +} + +Future> getTransactionTemplates(Account? account) { + if (account == null) { + return Future.value([]); + } + + return GetIt.I + .get() + .transactionTemplates + .filter() + .account((q) => q.idEqualTo(account.id)) + .recurringEqualTo(false) + .findAll(); +} + +Stream watchTransactions(Account account) { + return GetIt.I + .get() + .transactions + .filter() + .account((q) => q.idEqualTo(account.id)) + .watchLazy(fireImmediately: true); +} + +Future> getTransactions(Account? account) { + if (account == null) { + return Future.value([]); + } + + return GetIt.I + .get() + .transactions + .filter() + .account((q) => q.idEqualTo(account.id)) + .findAll(); +} + +Stream watchBeneficiaries() { + return GetIt.I.get().beneficiarys.watchLazy(fireImmediately: true); +} + +Future> getBeneficiaries() { + return GetIt.I.get().beneficiarys.where().findAll(); +} + +Stream watchBeneficiaryObject(Id id) { + return GetIt.I.get().beneficiarys.watchObject(id); +} + +Future upsertExpenseCategory(ExpenseCategory category) { + final db = GetIt.I.get(); + return db.writeTxn(() => db.expenseCategorys.put(category)); +} + +Future> getExpenseCategories() { + return GetIt.I.get().expenseCategorys.where().findAll(); +} + +Stream watchExpenseCategory() { + return GetIt.I.get().expenseCategorys.watchLazy(fireImmediately: true); +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..5393302 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,107 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:isar/isar.dart'; +import 'package:okane/database/database.dart'; +import 'package:okane/screen.dart'; +import 'package:okane/ui/navigation.dart'; +import 'package:okane/ui/pages/transaction_details.dart'; +import 'package:okane/ui/state/core.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + GetIt.I.registerSingleton(CoreCubit()); + GetIt.I.registerSingleton(await openDatabase()); + + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => GetIt.I.get()), + ], + child: MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + ), + home: const MyHomePage(), + onGenerateRoute: + (settings) => switch (settings.name) { + "/transactions/details" => TransactionDetailsPage.mobileRoute, + _ => MaterialPageRoute(builder: (_) => Text("Unknown!!")), + }, + ), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key}); + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final Completer _initCompleter = Completer(); + + @override + void initState() { + super.initState(); + asyncInit(); + } + + Future asyncInit() async { + print("Starting async init"); + await GetIt.I.get().init(); + _initCompleter.complete(1); + print("Async init done"); + } + + @override + Widget build(BuildContext context) { + final screenSize = getScreenSize(context); + if (screenSize == ScreenSize.normal) { + SchedulerBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).popUntil((route) => route.isFirst); + }); + } + return FutureBuilder( + future: _initCompleter.future, + builder: (context, snapshot) { + print("${snapshot.hasData}"); + if (!snapshot.hasData) { + return Center(child: CircularProgressIndicator()); + } + + return Scaffold( + bottomNavigationBar: switch (screenSize) { + ScreenSize.normal => null, + ScreenSize.small => OkaneNavigationBar(), + }, + body: switch (screenSize) { + ScreenSize.normal => const Row( + children: [ + OkaneNavigationRail(), + VerticalDivider(thickness: 1, width: 1), + Expanded(child: OkaneNavigationLayout()), + ], + ), + ScreenSize.small => const OkaneNavigationLayout(), + }, + ); + }, + ); + } +} diff --git a/lib/screen.dart b/lib/screen.dart new file mode 100644 index 0000000..5b38045 --- /dev/null +++ b/lib/screen.dart @@ -0,0 +1,17 @@ +import 'package:flutter/cupertino.dart'; + +enum ScreenSize { + small(300), + normal(400); + + final double size; + + const ScreenSize(this.size); +} + +ScreenSize getScreenSize(BuildContext context) { + final width = MediaQuery.sizeOf(context).shortestSide; + if (width > ScreenSize.normal.size) return ScreenSize.normal; + + return ScreenSize.small; +} diff --git a/lib/ui/navigation.dart b/lib/ui/navigation.dart new file mode 100644 index 0000000..d5131c1 --- /dev/null +++ b/lib/ui/navigation.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:okane/screen.dart'; +import 'package:okane/ui/pages/account/account.dart'; +import 'package:okane/ui/pages/template_list.dart'; +import 'package:okane/ui/pages/transaction_details.dart'; +import 'package:okane/ui/pages/transaction_list.dart'; +import 'package:okane/ui/state/core.dart'; + +enum OkanePage { accounts, transactions, beneficiaries, templates, budgets } + +typedef OkanePageBuilder = Widget Function(bool); + +class OkanePageItem { + final OkanePage page; + final IconData icon; + final String label; + final Widget child; + final OkanePageBuilder? details; + + const OkanePageItem( + this.page, + this.icon, + this.label, + this.child, + this.details, + ); + + NavigationDestination toDestination() => + NavigationDestination(icon: Icon(icon), label: label); + + NavigationRailDestination toRailDestination() => + NavigationRailDestination(icon: Icon(icon), label: Text(label)); +} + +final _pages = [ + OkanePageItem( + OkanePage.accounts, + Icons.account_box, + "Accounts", + AccountListPage(isPage: false), + null, + ), + OkanePageItem( + OkanePage.transactions, + Icons.monetization_on_rounded, + "Transactions", + TransactionListPage(), + (_) => TransactionDetailsPage(), + ), + OkanePageItem( + OkanePage.beneficiaries, + Icons.person, + "Beneficiaries", + Container(), + null, + ), + OkanePageItem( + OkanePage.templates, + Icons.list, + "Templates", + TemplateListPage(), + null, + ), + OkanePageItem( + OkanePage.budgets, + Icons.pie_chart, + "Budgets", + Placeholder(), + null, + ), +]; + +class OkaneNavigationRail extends StatelessWidget { + const OkaneNavigationRail({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: + (context, state) => NavigationRail( + onDestinationSelected: + (i) => context.read().setPage(_pages[i].page), + destinations: _pages.map((p) => p.toRailDestination()).toList(), + selectedIndex: _pages.indexWhere((p) => p.page == state.activePage), + ), + ); + } +} + +class OkaneNavigationBar extends StatelessWidget { + const OkaneNavigationBar({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: + (context, state) => NavigationBar( + onDestinationSelected: + (i) => context.read().setPage(_pages[i].page), + destinations: _pages.map((p) => p.toDestination()).toList(), + selectedIndex: _pages.indexWhere((p) => p.page == state.activePage), + ), + ); + } +} + +class OkaneNavigationLayout extends StatelessWidget { + const OkaneNavigationLayout({super.key}); + + @override + Widget build(BuildContext context) { + final screenSize = getScreenSize(context); + return BlocBuilder( + builder: + (_, state) => IndexedStack( + index: _pages.indexWhere((p) => p.page == state.activePage), + children: + _pages + .map( + (p) => switch (screenSize) { + ScreenSize.small => p.child, + ScreenSize.normal => + p.details != null + ? Row( + children: [ + Expanded(child: p.child), + Expanded(child: p.details!(false)), + ], + ) + : p.child, + }, + ) + .toList(), + ), + ); + } +} diff --git a/lib/ui/pages/account/account.dart b/lib/ui/pages/account/account.dart new file mode 100644 index 0000000..2a3ef13 --- /dev/null +++ b/lib/ui/pages/account/account.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.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/database.dart'; +import 'package:okane/ui/pages/account/breakdown_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/state/core.dart'; +import 'package:okane/ui/utils.dart'; + +class AccountListPage extends StatefulWidget { + final bool isPage; + + const AccountListPage({required this.isPage, super.key}); + + @override + AccountListPageState createState() => AccountListPageState(); +} + +class AccountListPageState extends State { + final TextEditingController _accountNameController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + ListView( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + "Accounts", + style: Theme.of(context).textTheme.titleLarge, + ), + ), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: BlocBuilder( + builder: + (context, state) => SizedBox( + child: SizedBox( + height: 100, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: state.accounts.length, + itemBuilder: + (context, index) => SizedBox( + width: 150, + height: 100, + child: Card( + color: colorHash(state.accounts[index].name!), + shape: + index == state.activeAccountIndex + ? RoundedRectangleBorder( + side: BorderSide( + color: Colors.black, + width: 2, + ), + borderRadius: BorderRadius.circular( + 12, + ), + ) + : null, + child: InkWell( + onTap: () { + GetIt.I + .get() + .setActiveAccountIndex(index); + }, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(state.accounts[index].name!), + FutureBuilder( + future: getTotalBalance( + state.accounts[index], + ), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Container(); + } + + return Text( + formatCurrency(snapshot.data!), + ); + }, + ), + ], + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: TotalBalanceCard(), + ), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: UpcomingTransactionsCard(), + ), + + /* + BlocBuilder( + builder: + (context, state) => Row( + children: [ + Expanded( + child: Padding( + padding: EdgeInsets.all(16), + child: AccountBalanceGraphCard(), + ), + ), + ], + ), + ),*/ + Row( + children: [ + Padding(padding: EdgeInsets.all(16), child: BreakdownCard()), + ], + ), + ], + ), + Positioned( + right: 16, + bottom: 16, + child: FloatingActionButton( + child: Icon(Icons.add), + onPressed: () { + showDialogOrModal( + context: context, + builder: + (ctx) => Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: TextField( + controller: _accountNameController, + decoration: InputDecoration( + hintText: "Account name", + ), + ), + ), + + OutlinedButton( + onPressed: () async { + if (_accountNameController.text.isEmpty) return; + + final a = + Account()..name = _accountNameController.text; + final b = + Beneficiary() + ..name = _accountNameController.text + ..account.value = + GetIt.I.get().activeAccount + ..type = BeneficiaryType.account; + await upsertAccount(a); + await upsertBeneficiary(b); + _accountNameController.text = ""; + Navigator.of(context).pop(); + }, + child: Text("Add"), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ); + } +} diff --git a/lib/ui/pages/account/balance_graph_card.dart b/lib/ui/pages/account/balance_graph_card.dart new file mode 100644 index 0000000..f03d39d --- /dev/null +++ b/lib/ui/pages/account/balance_graph_card.dart @@ -0,0 +1,150 @@ +import 'dart:collection'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:okane/database/database.dart'; +import 'package:okane/ui/state/core.dart'; +import 'package:okane/ui/utils.dart'; + +double getBalanceGraphScaling(double maxBalance) { + if (maxBalance < 100) { + return 10; + } else if (maxBalance < 1000) { + return 200; + } else if (maxBalance < 10000) { + return 1000; + } + + return 10000; +} + +class AccountBalanceGraphCard extends StatelessWidget { + const AccountBalanceGraphCard({super.key}); + + Future> getAccountBalance() async { + final coreCubit = GetIt.I.get(); + final today = toMidnight(DateTime.now()); + final transactions = await getLastTransactions( + coreCubit.activeAccount!, + today, + 30, + ); + final totalBalance = await getTotalBalance(coreCubit.activeAccount!); + + // Compute the differences per day + Map differences = Map.fromEntries( + List.generate(30, (i) => i).map((i) => MapEntry(i, 0)), + ); + for (final item in transactions) { + final diff = today.difference(toMidnight(item.date)).inDays; + final balance = differences[diff]!; + differences[diff] = balance + item.amount; + } + + // Compute the balance + final balances = HashMap(); + balances[0] = totalBalance; + for (final idx in List.generate(29, (i) => i + 1)) { + balances[idx] = balances[idx - 1]! + differences[idx]!; + } + + List result = List.empty(growable: true); + result.add(FlSpot(0, totalBalance)); + result.addAll( + List.generate( + 28, + (i) => i + 1, + ).map((i) => FlSpot(i.toDouble(), balances[i]!)), + ); + return result; + } + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Text("Account balance"), + SizedBox( + height: 150, + child: FutureBuilder( + future: getAccountBalance(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return CircularProgressIndicator(); + } + + final today = DateTime.now(); + final maxBalance = snapshot.data! + .map((spot) => spot.y) + .reduce((acc, y) => y > acc ? y : acc); + return LineChart( + LineChartData( + minY: + snapshot.data! + .map((spot) => spot.y) + .reduce((acc, y) => y < acc ? y : acc) - + 20, + maxY: maxBalance + 20, + titlesData: FlTitlesData( + bottomTitles: AxisTitles( + sideTitles: SideTitles( + interval: 4, + showTitles: true, + reservedSize: 40, + getTitlesWidget: (val, meta) { + final day = today.subtract( + Duration(days: val.toInt()), + ); + return SideTitleWidget( + meta: meta, + child: Text( + formatDateTime(day, formatYear: false), + ), + ); + }, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 50, + interval: getBalanceGraphScaling(maxBalance), + getTitlesWidget: + (value, meta) => SideTitleWidget( + meta: meta, + child: Text( + formatCurrency(value, precise: false), + ), + ), + ), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + gridData: const FlGridData(show: false), + lineBarsData: [ + LineChartBarData( + isCurved: false, + barWidth: 3, + dotData: const FlDotData(show: false), + spots: snapshot.data!, + ), + ], + ), + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui/pages/account/breakdown_card.dart b/lib/ui/pages/account/breakdown_card.dart new file mode 100644 index 0000000..9c90161 --- /dev/null +++ b/lib/ui/pages/account/breakdown_card.dart @@ -0,0 +1,172 @@ +import 'dart:math'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.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/utils.dart'; + +const CATEGORY_INCOME = "Income"; +const CATEGORY_OTHER = "Other"; + +typedef LegendData = + ({Map expenses, Map colors, double usable}); + +class LegendItem extends StatelessWidget { + final String text; + final Color color; + + const LegendItem({required this.text, required this.color, super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + DecoratedBox( + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(5), + ), + child: SizedBox(width: 10, height: 10), + ), + Padding(padding: EdgeInsets.only(left: 8), child: Text(text)), + ], + ); + } +} + +class BreakdownCard extends StatelessWidget { + const BreakdownCard({super.key}); + + LegendData _computeSections(List transactions) { + Map expenses = {}; + Map colors = {}; + double usableMoney = 0; + transactions.forEach((t) { + String category; + if (t.amount > 0) { + category = CATEGORY_INCOME; + colors[CATEGORY_INCOME] = Colors.green; + } else { + if (t.expenseCategory.value?.name == null) { + category = CATEGORY_OTHER; + colors[category] = Colors.red; + } else { + category = t.expenseCategory.value!.name; + colors[category] = colorHash(t.expenseCategory.value!.name); + } + } + + expenses.update( + category, + (value) => value + t.amount.abs(), + ifAbsent: () => t.amount.abs(), + ); + usableMoney += t.amount; + }); + return (expenses: expenses, colors: colors, usable: usableMoney); + } + + @override + Widget build(BuildContext context) { + final bloc = GetIt.I.get(); + return Card( + child: Padding( + padding: const EdgeInsets.all(8), + child: BlocBuilder( + builder: (context, state) { + if (bloc.activeAccount == null) { + return Text("No active account"); + } + + return FutureBuilder( + future: getLastTransactions( + bloc.activeAccount!, + DateTime.now(), + 30, + ), + builder: (context, snapshot) { + final title = Padding( + padding: EdgeInsets.only(bottom: 16), + child: Text("Expense Breakdown"), + ); + if (!snapshot.hasData) { + return Column(children: [title, CircularProgressIndicator()]); + } + + if (snapshot.data!.isEmpty) { + return Column(children: [title, Text("No transactions")]); + } + + final data = _computeSections(snapshot.data!); + final sectionData = + data.expenses.entries + .map( + (entry) => PieChartSectionData( + value: entry.value, + title: formatCurrency(entry.value, precise: false), + titleStyle: TextStyle(fontWeight: FontWeight.bold), + radius: 40, + color: data.colors[entry.key]!, + ), + ) + .toList(); + return Column( + children: [ + title, + Row( + children: [ + SizedBox( + width: 150, + height: 150, + child: AspectRatio( + aspectRatio: 1, + child: PieChart( + PieChartData( + borderData: FlBorderData(show: false), + sectionsSpace: 0, + centerSpaceRadius: 35, + sections: sectionData, + ), + ), + ), + ), + + Padding( + padding: EdgeInsets.only(left: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: + data.expenses.keys + .map( + (key) => LegendItem( + text: key, + color: data.colors[key]!, + ), + ) + .toList(), + ), + ), + ], + ), + + Padding( + padding: EdgeInsets.only(top: 16), + child: Text( + "Available money: ${formatCurrency(data.usable)}", + ), + ), + ], + ); + }, + ); + }, + ), + ), + ); + } +} diff --git a/lib/ui/pages/account/total_balance_card.dart b/lib/ui/pages/account/total_balance_card.dart new file mode 100644 index 0000000..2d09957 --- /dev/null +++ b/lib/ui/pages/account/total_balance_card.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:okane/database/collections/account.dart'; +import 'package:okane/database/database.dart'; +import 'package:okane/ui/state/core.dart'; +import 'package:okane/ui/utils.dart'; + +class TotalBalanceCard extends StatelessWidget { + const TotalBalanceCard({super.key}); + + Future _getTotalBalance(List accounts) async { + if (accounts.isEmpty) { + return 0; + } + + final results = await Future.wait(accounts.map(getTotalBalance).toList()); + + return results.reduce((acc, val) => acc + val); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Card( + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Total balance"), + FutureBuilder( + future: _getTotalBalance(state.accounts), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Text("..."); + } + + return Text(formatCurrency(snapshot.data!)); + }, + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/ui/pages/account/upcoming_transactions_card.dart b/lib/ui/pages/account/upcoming_transactions_card.dart new file mode 100644 index 0000000..09c41ca --- /dev/null +++ b/lib/ui/pages/account/upcoming_transactions_card.dart @@ -0,0 +1,69 @@ +import 'dart:math'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:okane/database/collections/recurrent.dart'; +import 'package:okane/ui/state/core.dart'; + +class UpcomingTransactionsCard extends StatelessWidget { + const UpcomingTransactionsCard({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final today = DateTime.now(); + final upcomingRaw = + state.recurringTransactions.where((t) { + if (t.lastExecution == null) { + return true; + } + + return today.difference(t.lastExecution!).inDays <= + (t.days * 1.5).toInt(); + }).toList(); + final List upcoming = + upcomingRaw.isEmpty + ? List.empty() + : upcomingRaw.sublist(0, min(upcomingRaw.length, 3)); + final transactions = + upcoming.isEmpty + ? [Text("No upcoming transactions")] + : upcoming + .map( + (t) => ListTile( + title: Text( + "${t.template.value!.name} (${t.template.value!.amount}€)", + ), + subtitle: Text( + "Due in ${today.difference(t.lastExecution ?? today).inDays} days", + ), + leading: Icon( + t.template.value!.amount < 0 + ? Icons.remove + : Icons.add, + color: + t.template.value!.amount < 0 + ? Colors.red + : Colors.green, + ), + trailing: IconButton( + icon: Icon(Icons.play_arrow), + onPressed: () {}, + ), + ), + ) + .toList(); + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [Text("Upcoming Transactions")] + transactions, + ), + ), + ); + }, + ); + } +} diff --git a/lib/ui/pages/template_list.dart b/lib/ui/pages/template_list.dart new file mode 100644 index 0000000..332fb50 --- /dev/null +++ b/lib/ui/pages/template_list.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:okane/ui/state/core.dart'; +import 'package:okane/ui/utils.dart'; +import 'package:okane/ui/widgets/add_template.dart'; + +class TemplateListPage extends StatefulWidget { + const TemplateListPage({super.key}); + + @override + State createState() => TemplateListState(); +} + +class TemplateListState extends State { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final account = GetIt.I.get().activeAccount; + return Stack( + children: [ + Column( + children: [ + Padding( + padding: EdgeInsets.only(top: 16), + child: ListView.builder( + itemCount: state.recurringTransactions.length, + shrinkWrap: true, + itemBuilder: + (ctx, idx) => Card( + child: ListTile( + title: Text( + state + .recurringTransactions[idx] + .template + .value! + .name, + ), + ), + ), + ), + ), + ], + ), + Positioned( + right: 16, + bottom: 16, + child: FloatingActionButton( + child: Icon(Icons.add), + onPressed: + account == null + ? () {} + : () { + showDialogOrModal( + context: context, + builder: + (ctx) => AddTransactionTemplateWidget( + activeAccountItem: account, + onAdd: () { + setState(() {}); + Navigator.of(context).pop(); + }, + ), + showDragHandle: true, + ); + }, + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/ui/pages/transaction_details.dart b/lib/ui/pages/transaction_details.dart new file mode 100644 index 0000000..d3d0072 --- /dev/null +++ b/lib/ui/pages/transaction_details.dart @@ -0,0 +1,118 @@ +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:okane/database/collections/beneficiary.dart'; +import 'package:okane/database/database.dart'; +import 'package:okane/ui/state/core.dart'; +import 'package:okane/ui/utils.dart'; +import 'package:okane/ui/widgets/image_wrapper.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as p; + +class TransactionDetailsPage extends StatelessWidget { + final bool isPage; + + const TransactionDetailsPage({this.isPage = false, super.key}); + + static MaterialPageRoute get mobileRoute => + MaterialPageRoute(builder: (_) => TransactionDetailsPage(isPage: true)); + + Future _updateBeneficiaryIcon(Beneficiary beneficiary) async { + final pickedFile = await FilePicker.platform.pickFiles( + type: FileType.image, + ); + if (pickedFile == null) { + return; + } + + final file = pickedFile.files.first; + final suppPath = await getApplicationSupportDirectory(); + final imageDir = p.join(suppPath.path, "beneficiaries"); + final imagePath = p.join(imageDir, file.name); + print("Copying ${file.path!} to $imagePath"); + + await Directory(imageDir).create(recursive: true); + if (beneficiary.imagePath != null) { + await File(beneficiary.imagePath!).delete(); + } + + await File(file.path!).copy(imagePath); + + print("Updating DB"); + beneficiary.imagePath = imagePath; + await upsertBeneficiary(beneficiary); + } + + @override + Widget build(BuildContext context) { + final widget = BlocBuilder( + builder: (context, state) { + if (state.activeTransaction == null) { + return Text("No transaction selected"); + } + + return Padding( + padding: const EdgeInsets.all(8.0), + child: ListView( + children: [ + Row( + children: [ + StreamBuilder( + stream: watchBeneficiaryObject( + state.activeTransaction!.beneficiary.value!.id, + ), + builder: (context, snapshot) { + final obj = + snapshot.data ?? + state.activeTransaction!.beneficiary.value!; + return ImageWrapper( + title: obj.name, + path: obj.imagePath, + onTap: () => _updateBeneficiaryIcon(obj), + ); + }, + ), + Padding( + padding: EdgeInsets.only(left: 8), + child: Text( + state.activeTransaction!.beneficiary.value!.name, + ), + ), + Spacer(), + IconButton( + icon: Icon(Icons.edit), + onPressed: () { + // TODO: Implement + }, + ), + ], + ), + Wrap( + spacing: 8, + children: + state.activeTransaction!.tags + .map((tag) => Chip(label: Text(tag))) + .toList(), + ), + Row( + children: [ + state.activeTransaction!.amount > 0 + ? Icon(Icons.add) + : Icon(Icons.remove), + Text(formatCurrency(state.activeTransaction!.amount)), + ], + ), + ], + ), + ); + }, + ); + + if (isPage) { + return Scaffold(body: widget); + } + return widget; + } +} diff --git a/lib/ui/pages/transaction_list.dart b/lib/ui/pages/transaction_list.dart new file mode 100644 index 0000000..1244cd3 --- /dev/null +++ b/lib/ui/pages/transaction_list.dart @@ -0,0 +1,163 @@ +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/collections/transaction.dart'; +import 'package:okane/database/database.dart'; +import 'package:okane/screen.dart'; +import 'package:okane/ui/pages/account/balance_graph_card.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/transaction_card.dart'; + +class TransactionListPage extends StatefulWidget { + const TransactionListPage({super.key}); + + @override + State createState() => TransactionListState(); +} + +class TransactionListState extends State { + final _scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final account = GetIt.I.get().activeAccount; + return Stack( + children: [ + CustomScrollView( + controller: _scrollController, + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: AccountBalanceGraphCard(), + ), + ), + SliverToBoxAdapter( + child: GroupedListView( + physics: NeverScrollableScrollPhysics(), + 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) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: TransactionCard( + transaction: item, + onTap: () { + GetIt.I.get().setActiveTransaction( + item, + ); + if (getScreenSize(ctx) == ScreenSize.small) { + Navigator.of( + context, + ).pushNamed("/transactions/details"); + } + }, + ), + ), + ), + ), + ], + ), + /*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().setActiveTransaction( + item, + ); + if (getScreenSize(ctx) == ScreenSize.small) { + Navigator.of( + context, + ).pushNamed("/transactions/details"); + } + }, + ), + ), + ), + ], + ),*/ + Positioned( + right: 16, + bottom: 16, + child: FloatingActionButton( + child: Icon(Icons.add), + onPressed: + account == null + ? () {} + : () { + showDialogOrModal( + context: context, + builder: + (ctx) => AddTransactionWidget( + activeAccountItem: account, + onAdd: () { + setState(() {}); + Navigator.of(context).pop(); + }, + ), + showDragHandle: true, + ); + }, + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/ui/state/core.dart b/lib/ui/state/core.dart new file mode 100644 index 0000000..4f96781 --- /dev/null +++ b/lib/ui/state/core.dart @@ -0,0 +1,152 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.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/database/database.dart'; +import 'package:okane/ui/navigation.dart'; + +part 'core.freezed.dart'; + +@freezed +abstract class CoreState with _$CoreState { + const factory CoreState({ + @Default(OkanePage.accounts) OkanePage activePage, + int? activeAccountIndex, + @Default(null) Transaction? activeTransaction, + @Default([]) List accounts, + @Default([]) List recurringTransactions, + @Default([]) List transactions, + @Default([]) List transactionTemplates, + @Default([]) List beneficiaries, + @Default([]) List expenseCategories, + }) = _CoreState; +} + +class CoreCubit extends Cubit { + CoreCubit() : super(CoreState()); + + StreamSubscription? _recurringTransactionStreamSubscription; + StreamSubscription? _transactionTemplatesStreamSubcription; + StreamSubscription? _accountsStreamSubscription; + StreamSubscription? _transactionsStreamSubscription; + StreamSubscription? _beneficiariesStreamSubscription; + StreamSubscription? _expenseCategoryStreamSubscription; + + void setupAccountStream() { + _accountsStreamSubscription?.cancel(); + _accountsStreamSubscription = watchAccounts().listen((_) async { + final resetStreams = state.activeAccountIndex == null; + final accounts = await getAccounts(); + emit( + state.copyWith( + accounts: accounts, + activeAccountIndex: state.activeAccountIndex ?? 0, + ), + ); + + if (resetStreams) { + setupStreams(accounts[0]); + } + }); + } + + void setupStreams(Account account) { + setupAccountStream(); + _recurringTransactionStreamSubscription?.cancel(); + _recurringTransactionStreamSubscription = watchRecurringTransactions( + activeAccount!, + ).listen((_) async { + emit( + state.copyWith( + recurringTransactions: await getRecurringTransactions(activeAccount!), + ), + ); + }); + _transactionTemplatesStreamSubcription?.cancel(); + _transactionTemplatesStreamSubcription = watchTransactionTemplates( + activeAccount!, + ).listen((_) async { + emit( + state.copyWith( + transactionTemplates: await getTransactionTemplates(activeAccount!), + ), + ); + }); + _transactionsStreamSubscription?.cancel(); + _transactionsStreamSubscription = watchTransactions(activeAccount!).listen(( + _, + ) async { + emit(state.copyWith(transactions: await getTransactions(activeAccount!))); + }); + _beneficiariesStreamSubscription?.cancel(); + _beneficiariesStreamSubscription = watchBeneficiaries().listen((_) async { + emit(state.copyWith(beneficiaries: await getBeneficiaries())); + }); + _expenseCategoryStreamSubscription?.cancel(); + _expenseCategoryStreamSubscription = watchExpenseCategory().listen(( + _, + ) async { + emit(state.copyWith(expenseCategories: await getExpenseCategories())); + }); + } + + Future init() async { + final accounts = await getAccounts(); + final account = accounts.isEmpty ? null : accounts[0]; + emit( + state.copyWith( + accounts: accounts, + activeAccountIndex: accounts.isEmpty ? null : 0, + transactions: await getTransactions(account), + beneficiaries: await getBeneficiaries(), + transactionTemplates: await getTransactionTemplates(account), + recurringTransactions: await getRecurringTransactions(account), + expenseCategories: await getExpenseCategories(), + ), + ); + + if (account != null) { + setupStreams(account); + } else { + setupAccountStream(); + } + print("Core init done"); + } + + void setPage(OkanePage page) { + emit(state.copyWith(activePage: page)); + } + + Future setActiveAccountIndex(int index) async { + final account = state.accounts[index]; + emit( + state.copyWith( + activeAccountIndex: index, + transactions: await getTransactions(account), + beneficiaries: await getBeneficiaries(), + transactionTemplates: await getTransactionTemplates(account), + recurringTransactions: await getRecurringTransactions(account), + ), + ); + setupStreams(account); + } + + void setActiveTransaction(Transaction? item) { + emit(state.copyWith(activeTransaction: item)); + } + + void setAccounts(List accounts) { + emit(state.copyWith(accounts: accounts)); + } + + Account? get activeAccount => + state.activeAccountIndex == null + ? null + : state.accounts[state.activeAccountIndex!]; +} diff --git a/lib/ui/state/core.freezed.dart b/lib/ui/state/core.freezed.dart new file mode 100644 index 0000000..49d8a24 --- /dev/null +++ b/lib/ui/state/core.freezed.dart @@ -0,0 +1,408 @@ +// 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 'core.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(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', +); + +/// @nodoc +mixin _$CoreState { + OkanePage get activePage => throw _privateConstructorUsedError; + int? get activeAccountIndex => throw _privateConstructorUsedError; + Transaction? get activeTransaction => throw _privateConstructorUsedError; + List get accounts => throw _privateConstructorUsedError; + List get recurringTransactions => + throw _privateConstructorUsedError; + List get transactions => throw _privateConstructorUsedError; + List get transactionTemplates => + throw _privateConstructorUsedError; + List get beneficiaries => throw _privateConstructorUsedError; + List get expenseCategories => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $CoreStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CoreStateCopyWith<$Res> { + factory $CoreStateCopyWith(CoreState value, $Res Function(CoreState) then) = + _$CoreStateCopyWithImpl<$Res, CoreState>; + @useResult + $Res call({ + OkanePage activePage, + int? activeAccountIndex, + Transaction? activeTransaction, + List accounts, + List recurringTransactions, + List transactions, + List transactionTemplates, + List beneficiaries, + List expenseCategories, + }); +} + +/// @nodoc +class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState> + implements $CoreStateCopyWith<$Res> { + _$CoreStateCopyWithImpl(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? activePage = null, + Object? activeAccountIndex = freezed, + Object? activeTransaction = freezed, + Object? accounts = null, + Object? recurringTransactions = null, + Object? transactions = null, + Object? transactionTemplates = null, + Object? beneficiaries = null, + Object? expenseCategories = null, + }) { + return _then( + _value.copyWith( + activePage: + null == activePage + ? _value.activePage + : activePage // ignore: cast_nullable_to_non_nullable + as OkanePage, + activeAccountIndex: + freezed == activeAccountIndex + ? _value.activeAccountIndex + : activeAccountIndex // ignore: cast_nullable_to_non_nullable + as int?, + activeTransaction: + freezed == activeTransaction + ? _value.activeTransaction + : activeTransaction // ignore: cast_nullable_to_non_nullable + as Transaction?, + accounts: + null == accounts + ? _value.accounts + : accounts // ignore: cast_nullable_to_non_nullable + as List, + recurringTransactions: + null == recurringTransactions + ? _value.recurringTransactions + : recurringTransactions // ignore: cast_nullable_to_non_nullable + as List, + transactions: + null == transactions + ? _value.transactions + : transactions // ignore: cast_nullable_to_non_nullable + as List, + transactionTemplates: + null == transactionTemplates + ? _value.transactionTemplates + : transactionTemplates // ignore: cast_nullable_to_non_nullable + as List, + beneficiaries: + null == beneficiaries + ? _value.beneficiaries + : beneficiaries // ignore: cast_nullable_to_non_nullable + as List, + expenseCategories: + null == expenseCategories + ? _value.expenseCategories + : expenseCategories // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$CoreStateImplCopyWith<$Res> + implements $CoreStateCopyWith<$Res> { + factory _$$CoreStateImplCopyWith( + _$CoreStateImpl value, + $Res Function(_$CoreStateImpl) then, + ) = __$$CoreStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + OkanePage activePage, + int? activeAccountIndex, + Transaction? activeTransaction, + List accounts, + List recurringTransactions, + List transactions, + List transactionTemplates, + List beneficiaries, + List expenseCategories, + }); +} + +/// @nodoc +class __$$CoreStateImplCopyWithImpl<$Res> + extends _$CoreStateCopyWithImpl<$Res, _$CoreStateImpl> + implements _$$CoreStateImplCopyWith<$Res> { + __$$CoreStateImplCopyWithImpl( + _$CoreStateImpl _value, + $Res Function(_$CoreStateImpl) _then, + ) : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? activePage = null, + Object? activeAccountIndex = freezed, + Object? activeTransaction = freezed, + Object? accounts = null, + Object? recurringTransactions = null, + Object? transactions = null, + Object? transactionTemplates = null, + Object? beneficiaries = null, + Object? expenseCategories = null, + }) { + return _then( + _$CoreStateImpl( + activePage: + null == activePage + ? _value.activePage + : activePage // ignore: cast_nullable_to_non_nullable + as OkanePage, + activeAccountIndex: + freezed == activeAccountIndex + ? _value.activeAccountIndex + : activeAccountIndex // ignore: cast_nullable_to_non_nullable + as int?, + activeTransaction: + freezed == activeTransaction + ? _value.activeTransaction + : activeTransaction // ignore: cast_nullable_to_non_nullable + as Transaction?, + accounts: + null == accounts + ? _value._accounts + : accounts // ignore: cast_nullable_to_non_nullable + as List, + recurringTransactions: + null == recurringTransactions + ? _value._recurringTransactions + : recurringTransactions // ignore: cast_nullable_to_non_nullable + as List, + transactions: + null == transactions + ? _value._transactions + : transactions // ignore: cast_nullable_to_non_nullable + as List, + transactionTemplates: + null == transactionTemplates + ? _value._transactionTemplates + : transactionTemplates // ignore: cast_nullable_to_non_nullable + as List, + beneficiaries: + null == beneficiaries + ? _value._beneficiaries + : beneficiaries // ignore: cast_nullable_to_non_nullable + as List, + expenseCategories: + null == expenseCategories + ? _value._expenseCategories + : expenseCategories // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc + +class _$CoreStateImpl implements _CoreState { + const _$CoreStateImpl({ + this.activePage = OkanePage.accounts, + this.activeAccountIndex, + this.activeTransaction = null, + final List accounts = const [], + final List recurringTransactions = const [], + final List transactions = const [], + final List transactionTemplates = const [], + final List beneficiaries = const [], + final List expenseCategories = const [], + }) : _accounts = accounts, + _recurringTransactions = recurringTransactions, + _transactions = transactions, + _transactionTemplates = transactionTemplates, + _beneficiaries = beneficiaries, + _expenseCategories = expenseCategories; + + @override + @JsonKey() + final OkanePage activePage; + @override + final int? activeAccountIndex; + @override + @JsonKey() + final Transaction? activeTransaction; + final List _accounts; + @override + @JsonKey() + List get accounts { + if (_accounts is EqualUnmodifiableListView) return _accounts; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_accounts); + } + + final List _recurringTransactions; + @override + @JsonKey() + List get recurringTransactions { + if (_recurringTransactions is EqualUnmodifiableListView) + return _recurringTransactions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_recurringTransactions); + } + + final List _transactions; + @override + @JsonKey() + List get transactions { + if (_transactions is EqualUnmodifiableListView) return _transactions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_transactions); + } + + final List _transactionTemplates; + @override + @JsonKey() + List get transactionTemplates { + if (_transactionTemplates is EqualUnmodifiableListView) + return _transactionTemplates; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_transactionTemplates); + } + + final List _beneficiaries; + @override + @JsonKey() + List get beneficiaries { + if (_beneficiaries is EqualUnmodifiableListView) return _beneficiaries; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_beneficiaries); + } + + final List _expenseCategories; + @override + @JsonKey() + List get expenseCategories { + if (_expenseCategories is EqualUnmodifiableListView) + return _expenseCategories; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_expenseCategories); + } + + @override + String toString() { + return 'CoreState(activePage: $activePage, activeAccountIndex: $activeAccountIndex, activeTransaction: $activeTransaction, accounts: $accounts, recurringTransactions: $recurringTransactions, transactions: $transactions, transactionTemplates: $transactionTemplates, beneficiaries: $beneficiaries, expenseCategories: $expenseCategories)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CoreStateImpl && + (identical(other.activePage, activePage) || + other.activePage == activePage) && + (identical(other.activeAccountIndex, activeAccountIndex) || + other.activeAccountIndex == activeAccountIndex) && + (identical(other.activeTransaction, activeTransaction) || + other.activeTransaction == activeTransaction) && + const DeepCollectionEquality().equals(other._accounts, _accounts) && + const DeepCollectionEquality().equals( + other._recurringTransactions, + _recurringTransactions, + ) && + const DeepCollectionEquality().equals( + other._transactions, + _transactions, + ) && + const DeepCollectionEquality().equals( + other._transactionTemplates, + _transactionTemplates, + ) && + const DeepCollectionEquality().equals( + other._beneficiaries, + _beneficiaries, + ) && + const DeepCollectionEquality().equals( + other._expenseCategories, + _expenseCategories, + )); + } + + @override + int get hashCode => Object.hash( + runtimeType, + activePage, + activeAccountIndex, + activeTransaction, + const DeepCollectionEquality().hash(_accounts), + const DeepCollectionEquality().hash(_recurringTransactions), + const DeepCollectionEquality().hash(_transactions), + const DeepCollectionEquality().hash(_transactionTemplates), + const DeepCollectionEquality().hash(_beneficiaries), + const DeepCollectionEquality().hash(_expenseCategories), + ); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$CoreStateImplCopyWith<_$CoreStateImpl> get copyWith => + __$$CoreStateImplCopyWithImpl<_$CoreStateImpl>(this, _$identity); +} + +abstract class _CoreState implements CoreState { + const factory _CoreState({ + final OkanePage activePage, + final int? activeAccountIndex, + final Transaction? activeTransaction, + final List accounts, + final List recurringTransactions, + final List transactions, + final List transactionTemplates, + final List beneficiaries, + final List expenseCategories, + }) = _$CoreStateImpl; + + @override + OkanePage get activePage; + @override + int? get activeAccountIndex; + @override + Transaction? get activeTransaction; + @override + List get accounts; + @override + List get recurringTransactions; + @override + List get transactions; + @override + List get transactionTemplates; + @override + List get beneficiaries; + @override + List get expenseCategories; + @override + @JsonKey(ignore: true) + _$$CoreStateImplCopyWith<_$CoreStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/ui/transaction.dart b/lib/ui/transaction.dart new file mode 100644 index 0000000..2db6a61 --- /dev/null +++ b/lib/ui/transaction.dart @@ -0,0 +1 @@ +enum TransactionDirection { send, receive } diff --git a/lib/ui/utils.dart b/lib/ui/utils.dart new file mode 100644 index 0000000..e85bfcf --- /dev/null +++ b/lib/ui/utils.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:okane/screen.dart'; + +Future showDialogOrModal({ + required BuildContext context, + required WidgetBuilder builder, + bool showDragHandle = true, +}) { + final screenSize = getScreenSize(context); + final width = MediaQuery.sizeOf(context).shortestSide; + + return switch (screenSize) { + ScreenSize.small => showModalBottomSheet( + context: context, + showDragHandle: showDragHandle, + builder: + (context) => Padding( + padding: EdgeInsets.only(bottom: 32), + child: builder(context), + ), + ), + ScreenSize.normal => showDialog( + context: context, + builder: + (context) => Dialog( + child: Container( + constraints: BoxConstraints(maxWidth: width * 0.7), + child: Padding( + padding: EdgeInsets.only(bottom: 32), + child: builder(context), + ), + ), + ), + ), + }; +} + +DateTime toMidnight(DateTime t) { + return DateTime(t.year, t.month, t.day); +} + +String zeroPad(int i) { + if (i <= 9) { + return "0$i"; + } + + return i.toString(); +} + +String formatDateTime(DateTime dt, {bool formatYear = true}) { + if (!formatYear) { + return "${zeroPad(dt.day)}.${zeroPad(dt.month)}"; + } + return "${zeroPad(dt.day)}.${zeroPad(dt.month)}.${zeroPad(dt.year)}"; +} + +Color colorHash(String text) { + final hue = + text.characters + .map((c) => c.codeUnitAt(0).toDouble()) + .reduce((acc, c) => c + ((acc.toInt() << 5) - acc)) % + 360; + return HSVColor.fromAHSV(1, hue, 0.5, 0.5).toColor(); +} + +String formatCurrency(double amount, {bool precise = true}) { + if (!precise) { + return "${amount.toInt()}€"; + } + return "${amount.toStringAsFixed(2)}€"; +} diff --git a/lib/ui/widgets/add_expense_category.dart b/lib/ui/widgets/add_expense_category.dart new file mode 100644 index 0000000..c9a94b3 --- /dev/null +++ b/lib/ui/widgets/add_expense_category.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:okane/database/collections/expense_category.dart'; +import 'package:okane/database/database.dart'; +import 'package:okane/ui/state/core.dart'; + +class AddExpenseCategory extends StatefulWidget { + const AddExpenseCategory({super.key}); + + @override + AddExpenseCategoryState createState() => AddExpenseCategoryState(); +} + +class AddExpenseCategoryState extends State { + final TextEditingController _categoryNameController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: + (context, state) => ConstrainedBox( + constraints: BoxConstraints(maxHeight: 300), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListView.builder( + itemCount: state.expenseCategories.length, + shrinkWrap: true, + itemBuilder: + (context, index) => ListTile( + title: Text(state.expenseCategories[index].name), + onTap: () { + _categoryNameController.text = ""; + Navigator.of( + context, + ).pop(state.expenseCategories[index]); + }, + ), + ), + + TextField( + decoration: InputDecoration(hintText: "Category name"), + controller: _categoryNameController, + ), + Row( + children: [ + Spacer(), + OutlinedButton( + onPressed: () async { + final category = + ExpenseCategory() + ..name = _categoryNameController.text; + await upsertExpenseCategory(category); + _categoryNameController.text = ""; + Navigator.of(context).pop(category); + }, + child: Text("Add"), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui/widgets/add_recurring_transaction.dart b/lib/ui/widgets/add_recurring_transaction.dart new file mode 100644 index 0000000..7f3089f --- /dev/null +++ b/lib/ui/widgets/add_recurring_transaction.dart @@ -0,0 +1,284 @@ +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 createState() => + _AddRecurringTransactionTemplateWidgetState(); +} + +class _AddRecurringTransactionTemplateWidgetState + extends State { + final TextEditingController _beneficiaryTextController = + TextEditingController(); + final TextEditingController _amountTextController = TextEditingController(); + final TextEditingController _templateNameController = TextEditingController(); + + List beneficiaries = []; + + SearchFieldListItem? _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 _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( + 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( + suggestions: + beneficiaries + .where((el) { + final bloc = GetIt.I.get(); + 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( + segments: [ + ButtonSegment( + value: TransactionDirection.send, + label: Text("Send"), + icon: Icon(Icons.remove), + ), + ButtonSegment( + value: TransactionDirection.receive, + label: Text("Receive"), + icon: Icon(Icons.add), + ), + ], + selected: {_selectedDirection}, + multiSelectionEnabled: false, + onSelectionChanged: (selection) { + setState(() => _selectedDirection = selection.first); + }, + ), + ), + + Padding( + padding: EdgeInsets.only(left: 16, right: 16, top: 16), + child: SegmentedButton( + 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: {_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"), + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/add_template.dart b/lib/ui/widgets/add_template.dart new file mode 100644 index 0000000..5cda454 --- /dev/null +++ b/lib/ui/widgets/add_template.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.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/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:okane/ui/widgets/add_expense_category.dart'; +import 'package:searchfield/searchfield.dart'; + +class AddTransactionTemplateWidget extends StatefulWidget { + final VoidCallback onAdd; + + final Account activeAccountItem; + + const AddTransactionTemplateWidget({ + super.key, + required this.activeAccountItem, + required this.onAdd, + }); + + @override + State createState() => + _AddTransactionTemplateWidgetState(); +} + +class _AddTransactionTemplateWidgetState + extends State { + final TextEditingController _beneficiaryTextController = + TextEditingController(); + final TextEditingController _amountTextController = TextEditingController(); + final TextEditingController _templateNameController = TextEditingController(); + + SearchFieldListItem? _selectedBeneficiary; + + TransactionDirection _selectedDirection = TransactionDirection.send; + + ExpenseCategory? _expenseCategory = null; + + String getBeneficiaryName(Beneficiary item) { + return switch (item.type) { + BeneficiaryType.account => "${item.name} (Account)", + BeneficiaryType.other => item.name, + }; + } + + Future _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( + 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 factor = switch (_selectedDirection) { + TransactionDirection.send => -1, + TransactionDirection.receive => 1, + }; + final amount = factor * double.parse(_amountTextController.text).abs(); + final transaction = + TransactionTemplate() + ..name = _templateNameController.text + ..account.value = widget.activeAccountItem + ..beneficiary.value = beneficiary + ..expenseCategory.value = _expenseCategory + ..recurring = false + ..amount = amount; + await upsertTransactionTemplate(transaction); + 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: BlocBuilder( + builder: + (context, state) => SearchField( + suggestions: + state.beneficiaries + .where((el) { + final bloc = GetIt.I.get(); + 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: const EdgeInsets.symmetric(vertical: 8), + child: SegmentedButton( + segments: [ + ButtonSegment( + value: TransactionDirection.send, + label: Text("Send"), + icon: Icon(Icons.remove), + ), + ButtonSegment( + value: TransactionDirection.receive, + label: Text("Receive"), + icon: Icon(Icons.add), + ), + ], + selected: {_selectedDirection}, + multiSelectionEnabled: false, + onSelectionChanged: (selection) { + setState(() => _selectedDirection = selection.first); + }, + ), + ), + + Align( + alignment: Alignment.centerRight, + child: OutlinedButton( + onPressed: () => _submit(context), + child: Text("Add"), + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/add_transaction.dart b/lib/ui/widgets/add_transaction.dart new file mode 100644 index 0000000..a32927e --- /dev/null +++ b/lib/ui/widgets/add_transaction.dart @@ -0,0 +1,311 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.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/transaction.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:okane/ui/widgets/add_expense_category.dart'; +import 'package:searchfield/searchfield.dart'; + +class AddTransactionWidget extends StatefulWidget { + final VoidCallback onAdd; + + final Account activeAccountItem; + + const AddTransactionWidget({ + super.key, + required this.activeAccountItem, + required this.onAdd, + }); + + @override + State createState() => _AddTransactionWidgetState(); +} + +class _AddTransactionWidgetState extends State { + final TextEditingController _beneficiaryTextController = + TextEditingController(); + final TextEditingController _amountTextController = TextEditingController(); + + DateTime _selectedDate = DateTime.now(); + + SearchFieldListItem? _selectedBeneficiary; + + TransactionDirection _selectedDirection = TransactionDirection.send; + + ExpenseCategory? _expenseCategory = null; + + String getBeneficiaryName(Beneficiary item) { + return switch (item.type) { + BeneficiaryType.account => "${item.name} (Account)", + BeneficiaryType.other => item.name, + }; + } + + Future _submit(BuildContext context) async { + final beneficiaryName = _beneficiaryTextController.text; + if (_selectedBeneficiary == null && beneficiaryName.isEmpty) { + return; + } + + Beneficiary? beneficiary = _selectedBeneficiary?.item; + if (beneficiary == null || + getBeneficiaryName(beneficiary) != beneficiaryName) { + // Add a new beneficiary, if none was selected + final result = await showDialog( + 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 factor = switch (_selectedDirection) { + TransactionDirection.send => -1, + TransactionDirection.receive => 1, + }; + final amount = factor * double.parse(_amountTextController.text).abs(); + final transaction = + Transaction() + ..account.value = widget.activeAccountItem + ..beneficiary.value = beneficiary + ..amount = amount + ..tags = [] + ..expenseCategory.value = _expenseCategory + ..date = _selectedDate; + await upsertTransaction(transaction); + + if (beneficiary.type == BeneficiaryType.account) { + final otherTransaction = + Transaction() + ..account.value = beneficiary.account.value! + ..beneficiary.value = await getAccountBeneficiary( + widget.activeAccountItem, + ) + ..date = _selectedDate + ..expenseCategory.value = _expenseCategory + ..amount = -1 * amount; + await upsertTransaction(otherTransaction); + } + + widget.onAdd(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: ListView( + shrinkWrap: true, + children: [ + OutlinedButton( + onPressed: () async { + final template = await showDialogOrModal( + context: context, + builder: (context) { + return BlocBuilder( + 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) { + return; + } + + _amountTextController.text = template.amount.toString(); + _selectedDirection = + template.amount > 0 + ? TransactionDirection.receive + : TransactionDirection.send; + _selectedBeneficiary = SearchFieldListItem( + getBeneficiaryName(template.beneficiary.value!), + item: template.beneficiary.value!, + ); + _beneficiaryTextController.text = getBeneficiaryName( + template.beneficiary.value!, + ); + }, + child: Text("Use template"), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: BlocBuilder( + builder: + (context, state) => SearchField( + suggestions: + state.beneficiaries + .where((el) { + final bloc = GetIt.I.get(); + 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: "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: Row( + mainAxisAlignment: MainAxisAlignment.start, + 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: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Icon(Icons.date_range), + Text(formatDateTime(_selectedDate)), + ], + ), + ), + ], + ), + ), + + 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: const EdgeInsets.symmetric(vertical: 8), + child: SegmentedButton( + segments: [ + ButtonSegment( + value: TransactionDirection.send, + label: Text("Send"), + icon: Icon(Icons.remove), + ), + ButtonSegment( + value: TransactionDirection.receive, + label: Text("Receive"), + icon: Icon(Icons.add), + ), + ], + selected: {_selectedDirection}, + multiSelectionEnabled: false, + onSelectionChanged: (selection) { + setState(() => _selectedDirection = selection.first); + }, + ), + ), + + Align( + alignment: Alignment.centerRight, + child: OutlinedButton( + onPressed: () => _submit(context), + child: Text("Add"), + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/image_wrapper.dart b/lib/ui/widgets/image_wrapper.dart new file mode 100644 index 0000000..47cf75c --- /dev/null +++ b/lib/ui/widgets/image_wrapper.dart @@ -0,0 +1,40 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; + +class ImageWrapper extends StatelessWidget { + final String title; + final String? path; + final VoidCallback onTap; + + const ImageWrapper({ + super.key, + required this.title, + required this.onTap, + this.path, + }); + + @override + Widget build(BuildContext context) { + Widget widget; + if (path == null) { + widget = SizedBox( + width: 45, + height: 45, + child: Container( + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(8), + ), + child: Center(child: Text(title[0])), + ), + ); + } else { + widget = ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file(File(path!), width: 45, height: 45), + ); + } + + return InkWell(onTap: onTap, child: widget); + } +} diff --git a/lib/ui/widgets/transaction_card.dart b/lib/ui/widgets/transaction_card.dart new file mode 100644 index 0000000..295fede --- /dev/null +++ b/lib/ui/widgets/transaction_card.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:okane/database/collections/transaction.dart'; +import 'package:okane/database/database.dart'; +import 'package:okane/ui/utils.dart'; +import 'package:okane/ui/widgets/image_wrapper.dart'; + +class TransactionCard extends StatelessWidget { + final Widget? subtitle; + + const TransactionCard({ + super.key, + required this.transaction, + required this.onTap, + this.subtitle, + }); + + final Transaction transaction; + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Card( + child: ListTile( + onTap: onTap, + leading: ImageWrapper( + title: transaction.beneficiary.value!.name, + path: transaction.beneficiary.value!.imagePath, + onTap: () {}, + ), + trailing: Text(formatDateTime(transaction.date)), + title: Text(transaction.beneficiary.value!.name), + subtitle: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + formatCurrency(transaction.amount), + style: TextStyle( + color: transaction.amount < 0 ? Colors.red : Colors.green, + ), + ), + if (subtitle != null) subtitle!, + ], + ), + ), + ); + } +} diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..bad38c7 --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "okane") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.okane") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..b898c8c --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin"); + isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar); +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..cb083af --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + isar_flutter_libs +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/runner/CMakeLists.txt b/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/linux/runner/main.cc b/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc new file mode 100644 index 0000000..6838b53 --- /dev/null +++ b/linux/runner/my_application.cc @@ -0,0 +1,130 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "okane"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "okane"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/linux/runner/my_application.h b/linux/runner/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..3fd23fa --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,818 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + url: "https://pub.dev" + source: hosted + version: "61.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + url: "https://pub.dev" + source: hosted + version: "5.13.0" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://pub.dev" + source: hosted + version: "2.12.0" + bloc: + dependency: "direct main" + description: + name: bloc + sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189" + url: "https://pub.dev" + source: hosted + version: "9.0.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + url: "https://pub.dev" + source: hosted + version: "4.0.4" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + url: "https://pub.dev" + source: hosted + version: "2.4.13" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + url: "https://pub.dev" + source: hosted + version: "7.3.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 + url: "https://pub.dev" + source: hosted + version: "8.9.5" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.dev" + source: hosted + version: "4.10.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + dartx: + dependency: transitive + description: + name: dartx + sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + url: "https://pub.dev" + source: hosted + version: "1.3.2" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "8986dec4581b4bcd4b6df5d75a2ea0bede3db802f500635d05fa8be298f9467f" + url: "https://pub.dev" + source: hosted + version: "10.1.2" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: f2e9137f261d0f53a820f6b829c80ba570ac915284c8e32789d973834796eca0 + url: "https://pub.dev" + source: hosted + version: "0.71.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: "1046d719fbdf230330d3443187cc33cc11963d15c9089f6cc56faa42a4c5f0cc" + url: "https://pub.dev" + source: hosted + version: "9.1.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_picker_plus: + dependency: "direct main" + description: + name: flutter_picker_plus + sha256: "357f81676edb657b589221ad079dc7d1c68e175046d0afd609d2cab3531e7dd7" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e + url: "https://pub.dev" + source: hosted + version: "2.0.28" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: "24f77b50776d4285cc4b3a1665bb79852714c09b878363efbe64788c179c4284" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: f126a3e286b7f5b578bf436d5592968706c4c1de28a228b870ce375d9f743103 + url: "https://pub.dev" + source: hosted + version: "8.0.3" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + grouped_list: + dependency: "direct main" + description: + name: grouped_list + sha256: c52551bc17699e304634d4653b824a1aa7c6b1d3a2c1a0da1a80839f867353fb + url: "https://pub.dev" + source: hosted + version: "6.0.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + 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: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + url: "https://pub.dev" + source: hosted + version: "10.0.8" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: "direct main" + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + provider: + dependency: transitive + description: + name: provider + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + searchfield: + dependency: "direct main" + description: + name: searchfield + sha256: "223fca0828ec95f45501db93feac7b120b93600760c0d8c04039fb2eeed9cc20" + url: "https://pub.dev" + source: hosted + version: "1.2.7" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + time: + dependency: transitive + description: + name: time + sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://pub.dev" + source: hosted + version: "14.3.1" + watcher: + dependency: transitive + description: + name: watcher + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: bfe6f435f6ec49cb6c01da1e275ae4228719e59a6b067048c51e72d9d63bcc4b + url: "https://pub.dev" + source: hosted + version: "1.0.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f + url: "https://pub.dev" + source: hosted + version: "5.12.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xxh3: + dependency: transitive + description: + name: xxh3 + sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..1a5f7de --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,36 @@ +name: okane +description: "A cross-platform finance tracker." +publish_to: 'none' + +version: 1.0.0+1 +environment: + sdk: ^3.7.0 + +dependencies: + flutter: + sdk: flutter + flutter_bloc: 9.1.0 + bloc: + cupertino_icons: ^1.0.8 + freezed_annotation: 2.4.4 + path_provider: ^2.1.5 + get_it: ^8.0.3 + grouped_list: ^6.0.0 + searchfield: ^1.2.7 + file_picker: ^10.1.2 + path: ^1.9.1 + fl_chart: ^0.71.0 + flutter_picker_plus: ^1.5.1 + isar: ^3.1.0+1 + isar_flutter_libs: ^3.1.0+1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + build_runner: ^2.4.13 + freezed: 2.5.0 + isar_generator: ^3.1.0+1 + +flutter: + uses-material-design: true