├── app
├── linux
│ ├── .gitignore
│ ├── main.cc
│ ├── flutter
│ │ ├── generated_plugin_registrant.cc
│ │ ├── generated_plugin_registrant.h
│ │ ├── generated_plugins.cmake
│ │ └── CMakeLists.txt
│ └── my_application.h
├── make.bat
├── ios
│ ├── Flutter
│ │ ├── Debug.xcconfig
│ │ ├── Release.xcconfig
│ │ └── AppFrameworkInfo.plist
│ ├── Runner
│ │ ├── Runner-Bridging-Header.h
│ │ ├── Assets.xcassets
│ │ │ ├── LaunchImage.imageset
│ │ │ │ ├── LaunchImage.png
│ │ │ │ ├── LaunchImage@2x.png
│ │ │ │ ├── LaunchImage@3x.png
│ │ │ │ ├── README.md
│ │ │ │ └── Contents.json
│ │ │ └── AppIcon.appiconset
│ │ │ │ ├── Icon-App-20x20@1x.png
│ │ │ │ ├── Icon-App-20x20@2x.png
│ │ │ │ ├── Icon-App-20x20@3x.png
│ │ │ │ ├── Icon-App-29x29@1x.png
│ │ │ │ ├── Icon-App-29x29@2x.png
│ │ │ │ ├── Icon-App-29x29@3x.png
│ │ │ │ ├── Icon-App-40x40@1x.png
│ │ │ │ ├── Icon-App-40x40@2x.png
│ │ │ │ ├── Icon-App-40x40@3x.png
│ │ │ │ ├── Icon-App-60x60@2x.png
│ │ │ │ ├── Icon-App-60x60@3x.png
│ │ │ │ ├── Icon-App-76x76@1x.png
│ │ │ │ ├── Icon-App-76x76@2x.png
│ │ │ │ ├── Icon-App-1024x1024@1x.png
│ │ │ │ ├── Icon-App-83.5x83.5@2x.png
│ │ │ │ └── Contents.json
│ │ ├── AppDelegate.swift
│ │ ├── Base.lproj
│ │ │ ├── Main.storyboard
│ │ │ └── LaunchScreen.storyboard
│ │ └── Info.plist
│ ├── Runner.xcodeproj
│ │ ├── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ │ ├── WorkspaceSettings.xcsettings
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── Runner.xcscheme
│ ├── Runner.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── WorkspaceSettings.xcsettings
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── RunnerTests
│ │ └── RunnerTests.swift
│ └── .gitignore
├── lib
│ ├── globals.dart
│ ├── models
│ │ ├── budget.dart
│ │ └── mylabel.dart
│ ├── providers
│ │ ├── filter.dart
│ │ ├── insights_range.dart
│ │ └── settings.dart
│ ├── charts
│ │ ├── chart_models
│ │ │ ├── transaction_history_model.dart
│ │ │ └── pie_chart_model.dart
│ │ ├── charts_base
│ │ │ ├── pie_chart_base.dart
│ │ │ ├── custom_circle_symbol_renderer.dart
│ │ │ └── grouped_bar_chart_base.dart
│ │ └── chart_widgets
│ │ │ ├── transaction_details_pie_chart.dart
│ │ │ └── transaction_history_chart.dart
│ ├── card_items
│ │ ├── label_chooser_card.dart
│ │ ├── edit_labels_card.dart
│ │ └── transaction_card.dart
│ ├── widgets
│ │ ├── about_app_list_tile.dart
│ │ ├── balance_cards_view.dart
│ │ ├── dashboard_screen_fab.dart
│ │ ├── color_picker_form_field.dart
│ │ ├── chart_container.dart
│ │ ├── transaction_details_appbar.dart
│ │ ├── app_drawer.dart
│ │ ├── dashboard_list_header.dart
│ │ ├── label_filter_dropdown.dart
│ │ ├── home_tabs_bar.dart
│ │ ├── budgets_range_buttons.dart
│ │ ├── transaction_type_chip_form_field.dart
│ │ ├── transactions_list.dart
│ │ ├── transactions_list_filtered.dart
│ │ ├── insights_range_buttons.dart
│ │ ├── balance_summary_card.dart
│ │ ├── transaction_details_charts_view.dart
│ │ └── edit_transaction_appbar.dart
│ ├── utils
│ │ ├── custom_icons_icons.dart
│ │ └── custom_colors.dart
│ ├── screens
│ │ ├── dashboard_screen.dart
│ │ ├── home_tabs_screen.dart
│ │ ├── insights_screen.dart
│ │ ├── onboarding.dart
│ │ └── budget_screen.dart
│ └── main.dart
├── macos
│ ├── Flutter
│ │ ├── Flutter-Debug.xcconfig
│ │ └── Flutter-Release.xcconfig
│ ├── Runner
│ │ ├── Configs
│ │ │ ├── Debug.xcconfig
│ │ │ ├── Release.xcconfig
│ │ │ ├── Warnings.xcconfig
│ │ │ └── AppInfo.xcconfig
│ │ ├── Assets.xcassets
│ │ │ └── AppIcon.appiconset
│ │ │ │ ├── app_icon_16.png
│ │ │ │ ├── app_icon_32.png
│ │ │ │ ├── app_icon_64.png
│ │ │ │ ├── app_icon_1024.png
│ │ │ │ ├── app_icon_128.png
│ │ │ │ ├── app_icon_256.png
│ │ │ │ ├── app_icon_512.png
│ │ │ │ └── Contents.json
│ │ ├── AppDelegate.swift
│ │ ├── Release.entitlements
│ │ ├── DebugProfile.entitlements
│ │ ├── MainFlutterWindow.swift
│ │ └── Info.plist
│ ├── .gitignore
│ ├── Runner.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── Runner.xcodeproj
│ │ ├── project.xcworkspace
│ │ │ └── xcshareddata
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── Runner.xcscheme
│ └── RunnerTests
│ │ └── RunnerTests.swift
├── web
│ ├── favicon.png
│ ├── icons
│ │ ├── Icon-192.png
│ │ ├── Icon-512.png
│ │ ├── Icon-maskable-192.png
│ │ └── Icon-maskable-512.png
│ ├── manifest.json
│ └── index.html
├── assets
│ ├── images
│ │ ├── Intro1.jpg
│ │ ├── Intro2.jpg
│ │ ├── Intro3.jpg
│ │ ├── Intro4.jpg
│ │ └── Intro5.jpg
│ ├── fonts
│ │ └── CustomIcons.ttf
│ └── icon
│ │ ├── launcher_icon.png
│ │ └── launcher_icon_adaptive.png
├── google_fonts
│ └── Cabin-Regular.ttf
├── windows
│ ├── runner
│ │ ├── resources
│ │ │ └── app_icon.ico
│ │ ├── resource.h
│ │ ├── utils.h
│ │ ├── runner.exe.manifest
│ │ ├── flutter_window.h
│ │ ├── main.cpp
│ │ ├── CMakeLists.txt
│ │ ├── utils.cpp
│ │ ├── flutter_window.cpp
│ │ ├── Runner.rc
│ │ └── win32_window.h
│ ├── flutter
│ │ ├── generated_plugin_registrant.cc
│ │ ├── generated_plugin_registrant.h
│ │ └── generated_plugins.cmake
│ └── .gitignore
├── android
│ ├── app
│ │ ├── src
│ │ │ ├── main
│ │ │ │ ├── res
│ │ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── drawable
│ │ │ │ │ │ └── launch_background.xml
│ │ │ │ │ ├── drawable-v21
│ │ │ │ │ │ └── launch_background.xml
│ │ │ │ │ ├── values
│ │ │ │ │ │ └── styles.xml
│ │ │ │ │ └── values-night
│ │ │ │ │ │ └── styles.xml
│ │ │ │ ├── kotlin
│ │ │ │ │ └── com
│ │ │ │ │ │ └── example
│ │ │ │ │ │ └── budget
│ │ │ │ │ │ └── MainActivity.kt
│ │ │ │ └── AndroidManifest.xml
│ │ │ ├── debug
│ │ │ │ └── AndroidManifest.xml
│ │ │ └── profile
│ │ │ │ └── AndroidManifest.xml
│ │ └── build.gradle.kts
│ ├── gradle.properties
│ ├── gradle
│ │ └── wrapper
│ │ │ └── gradle-wrapper.properties
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── settings.gradle.kts
├── .gitignore
├── README.md
├── analysis_options.yaml
└── pubspec.yaml
├── wealthica
├── .gitignore
├── requirements.txt
├── wealthica@.timer
├── wealthica@.service
├── config.ini.example
├── install_service.sh
└── config.py
├── plaid-sync
├── run.bat
├── .gitignore
├── requirements.txt
├── plaid@.timer
├── plaid@.service
├── install_service.sh
├── config
│ └── sandbox.example
├── LICENSE
├── config.py
├── webserver.py
└── README.md
├── images
├── rules.PNG
├── budgets.jpg
├── budgets.png
├── expenses.jpg
├── history.jpg
└── dashboard.jpg
├── tangerine
├── requirements.txt
├── .gitignore
├── config.ini.example
└── config.py
└── README.md
/app/linux/.gitignore:
--------------------------------------------------------------------------------
1 | flutter/ephemeral
2 |
--------------------------------------------------------------------------------
/app/make.bat:
--------------------------------------------------------------------------------
1 | flutter build apk --release
--------------------------------------------------------------------------------
/wealthica/.gitignore:
--------------------------------------------------------------------------------
1 | *.ini
2 | __pycache__
3 |
--------------------------------------------------------------------------------
/app/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Generated.xcconfig"
2 |
--------------------------------------------------------------------------------
/app/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Generated.xcconfig"
2 |
--------------------------------------------------------------------------------
/plaid-sync/run.bat:
--------------------------------------------------------------------------------
1 | python plaid-sync.py -c sandbox.example -v
2 |
--------------------------------------------------------------------------------
/app/lib/globals.dart:
--------------------------------------------------------------------------------
1 | library budget.globals;
2 | String version = "";
3 |
--------------------------------------------------------------------------------
/app/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/app/macos/Flutter/Flutter-Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "ephemeral/Flutter-Generated.xcconfig"
2 |
--------------------------------------------------------------------------------
/images/rules.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/images/rules.PNG
--------------------------------------------------------------------------------
/tangerine/requirements.txt:
--------------------------------------------------------------------------------
1 | pandas
2 | joblib
3 | notion_client
4 | influxdb_client
5 |
--------------------------------------------------------------------------------
/app/macos/Flutter/Flutter-Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "ephemeral/Flutter-Generated.xcconfig"
2 |
--------------------------------------------------------------------------------
/app/web/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/web/favicon.png
--------------------------------------------------------------------------------
/images/budgets.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/images/budgets.jpg
--------------------------------------------------------------------------------
/images/budgets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/images/budgets.png
--------------------------------------------------------------------------------
/images/expenses.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/images/expenses.jpg
--------------------------------------------------------------------------------
/images/history.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/images/history.jpg
--------------------------------------------------------------------------------
/images/dashboard.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/images/dashboard.jpg
--------------------------------------------------------------------------------
/plaid-sync/.gitignore:
--------------------------------------------------------------------------------
1 | /sandbox.example
2 | *.bkp
3 | /transactions.csv
4 | __pycache__
5 | /mint.csv
--------------------------------------------------------------------------------
/plaid-sync/requirements.txt:
--------------------------------------------------------------------------------
1 | plaid-python==7.1.0
2 | pandas
3 | influxdb-client
4 | notion-client
5 |
--------------------------------------------------------------------------------
/tangerine/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | *.swp
3 | /transactions
4 | /config.ini
5 | *~
6 | /tangerine.IPYNB
--------------------------------------------------------------------------------
/wealthica/requirements.txt:
--------------------------------------------------------------------------------
1 | pandas
2 | joblib
3 | notion_client
4 | influxdb_client
5 | gspread
6 |
--------------------------------------------------------------------------------
/app/web/icons/Icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/web/icons/Icon-192.png
--------------------------------------------------------------------------------
/app/web/icons/Icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/web/icons/Icon-512.png
--------------------------------------------------------------------------------
/app/assets/images/Intro1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/assets/images/Intro1.jpg
--------------------------------------------------------------------------------
/app/assets/images/Intro2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/assets/images/Intro2.jpg
--------------------------------------------------------------------------------
/app/assets/images/Intro3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/assets/images/Intro3.jpg
--------------------------------------------------------------------------------
/app/assets/images/Intro4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/assets/images/Intro4.jpg
--------------------------------------------------------------------------------
/app/assets/images/Intro5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/assets/images/Intro5.jpg
--------------------------------------------------------------------------------
/app/assets/fonts/CustomIcons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/assets/fonts/CustomIcons.ttf
--------------------------------------------------------------------------------
/app/assets/icon/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/assets/icon/launcher_icon.png
--------------------------------------------------------------------------------
/app/google_fonts/Cabin-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/google_fonts/Cabin-Regular.ttf
--------------------------------------------------------------------------------
/app/macos/Runner/Configs/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "../../Flutter/Flutter-Debug.xcconfig"
2 | #include "Warnings.xcconfig"
3 |
--------------------------------------------------------------------------------
/app/macos/Runner/Configs/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "../../Flutter/Flutter-Release.xcconfig"
2 | #include "Warnings.xcconfig"
3 |
--------------------------------------------------------------------------------
/app/web/icons/Icon-maskable-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/web/icons/Icon-maskable-192.png
--------------------------------------------------------------------------------
/app/web/icons/Icon-maskable-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/web/icons/Icon-maskable-512.png
--------------------------------------------------------------------------------
/app/macos/.gitignore:
--------------------------------------------------------------------------------
1 | # Flutter-related
2 | **/Flutter/ephemeral/
3 | **/Pods/
4 |
5 | # Xcode-related
6 | **/dgph
7 | **/xcuserdata/
8 |
--------------------------------------------------------------------------------
/app/assets/icon/launcher_icon_adaptive.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/assets/icon/launcher_icon_adaptive.png
--------------------------------------------------------------------------------
/app/windows/runner/resources/app_icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/windows/runner/resources/app_icon.ico
--------------------------------------------------------------------------------
/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/app/lib/models/budget.dart:
--------------------------------------------------------------------------------
1 | class BudgetData {
2 | const BudgetData(this.label, this.limit, this.order);
3 | final String label;
4 | final double limit;
5 | final double order;
6 | }
7 |
--------------------------------------------------------------------------------
/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
--------------------------------------------------------------------------------
/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
--------------------------------------------------------------------------------
/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
--------------------------------------------------------------------------------
/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
--------------------------------------------------------------------------------
/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
--------------------------------------------------------------------------------
/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
--------------------------------------------------------------------------------
/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/app/android/app/src/main/kotlin/com/example/budget/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.budget
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity : FlutterActivity()
6 |
--------------------------------------------------------------------------------
/app/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mzakharo/librebudgeteer/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/app/linux/main.cc:
--------------------------------------------------------------------------------
1 | #include "my_application.h"
2 |
3 | int main(int argc, char** argv) {
4 | g_autoptr(MyApplication) app = my_application_new();
5 | return g_application_run(G_APPLICATION(app), argc, argv);
6 | }
7 |
--------------------------------------------------------------------------------
/tangerine/config.ini.example:
--------------------------------------------------------------------------------
1 | [TANGERINE]
2 | influx_token = YYY
3 | influx_url = http://127.0.0.1:8086
4 | influx_org = org
5 | influx_bucket = bucket
6 | notion_secret = secret_XXXX
7 | notion_database = XXXXX
8 |
9 |
--------------------------------------------------------------------------------
/plaid-sync/plaid@.timer:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Run plaid
3 | Requires=plaid@.service
4 | [Timer]
5 | Unit=plaid@.service
6 | OnUnitInactiveSec=6hours
7 | Persistent=true
8 | [Install]
9 | WantedBy=timers.target
10 |
--------------------------------------------------------------------------------
/app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/macos/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/wealthica/wealthica@.timer:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Run wealthica sync
3 | Requires=wealthica@.service
4 | [Timer]
5 | Unit=wealthica@.service
6 | OnUnitInactiveSec=6hours
7 | Persistent=true
8 | [Install]
9 | WantedBy=timers.target
10 |
--------------------------------------------------------------------------------
/app/linux/flutter/generated_plugin_registrant.cc:
--------------------------------------------------------------------------------
1 | //
2 | // Generated file. Do not edit.
3 | //
4 |
5 | // clang-format off
6 |
7 | #include "generated_plugin_registrant.h"
8 |
9 |
10 | void fl_register_plugins(FlPluginRegistry* registry) {
11 | }
12 |
--------------------------------------------------------------------------------
/app/windows/flutter/generated_plugin_registrant.cc:
--------------------------------------------------------------------------------
1 | //
2 | // Generated file. Do not edit.
3 | //
4 |
5 | // clang-format off
6 |
7 | #include "generated_plugin_registrant.h"
8 |
9 |
10 | void RegisterPlugins(flutter::PluginRegistry* registry) {
11 | }
12 |
--------------------------------------------------------------------------------
/app/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
6 |
--------------------------------------------------------------------------------
/app/macos/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import FlutterMacOS
3 |
4 | @NSApplicationMain
5 | class AppDelegate: FlutterAppDelegate {
6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
7 | return true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/app/macos/Runner/Release.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/wealthica/wealthica@.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=wealthica sync service
3 | Wants=wealthica@.timer
4 |
5 | [Service]
6 | ExecStart=/usr/bin/python3 /home/%i/librebudgeteer/wealthica/main.py -v
7 | WorkingDirectory=/home/%i/librebudgeteer/wealthica
8 | User=%i
9 |
10 | [Install]
11 | WantedBy=multi-user.target
12 |
--------------------------------------------------------------------------------
/app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/plaid-sync/plaid@.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=plaid sync service
3 | Wants=plaid@.timer
4 |
5 | [Service]
6 | ExecStart=/usr/bin/python3 /home/%i/librebudgeteer/plaid-sync/plaid-sync.py -c sandbox.example -v -b
7 | WorkingDirectory=/home/%i/plaid-sync/
8 | User=%i
9 |
10 | [Install]
11 | WantedBy=multi-user.target
12 |
--------------------------------------------------------------------------------
/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 | .cxx/
9 |
10 | # Remember to never publicly share your keystore.
11 | # See https://flutter.dev/to/reference-keystore
12 | key.properties
13 | **/*.keystore
14 | **/*.jks
15 |
--------------------------------------------------------------------------------
/wealthica/config.ini.example:
--------------------------------------------------------------------------------
1 | [WEALTHICA]
2 | influx_token = YYY
3 | influx_url = http://127.0.0.1:8086
4 | influx_org = org
5 | influx_bucket = bucket
6 | notion_secret = secret_XXXX
7 | notion_database = XXXXX
8 | gspread_api_key = YYYY
9 | gspread_sheet_key = ZZZ
10 | gspread_range = XYXYX
11 | gspread_sheet_balances_key = YYY
12 | gspread_balances_range = YYY
13 |
--------------------------------------------------------------------------------
/app/ios/RunnerTests/RunnerTests.swift:
--------------------------------------------------------------------------------
1 | import Flutter
2 | import UIKit
3 | import XCTest
4 |
5 | class RunnerTests: XCTestCase {
6 |
7 | func testExample() {
8 | // If you add code to the Runner application, consider adding tests here.
9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
10 | }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/app/macos/RunnerTests/RunnerTests.swift:
--------------------------------------------------------------------------------
1 | import FlutterMacOS
2 | import Cocoa
3 | import XCTest
4 |
5 | class RunnerTests: XCTestCase {
6 |
7 | func testExample() {
8 | // If you add code to the Runner application, consider adding tests here.
9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
10 | }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/app/windows/.gitignore:
--------------------------------------------------------------------------------
1 | flutter/ephemeral/
2 |
3 | # Visual Studio user-specific files.
4 | *.suo
5 | *.user
6 | *.userosscache
7 | *.sln.docstates
8 |
9 | # Visual Studio build-related files.
10 | x64/
11 | x86/
12 |
13 | # Visual Studio cache files
14 | # files ending in .cache can be ignored
15 | *.[Cc]ache
16 | # but keep track of directories ending in .cache
17 | !*.[Cc]ache/
18 |
--------------------------------------------------------------------------------
/plaid-sync/install_service.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 |
5 | sudo cp plaid@.* /etc/systemd/system/
6 | sudo systemctl daemon-reload
7 | sudo systemctl enable plaid@$USER.service
8 | sudo systemctl enable plaid@$USER.timer
9 | sudo systemctl start plaid@$USER.timer
10 | sudo systemctl status plaid@$USER.timer
11 | sudo systemctl status plaid@$USER.service
12 |
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md:
--------------------------------------------------------------------------------
1 | # Launch Screen Assets
2 |
3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4 |
5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
--------------------------------------------------------------------------------
/app/linux/flutter/generated_plugin_registrant.h:
--------------------------------------------------------------------------------
1 | //
2 | // Generated file. Do not edit.
3 | //
4 |
5 | // clang-format off
6 |
7 | #ifndef GENERATED_PLUGIN_REGISTRANT_
8 | #define GENERATED_PLUGIN_REGISTRANT_
9 |
10 | #include
11 |
12 | // Registers Flutter plugins.
13 | void fl_register_plugins(FlPluginRegistry* registry);
14 |
15 | #endif // GENERATED_PLUGIN_REGISTRANT_
16 |
--------------------------------------------------------------------------------
/app/windows/flutter/generated_plugin_registrant.h:
--------------------------------------------------------------------------------
1 | //
2 | // Generated file. Do not edit.
3 | //
4 |
5 | // clang-format off
6 |
7 | #ifndef GENERATED_PLUGIN_REGISTRANT_
8 | #define GENERATED_PLUGIN_REGISTRANT_
9 |
10 | #include
11 |
12 | // Registers Flutter plugins.
13 | void RegisterPlugins(flutter::PluginRegistry* registry);
14 |
15 | #endif // GENERATED_PLUGIN_REGISTRANT_
16 |
--------------------------------------------------------------------------------
/wealthica/install_service.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 |
5 | sudo cp wealthica@.* /etc/systemd/system/
6 | sudo systemctl daemon-reload
7 | sudo systemctl enable wealthica@$USER.service
8 | sudo systemctl enable wealthica@$USER.timer
9 | sudo systemctl start wealthica@$USER.timer
10 | sudo systemctl status wealthica@$USER.timer
11 | sudo systemctl status wealthica@$USER.service
12 |
--------------------------------------------------------------------------------
/app/lib/providers/filter.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | // Used in HistoryScreen for filtering transactions there.
4 |
5 | class Filter with ChangeNotifier {
6 | // When null it means nothing is being filtered.
7 | String? _labelId;
8 |
9 | String? get labelId => _labelId;
10 |
11 | set labelId(String? labelId) {
12 | _labelId = labelId;
13 | notifyListeners();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/macos/Runner/DebugProfile.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.cs.allow-jit
8 |
9 | com.apple.security.network.server
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/lib/charts/chart_models/transaction_history_model.dart:
--------------------------------------------------------------------------------
1 | import 'package:intl/intl.dart';
2 |
3 | class TransactionHistoryModel {
4 | final DateTime? date;
5 | double incomeAmount;
6 | double expenseAmount;
7 |
8 | TransactionHistoryModel({
9 | required this.date,
10 | required this.incomeAmount,
11 | required this.expenseAmount,
12 | });
13 |
14 | String get dateString => DateFormat("MMM").format(date!);
15 | }
16 |
--------------------------------------------------------------------------------
/app/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Flutter
3 |
4 | @UIApplicationMain
5 | @objc class AppDelegate: FlutterAppDelegate {
6 | override func application(
7 | _ application: UIApplication,
8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
9 | ) -> Bool {
10 | GeneratedPluginRegistrant.register(with: self)
11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/linux/my_application.h:
--------------------------------------------------------------------------------
1 | #ifndef FLUTTER_MY_APPLICATION_H_
2 | #define FLUTTER_MY_APPLICATION_H_
3 |
4 | #include
5 |
6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
7 | GtkApplication)
8 |
9 | /**
10 | * my_application_new:
11 | *
12 | * Creates a new Flutter-based application.
13 | *
14 | * Returns: a new #MyApplication.
15 | */
16 | MyApplication* my_application_new();
17 |
18 | #endif // FLUTTER_MY_APPLICATION_H_
19 |
--------------------------------------------------------------------------------
/app/macos/Runner/MainFlutterWindow.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import FlutterMacOS
3 |
4 | class MainFlutterWindow: NSWindow {
5 | override func awakeFromNib() {
6 | let flutterViewController = FlutterViewController()
7 | let windowFrame = self.frame
8 | self.contentViewController = flutterViewController
9 | self.setFrame(windowFrame, display: true)
10 |
11 | RegisterGeneratedPlugins(registry: flutterViewController)
12 |
13 | super.awakeFromNib()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/windows/runner/resource.h:
--------------------------------------------------------------------------------
1 | //{{NO_DEPENDENCIES}}
2 | // Microsoft Visual C++ generated include file.
3 | // Used by Runner.rc
4 | //
5 | #define IDI_APP_ICON 101
6 |
7 | // Next default values for new objects
8 | //
9 | #ifdef APSTUDIO_INVOKED
10 | #ifndef APSTUDIO_READONLY_SYMBOLS
11 | #define _APS_NEXT_RESOURCE_VALUE 102
12 | #define _APS_NEXT_COMMAND_VALUE 40001
13 | #define _APS_NEXT_CONTROL_VALUE 1001
14 | #define _APS_NEXT_SYMED_VALUE 101
15 | #endif
16 | #endif
17 |
--------------------------------------------------------------------------------
/app/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/app/lib/charts/chart_models/pie_chart_model.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:charts_flutter/flutter.dart' as charts;
3 |
4 | class PieChartModel {
5 | final String label;
6 | final double amount;
7 | final charts.Color color;
8 |
9 | PieChartModel({
10 | required this.label,
11 | required this.amount,
12 | required Color color,
13 | }) : // Convert material color to chart color.
14 | this.color = charts.Color(
15 | r: color.red, g: color.green, b: color.blue, a: color.alpha);
16 | }
17 |
--------------------------------------------------------------------------------
/app/lib/providers/insights_range.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | // Used in InsightsScreen.
4 |
5 | // week and month mean the beginning of the week and month.
6 | // For week it's Sunday.
7 | // For month it's the first day of the month.
8 | enum Range {
9 | week,
10 | month,
11 | previousMonth,
12 | prevpreviousMonth,
13 | lifetime,
14 | }
15 |
16 | class InsightsRange with ChangeNotifier {
17 | Range _range = Range.month;
18 |
19 | Range get range => _range;
20 |
21 | set range(Range range) {
22 | _range = range;
23 | notifyListeners();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/lib/card_items/label_chooser_card.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | // Used in EditTransactionScreen in the LabelChooserDialog.
4 |
5 | class LabelChooserCard extends StatelessWidget {
6 | final Color color;
7 | final String title;
8 |
9 | const LabelChooserCard({
10 | required this.color,
11 | required this.title,
12 | });
13 |
14 | @override
15 | Widget build(BuildContext context) {
16 | return ListTile(
17 | leading: CircleAvatar(
18 | maxRadius: 18,
19 | backgroundColor: color,
20 | ),
21 | title: Text(title),
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/android/build.gradle.kts:
--------------------------------------------------------------------------------
1 | allprojects {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | }
7 |
8 | val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
9 | rootProject.layout.buildDirectory.value(newBuildDir)
10 |
11 | subprojects {
12 | val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
13 | project.layout.buildDirectory.value(newSubprojectBuildDir)
14 | }
15 | subprojects {
16 | project.evaluationDependsOn(":app")
17 | }
18 |
19 | tasks.register("clean") {
20 | delete(rootProject.layout.buildDirectory)
21 | }
22 |
--------------------------------------------------------------------------------
/app/macos/Runner/Configs/Warnings.xcconfig:
--------------------------------------------------------------------------------
1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
2 | GCC_WARN_UNDECLARED_SELECTOR = YES
3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
6 | CLANG_WARN_PRAGMA_PACK = YES
7 | CLANG_WARN_STRICT_PROTOTYPES = YES
8 | CLANG_WARN_COMMA = YES
9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES
10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
12 | GCC_WARN_SHADOW = YES
13 | CLANG_WARN_UNREACHABLE_CODE = YES
14 |
--------------------------------------------------------------------------------
/app/macos/Runner/Configs/AppInfo.xcconfig:
--------------------------------------------------------------------------------
1 | // Application-level settings for the Runner target.
2 | //
3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
4 | // future. If not, the values below would default to using the project name when this becomes a
5 | // 'flutter create' template.
6 |
7 | // The application's name. By default this is also the title of the Flutter window.
8 | PRODUCT_NAME = budget
9 |
10 | // The application's bundle identifier
11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.budget
12 |
13 | // The copyright displayed in application information
14 | PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved.
15 |
--------------------------------------------------------------------------------
/plaid-sync/config/sandbox.example:
--------------------------------------------------------------------------------
1 | [PLAID]
2 | client_id = XXXXXXXXXXXXX
3 | secret = XXXXXXXXXXXXXX
4 | public_key = xXXXXXXXXXXXXXXX
5 | environment = sandbox
6 |
7 | influx_token = INFLUX_TOKEN
8 | influx_url = http://your-influx-url:8086
9 | influx_org = YOUR_ORG
10 | influx_bucket = YOUR_BUCKET
11 |
12 | notion_secret = secret_XXXX
13 | notion_database = DB_RULES_UUID
14 |
15 | [plaid-sync]
16 | dbfile = money.db
17 |
18 | ; account definitions will be added by plaid-sync
19 | ; when --link-account step is run
20 | ;
21 | ; but if you already have Plaid access tokens, you
22 | ; can add them as such:
23 | ;
24 | ; [Friendly Account Name]
25 | ; access_token = XXXXXXXXXXXX
26 | ; disabled=false
27 |
28 |
--------------------------------------------------------------------------------
/app/ios/.gitignore:
--------------------------------------------------------------------------------
1 | **/dgph
2 | *.mode1v3
3 | *.mode2v3
4 | *.moved-aside
5 | *.pbxuser
6 | *.perspectivev3
7 | **/*sync/
8 | .sconsign.dblite
9 | .tags*
10 | **/.vagrant/
11 | **/DerivedData/
12 | Icon?
13 | **/Pods/
14 | **/.symlinks/
15 | profile
16 | xcuserdata
17 | **/.generated/
18 | Flutter/App.framework
19 | Flutter/Flutter.framework
20 | Flutter/Flutter.podspec
21 | Flutter/Generated.xcconfig
22 | Flutter/ephemeral/
23 | Flutter/app.flx
24 | Flutter/app.zip
25 | Flutter/flutter_assets/
26 | Flutter/flutter_export_environment.sh
27 | ServiceDefinitions.json
28 | Runner/GeneratedPluginRegistrant.*
29 |
30 | # Exceptions to above rules.
31 | !default.mode1v3
32 | !default.mode2v3
33 | !default.pbxuser
34 | !default.perspectivev3
35 |
--------------------------------------------------------------------------------
/app/windows/runner/utils.h:
--------------------------------------------------------------------------------
1 | #ifndef RUNNER_UTILS_H_
2 | #define RUNNER_UTILS_H_
3 |
4 | #include
5 | #include
6 |
7 | // Creates a console for the process, and redirects stdout and stderr to
8 | // it for both the runner and the Flutter library.
9 | void CreateAndAttachConsole();
10 |
11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
12 | // encoded in UTF-8. Returns an empty std::string on failure.
13 | std::string Utf8FromUtf16(const wchar_t* utf16_string);
14 |
15 | // Gets the command line arguments passed in as a std::vector,
16 | // encoded in UTF-8. Returns an empty std::vector on failure.
17 | std::vector GetCommandLineArguments();
18 |
19 | #endif // RUNNER_UTILS_H_
20 |
--------------------------------------------------------------------------------
/app/lib/widgets/about_app_list_tile.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../globals.dart' as globals;
4 |
5 | class AboutAppListTile extends StatelessWidget {
6 | @override
7 | Widget build(BuildContext context) {
8 | return AboutListTile(
9 | icon: const Icon(Icons.info),
10 | child: const Text('About'),
11 | applicationIcon: Image.asset(
12 | 'assets/icon/launcher_icon.png',
13 | height: 50,
14 | width: 50,
15 | ),
16 | applicationLegalese: 'By Mikhail Zakharov',
17 | applicationVersion: globals.version,
18 | aboutBoxChildren: [
19 | const SizedBox(height: 12),
20 | const Text('A Budgeting app'),
21 | const SizedBox(height: 7),
22 | ],
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/linux/flutter/generated_plugins.cmake:
--------------------------------------------------------------------------------
1 | #
2 | # Generated file, do not edit.
3 | #
4 |
5 | list(APPEND FLUTTER_PLUGIN_LIST
6 | )
7 |
8 | list(APPEND FLUTTER_FFI_PLUGIN_LIST
9 | )
10 |
11 | set(PLUGIN_BUNDLED_LIBRARIES)
12 |
13 | foreach(plugin ${FLUTTER_PLUGIN_LIST})
14 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
15 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
16 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
18 | endforeach(plugin)
19 |
20 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
23 | endforeach(ffi_plugin)
24 |
--------------------------------------------------------------------------------
/app/windows/flutter/generated_plugins.cmake:
--------------------------------------------------------------------------------
1 | #
2 | # Generated file, do not edit.
3 | #
4 |
5 | list(APPEND FLUTTER_PLUGIN_LIST
6 | )
7 |
8 | list(APPEND FLUTTER_FFI_PLUGIN_LIST
9 | )
10 |
11 | set(PLUGIN_BUNDLED_LIBRARIES)
12 |
13 | foreach(plugin ${FLUTTER_PLUGIN_LIST})
14 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
15 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
16 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
18 | endforeach(plugin)
19 |
20 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
23 | endforeach(ffi_plugin)
24 |
--------------------------------------------------------------------------------
/app/android/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | val flutterSdkPath = run {
3 | val properties = java.util.Properties()
4 | file("local.properties").inputStream().use { properties.load(it) }
5 | val flutterSdkPath = properties.getProperty("flutter.sdk")
6 | require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
7 | flutterSdkPath
8 | }
9 |
10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
11 |
12 | repositories {
13 | google()
14 | mavenCentral()
15 | gradlePluginPortal()
16 | }
17 | }
18 |
19 | plugins {
20 | id("dev.flutter.flutter-plugin-loader") version "1.0.0"
21 | id("com.android.application") version "8.7.0" apply false
22 | id("org.jetbrains.kotlin.android") version "1.8.22" apply false
23 | }
24 |
25 | include(":app")
26 |
--------------------------------------------------------------------------------
/app/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 11.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/windows/runner/runner.exe.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PerMonitorV2
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 | migrate_working_dir/
12 |
13 | /pubspec.lock
14 | /.metadata
15 | secrets.dart
16 |
17 | # IntelliJ related
18 | *.iml
19 | *.ipr
20 | *.iws
21 | .idea/
22 |
23 | # The .vscode folder contains launch configuration and tasks you configure in
24 | # VS Code which you may wish to be included in version control, so this line
25 | # is commented out by default.
26 | #.vscode/
27 |
28 | # Flutter/Dart/Pub related
29 | **/doc/api/
30 | GeneratedPluginRegistrant.swift
31 | **/ios/Flutter/.last_build_id
32 | .dart_tool/
33 | .flutter-plugins
34 | .flutter-plugins-dependencies
35 | .packages
36 | .pub-cache/
37 | .pub/
38 | /build/
39 |
40 | # Symbolication related
41 | app.*.symbols
42 |
43 | # Obfuscation related
44 | app.*.map.json
45 |
46 | # Android Studio will place build artifacts here
47 | /android/app/debug
48 | /android/app/profile
49 | /android/app/release
50 |
--------------------------------------------------------------------------------
/app/web/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "budget",
3 | "short_name": "budget",
4 | "start_url": ".",
5 | "display": "standalone",
6 | "background_color": "#0175C2",
7 | "theme_color": "#0175C2",
8 | "description": "A new Flutter project.",
9 | "orientation": "portrait-primary",
10 | "prefer_related_applications": false,
11 | "icons": [
12 | {
13 | "src": "icons/Icon-192.png",
14 | "sizes": "192x192",
15 | "type": "image/png"
16 | },
17 | {
18 | "src": "icons/Icon-512.png",
19 | "sizes": "512x512",
20 | "type": "image/png"
21 | },
22 | {
23 | "src": "icons/Icon-maskable-192.png",
24 | "sizes": "192x192",
25 | "type": "image/png",
26 | "purpose": "maskable"
27 | },
28 | {
29 | "src": "icons/Icon-maskable-512.png",
30 | "sizes": "512x512",
31 | "type": "image/png",
32 | "purpose": "maskable"
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/app/windows/runner/flutter_window.h:
--------------------------------------------------------------------------------
1 | #ifndef RUNNER_FLUTTER_WINDOW_H_
2 | #define RUNNER_FLUTTER_WINDOW_H_
3 |
4 | #include
5 | #include
6 |
7 | #include
8 |
9 | #include "win32_window.h"
10 |
11 | // A window that does nothing but host a Flutter view.
12 | class FlutterWindow : public Win32Window {
13 | public:
14 | // Creates a new FlutterWindow hosting a Flutter view running |project|.
15 | explicit FlutterWindow(const flutter::DartProject& project);
16 | virtual ~FlutterWindow();
17 |
18 | protected:
19 | // Win32Window:
20 | bool OnCreate() override;
21 | void OnDestroy() override;
22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
23 | LPARAM const lparam) noexcept override;
24 |
25 | private:
26 | // The project to run.
27 | flutter::DartProject project_;
28 |
29 | // The Flutter instance hosted by this window.
30 | std::unique_ptr flutter_controller_;
31 | };
32 |
33 | #endif // RUNNER_FLUTTER_WINDOW_H_
34 |
--------------------------------------------------------------------------------
/app/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/app/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/app/README.md:
--------------------------------------------------------------------------------
1 | # budget flutter app
2 |
3 | Setup:
4 |
5 | - create a `secrets.dart` file inside `lib` folder with the following contents
6 |
7 | ```
8 | String influx_url = "http://your-influx-url.com:8086";
9 | String influx_org = "some_org";
10 | String influx_token = "token";
11 | String influx_bucket = "bucket";
12 |
13 | String notion_secret = "secret_XXX";
14 | String notion_database = "DB_BUDGETS_UUID";
15 |
16 | ```
17 |
18 | - [Install](https://docs.flutter.dev/get-started/install) flutter.
19 | - run `flutter doctor -v` . Tested environment:
20 |
21 | ```
22 | [√] Flutter (Channel stable, 3.29.3)
23 | • Engine revision cf56914b32
24 | • Dart version 3.7.2
25 | • DevTools version 2.42.3
26 |
27 | [√] Android Studio (version 2024.3) [28ms]
28 | • Java version OpenJDK Runtime Environment (build 21.0.6+-13355223-b631.42)
29 | ```
30 |
31 | Build Targets:
32 |
33 | - Android Debug: Connect Phone via USB, with USB Debugging enabled: `flutter run`
34 | - Android release: `flutter build apk --release`
35 | - Windows: `flutter run -d windows`
36 |
37 | Inspired by https://github.com/rsquared226/budget_my_life
38 |
--------------------------------------------------------------------------------
/plaid-sync/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Matthew Bafford
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/app/lib/widgets/balance_cards_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:provider/provider.dart';
3 | import './balance_summary_card.dart';
4 | import '../providers/transactions.dart';
5 | import '../utils/custom_colors.dart';
6 |
7 | // Used in DashboardScreen.
8 |
9 | class BalanceCardsView extends StatefulWidget {
10 | @override
11 | _BalanceCardsViewState createState() => _BalanceCardsViewState();
12 | }
13 |
14 | class _BalanceCardsViewState extends State {
15 | @override
16 | Widget build(BuildContext context) {
17 | final transactionsData = Provider.of(context);
18 | final themeData = Theme.of(context);
19 |
20 | return Container(
21 | color: themeData.colorScheme.dashboardHeader(context),
22 | child: Column(
23 | children: [
24 | SizedBox(
25 | // This is the height of the BalanceSummaryCard.
26 | height: 136,
27 | child: BalanceSummaryCard(
28 | title: 'Net Balance',
29 | balance: transactionsData.balance,
30 | ),
31 | )
32 | ],
33 | ),
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/lib/utils/custom_icons_icons.dart:
--------------------------------------------------------------------------------
1 | /// Flutter icons CustomIcons
2 | /// Copyright (C) 2020 by original authors @ fluttericon.com, fontello.com
3 | /// This font was generated by FlutterIcon.com, which is derived from Fontello.
4 | ///
5 | /// To use this font, place it in your fonts/ directory and include the
6 | /// following in your pubspec.yaml
7 | ///
8 | /// flutter:
9 | /// fonts:
10 | /// - family: CustomIcons
11 | /// fonts:
12 | /// - asset: fonts/CustomIcons.ttf
13 | ///
14 | ///
15 | /// * Font Awesome 5, Copyright (C) 2016 by Dave Gandy
16 | /// Author: Dave Gandy
17 | /// License: SIL (https://github.com/FortAwesome/Font-Awesome/blob/master/LICENSE.txt)
18 | /// Homepage: http://fortawesome.github.com/Font-Awesome/
19 | ///
20 | import 'package:flutter/widgets.dart';
21 |
22 | class CustomIcons {
23 | CustomIcons._();
24 |
25 | static const _kFontFam = 'CustomIcons';
26 | static const dynamic _kFontPkg = null;
27 |
28 | static const IconData linkedin = IconData(0xf08c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
29 | static const IconData github = IconData(0xf09b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
30 | }
31 |
--------------------------------------------------------------------------------
/app/macos/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(FLUTTER_BUILD_NAME)
21 | CFBundleVersion
22 | $(FLUTTER_BUILD_NUMBER)
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSHumanReadableCopyright
26 | $(PRODUCT_COPYRIGHT)
27 | NSMainNibFile
28 | MainMenu
29 | NSPrincipalClass
30 | NSApplication
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/lib/widgets/dashboard_screen_fab.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:animations/animations.dart';
3 |
4 | import '../screens/edit_transaction_screen.dart';
5 |
6 | // Used in DashboardScreen
7 |
8 | const _fabDimension = 56.0;
9 |
10 | class DashboardScreenFAB extends StatelessWidget {
11 | @override
12 | Widget build(BuildContext context) {
13 | return OpenContainer(
14 | closedElevation: 6.0,
15 | closedShape: const RoundedRectangleBorder(
16 | borderRadius: BorderRadius.all(
17 | Radius.circular(_fabDimension / 2),
18 | ),
19 | ),
20 | closedColor: Theme.of(context).primaryColor,
21 | closedBuilder: (context, _) {
22 | return SizedBox(
23 | height: _fabDimension,
24 | width: _fabDimension,
25 | child: Center(
26 | child: Icon(
27 | Icons.add,
28 | color: Theme.of(context).colorScheme.onPrimary,
29 | ),
30 | ),
31 | );
32 | },
33 | openBuilder: (_, closeContainer) {
34 | return EditTransactionScreen(
35 | closeContainer: closeContainer,
36 | );
37 | },
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/windows/runner/main.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | #include "flutter_window.h"
6 | #include "utils.h"
7 |
8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
9 | _In_ wchar_t *command_line, _In_ int show_command) {
10 | // Attach to console when present (e.g., 'flutter run') or create a
11 | // new console when running with a debugger.
12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
13 | CreateAndAttachConsole();
14 | }
15 |
16 | // Initialize COM, so that it is available for use in the library and/or
17 | // plugins.
18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
19 |
20 | flutter::DartProject project(L"data");
21 |
22 | std::vector command_line_arguments =
23 | GetCommandLineArguments();
24 |
25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
26 |
27 | FlutterWindow window(project);
28 | Win32Window::Point origin(10, 10);
29 | Win32Window::Size size(1280, 720);
30 | if (!window.Create(L"budget", origin, size)) {
31 | return EXIT_FAILURE;
32 | }
33 | window.SetQuitOnClose(true);
34 |
35 | ::MSG msg;
36 | while (::GetMessage(&msg, nullptr, 0, 0)) {
37 | ::TranslateMessage(&msg);
38 | ::DispatchMessage(&msg);
39 | }
40 |
41 | ::CoUninitialize();
42 | return EXIT_SUCCESS;
43 | }
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LibreBudgeteer
2 | An open source Budget Flutter app
3 |
4 | - mint.com style budgeting
5 | - custom transaction sync rules (substring matching, filtering, re-categorization)
6 | - multiple users (couples budgeting)
7 | - transaction and balances data is synced to your local InfluxDB database
8 | - transaction rules and budget categories are hosted in notion.so
9 | - App tested with Android/Windows/Linux
10 |
11 |
12 |
13 | # Setup
14 |
15 | - Setup transaction rules page/database in notion.so. [example](https://github.com/mzakharo/librebudgeteer/blob/main/images/rules.PNG)
16 | - Setup budgets in another notion.so page/database [example](https://github.com/mzakharo/librebudgeteer/blob/main/images/budgets.png)
17 | - Setup InfluxDB 2.0 database: [Installation Instructions](https://docs.influxdata.com/influxdb/v2/install/?t=Docker)
18 | - Upload some transactions to InfluxDB. Examples in `tangerine` (csv import) or `wealthica` (G-Sheets)
19 | - Build and run the [app](https://github.com/mzakharo/librebudgeteer/tree/main/app)
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/lib/screens/dashboard_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:provider/provider.dart';
3 |
4 | import '../providers/filter.dart';
5 | import '../widgets/balance_cards_view.dart';
6 | import '../widgets/dashboard_list_header.dart';
7 | import '../widgets/overview.dart';
8 | import '../widgets/transactions_list.dart';
9 |
10 | // This screen is a tab under home_screen.
11 | class DashboardScreen extends StatelessWidget {
12 | SliverList buildSliverBalanceCard() {
13 | return SliverList(
14 | delegate: SliverChildListDelegate(
15 | [
16 | BalanceCardsView(),
17 | ],
18 | ),
19 | );
20 | }
21 |
22 | SliverList buildSliverOverview() {
23 | return SliverList(
24 | delegate: SliverChildListDelegate(
25 | [
26 | Overview(),
27 | ],
28 | ),
29 | );
30 | }
31 |
32 | @override
33 | Widget build(BuildContext context) {
34 | return Scaffold(
35 | //floatingActionButton: DashboardScreenFAB(),
36 | body: ChangeNotifierProvider(
37 | create: (_) => Filter(),
38 | // Fetch and set data in this screen because it is the first screen the user sees.
39 | child: CustomScrollView(
40 | slivers: [
41 | buildSliverBalanceCard(),
42 | buildSliverOverview(),
43 | DashboardListHeader(),
44 | TransactionsList(),
45 | ],
46 | ),
47 | ),
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | #include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at
17 | # https://dart-lang.github.io/linter/lints/index.html.
18 | #
19 | # Instead of disabling a lint rule for the entire project in the
20 | # section below, it can also be suppressed for a single line of code
21 | # or a specific dart file by using the `// ignore: name_of_lint` and
22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
23 | # producing the lint.
24 | rules:
25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
27 |
28 | # Additional information about this file can be found at
29 | # https://dart.dev/guides/language/analysis-options
30 |
--------------------------------------------------------------------------------
/app/android/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("kotlin-android")
4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
5 | id("dev.flutter.flutter-gradle-plugin")
6 | }
7 |
8 | android {
9 | namespace = "com.example.budget"
10 | compileSdk = flutter.compileSdkVersion
11 | ndkVersion = flutter.ndkVersion
12 |
13 | compileOptions {
14 | sourceCompatibility = JavaVersion.VERSION_11
15 | targetCompatibility = JavaVersion.VERSION_11
16 | }
17 |
18 | kotlinOptions {
19 | jvmTarget = JavaVersion.VERSION_11.toString()
20 | }
21 |
22 | defaultConfig {
23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
24 | applicationId = "com.example.budget"
25 | // You can update the following values to match your application needs.
26 | // For more information, see: https://flutter.dev/to/review-gradle-config.
27 | minSdk = flutter.minSdkVersion
28 | targetSdk = flutter.targetSdkVersion
29 | versionCode = flutter.versionCode
30 | versionName = flutter.versionName
31 | }
32 |
33 | buildTypes {
34 | release {
35 | // TODO: Add your own signing config for the release build.
36 | // Signing with the debug keys for now, so `flutter run --release` works.
37 | signingConfig = signingConfigs.getByName("debug")
38 | }
39 | }
40 | }
41 |
42 | flutter {
43 | source = "../.."
44 | }
45 |
--------------------------------------------------------------------------------
/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "app_icon_16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "app_icon_32.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "app_icon_32.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "app_icon_64.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "app_icon_128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "app_icon_256.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "app_icon_256.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "app_icon_512.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "app_icon_512.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "app_icon_1024.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/lib/card_items/edit_labels_card.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../models/mylabel.dart';
4 |
5 | // Used in EditLabelsScreen.
6 |
7 | class EditLabelsCard extends StatelessWidget {
8 | final MyLabel label;
9 | final Function editTransaction;
10 | final Function deleteTransaction;
11 |
12 | const EditLabelsCard({
13 | required this.label,
14 | required this.editTransaction,
15 | required this.deleteTransaction,
16 | });
17 |
18 | @override
19 | Widget build(BuildContext context) {
20 | return Card(
21 | margin: const EdgeInsets.symmetric(horizontal: 0, vertical: 1),
22 | elevation: 0,
23 | child: ListTile(
24 | leading: Align(
25 | widthFactor: 1,
26 | child: CircleAvatar(
27 | maxRadius: 12,
28 | backgroundColor: label.color,
29 | ),
30 | ),
31 | title: Text(label.title),
32 | trailing: Row(
33 | // Need to have this or row will take up entire width of card.
34 | mainAxisSize: MainAxisSize.min,
35 | children: [
36 | IconButton(
37 | icon: const Icon(
38 | Icons.edit,
39 | color: Colors.blue,
40 | ),
41 | onPressed: editTransaction as void Function()?,
42 | ),
43 | IconButton(
44 | icon: const Icon(
45 | Icons.delete,
46 | color: Colors.red,
47 | ),
48 | onPressed: deleteTransaction as void Function()?,
49 | ),
50 | ],
51 | ),
52 | ),
53 | );
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/lib/widgets/color_picker_form_field.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_colorpicker/flutter_colorpicker.dart';
3 |
4 | // Used in EditLabelScreen.
5 |
6 | class ColorPickerFormField extends FormField {
7 | static void _showColorPickerDialog(
8 | BuildContext context,
9 | Color? currentColor,
10 | Function onColorChanged,
11 | ) {
12 | showDialog(
13 | context: context,
14 | builder: (BuildContext context) {
15 | return AlertDialog(
16 | title: const Text('Pick a label color'),
17 | content: SingleChildScrollView(
18 | child: BlockPicker(
19 | pickerColor: currentColor!,
20 | onColorChanged: onColorChanged as void Function(Color),
21 | ),
22 | ),
23 | actions: [
24 | TextButton(
25 | onPressed: () => Navigator.pop(context),
26 | child: const Text('SELECT'),
27 | ),
28 | ],
29 | );
30 | },
31 | );
32 | }
33 |
34 | ColorPickerFormField({
35 | FormFieldSetter? onSaved,
36 | Color? initalValue = Colors.indigo,
37 | double maxRadius = 14,
38 | }) : super(
39 | onSaved: onSaved,
40 | initialValue: initalValue,
41 | builder: (state) {
42 | return GestureDetector(
43 | onTap: () {
44 | _showColorPickerDialog(
45 | state.context,
46 | state.value,
47 | (Color selectedColor) {
48 | state.didChange(selectedColor);
49 | },
50 | );
51 | },
52 | child: CircleAvatar(
53 | maxRadius: maxRadius,
54 | backgroundColor: state.value,
55 | ),
56 | );
57 | },
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/app/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | Budget
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | budget
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(FLUTTER_BUILD_NAME)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(FLUTTER_BUILD_NUMBER)
25 | LSRequiresIPhoneOS
26 |
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIMainStoryboardFile
30 | Main
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 | UIViewControllerBasedStatusBarAppearance
45 |
46 | CADisableMinimumFrameDurationOnPhone
47 |
48 | UIApplicationSupportsIndirectInputEvents
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/app/lib/models/mylabel.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:provider/provider.dart';
3 |
4 | import '../providers/insights_range.dart';
5 | import '../providers/transactions.dart';
6 |
7 | enum LabelType { INCOME, EXPENSE }
8 |
9 | class MyLabel {
10 | final String id;
11 | final String title;
12 | final Color color;
13 | final LabelType labelType;
14 |
15 | const MyLabel({
16 | required this.id,
17 | required this.title,
18 | required this.color,
19 | required this.labelType,
20 | });
21 |
22 | Map toMap() {
23 | return {
24 | 'id': id,
25 | 'title': title,
26 | 'color': color.value,
27 | 'labelType': labelType.index,
28 | };
29 | }
30 |
31 | static MyLabel fromMap(Map map) {
32 | return MyLabel(
33 | id: map['id'],
34 | title: map['title'],
35 | color: Color(map['color']),
36 | labelType: LabelType.values[map['labelType']],
37 | );
38 | }
39 |
40 | double getLabelAmountTotal(BuildContext context) => getLabelTotalWithRange(context, Range.lifetime);
41 |
42 | // From the beginning of the month.
43 | double getLabelMonthAmountTotal(BuildContext context) => getLabelTotalWithRange(context, Range.month);
44 | double getLabelPreviousMonthAmountTotal(BuildContext context) => getLabelTotalWithRange(context, Range.previousMonth);
45 | double getLabelPrevPreviousMonthAmountTotal(BuildContext context) => getLabelTotalWithRange(context, Range.prevpreviousMonth);
46 |
47 | double getLabelTotalWithRange(BuildContext context, Range range) {
48 | final transactionsData = Provider.of(context, listen: false);
49 | final labelTransactionsWithRange = transactionsData.filterTransactionsByLabelAndRange(context, id, range);
50 |
51 | return labelTransactionsWithRange.fold(
52 | 0,
53 | (previousValue, transaction) => previousValue + transaction.amount,
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/windows/runner/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.14)
2 | project(runner LANGUAGES CXX)
3 |
4 | # Define the application target. To change its name, change BINARY_NAME in the
5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
6 | # work.
7 | #
8 | # Any new source files that you add to the application should be added here.
9 | add_executable(${BINARY_NAME} WIN32
10 | "flutter_window.cpp"
11 | "main.cpp"
12 | "utils.cpp"
13 | "win32_window.cpp"
14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
15 | "Runner.rc"
16 | "runner.exe.manifest"
17 | )
18 |
19 | # Apply the standard set of build settings. This can be removed for applications
20 | # that need different build settings.
21 | apply_standard_settings(${BINARY_NAME})
22 |
23 | # Add preprocessor definitions for the build version.
24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
29 |
30 | # Disable Windows macros that collide with C++ standard library functions.
31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
32 |
33 | # Add dependency libraries and include directories. Add any application-specific
34 | # dependencies here.
35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
38 |
39 | # Run the Flutter tool portions of the build. This must not be removed.
40 | add_dependencies(${BINARY_NAME} flutter_assemble)
41 |
--------------------------------------------------------------------------------
/app/windows/runner/utils.cpp:
--------------------------------------------------------------------------------
1 | #include "utils.h"
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 |
8 | #include
9 |
10 | void CreateAndAttachConsole() {
11 | if (::AllocConsole()) {
12 | FILE *unused;
13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
14 | _dup2(_fileno(stdout), 1);
15 | }
16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
17 | _dup2(_fileno(stdout), 2);
18 | }
19 | std::ios::sync_with_stdio();
20 | FlutterDesktopResyncOutputStreams();
21 | }
22 | }
23 |
24 | std::vector GetCommandLineArguments() {
25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
26 | int argc;
27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
28 | if (argv == nullptr) {
29 | return std::vector();
30 | }
31 |
32 | std::vector command_line_arguments;
33 |
34 | // Skip the first argument as it's the binary name.
35 | for (int i = 1; i < argc; i++) {
36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
37 | }
38 |
39 | ::LocalFree(argv);
40 |
41 | return command_line_arguments;
42 | }
43 |
44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) {
45 | if (utf16_string == nullptr) {
46 | return std::string();
47 | }
48 | int target_length = ::WideCharToMultiByte(
49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
50 | -1, nullptr, 0, nullptr, nullptr)
51 | -1; // remove the trailing null character
52 | int input_length = (int)wcslen(utf16_string);
53 | std::string utf8_string;
54 | if (target_length <= 0 || target_length > utf8_string.max_size()) {
55 | return utf8_string;
56 | }
57 | utf8_string.resize(target_length);
58 | int converted_length = ::WideCharToMultiByte(
59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
60 | input_length, utf8_string.data(), target_length, nullptr, nullptr);
61 | if (converted_length == 0) {
62 | return std::string();
63 | }
64 | return utf8_string;
65 | }
66 |
--------------------------------------------------------------------------------
/app/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | budget
33 |
34 |
35 |
39 |
40 |
41 |
42 |
43 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/app/lib/charts/charts_base/pie_chart_base.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:charts_flutter/flutter.dart' as charts;
3 |
4 | import '../chart_models/pie_chart_model.dart';
5 |
6 | // This is used in TransactionDetailsScreen.
7 |
8 | class PieChartBase extends StatelessWidget {
9 | final String id;
10 | final bool animated;
11 | final List pieData;
12 | final bool showArcLabels;
13 | final charts.ArcLabelPosition arcLabelPosition;
14 | final List>? behaviors;
15 |
16 | const PieChartBase({
17 | required this.id,
18 | required this.animated,
19 | required this.pieData,
20 | required this.showArcLabels,
21 | this.arcLabelPosition = charts.ArcLabelPosition.auto,
22 | this.behaviors,
23 | });
24 |
25 | List> get chartData {
26 | return [
27 | charts.Series(
28 | id: id,
29 | domainFn: (PieChartModel data, _) => data.label,
30 | measureFn: (PieChartModel data, _) => data.amount,
31 | colorFn: (PieChartModel data, _) => data.color,
32 | data: pieData,
33 | ),
34 | ];
35 | }
36 |
37 | @override
38 | Widget build(BuildContext context) {
39 | return charts.PieChart(
40 | chartData,
41 | animate: animated,
42 | behaviors: behaviors,
43 | // if defaultRenderer is null, no arc labels will show.
44 | defaultRenderer: showArcLabels
45 | ? charts.ArcRendererConfig(
46 | arcRendererDecorators: [
47 | charts.ArcLabelDecorator(
48 | labelPosition: arcLabelPosition,
49 | outsideLabelStyleSpec: charts.TextStyleSpec(
50 | fontSize: 12,
51 | // Make sure labels show up on dark mode too.
52 | color: Theme.of(context).brightness == Brightness.light
53 | ? charts.Color.black
54 | : charts.Color.white,
55 | ),
56 | ),
57 | ],
58 | )
59 | : null,
60 | );
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/lib/charts/chart_widgets/transaction_details_pie_chart.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:charts_flutter/flutter.dart' as charts;
3 |
4 | import '../charts_base/pie_chart_base.dart';
5 | import '../chart_models/pie_chart_model.dart';
6 |
7 | // This is used in TransactionDetailsChartView.
8 |
9 | class TransactionDetailsPieChart extends StatelessWidget {
10 | final String chartTitle;
11 | final String transactionTitle;
12 | final String otherTitle;
13 | final double transactionAmount;
14 | final double totalAmount;
15 | final Color mainColor;
16 | final Color otherColor;
17 | final double height;
18 |
19 | const TransactionDetailsPieChart({
20 | required this.chartTitle,
21 | required this.transactionTitle,
22 | required this.otherTitle,
23 | required this.transactionAmount,
24 | required this.totalAmount,
25 | required this.mainColor,
26 | required this.otherColor,
27 | this.height = 250,
28 | });
29 |
30 | @override
31 | Widget build(BuildContext context) {
32 | return Column(
33 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
34 | children: [
35 | // Just in case the text overflows.
36 | FittedBox(
37 | child: Text(
38 | chartTitle,
39 | style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 22),
40 | ),
41 | ),
42 | SizedBox(
43 | height: height,
44 | child: PieChartBase(
45 | id: transactionTitle,
46 | animated: false,
47 | showArcLabels: true,
48 | arcLabelPosition: charts.ArcLabelPosition.outside,
49 | pieData: [
50 | PieChartModel(
51 | label: transactionTitle,
52 | amount: transactionAmount,
53 | color: mainColor,
54 | ),
55 | PieChartModel(
56 | label: otherTitle,
57 | amount: totalAmount - transactionAmount,
58 | color: otherColor,
59 | ),
60 | ],
61 | ),
62 | ),
63 | ],
64 | );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/lib/screens/home_tabs_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:animations/animations.dart';
3 |
4 | import '../providers/labels.dart';
5 | import '../providers/transactions.dart';
6 | import '../providers/settings.dart';
7 | import '../widgets/app_drawer.dart';
8 | import 'insights_screen.dart';
9 | import 'budget_screen.dart';
10 | import './dashboard_screen.dart';
11 | import '../widgets/home_tabs_bar.dart';
12 | import 'package:provider/provider.dart';
13 |
14 | class HomeTabsScreen extends StatefulWidget {
15 | @override
16 | _HomeTabsScreenState createState() => _HomeTabsScreenState();
17 | }
18 |
19 | class _HomeTabsScreenState extends State {
20 | int _pageIndex = 0;
21 |
22 | final List _pageList = [
23 | DashboardScreen(),
24 | BudgetScreen(),
25 | InsightsScreen(),
26 | ];
27 |
28 | @override
29 | Widget build(BuildContext context) {
30 | return Scaffold(
31 | appBar: AppBar(
32 | title: const Text('LibreBudgeteer'),
33 | ),
34 | drawer: AppDrawer(),
35 | body: RefreshIndicator(
36 | child: PageTransitionSwitcher(
37 | transitionBuilder: (child, primaryAnimation, secondaryAnimation) {
38 | return FadeThroughTransition(
39 | animation: primaryAnimation,
40 | secondaryAnimation: secondaryAnimation,
41 | child: child,
42 | );
43 | },
44 | child: _pageList[_pageIndex],
45 | ),
46 | onRefresh: () {
47 | print("fetch data");
48 | return Future.wait([
49 | Provider.of(context, listen: false).fetchAndSetSettings(),
50 | Provider.of(context, listen: false).fetchAndSetLabels(),
51 | Provider.of(context, listen: false).fetchAndSetTransactions(),
52 | ]);
53 | }),
54 | bottomNavigationBar: HomeTabsBar(
55 | pageIndex: _pageIndex,
56 | onPressed: (newPageIndex) {
57 | setState(() {
58 | _pageIndex = newPageIndex;
59 | });
60 | },
61 | ),
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/lib/widgets/chart_container.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../widgets/insights_range_buttons.dart';
4 |
5 | // Used in InsightsScreen.
6 |
7 | // TODO: Make chart range change animated so it's less jarring.
8 |
9 | class ChartContainer extends StatelessWidget {
10 | final String title;
11 | final Widget chart;
12 | final Color? backgroundColor;
13 | final bool isTransactionHistoryChart;
14 |
15 | const ChartContainer({
16 | required this.title,
17 | required this.chart,
18 | required this.backgroundColor,
19 | this.isTransactionHistoryChart = false,
20 | });
21 |
22 | // In light mode, the background is backgroundColor. In dark mode, the
23 | // background is the canvas color and the border is backgroundColor.
24 |
25 | @override
26 | Widget build(BuildContext context) {
27 | return Container(
28 | color: Theme.of(context).brightness == Brightness.light
29 | ? backgroundColor
30 | : Theme.of(context).canvasColor,
31 | child: Column(
32 | children: [
33 | const Spacer(flex: 4),
34 | Text(
35 | title,
36 | style: TextStyle(
37 | color: Colors.white,
38 | fontSize: 30,
39 | fontFamily: 'Roboto', // Replace GoogleFonts with built-in font
40 | ),
41 | ),
42 | const Spacer(flex: 3),
43 | Container(
44 | // Uneven because room is needed for ScrollingPageIndicator.
45 | margin: const EdgeInsets.only(left: 18, right: 25),
46 | height: 465,
47 | decoration: BoxDecoration(
48 | color: Theme.of(context).colorScheme.surface,
49 | borderRadius: BorderRadius.circular(13),
50 | border: Theme.of(context).brightness == Brightness.light
51 | ? null
52 | : Border.all(color: backgroundColor!, width: 5),
53 | ),
54 | child: chart,
55 | ),
56 | const Spacer(flex: 3),
57 | InsightsRangeButtons(
58 | isTransactionHistoryChart: isTransactionHistoryChart,
59 | ),
60 | const Spacer(flex: 4),
61 | ],
62 | ),
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/app/lib/widgets/transaction_details_appbar.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../utils/custom_colors.dart';
4 |
5 | // This is used in TransactionDetailsScreen.
6 |
7 | class TransactionDetailsAppBar extends StatelessWidget {
8 | final double? transactionAmount;
9 | final String formattedAmount;
10 | final Function editTransaction;
11 | final Function deleteTransaction;
12 |
13 | const TransactionDetailsAppBar({
14 | required this.transactionAmount,
15 | required this.formattedAmount,
16 | required this.editTransaction,
17 | required this.deleteTransaction,
18 | });
19 |
20 | @override
21 | Widget build(BuildContext context) {
22 | final appTheme = Theme.of(context);
23 |
24 | return SliverAppBar(
25 | expandedHeight: 150,
26 | pinned: true,
27 | backgroundColor: appTheme.colorScheme.surface,
28 | // This makes the status bar icons grey in light mode and white in dark
29 | // mode so the user's status bar is readable.
30 | //brightness: Theme.of(context).brightness,
31 | iconTheme: IconThemeData(color: appTheme.colorScheme.onSurface),
32 | actions: [
33 | IconButton(
34 | icon: Icon(
35 | Icons.edit,
36 | color: Colors.blue.shade800,
37 | ),
38 | onPressed: editTransaction as void Function()?,
39 | ),
40 | IconButton(
41 | icon: Icon(
42 | Icons.delete,
43 | color: Colors.red.shade900,
44 | ),
45 | onPressed: deleteTransaction as void Function()?,
46 | ),
47 | ],
48 | flexibleSpace: FlexibleSpaceBar(
49 | centerTitle: true,
50 | title: Container(
51 | padding: const EdgeInsets.all(6),
52 | decoration: BoxDecoration(
53 | color: Theme.of(context)
54 | .colorScheme
55 | .transactionTypeColor(transactionAmount),
56 | borderRadius: BorderRadius.circular(4),
57 | ),
58 | child: Text(
59 | formattedAmount,
60 | style: TextStyle(
61 | color: Theme.of(context).colorScheme.onIncomeExpenseColor,
62 | ),
63 | ),
64 | ),
65 | ),
66 | );
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/lib/charts/charts_base/custom_circle_symbol_renderer.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 | import 'package:charts_flutter/flutter.dart';
3 | import 'package:charts_flutter/src/text_style.dart' as style;
4 | import 'package:charts_flutter/src/text_element.dart' as element;
5 | import 'package:flutter/material.dart';
6 |
7 | typedef GetText = String Function();
8 |
9 | class CustomCircleSymbolRenderer extends CircleSymbolRenderer {
10 | final GetText getText;
11 | final EdgeInsets padding;
12 | final double marginBottom;
13 | CustomCircleSymbolRenderer(this.getText, {this.marginBottom = 8, this.padding = const EdgeInsets.all(8)});
14 | @override
15 | void paint(ChartCanvas canvas, Rectangle bounds,
16 | {List? dashPattern, Color? fillColor, FillPatternType? fillPattern, Color? strokeColor, double? strokeWidthPx}) {
17 | super.paint(canvas, bounds, dashPattern: dashPattern, fillColor: fillColor, strokeColor: strokeColor, strokeWidthPx: strokeWidthPx);
18 | canvas.drawRect(Rectangle(bounds.left - 5, bounds.top - 30, bounds.width + 10, bounds.height + 10), fill: Color.white);
19 | var textStyle = style.TextStyle();
20 | textStyle.color = Color.black;
21 | textStyle.fontSize = 15;
22 | element.TextElement textElement = element.TextElement(getText.call(), style: textStyle);
23 |
24 | //canvas.drawText(textElement, (bounds.left).round(), (bounds.top - 28).round());
25 |
26 | double width = textElement.measurement.horizontalSliceWidth;
27 | double height = textElement.measurement.verticalSliceWidth;
28 |
29 | double centerX = bounds.left + bounds.width / 2;
30 | double centerY = bounds.top + bounds.height / 2 - marginBottom - (padding.top + padding.bottom);
31 |
32 | canvas.drawRRect(
33 | Rectangle(
34 | centerX - (width / 2) - padding.left,
35 | centerY - (height / 2) - padding.top,
36 | width + (padding.left + padding.right),
37 | height + (padding.top + padding.bottom),
38 | ),
39 | fill: Color.white,
40 | radius: 16,
41 | roundTopLeft: true,
42 | roundTopRight: true,
43 | roundBottomRight: true,
44 | roundBottomLeft: true,
45 | );
46 | canvas.drawText(
47 | textElement,
48 | (centerX - (width / 2)).round(),
49 | (centerY - (height / 2)).round(),
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/lib/widgets/app_drawer.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../screens/edit_labels_screen.dart';
4 | import '../screens/settings_screen.dart';
5 | import './about_app_list_tile.dart';
6 |
7 | class AppDrawer extends StatelessWidget {
8 | @override
9 | Widget build(BuildContext context) {
10 | return Drawer(
11 | child: Column(
12 | children: [
13 | Container(
14 | height: 200,
15 | color: Theme.of(context).primaryColor,
16 | // SafeArea so it doesn't take the notification bar in account for centering vertically.
17 | child: SafeArea(
18 | child: Center(
19 | child: Text(
20 | 'Budget your life with ease.',
21 | style: TextStyle(
22 | color: Theme.of(context).colorScheme.onPrimary,
23 | fontSize: 15,
24 | ),
25 | ),
26 | ),
27 | ),
28 | ),
29 | ListTile(
30 | leading: const Icon(Icons.attach_money),
31 | title: const Text('Dashboard'),
32 | onTap: () => Navigator.of(context).pushReplacementNamed(
33 | '/',
34 | ),
35 | ),
36 | ListTile(
37 | leading: const Icon(Icons.edit),
38 | title: const Text('Edit Labels'),
39 | onTap: () => Navigator.of(context).pushReplacementNamed(
40 | EditLabelsScreen.routeName,
41 | ),
42 | ),
43 | Divider(),
44 | ListTile(
45 | leading: const Icon(Icons.settings),
46 | title: const Text('Settings'),
47 | onTap: () => Navigator.of(context).pushReplacementNamed(
48 | SettingsScreen.routeName,
49 | ),
50 | ),
51 | // ListTile(
52 | // leading: const Icon(Icons.help),
53 | // title: const Text('Help'),
54 | // onTap: () => Navigator.of(context).push(
55 | // MaterialPageRoute(
56 | // builder: (_) => Onboarding(openedFromDrawer: true),
57 | // ),
58 | // ),
59 | // ),
60 | Divider(),
61 | AboutAppListTile(),
62 | ],
63 | ),
64 | );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/lib/widgets/dashboard_list_header.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../utils/custom_colors.dart';
4 | import '../widgets/label_filter_dropdown.dart';
5 |
6 | // Used in DashboardScreen. Contains History text and dropdown filter.
7 |
8 | class DashboardListHeader extends StatelessWidget {
9 | @override
10 | Widget build(BuildContext context) {
11 | final themeData = Theme.of(context);
12 | return SliverPersistentHeader(
13 | pinned: true,
14 | delegate: SectionHeaderDelegate(
15 | child: Container(
16 | // Need to specify the color or it'll be transparent.
17 | color: themeData.colorScheme.dashboardHeader(context),
18 | child: Column(
19 | children: [
20 | const SizedBox(height: 5),
21 | Padding(
22 | padding: const EdgeInsets.symmetric(horizontal: 20),
23 | child: Row(
24 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
25 | children: [
26 | Text(
27 | 'History',
28 | style: TextStyle(
29 | fontSize: 16,
30 | color: themeData.brightness == Brightness.light ? Colors.black54 : Colors.white54,
31 | ),
32 | ),
33 | LabelFilterDropdown(),
34 | ],
35 | ),
36 | ),
37 | const SizedBox(height: 5),
38 | // Psuedo-shadow.
39 | const Divider(
40 | height: 0,
41 | thickness: 1.5,
42 | ),
43 | ],
44 | ),
45 | ),
46 | ),
47 | );
48 | }
49 | }
50 |
51 | class SectionHeaderDelegate extends SliverPersistentHeaderDelegate {
52 | final Widget child;
53 | final double height;
54 |
55 | const SectionHeaderDelegate({
56 | required this.child,
57 | // This is the height of the child.
58 | this.height = 58,
59 | });
60 |
61 | @override
62 | Widget build(BuildContext context, _, __) {
63 | return child;
64 | }
65 |
66 | @override
67 | double get maxExtent => height;
68 |
69 | @override
70 | double get minExtent => height;
71 |
72 | @override
73 | bool shouldRebuild(_) => false;
74 | }
75 |
--------------------------------------------------------------------------------
/app/lib/widgets/label_filter_dropdown.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:provider/provider.dart';
3 |
4 | import '../providers/filter.dart';
5 | import '../providers/labels.dart';
6 |
7 | // Used in DashboardListHeader.
8 |
9 | class LabelFilterDropdown extends StatelessWidget {
10 | Widget buildFilterLabelCard(Color? color, String title) {
11 | return Row(
12 | children: [
13 | CircleAvatar(
14 | maxRadius: 10,
15 | backgroundColor: color,
16 | ),
17 | const SizedBox(width: 10),
18 | //Text(title),
19 |
20 | Container(
21 | //Here you can control the width of your container ..
22 | //when text exceeds it will be trancated via elipses...
23 | width: 210.0,
24 | child: Text(
25 | title,
26 | softWrap: false,
27 | overflow: TextOverflow.ellipsis,
28 | )),
29 | ],
30 | );
31 | }
32 |
33 | @override
34 | Widget build(BuildContext context) {
35 | final labels = Provider.of(context).items;
36 | final filterData = Provider.of(context);
37 |
38 | return DropdownButton(
39 | icon: const Icon(Icons.filter_list),
40 | value: filterData.labelId,
41 | // So it's easy to tell when the list is actually filtering.
42 | underline: Container(
43 | height: filterData.labelId == null ? 1 : 2,
44 | color: Colors.grey[300],
45 | ),
46 | items: [
47 | DropdownMenuItem(
48 | value: null,
49 | child: buildFilterLabelCard(
50 | Colors.transparent,
51 | 'All',
52 | ),
53 | ),
54 | ...labels.map(
55 | (label) {
56 | var amt = label.getLabelAmountTotal(context).toInt();
57 | String extra = (amt != 0) ? " = ${amt}" : "";
58 | return DropdownMenuItem(
59 | value: label.id,
60 | child: buildFilterLabelCard(
61 | label.color,
62 | '${label.title.substring(0, (label.title.length < 20) ? label.title.length : 20)}${extra}',
63 | ),
64 | );
65 | },
66 | ).toList()
67 | ],
68 | onChanged: (newFilterId) {
69 | filterData.labelId = newFilterId;
70 | },
71 | );
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/lib/widgets/home_tabs_bar.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | // Used in HomeTabsScreen.
4 |
5 | class HomeTabsBar extends StatelessWidget {
6 | final int pageIndex;
7 | final void Function(int newPageIndex) onPressed;
8 |
9 | const HomeTabsBar({
10 | required this.pageIndex,
11 | required this.onPressed,
12 | });
13 |
14 | Color getSelectedItemColor(ThemeData themeData, int pageIndex) {
15 | if (themeData.brightness == Brightness.light) {
16 | // 2 different colors because bottom tabs background change color in light
17 | // mode.
18 | return pageIndex == 0
19 | ? themeData.colorScheme.primary
20 | : themeData.colorScheme.onPrimary;
21 | }
22 | // Bottom tabs background doesn't change in dark theme.
23 | return themeData.colorScheme.onPrimary;
24 | }
25 |
26 | Color? getUnselectedItemColor(ThemeData themeData, int pageIndex) {
27 | if (themeData.brightness == Brightness.light) {
28 | return pageIndex == 0
29 | ? themeData.textTheme.bodySmall!.color
30 | : Colors.white70;
31 | }
32 | return themeData.textTheme.bodySmall!.color;
33 | }
34 |
35 | Color getBackgroundColor(ThemeData themeData, int pageIndex) {
36 | if (themeData.brightness == Brightness.light) {
37 | return pageIndex == 0 ? themeData.canvasColor : themeData.primaryColor;
38 | }
39 |
40 | // Make it match the appbar if it's dark theme.
41 | return themeData.primaryColor;
42 | }
43 |
44 | @override
45 | Widget build(BuildContext context) {
46 | final themeData = Theme.of(context);
47 | return BottomNavigationBar(
48 | selectedItemColor: getSelectedItemColor(themeData, pageIndex),
49 | unselectedItemColor: getUnselectedItemColor(themeData, pageIndex),
50 | backgroundColor: getBackgroundColor(themeData, pageIndex),
51 | currentIndex: pageIndex,
52 | onTap: onPressed,
53 | items: [
54 | BottomNavigationBarItem(
55 | icon: Icon(Icons.attach_money),
56 | label: 'Dashboard',
57 | ),
58 | BottomNavigationBarItem(
59 | icon: Icon(Icons.shopping_basket_outlined),
60 | label: 'Budgets',
61 | ),
62 | BottomNavigationBarItem(
63 | icon: Icon(Icons.assessment),
64 | label: 'Insights',
65 | ),
66 | ],
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/windows/runner/flutter_window.cpp:
--------------------------------------------------------------------------------
1 | #include "flutter_window.h"
2 |
3 | #include
4 |
5 | #include "flutter/generated_plugin_registrant.h"
6 |
7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project)
8 | : project_(project) {}
9 |
10 | FlutterWindow::~FlutterWindow() {}
11 |
12 | bool FlutterWindow::OnCreate() {
13 | if (!Win32Window::OnCreate()) {
14 | return false;
15 | }
16 |
17 | RECT frame = GetClientArea();
18 |
19 | // The size here must match the window dimensions to avoid unnecessary surface
20 | // creation / destruction in the startup path.
21 | flutter_controller_ = std::make_unique(
22 | frame.right - frame.left, frame.bottom - frame.top, project_);
23 | // Ensure that basic setup of the controller was successful.
24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) {
25 | return false;
26 | }
27 | RegisterPlugins(flutter_controller_->engine());
28 | SetChildContent(flutter_controller_->view()->GetNativeWindow());
29 |
30 | flutter_controller_->engine()->SetNextFrameCallback([&]() {
31 | this->Show();
32 | });
33 |
34 | // Flutter can complete the first frame before the "show window" callback is
35 | // registered. The following call ensures a frame is pending to ensure the
36 | // window is shown. It is a no-op if the first frame hasn't completed yet.
37 | flutter_controller_->ForceRedraw();
38 |
39 | return true;
40 | }
41 |
42 | void FlutterWindow::OnDestroy() {
43 | if (flutter_controller_) {
44 | flutter_controller_ = nullptr;
45 | }
46 |
47 | Win32Window::OnDestroy();
48 | }
49 |
50 | LRESULT
51 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
52 | WPARAM const wparam,
53 | LPARAM const lparam) noexcept {
54 | // Give Flutter, including plugins, an opportunity to handle window messages.
55 | if (flutter_controller_) {
56 | std::optional result =
57 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
58 | lparam);
59 | if (result) {
60 | return *result;
61 | }
62 | }
63 |
64 | switch (message) {
65 | case WM_FONTCHANGE:
66 | flutter_controller_->engine()->ReloadSystemFonts();
67 | break;
68 | }
69 |
70 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
71 | }
72 |
--------------------------------------------------------------------------------
/app/lib/widgets/budgets_range_buttons.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:provider/provider.dart';
3 | import 'package:intl/intl.dart';
4 | import '../providers/insights_range.dart';
5 |
6 | class _ChipsData {
7 | final Range rangeValue;
8 | final String text;
9 |
10 | const _ChipsData({
11 | required this.rangeValue,
12 | required this.text,
13 | });
14 | }
15 |
16 | class BudgetsRangeButtons extends StatelessWidget {
17 | Widget buildRangeButton(bool isSelected, _ChipsData e, InsightsRange insightsRangeData) {
18 | return Expanded(
19 | child: TextButton(
20 | style: TextButton.styleFrom(
21 | shape: RoundedRectangleBorder(
22 | borderRadius: BorderRadius.circular(5),
23 | side: BorderSide(
24 | width: 2,
25 | color: isSelected ? Colors.white60 : Colors.transparent,
26 | ),
27 | ),
28 | ),
29 | child: FittedBox(
30 | child: Text(
31 | e.text,
32 | style: TextStyle(color: isSelected ? Colors.white : Colors.white54),
33 | ),
34 | ),
35 | onPressed: () {
36 | // Don't want to have to unnecessarily call notifyListeners.
37 | if (!isSelected) {
38 | insightsRangeData.range = e.rangeValue;
39 | }
40 | },
41 | ),
42 | );
43 | }
44 |
45 | @override
46 | Widget build(BuildContext context) {
47 | final insightsRangeData = Provider.of(context);
48 |
49 | var today = DateTime.now();
50 | var lastMonth = new DateTime(today.year, today.month - 1, 1);
51 | var lastlastmonth = new DateTime(today.year, today.month - 2, 1);
52 |
53 | var _chipsData = <_ChipsData>[
54 | _ChipsData(rangeValue: Range.prevpreviousMonth, text: DateFormat("MMMM").format(lastlastmonth)),
55 | _ChipsData(rangeValue: Range.previousMonth, text: DateFormat("MMMM").format(lastMonth)),
56 | _ChipsData(rangeValue: Range.month, text: DateFormat("MMMM").format(today)),
57 | ];
58 | return Padding(
59 | padding: const EdgeInsets.symmetric(horizontal: 12),
60 | child: Row(
61 | children: _chipsData.map(
62 | (e) {
63 | final isSelected = e.rangeValue == insightsRangeData.range;
64 |
65 | return buildRangeButton(isSelected, e, insightsRangeData);
66 | },
67 | ).toList(),
68 | ),
69 | );
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/app/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
15 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
33 |
34 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/app/lib/widgets/transaction_type_chip_form_field.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../utils/custom_colors.dart';
4 | import '../models/mylabel.dart';
5 |
6 | // Used in EditLabelScreen.
7 |
8 | class TransactionTypeChipFormField extends FormField {
9 | static const _avatarSizes = 18.0;
10 |
11 | static Widget _buildChoiceChip(
12 | FormFieldState state,
13 | Color color,
14 | LabelType labelType,
15 | ) {
16 | return ChoiceChip(
17 | // Mimic the leading widget of TransactionCard.
18 | avatar: Container(
19 | height: _avatarSizes,
20 | width: _avatarSizes,
21 | decoration: BoxDecoration(
22 | color: color,
23 | borderRadius: BorderRadius.circular(6),
24 | ),
25 | ),
26 | label: Text(labelType == LabelType.INCOME ? 'Income' : 'Expense'),
27 | selected: state.value == labelType,
28 | onSelected: (isSelected) {
29 | state.didChange(isSelected ? labelType : null);
30 | },
31 | );
32 | }
33 |
34 | TransactionTypeChipFormField({
35 | required BuildContext context,
36 | FormFieldSetter? onSaved,
37 | FormFieldValidator? validator,
38 | LabelType? initialValue,
39 | }) : super(
40 | onSaved: onSaved,
41 | validator: validator,
42 | initialValue: initialValue,
43 | builder: (state) {
44 | return Column(
45 | crossAxisAlignment: CrossAxisAlignment.start,
46 | children: [
47 | Wrap(
48 | children: [
49 | _buildChoiceChip(
50 | state,
51 | Theme.of(context).colorScheme.incomeColor,
52 | LabelType.INCOME,
53 | ),
54 | const SizedBox(width: 6),
55 | _buildChoiceChip(
56 | state,
57 | Theme.of(context).colorScheme.expenseColor,
58 | LabelType.EXPENSE,
59 | ),
60 | ],
61 | ),
62 | if (state.hasError)
63 | Text(
64 | state.errorText!,
65 | style: TextStyle(
66 | color: Theme.of(state.context).colorScheme.error,
67 | ),
68 | ),
69 | ],
70 | );
71 | },
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/app/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/app/lib/widgets/transactions_list.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:animations/animations.dart';
3 | import 'package:provider/provider.dart';
4 |
5 | import '../card_items/transaction_card.dart';
6 | import '../providers/filter.dart';
7 | import '../providers/transactions.dart';
8 | import '../screens/transaction_details_screen.dart';
9 | import '../utils/custom_colors.dart';
10 |
11 | // Used in HistoryScreen.
12 |
13 | class TransactionsList extends StatelessWidget {
14 | SliverList buildEmptyListMessage(String message) {
15 | return SliverList(
16 | delegate: SliverChildListDelegate(
17 | [
18 | const SizedBox(height: 20),
19 | Center(
20 | child: Text(message),
21 | ),
22 | ],
23 | ),
24 | );
25 | }
26 |
27 | @override
28 | SliverList build(BuildContext context) {
29 | final filterLabelId = Provider.of(context).labelId;
30 | final transactionsData = Provider.of(context);
31 | final filteredTransactions =
32 | transactionsData.filterTransactionsByLabel(context, filterLabelId);
33 |
34 | if (transactionsData.items.isEmpty) {
35 | return buildEmptyListMessage('No transactions for this filter!');
36 | }
37 |
38 | if (filteredTransactions.isEmpty) {
39 | return buildEmptyListMessage('No transactions for this filter!');
40 | }
41 |
42 | return SliverList(
43 | delegate: SliverChildBuilderDelegate(
44 | (context, index) {
45 | return Column(
46 | children: [
47 | OpenContainer(
48 | closedColor:
49 | Theme.of(context).colorScheme.transactionCards(context),
50 | openColor: Theme.of(context).colorScheme.surface,
51 | closedShape: const BeveledRectangleBorder(),
52 | closedElevation: 0,
53 | closedBuilder: (_, __) {
54 | return TransactionCard(
55 | transaction: filteredTransactions[index],
56 | );
57 | },
58 | openBuilder: (_, __) {
59 | return TransactionDetailsScreen(
60 | transactionId: filteredTransactions[index].id,
61 | );
62 | },
63 | ),
64 | const Divider(height: 1),
65 | ],
66 | );
67 | },
68 | childCount: filteredTransactions.length,
69 | ),
70 | );
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/app/lib/screens/insights_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:provider/provider.dart';
3 | import 'package:smooth_page_indicator/smooth_page_indicator.dart';
4 |
5 | import '../models/mylabel.dart';
6 | import '../charts/chart_widgets/labels_pie_chart.dart';
7 | import '../charts/chart_widgets/transaction_history_chart.dart';
8 | import '../providers/insights_range.dart';
9 | import '../utils/custom_colors.dart';
10 | import '../widgets/chart_container.dart';
11 |
12 | // This screen is a screen under home_screen.
13 |
14 | class InsightsScreen extends StatefulWidget {
15 | @override
16 | _InsightsScreenState createState() => _InsightsScreenState();
17 | }
18 |
19 | class _InsightsScreenState extends State {
20 | PageController _pageController = PageController();
21 |
22 | @override
23 | void dispose() {
24 | _pageController.dispose();
25 | super.dispose();
26 | }
27 |
28 | @override
29 | Widget build(BuildContext context) {
30 | final _graphScreens = [
31 | ChartContainer(
32 | title: 'Expenses',
33 | chart: LabelsPieChart(labelType: LabelType.EXPENSE),
34 | backgroundColor: Theme.of(context).colorScheme.expenseColor,
35 | ),
36 | ChartContainer(
37 | title: 'Income',
38 | chart: LabelsPieChart(labelType: LabelType.INCOME),
39 | backgroundColor: Theme.of(context).colorScheme.incomeColor,
40 | ),
41 | ChartContainer(
42 | title: 'Net Income History',
43 | chart: TransactionHistoryChart(),
44 | backgroundColor: Theme.of(context).brightness == Brightness.light ? Colors.blue[800] : Colors.blueGrey[400],
45 | isTransactionHistoryChart: true,
46 | )
47 | ];
48 |
49 | // Used Stack instead of Row so background can be behind ScrollingPageIndicator.
50 | return Stack(
51 | children: [
52 | // Use a builder instead of directly accessing the widgets so it's less resource intensive.
53 | ChangeNotifierProvider(
54 | create: (_) => InsightsRange(),
55 | child: PageView.builder(
56 | scrollDirection: Axis.vertical,
57 | controller: _pageController,
58 | itemBuilder: (_, index) {
59 | return _graphScreens[index];
60 | },
61 | itemCount: _graphScreens.length,
62 | ),
63 | ),
64 | Container(
65 | alignment: Alignment.centerRight,
66 | margin: const EdgeInsets.only(right: 7),
67 | child: SmoothPageIndicator(
68 | controller: _pageController,
69 | count: _graphScreens.length,
70 | axisDirection: Axis.vertical,
71 | effect: SlideEffect(spacing: 16, dotColor: Colors.white30, activeDotColor: Colors.white),
72 | ),
73 | ),
74 | ],
75 | );
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/app/lib/charts/chart_widgets/transaction_history_chart.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:provider/provider.dart';
3 |
4 | import '../charts_base/grouped_bar_chart_base.dart';
5 | import '../chart_models/transaction_history_model.dart';
6 | import '../../providers/transactions.dart';
7 |
8 | class TransactionHistoryChart extends StatelessWidget {
9 | @override
10 | Widget build(BuildContext context) {
11 | final transactions = Provider.of(context, listen: false).items;
12 |
13 | // Each TimeSeriesModel will contain one date with the sum of the transaction amounts from that specific day.
14 | var data = [];
15 |
16 | final today = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day);
17 |
18 | // Add up all the transactions in a format that's easier to read for the bar chart.
19 | transactions.forEach((transaction) {
20 | final amount = transaction.amount;
21 | if (data.length > 0 && transaction.date.month.compareTo(data.last.date!.month) == 0) {
22 | if (amount > 0) {
23 | data.last.incomeAmount += amount;
24 | } else {
25 | // Make it positive so it shows up next to the income bars.
26 | data.last.expenseAmount += amount.abs();
27 | }
28 | } else {
29 | data.add(
30 | TransactionHistoryModel(
31 | date: transaction.date,
32 | incomeAmount: amount > 0 ? amount : 0,
33 | expenseAmount: amount < 0 ? amount.abs() : 0,
34 | ),
35 | );
36 | }
37 | });
38 |
39 | if (data.length < 2) {
40 | return const Center(
41 | child: Text(
42 | 'Add some more transactions!',
43 | style: TextStyle(fontSize: 15),
44 | ),
45 | );
46 | }
47 | data = data.reversed.toList();
48 | data.removeAt(0);
49 |
50 | // Add earliest and latest days if they're not already in the data set so the graph is consistent (for weekly so far).
51 | /*
52 | if (data.first.date!.compareTo(today.subtract(Duration(days: 6))) != 0) {
53 | data.insert(
54 | 0,
55 | TransactionHistoryModel(
56 | date: today.subtract(Duration(days: 6)),
57 | incomeAmount: 0,
58 | expenseAmount: 0,
59 | ),
60 | );
61 | }
62 |
63 | if (data.last.date!.compareTo(today) < 0) {
64 | data.add(
65 | TransactionHistoryModel(
66 | date: today,
67 | incomeAmount: 0,
68 | expenseAmount: 0,
69 | ),
70 | );
71 | }
72 | */
73 | return Padding(
74 | padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
75 | child: GroupedBarChartBase(
76 | id: 'Balance History',
77 | color: Colors.blueAccent,
78 | data: data,
79 | ),
80 | );
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/tangerine/config.py:
--------------------------------------------------------------------------------
1 | """
2 | Handles reading the program configuration from an INI style file using Python configparser.
3 |
4 | Is expecting a file in the following format:
5 |
6 | [PLAID]
7 | client_id = xxxxxxxxxxxxxxxxxxxxxxxx
8 | secret = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
9 | public_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
10 | environment = development
11 | suppress_warnings=true
12 |
13 | [plaid-sync]
14 | dbfile = /data/transactions.db
15 |
16 | [Account1]
17 | access_token = access-development-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
18 | account = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
19 |
20 | [Account2]
21 | ....
22 | """
23 | import configparser
24 | import time
25 | import shutil
26 |
27 |
28 | class Config:
29 | def __init__(self, config_file: str):
30 | self.config_file = config_file
31 | self.config = configparser.ConfigParser()
32 | self.config.read(config_file)
33 |
34 | def get_plaid_client_config(self) -> str:
35 | return {
36 | 'client_id': self.config['PLAID']['client_id'],
37 | 'secret': self.config['PLAID']['secret'],
38 | 'environment': self.config['PLAID'].get('environment', 'sandbox'),
39 | 'suppress_warnings': self.config['PLAID'].get('suppress_warnings', True),
40 | }
41 |
42 | @property
43 | def environment(self):
44 | return self.config['PLAID']['environment']
45 |
46 | def get_dbfile(self) -> str:
47 | return self.config['plaid-sync']['dbfile']
48 |
49 | def get_all_config_sections(self) -> str:
50 | """
51 | Returns all defined configuration sections, not just accounts
52 | this is to check if adding a new account would create a duplicate
53 | section with that name.
54 | """
55 | return [
56 | account
57 | for account in self.config.sections()
58 | ]
59 |
60 | def get_enabled_accounts(self) -> str:
61 | return [
62 | account
63 | for account in self.config.sections()
64 | if (
65 | account != 'PLAID'
66 | and account != 'plaid-sync'
67 | and 'access_token' in self.config[account]
68 | and not self.config[account].getboolean('disabled', False)
69 | )
70 | ]
71 |
72 | def get_account_access_token(self, account_name: str) -> str:
73 | return self.config[account_name]['access_token']
74 |
75 | def add_account(self, account_name: str, access_token: str):
76 | """
77 | Saves an account and its credentials to the configuration file.
78 | """
79 | backup_file = f"{self.config_file}.{int(time.time())}.bkp"
80 | print("Backing up existing configuration to: %s" % backup_file)
81 | shutil.copyfile(self.config_file, backup_file)
82 |
83 | self.config.add_section(account_name)
84 | self.config.set(account_name, 'access_token', access_token)
85 |
86 | print("Overwriting existing config file: %s" % self.config_file)
87 | with open(self.config_file, "w") as f:
88 | self.config.write(f)
89 |
--------------------------------------------------------------------------------
/wealthica/config.py:
--------------------------------------------------------------------------------
1 | """
2 | Handles reading the program configuration from an INI style file using Python configparser.
3 |
4 | Is expecting a file in the following format:
5 |
6 | [PLAID]
7 | client_id = xxxxxxxxxxxxxxxxxxxxxxxx
8 | secret = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
9 | public_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
10 | environment = development
11 | suppress_warnings=true
12 |
13 | [plaid-sync]
14 | dbfile = /data/transactions.db
15 |
16 | [Account1]
17 | access_token = access-development-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
18 | account = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
19 |
20 | [Account2]
21 | ....
22 | """
23 | import configparser
24 | import time
25 | import shutil
26 |
27 |
28 | class Config:
29 | def __init__(self, config_file: str):
30 | self.config_file = config_file
31 | self.config = configparser.ConfigParser()
32 | self.config.read(config_file)
33 |
34 | def get_plaid_client_config(self) -> str:
35 | return {
36 | 'client_id': self.config['PLAID']['client_id'],
37 | 'secret': self.config['PLAID']['secret'],
38 | 'environment': self.config['PLAID'].get('environment', 'sandbox'),
39 | 'suppress_warnings': self.config['PLAID'].get('suppress_warnings', True),
40 | }
41 |
42 | @property
43 | def environment(self):
44 | return self.config['PLAID']['environment']
45 |
46 | def get_dbfile(self) -> str:
47 | return self.config['plaid-sync']['dbfile']
48 |
49 | def get_all_config_sections(self) -> str:
50 | """
51 | Returns all defined configuration sections, not just accounts
52 | this is to check if adding a new account would create a duplicate
53 | section with that name.
54 | """
55 | return [
56 | account
57 | for account in self.config.sections()
58 | ]
59 |
60 | def get_enabled_accounts(self) -> str:
61 | return [
62 | account
63 | for account in self.config.sections()
64 | if (
65 | account != 'PLAID'
66 | and account != 'plaid-sync'
67 | and 'access_token' in self.config[account]
68 | and not self.config[account].getboolean('disabled', False)
69 | )
70 | ]
71 |
72 | def get_account_access_token(self, account_name: str) -> str:
73 | return self.config[account_name]['access_token']
74 |
75 | def add_account(self, account_name: str, access_token: str):
76 | """
77 | Saves an account and its credentials to the configuration file.
78 | """
79 | backup_file = f"{self.config_file}.{int(time.time())}.bkp"
80 | print("Backing up existing configuration to: %s" % backup_file)
81 | shutil.copyfile(self.config_file, backup_file)
82 |
83 | self.config.add_section(account_name)
84 | self.config.set(account_name, 'access_token', access_token)
85 |
86 | print("Overwriting existing config file: %s" % self.config_file)
87 | with open(self.config_file, "w") as f:
88 | self.config.write(f)
89 |
--------------------------------------------------------------------------------
/plaid-sync/config.py:
--------------------------------------------------------------------------------
1 | """
2 | Handles reading the program configuration from an INI style file using Python configparser.
3 |
4 | Is expecting a file in the following format:
5 |
6 | [PLAID]
7 | client_id = xxxxxxxxxxxxxxxxxxxxxxxx
8 | secret = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
9 | public_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
10 | environment = development
11 | suppress_warnings=true
12 |
13 | [plaid-sync]
14 | dbfile = /data/transactions.db
15 |
16 | [Account1]
17 | access_token = access-development-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
18 | account = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
19 |
20 | [Account2]
21 | ....
22 | """
23 | import configparser
24 | import time
25 | import shutil
26 |
27 |
28 | class Config:
29 | def __init__(self, config_file: str):
30 | self.config_file = config_file
31 | self.config = configparser.ConfigParser()
32 | self.config.read(config_file)
33 |
34 | def get_plaid_client_config(self) -> str:
35 | return {
36 | 'client_id': self.config['PLAID']['client_id'],
37 | 'secret': self.config['PLAID']['secret'],
38 | 'environment': self.config['PLAID'].get('environment', 'sandbox'),
39 | 'suppress_warnings': self.config['PLAID'].get('suppress_warnings', True),
40 | }
41 |
42 | @property
43 | def environment(self):
44 | return self.config['PLAID']['environment']
45 |
46 | def get_dbfile(self) -> str:
47 | return self.config['plaid-sync']['dbfile']
48 |
49 | def get_all_config_sections(self) -> str:
50 | """
51 | Returns all defined configuration sections, not just accounts
52 | this is to check if adding a new account would create a duplicate
53 | section with that name.
54 | """
55 | return [
56 | account
57 | for account in self.config.sections()
58 | ]
59 |
60 | def get_enabled_accounts(self) -> str:
61 | return [
62 | account
63 | for account in self.config.sections()
64 | if (
65 | account != 'PLAID'
66 | and account != 'plaid-sync'
67 | and 'access_token' in self.config[account]
68 | and not self.config[account].getboolean('disabled', False)
69 | )
70 | ]
71 |
72 | def get_account_access_token(self, account_name: str) -> str:
73 | return self.config[account_name]['access_token']
74 |
75 | def add_account(self, account_name: str, access_token: str):
76 | """
77 | Saves an account and its credentials to the configuration file.
78 | """
79 | backup_file = f"{self.config_file}.{int(time.time())}.bkp"
80 | print("Backing up existing configuration to: %s" % backup_file)
81 | shutil.copyfile(self.config_file, backup_file)
82 |
83 | self.config.add_section(account_name)
84 | self.config.set(account_name, 'access_token', access_token)
85 |
86 | print("Overwriting existing config file: %s" % self.config_file)
87 | with open(self.config_file, "w") as f:
88 | self.config.write(f)
89 |
--------------------------------------------------------------------------------
/app/lib/providers/settings.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | //import 'package:notion_api/notion_databases.dart';
4 | //import 'package:notion_api/responses/notion_response.dart';
5 |
6 | import 'package:notion_sdk/notion_sdk.dart';
7 | import 'package:http/http.dart' as http;
8 | import '../models/budget.dart';
9 | import '../secrets.dart';
10 |
11 | // Used anywhere the currency symbol is needed, and in settings screen.
12 |
13 | class Settings with ChangeNotifier {
14 | // Have default values for now so null errors aren't thrown.
15 | String? _currencySymbol = '\$';
16 | bool _showCurrency = true;
17 | var items = [];
18 | double income_amount = 0;
19 |
20 | Future fetchAndSetSettings() async {
21 | //final settingsMap = await DBHelper.getSettingsMap();
22 | //_currencySymbol = settingsMap['currency'];
23 | //_showCurrency = settingsMap['showCurrency'] == 1;
24 |
25 | try {
26 | var hClient = http.Client();
27 | var client = NotionClient(httpClient: hClient, apiKey: notion_secret);
28 | var database = await client.databaseApi.queryDatabase(notion_database);
29 | if (database.hasMore) {
30 | throw UnimplementedError('database too large');
31 | }
32 | var _items = [];
33 | database.page?.forEach((page) {
34 | double amount = page.properties.getProperty('Amount')?.value.number ?? 0;
35 | String label = page.properties.getProperty('Category')?.value.toString() ?? '';
36 | double order = page.properties.getProperty('Order')?.value.number ?? 0;
37 | if (amount < 0) {
38 | if (label != '') {
39 | _items.add(BudgetData(label, -amount, order));
40 | }
41 | //print('label: $label amount: $amount');
42 | } else if (amount > 0) {
43 | if (label == 'Income') {
44 | //print('income $amount');
45 | income_amount = amount;
46 | }
47 | }
48 | });
49 | _items.sort((a, b) => a.order.compareTo(b.order));
50 | items = _items;
51 | } catch (e) {
52 | print(e.toString());
53 | rethrow;
54 | }
55 | notifyListeners();
56 | }
57 |
58 | // This is what the app actually displays.
59 | String? get displayedCurrencySymbol {
60 | // If we're showing the currency symbol, return a currency symbol.
61 | // Otherwise, just return an empty string. It's easier to deal with that way.
62 | if (_showCurrency) {
63 | return _currencySymbol;
64 | }
65 | return '';
66 | }
67 |
68 | // These methods are what the settings screen actually displays/changes.
69 | String? get currencySymbol => _currencySymbol;
70 |
71 | bool get showCurrency => _showCurrency;
72 |
73 | set currencySymbol(String? currencySymbol) {
74 | _currencySymbol = currencySymbol;
75 | notifyListeners();
76 | //DBHelper.updateSettings({'currency': currencySymbol});
77 | }
78 |
79 | set showCurrency(bool showCurrency) {
80 | _showCurrency = showCurrency;
81 | notifyListeners();
82 | // DBHelper.updateSettings({'showCurrency': showCurrency ? 1 : 0});
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/app/lib/widgets/transactions_list_filtered.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:animations/animations.dart';
3 | import 'package:provider/provider.dart';
4 |
5 | import '../card_items/transaction_card.dart';
6 | import '../providers/transactions.dart';
7 | import '../providers/labels.dart';
8 | import '../providers/insights_range.dart';
9 | import '../screens/transaction_details_screen.dart';
10 | import '../utils/custom_colors.dart';
11 | import '../models/transaction.dart';
12 | // Used in HistoryScreen.
13 |
14 | class TransactionsListFiltered extends StatelessWidget {
15 | TransactionsListFiltered(this.filterLabelId, this.range);
16 | final Range range;
17 | final String filterLabelId;
18 | SliverList buildEmptyListMessage(String message) {
19 | return SliverList(
20 | delegate: SliverChildListDelegate(
21 | [
22 | const SizedBox(height: 20),
23 | Center(
24 | child: Text(message),
25 | ),
26 | ],
27 | ),
28 | );
29 | }
30 |
31 | @override
32 | Widget build(BuildContext context) {
33 | final transactionsData = Provider.of(context);
34 | final labelsData = Provider.of(context, listen: false);
35 | var label = labelsData.findById(filterLabelId);
36 | List filteredTransactions;
37 | if (label == null) {
38 | filteredTransactions = transactionsData.filterTransactionsByRange(range);
39 | } else {
40 | filteredTransactions = transactionsData.filterTransactionsByLabelAndRange(
41 | context, filterLabelId, range);
42 | }
43 | filteredTransactions = filteredTransactions
44 | .where((transaction) => transaction.amount < 0)
45 | .toList();
46 |
47 | if (transactionsData.items.isEmpty) {
48 | return buildEmptyListMessage('No transactions for this filter!');
49 | }
50 |
51 | if (filteredTransactions.isEmpty) {
52 | return buildEmptyListMessage('No transactions for this filter!');
53 | }
54 |
55 | return SliverList(
56 | delegate: SliverChildBuilderDelegate(
57 | (context, index) {
58 | return Column(
59 | children: [
60 | OpenContainer(
61 | closedColor:
62 | Theme.of(context).colorScheme.transactionCards(context),
63 | openColor: Theme.of(context).colorScheme.surface,
64 | closedShape: const BeveledRectangleBorder(),
65 | closedElevation: 0,
66 | closedBuilder: (_, __) {
67 | return TransactionCard(
68 | transaction: filteredTransactions[index],
69 | );
70 | },
71 | openBuilder: (_, __) {
72 | return TransactionDetailsScreen(
73 | transactionId: filteredTransactions[index].id,
74 | );
75 | },
76 | ),
77 | const Divider(height: 1),
78 | ],
79 | );
80 | },
81 | childCount: filteredTransactions.length,
82 | ),
83 | );
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/app/linux/flutter/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | # This file controls Flutter-level build steps. It should not be edited.
2 | cmake_minimum_required(VERSION 3.10)
3 |
4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
5 |
6 | # Configuration provided via flutter tool.
7 | include(${EPHEMERAL_DIR}/generated_config.cmake)
8 |
9 | # TODO: Move the rest of this into files in ephemeral. See
10 | # https://github.com/flutter/flutter/issues/57146.
11 |
12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...),
13 | # which isn't available in 3.10.
14 | function(list_prepend LIST_NAME PREFIX)
15 | set(NEW_LIST "")
16 | foreach(element ${${LIST_NAME}})
17 | list(APPEND NEW_LIST "${PREFIX}${element}")
18 | endforeach(element)
19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
20 | endfunction()
21 |
22 | # === Flutter Library ===
23 | # System-level dependencies.
24 | find_package(PkgConfig REQUIRED)
25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
28 |
29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
30 |
31 | # Published to parent scope for install step.
32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
36 |
37 | list(APPEND FLUTTER_LIBRARY_HEADERS
38 | "fl_basic_message_channel.h"
39 | "fl_binary_codec.h"
40 | "fl_binary_messenger.h"
41 | "fl_dart_project.h"
42 | "fl_engine.h"
43 | "fl_json_message_codec.h"
44 | "fl_json_method_codec.h"
45 | "fl_message_codec.h"
46 | "fl_method_call.h"
47 | "fl_method_channel.h"
48 | "fl_method_codec.h"
49 | "fl_method_response.h"
50 | "fl_plugin_registrar.h"
51 | "fl_plugin_registry.h"
52 | "fl_standard_message_codec.h"
53 | "fl_standard_method_codec.h"
54 | "fl_string_codec.h"
55 | "fl_value.h"
56 | "fl_view.h"
57 | "flutter_linux.h"
58 | )
59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
60 | add_library(flutter INTERFACE)
61 | target_include_directories(flutter INTERFACE
62 | "${EPHEMERAL_DIR}"
63 | )
64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
65 | target_link_libraries(flutter INTERFACE
66 | PkgConfig::GTK
67 | PkgConfig::GLIB
68 | PkgConfig::GIO
69 | )
70 | add_dependencies(flutter flutter_assemble)
71 |
72 | # === Flutter tool backend ===
73 | # _phony_ is a non-existent file to force this command to run every time,
74 | # since currently there's no way to get a full input/output list from the
75 | # flutter tool.
76 | add_custom_command(
77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_
79 | COMMAND ${CMAKE_COMMAND} -E env
80 | ${FLUTTER_TOOL_ENVIRONMENT}
81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
83 | VERBATIM
84 | )
85 | add_custom_target(flutter_assemble DEPENDS
86 | "${FLUTTER_LIBRARY}"
87 | ${FLUTTER_LIBRARY_HEADERS}
88 | )
89 |
--------------------------------------------------------------------------------
/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-60x60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@1x.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-20x20@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-29x29@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-40x40@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-76x76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-App-83.5x83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "Icon-App-1024x1024@1x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/app/lib/widgets/insights_range_buttons.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:provider/provider.dart';
3 | import 'package:intl/intl.dart';
4 |
5 | import '../providers/insights_range.dart';
6 |
7 | class _ChipsData {
8 | final Range rangeValue;
9 | String text;
10 |
11 | _ChipsData({
12 | required this.rangeValue,
13 | required this.text,
14 | });
15 | }
16 |
17 | class InsightsRangeButtons extends StatelessWidget {
18 | // The transaction history chart doesn't have any options, only stuff from the past 7 days.
19 | final bool isTransactionHistoryChart;
20 |
21 | const InsightsRangeButtons({
22 | this.isTransactionHistoryChart = false,
23 | });
24 |
25 | Widget buildRangeButton(bool isSelected, _ChipsData e, InsightsRange insightsRangeData) {
26 | return Expanded(
27 | child: TextButton(
28 | style: TextButton.styleFrom(
29 | shape: RoundedRectangleBorder(
30 | borderRadius: BorderRadius.circular(5),
31 | side: BorderSide(
32 | width: 2,
33 | color: isSelected ? Colors.white60 : Colors.transparent,
34 | ),
35 | ),
36 | ),
37 | child: FittedBox(
38 | child: Text(
39 | e.text,
40 | style: TextStyle(color: isSelected ? Colors.white : Colors.white54),
41 | ),
42 | ),
43 | onPressed: () {
44 | // Don't want to have to unnecessarily call notifyListeners.
45 | if (!isSelected) {
46 | insightsRangeData.range = e.rangeValue;
47 | }
48 | },
49 | ),
50 | );
51 | }
52 |
53 | @override
54 | Widget build(BuildContext context) {
55 | if (isTransactionHistoryChart) {
56 | return TextButton(
57 | onPressed: () {},
58 | style: TextButton.styleFrom(
59 | shape: RoundedRectangleBorder(
60 | borderRadius: BorderRadius.circular(5),
61 | side: const BorderSide(
62 | width: 2,
63 | color: Colors.white60,
64 | ),
65 | ),
66 | padding: const EdgeInsets.symmetric(horizontal: 16)),
67 | child: const FittedBox(
68 | child: Text(
69 | 'PAST 5 MONTHS',
70 | style: TextStyle(color: Colors.white),
71 | ),
72 | ),
73 | );
74 | }
75 |
76 | final insightsRangeData = Provider.of(context);
77 |
78 | var today = DateTime.now();
79 | var lastMonth = new DateTime(today.year, today.month - 1, 1);
80 | var lastlastmonth = new DateTime(today.year, today.month - 2, 1);
81 |
82 | var _chipsData = <_ChipsData>[
83 | _ChipsData(rangeValue: Range.prevpreviousMonth, text: DateFormat("MMMM").format(lastlastmonth)),
84 | _ChipsData(rangeValue: Range.previousMonth, text: DateFormat("MMMM").format(lastMonth)),
85 | _ChipsData(rangeValue: Range.month, text: DateFormat("MMMM").format(today)),
86 | ];
87 | return Padding(
88 | padding: const EdgeInsets.symmetric(horizontal: 12),
89 | child: Row(
90 | children: _chipsData.map(
91 | (e) {
92 | final isSelected = e.rangeValue == insightsRangeData.range;
93 |
94 | return buildRangeButton(isSelected, e, insightsRangeData);
95 | },
96 | ).toList(),
97 | ),
98 | );
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/app/lib/widgets/balance_summary_card.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:provider/provider.dart';
3 |
4 | import '../providers/settings.dart';
5 | import '../utils/custom_colors.dart';
6 |
7 | // Used in BalanceCardsView.
8 |
9 | class BalanceSummaryCard extends StatelessWidget {
10 | final String title;
11 | final double balance;
12 | final void Function()? onTap;
13 |
14 | const BalanceSummaryCard({
15 | required this.title,
16 | required this.balance,
17 | this.onTap,
18 | });
19 |
20 | String formattedBalance(BuildContext context) {
21 | final currencySymbol =
22 | Provider.of(context).displayedCurrencySymbol;
23 | var formattedBalance = '$currencySymbol${balance.abs().toStringAsFixed(2)}';
24 | if (balance < 0) {
25 | formattedBalance = '-' + formattedBalance;
26 | }
27 | return formattedBalance;
28 | }
29 |
30 | @override
31 | Widget build(BuildContext context) {
32 | final cardRadius = BorderRadius.circular(15);
33 |
34 | return Card(
35 | elevation: 3,
36 | shape: RoundedRectangleBorder(
37 | borderRadius: cardRadius,
38 | ),
39 | margin: const EdgeInsets.all(10),
40 | // To ensure the InkWell shows up.
41 | child: Ink(
42 | decoration: BoxDecoration(
43 | // To match Card's border radius.
44 | borderRadius: cardRadius,
45 | color: Theme.of(context).colorScheme.largeTypeColor(amount: balance),
46 | ),
47 | child: InkWell(
48 | highlightColor: Colors.black12,
49 | splashColor: Colors.black12,
50 | borderRadius: cardRadius,
51 | onTap: onTap,
52 | child: Padding(
53 | padding: const EdgeInsets.symmetric(
54 | horizontal: 15,
55 | vertical: 8,
56 | ),
57 | child: Column(
58 | children: [
59 | // Must surround text with container so it will take up space to align left.
60 | Container(
61 | width: double.infinity,
62 | child: Text(
63 | title,
64 | textAlign: TextAlign.start,
65 | style: TextStyle(
66 | color: Theme.of(context).colorScheme.onIncomeExpenseColor,
67 | ),
68 | ),
69 | ),
70 | // Must surround text with container so it will take up space to align right.
71 | Container(
72 | // FittedBox would take up too much height otherwise.
73 | height: 80,
74 | width: double.infinity,
75 | child: FittedBox(
76 | alignment: Alignment.bottomRight,
77 | // Only shrink the fontSize if needed, but don't grow it.
78 | fit: BoxFit.scaleDown,
79 | child: Text(
80 | formattedBalance(context),
81 | textAlign: TextAlign.end,
82 | style: TextStyle(
83 | fontSize: 60,
84 | fontWeight: FontWeight.w300,
85 | color:
86 | Theme.of(context).colorScheme.onIncomeExpenseColor,
87 | ),
88 | ),
89 | ),
90 | ),
91 | ],
92 | ),
93 | ),
94 | ),
95 | ),
96 | );
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/app/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: budget
2 | description: Bugdet App
3 |
4 | # The following line prevents the package from being accidentally published to
5 | # pub.dev using `pub publish`. This is preferred for private packages.
6 | publish_to: "none" # Remove this line if you wish to publish to pub.dev
7 |
8 | # The following defines the version and build number for your application.
9 | # A version number is three numbers separated by dots, like 1.2.43
10 | # followed by an optional build number separated by a +.
11 | # Both the version and the builder number may be overridden in flutter
12 | # build by specifying --build-name and --build-number, respectively.
13 | # In Android, build-name is used as versionName while build-number used as versionCode.
14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning
15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
16 | # Read more about iOS versioning at
17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
18 | version: 1.8.3+18
19 |
20 | environment:
21 | sdk: '>=2.12.0 <3.0.0'
22 |
23 | dependencies:
24 | flutter:
25 | sdk: flutter
26 | animations: ^2.0.2
27 | provider: ^6.0.2
28 | intl: ^0.17.0
29 | introduction_screen: ^2.1.0
30 | charts_flutter:
31 | git:
32 | url: https://github.com/mzakharo/charts_flutter.git
33 | ref: 45c529764745766bfe3003dcc3318d6d1e194b31
34 | path: charts_flutter
35 | flutter_colorpicker: ^1.0.3
36 | smooth_page_indicator: ^1.0.0+2
37 | settings_ui: ^2.0.1
38 | path: ^1.6.4
39 | collection: ^1.15.0-nullsafety.4
40 | influxdb_client: ^2.9.0
41 | fl_chart: ^0.64.0
42 | notion_sdk:
43 | git:
44 | url: https://github.com/mzakharo/notion_dart_sdk.git
45 | ref: master
46 |
47 | dev_dependencies:
48 | flutter_test:
49 | sdk: flutter
50 | flutter_launcher_icons: ^0.9.2
51 | flutter_icons:
52 | android: true
53 | ios: false
54 | image_path: "assets/icon/launcher_icon.png"
55 | adaptive_icon_foreground: "assets/icon/launcher_icon_adaptive.png"
56 | adaptive_icon_background: "#ffffff"
57 |
58 | # For information on the generic Dart part of this file, see the
59 | # following page: https://dart.dev/tools/pub/pubspec
60 |
61 | # The following section is specific to Flutter.
62 | flutter:
63 | # The following line ensures that the Material Icons font is
64 | # included with your application, so that you can use the icons in
65 | # the material Icons class.
66 | uses-material-design: true
67 |
68 | assets:
69 | - assets/icon/launcher_icon.png
70 | - google_fonts/
71 | - assets/images/
72 | # An image asset can refer to one or more resolution-specific "variants", see
73 | # https://flutter.dev/assets-and-images/#resolution-aware.
74 | # For details regarding adding assets from package dependencies, see
75 | # https://flutter.dev/assets-and-images/#from-packages
76 | # To add custom fonts to your application, add a fonts section here,
77 | # in this "flutter" section. Each entry in this list should have a
78 | # "family" key with the font family name, and a "fonts" key with a
79 | # list giving the asset and other descriptors for the font. For
80 | # example:
81 | fonts:
82 | - family: CustomIcons
83 | fonts:
84 | - asset: assets/fonts/CustomIcons.ttf
85 | #
86 | # For details regarding fonts from package dependencies,
87 | # see https://flutter.dev/custom-fonts/#from-packages
88 |
--------------------------------------------------------------------------------
/app/windows/runner/Runner.rc:
--------------------------------------------------------------------------------
1 | // Microsoft Visual C++ generated resource script.
2 | //
3 | #pragma code_page(65001)
4 | #include "resource.h"
5 |
6 | #define APSTUDIO_READONLY_SYMBOLS
7 | /////////////////////////////////////////////////////////////////////////////
8 | //
9 | // Generated from the TEXTINCLUDE 2 resource.
10 | //
11 | #include "winres.h"
12 |
13 | /////////////////////////////////////////////////////////////////////////////
14 | #undef APSTUDIO_READONLY_SYMBOLS
15 |
16 | /////////////////////////////////////////////////////////////////////////////
17 | // English (United States) resources
18 |
19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
21 |
22 | #ifdef APSTUDIO_INVOKED
23 | /////////////////////////////////////////////////////////////////////////////
24 | //
25 | // TEXTINCLUDE
26 | //
27 |
28 | 1 TEXTINCLUDE
29 | BEGIN
30 | "resource.h\0"
31 | END
32 |
33 | 2 TEXTINCLUDE
34 | BEGIN
35 | "#include ""winres.h""\r\n"
36 | "\0"
37 | END
38 |
39 | 3 TEXTINCLUDE
40 | BEGIN
41 | "\r\n"
42 | "\0"
43 | END
44 |
45 | #endif // APSTUDIO_INVOKED
46 |
47 |
48 | /////////////////////////////////////////////////////////////////////////////
49 | //
50 | // Icon
51 | //
52 |
53 | // Icon with lowest ID value placed first to ensure application icon
54 | // remains consistent on all systems.
55 | IDI_APP_ICON ICON "resources\\app_icon.ico"
56 |
57 |
58 | /////////////////////////////////////////////////////////////////////////////
59 | //
60 | // Version
61 | //
62 |
63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
65 | #else
66 | #define VERSION_AS_NUMBER 1,0,0,0
67 | #endif
68 |
69 | #if defined(FLUTTER_VERSION)
70 | #define VERSION_AS_STRING FLUTTER_VERSION
71 | #else
72 | #define VERSION_AS_STRING "1.0.0"
73 | #endif
74 |
75 | VS_VERSION_INFO VERSIONINFO
76 | FILEVERSION VERSION_AS_NUMBER
77 | PRODUCTVERSION VERSION_AS_NUMBER
78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
79 | #ifdef _DEBUG
80 | FILEFLAGS VS_FF_DEBUG
81 | #else
82 | FILEFLAGS 0x0L
83 | #endif
84 | FILEOS VOS__WINDOWS32
85 | FILETYPE VFT_APP
86 | FILESUBTYPE 0x0L
87 | BEGIN
88 | BLOCK "StringFileInfo"
89 | BEGIN
90 | BLOCK "040904e4"
91 | BEGIN
92 | VALUE "CompanyName", "com.example" "\0"
93 | VALUE "FileDescription", "budget" "\0"
94 | VALUE "FileVersion", VERSION_AS_STRING "\0"
95 | VALUE "InternalName", "budget" "\0"
96 | VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0"
97 | VALUE "OriginalFilename", "budget.exe" "\0"
98 | VALUE "ProductName", "budget" "\0"
99 | VALUE "ProductVersion", VERSION_AS_STRING "\0"
100 | END
101 | END
102 | BLOCK "VarFileInfo"
103 | BEGIN
104 | VALUE "Translation", 0x409, 1252
105 | END
106 | END
107 |
108 | #endif // English (United States) resources
109 | /////////////////////////////////////////////////////////////////////////////
110 |
111 |
112 |
113 | #ifndef APSTUDIO_INVOKED
114 | /////////////////////////////////////////////////////////////////////////////
115 | //
116 | // Generated from the TEXTINCLUDE 3 resource.
117 | //
118 |
119 |
120 | /////////////////////////////////////////////////////////////////////////////
121 | #endif // not APSTUDIO_INVOKED
122 |
--------------------------------------------------------------------------------
/app/lib/utils/custom_colors.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:provider/provider.dart';
3 |
4 | import '../models/mylabel.dart';
5 | import '../providers/theme_provider.dart';
6 |
7 | extension CustomColors on ColorScheme {
8 | // The secondary colors are used in graphs.
9 |
10 | // These are swapped when the theme is dark (for the most part).
11 | static final _internalIncomeColor = Colors.green.shade800;
12 | static final _internalSecondaryIncomeColor = Colors.green.shade200;
13 | static final _internalDarkIncomeColor = Color.fromARGB(255, 53, 124, 57);
14 | static final _internalDarkLargeIncomeColor = Color.fromARGB(255, 31, 81, 38);
15 |
16 | static final _internalExpenseColor = Colors.pink.shade900;
17 | static final _internalSecondaryExpenseColor = Colors.red.shade400;
18 | static final _internalDarkExpenseColor = Color.fromARGB(255, 163, 70, 69);
19 | static final _internalDarkLargeExpenseColor = Color.fromARGB(255, 86, 29, 30);
20 |
21 | Color get incomeColor =>
22 | _isLight ? _internalIncomeColor : _internalDarkIncomeColor;
23 |
24 | Color get secondaryIncomeColor => _internalSecondaryIncomeColor;
25 |
26 | Color get expenseColor =>
27 | _isLight ? _internalExpenseColor : _internalDarkExpenseColor;
28 |
29 | Color get secondaryExpenseColor => _internalSecondaryExpenseColor;
30 |
31 | Color get onIncomeExpenseColor =>
32 | _isLight ? Colors.white : Colors.grey.shade200;
33 |
34 | // Takes in the amount and returns the income or expense color accordingly.
35 | Color transactionTypeColor(double? amount) {
36 | return (amount != null && amount >= 0) ? incomeColor : expenseColor;
37 | }
38 |
39 | // Same method above but for labelTypes.
40 | Color labelTypeColor(LabelType labelType) {
41 | return (labelType == LabelType.INCOME) ? incomeColor : expenseColor;
42 | }
43 |
44 | // On dark mode, the default income/expense colors look wrong for large objects.
45 | // These contain colors that look nice on dark mode.
46 | // Used on balance card and edit transaction header.
47 | // Only one of the parameters is needed.
48 | Color largeTypeColor({double? amount, LabelType? labelType}) {
49 | if (amount != null) {
50 | if (_isLight) {
51 | return (amount >= 0) ? _internalIncomeColor : _internalExpenseColor;
52 | }
53 | return (amount >= 0)
54 | ? _internalDarkLargeIncomeColor
55 | : _internalDarkLargeExpenseColor;
56 | }
57 |
58 | if (_isLight) {
59 | return (labelType == LabelType.INCOME)
60 | ? _internalIncomeColor
61 | : _internalExpenseColor;
62 | }
63 |
64 | return (labelType == LabelType.INCOME)
65 | ? _internalDarkLargeIncomeColor
66 | : _internalDarkLargeExpenseColor;
67 | }
68 |
69 | Color secondaryTransactionTypeColor(double? amount) {
70 | return (amount != null && amount >= 0)
71 | ? secondaryIncomeColor
72 | : secondaryExpenseColor;
73 | }
74 |
75 | Color dashboardHeader(BuildContext context) {
76 | final themeType = Provider.of(context).themeType;
77 |
78 | switch (themeType) {
79 | case ThemeType.Dark:
80 | return Theme.of(context).canvasColor;
81 | case ThemeType.Amoled:
82 | return Color.fromARGB(255, 15, 15, 15);
83 | // Fallback to light theme.
84 | default:
85 | return surface;
86 | }
87 | }
88 |
89 | Color transactionCards(BuildContext context) {
90 | final themeType = Provider.of(context).themeType;
91 | return themeType == ThemeType.Amoled
92 | ? Colors.black
93 | : Theme.of(context).cardColor;
94 | }
95 |
96 | // Small internal helper just to shorten code when checking for light or dark theme.
97 | bool get _isLight => brightness == Brightness.light;
98 | }
99 |
--------------------------------------------------------------------------------
/app/lib/screens/onboarding.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:introduction_screen/introduction_screen.dart';
3 |
4 | //import '../utils/db_helper.dart';
5 |
6 | class Onboarding extends StatelessWidget {
7 | static const routeName = '/onboarding';
8 |
9 | final bool openedFromDrawer;
10 |
11 | // We force light mode here because the images for the tutorial have white
12 | // backgrounds.
13 |
14 | const Onboarding({
15 | this.openedFromDrawer = false,
16 | });
17 |
18 | Widget buildImage(String assetName) {
19 | return Align(
20 | child: Image.asset('assets/images/$assetName.jpg', width: 350.0),
21 | alignment: Alignment.bottomCenter,
22 | );
23 | }
24 |
25 | @override
26 | Widget build(BuildContext context) {
27 | const pageDecoration = const PageDecoration(
28 | titleTextStyle: const TextStyle(
29 | fontSize: 28.0,
30 | fontWeight: FontWeight.w700,
31 | color: Colors.black,
32 | ),
33 | bodyTextStyle: const TextStyle(
34 | fontSize: 18.0,
35 | color: Colors.black,
36 | ),
37 | descriptionPadding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 16.0),
38 | pageColor: Colors.white,
39 | imagePadding: EdgeInsets.zero,
40 | );
41 |
42 | final introPages = [
43 | PageViewModel(
44 | image: buildImage('Intro1'),
45 | title: 'Welcome to Budget My Life!',
46 | body: 'Here are a few things you should know to get started',
47 | decoration: pageDecoration,
48 | ),
49 | PageViewModel(
50 | image: buildImage('Intro2'),
51 | title: 'Adding Transactions',
52 | body: 'Click on the button in the bottom right of the dashboard screen to add a transaction!',
53 | decoration: pageDecoration,
54 | ),
55 | PageViewModel(
56 | image: buildImage('Intro3'),
57 | title: 'Customize Labels',
58 | body: 'Open the drawer to customize labels. These help provide you with insights on how you earn/spend your money',
59 | decoration: pageDecoration,
60 | ),
61 | PageViewModel(
62 | image: buildImage('Intro4'),
63 | title: 'View Insights',
64 | body: 'Click on the second bottom tab on the main screen to view your insights!',
65 | decoration: pageDecoration,
66 | ),
67 | PageViewModel(
68 | image: buildImage('Intro5'),
69 | title: 'Get Started!',
70 | body: 'You can always view this intro again by tapping "Help" in the drawer',
71 | decoration: pageDecoration,
72 | ),
73 | ];
74 |
75 | return IntroductionScreen(
76 | pages: introPages,
77 | skipFlex: 0,
78 | nextFlex: 0,
79 | showSkipButton: true,
80 | skip: const Text(
81 | 'SKIP',
82 | style: TextStyle(color: Colors.black),
83 | ),
84 | next: const Icon(
85 | Icons.arrow_forward,
86 | color: Colors.black,
87 | ),
88 | done: const Text(
89 | 'LET\'S GO',
90 | style: TextStyle(
91 | fontWeight: FontWeight.w600,
92 | color: Colors.black,
93 | ),
94 | ),
95 | dotsDecorator: const DotsDecorator(
96 | size: Size(10.0, 10.0),
97 | color: Color(0xFFBDBDBD),
98 | activeSize: Size(22.0, 10.0),
99 | activeShape: RoundedRectangleBorder(
100 | borderRadius: BorderRadius.all(Radius.circular(25.0)),
101 | ),
102 | ),
103 | curve: Curves.easeInOutSine,
104 | onDone: () {
105 | if (!openedFromDrawer) {
106 | //DBHelper.onboardedUser();
107 | }
108 | Navigator.pop(context);
109 | },
110 | );
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/app/lib/charts/charts_base/grouped_bar_chart_base.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:charts_flutter/flutter.dart' as charts;
3 | import 'package:intl/intl.dart';
4 |
5 | import '../chart_models/transaction_history_model.dart';
6 | import '../../utils/custom_colors.dart';
7 |
8 | import 'custom_circle_symbol_renderer.dart';
9 |
10 | class GroupedBarChartBase extends StatelessWidget {
11 | final String id;
12 | final List data;
13 |
14 | GroupedBarChartBase({
15 | required this.id,
16 | required Color color,
17 | required this.data,
18 | });
19 |
20 | charts.Color incomeChartColor(BuildContext context) => convertToChartColor(Theme.of(context).colorScheme.incomeColor);
21 |
22 | charts.Color expenseChartColor(BuildContext context) => convertToChartColor(Theme.of(context).colorScheme.expenseColor);
23 |
24 | charts.Color convertToChartColor(Color color) => charts.Color(
25 | r: color.red,
26 | g: color.green,
27 | b: color.blue,
28 | a: color.alpha,
29 | );
30 |
31 | @override
32 | Widget build(BuildContext context) {
33 | final List> chartData = [
34 | charts.Series(
35 | id: id,
36 | colorFn: (_, __) => incomeChartColor(context),
37 | domainFn: (data, _) => data.dateString,
38 | measureFn: (data, _) => data.incomeAmount,
39 | data: data,
40 | ),
41 | charts.Series(
42 | id: id,
43 | colorFn: (_, __) => expenseChartColor(context),
44 | domainFn: (data, _) => data.dateString,
45 | measureFn: (data, _) => data.expenseAmount,
46 | data: data,
47 | ),
48 | ];
49 |
50 | final lineAndTextColor = Theme.of(context).brightness == Brightness.light ? charts.Color.black : charts.Color.white;
51 | double selectedDatum = 0.0;
52 | var f = NumberFormat("###,###", "en_US");
53 |
54 | return charts.BarChart(
55 | chartData,
56 | animate: true,
57 | selectionModels: [
58 | charts.SelectionModelConfig(
59 | type: charts.SelectionModelType.info,
60 | updatedListener: (model) {},
61 | changedListener: (charts.SelectionModel model) {
62 | if (model.hasDatumSelection) {
63 | model.selectedDatum.forEach((charts.SeriesDatum datumPair) {
64 | selectedDatum = datumPair.datum.incomeAmount - datumPair.datum.expenseAmount;
65 | });
66 | } else {
67 | selectedDatum = 0.0;
68 | }
69 | })
70 | ],
71 |
72 | behaviors: [charts.LinePointHighlighter(symbolRenderer: CustomCircleSymbolRenderer(() => '\$${f.format(selectedDatum)}'))],
73 | barGroupingType: charts.BarGroupingType.grouped,
74 | // Everything below is to make it look good on light and dark themes.
75 | domainAxis: charts.OrdinalAxisSpec(
76 | renderSpec: charts.SmallTickRendererSpec(
77 | labelStyle: charts.TextStyleSpec(
78 | fontSize: 12,
79 | color: lineAndTextColor,
80 | ),
81 | lineStyle: charts.LineStyleSpec(
82 | color: lineAndTextColor,
83 | ),
84 | ),
85 | ),
86 |
87 | primaryMeasureAxis: charts.NumericAxisSpec(
88 | renderSpec: charts.GridlineRendererSpec(
89 | labelStyle: charts.TextStyleSpec(
90 | fontSize: 12,
91 | color: lineAndTextColor,
92 | ),
93 | lineStyle: charts.LineStyleSpec(
94 | color: lineAndTextColor,
95 | ),
96 | ),
97 | ),
98 | );
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/app/lib/screens/budget_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../providers/insights_range.dart';
4 | import 'package:provider/provider.dart';
5 | import 'package:smooth_page_indicator/smooth_page_indicator.dart';
6 |
7 | import '../utils/custom_colors.dart';
8 |
9 | import '../widgets/budgets_range_buttons.dart';
10 | import '../charts/chart_widgets/budgets_bar_chart.dart';
11 |
12 | class BudgetScreen extends StatefulWidget {
13 | @override
14 | _BudgetScreenState createState() => _BudgetScreenState();
15 | }
16 |
17 | class _BudgetScreenState extends State {
18 | PageController _pageController = PageController();
19 |
20 | @override
21 | void dispose() {
22 | _pageController.dispose();
23 | super.dispose();
24 | }
25 |
26 | @override
27 | Widget build(BuildContext context) {
28 | final _graphScreens = [
29 | Container(
30 | color: Theme.of(context).brightness == Brightness.light
31 | ? Theme.of(context).colorScheme.expenseColor
32 | : Theme.of(context).canvasColor,
33 | child: Column(
34 | children: [
35 | const Spacer(flex: 4),
36 | Text(
37 | "Budget",
38 | style: TextStyle(
39 | color: Colors.white,
40 | fontSize: 30,
41 | fontFamily: 'Roboto', // Replace GoogleFonts with built-in font
42 | ),
43 | ),
44 | const Spacer(flex: 3),
45 | Container(
46 | margin: const EdgeInsets.only(left: 18, right: 25),
47 | height: 465,
48 | child: SingleChildScrollView(
49 | child: Container(
50 | // Uneven because room is needed for ScrollingPageIndicator.
51 | //margin: const EdgeInsets.only(left: 5, right: 5),
52 | height: 1000,
53 | decoration: BoxDecoration(
54 | color: Theme.of(context).colorScheme.surface,
55 | borderRadius: BorderRadius.circular(13),
56 | border: Theme.of(context).brightness == Brightness.light
57 | ? null
58 | : Border.all(
59 | color: Theme.of(context).colorScheme.expenseColor,
60 | width: 5),
61 | ),
62 | child: BudgetsPieChart(),
63 | ))),
64 | const Spacer(flex: 3),
65 | BudgetsRangeButtons(),
66 | const Spacer(flex: 4),
67 | ],
68 | ),
69 | ),
70 | ];
71 |
72 | return Stack(
73 | children: [
74 | // Use a builder instead of directly accessing the widgets so it's less resource intensive.
75 | ChangeNotifierProvider(
76 | create: (_) => InsightsRange(),
77 | child: PageView.builder(
78 | scrollDirection: Axis.vertical,
79 | controller: _pageController,
80 | itemBuilder: (_, index) {
81 | return _graphScreens[index];
82 | },
83 | itemCount: _graphScreens.length,
84 | ),
85 | ),
86 | Container(
87 | alignment: Alignment.centerRight,
88 | margin: const EdgeInsets.only(right: 7),
89 | child: SmoothPageIndicator(
90 | controller: _pageController,
91 | count: _graphScreens.length,
92 | axisDirection: Axis.vertical,
93 | effect: SlideEffect(
94 | spacing: 16,
95 | dotColor: Colors.white30,
96 | activeDotColor: Colors.white),
97 | ),
98 | ),
99 | ],
100 | );
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/app/lib/card_items/transaction_card.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../utils/custom_colors.dart';
4 | import '../models/transaction.dart';
5 |
6 | // Used in HistoryScreen.
7 | // Design inspired by Todoist tasks at time of writing.
8 |
9 | class TransactionCard extends StatelessWidget {
10 | final Transaction transaction;
11 |
12 | const TransactionCard({required this.transaction});
13 |
14 | // This looks better than a chip.
15 | Widget buildCategoryLabel(ThemeData theme, String title, Color? categoryColor) {
16 | return Row(
17 | children: [
18 | Container(
19 | //Here you can control the width of your container ..
20 | //when text exceeds it will be trancated via elipses...
21 | width: 110.0,
22 | child: Text(
23 | title,
24 | softWrap: false,
25 | style: theme.textTheme.bodySmall,
26 | textAlign: TextAlign.right,
27 | overflow: TextOverflow.ellipsis,
28 | )),
29 | const SizedBox(width: 4),
30 | CircleAvatar(
31 | maxRadius: 6,
32 | backgroundColor: categoryColor,
33 | ),
34 | ],
35 | );
36 | }
37 |
38 | @override
39 | Widget build(BuildContext context) {
40 | final theme = Theme.of(context);
41 |
42 | // No need to surround this with a card because it is surrounded by OpenContainer in HistoryScreen.
43 | return Padding(
44 | padding: const EdgeInsets.fromLTRB(22, 5, 22, 10),
45 | child: Column(
46 | children: [
47 | ListTile(
48 | contentPadding: const EdgeInsets.all(0),
49 | // Need to align the square in the center (height) of the card.
50 | leading: Align(
51 | // Don't have Align take up entire ListTile width.
52 | widthFactor: 1,
53 | // Small rounded square to easily show if it is an income or expense.
54 | child: Container(
55 | height: 25,
56 | width: 25,
57 | decoration: BoxDecoration(
58 | color: Theme.of(context).colorScheme.transactionTypeColor(transaction.amount),
59 | borderRadius: BorderRadius.circular(6),
60 | ),
61 | ),
62 | ),
63 | title: Text(
64 | transaction.title,
65 | softWrap: false,
66 | overflow: TextOverflow.fade,
67 | style: const TextStyle(
68 | fontSize: 16,
69 | ),
70 | ),
71 | trailing: Text(
72 | transaction.formattedAmount(context),
73 | softWrap: false,
74 | overflow: TextOverflow.fade,
75 | style: const TextStyle(
76 | fontSize: 17,
77 | fontWeight: FontWeight.bold,
78 | ),
79 | ),
80 | ),
81 | // Custom subtitle.
82 | Row(
83 | children: [
84 | const SizedBox(width: 55),
85 | Icon(
86 | Icons.calendar_today,
87 | color: theme.hintColor,
88 | size: 15,
89 | ),
90 | const SizedBox(width: 4),
91 | Text(
92 | transaction.formattedDate,
93 | style: theme.textTheme.bodySmall,
94 | ),
95 | const Spacer(),
96 | // Category label.
97 | buildCategoryLabel(
98 | theme,
99 | transaction.getLabel(context)!.title,
100 | transaction.getLabel(context)!.color,
101 | ),
102 | ],
103 | ),
104 | ],
105 | ),
106 | );
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/app/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter/services.dart';
4 | import 'package:provider/provider.dart';
5 |
6 | import './providers/labels.dart';
7 | import './providers/transactions.dart';
8 | import './providers/settings.dart';
9 | import './providers/theme_provider.dart';
10 | import './screens/home_tabs_screen.dart';
11 | import './screens/edit_labels_screen.dart';
12 | import './screens/settings_screen.dart';
13 | import 'globals.dart' as globals;
14 |
15 | Future main() async {
16 | // Add custom font license.
17 | LicenseRegistry.addLicense(() async* {
18 | final license = await rootBundle.loadString('google_fonts/OFL.txt');
19 | yield LicenseEntryWithLineBreaks(['google_fonts'], license);
20 | });
21 |
22 | // Needed for onboarding.
23 | WidgetsFlutterBinding.ensureInitialized();
24 |
25 | globals.version = "1.0.0";
26 |
27 | SystemChrome.setPreferredOrientations([
28 | DeviceOrientation.portraitUp,
29 | DeviceOrientation.portraitDown,
30 | ]);
31 |
32 | runApp(
33 | MultiProvider(
34 | providers: [
35 | ChangeNotifierProvider(
36 | create: (_) => ThemeProvider(),
37 | ),
38 | ChangeNotifierProvider(
39 | create: (_) => Settings(),
40 | ),
41 | ChangeNotifierProvider(
42 | create: (_) => Transactions(),
43 | ),
44 | ChangeNotifierProvider(
45 | create: (_) => Labels(),
46 | ),
47 | ],
48 | builder: (context, _) => MyApp(isOnboarded: true),
49 | ),
50 | );
51 | }
52 |
53 | Future fetchAndSetData(BuildContext context) async {
54 | print("fetch data");
55 | await Future.wait([
56 | Provider.of(context, listen: false).fetchAndSetTheme(),
57 | Provider.of(context, listen: false).fetchAndSetSettings(),
58 | Provider.of(context, listen: false).fetchAndSetLabels(),
59 | Provider.of(context, listen: false).fetchAndSetTransactions(),
60 | ]);
61 | }
62 |
63 | class MyApp extends StatefulWidget {
64 | final bool isOnboarded;
65 |
66 | const MyApp({
67 | required this.isOnboarded,
68 | });
69 |
70 | @override
71 | _MyAppState createState() => _MyAppState();
72 | }
73 |
74 | class _MyAppState extends State {
75 | // This determines the progress of data being read from storage.
76 | Future? dataFetchFuture;
77 |
78 | @override
79 | void initState() {
80 | super.initState();
81 | dataFetchFuture = fetchAndSetData(context);
82 | }
83 |
84 | @override
85 | Widget build(BuildContext context) {
86 | return FutureBuilder(
87 | future: dataFetchFuture,
88 | builder: (context, snapshot) {
89 | // Temporary loading screen until all data is loaded.
90 | if (snapshot.connectionState != ConnectionState.done) {
91 | return MaterialApp(
92 | theme: ThemeData.light(),
93 | darkTheme: ThemeData.dark(),
94 | home: Scaffold(
95 | body: Center(
96 | child: CircularProgressIndicator(),
97 | ),
98 | ),
99 | );
100 | }
101 |
102 | return Consumer(
103 | builder: (context, themeProvider, _) => MaterialApp(
104 | title: 'LibreBudgeteer',
105 | theme: themeProvider.themeData,
106 | initialRoute: '/',
107 | home: HomeTabsScreen(),
108 | routes: {
109 | EditLabelsScreen.routeName: (_) => EditLabelsScreen(),
110 | SettingsScreen.routeName: (_) => SettingsScreen(),
111 | //Onboarding.routeName: (_) => Onboarding()
112 | },
113 | ),
114 | );
115 | },
116 | );
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/app/windows/runner/win32_window.h:
--------------------------------------------------------------------------------
1 | #ifndef RUNNER_WIN32_WINDOW_H_
2 | #define RUNNER_WIN32_WINDOW_H_
3 |
4 | #include
5 |
6 | #include
7 | #include
8 | #include
9 |
10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be
11 | // inherited from by classes that wish to specialize with custom
12 | // rendering and input handling
13 | class Win32Window {
14 | public:
15 | struct Point {
16 | unsigned int x;
17 | unsigned int y;
18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {}
19 | };
20 |
21 | struct Size {
22 | unsigned int width;
23 | unsigned int height;
24 | Size(unsigned int width, unsigned int height)
25 | : width(width), height(height) {}
26 | };
27 |
28 | Win32Window();
29 | virtual ~Win32Window();
30 |
31 | // Creates a win32 window with |title| that is positioned and sized using
32 | // |origin| and |size|. New windows are created on the default monitor. Window
33 | // sizes are specified to the OS in physical pixels, hence to ensure a
34 | // consistent size this function will scale the inputted width and height as
35 | // as appropriate for the default monitor. The window is invisible until
36 | // |Show| is called. Returns true if the window was created successfully.
37 | bool Create(const std::wstring& title, const Point& origin, const Size& size);
38 |
39 | // Show the current window. Returns true if the window was successfully shown.
40 | bool Show();
41 |
42 | // Release OS resources associated with window.
43 | void Destroy();
44 |
45 | // Inserts |content| into the window tree.
46 | void SetChildContent(HWND content);
47 |
48 | // Returns the backing Window handle to enable clients to set icon and other
49 | // window properties. Returns nullptr if the window has been destroyed.
50 | HWND GetHandle();
51 |
52 | // If true, closing this window will quit the application.
53 | void SetQuitOnClose(bool quit_on_close);
54 |
55 | // Return a RECT representing the bounds of the current client area.
56 | RECT GetClientArea();
57 |
58 | protected:
59 | // Processes and route salient window messages for mouse handling,
60 | // size change and DPI. Delegates handling of these to member overloads that
61 | // inheriting classes can handle.
62 | virtual LRESULT MessageHandler(HWND window,
63 | UINT const message,
64 | WPARAM const wparam,
65 | LPARAM const lparam) noexcept;
66 |
67 | // Called when CreateAndShow is called, allowing subclass window-related
68 | // setup. Subclasses should return false if setup fails.
69 | virtual bool OnCreate();
70 |
71 | // Called when Destroy is called.
72 | virtual void OnDestroy();
73 |
74 | private:
75 | friend class WindowClassRegistrar;
76 |
77 | // OS callback called by message pump. Handles the WM_NCCREATE message which
78 | // is passed when the non-client area is being created and enables automatic
79 | // non-client DPI scaling so that the non-client area automatically
80 | // responds to changes in DPI. All other messages are handled by
81 | // MessageHandler.
82 | static LRESULT CALLBACK WndProc(HWND const window,
83 | UINT const message,
84 | WPARAM const wparam,
85 | LPARAM const lparam) noexcept;
86 |
87 | // Retrieves a class instance pointer for |window|
88 | static Win32Window* GetThisFromHandle(HWND const window) noexcept;
89 |
90 | // Update the window frame's theme to match the system theme.
91 | static void UpdateTheme(HWND const window);
92 |
93 | bool quit_on_close_ = false;
94 |
95 | // window handle for top level window.
96 | HWND window_handle_ = nullptr;
97 |
98 | // window handle for hosted content.
99 | HWND child_content_ = nullptr;
100 | };
101 |
102 | #endif // RUNNER_WIN32_WINDOW_H_
103 |
--------------------------------------------------------------------------------
/app/lib/widgets/transaction_details_charts_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:provider/provider.dart';
3 | import 'package:smooth_page_indicator/smooth_page_indicator.dart';
4 |
5 | import '../charts/chart_widgets/transaction_details_pie_chart.dart';
6 | import '../providers/transactions.dart';
7 | import '../utils/custom_colors.dart';
8 |
9 | // Used in TransactionDetailsScreen.
10 |
11 | class TransactionDetailsChartsView extends StatefulWidget {
12 | final String? transactionId;
13 |
14 | const TransactionDetailsChartsView({
15 | required this.transactionId,
16 | });
17 |
18 | @override
19 | _TransactionDetailsChartsViewState createState() =>
20 | _TransactionDetailsChartsViewState();
21 | }
22 |
23 | class _TransactionDetailsChartsViewState
24 | extends State {
25 | PageController _pageController = PageController();
26 |
27 | @override
28 | void dispose() {
29 | _pageController.dispose();
30 | super.dispose();
31 | }
32 |
33 | @override
34 | Widget build(BuildContext context) {
35 | final transactionsData = Provider.of(context);
36 | final transaction = transactionsData.findById(widget.transactionId)!;
37 | final label = transaction.getLabel(context)!;
38 |
39 | return Column(
40 | children: [
41 | // Need SizedBox or PageView will attempt to take up entire vertical viewport.
42 | SizedBox(
43 | height: 285,
44 | child: PageView(
45 | controller: _pageController,
46 | children: [
47 | // Both pie charts will show this month's statistics if the transaction is in the current month.
48 | // If not, it will show lifetime/total statistics.
49 | // This pie chart is for label statistics.
50 | TransactionDetailsPieChart(
51 | chartTitle:
52 | '${transaction.isAfterBeginningOfMonth ? 'Month\'s' : ''} Impact on Label ${label.title}',
53 | transactionTitle: transaction.title,
54 | otherTitle: 'Rest of ${label.title}',
55 | transactionAmount: transaction.amount,
56 | totalAmount: transaction.isAfterBeginningOfMonth
57 | ? label.getLabelMonthAmountTotal(context)
58 | : label.getLabelAmountTotal(context),
59 | mainColor: label.color,
60 | otherColor: label.color.withAlpha(125),
61 | ),
62 | // This pie chart is for general income/expense statistics.
63 | TransactionDetailsPieChart(
64 | chartTitle:
65 | '${transaction.isAfterBeginningOfMonth ? 'Month\'s' : ''} Impact on ${transaction.amount < 0 ? 'Expense' : 'Income'} Totals',
66 | transactionTitle: transaction.title,
67 | otherTitle:
68 | 'Rest of ${transaction.amount < 0 ? 'Expenses' : 'Income'}',
69 | transactionAmount: transaction.amount,
70 | totalAmount: transaction.amount > 0
71 | ? (transaction.isAfterBeginningOfMonth
72 | ? transactionsData.monthIncomeTotal
73 | : transactionsData.incomeTotal)
74 | : (transaction.isAfterBeginningOfMonth
75 | ? transactionsData.monthExpensesTotal
76 | : transactionsData.expensesTotal),
77 | mainColor: Theme.of(context)
78 | .colorScheme
79 | .transactionTypeColor(transaction.amount),
80 | otherColor: Theme.of(context)
81 | .colorScheme
82 | .secondaryTransactionTypeColor(transaction.amount),
83 | ),
84 | ],
85 | ),
86 | ),
87 | const SizedBox(height: 15),
88 | SmoothPageIndicator(
89 | controller: _pageController,
90 | count: 2
91 | )
92 | ],
93 | );
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/plaid-sync/webserver.py:
--------------------------------------------------------------------------------
1 | #!python
2 | """
3 | Wraps a very simple webserver designed only for serving up the HTML to
4 | run the Plaid Link service and capture the response JSON from the Plaid Link
5 | API. This API must be run in the web browser, so this does the bare minimum
6 | to accomplish this.
7 |
8 | https://plaid.com/docs/link/
9 | """
10 |
11 | import json
12 | import logging
13 | import mimetypes
14 | import sys
15 | from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
16 | from typing import Dict
17 |
18 | log = logging.getLogger(__name__)
19 |
20 |
21 | class DataStore:
22 | def __init__(self, config_json: Dict):
23 | self.config_json: Dict = config_json
24 | self.plaid_response: Dict = None
25 |
26 |
27 | class PlaidLinkHTTPServer(BaseHTTPRequestHandler):
28 | def __init__(self, data_store: DataStore, *args, **kwargs):
29 | self.data_store = data_store
30 | super().__init__( *args, **kwargs)
31 |
32 | def serve_file(self, file_path: str):
33 | mimetype = mimetypes.guess_type(file_path)
34 | self.send_response(200)
35 | self.send_header('Content-type', mimetype[0])
36 | self.end_headers()
37 |
38 | with open(file_path, "r") as f:
39 | html = f.read()
40 |
41 | html = html.replace(
42 | "{{CONFIG_JSON}}",
43 | json.dumps(self.data_store.config_json)
44 | )
45 |
46 | self.wfile.write(html.encode('utf-8'))
47 |
48 | self.wfile.flush()
49 |
50 | def log_request(self, code=None, size=None) -> None:
51 | pass
52 |
53 | def send_404(self):
54 | self.send_response(404)
55 | self.send_header('Content-type', "text/plain")
56 | self.end_headers()
57 | self.wfile.write(b"not found")
58 | self.wfile.flush()
59 |
60 | def do_POST(self):
61 | path = self.path.split("?")[0]
62 | if path == "/api/success":
63 | cl = int(self.headers.get('Content-Length', 0))
64 | body = self.rfile.read(cl)
65 |
66 | self.data_store.plaid_response = json.loads(body)
67 |
68 | self.server.shutdown()
69 | self.server.server_close()
70 |
71 | return
72 | else:
73 | self.send_404()
74 |
75 | def do_GET(self):
76 | path = self.path.split("?")[0]
77 |
78 | if path == "/link.html":
79 | self.serve_file("html/link.html")
80 | return
81 | else:
82 | self.send_404()
83 |
84 |
85 | def serve(env: str, clientName: str, token: str, pageTitle: str, accountName: str, type: str) -> Dict:
86 | """
87 | Starts a webserver and serves the html/link.html file with the
88 | specified configuration.
89 |
90 | Host and port will be 127.0.0.1:4583
91 |
92 | Returns the JSON returned by the Plaid Link API when the user has successfully
93 | finished the authorization flow.
94 | """
95 |
96 | config_json = dict(
97 | env=env,
98 | clientName=clientName,
99 | token=token,
100 | pageTitle=pageTitle,
101 | accountName=accountName,
102 | type=type
103 | )
104 |
105 | ds: DataStore = DataStore(config_json)
106 |
107 | def make_handler(*args, **kwargs):
108 | return PlaidLinkHTTPServer(ds, *args, **kwargs)
109 |
110 | with ThreadingHTTPServer(('127.0.0.1', 4583), make_handler) as httpd:
111 | host, port = httpd.socket.getsockname()
112 | print('Open the following page in your browser to continue:')
113 | print(f' http://{host}:{port}/link.html')
114 |
115 | try:
116 | # well, until the API to shutdown is called
117 | httpd.serve_forever()
118 | except KeyboardInterrupt:
119 | print("Keyboard interrupt received, exiting.")
120 | sys.exit(0)
121 |
122 | return ds.plaid_response
123 |
124 |
125 | if __name__ == '__main__':
126 | serve({})
127 |
--------------------------------------------------------------------------------
/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
43 |
49 |
50 |
51 |
52 |
53 |
63 |
65 |
71 |
72 |
73 |
74 |
80 |
82 |
88 |
89 |
90 |
91 |
93 |
94 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
43 |
49 |
50 |
51 |
52 |
53 |
63 |
65 |
71 |
72 |
73 |
74 |
80 |
82 |
88 |
89 |
90 |
91 |
93 |
94 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/plaid-sync/README.md:
--------------------------------------------------------------------------------
1 |
2 | Original sources taken from https://github.com/mbafford/plaid-sync
3 |
4 | # Overview
5 |
6 | `plaid-sync` is a Python based command-line interface to the [Plaid API](https://plaid.com/docs/api/) that synchronizes your bank/credit card transactions to InfluxDB Database
7 |
8 | # Usage
9 |
10 | ## Installation
11 |
12 | python[3] -m pip install -r requirements.txt
13 |
14 | ## Configuration
15 |
16 | Establish a configuration file with your Plaid credentials. This is in standard INI format. There is an example file `config/sandbox.example`. Copy this file to sandbox.example in the root directory.
17 |
18 | Once you've set up the basic credentials, run through linking a new account:
19 |
20 | ```$ python[3] plaid-sync.py -c sandbox.example --link 'Test Chase'
21 |
22 | Open the following page in your browser to continue:
23 | http://127.0.0.1:4583/link.html
24 | ```
25 |
26 | Open the above link, follow the instructions (click the button, find your bank, enter credentials).
27 |
28 | The console will then update with confirmation:
29 |
30 | ```Public token obtained [public-sandbox-XXXX]. Exchanging for access token.
31 | Access token received: access-sandbox-XXXX
32 |
33 | Saving new link to configuration file
34 | Backing up existing configuration to: config/sandbox.1605537592.bkp
35 | Overwriting existing config file: config/sandbox
36 |
37 | Test Chase is linked and is ready to sync.
38 | ```
39 |
40 | Your config file will have updated to have a line for this new account:
41 |
42 | ```
43 | [Test Chase]
44 | access_token = access-sandbox-XXXX
45 | ```
46 |
47 | And you can now run the sync process:
48 |
49 | ```
50 | $ ./plaid-sync.py -c sandbox.example -v -b
51 |
52 | ```
53 |
54 | ## Updating an Expired Account
55 |
56 | Occasionally you'll get an error like this while syncing:
57 |
58 | ```./plaid-sync.py -c sandbox.example
59 |
60 | Finished syncing 2 Plaid accounts
61 |
62 | Test Chase : 0 new transactions (0 pending), 0 archived transactions over 0 accounts
63 | : *** Plaid Error ***
64 | : ITEM_LOGIN_REQUIRED: the login details
65 | : of this item have changed (credentials,
66 | : MFA, or required user action) and a user
67 | : login is required to update this
68 | : information. use Link's update mode to
69 | : restore the item to a good state
70 | : *** re-run with: ***
71 | : --update 'Test Chase'
72 | : to fix
73 | ```
74 |
75 | This just means your bank either isn't accepting the old credentials, or has a setup/arrangement where the login needs to be refreshed periodically.
76 |
77 | This process requires the Plaid Link (web browser) process again, but it's fairly painless.
78 |
79 | Just run the update process:
80 |
81 | ```
82 | $ ./plaid-sync.py -c sandbox.example --update 'Test Chase'
83 | Starting account update process for [Test Chase]
84 |
85 | Open the following page in your browser to continue:
86 | http://127.0.0.1:4583/link.html
87 | ```
88 |
89 | Open the page in your browser, click the button, enter new credentials, return to the console, confirm the process completed:
90 |
91 | ```
92 | Public token obtained [public-sandbox-be30eb9a-8bcb-4dd0-9cf5-048ca7dfa5a3].
93 |
94 | There is nothing else to do, the account should sync properly now with the existing credentials.
95 | ```
96 |
97 | The sync process should run normally again.
98 |
99 | # WARNINGS
100 |
101 | When linking/setting up a new account, your public token (temporary) and access token (permanent) cannot be recovered if lost. I've taken care to show them to you during this process in both the browser and the command line so you can recover the flow if
102 | something goes wrong. Once you've saved the access token, you don't need the public token anymore.
103 |
104 | This is important for accounts in the "test" level, as there is a 100 lifetime account limit.
105 |
106 |
--------------------------------------------------------------------------------
/app/lib/widgets/edit_transaction_appbar.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | // Used in EditTransactionScreen.
4 | // The header containing the close button, section title, and the title form field.
5 |
6 | class EditTransactionAppbar extends StatelessWidget {
7 | final Color containerColor;
8 | final Function closeScreen;
9 | final String submitButtonText;
10 | final Function onButtonSubmit;
11 | final Widget titleFormField;
12 |
13 | const EditTransactionAppbar({
14 | required this.containerColor,
15 | required this.closeScreen,
16 | required this.submitButtonText,
17 | required this.onButtonSubmit,
18 | required this.titleFormField,
19 | });
20 |
21 | Widget buildCloseButton(BuildContext context) {
22 | return IconButton(
23 | // Padding is 0 to get icon to align with the title form field.
24 | padding: const EdgeInsets.all(0),
25 | alignment: Alignment.topLeft,
26 | icon: Icon(
27 | Icons.close,
28 | color: Theme.of(context).colorScheme.onPrimary,
29 | ),
30 | onPressed: closeScreen as void Function()?,
31 | );
32 | }
33 |
34 | Widget buildSubmitButton(BuildContext context) {
35 | final themeData = Theme.of(context);
36 | return FittedBox(
37 | child: TextButton(
38 | // Had to do this hacky ButtonTheme stuff to get it aligned to the close button.
39 | style: ButtonStyle(
40 | overlayColor: MaterialStateProperty.resolveWith(
41 | (Set states) {
42 | if (states.contains(MaterialState.hovered))
43 | return Colors.black.withOpacity(0.04);
44 | if (states.contains(MaterialState.focused) ||
45 | states.contains(MaterialState.pressed))
46 | return Colors.black.withOpacity(0.12);
47 | return null; // Defer to the widget's default.
48 | },
49 | ),
50 | padding: MaterialStateProperty.all(
51 | const EdgeInsets.only(bottom: 20, left: 20),
52 | ),
53 | ),
54 | child: Text(
55 | submitButtonText,
56 | style: themeData.textTheme.titleLarge!.copyWith(
57 | color: themeData.colorScheme.onPrimary,
58 | ),
59 | ),
60 | onPressed: onButtonSubmit as void Function()?,
61 | ),
62 | );
63 | }
64 |
65 | @override
66 | Widget build(BuildContext context) {
67 | // Animated because its color can change.
68 | return AnimatedContainer(
69 | height: 200,
70 | width: double.infinity,
71 | duration: Duration(milliseconds: 250),
72 | decoration: BoxDecoration(
73 | color: containerColor,
74 | // Just so it can have a neat shadow under the header.
75 | boxShadow: [
76 | BoxShadow(
77 | color: Theme.of(context).brightness == Brightness.light
78 | ? Colors.black26
79 | : Colors.black12,
80 | spreadRadius: 3,
81 | blurRadius: 3,
82 | ),
83 | ],
84 | ),
85 | // SafeArea here so that the close button is not in status bar. We don't want SafeArea surrounding the Container
86 | // because we want part of the colored Container to be under the status bar to look better.
87 | child: SafeArea(
88 | child: Padding(
89 | padding: const EdgeInsets.fromLTRB(15, 15, 15, 0),
90 | child: Column(
91 | children: [
92 | Row(
93 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
94 | crossAxisAlignment: CrossAxisAlignment.start,
95 | children: [
96 | buildCloseButton(context),
97 | buildSubmitButton(context),
98 | ],
99 | ),
100 | const SizedBox(height: 12),
101 | titleFormField,
102 | ],
103 | ),
104 | ),
105 | ),
106 | );
107 | }
108 | }
109 |
--------------------------------------------------------------------------------