├── 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 | --------------------------------------------------------------------------------