Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e2d02b643 | |||
| 4c4a1c7f20 | |||
| 97c67376f5 | |||
| aa7f99b3a5 | |||
| 78add73278 | |||
| c86c2dd888 | |||
| 68e302c600 | |||
| 94a055f21f | |||
| 68e4481f2e | |||
| 1ee4458f6f | |||
| 516db8edc1 | |||
| 4bf4843779 | |||
| 531e819c69 | |||
| a535603924 | |||
| 8c5e120fff | |||
| 63fb59bb73 | |||
| 1031e14153 | |||
| caf50aa8d2 | |||
| 6f3d987d19 | |||
| 40eaca3157 | |||
| 71cfae90f6 | |||
| d68e8ec305 | |||
| b4485915df | |||
| 03ea5b717c | |||
| a011b63fb1 | |||
| c2baac6dc0 | |||
| 8dc60b7f9d | |||
| 51d0572c11 | |||
| 7b3a1cfac6 | |||
| baef163b68 | |||
| 2011739c09 | |||
| 6c64f59ceb | |||
| 474009942e | |||
| 77da3be5d7 | |||
| e543286bc5 | |||
| 9eb8274907 | |||
| 7f189ce86d | |||
| 8d291a0d39 | |||
| 24a9e3d027 | |||
| 6a7588c3d4 | |||
| 52824a5459 | |||
| 4d716ba40d | |||
| 7324cd94f3 | |||
| 58cbd2a462 | |||
| a937d34df2 | |||
| d3804865d8 | |||
| fcdc820c74 | |||
| 58e0582304 | |||
| b9a7ef0dfa | |||
| 4014757319 | |||
| f765ba6268 | |||
| dbccb6b33d | |||
| b61d2ad096 | |||
| 2c30768746 | |||
| 824855b9b6 | |||
| 0c4c6d7c3d | |||
| 8d7f6bc4d3 | |||
| 5a12ff45c8 | |||
| 3387d86578 | |||
| c1010a8051 | |||
| 93d4d7a854 | |||
| 7a2abc84cb | |||
| a45169bf12 | |||
| 7916161e4f | |||
| 8a612fc27c | |||
| 05a5bddf09 | |||
| c11515d447 | |||
| 673d7de21c | |||
| 39323e28ac | |||
| 533feb2668 | |||
| 6bde42c815 | |||
| 246c0401cc | |||
| 016ba85416 | |||
| 7a76f0d40e | |||
| 92fec89333 | |||
| 277800a578 | |||
| 2115b06b9a | |||
| 4fd20fad22 | |||
| 34de70ab66 | |||
| c4225759d8 | |||
| 0f066c67d1 | |||
| 721e7c4bdf | |||
| efe7b4a903 | |||
| 69e7e9d817 | |||
| 47f467e17c | |||
| 46e7cb246c | |||
| 25b8f2176c | |||
| 668e3ebc67 | |||
| b42ba57570 | |||
| 19e8a0f3c7 | |||
| 7eae712808 | |||
| 0b33b83a9c | |||
| 9632cebf96 | |||
| 882d331488 | |||
| 5cb76e5d59 | |||
| 2b605f27ce | |||
| 4d80dd8a64 | |||
| c7a084b3fd | |||
| 6ef73b7921 | |||
| c8035a6ba0 | |||
| 9d8dc92d08 | |||
| 2eebfd7f5c | |||
| 4714a3ed62 | |||
| 5d6a1c4bed | |||
| 8af2bb5be8 |
1
.gitignore
vendored
@@ -196,3 +196,4 @@ doc/api/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
|
||||
debug_info/
|
||||
|
||||
39
.metadata
Normal file
@@ -0,0 +1,39 @@
|
||||
# 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: "d693b4b9dbac2acd4477aea4555ca6dcbea44ba2"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
|
||||
base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
|
||||
- platform: android
|
||||
create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
|
||||
base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
|
||||
- platform: linux
|
||||
create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
|
||||
base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
|
||||
- platform: web
|
||||
create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
|
||||
base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
|
||||
- platform: windows
|
||||
create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
|
||||
base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
|
||||
|
||||
# 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'
|
||||
17
README.md
@@ -1,3 +1,18 @@
|
||||
# dragon_ledger
|
||||
# DragonLedger 🐉📒
|
||||
|
||||
Eine Flutter-App um den Überblick über wiederkehrende Transaktionen und den damit verbunden Kontostand zu behalten.
|
||||
|
||||
# Fürs Debuggen:
|
||||
|
||||
`flutter run --web-header=Cross-Origin-Opener-Policy=same-origin --web-header=Cross-Origin-Embedder-Policy=require-corp`
|
||||
|
||||
## Release:
|
||||
|
||||
Build Android:
|
||||
- `flutter build apk --obfuscate --split-debug-info=debug_info`
|
||||
|
||||
Webserver Header:
|
||||
|
||||
- `Cross-Origin-Opener-Policy: same-origin`
|
||||
- `Cross-Origin-Embedder-Policy: require-corp`
|
||||
- sqlite3.wasm braucht `Content-Type: application/wasm`
|
||||
|
||||
252
analysis_options.yaml
Normal file
@@ -0,0 +1,252 @@
|
||||
# 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
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- "**/*.g.dart"
|
||||
|
||||
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:
|
||||
- always_declare_return_types
|
||||
- always_put_control_body_on_new_line
|
||||
- always_put_required_named_parameters_first
|
||||
- annotate_overrides
|
||||
- annotate_redeclares
|
||||
- avoid_annotating_with_dynamic
|
||||
- avoid_bool_literals_in_conditional_expressions
|
||||
- avoid_catches_without_on_clauses
|
||||
- avoid_catching_errors
|
||||
- avoid_double_and_int_checks
|
||||
- avoid_dynamic_calls
|
||||
- avoid_empty_else
|
||||
- avoid_equals_and_hash_code_on_mutable_classes
|
||||
- avoid_escaping_inner_quotes
|
||||
- avoid_field_initializers_in_const_classes
|
||||
- avoid_function_literals_in_foreach_calls
|
||||
- avoid_futureor_void
|
||||
- avoid_implementing_value_types
|
||||
- avoid_init_to_null
|
||||
- avoid_js_rounded_ints
|
||||
- avoid_multiple_declarations_per_line
|
||||
- avoid_null_checks_in_equality_operators
|
||||
- avoid_positional_boolean_parameters
|
||||
- avoid_print
|
||||
- avoid_private_typedef_functions
|
||||
- avoid_redundant_argument_values
|
||||
- avoid_relative_lib_imports
|
||||
- avoid_renaming_method_parameters
|
||||
- avoid_return_types_on_setters
|
||||
- avoid_returning_null_for_void
|
||||
- avoid_returning_this
|
||||
- avoid_setters_without_getters
|
||||
- avoid_shadowing_type_parameters
|
||||
- avoid_single_cascade_in_expression_statements
|
||||
- avoid_slow_async_io
|
||||
- avoid_type_to_string
|
||||
- avoid_types_as_parameter_names
|
||||
- avoid_unnecessary_containers
|
||||
- avoid_unused_constructor_parameters
|
||||
- avoid_void_async
|
||||
- avoid_web_libraries_in_flutter
|
||||
- await_only_futures
|
||||
- camel_case_extensions
|
||||
- camel_case_types
|
||||
- cancel_subscriptions
|
||||
- cascade_invocations
|
||||
- cast_nullable_to_non_nullable
|
||||
- close_sinks
|
||||
- collection_methods_unrelated_type
|
||||
- combinators_ordering
|
||||
- comment_references
|
||||
- conditional_uri_does_not_exist
|
||||
- constant_identifier_names
|
||||
- control_flow_in_finally
|
||||
- curly_braces_in_flow_control_structures
|
||||
- dangling_library_doc_comments
|
||||
- depend_on_referenced_packages
|
||||
- deprecated_consistency
|
||||
- deprecated_member_use_from_same_package
|
||||
- directives_ordering
|
||||
- discarded_futures
|
||||
- do_not_use_environment
|
||||
- document_ignores
|
||||
- empty_catches
|
||||
- empty_constructor_bodies
|
||||
- empty_statements
|
||||
- eol_at_end_of_file
|
||||
- exhaustive_cases
|
||||
- file_names
|
||||
- flutter_style_todos
|
||||
- hash_and_equals
|
||||
- implementation_imports
|
||||
- implicit_call_tearoffs
|
||||
- implicit_reopen
|
||||
- invalid_case_patterns
|
||||
- invalid_runtime_check_with_js_interop_types
|
||||
- join_return_with_assignment
|
||||
- leading_newlines_in_multiline_strings
|
||||
- library_annotations
|
||||
- library_names
|
||||
- library_prefixes
|
||||
- library_private_types_in_public_api
|
||||
- lines_longer_than_80_chars
|
||||
- literal_only_boolean_expressions
|
||||
- matching_super_parameters
|
||||
- missing_code_block_language_in_doc_comment
|
||||
- missing_whitespace_between_adjacent_strings
|
||||
- no_adjacent_strings_in_list
|
||||
- no_default_cases
|
||||
- no_duplicate_case_values
|
||||
- no_leading_underscores_for_library_prefixes
|
||||
- no_leading_underscores_for_local_identifiers
|
||||
- no_literal_bool_comparisons
|
||||
- no_logic_in_create_state
|
||||
- no_runtimeType_toString
|
||||
- no_self_assignments
|
||||
- no_wildcard_variable_uses
|
||||
- non_constant_identifier_names
|
||||
- noop_primitive_operations
|
||||
- null_check_on_nullable_type_parameter
|
||||
- null_closures
|
||||
- one_member_abstracts
|
||||
- only_throw_errors
|
||||
- overridden_fields
|
||||
- package_names
|
||||
- package_prefixed_library_names
|
||||
- parameter_assignments
|
||||
- prefer_adjacent_string_concatenation
|
||||
- prefer_asserts_in_initializer_lists
|
||||
- prefer_asserts_with_message
|
||||
- prefer_collection_literals
|
||||
- prefer_conditional_assignment
|
||||
- prefer_const_constructors
|
||||
- prefer_const_constructors_in_immutables
|
||||
- prefer_const_declarations
|
||||
- prefer_const_literals_to_create_immutables
|
||||
- prefer_constructors_over_static_methods
|
||||
- prefer_contains
|
||||
- prefer_expression_function_bodies
|
||||
- prefer_final_fields
|
||||
- prefer_final_in_for_each
|
||||
- prefer_final_locals
|
||||
- prefer_final_parameters
|
||||
- prefer_for_elements_to_map_fromIterable
|
||||
- prefer_foreach
|
||||
- prefer_function_declarations_over_variables
|
||||
- prefer_generic_function_type_aliases
|
||||
- prefer_if_elements_to_conditional_expressions
|
||||
- prefer_if_null_operators
|
||||
- prefer_initializing_formals
|
||||
- prefer_inlined_adds
|
||||
- prefer_int_literals
|
||||
- prefer_interpolation_to_compose_strings
|
||||
- prefer_is_empty
|
||||
- prefer_is_not_empty
|
||||
- prefer_is_not_operator
|
||||
- prefer_iterable_whereType
|
||||
- prefer_mixin
|
||||
- prefer_null_aware_method_calls
|
||||
- prefer_null_aware_operators
|
||||
- prefer_relative_imports
|
||||
- prefer_single_quotes
|
||||
- prefer_spread_collections
|
||||
- prefer_typing_uninitialized_variables
|
||||
- prefer_void_to_null
|
||||
- provide_deprecation_message
|
||||
- public_member_api_docs
|
||||
- recursive_getters
|
||||
- require_trailing_commas
|
||||
- secure_pubspec_urls
|
||||
- sized_box_for_whitespace
|
||||
- sized_box_shrink_expand
|
||||
- slash_for_doc_comments
|
||||
- sort_child_properties_last
|
||||
- sort_constructors_first
|
||||
- sort_pub_dependencies
|
||||
- sort_unnamed_constructors_first
|
||||
- specify_nonobvious_local_variable_types
|
||||
- specify_nonobvious_property_types
|
||||
- strict_top_level_inference
|
||||
- switch_on_type
|
||||
- test_types_in_equals
|
||||
- throw_in_finally
|
||||
- tighten_type_of_initializing_formals
|
||||
- type_annotate_public_apis
|
||||
- type_init_formals
|
||||
- type_literal_in_constant_pattern
|
||||
- unawaited_futures
|
||||
- unintended_html_in_doc_comment
|
||||
- unnecessary_async
|
||||
- unnecessary_await_in_return
|
||||
- unnecessary_brace_in_string_interps
|
||||
- unnecessary_breaks
|
||||
- unnecessary_const
|
||||
- unnecessary_constructor_name
|
||||
- unnecessary_getters_setters
|
||||
- unnecessary_ignore
|
||||
- unnecessary_lambdas
|
||||
- unnecessary_late
|
||||
- unnecessary_library_directive
|
||||
- unnecessary_library_name
|
||||
- unnecessary_new
|
||||
- unnecessary_null_aware_assignments
|
||||
- unnecessary_null_aware_operator_on_extension_on_nullable
|
||||
- unnecessary_null_checks
|
||||
- unnecessary_null_in_if_null_operators
|
||||
- unnecessary_nullable_for_final_variable_declarations
|
||||
- unnecessary_overrides
|
||||
- unnecessary_parenthesis
|
||||
- unnecessary_raw_strings
|
||||
- unnecessary_statements
|
||||
- unnecessary_string_escapes
|
||||
- unnecessary_string_interpolations
|
||||
- unnecessary_this
|
||||
- unnecessary_to_list_in_spreads
|
||||
- unnecessary_unawaited
|
||||
- unnecessary_underscores
|
||||
- unreachable_from_main
|
||||
- unrelated_type_equality_checks
|
||||
- unsafe_variance
|
||||
- use_build_context_synchronously
|
||||
- use_colored_box
|
||||
- use_decorated_box
|
||||
- use_enums
|
||||
- use_full_hex_values_for_flutter_colors
|
||||
- use_function_type_syntax_for_parameters
|
||||
- use_if_null_to_convert_nulls_to_bools
|
||||
- use_is_even_rather_than_modulo
|
||||
- use_key_in_widget_constructors
|
||||
- use_late_for_private_fields_and_variables
|
||||
- use_named_constants
|
||||
- use_raw_strings
|
||||
- use_rethrow_when_possible
|
||||
- use_setters_to_change_properties
|
||||
- use_string_buffers
|
||||
- use_string_in_part_of_directives
|
||||
- use_super_parameters
|
||||
- use_test_throws_matchers
|
||||
- use_to_and_as_if_applicable
|
||||
- use_truncating_division
|
||||
- valid_regexps
|
||||
- void_checks
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
14
android/.gitignore
vendored
Normal file
@@ -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
|
||||
56
android/app/build.gradle.kts
Normal file
@@ -0,0 +1,56 @@
|
||||
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 = "de.creativedragonslayer.dragon_ledger"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
// Flag to enable support for the new language APIs
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
// Sets Java compatibility to Java 11
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
multiDexEnabled = true
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "de.creativedragonslayer.dragon_ledger"
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
7
android/app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
52
android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,52 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="DragonLedger"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:turnScreenOn="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
<receiver
|
||||
android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.creativedragonslayer.dragon_ledger;
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity;
|
||||
|
||||
public class MainActivity extends FlutterActivity {
|
||||
}
|
||||
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 24 KiB |
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 290 KiB |
12
android/app/src/main/res/drawable/launch_background.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@drawable/ic_launcher_foreground"
|
||||
android:inset="16%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
18
android/app/src/main/res/values-night/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
4
android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#131636</color>
|
||||
</resources>
|
||||
18
android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
7
android/app/src/profile/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
24
android/build.gradle.kts
Normal file
@@ -0,0 +1,24 @@
|
||||
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<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
3
android/gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -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.12-all.zip
|
||||
26
android/settings.gradle.kts
Normal file
@@ -0,0 +1,26 @@
|
||||
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.9.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
BIN
assets/icon/icon.png
Executable file
|
After Width: | Height: | Size: 1.0 MiB |
4
devtools_options.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
- drift: true
|
||||
34
flutter_launcher_icons.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
# flutter pub run flutter_launcher_icons
|
||||
flutter_launcher_icons:
|
||||
image_path: "assets/icon/icon.png"
|
||||
|
||||
android: true
|
||||
# image_path_android: "assets/icon/icon.png"
|
||||
min_sdk_android: 21 # android min sdk min:16, default 21
|
||||
adaptive_icon_background: "#131636"
|
||||
adaptive_icon_foreground: "assets/icon/icon.png"
|
||||
# adaptive_icon_foreground_inset: 16
|
||||
# adaptive_icon_monochrome: "assets/icon/monochrome.png"
|
||||
|
||||
ios: false
|
||||
# image_path_ios: "assets/icon/icon.png"
|
||||
remove_alpha_ios: true
|
||||
# image_path_ios_dark_transparent: "assets/icon/icon_dark.png"
|
||||
# image_path_ios_tinted_grayscale: "assets/icon/icon_tinted.png"
|
||||
# desaturate_tinted_to_grayscale_ios: true
|
||||
# background_color_ios: "#ffffff"
|
||||
|
||||
web:
|
||||
generate: true
|
||||
image_path: "assets/icon/icon.png"
|
||||
background_color: "#50a7fa"
|
||||
theme_color: "#50a7fa"
|
||||
|
||||
windows:
|
||||
generate: true
|
||||
image_path: "assets/icon/icon.png"
|
||||
icon_size: 48 # min:48, max:256, default: 48
|
||||
|
||||
macos:
|
||||
generate: false
|
||||
image_path: "path/to/image.png"
|
||||
204
lib/Controller/account_controller.dart
Normal file
@@ -0,0 +1,204 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../Entities/drift_database.dart';
|
||||
import '../Pages/Dialog/dialog_action.dart';
|
||||
import '../Pages/Dialog/dialog_input_field.dart';
|
||||
import '../Pages/Dialog/dialog_type_enum.dart';
|
||||
import '../Pages/Dialog/dynamic_dialog.dart';
|
||||
import '../Repositories/account_repository.dart';
|
||||
|
||||
/// Steuert die Interaktion mit den Accounts
|
||||
class AccountController {
|
||||
/// Gibt die aktuell gültige Instanz der Klasse zurück
|
||||
factory AccountController() => _instance;
|
||||
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
AccountController._internal() {
|
||||
_newAccountDialog = DynamicDialog(
|
||||
title: 'Neues Konto erstellen',
|
||||
icon: Icons.account_balance_wallet,
|
||||
inputFields: [
|
||||
const DialogInputField(id: 'name', label: 'Name', autoFocus: true),
|
||||
],
|
||||
actions: [
|
||||
DialogAction(label: 'Abbruch'),
|
||||
DialogAction(
|
||||
label: 'Speichern',
|
||||
isPrimary: true,
|
||||
onPressed: _saveNewAccount,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
_errorNameEmptyDialog = DynamicDialog(
|
||||
title: 'Fehler!',
|
||||
icon: Icons.error,
|
||||
content: const Text('Der Name des Kontos darf nicht leer sein!'),
|
||||
dialogType: DialogTypeEnum.error,
|
||||
);
|
||||
|
||||
_accountCreatedDialog = DynamicDialog(
|
||||
title: 'Account erstellt!',
|
||||
icon: Icons.check_circle,
|
||||
content: const Text('Das Konto wurde erfolgreich erstellt!'),
|
||||
dialogType: DialogTypeEnum.success,
|
||||
);
|
||||
}
|
||||
|
||||
static final AccountController _instance = AccountController._internal();
|
||||
final AccountRepository _accountRepository = AccountRepository();
|
||||
|
||||
DynamicDialog? _newAccountDialog;
|
||||
DynamicDialog? _errorNameEmptyDialog;
|
||||
DynamicDialog? _accountCreatedDialog;
|
||||
|
||||
final ValueNotifier<Account?> _selected = ValueNotifier<Account?>(null);
|
||||
|
||||
/// Stellt das ausgewählte Konto dar, das angezeigt wird
|
||||
ValueNotifier<Account?> get selected {
|
||||
if (_selected.value == null) {
|
||||
unawaited(
|
||||
updateAccounts().then((_) {
|
||||
_selected.value = accounts.value.firstOrNull;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return _selected;
|
||||
}
|
||||
|
||||
set selected(final Account selected) {
|
||||
_selected.value = selected;
|
||||
}
|
||||
|
||||
final ValueNotifier<List<Account>> _accounts = ValueNotifier<List<Account>>(
|
||||
[],
|
||||
);
|
||||
|
||||
/// Stellt die Liste der Konten dar
|
||||
ValueNotifier<List<Account>> get accounts {
|
||||
if (_accounts.value == []) {
|
||||
unawaited(updateAccounts());
|
||||
}
|
||||
|
||||
return _accounts;
|
||||
}
|
||||
|
||||
set accounts(final List<Account> accounts) {
|
||||
_accounts.value = accounts;
|
||||
}
|
||||
|
||||
/// Gibt die gespeicherten Konten als Liste zurück
|
||||
Future<void> updateAccounts() async {
|
||||
final List<Account> accounts = await _accountRepository.findBy(
|
||||
orderBy: 'nameAsc',
|
||||
);
|
||||
|
||||
_accounts.value = accounts;
|
||||
|
||||
if (_selected.value == null && accounts.firstOrNull != null) {
|
||||
_selected.value = accounts.firstOrNull;
|
||||
}
|
||||
}
|
||||
|
||||
/// Startet den Prozess, um ein neues Konto anzulegen
|
||||
void newAccountHandler() {
|
||||
unawaited(_newAccountDialog?.show());
|
||||
}
|
||||
|
||||
/// Startet den Prozess, um ein Konto umzubenennen
|
||||
Future<void> renameAccountHandler(final int accountId) async {
|
||||
final Account? account = await _accountRepository.find(accountId);
|
||||
|
||||
if (account != null) {
|
||||
final renameAccountDialog = DynamicDialog(
|
||||
title: '${account.name} umbenennen',
|
||||
icon: Icons.edit,
|
||||
inputFields: [
|
||||
DialogInputField(
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
initialValue: account.name,
|
||||
autoFocus: true,
|
||||
),
|
||||
],
|
||||
actions: [
|
||||
DialogAction(label: 'Abbruch'),
|
||||
DialogAction(
|
||||
label: 'Speichern',
|
||||
isPrimary: true,
|
||||
onPressed: _renameAccount,
|
||||
),
|
||||
],
|
||||
hiddenValues: {'account': account},
|
||||
);
|
||||
unawaited(renameAccountDialog.show());
|
||||
}
|
||||
}
|
||||
|
||||
/// Startet den Prozess, um ein Konto zu löschen
|
||||
Future<void> deleteAccountHandler(final int accountId) async {
|
||||
final Account? account = await _accountRepository.find(accountId);
|
||||
|
||||
if (account != null) {
|
||||
final deleteAccountDialog = DynamicDialog(
|
||||
dialogType: DialogTypeEnum.error,
|
||||
title: '${account.name} löschen',
|
||||
content: Text('Willst du ${account.name} wirklich löschen?'),
|
||||
icon: Icons.delete_forever,
|
||||
actions: [
|
||||
DialogAction(label: 'Abbruch', isPrimary: true),
|
||||
DialogAction(label: 'Löschen', onPressed: _deleteAccount),
|
||||
],
|
||||
hiddenValues: {'account': account},
|
||||
);
|
||||
unawaited(deleteAccountDialog.show());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveNewAccount(final Map<String, dynamic> values) async {
|
||||
if (values['name'] == null || values['name'] == '') {
|
||||
await _errorNameEmptyDialog?.show();
|
||||
} else {
|
||||
final account = AccountsCompanion(name: Value(values['name']!));
|
||||
|
||||
await _accountRepository.add(account);
|
||||
await _accountCreatedDialog?.show();
|
||||
|
||||
await updateAccounts();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _renameAccount(final Map<String, dynamic> values) async {
|
||||
if (values['account'] != null && values['name'] != null) {
|
||||
final Account account = values['account'];
|
||||
final acc = AccountsCompanion(
|
||||
id: Value(account.id),
|
||||
name: Value(values['name']),
|
||||
);
|
||||
|
||||
await _accountRepository.update(acc);
|
||||
await updateAccounts();
|
||||
|
||||
if (account == selected.value) {
|
||||
selected.value = await _accountRepository.find(account.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteAccount(final Map<String, dynamic> values) async {
|
||||
if (values['account'] != null) {
|
||||
final Account account = values['account'];
|
||||
await _accountRepository.remove(account);
|
||||
|
||||
await updateAccounts();
|
||||
|
||||
if (account == _selected.value) {
|
||||
_selected.value = _accounts.value.firstOrNull;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
lib/Controller/background_task_controller.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:isolate_manager/isolate_manager.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
|
||||
import '../Tasks/BackgroundHandler/workers.dart';
|
||||
import '../Tasks/BackgroundHandler/workmanager_workers.dart';
|
||||
|
||||
/// Erstellt Hintergrundtasks und führt diese aus
|
||||
class BackgroundTaskController {
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
BackgroundTaskController() {
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
unawaited(Workmanager().initialize(callbackDispatcher));
|
||||
unawaited(
|
||||
Workmanager().registerPeriodicTask(
|
||||
'generate-transactions',
|
||||
'generate_transactions',
|
||||
frequency: const Duration(minutes: 30),
|
||||
initialDelay: const Duration(minutes: 1),
|
||||
),
|
||||
);
|
||||
unawaited(
|
||||
Workmanager().registerPeriodicTask(
|
||||
'show-notifications',
|
||||
'show_notifications',
|
||||
frequency: const Duration(minutes: 120),
|
||||
initialDelay: const Duration(minutes: 5),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
unawaited(
|
||||
IsolateManager.runFunction(runTask, {
|
||||
'taskName': 'generate_transactions',
|
||||
'initialDelayMinutes': 1,
|
||||
'frequencyMinutes': 30,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!kIsWeb) {
|
||||
unawaited(
|
||||
IsolateManager.runFunction(runTask, {
|
||||
'taskName': 'show_notifications',
|
||||
'initialDelayMinutes': 5,
|
||||
'frequencyMinutes': 120,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Führt den angegebenen Task sofort aus.
|
||||
static Future<void> run(final String taskName) async {
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
final String uuid = const Uuid().v4();
|
||||
unawaited(Workmanager().registerOneOffTask('$taskName-$uuid', taskName));
|
||||
} else {
|
||||
unawaited(IsolateManager.runFunction(runTask, {
|
||||
'taskName': taskName,
|
||||
'initialDelayMinutes': 0,
|
||||
'frequencyMinutes': 0,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
204
lib/Controller/local_notifications.dart
Normal file
@@ -0,0 +1,204 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
import '../Entities/drift_database.dart';
|
||||
import '../Repositories/transaction_repository.dart';
|
||||
import '../Services/date_service.dart';
|
||||
import '../Services/initializer.dart';
|
||||
import '../Services/transaction_service.dart';
|
||||
import 'port_controller.dart';
|
||||
import 'transaction_controller.dart';
|
||||
|
||||
/// Kümmert sich um die Verwendung von Benachrichtigungen auf verschiedenen
|
||||
/// Plattformen
|
||||
class LocalNotifications {
|
||||
/// Gibt die aktuell gültige Instanz dieser Klasse zurück
|
||||
factory LocalNotifications() => _instance;
|
||||
|
||||
LocalNotifications._internal() {
|
||||
unawaited(_initialise());
|
||||
}
|
||||
|
||||
static final _instance = LocalNotifications._internal();
|
||||
|
||||
final FlutterLocalNotificationsPlugin _localNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
final Initializer _initializer = Initializer();
|
||||
|
||||
Future<void> _initialise() async {
|
||||
if (_initializer.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _localNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin
|
||||
>()
|
||||
?.requestNotificationsPermission();
|
||||
|
||||
const AndroidInitializationSettings androidInitializationSettings =
|
||||
AndroidInitializationSettings('ic_launcher_foreground');
|
||||
const LinuxInitializationSettings linuxInitializationSettings =
|
||||
LinuxInitializationSettings(defaultActionName: 'Öffnen');
|
||||
|
||||
const InitializationSettings initializationSettings =
|
||||
InitializationSettings(
|
||||
android: androidInitializationSettings,
|
||||
linux: linuxInitializationSettings,
|
||||
);
|
||||
|
||||
await _localNotificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
onDidReceiveNotificationResponse: _onDidReceiveNotificationResponse,
|
||||
onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
|
||||
);
|
||||
|
||||
if (!kIsWeb && !Platform.isLinux) {
|
||||
final NotificationAppLaunchDetails? notificationAppLaunchDetails =
|
||||
await _localNotificationsPlugin.getNotificationAppLaunchDetails();
|
||||
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
|
||||
_onDidReceiveNotificationResponse(
|
||||
NotificationResponse(
|
||||
notificationResponseType:
|
||||
NotificationResponseType.selectedNotification,
|
||||
payload:
|
||||
notificationAppLaunchDetails!.notificationResponse?.payload,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_initializer.setInitialized();
|
||||
}
|
||||
|
||||
/// Zeigt eine Benachrichtigung an, die einen dazu auffordert,
|
||||
/// automatisch generierte Transaktionen zu überprüfen
|
||||
Future<void> showTransactionsToCheckNotification(
|
||||
final List<Transaction> transactions,
|
||||
) async {
|
||||
await _initializer.waitUntilInitialized();
|
||||
|
||||
const AndroidNotificationDetails androidNotificationDetails =
|
||||
AndroidNotificationDetails(
|
||||
'transactions_to_check',
|
||||
'Transaktionen zu prüfen',
|
||||
channelDescription:
|
||||
'Zeigt an, dass es zu prüfende Transaktionen gibt',
|
||||
actions: <AndroidNotificationAction>[
|
||||
AndroidNotificationAction('mark_checked', 'Als geprüft markieren'),
|
||||
AndroidNotificationAction(
|
||||
'show_transactions',
|
||||
'Anzeigen',
|
||||
showsUserInterface: true,
|
||||
),
|
||||
// TODO: Prüfen, ob die App geöffnet wird
|
||||
],
|
||||
);
|
||||
const LinuxNotificationDetails linuxNotificationDetails =
|
||||
LinuxNotificationDetails(
|
||||
actions: [
|
||||
LinuxNotificationAction(
|
||||
key: 'mark_checked',
|
||||
label: 'Als geprüft markieren',
|
||||
),
|
||||
LinuxNotificationAction(
|
||||
key: 'show_transactions',
|
||||
label: 'Anzeigen',
|
||||
),
|
||||
],
|
||||
);
|
||||
const NotificationDetails notificationDetails = NotificationDetails(
|
||||
android: androidNotificationDetails,
|
||||
linux: linuxNotificationDetails,
|
||||
);
|
||||
|
||||
final String title;
|
||||
final String body;
|
||||
|
||||
if (transactions.length == 1) {
|
||||
title = 'Transaktion prüfen';
|
||||
body =
|
||||
'Es wurde eine neue Transaktion anhand einer '
|
||||
'wiederkehrenden Transaktion erstellt:\n'
|
||||
'${transactions[0].name} - '
|
||||
'${DateService.dateFormat.format(transactions[0].date!)} - '
|
||||
'${transactions[0].amount} €\n\n'
|
||||
'Diese muss überprüft werden!';
|
||||
} else {
|
||||
int counter = 0;
|
||||
final List<String> transactionsToShow = [];
|
||||
|
||||
for (final Transaction transaction in transactions) {
|
||||
if (counter >= 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
transactionsToShow.add(
|
||||
'${transaction.name} - '
|
||||
'${DateService.dateFormat.format(transaction.date!)} - '
|
||||
'${transaction.amount}',
|
||||
);
|
||||
counter++;
|
||||
}
|
||||
|
||||
title = 'Transaktionen prüfen';
|
||||
body =
|
||||
'Es wurden neue Transaktionen anhand '
|
||||
'wiederkehrender Transaktionen erstellt:\n'
|
||||
'${transactionsToShow.join('\n')}\n\n'
|
||||
'Diese müssen überprüft werden!';
|
||||
}
|
||||
|
||||
await _localNotificationsPlugin.show(
|
||||
0,
|
||||
title,
|
||||
body,
|
||||
notificationDetails,
|
||||
payload: TransactionService.transactionsToString(transactions),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDidReceiveNotificationResponse(
|
||||
final NotificationResponse notificationResponse,
|
||||
) {
|
||||
if (notificationResponse.actionId == 'mark_checked') {
|
||||
TransactionRepository().markTransactionsAsChecked(
|
||||
TransactionService.transactionsFromString(notificationResponse.payload),
|
||||
);
|
||||
|
||||
if (kIsWeb) {
|
||||
unawaited(TransactionController().updateTransactions());
|
||||
} else {
|
||||
PortController().getPort('update-transactions')?.send('ready');
|
||||
}
|
||||
} else {
|
||||
if (kIsWeb) {
|
||||
TransactionController().goToTransactions(
|
||||
transactions: TransactionService.transactionsFromString(
|
||||
notificationResponse.payload,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
PortController()
|
||||
.getPort('go-to-transactions')
|
||||
?.send(notificationResponse.payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wird von den LocalNotifications aufgerufen,
|
||||
/// wenn eine Aktion im Hintergrund abgehandelt werden soll
|
||||
@pragma('vm:entry-point')
|
||||
static void notificationTapBackground(
|
||||
final NotificationResponse notificationResponse,
|
||||
) {
|
||||
if (notificationResponse.actionId == 'mark_checked') {
|
||||
TransactionRepository().markTransactionsAsChecked(
|
||||
TransactionService.transactionsFromString(notificationResponse.payload),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
81
lib/Controller/port_controller.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
/// Ein PortController mit verschiedenen Funktionen zur Kommunikation zwischen
|
||||
/// main und anderen Isolates
|
||||
class PortController {
|
||||
/// Gibt eine Instanz dieser Klasse zurück
|
||||
factory PortController() => _instance;
|
||||
|
||||
PortController._internal() {
|
||||
if (!kIsWeb && ServicesBinding.rootIsolateToken != null) {
|
||||
_registerRootIsolateTokenSender();
|
||||
}
|
||||
}
|
||||
|
||||
static final PortController _instance = PortController._internal();
|
||||
final Logger _logger = Logger();
|
||||
|
||||
/// Fügt einen Port mit [name] zum NameServer hinzu
|
||||
void addPort(final SendPort sendPort, final String name) {
|
||||
IsolateNameServer.removePortNameMapping(name);
|
||||
IsolateNameServer.registerPortWithName(sendPort, name);
|
||||
}
|
||||
|
||||
/// Gibt einen Port mit [name] vom NameServer zurück, falls gefunden
|
||||
SendPort? getPort(final String name) =>
|
||||
IsolateNameServer.lookupPortByName(name);
|
||||
|
||||
/// Gibt das [RootIsolateToken] der main-Isolate zurück
|
||||
Future<RootIsolateToken?> getRootIsolateToken() async {
|
||||
_logger.d('Trying to retrieve RootIsolateToken...');
|
||||
|
||||
final ReceivePort receivePort = ReceivePort();
|
||||
|
||||
final SendPort? rootPort = IsolateNameServer.lookupPortByName(
|
||||
'root-isolate-token',
|
||||
);
|
||||
|
||||
if (rootPort == null) {
|
||||
_logger.e("Couldn't get Port from IsolateNameServer!");
|
||||
receivePort.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.i('Sending communication attempt...');
|
||||
rootPort.send(receivePort.sendPort);
|
||||
|
||||
try {
|
||||
final dynamic message = await receivePort.first;
|
||||
|
||||
if (message is RootIsolateToken) {
|
||||
_logger.i('Got RootIsolateToken, returning...');
|
||||
return message;
|
||||
}
|
||||
|
||||
_logger.w("Couldn't get RootIsolateToken!");
|
||||
return null;
|
||||
} finally {
|
||||
_logger.i('Closing receivePort...');
|
||||
receivePort.close();
|
||||
}
|
||||
}
|
||||
|
||||
void _registerRootIsolateTokenSender() {
|
||||
final ReceivePort receivePort = ReceivePort()
|
||||
..listen((final value) {
|
||||
_logger.d('Received Message with $value');
|
||||
|
||||
if (value is SendPort) {
|
||||
value.send(ServicesBinding.rootIsolateToken);
|
||||
}
|
||||
});
|
||||
|
||||
addPort(receivePort.sendPort, 'root-isolate-token');
|
||||
}
|
||||
}
|
||||
313
lib/Controller/recurring_transaction_controller.dart
Normal file
@@ -0,0 +1,313 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../Entities/drift_database.dart';
|
||||
import '../Entities/time_frame_enum.dart';
|
||||
import '../Pages/Dialog/dialog_action.dart';
|
||||
import '../Pages/Dialog/dialog_input_field.dart';
|
||||
import '../Pages/Dialog/dialog_input_field_select_item.dart';
|
||||
import '../Pages/Dialog/dialog_input_field_type_enum.dart';
|
||||
import '../Pages/Dialog/dialog_type_enum.dart';
|
||||
import '../Pages/Dialog/dynamic_dialog.dart';
|
||||
import '../Repositories/recurring_transacation_repository.dart';
|
||||
import 'account_controller.dart';
|
||||
import 'background_task_controller.dart';
|
||||
|
||||
/// Steuert die Interaktion mit den wiederkehrenden Transaktionen
|
||||
class RecurringTransactionController {
|
||||
/// Gibt die aktuell gültige Instanz der Klasse zurück
|
||||
factory RecurringTransactionController() => _instance;
|
||||
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
RecurringTransactionController._internal() {
|
||||
_newRecurringTransactionDialog = DynamicDialog(
|
||||
title: 'Neue wiederkehrende Transaktion erstellen',
|
||||
icon: Icons.repeat,
|
||||
inputFields: [
|
||||
const DialogInputField(id: 'name', label: 'Name', autoFocus: true),
|
||||
const DialogInputField(
|
||||
id: 'startDate',
|
||||
label: 'Anfangsdatum',
|
||||
keyboardType: TextInputType.datetime,
|
||||
inputType: DialogInputFieldTypeEnum.date,
|
||||
),
|
||||
DialogInputField(
|
||||
id: 'timeFrame',
|
||||
label: 'Zeitraum',
|
||||
inputType: DialogInputFieldTypeEnum.select,
|
||||
selectItems: TimeFrameEnum.values
|
||||
.map(
|
||||
(final value) => DialogInputFieldSelectItem(
|
||||
id: value.index,
|
||||
value: value.name,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
const DialogInputField(
|
||||
id: 'amount',
|
||||
label: 'Betrag €',
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
],
|
||||
actions: [
|
||||
DialogAction(label: 'Abbruch'),
|
||||
DialogAction(
|
||||
label: 'Speichern',
|
||||
isPrimary: true,
|
||||
onPressed: _saveNewRecurringTransaction,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
_errorRecurringTransactionValueEmptyDialog = DynamicDialog(
|
||||
title: 'Fehler!',
|
||||
icon: Icons.error,
|
||||
content: const Text('Es wurden nicht alle Werte eingetragen!'),
|
||||
dialogType: DialogTypeEnum.error,
|
||||
);
|
||||
|
||||
_recurringTransactionCreatedDialog = DynamicDialog(
|
||||
title: 'Wiederkehrende Transaktion erstellt!',
|
||||
icon: Icons.check_circle,
|
||||
content: const Text(
|
||||
'Die wiederkehrende Transaktion wurde erfolgreich erstellt!',
|
||||
),
|
||||
dialogType: DialogTypeEnum.success,
|
||||
);
|
||||
|
||||
_selectedAccount = _accountController.selected.value;
|
||||
_accountController.selected.addListener(() {
|
||||
_selectedAccount = _accountController.selected.value;
|
||||
unawaited(updateRecurringTransactions());
|
||||
});
|
||||
|
||||
unawaited(updateRecurringTransactions());
|
||||
}
|
||||
|
||||
static final RecurringTransactionController _instance =
|
||||
RecurringTransactionController._internal();
|
||||
final RecurringTransactionRepository _recurringTransactionRepository =
|
||||
RecurringTransactionRepository();
|
||||
final AccountController _accountController = AccountController();
|
||||
|
||||
DynamicDialog? _newRecurringTransactionDialog;
|
||||
DynamicDialog? _errorRecurringTransactionValueEmptyDialog;
|
||||
DynamicDialog? _recurringTransactionCreatedDialog;
|
||||
|
||||
final ValueNotifier<List<RecurringTransaction>> _recurringTransactions =
|
||||
ValueNotifier<List<RecurringTransaction>>([]);
|
||||
|
||||
/// Stellt die Liste der wiederkehrenden Transaktionen dar
|
||||
ValueNotifier<List<RecurringTransaction>> get recurringTransactions {
|
||||
if (_recurringTransactions.value == []) {
|
||||
unawaited(updateRecurringTransactions());
|
||||
}
|
||||
|
||||
return _recurringTransactions;
|
||||
}
|
||||
|
||||
set recurringTransactions(
|
||||
final List<RecurringTransaction> recurringTransactions,
|
||||
) {
|
||||
_recurringTransactions.value = recurringTransactions;
|
||||
}
|
||||
|
||||
Account? _selectedAccount;
|
||||
|
||||
/// Aktualisiert die gespeicherten wiederkehrenden Transaktionen
|
||||
/// in der internen Liste.
|
||||
Future<void> updateRecurringTransactions() async {
|
||||
if (_selectedAccount != null) {
|
||||
final List<RecurringTransaction> recurringTransactions =
|
||||
await _recurringTransactionRepository.findBy(
|
||||
account: _selectedAccount,
|
||||
orderBy: 'nameAsc',
|
||||
);
|
||||
|
||||
_recurringTransactions.value = recurringTransactions;
|
||||
}
|
||||
}
|
||||
|
||||
/// Startet den Prozess, um eine neue wiederkehrende Transaktion anzulegen
|
||||
void newRecurringTransactionHandler() {
|
||||
unawaited(_newRecurringTransactionDialog?.show());
|
||||
}
|
||||
|
||||
/// Startet den Prozess, um eine wiederkehrende Transaktion zu bearbeiten
|
||||
Future<void> editRecurringTransactionHandler(
|
||||
final int recurringTransactionId,
|
||||
) async {
|
||||
final RecurringTransaction? recurringTransaction =
|
||||
await _recurringTransactionRepository.find(recurringTransactionId);
|
||||
|
||||
if (recurringTransaction != null) {
|
||||
final editRecurringTransactionDialog = DynamicDialog(
|
||||
title: '${recurringTransaction.name} bearbeiten',
|
||||
icon: Icons.edit,
|
||||
inputFields: [
|
||||
DialogInputField(
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
autoFocus: true,
|
||||
initialValue: recurringTransaction.name,
|
||||
),
|
||||
DialogInputField(
|
||||
id: 'startDate',
|
||||
label: 'Anfangsdatum',
|
||||
keyboardType: TextInputType.datetime,
|
||||
inputType: DialogInputFieldTypeEnum.date,
|
||||
initialValue: recurringTransaction.startDate,
|
||||
),
|
||||
DialogInputField(
|
||||
id: 'timeFrame',
|
||||
label: 'Zeitraum',
|
||||
inputType: DialogInputFieldTypeEnum.select,
|
||||
selectItems: TimeFrameEnum.values
|
||||
.map(
|
||||
(final value) => DialogInputFieldSelectItem(
|
||||
id: value.index,
|
||||
value: value.name,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
initialValue: recurringTransaction.timeFrame,
|
||||
),
|
||||
DialogInputField(
|
||||
id: 'amount',
|
||||
label: 'Betrag €',
|
||||
keyboardType: TextInputType.number,
|
||||
initialValue: recurringTransaction.amount.toString(),
|
||||
),
|
||||
],
|
||||
actions: [
|
||||
DialogAction(label: 'Abbruch'),
|
||||
DialogAction(
|
||||
label: 'Bearbeiten',
|
||||
isPrimary: true,
|
||||
onPressed: _editRecurringTransaction,
|
||||
),
|
||||
],
|
||||
hiddenValues: {'recurringTransaction': recurringTransaction},
|
||||
);
|
||||
unawaited(editRecurringTransactionDialog.show());
|
||||
}
|
||||
}
|
||||
|
||||
/// Startet den Prozess, um eine wiederkehrende Transaktion zu löschen
|
||||
Future<void> deleteRecurringTransactionHandler(
|
||||
final int recurringTransactionId,
|
||||
) async {
|
||||
final RecurringTransaction? recurringTransaction =
|
||||
await _recurringTransactionRepository.find(recurringTransactionId);
|
||||
|
||||
if (recurringTransaction != null) {
|
||||
final deleteRecurringTransactionDialog = DynamicDialog(
|
||||
dialogType: DialogTypeEnum.error,
|
||||
title: '${recurringTransaction.name} löschen',
|
||||
content: Text(
|
||||
'Willst du ${recurringTransaction.name} wirklich löschen?',
|
||||
),
|
||||
icon: Icons.delete_forever,
|
||||
actions: [
|
||||
DialogAction(label: 'Abbruch', isPrimary: true),
|
||||
DialogAction(
|
||||
label: 'Löschen',
|
||||
onPressed: _deleteRecurringTransaction,
|
||||
),
|
||||
],
|
||||
hiddenValues: {'recurringTransaction': recurringTransaction},
|
||||
);
|
||||
unawaited(deleteRecurringTransactionDialog.show());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveNewRecurringTransaction(
|
||||
final Map<String, dynamic> values,
|
||||
) async {
|
||||
if (values['name'] == null ||
|
||||
values['name'] == '' ||
|
||||
values['startDate'] == null ||
|
||||
values['startDate'] == '' ||
|
||||
values['timeFrame'] == null ||
|
||||
values['timeFrame'] == '' ||
|
||||
values['amount'] == null ||
|
||||
values['amount'] == '' ||
|
||||
_selectedAccount == null) {
|
||||
await _errorRecurringTransactionValueEmptyDialog?.show();
|
||||
} else {
|
||||
final DialogInputFieldSelectItem timeFrame = values['timeFrame'];
|
||||
values['timeFrame'] = TimeFrameEnum.values[timeFrame.id];
|
||||
|
||||
final String amount = values['amount'];
|
||||
values['amount'] = double.tryParse(amount);
|
||||
|
||||
if (values['amount'] == null || values['amount'] == 0) {
|
||||
await _errorRecurringTransactionValueEmptyDialog?.show();
|
||||
} else {
|
||||
final recurringTransaction = RecurringTransactionsCompanion(
|
||||
name: Value(values['name']),
|
||||
startDate: Value(values['startDate']),
|
||||
timeFrame: Value(values['timeFrame']),
|
||||
amount: Value(values['amount']),
|
||||
accountId: Value(_selectedAccount!.id),
|
||||
);
|
||||
|
||||
await _recurringTransactionRepository.add(recurringTransaction);
|
||||
await _recurringTransactionCreatedDialog?.show();
|
||||
|
||||
await updateRecurringTransactions();
|
||||
unawaited(BackgroundTaskController.run('generate_transactions'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _editRecurringTransaction(
|
||||
final Map<String, dynamic> values,
|
||||
) async {
|
||||
if (values['recurringTransaction'] != null &&
|
||||
values['recurringTransaction'] != null &&
|
||||
values['startDate'] != null &&
|
||||
values['startDate'] != '' &&
|
||||
values['timeFrame'] != null &&
|
||||
values['timeFrame'] != '' &&
|
||||
values['amount'] != null &&
|
||||
values['amount'] != '') {
|
||||
final DialogInputFieldSelectItem timeFrame = values['timeFrame'];
|
||||
values['timeFrame'] = TimeFrameEnum.values[timeFrame.id];
|
||||
|
||||
final String amount = values['amount'];
|
||||
values['amount'] = double.tryParse(amount);
|
||||
|
||||
if (values['amount'] != null && values['amount'] != 0) {
|
||||
final RecurringTransaction recurringTransaction =
|
||||
values['recurringTransaction'];
|
||||
final rtc = RecurringTransactionsCompanion(
|
||||
id: Value(recurringTransaction.id),
|
||||
name: Value(values['name']),
|
||||
startDate: Value(values['startDate']),
|
||||
timeFrame: Value(values['timeFrame']),
|
||||
amount: Value(values['amount']),
|
||||
accountId: Value(recurringTransaction.accountId),
|
||||
);
|
||||
|
||||
await _recurringTransactionRepository.update(rtc);
|
||||
await updateRecurringTransactions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteRecurringTransaction(
|
||||
final Map<String, dynamic> values,
|
||||
) async {
|
||||
if (values['recurringTransaction'] != null) {
|
||||
final RecurringTransaction recurringTransaction =
|
||||
values['recurringTransaction'];
|
||||
await _recurringTransactionRepository.remove(recurringTransaction);
|
||||
|
||||
await updateRecurringTransactions();
|
||||
}
|
||||
}
|
||||
}
|
||||
312
lib/Controller/transaction_controller.dart
Normal file
@@ -0,0 +1,312 @@
|
||||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:routemaster/routemaster.dart';
|
||||
|
||||
import '../Entities/drift_database.dart';
|
||||
import '../Pages/Dialog/dialog_action.dart';
|
||||
import '../Pages/Dialog/dialog_input_field.dart';
|
||||
import '../Pages/Dialog/dialog_input_field_type_enum.dart';
|
||||
import '../Pages/Dialog/dialog_type_enum.dart';
|
||||
import '../Pages/Dialog/dynamic_dialog.dart';
|
||||
import '../Repositories/transaction_repository.dart';
|
||||
import '../Services/navigation_service.dart';
|
||||
import '../Services/transaction_service.dart';
|
||||
import 'account_controller.dart';
|
||||
import 'port_controller.dart';
|
||||
|
||||
/// Steuert die Interaktion mit den Transaktionen
|
||||
class TransactionController {
|
||||
/// Gibt die aktuell gültige Instanz der Klasse zurück
|
||||
factory TransactionController() => _instance;
|
||||
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
TransactionController._internal() {
|
||||
_newTransactionDialog = DynamicDialog(
|
||||
title: 'Neue Transaktion erstellen',
|
||||
icon: Icons.swap_horiz,
|
||||
inputFields: [
|
||||
const DialogInputField(id: 'name', label: 'Name', autoFocus: true),
|
||||
DialogInputField(
|
||||
id: 'date',
|
||||
label: 'Transaktionsdatum',
|
||||
keyboardType: TextInputType.datetime,
|
||||
inputType: DialogInputFieldTypeEnum.date,
|
||||
initialValue: DateTime.now(),
|
||||
),
|
||||
const DialogInputField(
|
||||
id: 'amount',
|
||||
label: 'Betrag €',
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
],
|
||||
actions: [
|
||||
DialogAction(label: 'Abbruch'),
|
||||
DialogAction(
|
||||
label: 'Speichern',
|
||||
isPrimary: true,
|
||||
onPressed: _saveNewTransaction,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
_errorTransactionValueEmptyDialog = DynamicDialog(
|
||||
title: 'Fehler!',
|
||||
icon: Icons.error,
|
||||
content: const Text('Es wurden nicht alle Werte eingetragen!'),
|
||||
dialogType: DialogTypeEnum.error,
|
||||
);
|
||||
|
||||
_transactionCreatedDialog = DynamicDialog(
|
||||
title: 'Transaktion erstellt!',
|
||||
icon: Icons.check_circle,
|
||||
content: const Text('Die Transaktion wurde erfolgreich erstellt!'),
|
||||
dialogType: DialogTypeEnum.success,
|
||||
);
|
||||
|
||||
_selectedAccount = _accountController.selected.value;
|
||||
_accountController.selected.addListener(() {
|
||||
_selectedAccount = _accountController.selected.value;
|
||||
unawaited(updateTransactions());
|
||||
});
|
||||
|
||||
if (!kIsWeb) {
|
||||
final ReceivePort updateTransactionsReceivePort = ReceivePort()
|
||||
..listen((_) {
|
||||
Logger().i('Received update-transactions signal');
|
||||
unawaited(updateTransactions());
|
||||
});
|
||||
|
||||
PortController().addPort(
|
||||
updateTransactionsReceivePort.sendPort,
|
||||
'update-transactions',
|
||||
);
|
||||
|
||||
final ReceivePort gotToTransactionsReceivePort = ReceivePort()
|
||||
..listen((final value) {
|
||||
Logger().i('Received go-to-transactions signal');
|
||||
|
||||
final List<Transaction> transactions =
|
||||
TransactionService.transactionsFromString(value);
|
||||
|
||||
goToTransactions(transactions: transactions);
|
||||
});
|
||||
|
||||
PortController().addPort(
|
||||
gotToTransactionsReceivePort.sendPort,
|
||||
'go-to-transactions',
|
||||
);
|
||||
}
|
||||
|
||||
unawaited(updateTransactions());
|
||||
}
|
||||
|
||||
static final TransactionController _instance =
|
||||
TransactionController._internal();
|
||||
final TransactionRepository _transactionRepository = TransactionRepository();
|
||||
final AccountController _accountController = AccountController();
|
||||
|
||||
DynamicDialog? _newTransactionDialog;
|
||||
DynamicDialog? _errorTransactionValueEmptyDialog;
|
||||
DynamicDialog? _transactionCreatedDialog;
|
||||
|
||||
final ValueNotifier<List<Transaction>> _transactions =
|
||||
ValueNotifier<List<Transaction>>([]);
|
||||
|
||||
/// Stellt die Liste der Transaktionen dar
|
||||
ValueNotifier<List<Transaction>> get transactions {
|
||||
if (_transactions.value == []) {
|
||||
unawaited(updateTransactions());
|
||||
}
|
||||
|
||||
return _transactions;
|
||||
}
|
||||
|
||||
set transactions(final List<Transaction> transactions) {
|
||||
_transactions.value = transactions;
|
||||
}
|
||||
|
||||
Account? _selectedAccount;
|
||||
|
||||
/// Aktualisiert die Transaktionen in der internen Liste
|
||||
Future<void> updateTransactions() async {
|
||||
if (_selectedAccount != null) {
|
||||
final List<Transaction> transactions = await _transactionRepository
|
||||
.findBy(account: _selectedAccount, orderBy: 'dateDesc');
|
||||
|
||||
_transactions.value = transactions;
|
||||
}
|
||||
}
|
||||
|
||||
/// Wechselt zur Übersicht über die Transaktionen
|
||||
void goToTransactions({final List<Transaction>? transactions}) {
|
||||
final BuildContext? context = NavigationService.getCurrentBuildContext();
|
||||
|
||||
if (context != null && transactions != null) {
|
||||
if (transactions.length == 1) {
|
||||
Routemaster.of(
|
||||
context,
|
||||
).push('/trend', queryParameters: {'name': transactions.first.name});
|
||||
}
|
||||
|
||||
Routemaster.of(context).replace('/trend');
|
||||
}
|
||||
}
|
||||
|
||||
/// Startet den Prozess, um eine neue Transaktion anzulegen
|
||||
void newTransactionHandler() {
|
||||
unawaited(_newTransactionDialog?.show());
|
||||
}
|
||||
|
||||
/// Startet den Prozess, um eine Transaktion zu bearbeiten
|
||||
Future<void> editTransactionHandler(final int transactionId) async {
|
||||
final Transaction? transaction = await _transactionRepository.find(
|
||||
transactionId,
|
||||
);
|
||||
|
||||
if (transaction != null) {
|
||||
final String title;
|
||||
final String submitButtonText;
|
||||
|
||||
if (transaction.checked) {
|
||||
title = '${transaction.name} bearbeiten';
|
||||
submitButtonText = 'Bearbeiten';
|
||||
} else {
|
||||
title = '${transaction.name} prüfen';
|
||||
submitButtonText = 'Prüfen';
|
||||
}
|
||||
|
||||
final editTransactionDialog = DynamicDialog(
|
||||
title: title,
|
||||
icon: Icons.edit,
|
||||
inputFields: [
|
||||
DialogInputField(
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
autoFocus: true,
|
||||
initialValue: transaction.name,
|
||||
),
|
||||
DialogInputField(
|
||||
id: 'date',
|
||||
label: 'Transaktionsdatum',
|
||||
keyboardType: TextInputType.datetime,
|
||||
inputType: DialogInputFieldTypeEnum.date,
|
||||
initialValue: transaction.date,
|
||||
),
|
||||
DialogInputField(
|
||||
id: 'amount',
|
||||
label: 'Betrag €',
|
||||
keyboardType: TextInputType.number,
|
||||
initialValue: transaction.amount.toString(),
|
||||
),
|
||||
],
|
||||
actions: [
|
||||
DialogAction(label: 'Abbruch'),
|
||||
DialogAction(
|
||||
label: submitButtonText,
|
||||
isPrimary: true,
|
||||
onPressed: _editTransaction,
|
||||
),
|
||||
],
|
||||
hiddenValues: {'transaction': transaction},
|
||||
);
|
||||
unawaited(editTransactionDialog.show());
|
||||
}
|
||||
}
|
||||
|
||||
/// Startet den Prozess, um eine Transaktion zu löschen
|
||||
Future<void> deleteTransactionHandler(final int transactionId) async {
|
||||
final Transaction? transaction = await _transactionRepository.find(
|
||||
transactionId,
|
||||
);
|
||||
|
||||
if (transaction != null) {
|
||||
final deleteTransactionDialog = DynamicDialog(
|
||||
dialogType: DialogTypeEnum.error,
|
||||
title: '${transaction.name} löschen',
|
||||
content: Text('Willst du ${transaction.name} wirklich löschen?'),
|
||||
icon: Icons.delete_forever,
|
||||
actions: [
|
||||
DialogAction(label: 'Abbruch', isPrimary: true),
|
||||
DialogAction(
|
||||
label: 'Löschen',
|
||||
onPressed: _deleteTransaction,
|
||||
),
|
||||
],
|
||||
hiddenValues: {'transaction': transaction},
|
||||
);
|
||||
unawaited(deleteTransactionDialog.show());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveNewTransaction(final Map<String, dynamic> values) async {
|
||||
if (values['name'] == null ||
|
||||
values['name'] == '' ||
|
||||
values['date'] == null ||
|
||||
values['date'] == '' ||
|
||||
values['amount'] == null ||
|
||||
values['amount'] == '' ||
|
||||
_selectedAccount == null) {
|
||||
await _errorTransactionValueEmptyDialog?.show();
|
||||
} else {
|
||||
final String amount = values['amount'];
|
||||
values['amount'] = double.tryParse(amount);
|
||||
|
||||
if (values['amount'] == null || values['amount'] == 0) {
|
||||
await _errorTransactionValueEmptyDialog?.show();
|
||||
} else {
|
||||
final transaction = TransactionsCompanion(
|
||||
name: Value(values['name']),
|
||||
date: Value(values['date']),
|
||||
amount: Value(values['amount']),
|
||||
accountId: Value(_selectedAccount!.id),
|
||||
);
|
||||
|
||||
await _transactionRepository.add(transaction);
|
||||
await _transactionCreatedDialog?.show();
|
||||
|
||||
await updateTransactions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _editTransaction(final Map<String, dynamic> values) async {
|
||||
if (values['transaction'] != null &&
|
||||
values['transaction'] != null &&
|
||||
values['date'] != null &&
|
||||
values['date'] != '' &&
|
||||
values['amount'] != null &&
|
||||
values['amount'] != '') {
|
||||
final String amount = values['amount'];
|
||||
values['amount'] = double.tryParse(amount);
|
||||
|
||||
if (values['amount'] != null && values['amount'] != 0) {
|
||||
final Transaction transaction = values['transaction'];
|
||||
final rtc = TransactionsCompanion(
|
||||
id: Value(transaction.id),
|
||||
name: Value(values['name']),
|
||||
date: Value(values['date']),
|
||||
amount: Value(values['amount']),
|
||||
accountId: Value(transaction.accountId),
|
||||
checked: const Value(true),
|
||||
);
|
||||
|
||||
await _transactionRepository.update(rtc);
|
||||
await updateTransactions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteTransaction(final Map<String, dynamic> values) async {
|
||||
if (values['transaction'] != null) {
|
||||
final Transaction transaction = values['transaction'];
|
||||
await _transactionRepository.remove(transaction);
|
||||
|
||||
await updateTransactions();
|
||||
}
|
||||
}
|
||||
}
|
||||
116
lib/Entities/drift_database.dart
Normal file
@@ -0,0 +1,116 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_flutter/drift_flutter.dart';
|
||||
|
||||
import 'sync_log_type_enum.dart';
|
||||
import 'time_frame_enum.dart';
|
||||
|
||||
part 'drift_database.g.dart';
|
||||
|
||||
/// Eine Tabelle für Konten.
|
||||
class Accounts extends Table {
|
||||
/// Eindeutige ID des Kontos
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
|
||||
/// Name des Kontos
|
||||
TextColumn get name => text().withDefault(const Constant(''))();
|
||||
|
||||
/// Der externe Identifier, wenn woanders gespeichert
|
||||
IntColumn get externalIdentifier => integer().nullable()();
|
||||
|
||||
/// Wann das Konto das letzte mal geupdated wurde
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
}
|
||||
|
||||
/// Eine Tabelle für einzelne Transaktionen.
|
||||
class Transactions extends Table {
|
||||
/// Eindeutige ID der Transaktion
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
|
||||
/// Name/Beschreibung der Transaktion
|
||||
TextColumn get name => text().withDefault(const Constant(''))();
|
||||
|
||||
/// Datum der Transaktion
|
||||
DateTimeColumn get date => dateTime().nullable()();
|
||||
|
||||
/// Betrag der Transaktion
|
||||
RealColumn get amount => real().withDefault(const Constant(0))();
|
||||
|
||||
/// Ob diese Transaktion bereits geprüft wurde
|
||||
BoolColumn get checked => boolean().withDefault(const Constant(true))();
|
||||
|
||||
/// Fremdschlüssel zum zugehörigen Konto
|
||||
IntColumn get accountId => integer().references(Accounts, #id)();
|
||||
|
||||
/// Fremdschlüssel zur zugehörigen wiederkehrenden Transaktion,
|
||||
/// falls vorhanden
|
||||
IntColumn get recurringTransactionId =>
|
||||
integer().nullable().references(RecurringTransactions, #id)();
|
||||
|
||||
/// Der externe Identifier, wenn woanders gespeichert
|
||||
IntColumn get externalIdentifier => integer().nullable()();
|
||||
|
||||
/// Wann die Transaktion das letzte mal geupdated wurde
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
}
|
||||
|
||||
/// Eine Tabelle für wiederkehrende Transaktionen.
|
||||
class RecurringTransactions extends Table {
|
||||
/// Eindeutige ID der wiederkehrenden Transaktion
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
|
||||
/// Name/Beschreibung der wiederkehrenden Transaktion
|
||||
TextColumn get name => text().withDefault(const Constant(''))();
|
||||
|
||||
/// Startdatum der wiederkehrenden Transaktion
|
||||
DateTimeColumn get startDate => dateTime().nullable()();
|
||||
|
||||
/// Zeitlicher Rahmen für die Wiederholung
|
||||
IntColumn get timeFrame => intEnum<TimeFrameEnum>()();
|
||||
|
||||
/// Betrag der wiederkehrenden Transaktion
|
||||
RealColumn get amount => real().withDefault(const Constant(0))();
|
||||
|
||||
/// Fremdschlüssel zum zugehörigen Konto
|
||||
IntColumn get accountId => integer().references(Accounts, #id)();
|
||||
|
||||
/// Der externe Identifier, wenn woanders gespeichert
|
||||
IntColumn get externalIdentifier => integer().nullable()();
|
||||
|
||||
/// Wann die wiederkehrende Transaktion das letzte mal geupdated wurde
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
}
|
||||
|
||||
/// Eine Tabelle um den Status der Synchronisation zu loggen
|
||||
class SyncLog extends Table {
|
||||
/// Eindeutige ID des SyncLogs
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
|
||||
/// Der Typ des SyncLogs
|
||||
IntColumn get type => intEnum<SyncLogTypeEnum>()();
|
||||
|
||||
/// Die Beschreibung der Eintragung des SyncLogs
|
||||
TextColumn get description => text().withDefault(const Constant(''))();
|
||||
|
||||
/// Wann dieser SyncLog das letzte mal geupdated wurde
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
}
|
||||
|
||||
/// Hauptklasse für die Drift-Datenbank der Anwendung.
|
||||
@DriftDatabase(tables: [Accounts, Transactions, RecurringTransactions, SyncLog])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
/// Erstellt eine neue Datenbankinstanz
|
||||
AppDatabase() : super(_openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
/// Öffnet die Datenbankverbindung
|
||||
static QueryExecutor _openConnection() => driftDatabase(
|
||||
name: 'dragon_ledger',
|
||||
web: DriftWebOptions(
|
||||
sqlite3Wasm: Uri.parse('sqlite3.wasm'),
|
||||
driftWorker: Uri.parse('drift_worker.js'),
|
||||
),
|
||||
native: const DriftNativeOptions(shareAcrossIsolates: true)
|
||||
);
|
||||
}
|
||||
3324
lib/Entities/drift_database.g.dart
Normal file
11
lib/Entities/list_item.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
/// Ein Listenitem, verwendet um editierbare Listen zu erstellen
|
||||
class ListItem {
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
ListItem({required this.id, required this.name});
|
||||
|
||||
/// Die Id des Eintrags
|
||||
int id;
|
||||
|
||||
/// Der Name des Eintrags
|
||||
String name;
|
||||
}
|
||||
15
lib/Entities/sync_log_type_enum.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
/// Eine Enum um den Typ der Lognachrichten
|
||||
/// für den Synchronisationsstatus festzulegen
|
||||
enum SyncLogTypeEnum {
|
||||
/// Eine Info
|
||||
info,
|
||||
|
||||
/// Ein Fehler
|
||||
error,
|
||||
|
||||
/// Der Start des Prozesses
|
||||
start,
|
||||
|
||||
/// Das Ende des Prozesses
|
||||
end,
|
||||
}
|
||||
14
lib/Entities/time_frame_enum.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
/// Ein Enum, das bestimmte Zeitspannen darstellt
|
||||
enum TimeFrameEnum {
|
||||
/// Für eine tägliche Zeitspanne
|
||||
daily,
|
||||
|
||||
/// Für eine wöchentliche Zeitspanne
|
||||
weekly,
|
||||
|
||||
/// Für eine monatliche Zeitspanne
|
||||
monthly,
|
||||
|
||||
/// Für eine jährliche Zeitspanne
|
||||
yearly,
|
||||
}
|
||||
142
lib/Pages/Dashboard/current_balance.dart
Normal file
@@ -0,0 +1,142 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../Controller/account_controller.dart';
|
||||
import '../../Controller/transaction_controller.dart';
|
||||
import '../../Repositories/transaction_repository.dart';
|
||||
|
||||
/// Gibt eine Übersicht über den aktuellen Kontostand
|
||||
/// und der Veränderung zum Vormonat zurück
|
||||
class CurrentBalance extends StatefulWidget {
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
const CurrentBalance({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _CurrentBalanceState();
|
||||
}
|
||||
|
||||
class _CurrentBalanceState extends State<CurrentBalance> {
|
||||
final TransactionController _transactionController = TransactionController();
|
||||
final AccountController _accountController = AccountController();
|
||||
|
||||
final TransactionRepository _transactionRepository = TransactionRepository();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_transactionController.transactions.addListener(() {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => FutureBuilder(
|
||||
future: _getBalance(),
|
||||
builder:
|
||||
(
|
||||
final BuildContext context,
|
||||
final AsyncSnapshot<(double, double, double)> snapshot,
|
||||
) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
Widget widget;
|
||||
|
||||
if (snapshot.hasData) {
|
||||
final double balanceOfLastMonth = snapshot.data!.$1;
|
||||
final double balanceNow = snapshot.data!.$2;
|
||||
final double balanceOfThisMonth = snapshot.data!.$3;
|
||||
|
||||
final double diff = balanceOfThisMonth - balanceOfLastMonth;
|
||||
|
||||
widget = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text('Aktuell', style: theme.textTheme.bodyMedium),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${balanceNow.toStringAsFixed(2)} €',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Differenz zum Vormonat',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
diff >= 0 ? Icons.arrow_upward : Icons.arrow_downward,
|
||||
color: diff >= 0 ? Colors.green : Colors.red,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${diff.abs().toStringAsFixed(2)} €',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: diff >= 0 ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
widget = Column(
|
||||
children: [
|
||||
Icon(Icons.error, color: theme.colorScheme.error),
|
||||
const Text('Fehler beim holen der letzten Transaktionen!'),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
widget = const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: widget,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Future<(double, double, double)> _getBalance() async {
|
||||
final now = DateTime.now();
|
||||
final dateOfLastMonth = DateTime(now.year, now.month, 0);
|
||||
final dateOfThisMonth = DateTime(now.year, now.month + 1, 0);
|
||||
|
||||
final double balanceOfLastMonth = await _transactionRepository.balance(
|
||||
account: _accountController.selected.value,
|
||||
until: dateOfLastMonth,
|
||||
);
|
||||
final double balanceNow = await _transactionRepository.balance(
|
||||
account: _accountController.selected.value,
|
||||
until: now,
|
||||
);
|
||||
final double balanceOfThisMonth = await _transactionRepository.balance(
|
||||
account: _accountController.selected.value,
|
||||
until: dateOfThisMonth,
|
||||
);
|
||||
|
||||
return (balanceOfLastMonth, balanceNow, balanceOfThisMonth);
|
||||
}
|
||||
}
|
||||
50
lib/Pages/Dashboard/dashboard.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../Misc/monthly_balance_chart.dart';
|
||||
import 'current_balance.dart';
|
||||
import 'recent_transactions_list.dart';
|
||||
|
||||
/// Eine Seite, die das Dashboard der App darstellt.
|
||||
///
|
||||
/// Diese Seite zeigt eine Übersicht über den aktuellen Kontostand,
|
||||
/// die Entwicklung des Kontostands der letzten Monate sowie
|
||||
/// die letzten Transaktionen.
|
||||
class Dashboard extends StatelessWidget {
|
||||
/// Erstellt eine neue Instanz der Dashboard-Seite.
|
||||
const Dashboard({super.key});
|
||||
|
||||
/// Baut das Dashboard-Widget auf.
|
||||
/// [context] ist der Build-Kontext
|
||||
@override
|
||||
Widget build(final BuildContext context) => const Scaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
CurrentBalance(),
|
||||
SizedBox(height: 32),
|
||||
|
||||
Text(
|
||||
'Kontostand pro Monat',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
MonthlyBalanceChart(),
|
||||
|
||||
SizedBox(height: 32),
|
||||
Text(
|
||||
'Letzte Transaktionen',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
RecentTransactionsList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
144
lib/Pages/Dashboard/recent_transactions_list.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../Controller/account_controller.dart';
|
||||
import '../../Controller/transaction_controller.dart';
|
||||
import '../../Entities/drift_database.dart';
|
||||
import '../../Repositories/transaction_repository.dart';
|
||||
|
||||
/// Eine Liste mit den zuletzt getätigten Transaktionen
|
||||
class RecentTransactionsList extends StatefulWidget {
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
const RecentTransactionsList({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _RecentTransactionsListState();
|
||||
}
|
||||
|
||||
class _RecentTransactionsListState extends State<RecentTransactionsList> {
|
||||
final TransactionController _transactionController = TransactionController();
|
||||
final AccountController _accountController = AccountController();
|
||||
|
||||
final TransactionRepository _transactionRepository = TransactionRepository();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_transactionController.transactions.addListener(() {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
final Future<List<Transaction>> recentTransactions = _transactionRepository
|
||||
.findBy(
|
||||
account: _accountController.selected.value,
|
||||
dateTo: DateTime.now(),
|
||||
limit: 5,
|
||||
orderBy: 'dateDesc',
|
||||
);
|
||||
|
||||
return FutureBuilder(
|
||||
future: recentTransactions,
|
||||
builder:
|
||||
(
|
||||
final BuildContext context,
|
||||
final AsyncSnapshot<List<Transaction>> snapshot,
|
||||
) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
if (snapshot.hasData) {
|
||||
final List<Widget>? recentTransactionsWidgetList = snapshot.data
|
||||
?.map(
|
||||
(final Transaction transaction) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
onTap: () {
|
||||
unawaited(
|
||||
_transactionController.editTransactionHandler(
|
||||
transaction.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
title: Text(
|
||||
transaction.name,
|
||||
style: (transaction.checked)
|
||||
? const TextStyle()
|
||||
: TextStyle(color: theme.colorScheme.error),
|
||||
),
|
||||
trailing: Text(
|
||||
'${transaction.amount.abs().toStringAsFixed(2)} €',
|
||||
style: TextStyle(
|
||||
color: transaction.amount == 0
|
||||
? null
|
||||
: (transaction.amount < 0
|
||||
? Colors.green
|
||||
: Colors.red),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (recentTransactionsWidgetList != null &&
|
||||
recentTransactionsWidgetList.isNotEmpty) {
|
||||
recentTransactionsWidgetList.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: TextButton(
|
||||
onPressed: _transactionController.newTransactionHandler,
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.swap_horiz),
|
||||
SizedBox(width: 10),
|
||||
Text('Transaktion hinzufügen'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return Column(children: [...recentTransactionsWidgetList]);
|
||||
} else {
|
||||
if (_accountController.selected.value != null) {
|
||||
return TextButton(
|
||||
onPressed: _transactionController.newTransactionHandler,
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.swap_horiz),
|
||||
SizedBox(width: 10),
|
||||
Text('Transaktion hinzufügen'),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const Text('');
|
||||
}
|
||||
}
|
||||
} else if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.error, color: theme.colorScheme.error),
|
||||
const Text('Fehler beim holen der letzten Transaktionen!'),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
14
lib/Pages/Dialog/dialog_action.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
/// Action/Knopf für den Dialog
|
||||
class DialogAction {
|
||||
/// Erstellt eine neue Aktion für den Dialog
|
||||
DialogAction({required this.label, this.isPrimary = false, this.onPressed});
|
||||
|
||||
/// Das Label der Aktion
|
||||
final String label;
|
||||
|
||||
/// Ob es die primäre Aktion ist
|
||||
final bool isPrimary;
|
||||
|
||||
/// Was bei einem Knopfdruck passieren soll
|
||||
final void Function(Map<String, dynamic> inputValues)? onPressed;
|
||||
}
|
||||
47
lib/Pages/Dialog/dialog_input_field.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'dialog_input_field_select_item.dart';
|
||||
import 'dialog_input_field_type_enum.dart';
|
||||
|
||||
/// Ein Input-Feld für den dynamischen Dialog
|
||||
class DialogInputField {
|
||||
/// Erstellt ein neues Input-Feld
|
||||
const DialogInputField({
|
||||
required this.id,
|
||||
this.label,
|
||||
this.initialValue,
|
||||
this.keyboardType = TextInputType.text,
|
||||
this.obscureText = false,
|
||||
this.autoFocus = false,
|
||||
this.onChanged,
|
||||
this.inputType = DialogInputFieldTypeEnum.text,
|
||||
this.selectItems = const [],
|
||||
});
|
||||
|
||||
/// Die Id des InputFelds
|
||||
final String id;
|
||||
|
||||
/// Der Label des InputFelds
|
||||
final String? label;
|
||||
|
||||
/// Der initiale Wert des InputFelds
|
||||
final dynamic initialValue;
|
||||
|
||||
/// Der InputTyp des InputFeld
|
||||
final TextInputType keyboardType;
|
||||
|
||||
/// Ob der Text obskur angezeigt werden soll
|
||||
final bool obscureText;
|
||||
|
||||
/// Ob das Textfeld automatisch fokussiert werden soll
|
||||
final bool autoFocus;
|
||||
|
||||
/// Was bei Veränderung des Textfeldes geschehen soll
|
||||
final ValueChanged<dynamic>? onChanged;
|
||||
|
||||
/// Stellt den Eingabetypen des Inputfeldes dar
|
||||
final DialogInputFieldTypeEnum inputType;
|
||||
|
||||
/// Die Auswahlmöglichkeiten für den Select-Input-Typen
|
||||
final List<DialogInputFieldSelectItem> selectItems;
|
||||
}
|
||||
11
lib/Pages/Dialog/dialog_input_field_select_item.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
/// Eine Klasse, um den DialogInputFeldern des Select-Typs die Werte zu geben
|
||||
class DialogInputFieldSelectItem {
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
DialogInputFieldSelectItem({required this.id, required this.value});
|
||||
|
||||
/// Die id des Feldes
|
||||
final int id;
|
||||
|
||||
/// Der Wert des Feldes
|
||||
final String value;
|
||||
}
|
||||
13
lib/Pages/Dialog/dialog_input_field_type_enum.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'dialog_input_field.dart';
|
||||
|
||||
/// Eine Enum um [DialogInputField] sagen zu können, welcher Typ er ist
|
||||
enum DialogInputFieldTypeEnum {
|
||||
/// Der Standard Text-Typ
|
||||
text,
|
||||
|
||||
/// Der Datums-Typ
|
||||
date,
|
||||
|
||||
/// Ein Select-Feld
|
||||
select,
|
||||
}
|
||||
11
lib/Pages/Dialog/dialog_type_enum.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
/// Eine Enum, um den Typ eines Dialogs festzulegen
|
||||
enum DialogTypeEnum {
|
||||
/// Der Standarddialog
|
||||
info,
|
||||
|
||||
/// Der Errordialog
|
||||
error,
|
||||
|
||||
/// Der Erfolgreichdialog
|
||||
success,
|
||||
}
|
||||
266
lib/Pages/Dialog/dynamic_dialog.dart
Normal file
@@ -0,0 +1,266 @@
|
||||
import 'package:dropdown_search/dropdown_search.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../Services/navigation_service.dart';
|
||||
import '../../Services/theme_service.dart';
|
||||
import '../Misc/InputFields/dynamic_date_time_field.dart';
|
||||
import 'dialog_action.dart';
|
||||
import 'dialog_input_field.dart';
|
||||
import 'dialog_input_field_select_item.dart';
|
||||
import 'dialog_input_field_type_enum.dart';
|
||||
import 'dialog_type_enum.dart';
|
||||
|
||||
/// Erstellt einen neuen dynamischen Dialog
|
||||
class DynamicDialog {
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
DynamicDialog({
|
||||
this.title,
|
||||
this.icon,
|
||||
this.content,
|
||||
final List<DialogInputField>? inputFields,
|
||||
final List<DialogAction>? actions,
|
||||
this.backgroundColor,
|
||||
this.borderRadius = 16,
|
||||
this.barrierDismissible = true,
|
||||
this.dialogType = DialogTypeEnum.info,
|
||||
this.hiddenValues,
|
||||
}) : inputFields = inputFields ?? const [],
|
||||
actions = actions ?? [DialogAction(label: 'Schließen')],
|
||||
_values = hiddenValues ?? {};
|
||||
|
||||
/// Der Titel des Dialogs
|
||||
final String? title;
|
||||
|
||||
/// Der Icon des Dialogs
|
||||
final IconData? icon;
|
||||
|
||||
/// Der Inhalt des Dialogs
|
||||
final Widget? content;
|
||||
|
||||
/// Die Hintergrundfarbe des Dialogs, Standard wenn nicht gesetzt
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// Der BorderRadius des Dialogs
|
||||
final double borderRadius;
|
||||
|
||||
/// Ob der Dialog bei einem Klick auf die Barriere geschlossen werden kann
|
||||
final bool barrierDismissible;
|
||||
|
||||
/// Die InputFelder des Dialogs
|
||||
final List<DialogInputField> inputFields;
|
||||
|
||||
/// Die Aktionen des Dialogs
|
||||
final List<DialogAction> actions;
|
||||
|
||||
/// Der Typ des Dialogs
|
||||
final DialogTypeEnum dialogType;
|
||||
|
||||
/// Versteckte Werte, die beim Abschicken mit zurückgegeben werden
|
||||
final Map<String, dynamic>? hiddenValues;
|
||||
|
||||
final Map<String, dynamic> _values;
|
||||
|
||||
final Map<String, TextEditingController> _controllers = {};
|
||||
|
||||
BuildContext? _dialogContext;
|
||||
|
||||
/// Zeigt den vorher zusammengebauten Dialog an
|
||||
Future<void> show() async {
|
||||
final BuildContext? context = NavigationService.getCurrentBuildContext();
|
||||
|
||||
if (context != null) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: barrierDismissible,
|
||||
builder: (final BuildContext ctx) {
|
||||
_dialogContext = ctx;
|
||||
final DialogAction primaryAction = actions.firstWhere(
|
||||
(final a) => a.isPrimary,
|
||||
orElse: () => actions.first,
|
||||
);
|
||||
|
||||
Color backgroundColor =
|
||||
this.backgroundColor ?? theme.colorScheme.surface;
|
||||
|
||||
if (dialogType == DialogTypeEnum.error) {
|
||||
backgroundColor = theme.colorScheme.errorContainer;
|
||||
} else if (dialogType == DialogTypeEnum.success) {
|
||||
backgroundColor = ThemeService.getSuccessColor(
|
||||
brightness: theme.brightness,
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: AlertDialog(
|
||||
backgroundColor: backgroundColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
),
|
||||
title: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null)
|
||||
Icon(icon, size: 48, color: theme.colorScheme.primary),
|
||||
if (title != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
title!,
|
||||
style: theme.textTheme.titleLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(ctx).size.height * 0.7,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (content != null) content!,
|
||||
...inputFields.map(
|
||||
(final DialogInputField field) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: _getInputField(field, primaryAction),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: actions.map(_getAction).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Schließt den dynamischen Dialog
|
||||
void close() {
|
||||
if (_dialogContext != null) {
|
||||
Navigator.of(_dialogContext!).pop();
|
||||
}
|
||||
|
||||
_values.clear();
|
||||
_controllers.clear();
|
||||
}
|
||||
|
||||
void _submit(final DialogAction action) {
|
||||
final Map<String, dynamic> values = {}
|
||||
..addEntries(_values.entries);
|
||||
|
||||
_controllers.forEach((
|
||||
final String id,
|
||||
final TextEditingController controller,
|
||||
) {
|
||||
values[id] = controller.text;
|
||||
});
|
||||
|
||||
close();
|
||||
action.onPressed?.call(values);
|
||||
}
|
||||
|
||||
Widget _getInputField(
|
||||
final DialogInputField inputField,
|
||||
final DialogAction primaryAction,
|
||||
) {
|
||||
if (_values[inputField.id] == null) {
|
||||
_values[inputField.id] = inputField.initialValue;
|
||||
}
|
||||
|
||||
if (inputField.inputType == DialogInputFieldTypeEnum.date) {
|
||||
return DynamicDateTimeField(
|
||||
initialValue: _values[inputField.id],
|
||||
autofocus: inputField.autoFocus,
|
||||
onChanged: (final value) {
|
||||
inputField.onChanged?.call(value);
|
||||
_values[inputField.id] = value;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: inputField.label,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
);
|
||||
} else if (inputField.inputType == DialogInputFieldTypeEnum.select) {
|
||||
if (_values[inputField.id] is Enum) {
|
||||
final Enum inputFieldInitialValue = _values[inputField.id];
|
||||
|
||||
for (final DialogInputFieldSelectItem value in inputField.selectItems) {
|
||||
if (value.id == inputFieldInitialValue.index) {
|
||||
_values[inputField.id] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DropdownSearch<DialogInputFieldSelectItem>(
|
||||
items: (final f, final cs) => inputField.selectItems,
|
||||
itemAsString: (final DialogInputFieldSelectItem value) => value.value,
|
||||
selectedItem: _values[inputField.id],
|
||||
onChanged: (final DialogInputFieldSelectItem? value) {
|
||||
inputField.onChanged?.call(value);
|
||||
_values[inputField.id] = value;
|
||||
},
|
||||
decoratorProps: DropDownDecoratorProps(
|
||||
decoration: InputDecoration(
|
||||
labelText: inputField.label,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
compareFn:
|
||||
(
|
||||
final DialogInputFieldSelectItem v1,
|
||||
final DialogInputFieldSelectItem v2,
|
||||
) => v1.id == v2.id,
|
||||
);
|
||||
} else {
|
||||
if (_controllers[inputField.id] == null) {
|
||||
_controllers[inputField.id] = TextEditingController(
|
||||
text: (_values[inputField.id] ?? '').toString()
|
||||
);
|
||||
}
|
||||
|
||||
return TextField(
|
||||
controller: _controllers[inputField.id],
|
||||
autofocus: inputField.autoFocus,
|
||||
keyboardType: inputField.keyboardType,
|
||||
obscureText: inputField.obscureText,
|
||||
onChanged: (final value) {
|
||||
inputField.onChanged?.call(value);
|
||||
_values[inputField.id] = value;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: inputField.label,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
onSubmitted: (_) {
|
||||
_submit(primaryAction);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _getAction(final DialogAction action) {
|
||||
if (action.isPrimary) {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
_submit(action);
|
||||
},
|
||||
child: Text(action.label),
|
||||
);
|
||||
} else {
|
||||
return TextButton(
|
||||
onPressed: () {
|
||||
_submit(action);
|
||||
},
|
||||
child: Text(action.label),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
110
lib/Pages/Misc/InputFields/date_range_picker.dart
Normal file
@@ -0,0 +1,110 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../Services/date_service.dart';
|
||||
import '../../Dialog/dialog_action.dart';
|
||||
import '../../Dialog/dialog_input_field.dart';
|
||||
import '../../Dialog/dialog_input_field_type_enum.dart';
|
||||
import '../../Dialog/dynamic_dialog.dart';
|
||||
|
||||
/// Stellt einen DateRange-Picker als Input-Feld dar
|
||||
class DateRangePicker extends StatefulWidget {
|
||||
/// Initialisiert eine neue Instanz dieser Klasse
|
||||
const DateRangePicker({
|
||||
this.controller,
|
||||
this.autofocus = false,
|
||||
super.key,
|
||||
this.decoration,
|
||||
this.onChanged,
|
||||
});
|
||||
|
||||
/// Ob das Feld automatisch ausgewählt werden soll
|
||||
final bool autofocus;
|
||||
|
||||
/// Die Funktion, die bei Veränderung des Wertes aufgerufen wird
|
||||
final Function(DateTimeRange?)? onChanged;
|
||||
|
||||
/// Die Dekoration, wie das Feld aussehen soll
|
||||
final InputDecoration? decoration;
|
||||
|
||||
/// Der Controller über den der Wert des Inputfeldes gesetzt
|
||||
/// und ausgelesen werden kann
|
||||
final TextEditingController? controller;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DateRangePicker();
|
||||
}
|
||||
|
||||
class _DateRangePicker extends State<DateRangePicker> {
|
||||
final TextEditingController _valueController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_valueController.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => TextField(
|
||||
readOnly: true,
|
||||
controller: widget.controller ?? _valueController,
|
||||
decoration: widget.decoration,
|
||||
onTap: _pickDateRange,
|
||||
);
|
||||
|
||||
Future<void> _pickDateRange() async {
|
||||
final TextEditingController controller =
|
||||
widget.controller ?? _valueController;
|
||||
|
||||
final DateTimeRange<DateTime>? dateTimeRange =
|
||||
DateService.stringToDateTimeRange(controller.text);
|
||||
|
||||
await DynamicDialog(
|
||||
title: 'Zeitraum auswählen',
|
||||
inputFields: [
|
||||
DialogInputField(
|
||||
id: 'dateFrom',
|
||||
label: 'Startdatum',
|
||||
keyboardType: TextInputType.datetime,
|
||||
inputType: DialogInputFieldTypeEnum.date,
|
||||
initialValue: dateTimeRange?.start,
|
||||
),
|
||||
DialogInputField(
|
||||
id: 'dateTo',
|
||||
label: 'Enddatum',
|
||||
keyboardType: TextInputType.datetime,
|
||||
inputType: DialogInputFieldTypeEnum.date,
|
||||
initialValue: dateTimeRange?.end,
|
||||
),
|
||||
],
|
||||
actions: [
|
||||
DialogAction(label: 'Abbrechen'),
|
||||
DialogAction(
|
||||
label: 'Leeren',
|
||||
onPressed: (_) {
|
||||
controller.text = '';
|
||||
widget.onChanged?.call(null);
|
||||
},
|
||||
),
|
||||
DialogAction(
|
||||
label: 'Auswählen',
|
||||
isPrimary: true,
|
||||
onPressed: (final Map<String, dynamic> values) {
|
||||
if (values['dateFrom'] != null &&
|
||||
values['dateFrom'] is DateTime &&
|
||||
values['dateTo'] != null &&
|
||||
values['dateTo'] is DateTime) {
|
||||
final DateTimeRange<DateTime> timeRange = DateTimeRange(
|
||||
start: values['dateFrom'],
|
||||
end: values['dateTo'],
|
||||
);
|
||||
|
||||
controller.text = DateService.dateTimeRangeToString(timeRange)!;
|
||||
widget.onChanged?.call(timeRange);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
).show();
|
||||
}
|
||||
}
|
||||
62
lib/Pages/Misc/InputFields/dynamic_date_time_field.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'package:date_field/date_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../Services/date_service.dart';
|
||||
|
||||
/// Ein Feld mit Popup über welches man Datumsfelder auswählen kann
|
||||
class DynamicDateTimeField extends StatefulWidget {
|
||||
/// Initialisiert eine neue Instanz dieser Klasse
|
||||
const DynamicDateTimeField({
|
||||
this.initialValue,
|
||||
this.mode = DateTimeFieldPickerMode.date,
|
||||
this.autofocus = false,
|
||||
this.onChanged,
|
||||
this.decoration,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// Der Initiale Wert des Input-Feldes
|
||||
final DateTime? initialValue;
|
||||
|
||||
/// Der Modus des Datums-Feldes
|
||||
final DateTimeFieldPickerMode mode;
|
||||
|
||||
/// Ob das Feld automatisch ausgewählt werden soll
|
||||
final bool autofocus;
|
||||
|
||||
/// Die Funktion, die bei Veränderung des Wertes aufgerufen wird
|
||||
final Function(DateTime?)? onChanged;
|
||||
|
||||
/// Die Dekoration, wie das Feld aussehen soll
|
||||
final InputDecoration? decoration;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DynamicDateTimeFieldState();
|
||||
}
|
||||
|
||||
class _DynamicDateTimeFieldState extends State<DynamicDateTimeField> {
|
||||
DateTime? _value;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_value = widget.initialValue;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => DateTimeField(
|
||||
initialPickerDateTime: widget.initialValue,
|
||||
dateFormat: DateService.dateFormat,
|
||||
value: _value,
|
||||
mode: widget.mode,
|
||||
autofocus: widget.autofocus,
|
||||
onChanged: (final value) {
|
||||
widget.onChanged?.call(value);
|
||||
|
||||
setState(() {
|
||||
_value = value;
|
||||
});
|
||||
},
|
||||
decoration: widget.decoration,
|
||||
);
|
||||
}
|
||||
85
lib/Pages/Misc/account_select.dart
Normal file
@@ -0,0 +1,85 @@
|
||||
import 'package:dropdown_search/dropdown_search.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../Controller/account_controller.dart';
|
||||
import '../../Entities/drift_database.dart';
|
||||
|
||||
/// Ein Dropdown, mit welchem man das Konto auswählen kann
|
||||
class AccountSelect extends StatefulWidget {
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
const AccountSelect({super.key});
|
||||
|
||||
@override
|
||||
State<AccountSelect> createState() => _AccountSelectState();
|
||||
}
|
||||
|
||||
class _AccountSelectState extends State<AccountSelect> {
|
||||
final AccountController _accountController = AccountController();
|
||||
Account? _selected;
|
||||
List<Account> _accounts = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_selected = _accountController.selected.value;
|
||||
_accounts = _accountController.accounts.value;
|
||||
|
||||
_accountController.selected.addListener(() {
|
||||
setState(() {
|
||||
if (mounted) {
|
||||
_selected = _accountController.selected.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
_accountController.accounts.addListener(() {
|
||||
setState(() {
|
||||
if (mounted) {
|
||||
_accounts = _accountController.accounts.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
if (_selected != null && _accounts.isNotEmpty) {
|
||||
return DropdownSearch<Account>(
|
||||
items: (final f, final cs) => _accounts,
|
||||
selectedItem: _selected,
|
||||
onChanged: (final Account? account) {
|
||||
if (account != null) {
|
||||
_accountController.selected = account;
|
||||
}
|
||||
},
|
||||
|
||||
itemAsString: (final Account account) => account.name,
|
||||
compareFn: (final Account a1, final Account a2) => a1.id == a2.id,
|
||||
popupProps: const PopupProps<Account>.menu(
|
||||
showSearchBox: true,
|
||||
searchFieldProps: TextFieldProps(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Konto suchen...',
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (_selected == null && _accounts.isEmpty) {
|
||||
return TextButton(
|
||||
onPressed: _accountController.newAccountHandler,
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.account_balance_wallet),
|
||||
SizedBox(width: 10),
|
||||
Text('Konto hinzufügen'),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
}
|
||||
}
|
||||
92
lib/Pages/Misc/editable_list.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../Entities/list_item.dart';
|
||||
|
||||
/// Eine editierbare Liste mit Funktionen zur Bearbeitung
|
||||
class EditableList extends StatelessWidget {
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
const EditableList({
|
||||
required this.name,
|
||||
required this.items,
|
||||
required this.onAdd,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
this.icon,
|
||||
this.addTooltip,
|
||||
this.menuTooltip,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// Der Name, der oben angezeigt wird
|
||||
final String name;
|
||||
|
||||
/// Die
|
||||
final List<ListItem> items;
|
||||
|
||||
/// Die Funktion, die aufgerufen wird,
|
||||
/// wenn ein neuer Eintrag hinzugefügt werden soll
|
||||
final void Function() onAdd;
|
||||
|
||||
/// Die Funktion, die beim umbenennen aufgerufen wird
|
||||
final void Function(int) onEdit;
|
||||
|
||||
///Die Funktion, die beim Löschen aufgerufen wird
|
||||
final void Function(int) onDelete;
|
||||
|
||||
/// Das Icon, das angezeigt wird
|
||||
final Icon? icon;
|
||||
|
||||
/// Der Tooltip, der beim erstellen-Button angezeigt wird
|
||||
final String? addTooltip;
|
||||
|
||||
/// Der Tooltip, der auf dem Menü angezeigt wird
|
||||
final String? menuTooltip;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
name,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: onAdd,
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: addTooltip,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (_, _) => const Divider(),
|
||||
itemBuilder: (final context, final index) => ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(items[index].name),
|
||||
leading: icon,
|
||||
trailing: PopupMenuButton<String>(
|
||||
tooltip: menuTooltip,
|
||||
onSelected: (final value) {
|
||||
if (value == 'edit') {
|
||||
onEdit(items[index].id);
|
||||
} else if (value == 'delete') {
|
||||
onDelete(items[index].id);
|
||||
}
|
||||
},
|
||||
itemBuilder: (_) => const [
|
||||
PopupMenuItem(value: 'edit', child: Text('Bearbeiten')),
|
||||
PopupMenuItem(value: 'delete', child: Text('Entfernen')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
129
lib/Pages/Misc/floating_creation_button.dart
Normal file
@@ -0,0 +1,129 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
||||
|
||||
import '../../Controller/account_controller.dart';
|
||||
import '../../Controller/recurring_transaction_controller.dart';
|
||||
import '../../Controller/transaction_controller.dart';
|
||||
import '../../Services/navigation_service.dart';
|
||||
|
||||
/// Ein Floating Action Button, der beim Klicken ein expandierendes Menü öffnet,
|
||||
/// um neue Transaktionen oder Konten anzulegen.
|
||||
class FloatingCreationButton extends StatefulWidget {
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
const FloatingCreationButton({super.key});
|
||||
|
||||
@override
|
||||
State<FloatingCreationButton> createState() => _FloatingCreationButtonState();
|
||||
}
|
||||
|
||||
class _FloatingCreationButtonState extends State<FloatingCreationButton> {
|
||||
final AccountController _accountController = AccountController();
|
||||
final RecurringTransactionController _recurringTransactionController =
|
||||
RecurringTransactionController();
|
||||
final TransactionController _transactionController = TransactionController();
|
||||
|
||||
final _key = GlobalKey<ExpandableFabState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_accountController.selected.addListener(() {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
final List<Widget> fabs = [];
|
||||
if (_accountController.selected.value != null) {
|
||||
fabs
|
||||
..add(
|
||||
_expandableButton(
|
||||
label: 'Neue Transaktion',
|
||||
icon: Icons.swap_horiz,
|
||||
onPressed: _transactionController.newTransactionHandler,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
_expandableButton(
|
||||
label: 'Neue wiederkehrende Transaktion',
|
||||
icon: Icons.repeat,
|
||||
onPressed:
|
||||
_recurringTransactionController.newRecurringTransactionHandler,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
fabs.add(
|
||||
_expandableButton(
|
||||
label: 'Neues Konto',
|
||||
icon: Icons.account_balance_wallet,
|
||||
onPressed: _accountController.newAccountHandler,
|
||||
),
|
||||
);
|
||||
|
||||
return ExpandableFab(
|
||||
key: _key,
|
||||
openButtonBuilder: RotateFloatingActionButtonBuilder(
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
type: ExpandableFabType.up,
|
||||
childrenAnimation: ExpandableFabAnimation.none,
|
||||
distance: 70,
|
||||
children: fabs,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _expandableButton({
|
||||
required final String label,
|
||||
required final IconData icon,
|
||||
required final VoidCallback onPressed,
|
||||
}) {
|
||||
final ThemeData theme = Theme.of(
|
||||
NavigationService.getCurrentBuildContext()!,
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onPressed,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.onPrimary.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(label),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
FloatingActionButton.small(
|
||||
heroTag: null,
|
||||
onPressed: () {
|
||||
onPressed.call();
|
||||
|
||||
final ExpandableFabState? state = _key.currentState;
|
||||
if (state != null && state.isOpen) {
|
||||
state.close();
|
||||
}
|
||||
},
|
||||
child: Icon(icon),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
175
lib/Pages/Misc/monthly_balance_chart.dart
Normal file
@@ -0,0 +1,175 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../Controller/account_controller.dart';
|
||||
import '../../Controller/transaction_controller.dart';
|
||||
import '../../Repositories/transaction_repository.dart';
|
||||
|
||||
/// Stellt einen Chart des Monats-Kontostands dar
|
||||
class MonthlyBalanceChart extends StatefulWidget {
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
const MonthlyBalanceChart({
|
||||
super.key,
|
||||
this.amountMax,
|
||||
this.amountMin,
|
||||
this.dateFrom,
|
||||
this.dateTo,
|
||||
this.name,
|
||||
});
|
||||
|
||||
/// Der Name der Transaktion, nach der gesucht werden soll
|
||||
final String? name;
|
||||
|
||||
/// Der Mindestbetrag der Transaktion, nach der gesucht werden soll
|
||||
final double? amountMin;
|
||||
|
||||
/// Der Maximalbetrag der Transaktion, nach der gesucht werden soll
|
||||
final double? amountMax;
|
||||
|
||||
/// Das Datum der Transaktionen, ab dem gestartet wurde
|
||||
final DateTime? dateFrom;
|
||||
|
||||
///Das Datum der Transaktionen, bis zu welchen beendet wurde
|
||||
final DateTime? dateTo;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _MonthlyBalanceChart();
|
||||
}
|
||||
|
||||
class _MonthlyBalanceChart extends State<MonthlyBalanceChart> {
|
||||
final TransactionRepository _transactionRepository = TransactionRepository();
|
||||
|
||||
final AccountController _accountController = AccountController();
|
||||
final TransactionController _transactionController = TransactionController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_transactionController.transactions.addListener(() {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => FutureBuilder(
|
||||
future: _transactionRepository.monthlyBalances(
|
||||
account: _accountController.selected.value,
|
||||
name: widget.name,
|
||||
amountMin: widget.amountMin,
|
||||
amountMax: widget.amountMax,
|
||||
dateFrom: widget.dateFrom,
|
||||
dateTo: (widget.dateTo != null) ? widget.dateTo : DateTime.now(),
|
||||
),
|
||||
builder:
|
||||
(
|
||||
final BuildContext context,
|
||||
final AsyncSnapshot<List<Map<String, dynamic>>> snapshot,
|
||||
) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final MediaQueryData mediaQuery = MediaQuery.of(context);
|
||||
|
||||
if (snapshot.hasData) {
|
||||
final List<Map<String, dynamic>> monthlyBalances = snapshot.data!;
|
||||
|
||||
double maxBalance = 0;
|
||||
double minBalance = 0;
|
||||
|
||||
for (final value in monthlyBalances) {
|
||||
if (maxBalance < value['balance']) {
|
||||
maxBalance = value['balance'];
|
||||
}
|
||||
|
||||
if (minBalance > value['balance']) {
|
||||
minBalance = value['balance'];
|
||||
}
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: 180,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
minY: minBalance,
|
||||
maxY: maxBalance,
|
||||
titlesData: FlTitlesData(
|
||||
topTitles: const AxisTitles(),
|
||||
rightTitles: const AxisTitles(),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 28,
|
||||
interval: 1,
|
||||
getTitlesWidget:
|
||||
(final double value, final TitleMeta meta) {
|
||||
final List<String> months = monthlyBalances.map((
|
||||
final value,
|
||||
) {
|
||||
final DateTime date = value['date'];
|
||||
final DateFormat format = DateFormat('MMMM');
|
||||
|
||||
String month = format.format(date);
|
||||
|
||||
if (mediaQuery.size.width < 470) {
|
||||
month = month.substring(0, 1);
|
||||
} else if (mediaQuery.size.width < 920) {
|
||||
month = month.substring(0, 3);
|
||||
}
|
||||
|
||||
return month;
|
||||
}).toList();
|
||||
|
||||
if (value.toInt() >= 0 &&
|
||||
value.toInt() < months.length) {
|
||||
return Text(months[value.toInt()]);
|
||||
}
|
||||
return const Text('');
|
||||
},
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 50,
|
||||
getTitlesWidget:
|
||||
(final double value, final TitleMeta meta) => Text(
|
||||
'${value.toInt()} €',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
lineBarsData: <LineChartBarData>[
|
||||
LineChartBarData(
|
||||
spots: List<FlSpot>.generate(
|
||||
monthlyBalances.length,
|
||||
(final int index) => FlSpot(
|
||||
index.toDouble(),
|
||||
monthlyBalances[index]['balance'],
|
||||
),
|
||||
),
|
||||
isCurved: true,
|
||||
barWidth: 3,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.error, color: theme.colorScheme.error),
|
||||
const Text('Fehler beim holen der Monatsübersicht!'),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
60
lib/Pages/Settings/account_list.dart
Normal file
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../Controller/account_controller.dart';
|
||||
import '../../Entities/drift_database.dart';
|
||||
import '../../Entities/list_item.dart';
|
||||
import '../Misc/editable_list.dart';
|
||||
|
||||
/// Ein Widget, das die Liste mit vorhandenen Konten anzeigt
|
||||
class AccountList extends StatefulWidget {
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
const AccountList({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _AccountListState();
|
||||
}
|
||||
|
||||
class _AccountListState extends State<AccountList> {
|
||||
final AccountController _accountController = AccountController();
|
||||
List<Account> _accounts = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_accounts = _accountController.accounts.value;
|
||||
|
||||
_accountController.accounts.addListener(() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
if (mounted) {
|
||||
_accounts = _accountController.accounts.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
if (_accounts != []) {
|
||||
final List<ListItem> formatedAccounts = [];
|
||||
for (final Account data in _accounts) {
|
||||
formatedAccounts.add(ListItem(id: data.id, name: data.name));
|
||||
}
|
||||
|
||||
return EditableList(
|
||||
name: 'Konten',
|
||||
items: formatedAccounts,
|
||||
onAdd: _accountController.newAccountHandler,
|
||||
onEdit: _accountController.renameAccountHandler,
|
||||
onDelete: _accountController.deleteAccountHandler,
|
||||
icon: const Icon(Icons.account_balance_wallet),
|
||||
addTooltip: 'Konto hinzufügen',
|
||||
menuTooltip: 'Menü anzeigen',
|
||||
);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
}
|
||||
}
|
||||
67
lib/Pages/Settings/recurring_transaction_list.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../Controller/recurring_transaction_controller.dart';
|
||||
import '../../Entities/drift_database.dart';
|
||||
import '../../Entities/list_item.dart';
|
||||
import '../Misc/editable_list.dart';
|
||||
|
||||
/// Ein Widget,
|
||||
/// das die Liste mit vorhandenen wiederkehrenden Transaktionen anzeigt
|
||||
class RecurringTransactionList extends StatefulWidget {
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
const RecurringTransactionList({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _RecurringTransactionListState();
|
||||
}
|
||||
|
||||
class _RecurringTransactionListState extends State<RecurringTransactionList> {
|
||||
final RecurringTransactionController _recurringTransactionController =
|
||||
RecurringTransactionController();
|
||||
List<RecurringTransaction> _recurringTransactions = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_recurringTransactions =
|
||||
_recurringTransactionController.recurringTransactions.value;
|
||||
|
||||
_recurringTransactionController.recurringTransactions.addListener(() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
if (mounted) {
|
||||
_recurringTransactions =
|
||||
_recurringTransactionController.recurringTransactions.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
if (_recurringTransactions != []) {
|
||||
final List<ListItem> formatedRecurringTransactions = [];
|
||||
for (final RecurringTransaction data in _recurringTransactions) {
|
||||
formatedRecurringTransactions.add(
|
||||
ListItem(id: data.id, name: data.name),
|
||||
);
|
||||
}
|
||||
|
||||
return EditableList(
|
||||
name: 'Wiederkehrende Transaktionen',
|
||||
items: formatedRecurringTransactions,
|
||||
onAdd: _recurringTransactionController.newRecurringTransactionHandler,
|
||||
onEdit: _recurringTransactionController.editRecurringTransactionHandler,
|
||||
onDelete:
|
||||
_recurringTransactionController.deleteRecurringTransactionHandler,
|
||||
icon: const Icon(Icons.repeat),
|
||||
addTooltip: 'Wiederkehrende Transaktion hinzufügen',
|
||||
menuTooltip: 'Menü anzeigen',
|
||||
);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
}
|
||||
}
|
||||
37
lib/Pages/Settings/settings.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'account_list.dart';
|
||||
import 'recurring_transaction_list.dart';
|
||||
import 'version_number.dart';
|
||||
|
||||
/// Eine Widget-Klasse, die die Einstellungsseite der Anwendung darstellt.
|
||||
class Settings extends StatelessWidget {
|
||||
/// Erstellt eine neue Instanz dieser Klasse.
|
||||
const Settings({super.key});
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => const Scaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Einstellungen',
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
|
||||
AccountList(),
|
||||
SizedBox(height: 24),
|
||||
|
||||
RecurringTransactionList(),
|
||||
SizedBox(height: 8),
|
||||
VersionNumber(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
41
lib/Pages/Settings/version_number.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
/// Ein Widget mit der aktuellen Versionsnummer
|
||||
class VersionNumber extends StatefulWidget {
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
const VersionNumber({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _VersionNumber();
|
||||
}
|
||||
|
||||
class _VersionNumber extends State<VersionNumber> {
|
||||
@override
|
||||
Widget build(final BuildContext context) => Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: FutureBuilder(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder:
|
||||
(
|
||||
final BuildContext context,
|
||||
final AsyncSnapshot<PackageInfo> snapshot,
|
||||
) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
if (snapshot.hasData) {
|
||||
return Text(
|
||||
'${snapshot.data?.version}+${snapshot.data?.buildNumber}',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withAlpha(
|
||||
(0.6 * 255).round(),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
271
lib/Pages/Trend/input_fields.dart
Normal file
@@ -0,0 +1,271 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:routemaster/routemaster.dart';
|
||||
|
||||
import '../../Services/date_service.dart';
|
||||
import '../Misc/InputFields/date_range_picker.dart';
|
||||
|
||||
/// Stellt die Inputfelder für die Verlaufsübersicht dar
|
||||
class InputFields extends StatefulWidget {
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
const InputFields({
|
||||
super.key,
|
||||
this.amountMax,
|
||||
this.amountMin,
|
||||
this.dateFrom,
|
||||
this.dateTo,
|
||||
this.name,
|
||||
});
|
||||
|
||||
/// Der Name der Transaktion, nach der gesucht werden soll
|
||||
final String? name;
|
||||
|
||||
/// Der Mindestbetrag der Transaktion, nach der gesucht werden soll
|
||||
final double? amountMin;
|
||||
|
||||
/// Der Maximalbetrag der Transaktion, nach der gesucht werden soll
|
||||
final double? amountMax;
|
||||
|
||||
/// Das Datum der Transaktionen, ab dem gestartet wurde
|
||||
final DateTime? dateFrom;
|
||||
|
||||
///Das Datum der Transaktionen, bis zu welchen beendet wurde
|
||||
final DateTime? dateTo;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _InputFields();
|
||||
}
|
||||
|
||||
class _InputFields extends State<InputFields> {
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _amountMinController = TextEditingController();
|
||||
final TextEditingController _amountMaxController = TextEditingController();
|
||||
final TextEditingController _dateTimeController = TextEditingController();
|
||||
|
||||
static const double _filterBreakpoint = 600;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
if (widget.name != null) {
|
||||
_nameController.text = widget.name!;
|
||||
}
|
||||
|
||||
if (widget.amountMin != null) {
|
||||
_amountMinController.text = widget.amountMin!.toString();
|
||||
}
|
||||
|
||||
if (widget.amountMax != null) {
|
||||
_amountMaxController.text = widget.amountMax!.toString();
|
||||
}
|
||||
|
||||
if (widget.dateFrom != null && widget.dateTo != null) {
|
||||
final DateTimeRange dateTimeRange = DateTimeRange(
|
||||
start: widget.dateFrom!,
|
||||
end: widget.dateTo!,
|
||||
);
|
||||
|
||||
_dateTimeController.text = DateService.dateTimeRangeToString(
|
||||
dateTimeRange,
|
||||
)!;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_amountMinController.dispose();
|
||||
_amountMaxController.dispose();
|
||||
_dateTimeController.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (final context, final constraints) {
|
||||
if (constraints.maxWidth < _filterBreakpoint) {
|
||||
return _buildFilterButton(context, theme);
|
||||
}
|
||||
|
||||
return _buildInlineFilters(theme);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInlineFilters(final ThemeData theme) => Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Name',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (_) => _updateUrl(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _amountMinController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Min Betrag €',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
onChanged: (_) => _updateUrl(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _amountMaxController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Max Betrag €',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
onChanged: (_) => _updateUrl(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: DateRangePicker(
|
||||
controller: _dateTimeController,
|
||||
onChanged: (_) => _updateUrl(),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Zeitraum',
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: Icon(
|
||||
Icons.calendar_month,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(onPressed: _clear, icon: const Icon(Icons.clear)),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildFilterButton(
|
||||
final BuildContext context,
|
||||
final ThemeData theme,
|
||||
) => Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.filter_alt),
|
||||
label: const Text('Filter'),
|
||||
onPressed: () => _openFilterDialog(context),
|
||||
),
|
||||
);
|
||||
|
||||
void _openFilterDialog(final BuildContext context) {
|
||||
unawaited(
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (final context) => AlertDialog(
|
||||
title: const Text('Filter'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Name',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _amountMinController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Min Betrag €',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _amountMaxController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Max Betrag €',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DateRangePicker(
|
||||
controller: _dateTimeController,
|
||||
onChanged: (_) {},
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Zeitraum',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_clear();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Zurücksetzen'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
_updateUrl();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Anwenden'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _clear() {
|
||||
_nameController.clear();
|
||||
_amountMinController.clear();
|
||||
_amountMaxController.clear();
|
||||
_dateTimeController.clear();
|
||||
_updateUrl();
|
||||
}
|
||||
|
||||
void _updateUrl() {
|
||||
final params = <String, String>{
|
||||
if (_nameController.text != '') 'name': _nameController.text,
|
||||
if (_amountMinController.text != '')
|
||||
'amountMin': _amountMinController.text,
|
||||
if (_amountMaxController.text != '')
|
||||
'amountMax': _amountMaxController.text,
|
||||
};
|
||||
|
||||
if (_dateTimeController.text != '') {
|
||||
final DateTimeRange<DateTime>? range = DateService.stringToDateTimeRange(
|
||||
_dateTimeController.text,
|
||||
);
|
||||
|
||||
if (range != null) {
|
||||
params['dateFrom'] = range.start.toIso8601String();
|
||||
params['dateTo'] = range.end.toIso8601String();
|
||||
}
|
||||
}
|
||||
|
||||
Routemaster.of(context).replace('/trend', queryParameters: params);
|
||||
}
|
||||
}
|
||||
174
lib/Pages/Trend/transaction_list.dart
Normal file
@@ -0,0 +1,174 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../Controller/account_controller.dart';
|
||||
import '../../Controller/transaction_controller.dart';
|
||||
import '../../Entities/drift_database.dart';
|
||||
import '../../Repositories/transaction_repository.dart';
|
||||
|
||||
/// Stellt eine filterbare Liste der Transaktionen dar
|
||||
class TransactionList extends StatefulWidget {
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
const TransactionList({
|
||||
super.key,
|
||||
this.amountMax,
|
||||
this.amountMin,
|
||||
this.dateFrom,
|
||||
this.dateTo,
|
||||
this.name,
|
||||
});
|
||||
|
||||
/// Der Name der Transaktion, nach der gesucht werden soll
|
||||
final String? name;
|
||||
|
||||
/// Der Mindestbetrag der Transaktion, nach der gesucht werden soll
|
||||
final double? amountMin;
|
||||
|
||||
/// Der Maximalbetrag der Transaktion, nach der gesucht werden soll
|
||||
final double? amountMax;
|
||||
|
||||
/// Das Datum der Transaktionen, ab dem gestartet wurde
|
||||
final DateTime? dateFrom;
|
||||
|
||||
///Das Datum der Transaktionen, bis zu welchen beendet wurde
|
||||
final DateTime? dateTo;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TransactionListState();
|
||||
}
|
||||
|
||||
class _TransactionListState extends State<TransactionList> {
|
||||
final TransactionController _transactionController = TransactionController();
|
||||
final AccountController _accountController = AccountController();
|
||||
|
||||
final TransactionRepository _transactionRepository = TransactionRepository();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_transactionController.transactions.addListener(() {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => FutureBuilder(
|
||||
future: _transactionRepository.findBy(
|
||||
name: widget.name,
|
||||
amountMin: widget.amountMin,
|
||||
amountMax: widget.amountMax,
|
||||
dateFrom: widget.dateFrom,
|
||||
dateTo: (widget.dateTo != null) ? widget.dateTo : DateTime.now(),
|
||||
account: _accountController.selected.value,
|
||||
orderBy: 'dateDesc',
|
||||
),
|
||||
builder:
|
||||
(
|
||||
final BuildContext context,
|
||||
final AsyncSnapshot<List<Transaction>> snapshot,
|
||||
) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
if (snapshot.hasData) {
|
||||
final transactionsByMonth = <String, List<Transaction>>{};
|
||||
for (final Transaction transaction in snapshot.data!) {
|
||||
final DateTime date = transaction.date!;
|
||||
final DateFormat format = DateFormat('MMMM');
|
||||
|
||||
final monthName = '${format.format(date)} ${date.year}';
|
||||
transactionsByMonth
|
||||
.putIfAbsent(monthName, () => [])
|
||||
.add(transaction);
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: CustomScrollView(
|
||||
slivers: transactionsByMonth.entries
|
||||
.map(
|
||||
(
|
||||
final MapEntry<String, List<Transaction>> entry,
|
||||
) => SliverStickyHeader(
|
||||
header: Container(
|
||||
color: theme.colorScheme.surface,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
entry.key,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate((
|
||||
final BuildContext context,
|
||||
final int index,
|
||||
) {
|
||||
final Transaction transaction = entry.value[index];
|
||||
|
||||
return ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
onTap: () {
|
||||
unawaited(
|
||||
_transactionController.editTransactionHandler(
|
||||
transaction.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(
|
||||
transaction.name,
|
||||
style: (transaction.checked)
|
||||
? const TextStyle()
|
||||
: TextStyle(color: theme.colorScheme.error),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${transaction.date?.day}'
|
||||
'.${transaction.date?.month}'
|
||||
'.${transaction.date?.year}',
|
||||
),
|
||||
trailing: Text(
|
||||
'${transaction.amount.abs().toStringAsFixed(2)}'
|
||||
' €',
|
||||
style: TextStyle(
|
||||
color: transaction.amount >= 0
|
||||
? Colors.red
|
||||
: Colors.green,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}, childCount: entry.value.length),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.error, color: theme.colorScheme.error),
|
||||
const Text('Fehler beim holen der Transaktionen!'),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
85
lib/Pages/Trend/trend.dart
Normal file
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:routemaster/routemaster.dart';
|
||||
|
||||
import '../Misc/monthly_balance_chart.dart';
|
||||
import 'input_fields.dart';
|
||||
import 'transaction_list.dart';
|
||||
|
||||
/// Eine Seite, die den Trend von Transaktionen zeigt.
|
||||
///
|
||||
/// Die Seite zeigt einen Liniendiagramm für den Kontostand über die Zeit,
|
||||
/// sowie eine Liste aller Transaktionen, die nach Monaten gruppiert sind.
|
||||
/// Die Transaktionen können nach Name, Betrag und Zeitraum gefiltert werden.
|
||||
class Trend extends StatefulWidget {
|
||||
/// Erstellt eine neue Instanz der Trend-Seite.
|
||||
const Trend({super.key});
|
||||
|
||||
@override
|
||||
State<Trend> createState() => _TrendState();
|
||||
}
|
||||
|
||||
class _TrendState extends State<Trend> {
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
final Map<String, String> query = Routemaster.of(
|
||||
context,
|
||||
).currentRoute.queryParameters;
|
||||
|
||||
final String? name = query['name'];
|
||||
final double? amountMin = double.tryParse(query['amountMin'] ?? '');
|
||||
final double? amountMax = double.tryParse(query['amountMax'] ?? '');
|
||||
final DateTime? dateFrom = DateTime.tryParse(query['dateFrom'] ?? '');
|
||||
final DateTime? dateTo = DateTime.tryParse(query['dateTo'] ?? '');
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
InputFields(
|
||||
name: name,
|
||||
amountMin: amountMin,
|
||||
amountMax: amountMax,
|
||||
dateFrom: dateFrom,
|
||||
dateTo: dateTo,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
MonthlyBalanceChart(
|
||||
name: name,
|
||||
amountMin: amountMin,
|
||||
amountMax: amountMax,
|
||||
dateFrom: (dateFrom != null)
|
||||
? DateTime(dateFrom.year, dateFrom.month, dateFrom.day - 1)
|
||||
: null,
|
||||
dateTo: (dateTo != null)
|
||||
? DateTime(dateTo.year, dateTo.month, dateTo.day + 1)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
const Text(
|
||||
'Transaktionen',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TransactionList(
|
||||
name: name,
|
||||
amountMin: amountMin,
|
||||
amountMax: amountMax,
|
||||
dateFrom: (dateFrom != null)
|
||||
? DateTime(dateFrom.year, dateFrom.month, dateFrom.day - 1)
|
||||
: null,
|
||||
dateTo: (dateTo != null)
|
||||
? DateTime(dateTo.year, dateTo.month, dateTo.day + 1)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
63
lib/Pages/home_page.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
||||
import 'package:routemaster/routemaster.dart';
|
||||
|
||||
import 'Misc/account_select.dart';
|
||||
import 'Misc/floating_creation_button.dart';
|
||||
|
||||
/// Eine Seite, die als Container für die verschiedenen Tabs der App dient.
|
||||
///
|
||||
/// Diese Seite enthält eine App-Bar mit einer Kontoauswahl sowie ein
|
||||
/// Bottom-Navigation-Bar für die Navigation zwischen
|
||||
/// Dashboard, Verlauf und Einstellungen.
|
||||
class HomePage extends StatelessWidget {
|
||||
/// Erstellt eine neue Instanz dieser Klasse
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
final TabPageState tabPage = TabPage.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
titleSpacing: 0,
|
||||
title: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: AccountSelect(),
|
||||
),
|
||||
),
|
||||
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: TabBarView(
|
||||
controller: tabPage.controller,
|
||||
children: <Widget>[
|
||||
for (final PageStack stack in tabPage.stacks)
|
||||
PageStackNavigator(stack: stack),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
bottomNavigationBar: _bottomNav(tabPage),
|
||||
|
||||
floatingActionButtonLocation: ExpandableFab.location,
|
||||
floatingActionButton: const FloatingCreationButton(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _bottomNav(final TabPageState tabPage) => BottomNavigationBar(
|
||||
currentIndex: tabPage.index,
|
||||
onTap: tabPage.controller.animateTo,
|
||||
items: const <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(icon: Icon(Icons.dashboard), label: 'Dashboard'),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.history), label: 'Verlauf'),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.settings),
|
||||
label: 'Einstellungen',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
6
lib/PlatformDependent/Native/update_transactions.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
import '../../Controller/port_controller.dart';
|
||||
|
||||
/// Aktualisiert die Transaktionen, die angezeigt werden
|
||||
void updateTransactions() {
|
||||
PortController().getPort('update-transactions')?.send('ready');
|
||||
}
|
||||
47
lib/PlatformDependent/Web/date_utils_web.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
/// Utility functions for working with dates.
|
||||
class DateUtils {
|
||||
/// Returns a [DateTime] that is [monthDate] with the added number
|
||||
/// of months and the day set to 1 and time set to midnight.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// DateTime date = DateTime(2019, 1, 15);
|
||||
/// DateTime futureDate = DateUtils.addMonthsToMonthDate(date, 3);
|
||||
/// ```
|
||||
///
|
||||
/// `date` would be January 15, 2019.
|
||||
/// `futureDate` would be April 1, 2019 since it adds 3 months.
|
||||
static DateTime addMonthsToMonthDate(
|
||||
final DateTime monthDate,
|
||||
final int monthsToAdd,
|
||||
) => DateTime(monthDate.year, monthDate.month + monthsToAdd);
|
||||
|
||||
/// Returns the number of days in a month, according to the proleptic
|
||||
/// Gregorian calendar.
|
||||
///
|
||||
/// This applies the leap year logic introduced by the Gregorian reforms of
|
||||
/// 1582. It will not give valid results for dates prior to that time.
|
||||
static int getDaysInMonth(final int year, final int month) {
|
||||
if (month == DateTime.february) {
|
||||
final bool isLeapYear =
|
||||
(year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0);
|
||||
return isLeapYear ? 29 : 28;
|
||||
}
|
||||
const List<int> daysInMonth = <int>[
|
||||
31,
|
||||
-1,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
];
|
||||
return daysInMonth[month - 1];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/// Ein Stub
|
||||
class LocalNotifications {
|
||||
/// Ein Stub
|
||||
Future<void> showTransactionsToCheckNotification(
|
||||
final List<dynamic> transactions,
|
||||
) async {}
|
||||
}
|
||||
2
lib/PlatformDependent/Web/update_transactions.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
/// Ein Stub
|
||||
void updateTransactions() {}
|
||||
66
lib/Repositories/account_repository.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
import '../Entities/drift_database.dart';
|
||||
import '../Services/database_service.dart';
|
||||
|
||||
/// Funktionen zum interagieren mit der Datenbank für die Konten
|
||||
class AccountRepository {
|
||||
final AppDatabase _db = DatabaseService().database;
|
||||
|
||||
/// Fügt ein neues Konto zur Datenbank hinzu
|
||||
Future<Account?> add(final AccountsCompanion account) async {
|
||||
final int id = await _db.into(_db.accounts).insert(account);
|
||||
return find(id);
|
||||
}
|
||||
|
||||
/// Aktualisiert ein Konto in der Datenbank
|
||||
Future<bool> update(final AccountsCompanion account) {
|
||||
final AccountsCompanion accountToUpdate = AccountsCompanion(
|
||||
id: account.id,
|
||||
name: account.name,
|
||||
updatedAt: Value(DateTime.now()),
|
||||
);
|
||||
|
||||
return _db.update(_db.accounts).replace(accountToUpdate);
|
||||
}
|
||||
|
||||
/// Entfernt ein Konto aus der Datenbank
|
||||
Future<int> remove(final Account account) => (_db.delete(
|
||||
_db.accounts,
|
||||
)..where((final t) => t.id.equals(account.id))).go();
|
||||
|
||||
/// Sucht ein Konto anhand seiner Id aus der Datenbank
|
||||
Future<Account?> find(final int id) => (_db.select(
|
||||
_db.accounts,
|
||||
)..where((final t) => t.id.equals(id))).getSingleOrNull();
|
||||
|
||||
/// Sucht Konten anhand der gegebenen Parameter aus der Datenbank
|
||||
Future<List<Account>> findBy({
|
||||
final int? id,
|
||||
final String? name,
|
||||
final String? orderBy,
|
||||
}) {
|
||||
final SimpleSelectStatement<$AccountsTable, Account> query = _db.select(
|
||||
_db.accounts,
|
||||
);
|
||||
|
||||
if (id != null) {
|
||||
query.where((final t) => t.id.equals(id));
|
||||
}
|
||||
|
||||
if (name != null && name.isNotEmpty) {
|
||||
query.where((final t) => t.name.like('%$name%'));
|
||||
}
|
||||
|
||||
if (orderBy != null) {
|
||||
switch (orderBy) {
|
||||
case 'nameAsc':
|
||||
query.orderBy([(final t) => OrderingTerm.asc(t.name)]);
|
||||
case 'nameDesc':
|
||||
query.orderBy([(final t) => OrderingTerm.desc(t.name)]);
|
||||
}
|
||||
}
|
||||
|
||||
return query.get();
|
||||
}
|
||||
}
|
||||
131
lib/Repositories/recurring_transacation_repository.dart
Normal file
@@ -0,0 +1,131 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
import '../Entities/drift_database.dart';
|
||||
import '../Entities/time_frame_enum.dart';
|
||||
import '../Services/database_service.dart';
|
||||
|
||||
/// Funktionen zum interagieren mit der Datenbank für die Transaktionen
|
||||
class RecurringTransactionRepository {
|
||||
final AppDatabase _db = DatabaseService().database;
|
||||
|
||||
/// Fügt eine neue wiederkehrende Transaktion zur Datenbank hinzu
|
||||
Future<RecurringTransaction?> add(
|
||||
final RecurringTransactionsCompanion recurringTransaction,
|
||||
) async {
|
||||
final int id = await _db
|
||||
.into(_db.recurringTransactions)
|
||||
.insert(recurringTransaction);
|
||||
return find(id);
|
||||
}
|
||||
|
||||
/// Aktualisiert eine wiederkehrende Transaktion in der Datenbank
|
||||
Future<bool> update(
|
||||
final RecurringTransactionsCompanion recurringTransaction,) {
|
||||
final RecurringTransactionsCompanion recurringTransactionToUpdate =
|
||||
RecurringTransactionsCompanion(
|
||||
id: recurringTransaction.id,
|
||||
name: recurringTransaction.name,
|
||||
startDate: recurringTransaction.startDate,
|
||||
timeFrame: recurringTransaction.timeFrame,
|
||||
amount: recurringTransaction.amount,
|
||||
accountId: recurringTransaction.accountId,
|
||||
updatedAt: Value(DateTime.now()),
|
||||
);
|
||||
|
||||
return _db
|
||||
.update(_db.recurringTransactions)
|
||||
.replace(recurringTransactionToUpdate);
|
||||
}
|
||||
|
||||
/// Entfernt eine wiederkehrende Transaktion aus der Datenbank
|
||||
Future<int> remove(final RecurringTransaction recurringTransaction) =>
|
||||
(_db.delete(
|
||||
_db.recurringTransactions,
|
||||
)..where((final t) => t.id.equals(recurringTransaction.id))).go();
|
||||
|
||||
/// Sucht eine wiederkehrende Transaktion anhand seiner Id aus der Datenbank
|
||||
Future<RecurringTransaction?> find(final int id) => (_db.select(
|
||||
_db.recurringTransactions,
|
||||
)..where((final t) => t.id.equals(id))).getSingleOrNull();
|
||||
|
||||
/// Sucht wiederkehrende Transaktionen anhand der gegebenen Parameter
|
||||
/// aus der Datenbank
|
||||
Future<List<RecurringTransaction>> findBy({
|
||||
final int? id,
|
||||
final String? name,
|
||||
final DateTime? startDate,
|
||||
final DateTime? startDateBefore,
|
||||
final DateTime? startDateAfter,
|
||||
final TimeFrameEnum? timeFrame,
|
||||
final double? amount,
|
||||
final double? amountMin,
|
||||
final double? amountMax,
|
||||
final Account? account,
|
||||
final String? orderBy,
|
||||
}) {
|
||||
final SimpleSelectStatement<
|
||||
$RecurringTransactionsTable,
|
||||
RecurringTransaction
|
||||
>
|
||||
query = _db.select(_db.recurringTransactions);
|
||||
|
||||
if (id != null) {
|
||||
query.where((final t) => t.id.equals(id));
|
||||
}
|
||||
|
||||
if (name != null && name.isNotEmpty) {
|
||||
query.where((final t) => t.name.like('%$name%'));
|
||||
}
|
||||
|
||||
if (startDate != null) {
|
||||
query.where((final t) => t.startDate.equals(startDate));
|
||||
}
|
||||
|
||||
if (startDateAfter != null) {
|
||||
query.where((final t) => t.startDate.isBiggerThanValue(startDateAfter));
|
||||
}
|
||||
|
||||
if (startDateBefore != null) {
|
||||
query.where((final t) => t.startDate.isSmallerThanValue(startDateBefore));
|
||||
}
|
||||
|
||||
if (timeFrame != null) {
|
||||
query.where((final t) => t.timeFrame.equals(timeFrame.index));
|
||||
}
|
||||
|
||||
if (amount != null) {
|
||||
query.where((final t) => t.amount.equals(amount));
|
||||
}
|
||||
|
||||
if (amountMin != null) {
|
||||
query.where((final t) => t.amount.isBiggerThanValue(amountMin));
|
||||
}
|
||||
|
||||
if (amountMax != null) {
|
||||
query.where((final t) => t.amount.isSmallerThanValue(amountMax));
|
||||
}
|
||||
|
||||
if (account != null) {
|
||||
query.where((final t) => t.accountId.equals(account.id));
|
||||
}
|
||||
|
||||
if (orderBy != null) {
|
||||
switch (orderBy) {
|
||||
case 'nameAsc':
|
||||
query.orderBy([(final t) => OrderingTerm.asc(t.name)]);
|
||||
case 'nameDesc':
|
||||
query.orderBy([(final t) => OrderingTerm.desc(t.name)]);
|
||||
case 'amountAsc':
|
||||
query.orderBy([(final t) => OrderingTerm.asc(t.amount)]);
|
||||
case 'amountDesc':
|
||||
query.orderBy([(final t) => OrderingTerm.desc(t.amount)]);
|
||||
case 'startDateAsc':
|
||||
query.orderBy([(final t) => OrderingTerm.asc(t.startDate)]);
|
||||
case 'startDateDesc':
|
||||
query.orderBy([(final t) => OrderingTerm.desc(t.startDate)]);
|
||||
}
|
||||
}
|
||||
|
||||
return query.get();
|
||||
}
|
||||
}
|
||||
294
lib/Repositories/transaction_repository.dart
Normal file
@@ -0,0 +1,294 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
import '../Entities/drift_database.dart';
|
||||
import '../Services/database_service.dart';
|
||||
|
||||
/// Funktionen zum interagieren mit der Datenbank für die Transaktionen
|
||||
class TransactionRepository {
|
||||
final AppDatabase _db = DatabaseService().database;
|
||||
|
||||
/// Fügt eine neue Transaktion zur Datenbank hinzu
|
||||
Future<Transaction?> add(final TransactionsCompanion transaction) async {
|
||||
final DateTime date = transaction.date.value!.add(
|
||||
transaction.date.value!.timeZoneOffset,
|
||||
);
|
||||
|
||||
final TransactionsCompanion transactionToAdd = TransactionsCompanion(
|
||||
id: transaction.id,
|
||||
name: transaction.name,
|
||||
date: Value(date),
|
||||
amount: transaction.amount,
|
||||
checked: transaction.checked,
|
||||
accountId: transaction.accountId,
|
||||
recurringTransactionId: transaction.recurringTransactionId,
|
||||
updatedAt: Value(DateTime.now()),
|
||||
);
|
||||
|
||||
final int id = await _db.into(_db.transactions).insert(transactionToAdd);
|
||||
return find(id);
|
||||
}
|
||||
|
||||
/// Aktualisiert eine Transaktion in der Datenbank
|
||||
Future<bool> update(final TransactionsCompanion transaction) async {
|
||||
final Transaction? transactionInDb = await find(transaction.id.value);
|
||||
final Transaction? transactionData = transactionInDb?.copyWithCompanion(
|
||||
transaction,
|
||||
);
|
||||
|
||||
final DateTime date = transaction.date.value!.add(
|
||||
transaction.date.value!.timeZoneOffset,
|
||||
);
|
||||
|
||||
final TransactionsCompanion transactionToUpdate = TransactionsCompanion(
|
||||
id: Value(transactionData?.id ?? transaction.id.value),
|
||||
name: Value(transactionData?.name ?? transaction.name.value),
|
||||
date: Value(date),
|
||||
amount: Value(transactionData?.amount ?? transaction.amount.value),
|
||||
checked: const Value(true),
|
||||
accountId: Value(
|
||||
transactionData?.accountId ?? transaction.accountId.value,
|
||||
),
|
||||
recurringTransactionId: Value(
|
||||
transactionData?.recurringTransactionId ??
|
||||
transaction.recurringTransactionId.value,
|
||||
),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
);
|
||||
|
||||
return _db.update(_db.transactions).replace(transactionToUpdate);
|
||||
}
|
||||
|
||||
/// Entfernt eine Transaktion aus der Datenbank
|
||||
Future<int> remove(final Transaction transaction) => (_db.delete(
|
||||
_db.transactions,
|
||||
)..where((final t) => t.id.equals(transaction.id))).go();
|
||||
|
||||
/// Sucht eine Transaktion anhand seiner Id aus der Datenbank
|
||||
Future<Transaction?> find(final int id) => (_db.select(
|
||||
_db.transactions,
|
||||
)..where((final t) => t.id.equals(id))).getSingleOrNull();
|
||||
|
||||
/// Sucht Transaktionen anhand der gegebenen Parameter aus der Datenbank
|
||||
Future<List<Transaction>> findBy({
|
||||
final int? id,
|
||||
final String? name,
|
||||
final DateTime? date,
|
||||
final DateTime? dateFrom,
|
||||
final DateTime? dateTo,
|
||||
final double? amount,
|
||||
final double? amountMin,
|
||||
final double? amountMax,
|
||||
final Account? account,
|
||||
final bool? checked,
|
||||
final RecurringTransaction? recurringTransaction,
|
||||
final int? limit,
|
||||
final int? offset,
|
||||
final String? orderBy,
|
||||
}) {
|
||||
final SimpleSelectStatement<$TransactionsTable, Transaction> query = _db
|
||||
.select(_db.transactions);
|
||||
|
||||
if (id != null) {
|
||||
query.where((final t) => t.id.equals(id));
|
||||
}
|
||||
|
||||
if (name != null && name.isNotEmpty) {
|
||||
query.where((final t) => t.name.like('%$name%'));
|
||||
}
|
||||
|
||||
if (date != null) {
|
||||
query.where((final t) => t.date.equals(date));
|
||||
}
|
||||
|
||||
if (dateFrom != null) {
|
||||
query.where((final t) => t.date.isBiggerThanValue(dateFrom));
|
||||
}
|
||||
|
||||
if (dateTo != null) {
|
||||
query.where((final t) => t.date.isSmallerThanValue(dateTo));
|
||||
}
|
||||
|
||||
if (amount != null) {
|
||||
query.where((final t) => t.amount.equals(amount));
|
||||
}
|
||||
|
||||
if (amountMin != null) {
|
||||
query.where((final t) => t.amount.isBiggerThanValue(amountMin));
|
||||
}
|
||||
|
||||
if (amountMax != null) {
|
||||
query.where((final t) => t.amount.isSmallerThanValue(amountMax));
|
||||
}
|
||||
|
||||
if (account != null) {
|
||||
query.where((final t) => t.accountId.equals(account.id));
|
||||
}
|
||||
|
||||
if (checked != null) {
|
||||
query.where((final t) => t.checked.equals(checked));
|
||||
}
|
||||
|
||||
if (recurringTransaction != null) {
|
||||
query.where(
|
||||
(final t) => t.recurringTransactionId.equals(recurringTransaction.id),
|
||||
);
|
||||
}
|
||||
|
||||
if (limit != null) {
|
||||
query.limit(limit, offset: offset);
|
||||
}
|
||||
|
||||
if (orderBy != null) {
|
||||
switch (orderBy) {
|
||||
case 'nameAsc':
|
||||
query.orderBy([(final t) => OrderingTerm.asc(t.name)]);
|
||||
case 'nameDesc':
|
||||
query.orderBy([(final t) => OrderingTerm.desc(t.name)]);
|
||||
case 'dateAsc':
|
||||
query.orderBy([(final t) => OrderingTerm.asc(t.date)]);
|
||||
case 'dateDesc':
|
||||
query.orderBy([(final t) => OrderingTerm.desc(t.date)]);
|
||||
case 'amountAsc':
|
||||
query.orderBy([(final t) => OrderingTerm.asc(t.amount)]);
|
||||
case 'amountDesc':
|
||||
query.orderBy([(final t) => OrderingTerm.desc(t.amount)]);
|
||||
}
|
||||
}
|
||||
|
||||
return query.get();
|
||||
}
|
||||
|
||||
/// Gibt den Kontostand zurück
|
||||
Future<double> balance({
|
||||
final Account? account,
|
||||
final DateTime? until,
|
||||
}) async {
|
||||
final JoinedSelectStatement<$TransactionsTable, Transaction> query =
|
||||
_db.selectOnly(_db.transactions)
|
||||
..addColumns([_db.transactions.amount.sum()]);
|
||||
|
||||
if (account != null) {
|
||||
query.where(_db.transactions.accountId.equals(account.id));
|
||||
}
|
||||
|
||||
if (until != null) {
|
||||
query.where(_db.transactions.date.isSmallerOrEqualValue(until));
|
||||
}
|
||||
|
||||
return (await query
|
||||
.map((final row) => row.read(_db.transactions.amount.sum()) ?? 0)
|
||||
.getSingle()) *
|
||||
-1;
|
||||
}
|
||||
|
||||
/// Gibt den Kontostand der letzten 12 Monate zurück
|
||||
Future<List<Map<String, dynamic>>> monthlyBalances({
|
||||
final Account? account,
|
||||
final String? name,
|
||||
final double? amountMin,
|
||||
final double? amountMax,
|
||||
DateTime? dateFrom,
|
||||
DateTime? dateTo,
|
||||
}) async {
|
||||
final now = DateTime.now();
|
||||
|
||||
final monthStart = DateTime(now.year, now.month - 12);
|
||||
final monthEnd = DateTime(now.year, now.month + 1, 0);
|
||||
|
||||
if (dateFrom == null || dateFrom.compareTo(monthStart) < 0) {
|
||||
dateFrom = monthStart;
|
||||
}
|
||||
|
||||
if (dateTo == null || dateTo.compareTo(monthEnd) > 0) {
|
||||
dateTo = monthEnd;
|
||||
}
|
||||
|
||||
final Expression<int> yearExpr = _db.transactions.date.year;
|
||||
final Expression<int> monthExpr = _db.transactions.date.month;
|
||||
final Expression<double> sumExpr = _db.transactions.amount.sum();
|
||||
|
||||
final JoinedSelectStatement<$TransactionsTable, Transaction> query =
|
||||
_db.selectOnly(_db.transactions)
|
||||
..addColumns([yearExpr, monthExpr, sumExpr])
|
||||
..groupBy([yearExpr, monthExpr])
|
||||
..orderBy([OrderingTerm.asc(yearExpr), OrderingTerm.asc(monthExpr)])
|
||||
..where(_db.transactions.date.isBiggerOrEqualValue(dateFrom))
|
||||
..where(_db.transactions.date.isSmallerOrEqualValue(dateTo));
|
||||
|
||||
if (account != null) {
|
||||
query.where(_db.transactions.accountId.equals(account.id));
|
||||
}
|
||||
|
||||
if (name != null && name.isNotEmpty) {
|
||||
query.where(_db.transactions.name.like('%$name%'));
|
||||
}
|
||||
|
||||
if (amountMin != null) {
|
||||
query.where(_db.transactions.amount.isBiggerOrEqualValue(amountMin));
|
||||
}
|
||||
|
||||
if (amountMax != null) {
|
||||
query.where(_db.transactions.amount.isSmallerOrEqualValue(amountMax));
|
||||
}
|
||||
|
||||
final List<Map<String, Object>> rows = (await query.get()).map((final row) {
|
||||
final int year = row.read(yearExpr)!;
|
||||
final int month = row.read(monthExpr)!;
|
||||
|
||||
return {
|
||||
'date': DateTime(year, month),
|
||||
'balance': (row.read(sumExpr) ?? 0) * -1,
|
||||
};
|
||||
}).toList();
|
||||
|
||||
double amount = await balance(account: account, until: dateFrom);
|
||||
|
||||
DateTime dateTimeLoop = dateFrom;
|
||||
int loop = 0;
|
||||
|
||||
final List<Map<String, Object>> result = [];
|
||||
|
||||
while (dateTimeLoop.compareTo(monthEnd) < 0) {
|
||||
Map<String, Object>? row;
|
||||
for (final value in rows) {
|
||||
final Object? rowDate = value['date'];
|
||||
|
||||
if (rowDate is DateTime) {
|
||||
if (dateTimeLoop.compareTo(rowDate) == 0) {
|
||||
row = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (row == null) {
|
||||
result.add({'date': dateTimeLoop, 'balance': 0.0});
|
||||
} else {
|
||||
result.add(row);
|
||||
}
|
||||
|
||||
final double balance = double.parse(result[loop]['balance'].toString());
|
||||
amount = balance + amount;
|
||||
|
||||
result[loop]['balance'] = amount;
|
||||
|
||||
loop = loop + 1;
|
||||
dateTimeLoop = DateTime(dateTimeLoop.year, dateTimeLoop.month + 1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Markiert die übergebenen Transaktionen als überprüft
|
||||
void markTransactionsAsChecked(final List<Transaction> transactions) {
|
||||
for (final value in transactions) {
|
||||
final TransactionsCompanion transaction = TransactionsCompanion(
|
||||
id: Value(value.id),
|
||||
date: Value(value.date),
|
||||
checked: const Value(true),
|
||||
);
|
||||
unawaited(update(transaction));
|
||||
}
|
||||
}
|
||||
}
|
||||
19
lib/Services/database_service.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import '../Entities/drift_database.dart';
|
||||
|
||||
/// Eine Klasse, um auf die Datenbank zugreifen zu können
|
||||
class DatabaseService {
|
||||
/// Gibt die aktuell gültige Instanz der Klasse zurück
|
||||
factory DatabaseService() => _instance;
|
||||
|
||||
DatabaseService._internal();
|
||||
|
||||
static final DatabaseService _instance = DatabaseService._internal();
|
||||
|
||||
AppDatabase? _database;
|
||||
|
||||
/// Stellt die Datenbank dar
|
||||
AppDatabase get database {
|
||||
_database ??= AppDatabase();
|
||||
return _database!;
|
||||
}
|
||||
}
|
||||
45
lib/Services/date_service.dart
Normal file
@@ -0,0 +1,45 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// Eine Service-Klasse um mit Datums-Werten umzugehen
|
||||
class DateService {
|
||||
/// Die Standard-Formatierung von einem Datum
|
||||
static final DateFormat dateFormat = DateFormat('dd.MM.yyyy');
|
||||
|
||||
/// Wandelt einen String in eine DateTimeRange um
|
||||
static DateTimeRange? stringToDateTimeRange(final String? value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final List<String> parts = value.split(' - ');
|
||||
if (parts.length != 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final DateTime start = DateService.dateFormat.parseStrict(
|
||||
parts[0].trim(),
|
||||
);
|
||||
final DateTime end = DateService.dateFormat.parseStrict(parts[1].trim());
|
||||
|
||||
if (end.isBefore(start)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return DateTimeRange(start: start, end: end);
|
||||
} on FormatException catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Wandelt eine DateTimeRange in einen String um
|
||||
static String? dateTimeRangeToString(final DateTimeRange? value) {
|
||||
if (value != null) {
|
||||
return '${dateFormat.format(value.start)}'
|
||||
' - '
|
||||
'${dateFormat.format(value.end)}';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
25
lib/Services/initializer.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'dart:async';
|
||||
|
||||
/// Ein Service zur vereinfachung, um darauf zu warten,
|
||||
/// dass etwas Initialisiert wurde
|
||||
class Initializer {
|
||||
bool _initialized = false;
|
||||
final Completer<void> _initializedCompleter = Completer<void>();
|
||||
|
||||
/// Gibt zurück, ob bereits initialisiert wurde
|
||||
bool get initialized => _initialized;
|
||||
|
||||
/// Auf diese Funktion kann gewartet werden,
|
||||
/// bis [Initializer] initialisiert wurde
|
||||
Future<void> waitUntilInitialized() => _initializedCompleter.future;
|
||||
|
||||
/// Setzt den [Initializer] auf initialisiert
|
||||
void setInitialized() {
|
||||
if (_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
_initializedCompleter.complete();
|
||||
}
|
||||
}
|
||||
11
lib/Services/navigation_service.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Eine Klasse, die die Navigation innerhalb der App steuert
|
||||
class NavigationService {
|
||||
/// Der Navigator-Schlüssel, der für die Navigation verwendet wird
|
||||
static final GlobalKey<NavigatorState> navigatorKey =
|
||||
GlobalKey<NavigatorState>();
|
||||
|
||||
/// Gibt den aktuell gültigen BuildContext zurück
|
||||
static BuildContext? getCurrentBuildContext() => navigatorKey.currentContext;
|
||||
}
|
||||
27
lib/Services/router_service.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:routemaster/routemaster.dart';
|
||||
|
||||
import '../Pages/Dashboard/dashboard.dart';
|
||||
import '../Pages/Settings/settings.dart';
|
||||
import '../Pages/Trend/trend.dart';
|
||||
import '../Pages/home_page.dart';
|
||||
|
||||
/// Ein Service für das Routing innerhalb der App.
|
||||
///
|
||||
/// Diese Klasse definiert die verfügbaren Routen
|
||||
/// und deren Zuordnung zu den entsprechenden Seiten.
|
||||
class RouterService {
|
||||
/// Die Routenkonfiguration der App.
|
||||
static final RouteMap routes = RouteMap(
|
||||
routes: <String, PageBuilder>{
|
||||
'/': (_) => const TabPage(
|
||||
child: HomePage(),
|
||||
paths: <String>['/dashboard', '/trend', '/settings'],
|
||||
),
|
||||
'/dashboard': (_) => const MaterialPage<void>(child: Dashboard()),
|
||||
'/trend': (_) => const MaterialPage<void>(child: Trend()),
|
||||
'/settings': (_) => const MaterialPage<void>(child: Settings()),
|
||||
},
|
||||
onUnknownRoute: (_) => const Redirect('/'),
|
||||
);
|
||||
}
|
||||
47
lib/Services/theme_service.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Ein Service für die Verwaltung der App-Themes.
|
||||
///
|
||||
/// Diese Klasse stellt Methoden bereit,
|
||||
/// um das helle und dunkle Theme der App zu definieren.
|
||||
class ThemeService {
|
||||
/// Die Hauptfarbe der App, die als Basis für das Farbschema verwendet wird.
|
||||
/// Ein helles Blau mit dem Hex-Wert #50A7FA.
|
||||
static const Color color = Color(0xFF50A7FA);
|
||||
|
||||
/// Erstellt und gibt das helle Theme der App zurück.
|
||||
static ThemeData getLightTheme() => ThemeData(
|
||||
brightness: Brightness.light,
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: color),
|
||||
);
|
||||
|
||||
/// Erstellt und gibt das dunkle Theme der App zurück.
|
||||
static ThemeData getDarkTheme() => ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: color,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
/// Gibt die "Erfolgreich"-Farbe des Farbschemas zurück
|
||||
static Color getSuccessColor({required final Brightness brightness}) {
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
|
||||
const double greenHue = 120;
|
||||
|
||||
final double saturation = (hsl.saturation * 0.7).clamp(0.0, 1.0);
|
||||
final double lightness = brightness == Brightness.dark
|
||||
? (hsl.lightness * 0.4).clamp(0.0, 1.0)
|
||||
: (hsl.lightness * 1.2).clamp(0.0, 1.0);
|
||||
|
||||
final successHsl = HSLColor.fromAHSL(
|
||||
hsl.alpha,
|
||||
greenHue,
|
||||
saturation,
|
||||
lightness,
|
||||
);
|
||||
|
||||
return successHsl.toColor();
|
||||
}
|
||||
}
|
||||
29
lib/Services/transaction_service.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../Entities/drift_database.dart';
|
||||
|
||||
/// Ein Service um Transaktionen zu verarbeiten
|
||||
class TransactionService {
|
||||
/// Wandelt die übergebenen Transaktionen in einen String um
|
||||
static String transactionsToString(final List<Transaction> transactions) {
|
||||
final List<Map<String, dynamic>> jsonTransactions = transactions
|
||||
.map((final value) => value.toJson())
|
||||
.toList();
|
||||
|
||||
return jsonEncode(jsonTransactions);
|
||||
}
|
||||
|
||||
/// Wandelt den String in eine Liste von Transaktionen um
|
||||
static List<Transaction> transactionsFromString(final String? transactions) {
|
||||
if (transactions == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
final List<dynamic> decoded = jsonDecode(transactions);
|
||||
return decoded.map((final item) => Transaction.fromJson(item)).toList();
|
||||
} on Exception {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
16
lib/Tasks/BackgroundHandler/background_init.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import '../../Controller/port_controller.dart';
|
||||
|
||||
/// Initialisiert benötigte Services in Background-Isolates
|
||||
Future<void> initBackground() async {
|
||||
final Logger logger = Logger()..d('Init Background for Native');
|
||||
|
||||
final RootIsolateToken? rootIsolateToken = await PortController()
|
||||
.getRootIsolateToken();
|
||||
if (rootIsolateToken != null) {
|
||||
logger.i('Initialising BackgroundIsolateBinaryMessenger...');
|
||||
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
|
||||
}
|
||||
}
|
||||
6
lib/Tasks/BackgroundHandler/background_init_web.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
/// Initialisiert benötigte Services in Background-Isolates für PlatformDependent
|
||||
Future<void> initBackground() async {
|
||||
Logger().d('Init Background for PlatformDependent');
|
||||
}
|
||||
43
lib/Tasks/BackgroundHandler/workers.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:isolate_manager/isolate_manager.dart';
|
||||
|
||||
import '../generate_transactions_task.dart';
|
||||
import '../show_notifications_task.dart';
|
||||
import 'background_init_web.dart' if (dart.library.io) 'background_init.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
@isolateManagerWorker
|
||||
/// Führt eine Hintergrundtask lokal aus
|
||||
Future<void> runTask(final Map<String, dynamic> params) async {
|
||||
final String taskName = params['taskName'];
|
||||
final int initialDelayMinutes = params['initialDelayMinutes'];
|
||||
final int frequencyMinutes = params['frequencyMinutes'];
|
||||
|
||||
await Future.delayed(Duration(minutes: initialDelayMinutes));
|
||||
|
||||
await executeTask(taskName, null);
|
||||
|
||||
if (frequencyMinutes > 0) {
|
||||
await Future.delayed(Duration(minutes: frequencyMinutes));
|
||||
|
||||
unawaited(runTask(params));
|
||||
}
|
||||
}
|
||||
|
||||
/// Funktion um Hintergrundaufgaben auszuführen
|
||||
Future<bool> executeTask(
|
||||
final String taskName,
|
||||
final Map<String, dynamic>? inputData,
|
||||
) async {
|
||||
await initBackground();
|
||||
|
||||
switch (taskName) {
|
||||
case 'generate_transactions':
|
||||
return GenerateTransactionsTask().execute();
|
||||
case 'show_notifications':
|
||||
return ShowNotificationsTask().execute();
|
||||
}
|
||||
|
||||
return Future.value(true);
|
||||
}
|
||||
9
lib/Tasks/BackgroundHandler/workmanager_workers.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
|
||||
import 'workers.dart';
|
||||
|
||||
/// Die Funktion wird von Hintergrundtasks ausgerufen, um diese auszuführen
|
||||
@pragma('vm:entry-point')
|
||||
void callbackDispatcher() {
|
||||
Workmanager().executeTask(executeTask);
|
||||
}
|
||||
157
lib/Tasks/generate_transactions_task.dart
Normal file
@@ -0,0 +1,157 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import '../Entities/drift_database.dart';
|
||||
import '../Entities/time_frame_enum.dart';
|
||||
import '../PlatformDependent/Web/date_utils_web.dart'
|
||||
if (dart.library.io) 'package:flutter/material.dart';
|
||||
import '../PlatformDependent/Web/update_transactions.dart'
|
||||
if (dart.library.io) '../PlatformDependent/Native/update_transactions.dart';
|
||||
import '../Repositories/recurring_transacation_repository.dart';
|
||||
import '../Repositories/transaction_repository.dart';
|
||||
import 'task.dart';
|
||||
|
||||
/// Generiert neue Transaktionen
|
||||
/// anhand der erstellten wiederkehrenden Transaktionen
|
||||
class GenerateTransactionsTask extends Task {
|
||||
final TransactionRepository _transactionRepository = TransactionRepository();
|
||||
final RecurringTransactionRepository _recurringTransactionRepository =
|
||||
RecurringTransactionRepository();
|
||||
|
||||
final Logger _logger = Logger();
|
||||
|
||||
@override
|
||||
Future<bool> execute() async {
|
||||
_logger.i('Generating Transactions of recurring Transactions...');
|
||||
|
||||
final List<RecurringTransaction> recurringTransactions =
|
||||
await _recurringTransactionRepository.findBy();
|
||||
|
||||
for (final recurringTransaction in recurringTransactions) {
|
||||
_logger.i('Generating Transactions of ${recurringTransaction.name}...');
|
||||
|
||||
final List<Transaction> transactions = await _transactionRepository
|
||||
.findBy(
|
||||
recurringTransaction: recurringTransaction,
|
||||
orderBy: 'dateDesc',
|
||||
);
|
||||
final Transaction? transaction = transactions.firstOrNull;
|
||||
|
||||
await _generateTransactions(
|
||||
recurringTransaction,
|
||||
transaction?.date,
|
||||
number: transactions.length,
|
||||
);
|
||||
}
|
||||
|
||||
_logger.i('Generating transactions completed.');
|
||||
|
||||
updateTransactions();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> _generateTransactions(
|
||||
final RecurringTransaction recurringTransaction,
|
||||
final DateTime? lastTransactionDate, {
|
||||
final int number = 0,
|
||||
}) async {
|
||||
final DateTime newTransactionDate;
|
||||
|
||||
if (lastTransactionDate != null) {
|
||||
switch (recurringTransaction.timeFrame) {
|
||||
case TimeFrameEnum.daily:
|
||||
newTransactionDate = lastTransactionDate.add(const Duration(days: 1));
|
||||
case TimeFrameEnum.weekly:
|
||||
newTransactionDate = lastTransactionDate.add(const Duration(days: 7));
|
||||
case TimeFrameEnum.monthly:
|
||||
final DateTime monthDate = DateUtils.addMonthsToMonthDate(
|
||||
lastTransactionDate,
|
||||
1,
|
||||
);
|
||||
|
||||
final int day =
|
||||
recurringTransaction.startDate!.day <
|
||||
DateUtils.getDaysInMonth(
|
||||
monthDate.year,
|
||||
monthDate.day,
|
||||
)
|
||||
? recurringTransaction.startDate!.day
|
||||
: DateUtils.getDaysInMonth(
|
||||
monthDate.year,
|
||||
monthDate.month,
|
||||
);
|
||||
|
||||
newTransactionDate = DateTime(
|
||||
monthDate.year,
|
||||
monthDate.month,
|
||||
day,
|
||||
);
|
||||
case TimeFrameEnum.yearly:
|
||||
final DateTime monthDate = DateUtils.addMonthsToMonthDate(
|
||||
lastTransactionDate,
|
||||
12,
|
||||
);
|
||||
|
||||
final int day =
|
||||
recurringTransaction.startDate!.day <
|
||||
DateUtils.getDaysInMonth(
|
||||
monthDate.year,
|
||||
monthDate.day,
|
||||
)
|
||||
? recurringTransaction.startDate!.day
|
||||
: DateUtils.getDaysInMonth(
|
||||
monthDate.year,
|
||||
monthDate.month,
|
||||
);
|
||||
|
||||
newTransactionDate = DateTime(
|
||||
monthDate.year,
|
||||
monthDate.month,
|
||||
day,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
newTransactionDate = recurringTransaction.startDate!;
|
||||
}
|
||||
|
||||
_logger.i('New transaction-date is $newTransactionDate');
|
||||
|
||||
final DateTime endOfMonth = DateTime(
|
||||
DateTime.now().year,
|
||||
DateTime.now().month + 1,
|
||||
0,
|
||||
);
|
||||
|
||||
if (endOfMonth.compareTo(newTransactionDate) >= 0) {
|
||||
_logger.i('$newTransactionDate is before $endOfMonth');
|
||||
|
||||
final TransactionsCompanion transaction = TransactionsCompanion(
|
||||
name: Value('${recurringTransaction.name} #$number'),
|
||||
date: Value(newTransactionDate),
|
||||
amount: Value(recurringTransaction.amount),
|
||||
checked: const Value(false),
|
||||
accountId: Value(recurringTransaction.accountId),
|
||||
recurringTransactionId: Value(recurringTransaction.id),
|
||||
);
|
||||
|
||||
_logger.i(
|
||||
'Adding transaction ${transaction.name.value}'
|
||||
' on ${transaction.date.value}',
|
||||
);
|
||||
|
||||
await _transactionRepository.add(transaction);
|
||||
|
||||
return _generateTransactions(
|
||||
recurringTransaction,
|
||||
newTransactionDate,
|
||||
number: number + 1,
|
||||
);
|
||||
} else {
|
||||
_logger.i('$newTransactionDate is after $endOfMonth');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
lib/Tasks/show_notifications_task.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import '../Entities/drift_database.dart';
|
||||
import '../PlatformDependent/Web/local_notifications_web_stub.dart'
|
||||
if (dart.library.io) '../Controller/local_notifications.dart';
|
||||
import '../Repositories/transaction_repository.dart';
|
||||
import 'task.dart';
|
||||
|
||||
/// Zeigt Benachrichtigungen für nicht überprüfte Transaktionen an.
|
||||
class ShowNotificationsTask extends Task {
|
||||
final TransactionRepository _transactionRepository = TransactionRepository();
|
||||
|
||||
@override
|
||||
Future<bool> execute() async {
|
||||
final List<Transaction> transactions = await _transactionRepository.findBy(
|
||||
checked: false,
|
||||
dateTo: DateTime.now(),
|
||||
);
|
||||
|
||||
if (transactions.isNotEmpty) {
|
||||
Logger().i('Showing notification for unchecked transactions...');
|
||||
|
||||
await LocalNotifications().showTransactionsToCheckNotification(
|
||||
transactions,
|
||||
);
|
||||
}
|
||||
|
||||
return Future.value(true);
|
||||
}
|
||||
}
|
||||
5
lib/Tasks/task.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
/// Ein Command, welcher ausgeführt werden kann
|
||||
abstract class Task {
|
||||
/// Führt den Command aus
|
||||
Future<bool> execute();
|
||||
}
|
||||
43
lib/main.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:routemaster/routemaster.dart';
|
||||
|
||||
import 'Controller/background_task_controller.dart';
|
||||
import 'Controller/local_notifications.dart';
|
||||
import 'Controller/port_controller.dart';
|
||||
import 'Services/navigation_service.dart';
|
||||
import 'Services/router_service.dart';
|
||||
import 'Services/theme_service.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
PortController();
|
||||
BackgroundTaskController();
|
||||
|
||||
if (!kIsWeb) {
|
||||
LocalNotifications();
|
||||
}
|
||||
|
||||
runApp(
|
||||
MaterialApp.router(
|
||||
routerDelegate: RoutemasterDelegate(
|
||||
routesBuilder: (final BuildContext context) => RouterService.routes,
|
||||
navigatorKey: NavigationService.navigatorKey,
|
||||
),
|
||||
routeInformationParser: const RoutemasterParser(),
|
||||
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [Locale('de')],
|
||||
|
||||
title: 'DragonLedger 🐉📒',
|
||||
theme: ThemeService.getLightTheme(),
|
||||
darkTheme: ThemeService.getDarkTheme(),
|
||||
),
|
||||
);
|
||||
}
|
||||
1
linux/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
flutter/ephemeral
|
||||
128
linux/CMakeLists.txt
Normal file
@@ -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 "dragon_ledger")
|
||||
# The unique GTK application identifier for this application. See:
|
||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||
set(APPLICATION_ID "de.creativedragonslayer.dragon_ledger")
|
||||
|
||||
# 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 "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>: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()
|
||||
88
linux/flutter/CMakeLists.txt
Normal file
@@ -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}
|
||||
)
|
||||
15
linux/flutter/generated_plugin_registrant.cc
Normal file
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
|
||||
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
|
||||
}
|
||||
15
linux/flutter/generated_plugin_registrant.h
Normal file
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||
#define GENERATED_PLUGIN_REGISTRANT_
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
|
||||
// Registers Flutter plugins.
|
||||
void fl_register_plugins(FlPluginRegistry* registry);
|
||||
|
||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||
24
linux/flutter/generated_plugins.cmake
Normal file
@@ -0,0 +1,24 @@
|
||||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
sqlite3_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 $<TARGET_FILE:${plugin}_plugin>)
|
||||
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)
|
||||
26
linux/runner/CMakeLists.txt
Normal file
@@ -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}")
|
||||
6
linux/runner/main.cc
Normal file
@@ -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);
|
||||
}
|
||||
144
linux/runner/my_application.cc
Normal file
@@ -0,0 +1,144 @@
|
||||
#include "my_application.h"
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
#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)
|
||||
|
||||
// Called when first Flutter frame received.
|
||||
static void first_frame_cb(MyApplication* self, FlView *view)
|
||||
{
|
||||
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
|
||||
}
|
||||
|
||||
// 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, "DragonLedger");
|
||||
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, "DragonLedger");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
|
||||
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);
|
||||
GdkRGBA background_color;
|
||||
// Background defaults to black, override it here if necessary, e.g. #00000000 for transparent.
|
||||
gdk_rgba_parse(&background_color, "#000000");
|
||||
fl_view_set_background_color(view, &background_color);
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||
|
||||
// Show the window when Flutter renders.
|
||||
// Requires the view to be realized so we can start rendering.
|
||||
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), self);
|
||||
gtk_widget_realize(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));
|
||||
}
|
||||
18
linux/runner/my_application.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#ifndef FLUTTER_MY_APPLICATION_H_
|
||||
#define FLUTTER_MY_APPLICATION_H_
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
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_
|
||||
102
pubspec.yaml
Normal file
@@ -0,0 +1,102 @@
|
||||
name: dragon_ledger
|
||||
description: "Eine Flutter-App um den Überblick über wiederkehrende Transaktionen und den damit verbunden Kontostand zu behalten."
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
# The following defines the version and build number for your application.
|
||||
# A version number is three numbers separated by dots, like 1.2.43
|
||||
# followed by an optional build number separated by a +.
|
||||
# Both the version and the builder number may be overridden in flutter
|
||||
# build by specifying --build-name and --build-number, respectively.
|
||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.9.2
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||
# dependencies can be manually updated by changing the version numbers below to
|
||||
# the latest version available on pub.dev. To see which dependencies have newer
|
||||
# versions available, run `flutter pub outdated`.
|
||||
dependencies:
|
||||
date_field: ^6.0.3+1
|
||||
drift: ^2.30.0
|
||||
drift_flutter: ^0.2.8
|
||||
dropdown_search: ^6.0.2
|
||||
fl_chart: ^1.1.1
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_expandable_fab: ^2.5.2
|
||||
flutter_local_notifications: ^19.5.0
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
flutter_sticky_header: ^0.8.0
|
||||
font_awesome_flutter: ^10.10.0
|
||||
intl: ^0.20.2
|
||||
isolate_manager: ^6.1.2
|
||||
logger: ^2.6.2
|
||||
package_info_plus: ^9.0.0
|
||||
path_provider: ^2.1.5
|
||||
routemaster: ^1.0.1
|
||||
uuid: ^4.5.2
|
||||
workmanager: ^0.9.0+3
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.10.4
|
||||
drift_dev: ^2.30.0
|
||||
flutter_launcher_icons: ^0.14.4
|
||||
flutter_lints: ^6.0.0
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
isolate_manager_generator: ^0.3.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/to/asset-from-package
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/to/font-from-package
|
||||