├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── my_expense_tracker │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── 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 │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── images │ ├── 2.0x │ │ └── flutter_logo.png │ ├── 3.0x │ │ └── flutter_logo.png │ └── flutter_logo.png └── sample_transactions.json ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── 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-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h └── RunnerTests │ └── RunnerTests.swift ├── l10n.yaml ├── lib ├── main.dart └── src │ ├── app.dart │ ├── data │ ├── data.dart │ └── tz.json │ ├── localization │ ├── app_am.arb │ └── app_en.arb │ ├── models │ ├── db.dart │ └── transaction.dart │ ├── sample_feature │ ├── sample_item.dart │ ├── sample_item_details_view.dart │ └── sample_item_list_view.dart │ ├── screen │ ├── error.dart │ ├── home │ │ ├── home.dart │ │ ├── main_screen.dart │ │ └── recent_transactions.dart │ └── transactions │ │ ├── transactions_details.dart │ │ ├── transactions_list copy.dart │ │ ├── transactions_list.dart │ │ └── transactions_list_item.dart │ ├── services │ ├── sms │ │ ├── cbe_sms_parser.dart │ │ ├── sms_service.dart │ │ └── telebirr_receipt_parser.dart │ └── transactions │ │ └── transaction_service.dart │ ├── settings │ ├── settings_controller.dart │ ├── settings_service.dart │ └── settings_view.dart │ ├── theme.dart │ ├── theme │ └── theme_type.dart │ └── utils │ ├── background_service.dart │ ├── cbe_client.dart │ ├── constants.dart │ ├── helpers.dart │ ├── populate_sms.dart │ └── types.dart ├── linux ├── .gitignore ├── CMakeLists.txt ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake └── runner │ ├── CMakeLists.txt │ ├── main.cc │ ├── my_application.cc │ └── my_application.h ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── app_icon_1024.png │ │ │ ├── app_icon_128.png │ │ │ ├── app_icon_16.png │ │ │ ├── app_icon_256.png │ │ │ ├── app_icon_32.png │ │ │ ├── app_icon_512.png │ │ │ └── app_icon_64.png │ ├── Base.lproj │ │ └── MainMenu.xib │ ├── Configs │ │ ├── AppInfo.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── Release.entitlements └── RunnerTests │ └── RunnerTests.swift ├── plan.json ├── plan.md ├── pubspec.lock ├── pubspec.yaml ├── schema.md ├── test ├── unit_test.dart └── widget_test.dart ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── index.html └── manifest.json └── windows ├── .gitignore ├── CMakeLists.txt ├── flutter ├── CMakeLists.txt ├── generated_plugin_registrant.cc ├── generated_plugin_registrant.h └── generated_plugins.cmake └── runner ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release APK 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Setup Java 15 | uses: actions/setup-java@v3 16 | with: 17 | distribution: 'zulu' 18 | java-version: '17' 19 | 20 | - name: Setup Flutter 21 | uses: subosito/flutter-action@v2 22 | with: 23 | channel: stable 24 | flutter-version-file: pubspec.yaml 25 | 26 | - name: Get dependencies 27 | run: flutter pub get 28 | 29 | - name: Build APK 30 | run: flutter build apk --release 31 | 32 | - name: Create Release 33 | id: create_release 34 | uses: softprops/action-gh-release@v1 35 | with: 36 | files: build/app/outputs/flutter-apk/app-release.apk 37 | draft: false 38 | prerelease: false 39 | generate_release_notes: true 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Temkin Mengsitu 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Muday - Ethiopian Expense Tracker 2 | 3 |
4 | 5 | 6 | 7 | [![Build Status](https://github.com/yourusername/muday/workflows/Build/badge.svg)](https://github.com/yourusername/muday/actions) 8 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 9 | [![API Level](https://img.shields.io/badge/API-24%2B-brightgreen.svg)](https://android-arsenal.com/api?level=24) 10 | 11 |
12 | 13 | Muday is a comprehensive expense tracking application designed specifically for Ethiopian users. It automatically tracks your expenses by parsing SMS notifications from major Ethiopian banks and mobile money services, including Telebirr, Commercial Bank of Ethiopia (CBE), and Bank of Abyssinia. 14 | 15 | ## Features 16 | 17 | ### 🔄 Automatic Transaction Tracking 18 | - Real-time SMS parsing for automatic expense tracking 19 | - Support for multiple Ethiopian banks and payment services: 20 | - Telebirr 21 | - Commercial Bank of Ethiopia (CBE) 22 | - Bank of Abyssinia 23 | - More banks coming soon 24 | - Automatic parsing of transaction details including: 25 | - Transaction amount 26 | - Balance 27 | - Reference numbers 28 | - Sender/receiver information 29 | - Commission and VAT charges 30 | 31 | ### 📊 Smart Analytics 32 | - Detailed transaction analytics and visualization 33 | - AI-powered expense categorization using Google's Gemini AI 34 | - Custom categorization options 35 | - Flexible date range filtering: 36 | - Daily 37 | - Weekly 38 | - Monthly 39 | - Yearly 40 | - Custom date ranges 41 | 42 | ### 💰 Budget Management 43 | - Set and track budgets for different time periods 44 | - Budget alerts and notifications 45 | - Category-based budget allocation 46 | 47 | ### ☁️ Cloud Features 48 | - Secure cloud backup with Firebase 49 | - Cross-device synchronization 50 | - Data export in multiple formats: 51 | - CSV 52 | - JSON 53 | - PDF reports 54 | - Share transactions with other Muday users 55 | 56 | ## Getting Started 57 | 58 | ### Prerequisites 59 | - VS Code or Android Studio 60 | - Android SDK 24 or higher 61 | - Firebase account for cloud features 62 | - Google Cloud account for AI features 63 | 64 | ### Installation 65 | 1. Clone the repository: 66 | ```bash 67 | git clone https://github.com/chapimenge3/Muday.git 68 | ``` 69 | 70 | 2. Open the project in Android Studio or VS Code 71 | 72 | 3. Create a Firebase project and add the `google-services.json` file to the app directory(NOT IMPLEMENTED SKIP this) 73 | 74 | 4. Configure your Google Cloud credentials for Gemini AI integration(NOT IMPLEMENTED SKIP this) 75 | 76 | 5. Build and run the project 77 | 78 | ### Configuration 79 | 1. Enable SMS permissions when prompted 80 | 2. Set up your preferred banks and payment services 81 | 3. Configure cloud backup settings (optional) 82 | 4. Set up your budget preferences 83 | 84 | ## Architecture (TO BE IMPLEMENTED) 85 | Muday follows the MVVM architecture pattern and is built with modern Android development practices: 86 | 87 | - **UI Layer**: Jetpack Compose 88 | - **Business Logic**: ViewModel + Use Cases 89 | - **Data Layer**: Repository Pattern 90 | - **Local Storage**: Room Database 91 | - **Cloud Storage**: Firebase 92 | - **Dependencies**: Hilt for dependency injection 93 | 94 | ## Contributing 95 | 96 | We welcome contributions! Please read our [Contributing Guidelines](CONTRIBUTING.md) before submitting pull requests. 97 | 98 | ### Development Setup 99 | 1. Fork the repository 100 | 2. Create a new branch for your feature 101 | 3. Implement your changes 102 | 4. Submit a pull request 103 | 104 | ## Privacy & Security 105 | 106 | Muday takes your financial privacy seriously: 107 | - All sensitive data is encrypted 108 | - SMS parsing happens locally on your device 109 | - Cloud sync is optional and secured with Firebase Authentication 110 | - No sensitive data is shared without explicit user consent 111 | 112 | ## Support 113 | 114 | For support, please: 115 | - Check our [Documentation](docs/README.md) 116 | - Visit our [Issues](https://github.com/chapimenge3/muday/issues) page 117 | - Join our [Telegram Community](https://t.me/chapidevtalks) 118 | 119 | ## License 120 | 121 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 122 | 123 | ## Acknowledgments 124 | 125 | - Thanks to all contributors and users 126 | - Special thanks to the Ethiopian developer community 127 | - Icons and graphics from [Material Design Icons](https://material.io/icons) 128 | 129 | --- 130 | 131 |
132 | Made with ❤️ in Ethiopia 133 |
-------------------------------------------------------------------------------- /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 https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/to/reference-keystore 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 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.chapimenge.muday" 10 | compileSdk = flutter.compileSdkVersion 11 | ndkVersion = flutter.ndkVersion 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_1_8 15 | targetCompatibility = JavaVersion.VERSION_1_8 16 | } 17 | 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_1_8 20 | } 21 | 22 | defaultConfig { 23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 24 | applicationId = "com.chapimenge.muday" 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.debug 38 | } 39 | } 40 | } 41 | 42 | flutter { 43 | source = "../.." 44 | } 45 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 17 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 35 | 36 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/my_expense_tracker/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.my_expense_tracker 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = "../build" 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(":app") 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /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.3-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return 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.2.1" apply false 22 | id "org.jetbrains.kotlin.android" version "1.8.22" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /assets/images/2.0x/flutter_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/assets/images/2.0x/flutter_logo.png -------------------------------------------------------------------------------- /assets/images/3.0x/flutter_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/assets/images/3.0x/flutter_logo.png -------------------------------------------------------------------------------- /assets/images/flutter_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/assets/images/flutter_logo.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | My Expense Tracker 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | my_expense_tracker 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 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/src/localization 2 | template-arb-file: app_en.arb 3 | output-localization-file: app_localizations.dart 4 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:muday/src/models/db.dart'; 4 | import 'package:muday/src/utils/background_service.dart'; 5 | 6 | // import 'package:muday/src/utils/sms_parsers.dart'; 7 | 8 | import 'src/app.dart'; 9 | import 'src/settings/settings_controller.dart'; 10 | import 'src/settings/settings_service.dart'; 11 | 12 | void main() async { 13 | // Set up the SettingsController, which will glue user settings to multiple 14 | // Flutter Widgets. 15 | final settingsController = SettingsController(SettingsService()); 16 | 17 | // Load the user's preferred theme while the splash screen is displayed. 18 | // This prevents a sudden theme change when the app is first displayed. 19 | await settingsController.loadSettings(); 20 | 21 | print('Settings loaded!'); 22 | 23 | WidgetsFlutterBinding.ensureInitialized(); 24 | 25 | // Run background service 26 | await initializeService(); 27 | 28 | // // create a database 29 | final db = DatabaseService.instance; 30 | // print('Database initialized: $db'); 31 | // print('Database getTables: ${await db.getTables()}'); 32 | 33 | // Run the app and pass in the SettingsController. The app listens to the 34 | // SettingsController for changes, then passes it further down to the 35 | // SettingsView. 36 | runApp(MyApp(settingsController: settingsController, db: db)); 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 3 | import 'package:flutter_localizations/flutter_localizations.dart'; 4 | import 'package:muday/src/models/db.dart'; 5 | import 'package:muday/src/models/transaction.dart'; 6 | import 'package:muday/src/screen/error.dart'; 7 | import 'package:muday/src/screen/home/home.dart'; 8 | import 'package:muday/src/screen/transactions/transactions_details.dart'; 9 | import 'package:muday/src/screen/transactions/transactions_list.dart'; 10 | import 'package:permission_handler/permission_handler.dart'; 11 | 12 | import 'theme.dart'; 13 | 14 | import 'sample_feature/sample_item_details_view.dart'; 15 | import 'sample_feature/sample_item_list_view.dart'; 16 | import 'settings/settings_controller.dart'; 17 | import 'settings/settings_view.dart'; 18 | 19 | class Routes { 20 | static const String settings = '/settings'; 21 | static const String sampleItemDetails = '/item-details'; 22 | static const String sampleItemList = '/item-list'; 23 | static const String transactionDetails = '/transaction-details'; 24 | static const String transactionList = '/transaction-list'; 25 | } 26 | 27 | /// The Widget that configures your application. 28 | class MyApp extends StatelessWidget { 29 | const MyApp({super.key, required this.settingsController, required this.db}); 30 | 31 | final SettingsController settingsController; 32 | final DatabaseService db; 33 | 34 | Future _checkPermissionAndReadSMS() async { 35 | var permission = await Permission.sms.status; 36 | if (!permission.isGranted) { 37 | permission = await Permission.sms.request(); 38 | if (!permission.isGranted) return false; 39 | } 40 | return true; 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | final materialTheme = MaterialTheme(Theme.of(context).textTheme); 46 | // Glue the SettingsController to the MaterialApp. 47 | // 48 | // The ListenableBuilder Widget listens to the SettingsController for changes. 49 | // Whenever the user updates their settings, the MaterialApp is rebuilt. 50 | 51 | return ListenableBuilder( 52 | listenable: settingsController, 53 | builder: (BuildContext context, Widget? child) { 54 | return MaterialApp( 55 | // Providing a restorationScopeId allows the Navigator built by the 56 | // MaterialApp to restore the navigation stack when a user leaves and 57 | // returns to the app after it has been killed while running in the 58 | // background. 59 | restorationScopeId: 'app', 60 | 61 | debugShowCheckedModeBanner: false, 62 | 63 | // Provide the generated AppLocalizations to the MaterialApp. This 64 | // allows descendant Widgets to display the correct translations 65 | // depending on the user's locale. 66 | localizationsDelegates: const [ 67 | AppLocalizations.delegate, 68 | GlobalMaterialLocalizations.delegate, 69 | GlobalWidgetsLocalizations.delegate, 70 | GlobalCupertinoLocalizations.delegate, 71 | ], 72 | supportedLocales: const [ 73 | Locale('en', ''), // English, no country code 74 | Locale('am', ''), // Amharic, no country code 75 | ], 76 | 77 | // Use AppLocalizations to configure the correct application title 78 | // depending on the user's locale. 79 | // 80 | // The appTitle is defined in .arb files found in the localization 81 | // directory. 82 | onGenerateTitle: (BuildContext context) => 83 | AppLocalizations.of(context)!.appTitle, 84 | 85 | // Define a light and dark color theme. Then, read the user's 86 | // preferred ThemeMode (light, dark, or system default) from the 87 | // SettingsController to display the correct theme. 88 | // theme: ThemeData( 89 | // // colorScheme: ColorScheme.light() 90 | // colorScheme: ColorScheme.light( 91 | // primary: Color(0xFF36618e), 92 | // secondary: Color(0xFF535f70), 93 | // tertiary: Color(0xFF6b5778), 94 | // error: Color(0xFFba1a1a), 95 | // ), 96 | // ), 97 | // darkTheme: ThemeData( 98 | // colorScheme: ColorScheme.dark( 99 | // primary: Color(0xFFa0cafd), 100 | // secondary: Color(0xFFbbc7db), 101 | // tertiary: Color(0xFFd6bee4), 102 | // error: Color(0xFFffb4ab), 103 | // ), 104 | // ), 105 | theme: materialTheme.light(), 106 | darkTheme: materialTheme.dark(), 107 | themeMode: settingsController.themeMode, 108 | home: FutureBuilder( 109 | future: _checkPermissionAndReadSMS(), 110 | builder: (context, snapshot) { 111 | if (snapshot.connectionState == ConnectionState.waiting) { 112 | return const Scaffold( 113 | body: Center( 114 | child: CircularProgressIndicator(), 115 | ), 116 | ); 117 | } 118 | 119 | if (snapshot.hasError || snapshot.data == false) { 120 | return const Scaffold( 121 | body: Center( 122 | child: Text( 123 | 'SMS permission is required to use this app', 124 | style: TextStyle(color: Colors.red), 125 | ), 126 | ), 127 | ); 128 | } 129 | 130 | return const HomeScreen(); 131 | }, 132 | ), 133 | // home: const HomeScreen(), 134 | 135 | // Define a function to handle named routes in order to support 136 | // Flutter web url navigation and deep linking. 137 | onGenerateRoute: (RouteSettings routeSettings) { 138 | return MaterialPageRoute( 139 | settings: routeSettings, 140 | builder: (BuildContext context) { 141 | switch (routeSettings.name) { 142 | case Routes.settings: 143 | return SettingsView(controller: settingsController); 144 | 145 | case Routes.sampleItemDetails: 146 | return const SampleItemDetailsView(); 147 | 148 | case Routes.transactionDetails: 149 | // Extract the transaction from arguments 150 | final args = 151 | routeSettings.arguments as Map?; 152 | final transaction = args?['transaction']; 153 | 154 | if (transaction == null) { 155 | return const ErrorView(message: 'Transaction not found'); 156 | } 157 | 158 | return TransactionDetail( 159 | transaction: transaction as TransactionCls); 160 | case Routes.transactionList: 161 | return const TransactionListView(); 162 | 163 | case Routes.sampleItemList: 164 | default: 165 | return const SampleItemListView(); 166 | } 167 | }, 168 | ); 169 | }, 170 | ); 171 | }, 172 | ); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lib/src/data/data.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | import 'package:muday/src/utils/types.dart'; 5 | 6 | final List sampleTransactions = [ 7 | Transaction( 8 | id: '1', 9 | title: 'Shopping', 10 | description: 'Buy some grocery', 11 | amount: 120.00, 12 | dateTime: DateTime.now(), 13 | category: 'Shopping', 14 | icon: '🛍️', 15 | isExpense: true, 16 | ), 17 | Transaction( 18 | id: '2', 19 | title: 'Subscription', 20 | description: 'Disney+ Annual Plan', 21 | amount: 80.00, 22 | dateTime: DateTime.now().subtract(Duration(hours: 1)), 23 | category: 'Entertainment', 24 | icon: '📱', 25 | isExpense: true, 26 | ), 27 | Transaction( 28 | id: '3', 29 | title: 'Buy Phone', 30 | description: 'Buy a Samsung S24 Ultra', 31 | amount: 145_000.00, 32 | dateTime: DateTime.now().subtract(Duration(hours: 3)), 33 | category: 'Utilities', 34 | icon: '📱', 35 | isExpense: true, 36 | ), 37 | Transaction( 38 | id: '4', 39 | title: 'Salary', 40 | description: 'Salary for July', 41 | amount: 5000.00, 42 | dateTime: DateTime.now().subtract(Duration(hours: 12)), 43 | category: 'Income', 44 | icon: '💰', 45 | isExpense: false, 46 | ), 47 | Transaction( 48 | id: '5', 49 | title: 'Transportation', 50 | description: 'Charging Tesla', 51 | amount: 18.00, 52 | dateTime: DateTime.now().subtract(Duration(days: 1)), 53 | category: 'Transport', 54 | icon: '🚗', 55 | isExpense: true, 56 | ), 57 | Transaction( 58 | id: '6', 59 | title: 'Buy House', 60 | description: 'Buy a new house', 61 | amount: 1_500_000.00, 62 | dateTime: DateTime.now().subtract(Duration(days: 4)), 63 | category: 'Housing', 64 | icon: '🏠', 65 | isExpense: true, 66 | ), 67 | // ... Add more sample transactions 68 | ] + List.generate(14, (index) => Transaction( 69 | id: (index + 7).toString(), 70 | title: ['Coffee', 'Lunch', 'Dinner', 'Movies', 'Shopping', 'Gas', 'Internet'][index % 7], 71 | description: 'Transaction description', 72 | amount: (index + 1) * 10.0, 73 | dateTime: DateTime.now().subtract(Duration(days: (index + 1) * 7)), 74 | category: ['Food', 'Entertainment', 'Transport', 'Utilities'][index % 4], 75 | icon: ['☕', '🍽️', '🎬', '🛒', '⛽', '🌐'][index % 6], 76 | isExpense: true, 77 | )); 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /lib/src/localization/app_am.arb: -------------------------------------------------------------------------------- 1 | { 2 | "appTitle": "ወጪ መቆጣጠሪያ | ኢትዮጵያ", 3 | "@appTitle": { 4 | "description": "ወጪ መቆጣጠሪያ መተግበሪያ" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/src/localization/app_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "appTitle": "Expense Tracker | ET", 3 | "@appTitle": { 4 | "description": "Tracking expenses made easy" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/src/models/db.dart: -------------------------------------------------------------------------------- 1 | import 'package:muday/src/models/transaction.dart'; 2 | import 'package:path/path.dart'; 3 | import 'package:sqflite/sqflite.dart'; 4 | 5 | class DatabaseService { 6 | static const _databaseName = "expense_tracker.db"; 7 | static const _databaseVersion = 1; 8 | static Database? _database; 9 | 10 | // Singleton pattern 11 | DatabaseService._privateConstructor(); 12 | static final DatabaseService instance = DatabaseService._privateConstructor(); 13 | 14 | Future get database async { 15 | _database ??= await _initDatabase(); 16 | return _database!; 17 | } 18 | 19 | Future _initDatabase() async { 20 | print('Initializing database'); 21 | String path = join(await getDatabasesPath(), _databaseName); 22 | return await openDatabase( 23 | path, 24 | version: _databaseVersion, 25 | onCreate: _onCreate, 26 | onUpgrade: _onUpgrade, 27 | ); 28 | } 29 | 30 | Future _onCreate(Database db, int version) async { 31 | // User table 32 | await db.execute(''' 33 | CREATE TABLE users ( 34 | id TEXT PRIMARY KEY, 35 | name TEXT, 36 | email TEXT, 37 | phone TEXT, 38 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP 39 | ) 40 | '''); 41 | 42 | // Transaction table 43 | await db.execute(transactionDbCreateQuery); 44 | 45 | // checkpoint table with automatic date and time 46 | await db.execute(''' 47 | CREATE TABLE checkpoints ( 48 | id TEXT PRIMARY KEY, 49 | name TEXT NOT NULL, 50 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP 51 | ) 52 | '''); 53 | 54 | print('Database created'); 55 | } 56 | 57 | Future _onUpgrade(Database db, int oldVersion, int newVersion) async { 58 | // Handle database upgrades here 59 | print('Database upgraded from $oldVersion to $newVersion'); 60 | } 61 | 62 | // Generic CRUD operations 63 | Future insert(String table, Map row) async { 64 | Database db = await database; 65 | return await db.insert(table, row); 66 | } 67 | 68 | Future>> queryAll(String table) async { 69 | Database db = await database; 70 | return await db.query(table); 71 | } 72 | 73 | Future>> query( 74 | String table, { 75 | String? where, 76 | List? whereArgs, 77 | String? groupBy, 78 | String? having, 79 | String? orderBy, 80 | int? limit, 81 | int? offset, 82 | }) async { 83 | Database db = await database; 84 | return await db.query( 85 | table, 86 | where: where, 87 | whereArgs: whereArgs, 88 | orderBy: orderBy, 89 | limit: limit, 90 | offset: offset, 91 | ); 92 | } 93 | 94 | Future update( 95 | String table, 96 | Map row, 97 | String where, 98 | List whereArgs, 99 | ) async { 100 | Database db = await database; 101 | return await db.update(table, row, where: where, whereArgs: whereArgs); 102 | } 103 | 104 | Future delete( 105 | String table, String where, List whereArgs) async { 106 | Database db = await database; 107 | return await db.delete(table, where: where, whereArgs: whereArgs); 108 | } 109 | 110 | // Transaction specific operations 111 | Future>> getTransactions({ 112 | String? userId, 113 | DateTime? startDate, 114 | DateTime? endDate, 115 | String? type, 116 | }) async { 117 | Database db = await database; 118 | List whereConditions = []; 119 | List whereArgs = []; 120 | 121 | if (userId != null) { 122 | whereConditions.add('user_id = ?'); 123 | whereArgs.add(userId); 124 | } 125 | if (startDate != null) { 126 | whereConditions.add('date >= ?'); 127 | whereArgs.add(startDate.toIso8601String()); 128 | } 129 | if (endDate != null) { 130 | whereConditions.add('date <= ?'); 131 | whereArgs.add(endDate.toIso8601String()); 132 | } 133 | if (type != null) { 134 | whereConditions.add('type = ?'); 135 | whereArgs.add(type); 136 | } 137 | 138 | String? where = 139 | whereConditions.isEmpty ? null : whereConditions.join(' AND '); 140 | 141 | return await db.query( 142 | 'transactions', 143 | where: where, 144 | whereArgs: whereArgs, 145 | orderBy: 'date DESC', 146 | ); 147 | } 148 | 149 | // return all tables in the database 150 | Future>> getTables() async { 151 | Database db = await database; 152 | return await db 153 | .query('sqlite_master', where: 'type = ?', whereArgs: ['table']); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lib/src/models/transaction.dart: -------------------------------------------------------------------------------- 1 | class TransactionCls { 2 | final String id; 3 | final TransactionWallet wallet; // enum: CBE, TELEBIRR 4 | final TransactionType type; // enum: DEBITED, CREDITED 5 | final double amount; 6 | final double? serviceFee; 7 | final double? vat; 8 | final DateTime? date; 9 | final String? referenceNumber; 10 | 11 | // Common optional fields from PDF 12 | final String? payer; 13 | final String? payerAccount; 14 | final String? receiver; 15 | final String? receiverAccount; 16 | final String? reason; 17 | 18 | // TeleBirr specific fields 19 | final String? payerAccountType; 20 | final String? payerTinNumber; 21 | final TransactionStatus? status; 22 | final double? stampDuty; 23 | final double? discount; 24 | final String? channel; 25 | 26 | // Computed fields 27 | double get total => 28 | amount + 29 | (serviceFee ?? 0) + 30 | (vat ?? 0) + 31 | (stampDuty ?? 0) - 32 | (discount ?? 0); 33 | 34 | TransactionCls({ 35 | required this.id, 36 | required this.wallet, 37 | required this.type, 38 | required this.amount, 39 | required this.date, 40 | this.serviceFee, 41 | this.vat, 42 | this.referenceNumber, 43 | this.payer, 44 | this.payerAccount, 45 | this.receiver, 46 | this.receiverAccount, 47 | this.reason, 48 | this.payerAccountType, 49 | this.payerTinNumber, 50 | this.status, 51 | this.stampDuty, 52 | this.discount, 53 | this.channel, 54 | }); 55 | 56 | // SQLite converter 57 | Map toMap() { 58 | return { 59 | 'id': id, 60 | 'wallet': wallet.toString(), 61 | 'type': type.toString(), 62 | 'amount': amount, 63 | 'service_fee': serviceFee, 64 | 'vat': vat, 65 | 'date': date?.toIso8601String(), 66 | 'reference_number': referenceNumber, 67 | 'payer': payer, 68 | 'payer_account': payerAccount, 69 | 'receiver': receiver, 70 | 'receiver_account': receiverAccount, 71 | 'reason': reason, 72 | 'payer_account_type': payerAccountType, 73 | 'payer_tin_number': payerTinNumber, 74 | 'status': status?.toString(), 75 | 'stamp_duty': stampDuty, 76 | 'discount': discount, 77 | 'channel': channel, 78 | }; 79 | } 80 | 81 | // Firebase converter 82 | Map toJson() => toMap(); 83 | 84 | // Factory constructor for SQLite 85 | factory TransactionCls.fromMap(Map map) { 86 | return TransactionCls( 87 | id: map['id'] ?? '', 88 | wallet: TransactionWallet.values.firstWhere( 89 | (e) => e.toString() == map['type'], 90 | orElse: () => TransactionWallet.CBE, 91 | ), 92 | type: TransactionType.values.firstWhere( 93 | (e) => e.toString() == map['type'], 94 | orElse: () => TransactionType.DEBITED, 95 | ), 96 | amount: map['amount'] ?? 0.0, 97 | serviceFee: map['service_fee'] ?? 0.0, 98 | vat: map['vat'] ?? 0.0, 99 | date: map['date'] != null ? DateTime.parse(map['date']) : DateTime.now(), 100 | referenceNumber: map['reference_number'] ?? '', 101 | payer: map['payer'] ?? '', 102 | payerAccount: map['payer_account'] ?? '', 103 | receiver: map['receiver'] ?? '', 104 | receiverAccount: map['receiver_account'] ?? '', 105 | reason: map['reason'] ?? '', 106 | payerAccountType: map['payer_account_type'] ?? '', 107 | payerTinNumber: map['payer_tin_number'] ?? '', 108 | status: map['status'] != null 109 | ? TransactionStatus.values.firstWhere( 110 | (e) => e.toString() == map['status'], 111 | orElse: () => TransactionStatus.PENDING, 112 | ) 113 | : TransactionStatus.PENDING, 114 | stampDuty: map['stamp_duty'] ?? 0.0, 115 | discount: map['discount'] ?? 0.0, 116 | channel: map['channel'] ?? '', 117 | ); 118 | } 119 | 120 | // Factory constructor for Firebase 121 | factory TransactionCls.fromJson(Map json) => 122 | TransactionCls.fromMap(json); 123 | 124 | // write me a method to calculate the PDF link of the transaction based on the reference number 125 | // if the transaction is CBE, the link should be https://apps.cbe.com.et:100/?id=referenceNumber 126 | // if the transaction is TeleBirr, the link should be https://transactionsinfo.ethiotelecom.et/receipt/referenceNumber 127 | String get pdfLink { 128 | if (wallet == TransactionWallet.CBE) { 129 | return 'https://apps.cbe.com.et:100/?id=$referenceNumber'; 130 | } else { 131 | return 'https://transactionsinfo.ethiotelecom.et/receipt/$referenceNumber'; 132 | } 133 | } 134 | } 135 | 136 | final transactionDbCreateQuery = '''CREATE TABLE transactions ( 137 | id TEXT PRIMARY KEY, 138 | type TEXT NOT NULL, 139 | wallet TEXT NOT NULL, 140 | amount REAL NOT NULL, 141 | service_fee REAL, 142 | vat REAL, 143 | date DATETIME NOT NULL, 144 | reference_number TEXT, 145 | payer TEXT, 146 | payer_account TEXT, 147 | receiver TEXT, 148 | receiver_account TEXT, 149 | reason TEXT, 150 | payer_account_type TEXT, 151 | payer_tin_number TEXT, 152 | status TEXT, 153 | stamp_duty REAL, 154 | discount REAL, 155 | channel TEXT, 156 | user_id TEXT, 157 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 158 | FOREIGN KEY (user_id) REFERENCES users (id) 159 | ) 160 | '''; 161 | 162 | enum TransactionWallet { CBE, TELEBIRR } 163 | 164 | enum TransactionStatus { PENDING, COMPLETED, FAILED, CANCELLED } 165 | 166 | enum TransactionType { DEBITED, CREDITED } 167 | -------------------------------------------------------------------------------- /lib/src/sample_feature/sample_item.dart: -------------------------------------------------------------------------------- 1 | /// A placeholder class that represents an entity or model. 2 | class SampleItem { 3 | const SampleItem(this.id); 4 | 5 | final int id; 6 | } 7 | -------------------------------------------------------------------------------- /lib/src/sample_feature/sample_item_details_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// Displays detailed information about a SampleItem. 4 | class SampleItemDetailsView extends StatelessWidget { 5 | const SampleItemDetailsView({super.key}); 6 | 7 | static const routeName = '/sample_item'; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | appBar: AppBar( 13 | title: const Text('Item Details'), 14 | ), 15 | body: const Center( 16 | child: Text('More Information Here'), 17 | ), 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/sample_feature/sample_item_list_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../settings/settings_view.dart'; 4 | import 'sample_item.dart'; 5 | import 'sample_item_details_view.dart'; 6 | 7 | /// Displays a list of SampleItems. 8 | class SampleItemListView extends StatelessWidget { 9 | const SampleItemListView({ 10 | super.key, 11 | this.items = const [SampleItem(1), SampleItem(2), SampleItem(3)], 12 | }); 13 | 14 | static const routeName = '/'; 15 | 16 | final List items; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Scaffold( 21 | appBar: AppBar( 22 | title: const Text('Sample Items'), 23 | actions: [ 24 | IconButton( 25 | icon: const Icon(Icons.settings), 26 | onPressed: () { 27 | // Navigate to the settings page. If the user leaves and returns 28 | // to the app after it has been killed while running in the 29 | // background, the navigation stack is restored. 30 | Navigator.restorablePushNamed(context, SettingsView.routeName); 31 | }, 32 | ), 33 | ], 34 | ), 35 | bottomNavigationBar: BottomNavigationBar( 36 | backgroundColor: Theme.of(context).navigationBarTheme.backgroundColor, 37 | items: [ 38 | BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), 39 | BottomNavigationBarItem( 40 | icon: Icon(Icons.compare_arrows), label: 'Transactions'), 41 | BottomNavigationBarItem(icon: Icon(Icons.add), label: 'Add'), 42 | // BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'), 43 | ], 44 | ), 45 | // add a center floating action button 46 | floatingActionButton: FloatingActionButton( 47 | onPressed: () { 48 | // Add your onPressed code here! 49 | // show pop-up dialog 50 | ScaffoldMessenger.of(context).showSnackBar( 51 | const SnackBar(content: Text('Add Button Pressed')), 52 | ); 53 | }, 54 | shape: const CircleBorder(), 55 | child: const Icon(Icons.add), 56 | ), 57 | floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, 58 | // To work with lists that may contain a large number of items, it’s best 59 | // to use the ListView.builder constructor. 60 | // 61 | // In contrast to the default ListView constructor, which requires 62 | // building all Widgets up front, the ListView.builder constructor lazily 63 | // builds Widgets as they’re scrolled into view. 64 | body: ListView.builder( 65 | // Providing a restorationId allows the ListView to restore the 66 | // scroll position when a user leaves and returns to the app after it 67 | // has been killed while running in the background. 68 | restorationId: 'sampleItemListView', 69 | itemCount: items.length, 70 | itemBuilder: (BuildContext context, int index) { 71 | final item = items[index]; 72 | 73 | return ListTile( 74 | title: Text('SampleItem ${item.id}'), 75 | leading: const CircleAvatar( 76 | // Display the Flutter Logo image asset. 77 | foregroundImage: AssetImage('assets/images/flutter_logo.png'), 78 | ), 79 | onTap: () { 80 | // Navigate to the details page. If the user leaves and returns to 81 | // the app after it has been killed while running in the 82 | // background, the navigation stack is restored. 83 | Navigator.restorablePushNamed( 84 | context, 85 | SampleItemDetailsView.routeName, 86 | ); 87 | }); 88 | }, 89 | ), 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/src/screen/error.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ErrorView extends StatelessWidget { 4 | final String message; 5 | 6 | const ErrorView({ 7 | super.key, 8 | required this.message, 9 | }); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: AppBar( 15 | title: Text('Error'), 16 | ), 17 | body: Center( 18 | child: Text(message), 19 | ), 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/screen/home/home.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:muday/src/app.dart'; 3 | import 'package:muday/src/screen/home/main_screen.dart'; 4 | import 'package:muday/src/settings/settings_view.dart'; 5 | 6 | class HomeScreen extends StatefulWidget { 7 | const HomeScreen({super.key}); 8 | 9 | @override 10 | State createState() => _HomeScreenState(); 11 | } 12 | 13 | class _HomeScreenState extends State { 14 | int _selectedIndex = 0; 15 | 16 | void _onItemTapped(int index) { 17 | print('Selected Index: $index'); 18 | // Adjust index for FAB 19 | // if (index >= 2) { 20 | // index += 1; // Skip the FAB index 21 | // } 22 | 23 | if (index == 1) { 24 | Navigator.restorablePushNamed(context, Routes.transactionList); 25 | } else if (index == 3) { 26 | Navigator.restorablePushNamed(context, SettingsView.routeName); 27 | } 28 | 29 | setState(() { 30 | _selectedIndex = index; 31 | }); 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return Scaffold( 37 | body: MainScreen(), 38 | floatingActionButton: FloatingActionButton( 39 | onPressed: () { 40 | // Handle add button press 41 | // ScaffoldMessenger.of(context).showSnackBar( 42 | // const SnackBar(content: Text('Add Button Pressed')), 43 | // ); 44 | }, 45 | shape: const CircleBorder(), 46 | child: Container( 47 | width: 60, 48 | height: 60, 49 | decoration: BoxDecoration( 50 | shape: BoxShape.circle, 51 | // color: Color(0xFF29756F), 52 | ), 53 | child: const Icon(Icons.add, size: 40), 54 | )), 55 | floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, 56 | bottomNavigationBar: BottomNavigationBar( 57 | type: BottomNavigationBarType.fixed, 58 | // backgroundColor: Colors.grey.shade300, 59 | selectedItemColor: Theme.of(context).colorScheme.tertiary, 60 | items: [ 61 | BottomNavigationBarItem( 62 | icon: Icon( 63 | Icons.home_filled, 64 | // depending on the selected index change the color 65 | // color: _selectedIndex == 0 ? Colors.blue : Colors.grey, 66 | ), 67 | label: 'Home', 68 | ), 69 | const BottomNavigationBarItem( 70 | icon: Icon(Icons.account_balance_wallet), 71 | label: 'Transaction', 72 | ), 73 | const BottomNavigationBarItem( 74 | icon: Icon(Icons.pie_chart), 75 | label: 'Budget', 76 | ), 77 | const BottomNavigationBarItem( 78 | icon: Icon(Icons.person), 79 | label: 'Profile', 80 | ), 81 | ], 82 | currentIndex: _selectedIndex, 83 | onTap: _onItemTapped, 84 | ), 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/src/screen/home/recent_transactions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:muday/src/app.dart'; 3 | import 'package:muday/src/models/transaction.dart'; 4 | import 'package:muday/src/screen/transactions/transactions_list_item.dart'; 5 | import 'package:muday/src/services/transactions/transaction_service.dart'; 6 | import 'package:muday/src/utils/helpers.dart'; 7 | 8 | class RecentTransactions extends StatefulWidget { 9 | const RecentTransactions({super.key}); 10 | 11 | @override 12 | State createState() => _RecentTransactionsState(); 13 | } 14 | 15 | class _RecentTransactionsState extends State { 16 | final _transactionService = TransactionService(); 17 | List transactions = []; 18 | bool _isLoading = true; 19 | Map> groupedTransactions = {}; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | _loadTransactions(); 25 | } 26 | 27 | Future _loadTransactions() async { 28 | try { 29 | final fetchedTransactions = 30 | await _transactionService.getAllTransactions(count: 5); 31 | setState(() { 32 | transactions = fetchedTransactions; 33 | _isLoading = false; 34 | _groupTransactions(); 35 | }); 36 | } catch (e) { 37 | setState(() => _isLoading = false); 38 | // Handle error appropriately 39 | print('Error loading transactions: $e'); 40 | } 41 | } 42 | 43 | void _groupTransactions() { 44 | groupedTransactions = {}; 45 | for (var transaction in transactions) { 46 | final date = humanReadableDate(transaction.date); 47 | groupedTransactions.putIfAbsent(date, () => []).add(transaction); 48 | } 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return Column( 54 | children: [ 55 | // _buildTransactionHeader(context), 56 | const SizedBox(height: 20), 57 | _isLoading 58 | ? const Center(child: CircularProgressIndicator()) 59 | : Expanded( 60 | child: ListView.builder( 61 | itemCount: groupedTransactions.length, 62 | itemBuilder: (context, index) { 63 | final date = groupedTransactions.keys.elementAt(index); 64 | final dateTransactions = groupedTransactions[date]!; 65 | 66 | return Column( 67 | crossAxisAlignment: CrossAxisAlignment.start, 68 | children: [ 69 | Padding( 70 | padding: const EdgeInsets.symmetric(vertical: 8), 71 | child: Row( 72 | crossAxisAlignment: CrossAxisAlignment.start, 73 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 74 | children: [ 75 | Text( 76 | date, 77 | style: Theme.of(context) 78 | .textTheme 79 | .titleSmall 80 | ?.copyWith( 81 | fontWeight: FontWeight.bold, 82 | ), 83 | ), 84 | if (index == 0) 85 | GestureDetector( 86 | onTap: () => Navigator.pushNamed( 87 | context, '/transactions'), 88 | child: GestureDetector( 89 | onTap: () => Navigator.restorablePushNamed( 90 | context, Routes.transactionList), 91 | child: Text( 92 | 'View All', 93 | style: TextStyle( 94 | fontSize: 18, 95 | fontWeight: FontWeight.normal, 96 | color: Theme.of(context) 97 | .colorScheme 98 | .outline, 99 | ), 100 | ), 101 | ), 102 | ), 103 | ], 104 | ), 105 | ), 106 | ...dateTransactions 107 | .map((transaction) => TransactionListItem( 108 | transaction: transaction, 109 | onTap: () => Navigator.pushNamed( 110 | context, 111 | '/transaction-details', 112 | arguments: {'transaction': transaction}, 113 | ), 114 | )), 115 | ], 116 | ); 117 | }, 118 | ), 119 | ), 120 | ], 121 | ); 122 | } 123 | 124 | Widget _buildTransactionHeader(BuildContext context) { 125 | return Row( 126 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 127 | children: [ 128 | Text( 129 | 'Transactions History', 130 | style: TextStyle( 131 | fontSize: 18, 132 | fontWeight: FontWeight.bold, 133 | color: Theme.of(context).colorScheme.secondary, 134 | ), 135 | ), 136 | GestureDetector( 137 | onTap: () => Navigator.pushNamed(context, '/transactions'), 138 | child: GestureDetector( 139 | onTap: () => 140 | Navigator.restorablePushNamed(context, Routes.transactionList), 141 | child: Text( 142 | 'View All', 143 | style: TextStyle( 144 | fontSize: 18, 145 | fontWeight: FontWeight.normal, 146 | color: Theme.of(context).colorScheme.outline, 147 | ), 148 | ), 149 | ), 150 | ), 151 | ], 152 | ); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/src/screen/transactions/transactions_list copy.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:muday/src/data/data.dart'; 3 | import 'package:muday/src/utils/helpers.dart'; 4 | import 'package:muday/src/utils/types.dart'; 5 | 6 | class TransactionListView extends StatelessWidget { 7 | const TransactionListView({super.key}); 8 | 9 | String _getGroupTitle(DateTime date) { 10 | final now = DateTime.now(); 11 | final difference = now.difference(date); 12 | 13 | // if (difference.inDays == 0) return 'Today'; 14 | // if (difference.inDays == 1) return 'Yesterday'; 15 | if (difference.inDays < 7) return 'This week'; 16 | if (difference.inDays < 30) { 17 | return '${(difference.inDays / 7).floor()} weeks ago'; 18 | } 19 | if (difference.inDays < 365) { 20 | return '${(difference.inDays / 30).floor()} months ago'; 21 | } 22 | return '${(difference.inDays / 365).floor()} years ago'; 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | // Group transactions by date 28 | final groupedTransactions = >{}; 29 | 30 | for (var transaction in sampleTransactions) { 31 | final group = _getGroupTitle(transaction.dateTime); 32 | groupedTransactions.putIfAbsent(group, () => []); 33 | groupedTransactions[group]!.add(transaction); 34 | } 35 | 36 | return Scaffold( 37 | appBar: AppBar( 38 | title: Text('Transactions'), 39 | actions: [ 40 | IconButton( 41 | icon: Icon(Icons.search), 42 | onPressed: () { 43 | // Implement search functionality 44 | }, 45 | ), 46 | ], 47 | ), 48 | body: Column( 49 | children: [ 50 | // Financial Report Card 51 | Container( 52 | margin: EdgeInsets.all(16), 53 | padding: EdgeInsets.all(16), 54 | decoration: BoxDecoration( 55 | color: Colors.purple.shade50, 56 | borderRadius: BorderRadius.circular(12), 57 | ), 58 | child: Row( 59 | children: [ 60 | Expanded( 61 | child: Text( 62 | 'See your financial report', 63 | style: TextStyle( 64 | color: Colors.purple, 65 | fontWeight: FontWeight.w500, 66 | ), 67 | ), 68 | ), 69 | Icon( 70 | Icons.chevron_right, 71 | color: Colors.purple, 72 | ), 73 | ], 74 | ), 75 | ), 76 | // Transactions List 77 | Expanded( 78 | child: ListView.builder( 79 | itemCount: groupedTransactions.length, 80 | itemBuilder: (context, index) { 81 | final group = groupedTransactions.keys.elementAt(index); 82 | final transactions = groupedTransactions[group]!; 83 | 84 | return Column( 85 | crossAxisAlignment: CrossAxisAlignment.start, 86 | children: [ 87 | Padding( 88 | padding: 89 | EdgeInsets.symmetric(horizontal: 16, vertical: 8), 90 | child: Text( 91 | group, 92 | style: Theme.of(context).textTheme.titleSmall?.copyWith( 93 | fontWeight: FontWeight.bold, 94 | ), 95 | ), 96 | ), 97 | ...transactions.map((transaction) => TransactionListItem( 98 | transaction: transaction, 99 | onTap: () { 100 | Navigator.pushNamed( 101 | context, 102 | '/transaction-details', 103 | arguments: {'transaction': transaction}, 104 | ); 105 | }, 106 | )), 107 | ], 108 | ); 109 | }, 110 | ), 111 | ), 112 | ], 113 | ), 114 | ); 115 | } 116 | } 117 | 118 | class TransactionListItem extends StatelessWidget { 119 | final Transaction transaction; 120 | final VoidCallback onTap; 121 | 122 | const TransactionListItem({ 123 | super.key, 124 | required this.transaction, 125 | required this.onTap, 126 | }); 127 | 128 | @override 129 | Widget build(BuildContext context) { 130 | return InkWell( 131 | onTap: onTap, 132 | child: Padding( 133 | padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), 134 | child: Row( 135 | children: [ 136 | // Category Icon 137 | Container( 138 | width: 48, 139 | height: 48, 140 | decoration: BoxDecoration( 141 | color: transaction.isExpense 142 | ? Colors.red.shade50 143 | : Colors.green.shade50, 144 | borderRadius: BorderRadius.circular(12), 145 | ), 146 | child: Center( 147 | child: Text( 148 | transaction.icon, 149 | style: TextStyle(fontSize: 24), 150 | ), 151 | ), 152 | ), 153 | SizedBox(width: 12), 154 | // Transaction Details 155 | Expanded( 156 | child: Column( 157 | crossAxisAlignment: CrossAxisAlignment.start, 158 | children: [ 159 | Text( 160 | transaction.title, 161 | style: Theme.of(context).textTheme.titleMedium, 162 | ), 163 | Text( 164 | transaction.description, 165 | style: Theme.of(context).textTheme.bodySmall?.copyWith( 166 | color: Colors.grey, 167 | ), 168 | ), 169 | ], 170 | ), 171 | ), 172 | // Amount and Time 173 | Column( 174 | crossAxisAlignment: CrossAxisAlignment.end, 175 | children: [ 176 | Text( 177 | '${transaction.isExpense ? "-" : "+"} \$${transaction.amount.toStringAsFixed(2)}', 178 | style: TextStyle( 179 | color: transaction.isExpense ? Colors.red : Colors.green, 180 | fontWeight: FontWeight.bold, 181 | ), 182 | ), 183 | Text( 184 | humanReadableDate(transaction.dateTime), 185 | style: Theme.of(context).textTheme.bodySmall?.copyWith( 186 | color: Colors.grey, 187 | ), 188 | ), 189 | ], 190 | ), 191 | ], 192 | ), 193 | ), 194 | ); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /lib/src/screen/transactions/transactions_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:muday/src/models/transaction.dart'; 3 | import 'package:muday/src/screen/transactions/transactions_list_item.dart'; 4 | import 'package:muday/src/services/transactions/transaction_service.dart'; 5 | 6 | class TransactionListView extends StatefulWidget { 7 | const TransactionListView({super.key}); 8 | 9 | @override 10 | State createState() => _TransactionListViewState(); 11 | } 12 | 13 | class _TransactionListViewState extends State { 14 | final _transactionService = TransactionService(); 15 | final ScrollController _scrollController = ScrollController(); 16 | List _transactions = []; 17 | bool _isLoading = true; 18 | int _currentOffset = 0; 19 | static const int _pageSize = 20; 20 | int _currentPage = 1; 21 | bool _isLoadingMore = false; 22 | final bool _hasMore = true; 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | _loadTransactions(); 28 | _scrollController.addListener(_onScroll); 29 | } 30 | 31 | @override 32 | void dispose() { 33 | _scrollController.dispose(); 34 | super.dispose(); 35 | } 36 | 37 | void _onScroll() { 38 | if (_scrollController.position.pixels >= 39 | _scrollController.position.maxScrollExtent) { 40 | _loadMoreTransactions(); 41 | } 42 | } 43 | 44 | Future _loadTransactions() async { 45 | try { 46 | print('Loading initial transactions'); 47 | // if last transaction 48 | final transactions = await _transactionService.getAllTransactions( 49 | offset: _currentOffset, 50 | count: _pageSize, 51 | ); 52 | print('Loaded transactions: ${transactions.length}'); 53 | setState(() { 54 | _transactions = transactions; 55 | _isLoading = false; 56 | _currentOffset += (_currentPage * _pageSize) + 1; 57 | _currentPage++; 58 | }); 59 | print('Current Offset: $_currentOffset and Current Page: $_currentPage'); 60 | } catch (e) { 61 | print('Error: $e'); 62 | setState(() => _isLoading = false); 63 | // Handle error 64 | } 65 | } 66 | 67 | Future _loadMoreTransactions() async { 68 | if (_isLoadingMore) { 69 | print('Already loading more transactions'); 70 | return; 71 | } 72 | 73 | try { 74 | setState(() { 75 | _isLoadingMore = true; 76 | }); 77 | 78 | // Create a temporary list to hold new transactions 79 | final moreTransactions = await _transactionService.getAllTransactions( 80 | offset: _currentOffset, 81 | count: _pageSize, 82 | ); 83 | print('Got: ${moreTransactions.length} transactions'); 84 | 85 | // Only update state once with all changes 86 | if (mounted) { 87 | setState(() { 88 | // Use List.addAll() on a new list to avoid concurrent modification 89 | final newTransactions = List.from(_transactions) 90 | ..addAll(moreTransactions); 91 | 92 | // Update the state variables 93 | _transactions.clear(); 94 | _transactions.addAll(newTransactions); 95 | _currentOffset += moreTransactions.length; 96 | _isLoadingMore = false; 97 | print('Current Offset: $_currentOffset'); 98 | print('Current Transactions: ${_transactions.length}'); 99 | }); 100 | } 101 | } catch (e) { 102 | print('_loadMoreTransactions Error: ${e.toString()}'); 103 | if (mounted) { 104 | setState(() { 105 | _isLoadingMore = false; 106 | }); 107 | } 108 | } 109 | } 110 | 111 | String _getGroupTitle(DateTime? date) { 112 | if (date == null) return 'All transactions'; 113 | final now = DateTime.now(); 114 | final difference = now.difference(date); 115 | 116 | if (difference.inDays < 7) return 'This week'; 117 | if (difference.inDays < 30) { 118 | return '${(difference.inDays / 7).floor()} weeks ago'; 119 | } 120 | if (difference.inDays < 365) { 121 | return '${(difference.inDays / 30).floor()} months ago'; 122 | } 123 | return '${(difference.inDays / 365).floor()} years ago'; 124 | } 125 | 126 | @override 127 | Widget build(BuildContext context) { 128 | if (_isLoading) { 129 | return const Center(child: CircularProgressIndicator()); 130 | } 131 | 132 | final groupedTransactions = >{}; 133 | for (var transaction in _transactions) { 134 | final group = _getGroupTitle(transaction.date); 135 | groupedTransactions.putIfAbsent(group, () => []); 136 | groupedTransactions[group]!.add(transaction); 137 | } 138 | 139 | return Scaffold( 140 | appBar: AppBar( 141 | title: const Text('Transactions'), 142 | actions: [ 143 | IconButton( 144 | icon: const Icon(Icons.search), 145 | onPressed: () { 146 | // Implement search 147 | }, 148 | ), 149 | ], 150 | ), 151 | body: Column( 152 | children: [ 153 | Container( 154 | margin: const EdgeInsets.all(16), 155 | padding: const EdgeInsets.all(16), 156 | decoration: BoxDecoration( 157 | color: Colors.purple.shade50, 158 | borderRadius: BorderRadius.circular(12), 159 | ), 160 | child: const Row( 161 | children: [ 162 | Expanded( 163 | child: Text( 164 | 'See your financial report', 165 | style: TextStyle( 166 | color: Colors.purple, 167 | fontWeight: FontWeight.w500, 168 | ), 169 | ), 170 | ), 171 | Icon(Icons.chevron_right, color: Colors.purple), 172 | ], 173 | ), 174 | ), 175 | Expanded( 176 | child: ListView.builder( 177 | controller: _scrollController, 178 | itemCount: groupedTransactions.length, 179 | itemBuilder: (context, index) { 180 | if (index == groupedTransactions.length) { 181 | return Padding( 182 | padding: const EdgeInsets.all(16.0), 183 | child: Center( 184 | child: _isLoadingMore 185 | ? const CircularProgressIndicator() 186 | : TextButton( 187 | onPressed: _loadMoreTransactions, 188 | child: const Text('Load More'), 189 | ), 190 | ), 191 | ); 192 | } 193 | 194 | final group = groupedTransactions.keys.elementAt(index); 195 | final transactions = groupedTransactions[group]!; 196 | 197 | return Column( 198 | crossAxisAlignment: CrossAxisAlignment.start, 199 | children: [ 200 | Padding( 201 | padding: const EdgeInsets.symmetric( 202 | horizontal: 16, vertical: 8), 203 | child: Text( 204 | group, 205 | style: Theme.of(context).textTheme.titleSmall?.copyWith( 206 | fontWeight: FontWeight.bold, 207 | ), 208 | ), 209 | ), 210 | ...transactions.map((transaction) => TransactionListItem( 211 | transaction: transaction, 212 | onTap: () { 213 | Navigator.pushNamed( 214 | context, 215 | '/transaction-details', 216 | arguments: {'transaction': transaction}, 217 | ); 218 | }, 219 | )), 220 | ], 221 | ); 222 | }, 223 | ), 224 | ), 225 | ], 226 | ), 227 | ); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /lib/src/screen/transactions/transactions_list_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:muday/src/models/transaction.dart'; 3 | import 'package:muday/src/utils/helpers.dart'; 4 | 5 | class TransactionListItem extends StatelessWidget { 6 | final TransactionCls transaction; 7 | final VoidCallback onTap; 8 | 9 | const TransactionListItem({ 10 | super.key, 11 | required this.transaction, 12 | required this.onTap, 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final bool isExpense = transaction.type == TransactionType.DEBITED; 18 | String title; 19 | if (transaction.type == TransactionType.CREDITED) { 20 | title = transaction.payer ?? 'Unknown Sender'; 21 | if (title.isEmpty) { 22 | title = 'Unknown Sender'; 23 | } 24 | } else { 25 | title = transaction.receiver ?? 'Unknown Receiver'; 26 | if (title.isEmpty) { 27 | title = 'Unknown Receiver'; 28 | } 29 | } 30 | 31 | return InkWell( 32 | onTap: onTap, 33 | child: Padding( 34 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 35 | child: Row( 36 | children: [ 37 | Container( 38 | width: 25, 39 | height: 25, 40 | decoration: BoxDecoration( 41 | color: isExpense ? Colors.red.shade50 : Colors.green.shade50, 42 | borderRadius: BorderRadius.circular(12), 43 | ), 44 | child: Center( 45 | child: Icon( 46 | isExpense 47 | ? Icons.arrow_circle_up_outlined 48 | : Icons.arrow_circle_down_outlined, 49 | color: isExpense ? Colors.red : Colors.green, 50 | ), 51 | ), 52 | ), 53 | const SizedBox(width: 12), 54 | Expanded( 55 | child: Column( 56 | crossAxisAlignment: CrossAxisAlignment.start, 57 | children: [ 58 | Text( 59 | title, 60 | style: Theme.of(context).textTheme.titleMedium, 61 | ), 62 | Text( 63 | transaction.reason ?? 'Transaction', 64 | style: Theme.of(context).textTheme.bodySmall?.copyWith( 65 | color: Colors.grey, 66 | ), 67 | ), 68 | ], 69 | ), 70 | ), 71 | Column( 72 | crossAxisAlignment: CrossAxisAlignment.end, 73 | children: [ 74 | Text( 75 | 'ETB ${humanReadableNumber(transaction.amount)}', 76 | style: TextStyle( 77 | color: isExpense ? Colors.red : Colors.green, 78 | fontWeight: FontWeight.bold, 79 | ), 80 | ), 81 | Text( 82 | transaction.date.toString().substring(0, 16), 83 | style: Theme.of(context).textTheme.bodySmall?.copyWith( 84 | color: Colors.grey, 85 | ), 86 | ), 87 | ], 88 | ), 89 | ], 90 | ), 91 | ), 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/services/sms/sms_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_sms_inbox/flutter_sms_inbox.dart'; 2 | import 'package:muday/src/models/transaction.dart'; 3 | import 'package:muday/src/services/sms/cbe_sms_parser.dart'; 4 | import 'package:permission_handler/permission_handler.dart'; 5 | 6 | class SMSService { 7 | final SmsQuery _query = SmsQuery(); 8 | final List _targetSenders = ['127', 'CBE']; 9 | 10 | // count is the number of messages to read if -1 read all messages 11 | Future> readMessages(String address, 12 | {int count = -1, start = 0}) async { 13 | var permission = await Permission.sms.status; 14 | if (!permission.isGranted) { 15 | permission = await Permission.sms.request(); 16 | if (!permission.isGranted) return []; 17 | } 18 | if (count == -1) { 19 | print('Reading all messages'); 20 | return await _query.querySms( 21 | kinds: [SmsQueryKind.inbox], 22 | address: address, 23 | start: start, 24 | count: count == -1 ? 100 : count, 25 | ); 26 | } else { 27 | return await _query.querySms( 28 | kinds: [SmsQueryKind.inbox], 29 | address: address, 30 | count: count, 31 | start: start, 32 | ); 33 | } 34 | } 35 | 36 | // read all CBE messages and parse them and convert them to TransactionCls and return it as a list 37 | Future> getParsedCBETransaction( 38 | {int count = -1, offset = 0, bool includeReceipt = false}) async { 39 | // print('Reading CBE messages with count: $count'); 40 | var messages = await readMessages('CBE', count: count, start: offset); 41 | // print('Total messages: ${messages.length}'); 42 | List transactions = []; 43 | for (var message in messages) { 44 | try { 45 | var msg = await CBEReceiptParser.parseMessage( 46 | message.body ?? '', message.date, 47 | includeReceipt: includeReceipt); 48 | if (msg == null) { 49 | print('ERROR: Failed to parse message: ${message.body}'); 50 | continue; 51 | } 52 | 53 | transactions.add(TransactionCls( 54 | id: msg['id'] ?? '', 55 | wallet: msg['wallet'] == 'CBE' 56 | ? TransactionWallet.CBE 57 | : TransactionWallet.TELEBIRR, 58 | type: msg['type'] == 'debited' 59 | ? TransactionType.DEBITED 60 | : TransactionType.CREDITED, 61 | amount: double.parse(msg['amount'] ?? '0'), 62 | serviceFee: double.parse(msg['serviceFee'] ?? '0'), 63 | vat: double.parse(msg['vat'] ?? '0'), 64 | date: message.date, 65 | referenceNumber: msg['referenceNumber'], 66 | payer: msg['payer'], 67 | payerAccount: msg['payerAccount'], 68 | receiver: msg['receiver'], 69 | receiverAccount: msg['receiverAccount'], 70 | reason: msg['reason'], 71 | status: TransactionStatus.COMPLETED, 72 | channel: msg['channel'], 73 | )); 74 | } catch (e) { 75 | print('Error parsing message: $e and msg: ${message.body}'); 76 | } 77 | } 78 | print('Total transactions: ${transactions.length}'); 79 | return transactions; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/src/services/sms/telebirr_receipt_parser.dart: -------------------------------------------------------------------------------- 1 | class TelebirrReceiptParser { 2 | @override 3 | Future> parseReceipt(String link) async { 4 | // TODO: Implement TeleBirr receipt parsing 5 | // This is a placeholder that returns an empty map 6 | // Implement actual parsing logic when TeleBirr receipt format is known 7 | return { 8 | 'status': 'PENDING', 9 | 'transactionId': '', 10 | 'amount': '', 11 | 'date': '', 12 | 'sender': '', 13 | 'receiver': '', 14 | 'description': '', 15 | 'fee': '', 16 | 'total': '', 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/services/transactions/transaction_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:muday/src/models/db.dart'; 2 | import 'package:muday/src/models/transaction.dart'; 3 | 4 | class TransactionService { 5 | final db = DatabaseService.instance; 6 | List? _transactions; 7 | String errorText = ''; 8 | 9 | // get error message 10 | String getError() { 11 | return errorText; 12 | } 13 | 14 | Future> getAllTransactions( 15 | {int count = -1, int offset = 0}) async { 16 | try { 17 | if (_transactions != null) return _transactions!; 18 | 19 | final transactions = await db.query( 20 | 'transactions', 21 | limit: count == -1 ? null : count, 22 | offset: offset, 23 | ); 24 | 25 | _transactions = 26 | transactions.map((json) => TransactionCls.fromJson(json)).toList(); 27 | return _transactions!; 28 | } catch (e) { 29 | errorText = e.toString(); 30 | print('Error: $e'); 31 | } 32 | return []; 33 | } 34 | 35 | Future getTransactionById(String id) async { 36 | try { 37 | final transaction = await db.query( 38 | 'transactions', 39 | where: 'id = ?', 40 | whereArgs: [id], 41 | ); 42 | 43 | if (transaction.isNotEmpty) { 44 | return TransactionCls.fromJson(transaction.first); 45 | } else { 46 | return null; 47 | } 48 | } catch (e) { 49 | errorText = e.toString(); 50 | print('Error: $e'); 51 | return null; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/settings/settings_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'settings_service.dart'; 4 | 5 | /// A class that many Widgets can interact with to read user settings, update 6 | /// user settings, or listen to user settings changes. 7 | /// 8 | /// Controllers glue Data Services to Flutter Widgets. The SettingsController 9 | /// uses the SettingsService to store and retrieve user settings. 10 | class SettingsController with ChangeNotifier { 11 | SettingsController(this._settingsService); 12 | 13 | // Make SettingsService a private variable so it is not used directly. 14 | final SettingsService _settingsService; 15 | 16 | // Make ThemeMode a private variable so it is not updated directly without 17 | // also persisting the changes with the SettingsService. 18 | late ThemeMode _themeMode; 19 | 20 | // Allow Widgets to read the user's preferred ThemeMode. 21 | ThemeMode get themeMode => _themeMode; 22 | 23 | /// Load the user's settings from the SettingsService. It may load from a 24 | /// local database or the internet. The controller only knows it can load the 25 | /// settings from the service. 26 | Future loadSettings() async { 27 | _themeMode = await _settingsService.themeMode(); 28 | 29 | // Important! Inform listeners a change has occurred. 30 | notifyListeners(); 31 | } 32 | 33 | /// Update and persist the ThemeMode based on the user's selection. 34 | Future updateThemeMode(ThemeMode? newThemeMode) async { 35 | if (newThemeMode == null) return; 36 | 37 | // Do not perform any work if new and old ThemeMode are identical 38 | if (newThemeMode == _themeMode) return; 39 | 40 | // Otherwise, store the new ThemeMode in memory 41 | _themeMode = newThemeMode; 42 | 43 | // Important! Inform listeners a change has occurred. 44 | notifyListeners(); 45 | 46 | // Persist the changes to a local database or the internet using the 47 | // SettingService. 48 | await _settingsService.updateThemeMode(newThemeMode); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/settings/settings_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// A service that stores and retrieves user settings. 4 | /// 5 | /// By default, this class does not persist user settings. If you'd like to 6 | /// persist the user settings locally, use the shared_preferences package. If 7 | /// you'd like to store settings on a web server, use the http package. 8 | class SettingsService { 9 | /// Loads the User's preferred ThemeMode from local or remote storage. 10 | Future themeMode() async => ThemeMode.system; 11 | 12 | /// Persists the user's preferred ThemeMode to local or remote storage. 13 | Future updateThemeMode(ThemeMode theme) async { 14 | // Use the shared_preferences package to persist settings locally or the 15 | // http package to persist settings over the network. 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/settings/settings_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'settings_controller.dart'; 4 | 5 | /// Displays the various settings that can be customized by the user. 6 | /// 7 | /// When a user changes a setting, the SettingsController is updated and 8 | /// Widgets that listen to the SettingsController are rebuilt. 9 | class SettingsView extends StatelessWidget { 10 | const SettingsView({super.key, required this.controller}); 11 | 12 | static const routeName = '/settings'; 13 | 14 | final SettingsController controller; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Scaffold( 19 | appBar: AppBar( 20 | title: const Text('Settings'), 21 | ), 22 | body: Padding( 23 | padding: const EdgeInsets.all(16), 24 | // Glue the SettingsController to the theme selection DropdownButton. 25 | // 26 | // When a user selects a theme from the dropdown list, the 27 | // SettingsController is updated, which rebuilds the MaterialApp. 28 | child: DropdownButton( 29 | // Read the selected themeMode from the controller 30 | value: controller.themeMode, 31 | // Call the updateThemeMode method any time the user selects a theme. 32 | onChanged: controller.updateThemeMode, 33 | items: const [ 34 | DropdownMenuItem( 35 | value: ThemeMode.system, 36 | child: Text('System Theme'), 37 | ), 38 | DropdownMenuItem( 39 | value: ThemeMode.light, 40 | child: Text('Light Theme'), 41 | ), 42 | DropdownMenuItem( 43 | value: ThemeMode.dark, 44 | child: Text('Dark Theme'), 45 | ) 46 | ], 47 | ), 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/theme.dart: -------------------------------------------------------------------------------- 1 | import "package:flutter/material.dart"; 2 | 3 | class MaterialTheme { 4 | final TextTheme textTheme; 5 | const MaterialTheme(this.textTheme); 6 | 7 | static ColorScheme lightScheme() { 8 | return const ColorScheme( 9 | brightness: Brightness.light, 10 | primary: Color(0xff2B6777), // Calm teal 11 | surfaceTint: Color(0xff2B6777), 12 | onPrimary: Color(0xffffffff), 13 | primaryContainer: Color(0xffC8E3E7), // Soft light teal 14 | onPrimaryContainer: Color(0xff001F26), 15 | secondary: Color(0xff52AB98), // Muted green 16 | onSecondary: Color(0xffffffff), 17 | secondaryContainer: Color(0xffD5E8E3), // Soft light green 18 | onSecondaryContainer: Color(0xff0F1F1B), 19 | tertiary: Color(0xff7C8CA1), // Gentle blue-gray 20 | onTertiary: Color(0xffffffff), 21 | tertiaryContainer: Color(0xffE2E7F0), // Soft light blue-gray 22 | onTertiaryContainer: Color(0xff161B23), 23 | error: Color(0xffBA1A1A), 24 | onError: Color(0xffffffff), 25 | errorContainer: Color(0xffFFDAD6), 26 | onErrorContainer: Color(0xff410002), 27 | surface: Color(0xffFAFAFA), // Almost white 28 | onSurface: Color(0xff1A1C1D), 29 | onSurfaceVariant: Color(0xff41484D), 30 | outline: Color(0xff72787D), 31 | outlineVariant: Color(0xffC1C7CD), 32 | shadow: Color(0xff000000), 33 | scrim: Color(0xff000000), 34 | inverseSurface: Color(0xff2E3132), 35 | inversePrimary: Color(0xff8ECFD6), 36 | surfaceDim: Color(0xffDBDCDD), 37 | surfaceBright: Color(0xffFAFAFA), 38 | surfaceContainerLowest: Color(0xffFFFFFF), 39 | surfaceContainerLow: Color(0xffF4F4F5), 40 | surfaceContainer: Color(0xffEEEEEF), 41 | surfaceContainerHigh: Color(0xffE8E8E9), 42 | surfaceContainerHighest: Color(0xffE2E2E3), 43 | ); 44 | } 45 | 46 | static ColorScheme darkScheme() { 47 | return const ColorScheme( 48 | brightness: Brightness.dark, 49 | primary: Color(0xff8ECFD6), // Soft teal 50 | surfaceTint: Color(0xff8ECFD6), 51 | onPrimary: Color(0xff00363D), 52 | primaryContainer: Color(0xff204E55), // Deep muted teal 53 | onPrimaryContainer: Color(0xffB9EAEF), 54 | secondary: Color(0xff9CCEC4), // Soft green 55 | onSecondary: Color(0xff233430), 56 | secondaryContainer: Color(0xff3A4B47), // Deep muted green 57 | onSecondaryContainer: Color(0xffD5E8E3), 58 | tertiary: Color(0xffBBC7DB), // Soft blue-gray 59 | onTertiary: Color(0xff263141), 60 | tertiaryContainer: Color(0xff3C4858), // Deep muted blue-gray 61 | onTertiaryContainer: Color(0xffD7E2F6), 62 | error: Color(0xffFFB4AB), 63 | onError: Color(0xff690005), 64 | errorContainer: Color(0xff93000A), 65 | onErrorContainer: Color(0xffFFDAD6), 66 | surface: Color(0xff1A1C1D), // Almost black 67 | onSurface: Color(0xffE2E2E3), 68 | onSurfaceVariant: Color(0xffC1C7CD), 69 | outline: Color(0xff8B9198), 70 | outlineVariant: Color(0xff41484D), 71 | shadow: Color(0xff000000), 72 | scrim: Color(0xff000000), 73 | inverseSurface: Color(0xffE2E2E3), 74 | inversePrimary: Color(0xff2B6777), 75 | surfaceDim: Color(0xff1A1C1D), 76 | surfaceBright: Color(0xff37393A), 77 | surfaceContainerLowest: Color(0xff0F1112), 78 | surfaceContainerLow: Color(0xff1A1C1D), 79 | surfaceContainer: Color(0xff1E2021), 80 | surfaceContainerHigh: Color(0xff282A2B), 81 | surfaceContainerHighest: Color(0xff333536), 82 | ); 83 | } 84 | 85 | ThemeData light() { 86 | return theme(lightScheme()); 87 | } 88 | 89 | ThemeData dark() { 90 | return theme(darkScheme()); 91 | } 92 | 93 | ThemeData theme(ColorScheme colorScheme) => ThemeData( 94 | useMaterial3: true, 95 | brightness: colorScheme.brightness, 96 | colorScheme: colorScheme, 97 | textTheme: textTheme.apply( 98 | bodyColor: colorScheme.onSurface, 99 | displayColor: colorScheme.onSurface, 100 | ), 101 | scaffoldBackgroundColor: colorScheme.surface, 102 | canvasColor: colorScheme.surface, 103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /lib/src/theme/theme_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | // Custom Theme Extension 4 | @immutable 5 | class ExpenseThemeExtension extends ThemeExtension { 6 | const ExpenseThemeExtension({ 7 | required this.incomeColor, 8 | required this.expenseColor, 9 | required this.savingsColor, 10 | required this.budgetColor, 11 | required this.transactionTileColor, 12 | required this.chartLineColor, 13 | required this.customShadow, 14 | required this.successGradient, 15 | required this.dangerGradient, 16 | }); 17 | 18 | final Color incomeColor; 19 | final Color expenseColor; 20 | final Color savingsColor; 21 | final Color budgetColor; 22 | final Color transactionTileColor; 23 | final Color chartLineColor; 24 | final BoxShadow customShadow; 25 | final LinearGradient successGradient; 26 | final LinearGradient dangerGradient; 27 | 28 | @override 29 | ExpenseThemeExtension copyWith({ 30 | Color? incomeColor, 31 | Color? expenseColor, 32 | Color? savingsColor, 33 | Color? budgetColor, 34 | Color? transactionTileColor, 35 | Color? chartLineColor, 36 | BoxShadow? customShadow, 37 | LinearGradient? successGradient, 38 | LinearGradient? dangerGradient, 39 | }) { 40 | return ExpenseThemeExtension( 41 | incomeColor: incomeColor ?? this.incomeColor, 42 | expenseColor: expenseColor ?? this.expenseColor, 43 | savingsColor: savingsColor ?? this.savingsColor, 44 | budgetColor: budgetColor ?? this.budgetColor, 45 | transactionTileColor: transactionTileColor ?? this.transactionTileColor, 46 | chartLineColor: chartLineColor ?? this.chartLineColor, 47 | customShadow: customShadow ?? this.customShadow, 48 | successGradient: successGradient ?? this.successGradient, 49 | dangerGradient: dangerGradient ?? this.dangerGradient, 50 | ); 51 | } 52 | 53 | @override 54 | ExpenseThemeExtension lerp( 55 | ThemeExtension? other, double t) { 56 | if (other is! ExpenseThemeExtension) { 57 | return this; 58 | } 59 | return ExpenseThemeExtension( 60 | incomeColor: Color.lerp(incomeColor, other.incomeColor, t)!, 61 | expenseColor: Color.lerp(expenseColor, other.expenseColor, t)!, 62 | savingsColor: Color.lerp(savingsColor, other.savingsColor, t)!, 63 | budgetColor: Color.lerp(budgetColor, other.budgetColor, t)!, 64 | transactionTileColor: 65 | Color.lerp(transactionTileColor, other.transactionTileColor, t)!, 66 | chartLineColor: Color.lerp(chartLineColor, other.chartLineColor, t)!, 67 | customShadow: BoxShadow.lerp(customShadow, other.customShadow, t)!, 68 | successGradient: 69 | LinearGradient.lerp(successGradient, other.successGradient, t)!, 70 | dangerGradient: 71 | LinearGradient.lerp(dangerGradient, other.dangerGradient, t)!, 72 | ); 73 | } 74 | } 75 | // Light theme values 76 | 77 | const lightTheme = ExpenseThemeExtension( 78 | incomeColor: Color(0xFF4CAF50), 79 | expenseColor: Color(0xFFE53935), 80 | savingsColor: Color(0xFF2196F3), 81 | budgetColor: Color(0xFFFF9800), 82 | transactionTileColor: Color(0xFFFAFAFA), 83 | chartLineColor: Color(0xFF2196F3), 84 | customShadow: BoxShadow( 85 | color: Color(0x1A000000), 86 | blurRadius: 8, 87 | offset: Offset(0, 2), 88 | ), 89 | successGradient: LinearGradient( 90 | colors: [Color(0xFF4CAF50), Color(0xFF81C784)], 91 | begin: Alignment.topLeft, 92 | end: Alignment.bottomRight, 93 | ), 94 | dangerGradient: LinearGradient( 95 | colors: [Color(0xFFE53935), Color(0xFFEF5350)], 96 | begin: Alignment.topLeft, 97 | end: Alignment.bottomRight, 98 | ), 99 | ); 100 | 101 | // Dark theme values 102 | const darkTheme = ExpenseThemeExtension( 103 | incomeColor: Color(0xFF81C784), 104 | expenseColor: Color(0xFFE57373), 105 | savingsColor: Color(0xFF64B5F6), 106 | budgetColor: Color(0xFFFFB74D), 107 | transactionTileColor: Color(0xFF2D2D2D), 108 | chartLineColor: Color(0xFF90CAF9), 109 | customShadow: BoxShadow( 110 | color: Color(0x3A000000), 111 | blurRadius: 8, 112 | offset: Offset(0, 2), 113 | ), 114 | successGradient: LinearGradient( 115 | colors: [Color(0xFF81C784), Color(0xFFA5D6A7)], 116 | begin: Alignment.topLeft, 117 | end: Alignment.bottomRight, 118 | ), 119 | dangerGradient: LinearGradient( 120 | colors: [Color(0xFFE57373), Color(0xFFEF9A9A)], 121 | begin: Alignment.topLeft, 122 | end: Alignment.bottomRight, 123 | ), 124 | ); 125 | 126 | 127 | // Modified AppTheme class to include the extension 128 | // class AppTheme { 129 | // static ThemeData lightTheme() { 130 | // return ThemeData.light().copyWith( 131 | // // Your existing theme configurations... 132 | // extensions: const [ 133 | // ExpenseThemeExtension.light, 134 | // ], 135 | // ); 136 | // } 137 | 138 | // static ThemeData darkTheme() { 139 | // return ThemeData.dark().copyWith( 140 | // // Your existing theme configurations... 141 | // extensions: const [ 142 | // ExpenseThemeExtension.dark, 143 | // ], 144 | // ); 145 | // } 146 | // } 147 | 148 | // // Example usage in a widget 149 | // class TransactionTile extends StatelessWidget { 150 | // final bool isIncome; 151 | // final String amount; 152 | // final String title; 153 | 154 | // const TransactionTile({ 155 | // required this.isIncome, 156 | // required this.amount, 157 | // required this.title, 158 | // Key? key, 159 | // }) : super(key: key); 160 | 161 | // @override 162 | // Widget build(BuildContext context) { 163 | // // Get the custom theme extension 164 | // final customTheme = Theme.of(context).extension()!; 165 | 166 | // return Container( 167 | // decoration: BoxDecoration( 168 | // color: customTheme.transactionTileColor, 169 | // boxShadow: [customTheme.customShadow], 170 | // gradient: 171 | // isIncome ? customTheme.successGradient : customTheme.dangerGradient, 172 | // ), 173 | // child: ListTile( 174 | // title: Text(title), 175 | // trailing: Text( 176 | // amount, 177 | // style: TextStyle( 178 | // color: 179 | // isIncome ? customTheme.incomeColor : customTheme.expenseColor, 180 | // fontWeight: FontWeight.bold, 181 | // ), 182 | // ), 183 | // ), 184 | // ); 185 | // } 186 | // } 187 | -------------------------------------------------------------------------------- /lib/src/utils/background_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | import 'package:flutter_background_service/flutter_background_service.dart'; 7 | import 'package:muday/src/utils/populate_sms.dart'; 8 | 9 | void startBackgroundService() { 10 | final service = FlutterBackgroundService(); 11 | service.startService(); 12 | } 13 | 14 | void stopBackgroundService() { 15 | final service = FlutterBackgroundService(); 16 | service.invoke("stop"); 17 | } 18 | 19 | Future initializeService() async { 20 | final service = FlutterBackgroundService(); 21 | 22 | await service.configure( 23 | iosConfiguration: IosConfiguration( 24 | autoStart: true, 25 | onForeground: onStart, 26 | onBackground: onIosBackground, 27 | ), 28 | androidConfiguration: AndroidConfiguration( 29 | autoStart: true, 30 | onStart: onStart, 31 | isForegroundMode: false, 32 | autoStartOnBoot: true, 33 | ), 34 | ); 35 | } 36 | 37 | @pragma('vm:entry-point') 38 | Future onIosBackground(ServiceInstance service) async { 39 | WidgetsFlutterBinding.ensureInitialized(); 40 | DartPluginRegistrant.ensureInitialized(); 41 | return true; 42 | } 43 | 44 | @pragma('vm:entry-point') 45 | void onStart(ServiceInstance service) async { 46 | service.on("stop").listen((event) { 47 | service.stopSelf(); 48 | print("background process is now stopped"); 49 | }); 50 | 51 | service.on("start").listen((event) {}); 52 | 53 | await populateSms(); 54 | Timer.periodic(const Duration(seconds: 10), (timer) async { 55 | print("service is successfully running ${DateTime.now().second}"); 56 | // final db = DatabaseService.instance; 57 | // var txn = await db.query('transactions', limit: 10); 58 | // print('Transactions: $txn'); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/utils/cbe_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:dio/io.dart'; 4 | import 'package:path/path.dart' as path; 5 | import 'package:syncfusion_flutter_pdf/pdf.dart'; 6 | 7 | class CBEHttpClient { 8 | late Dio _dio; 9 | 10 | CBEHttpClient() { 11 | _dio = Dio(BaseOptions( 12 | baseUrl: 'https://apps.cbe.com.et:100', 13 | connectTimeout: const Duration(seconds: 30), 14 | receiveTimeout: const Duration(seconds: 30), 15 | validateStatus: (status) { 16 | return status != null && status < 500; 17 | }, 18 | )); 19 | 20 | // Configure certificate handling using the new approach 21 | (_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () { 22 | final client = 23 | HttpClient(context: SecurityContext(withTrustedRoots: false)); 24 | 25 | client.badCertificateCallback = 26 | (X509Certificate cert, String host, int port) { 27 | // Only accept certificates from CBE domain 28 | return host == 'apps.cbe.com.et'; 29 | }; 30 | 31 | return client; 32 | }; 33 | 34 | // Add logging interceptor 35 | _dio.interceptors.add(InterceptorsWrapper( 36 | onRequest: (options, handler) { 37 | print('Making request to: ${options.uri}'); 38 | return handler.next(options); 39 | }, 40 | onResponse: (response, handler) { 41 | print('Received response: ${response.statusCode}'); 42 | return handler.next(response); 43 | }, 44 | onError: (DioException error, handler) { 45 | print('Error occurred: ${error.message}'); 46 | return handler.next(error); 47 | }, 48 | )); 49 | } 50 | 51 | Future readPDF(String url) async { 52 | try { 53 | // Extract ID from URL if full URL is provided 54 | final Uri uri = Uri.parse(url); 55 | final String id = uri.queryParameters['id'] ?? url; 56 | 57 | // Make request 58 | final response = await _dio.get( 59 | '/', 60 | queryParameters: {'id': id}, 61 | options: Options( 62 | responseType: ResponseType.bytes, 63 | followRedirects: true, 64 | ), 65 | ); 66 | 67 | if (response.statusCode != 200) { 68 | throw Exception( 69 | 'Failed to download PDF. Status: ${response.statusCode}'); 70 | } 71 | 72 | // Create temporary file path 73 | final tempDir = Directory.systemTemp; 74 | final filePath = path.join(tempDir.path, '$id.pdf'); 75 | 76 | // Save PDF to temporary location 77 | final file = File(filePath); 78 | await file.writeAsBytes(response.data); 79 | 80 | // Load and extract text 81 | final document = PdfDocument(inputBytes: await file.readAsBytes()); 82 | final PdfTextExtractor extractor = PdfTextExtractor(document); 83 | final String text = extractor.extractText(); 84 | 85 | // Clean up 86 | document.dispose(); 87 | await file.delete(); 88 | 89 | return text; 90 | } on DioException catch (e) { 91 | if (e.type == DioExceptionType.badCertificate) { 92 | throw Exception( 93 | 'Certificate verification failed. Please check your connection or try again later.'); 94 | } 95 | throw Exception('Network error: ${e.message}'); 96 | } catch (e) { 97 | throw Exception('Error reading PDF: $e'); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/src/utils/constants.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | const Map> currencies = { 4 | 'ETB': { 5 | 'name': 'Ethiopian Birr', 6 | 'symbol': 'Br', 7 | 'icon': Text('ብር', style: TextStyle(fontSize: 12)), 8 | }, 9 | 'USD': { 10 | 'name': 'US Dollar', 11 | 'symbol': '\$', 12 | 'icon': Icons.attach_money, 13 | }, 14 | 'EUR': { 15 | 'name': 'Euro', 16 | 'symbol': '€', 17 | 'icon': Icons.euro, 18 | }, 19 | 'GBP': { 20 | 'name': 'British Pound', 21 | 'symbol': '£', 22 | 'icon': Icons.money, 23 | }, 24 | 'JPY': { 25 | 'name': 'Japanese Yen', 26 | 'symbol': '¥', 27 | 'icon': Icons.money_off, 28 | }, 29 | 'INR': { 30 | 'name': 'Indian Rupee', 31 | 'symbol': '₹', 32 | 'icon': Icons.currency_rupee, 33 | }, 34 | 'CAD': { 35 | 'name': 'Canadian Dollar', 36 | 'symbol': 'C\$', 37 | 'icon': Icons.attach_money, 38 | }, 39 | 'AUD': { 40 | 'name': 'Australian Dollar', 41 | 'symbol': 'A\$', 42 | 'icon': Icons.attach_money, 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /lib/src/utils/helpers.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | String humanReadableDate(dynamic date) { 4 | DateTime dateTime; 5 | 6 | if (date is String) { 7 | dateTime = DateTime.parse(date); 8 | } else if (date is DateTime) { 9 | dateTime = date; 10 | } else { 11 | throw ArgumentError('Invalid date format'); 12 | } 13 | 14 | final now = DateTime.now(); 15 | final difference = now.difference(dateTime); 16 | 17 | if (difference.inDays == 0) { 18 | if (difference.inMinutes == 0) { 19 | return 'Just now'; 20 | } else if (difference.inHours == 0) { 21 | return '${difference.inMinutes} minutes ago'; 22 | } else if (difference.inHours > 0 && difference.inHours < 12) { 23 | return '${difference.inHours} hours ago'; 24 | } 25 | return 'Today'; 26 | } else if (difference.inDays == 1) { 27 | return 'Yesterday'; 28 | } else if (difference.inDays < 7) { 29 | return '${difference.inDays} days ago'; 30 | } else if (difference.inDays < 14) { 31 | return 'Last week'; 32 | } else if (difference.inDays < 30) { 33 | return '${(difference.inDays / 7).floor()} weeks ago'; 34 | } else if (difference.inDays < 365) { 35 | return '${(difference.inDays / 30).floor()} months ago'; 36 | } else { 37 | return DateFormat.yMMMMd().format(dateTime); 38 | } 39 | } 40 | 41 | String humanReadableNumber(num number) { 42 | if (number >= 1000000000) { 43 | return '${(number / 1000000000).toStringAsFixed(1)}B'; 44 | } else if (number >= 1000000) { 45 | return '${(number / 1000000).toStringAsFixed(1)}M'; 46 | } else if (number >= 1000) { 47 | return '${(number / 1000).toStringAsFixed(1)}K'; 48 | } else { 49 | return number.toString(); 50 | } 51 | } 52 | 53 | var birrFormatter = NumberFormat.currency( 54 | symbol: 'Br. ', decimalDigits: 2, customPattern: '###,###.00 Br.'); 55 | -------------------------------------------------------------------------------- /lib/src/utils/populate_sms.dart: -------------------------------------------------------------------------------- 1 | // write me void async fun 2 | import 'package:muday/src/models/db.dart'; 3 | import 'package:muday/src/services/sms/sms_service.dart'; 4 | 5 | Future populateSms() async { 6 | final smsService = SMSService(); 7 | final db = DatabaseService.instance; 8 | 9 | // Uncomment to delete all transactions 10 | // await db.delete('transactions', '1=1', []); 11 | 12 | var txn = await db.query('transactions'); 13 | // if there are transactions in the database, don't populate 14 | if (txn.isNotEmpty) { 15 | print('Database already populated with ${txn.length} transactions'); 16 | return; 17 | } 18 | print('Database is empty. Populating...'); 19 | int total = 0; 20 | int count = 10; 21 | int offset = 0; 22 | while (true) { 23 | var txns = await smsService.getParsedCBETransaction( 24 | count: count, offset: offset, includeReceipt: true); 25 | if (txns.isEmpty) { 26 | break; 27 | } 28 | for (var txn in txns) { 29 | var mapped = txn.toMap(); 30 | db.insert('transactions', mapped); 31 | print('!!Inserted transaction: $mapped'); 32 | print('-' * 50); 33 | } 34 | offset += count; 35 | total += txns.length; 36 | print('Total transactions: $total so far'); 37 | } 38 | print('Total transactions: $total in the database'); 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/utils/types.dart: -------------------------------------------------------------------------------- 1 | enum TransactionType { income, expense } 2 | 3 | class Transaction { 4 | final String id; 5 | final String title; 6 | final String description; 7 | final double amount; 8 | final DateTime dateTime; 9 | final String category; 10 | final String icon; 11 | final bool isExpense; 12 | 13 | Transaction({ 14 | required this.id, 15 | required this.title, 16 | required this.description, 17 | required this.amount, 18 | required this.dateTime, 19 | required this.category, 20 | required this.icon, 21 | required this.isExpense, 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.13) 3 | project(runner LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "my_expense_tracker") 8 | # The unique GTK application identifier for this application. See: 9 | # https://wiki.gnome.org/HowDoI/ChooseApplicationID 10 | set(APPLICATION_ID "com.example.my_expense_tracker") 11 | 12 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 13 | # versions of CMake. 14 | cmake_policy(SET CMP0063 NEW) 15 | 16 | # Load bundled libraries from the lib/ directory relative to the binary. 17 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 18 | 19 | # Root filesystem for cross-building. 20 | if(FLUTTER_TARGET_PLATFORM_SYSROOT) 21 | set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) 22 | set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) 23 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 24 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 25 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 26 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 27 | endif() 28 | 29 | # Define build configuration options. 30 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 31 | set(CMAKE_BUILD_TYPE "Debug" CACHE 32 | STRING "Flutter build mode" FORCE) 33 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 34 | "Debug" "Profile" "Release") 35 | endif() 36 | 37 | # Compilation settings that should be applied to most targets. 38 | # 39 | # Be cautious about adding new options here, as plugins use this function by 40 | # default. In most cases, you should add new options to specific targets instead 41 | # of modifying this function. 42 | function(APPLY_STANDARD_SETTINGS TARGET) 43 | target_compile_features(${TARGET} PUBLIC cxx_std_14) 44 | target_compile_options(${TARGET} PRIVATE -Wall -Werror) 45 | target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") 46 | target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") 47 | endfunction() 48 | 49 | # Flutter library and tool build rules. 50 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 51 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 52 | 53 | # System-level dependencies. 54 | find_package(PkgConfig REQUIRED) 55 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 56 | 57 | # Application build; see runner/CMakeLists.txt. 58 | add_subdirectory("runner") 59 | 60 | # Run the Flutter tool portions of the build. This must not be removed. 61 | add_dependencies(${BINARY_NAME} flutter_assemble) 62 | 63 | # Only the install-generated bundle's copy of the executable will launch 64 | # correctly, since the resources must in the right relative locations. To avoid 65 | # people trying to run the unbundled copy, put it in a subdirectory instead of 66 | # the default top-level location. 67 | set_target_properties(${BINARY_NAME} 68 | PROPERTIES 69 | RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" 70 | ) 71 | 72 | 73 | # Generated plugin build rules, which manage building the plugins and adding 74 | # them to the application. 75 | include(flutter/generated_plugins.cmake) 76 | 77 | 78 | # === Installation === 79 | # By default, "installing" just makes a relocatable bundle in the build 80 | # directory. 81 | set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") 82 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 83 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 84 | endif() 85 | 86 | # Start with a clean build bundle directory every time. 87 | install(CODE " 88 | file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") 89 | " COMPONENT Runtime) 90 | 91 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 92 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") 93 | 94 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 95 | COMPONENT Runtime) 96 | 97 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 98 | COMPONENT Runtime) 99 | 100 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 101 | COMPONENT Runtime) 102 | 103 | foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) 104 | install(FILES "${bundled_library}" 105 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 106 | COMPONENT Runtime) 107 | endforeach(bundled_library) 108 | 109 | # Copy the native assets provided by the build.dart from all packages. 110 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") 111 | install(DIRECTORY "${NATIVE_ASSETS_DIR}" 112 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 113 | COMPONENT Runtime) 114 | 115 | # Fully re-copy the assets directory on each build to avoid having stale files 116 | # from a previous install. 117 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 118 | install(CODE " 119 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 120 | " COMPONENT Runtime) 121 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 122 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 123 | 124 | # Install the AOT library on non-Debug builds only. 125 | if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") 126 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 127 | COMPONENT Runtime) 128 | endif() 129 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | #include 10 | 11 | void fl_register_plugins(FlPluginRegistry* registry) { 12 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 13 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 14 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | url_launcher_linux 7 | ) 8 | 9 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 10 | ) 11 | 12 | set(PLUGIN_BUNDLED_LIBRARIES) 13 | 14 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 15 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 16 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 19 | endforeach(plugin) 20 | 21 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 22 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 24 | endforeach(ffi_plugin) 25 | -------------------------------------------------------------------------------- /linux/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 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} 10 | "main.cc" 11 | "my_application.cc" 12 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 13 | ) 14 | 15 | # Apply the standard set of build settings. This can be removed for applications 16 | # that need different build settings. 17 | apply_standard_settings(${BINARY_NAME}) 18 | 19 | # Add preprocessor definitions for the application ID. 20 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") 21 | 22 | # Add dependency libraries. Add any application-specific dependencies here. 23 | target_link_libraries(${BINARY_NAME} PRIVATE flutter) 24 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 25 | 26 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 27 | -------------------------------------------------------------------------------- /linux/runner/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 | -------------------------------------------------------------------------------- /linux/runner/my_application.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | #include 4 | #ifdef GDK_WINDOWING_X11 5 | #include 6 | #endif 7 | 8 | #include "flutter/generated_plugin_registrant.h" 9 | 10 | struct _MyApplication { 11 | GtkApplication parent_instance; 12 | char** dart_entrypoint_arguments; 13 | }; 14 | 15 | G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) 16 | 17 | // Implements GApplication::activate. 18 | static void my_application_activate(GApplication* application) { 19 | MyApplication* self = MY_APPLICATION(application); 20 | GtkWindow* window = 21 | GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); 22 | 23 | // Use a header bar when running in GNOME as this is the common style used 24 | // by applications and is the setup most users will be using (e.g. Ubuntu 25 | // desktop). 26 | // If running on X and not using GNOME then just use a traditional title bar 27 | // in case the window manager does more exotic layout, e.g. tiling. 28 | // If running on Wayland assume the header bar will work (may need changing 29 | // if future cases occur). 30 | gboolean use_header_bar = TRUE; 31 | #ifdef GDK_WINDOWING_X11 32 | GdkScreen* screen = gtk_window_get_screen(window); 33 | if (GDK_IS_X11_SCREEN(screen)) { 34 | const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); 35 | if (g_strcmp0(wm_name, "GNOME Shell") != 0) { 36 | use_header_bar = FALSE; 37 | } 38 | } 39 | #endif 40 | if (use_header_bar) { 41 | GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); 42 | gtk_widget_show(GTK_WIDGET(header_bar)); 43 | gtk_header_bar_set_title(header_bar, "my_expense_tracker"); 44 | gtk_header_bar_set_show_close_button(header_bar, TRUE); 45 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); 46 | } else { 47 | gtk_window_set_title(window, "my_expense_tracker"); 48 | } 49 | 50 | gtk_window_set_default_size(window, 1280, 720); 51 | gtk_widget_show(GTK_WIDGET(window)); 52 | 53 | g_autoptr(FlDartProject) project = fl_dart_project_new(); 54 | fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); 55 | 56 | FlView* view = fl_view_new(project); 57 | gtk_widget_show(GTK_WIDGET(view)); 58 | gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); 59 | 60 | fl_register_plugins(FL_PLUGIN_REGISTRY(view)); 61 | 62 | gtk_widget_grab_focus(GTK_WIDGET(view)); 63 | } 64 | 65 | // Implements GApplication::local_command_line. 66 | static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { 67 | MyApplication* self = MY_APPLICATION(application); 68 | // Strip out the first argument as it is the binary name. 69 | self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); 70 | 71 | g_autoptr(GError) error = nullptr; 72 | if (!g_application_register(application, nullptr, &error)) { 73 | g_warning("Failed to register: %s", error->message); 74 | *exit_status = 1; 75 | return TRUE; 76 | } 77 | 78 | g_application_activate(application); 79 | *exit_status = 0; 80 | 81 | return TRUE; 82 | } 83 | 84 | // Implements GApplication::startup. 85 | static void my_application_startup(GApplication* application) { 86 | //MyApplication* self = MY_APPLICATION(object); 87 | 88 | // Perform any actions required at application startup. 89 | 90 | G_APPLICATION_CLASS(my_application_parent_class)->startup(application); 91 | } 92 | 93 | // Implements GApplication::shutdown. 94 | static void my_application_shutdown(GApplication* application) { 95 | //MyApplication* self = MY_APPLICATION(object); 96 | 97 | // Perform any actions required at application shutdown. 98 | 99 | G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); 100 | } 101 | 102 | // Implements GObject::dispose. 103 | static void my_application_dispose(GObject* object) { 104 | MyApplication* self = MY_APPLICATION(object); 105 | g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); 106 | G_OBJECT_CLASS(my_application_parent_class)->dispose(object); 107 | } 108 | 109 | static void my_application_class_init(MyApplicationClass* klass) { 110 | G_APPLICATION_CLASS(klass)->activate = my_application_activate; 111 | G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; 112 | G_APPLICATION_CLASS(klass)->startup = my_application_startup; 113 | G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; 114 | G_OBJECT_CLASS(klass)->dispose = my_application_dispose; 115 | } 116 | 117 | static void my_application_init(MyApplication* self) {} 118 | 119 | MyApplication* my_application_new() { 120 | // Set the program name to the application ID, which helps various systems 121 | // like GTK and desktop environments map this running application to its 122 | // corresponding .desktop file. This ensures better integration by allowing 123 | // the application to be recognized beyond its binary name. 124 | g_set_prgname(APPLICATION_ID); 125 | 126 | return MY_APPLICATION(g_object_new(my_application_get_type(), 127 | "application-id", APPLICATION_ID, 128 | "flags", G_APPLICATION_NON_UNIQUE, 129 | nullptr)); 130 | } 131 | -------------------------------------------------------------------------------- /linux/runner/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 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import sqflite_darwin 9 | import url_launcher_macos 10 | 11 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 12 | SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) 13 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 14 | } 15 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | 10 | override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 11 | return true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /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 = my_expense_tracker 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.myExpenseTracker 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved. 15 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 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 | -------------------------------------------------------------------------------- /plan.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": "muday-expense-tracker", 3 | "description": "Ethiopian expense tracking app with SMS parsing capabilities", 4 | "labels": { 5 | "feature": "2da44e", 6 | "bug": "d73a4a", 7 | "enhancement": "a2eeef", 8 | "documentation": "0075ca", 9 | "high-priority": "b60205", 10 | "medium-priority": "fbca04", 11 | "low-priority": "0e8a16" 12 | }, 13 | "milestones": [ 14 | { 15 | "title": "Core SMS Parsing Framework", 16 | "description": "Implement basic SMS parsing functionality for different banks", 17 | "issues": [ 18 | { 19 | "title": "Implement SMS Listener Service", 20 | "description": "Create background service to listen for incoming SMS from specified senders", 21 | "labels": ["feature", "high-priority"], 22 | "tasks": [ 23 | "Set up SMS permission handling", 24 | "Create SMS receiver broadcast", 25 | "Implement sender filtering logic", 26 | "Add background service registration" 27 | ] 28 | }, 29 | { 30 | "title": "Create Bank SMS Parser Interface", 31 | "description": "Design and implement generic parser interface for different banks", 32 | "labels": ["feature", "high-priority"], 33 | "tasks": [ 34 | "Define parser interface", 35 | "Create abstract bank parser class", 36 | "Implement regex pattern management", 37 | "Add field extraction utilities" 38 | ] 39 | }, 40 | { 41 | "title": "Implement Telebirr SMS Parser", 42 | "description": "Parse Telebirr SMS messages with all required fields", 43 | "labels": ["feature", "high-priority"], 44 | "tasks": [ 45 | "Parse balance", 46 | "Extract transaction date", 47 | "Parse transaction amount", 48 | "Extract reference number", 49 | "Parse receiver details", 50 | "Handle commission and VAT", 51 | "Extract transaction ID", 52 | "Parse payment details" 53 | ] 54 | }, 55 | { 56 | "title": "Implement CBE SMS Parser", 57 | "description": "Parse Commercial Bank of Ethiopia SMS messages", 58 | "labels": ["feature", "high-priority"] 59 | }, 60 | { 61 | "title": "Implement Abyssinia Bank Parser", 62 | "description": "Parse Bank of Abyssinia SMS messages", 63 | "labels": ["feature", "medium-priority"] 64 | } 65 | ] 66 | }, 67 | { 68 | "title": "Data Management", 69 | "description": "Database design and implementation for storing parsed transactions", 70 | "issues": [ 71 | { 72 | "title": "Design Database Schema", 73 | "description": "Create database schema for transactions and categories", 74 | "labels": ["feature", "high-priority"], 75 | "tasks": [ 76 | "Design transaction table", 77 | "Design category table", 78 | "Design budget table", 79 | "Create database migration scripts" 80 | ] 81 | }, 82 | { 83 | "title": "Implement Local Database Operations", 84 | "description": "Create repository layer for local data operations", 85 | "labels": ["feature", "high-priority"] 86 | }, 87 | { 88 | "title": "Implement Cloud Sync", 89 | "description": "Setup Firebase integration and cloud sync functionality", 90 | "labels": ["feature", "medium-priority"], 91 | "tasks": [ 92 | "Setup Firebase configuration", 93 | "Implement user authentication", 94 | "Create cloud data structure", 95 | "Implement sync logic", 96 | "Handle conflict resolution" 97 | ] 98 | } 99 | ] 100 | }, 101 | { 102 | "title": "Transaction Processing", 103 | "description": "Advanced transaction processing features", 104 | "issues": [ 105 | { 106 | "title": "Implement PDF Invoice Parser", 107 | "description": "Parse transaction invoices from PDF files", 108 | "labels": ["feature", "medium-priority"], 109 | "tasks": [ 110 | "Implement PDF text extraction", 111 | "Create invoice field parser", 112 | "Match invoices with transactions", 113 | "Handle different invoice formats" 114 | ] 115 | }, 116 | { 117 | "title": "Implement AI-based Categorization", 118 | "description": "Integration with Gemini AI for transaction categorization", 119 | "labels": ["feature", "high-priority"], 120 | "tasks": [ 121 | "Setup Gemini AI integration", 122 | "Implement categorization logic", 123 | "Create custom category management", 124 | "Add category override functionality", 125 | "Implement AI provider switching" 126 | ] 127 | }, 128 | { 129 | "title": "Manual Transaction Management", 130 | "description": "Allow users to manually add and edit transactions", 131 | "labels": ["feature", "medium-priority"] 132 | } 133 | ] 134 | }, 135 | { 136 | "title": "Budget Management", 137 | "description": "Budget setting and tracking features", 138 | "issues": [ 139 | { 140 | "title": "Implement Budget Settings", 141 | "description": "Allow users to set and manage budgets", 142 | "labels": ["feature", "high-priority"], 143 | "tasks": [ 144 | "Create daily budget setting", 145 | "Implement weekly budget", 146 | "Add monthly budget tracking", 147 | "Create yearly budget planning", 148 | "Add budget notification system" 149 | ] 150 | } 151 | ] 152 | }, 153 | { 154 | "title": "Analytics and Reporting", 155 | "description": "Data visualization and reporting features", 156 | "issues": [ 157 | { 158 | "title": "Implement Transaction Analytics", 159 | "description": "Create graphical representations of transaction data", 160 | "labels": ["feature", "high-priority"], 161 | "tasks": [ 162 | "Implement date-based filtering", 163 | "Create category-based analysis", 164 | "Add custom date range support", 165 | "Create trend analysis charts" 166 | ] 167 | }, 168 | { 169 | "title": "Export/Import Functionality", 170 | "description": "Implement data export and import features", 171 | "labels": ["feature", "medium-priority"], 172 | "tasks": [ 173 | "Implement CSV export", 174 | "Add JSON export", 175 | "Create PDF report generation", 176 | "Implement CSV import validation", 177 | "Add import data mapping" 178 | ] 179 | } 180 | ] 181 | }, 182 | { 183 | "title": "Social Features", 184 | "description": "Implement user sharing and social features", 185 | "issues": [ 186 | { 187 | "title": "Transaction Sharing", 188 | "description": "Allow users to share transactions with other app users", 189 | "labels": ["feature", "low-priority"], 190 | "tasks": [ 191 | "Implement share functionality", 192 | "Create transaction view permissions", 193 | "Add user search/selection", 194 | "Implement share notifications" 195 | ] 196 | } 197 | ] 198 | } 199 | ] 200 | } -------------------------------------------------------------------------------- /plan.md: -------------------------------------------------------------------------------- 1 | Screen List 2 | 3 | 4 | 1. **Authentication Screens** (Firebase Auth) 5 | - Login/Sign up 6 | - PIN/Biometric setup 7 | - Forgot password 8 | 9 | 2. **Main Screens** 10 | - Dashboard/Home 11 | - Transaction history 12 | - Analytics/Reports 13 | - Settings 14 | 15 | 3. **Transaction Screens** 16 | - Transaction details 17 | - Manual transaction entry 18 | - Category assignment 19 | - Edit transaction 20 | 21 | 4. **Profile & Settings** 22 | - User profile 23 | - Notification settings 24 | - Language settings (Amharic/English) 25 | - Bank account management 26 | - Contact nicknames 27 | 28 | 5. **Analytics & Reports** 29 | - Monthly summary 30 | - Category breakdown 31 | - Budget tracking 32 | - Spending trends 33 | 34 | 6. **Additional Screens** 35 | - Search transactions 36 | - Export data 37 | - Help/FAQ 38 | - About app 39 | - SMS settings/permissions 40 | 41 | Would you like me to provide the Flutter implementation for any of these screens? -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: muday 2 | description: "Expense Tracker For Ethiopian Banks and Finanical Wallets" 3 | 4 | # Prevent accidental publishing to pub.dev. 5 | publish_to: 'none' 6 | 7 | version: 1.0.0+1 8 | 9 | environment: 10 | sdk: ^3.6.0 11 | 12 | dependencies: 13 | flutter: 14 | sdk: flutter 15 | flutter_localizations: 16 | sdk: flutter 17 | http: ^1.2.2 18 | url_launcher: ^6.3.1 # to be removed 19 | sqflite: ^2.4.1 20 | path: 1.9.0 21 | uuid: ^4.4.2 22 | 23 | # for reading sms 24 | flutter_sms_inbox: ^1.0.3 25 | permission_handler: ^10.2.0 26 | syncfusion_flutter_pdf: ^28.1.35 27 | dio: ^5.7.0 28 | 29 | # for background tasks 30 | flutter_background_service: ^5.1.0 31 | 32 | # DEV DEPENDENCIES 33 | dev_dependencies: 34 | flutter_test: 35 | sdk: flutter 36 | 37 | flutter_lints: ^5.0.0 38 | sqflite_common_ffi: any 39 | 40 | flutter_launcher_icons: 41 | android: "launcher_icon" 42 | ios: true 43 | image_path: "assets/icon/icon.png" 44 | 45 | 46 | 47 | flutter: 48 | uses-material-design: true 49 | # Enable generation of localized Strings from arb files. 50 | generate: true 51 | 52 | assets: 53 | # Add assets from the images directory to the application. 54 | - assets/images/ 55 | -------------------------------------------------------------------------------- /schema.md: -------------------------------------------------------------------------------- 1 | Schema for Transactions 2 | 3 | ## Transaction 4 | 5 | ### CBE Transaction 6 | 7 | data i can get from the sms[always available] 8 | - id [Required, String]: Unique identifier for the transaction. 9 | - amount [Required, Number]: Amount of the transaction. 10 | - service_fee [Number]: Service fee for the transaction. 11 | - vat [Number]: Value-added tax for the transaction. 12 | - total [Required, Number]: Total amount of the transaction. 13 | - date [Required, Date]: Date of the transaction. 14 | 15 | pdf parse[based on the user preference so all are optional] 16 | 17 | - payer [String]: Payer of the transaction. 18 | - PayerAccount [String]: Account of the transaction. 19 | - Receiver [String]: Receiver of the transaction. 20 | - ReceiverAccount [String]: Account of the transaction. 21 | - Reason [String]: Reason for the transaction. 22 | 23 | ### Telebirr Transaction 24 | 25 | data i can get from the sms[always available] 26 | - id [Required, String]: Unique identifier for the transaction. 27 | - amount [Required, Number]: Amount of the transaction. 28 | - service_fee [Number]: Service fee for the transaction. 29 | - vat [Number]: Value-added tax for the transaction. 30 | 31 | pdf parse [based on the user preference so all are optional] 32 | - payer [String]: Payer of the transaction. 33 | - PayerAccount [String]: Account of the transaction. 34 | - payer_account_type [String]: Account type of the payer. 35 | - payer_tin_number [String]: TIN number of the payer. 36 | - Receiver [String]: Receiver of the transaction. 37 | - ReceiverAccount [String]: Account of the transaction. 38 | - status [String]: Status of the transaction. 39 | - stamp_duty [Number]: Stamp duty for the transaction. 40 | - discount [Number]: Discount for the transaction. 41 | - channel [String]: Channel of the transaction. -------------------------------------------------------------------------------- /test/unit_test.dart: -------------------------------------------------------------------------------- 1 | // This is an example unit test. 2 | // 3 | // A unit test tests a single function, method, or class. To learn more about 4 | // writing unit tests, visit 5 | // https://flutter.dev/to/unit-testing 6 | 7 | import 'package:flutter_test/flutter_test.dart'; 8 | 9 | void main() { 10 | group('Plus Operator', () { 11 | test('should add two numbers together', () { 12 | expect(1 + 1, 2); 13 | }); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is an example Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | // 8 | // Visit https://flutter.dev/to/widget-testing for 9 | // more information about Widget testing. 10 | 11 | import 'package:flutter/material.dart'; 12 | import 'package:flutter_test/flutter_test.dart'; 13 | 14 | void main() { 15 | group('MyWidget', () { 16 | testWidgets('should display a string of text', (WidgetTester tester) async { 17 | // Define a Widget 18 | const myWidget = MaterialApp( 19 | home: Scaffold( 20 | body: Text('Hello'), 21 | ), 22 | ); 23 | 24 | // Build myWidget and trigger a frame. 25 | await tester.pumpWidget(myWidget); 26 | 27 | // Verify myWidget shows some text 28 | expect(find.byType(Text), findsOneWidget); 29 | }); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | my_expense_tracker 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my_expense_tracker", 3 | "short_name": "my_expense_tracker", 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.14) 3 | project(my_expense_tracker LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "my_expense_tracker") 8 | 9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 10 | # versions of CMake. 11 | cmake_policy(VERSION 3.14...3.25) 12 | 13 | # Define build configuration option. 14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 15 | if(IS_MULTICONFIG) 16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 17 | CACHE STRING "" FORCE) 18 | else() 19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 20 | set(CMAKE_BUILD_TYPE "Debug" CACHE 21 | STRING "Flutter build mode" FORCE) 22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 23 | "Debug" "Profile" "Release") 24 | endif() 25 | endif() 26 | # Define settings for the Profile build mode. 27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 31 | 32 | # Use Unicode for all projects. 33 | add_definitions(-DUNICODE -D_UNICODE) 34 | 35 | # Compilation settings that should be applied to most targets. 36 | # 37 | # Be cautious about adding new options here, as plugins use this function by 38 | # default. In most cases, you should add new options to specific targets instead 39 | # of modifying this function. 40 | function(APPLY_STANDARD_SETTINGS TARGET) 41 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 43 | target_compile_options(${TARGET} PRIVATE /EHsc) 44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 46 | endfunction() 47 | 48 | # Flutter library and tool build rules. 49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 50 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 51 | 52 | # Application build; see runner/CMakeLists.txt. 53 | add_subdirectory("runner") 54 | 55 | 56 | # Generated plugin build rules, which manage building the plugins and adding 57 | # them to the application. 58 | include(flutter/generated_plugins.cmake) 59 | 60 | 61 | # === Installation === 62 | # Support files are copied into place next to the executable, so that it can 63 | # run in place. This is done instead of making a separate bundle (as on Linux) 64 | # so that building and running from within Visual Studio will work. 65 | set(BUILD_BUNDLE_DIR "$") 66 | # Make the "install" step default, as it's required to run. 67 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 68 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 69 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 70 | endif() 71 | 72 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 73 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 74 | 75 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 76 | COMPONENT Runtime) 77 | 78 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 79 | COMPONENT Runtime) 80 | 81 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 82 | COMPONENT Runtime) 83 | 84 | if(PLUGIN_BUNDLED_LIBRARIES) 85 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 86 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 87 | COMPONENT Runtime) 88 | endif() 89 | 90 | # Copy the native assets provided by the build.dart from all packages. 91 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") 92 | install(DIRECTORY "${NATIVE_ASSETS_DIR}" 93 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 94 | COMPONENT Runtime) 95 | 96 | # Fully re-copy the assets directory on each build to avoid having stale files 97 | # from a previous install. 98 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 99 | install(CODE " 100 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 101 | " COMPONENT Runtime) 102 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 103 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 104 | 105 | # Install the AOT library on non-Debug builds only. 106 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 107 | CONFIGURATIONS Profile;Release 108 | COMPONENT Runtime) 109 | -------------------------------------------------------------------------------- /windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.14) 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 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 12 | 13 | # Set fallback configurations for older versions of the flutter tool. 14 | if (NOT DEFINED FLUTTER_TARGET_PLATFORM) 15 | set(FLUTTER_TARGET_PLATFORM "windows-x64") 16 | endif() 17 | 18 | # === Flutter Library === 19 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 20 | 21 | # Published to parent scope for install step. 22 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 23 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 24 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 25 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 26 | 27 | list(APPEND FLUTTER_LIBRARY_HEADERS 28 | "flutter_export.h" 29 | "flutter_windows.h" 30 | "flutter_messenger.h" 31 | "flutter_plugin_registrar.h" 32 | "flutter_texture_registrar.h" 33 | ) 34 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 35 | add_library(flutter INTERFACE) 36 | target_include_directories(flutter INTERFACE 37 | "${EPHEMERAL_DIR}" 38 | ) 39 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 40 | add_dependencies(flutter flutter_assemble) 41 | 42 | # === Wrapper === 43 | list(APPEND CPP_WRAPPER_SOURCES_CORE 44 | "core_implementations.cc" 45 | "standard_codec.cc" 46 | ) 47 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 48 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 49 | "plugin_registrar.cc" 50 | ) 51 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 52 | list(APPEND CPP_WRAPPER_SOURCES_APP 53 | "flutter_engine.cc" 54 | "flutter_view_controller.cc" 55 | ) 56 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 57 | 58 | # Wrapper sources needed for a plugin. 59 | add_library(flutter_wrapper_plugin STATIC 60 | ${CPP_WRAPPER_SOURCES_CORE} 61 | ${CPP_WRAPPER_SOURCES_PLUGIN} 62 | ) 63 | apply_standard_settings(flutter_wrapper_plugin) 64 | set_target_properties(flutter_wrapper_plugin PROPERTIES 65 | POSITION_INDEPENDENT_CODE ON) 66 | set_target_properties(flutter_wrapper_plugin PROPERTIES 67 | CXX_VISIBILITY_PRESET hidden) 68 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 69 | target_include_directories(flutter_wrapper_plugin PUBLIC 70 | "${WRAPPER_ROOT}/include" 71 | ) 72 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 73 | 74 | # Wrapper sources needed for the runner. 75 | add_library(flutter_wrapper_app STATIC 76 | ${CPP_WRAPPER_SOURCES_CORE} 77 | ${CPP_WRAPPER_SOURCES_APP} 78 | ) 79 | apply_standard_settings(flutter_wrapper_app) 80 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 81 | target_include_directories(flutter_wrapper_app PUBLIC 82 | "${WRAPPER_ROOT}/include" 83 | ) 84 | add_dependencies(flutter_wrapper_app flutter_assemble) 85 | 86 | # === Flutter tool backend === 87 | # _phony_ is a non-existent file to force this command to run every time, 88 | # since currently there's no way to get a full input/output list from the 89 | # flutter tool. 90 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 91 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 92 | add_custom_command( 93 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 94 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 95 | ${CPP_WRAPPER_SOURCES_APP} 96 | ${PHONY_OUTPUT} 97 | COMMAND ${CMAKE_COMMAND} -E env 98 | ${FLUTTER_TOOL_ENVIRONMENT} 99 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 100 | ${FLUTTER_TARGET_PLATFORM} $ 101 | VERBATIM 102 | ) 103 | add_custom_target(flutter_assemble DEPENDS 104 | "${FLUTTER_LIBRARY}" 105 | ${FLUTTER_LIBRARY_HEADERS} 106 | ${CPP_WRAPPER_SOURCES_CORE} 107 | ${CPP_WRAPPER_SOURCES_PLUGIN} 108 | ${CPP_WRAPPER_SOURCES_APP} 109 | ) 110 | -------------------------------------------------------------------------------- /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 | #include 10 | #include 11 | 12 | void RegisterPlugins(flutter::PluginRegistry* registry) { 13 | PermissionHandlerWindowsPluginRegisterWithRegistrar( 14 | registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); 15 | UrlLauncherWindowsRegisterWithRegistrar( 16 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 17 | } 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | permission_handler_windows 7 | url_launcher_windows 8 | ) 9 | 10 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 11 | ) 12 | 13 | set(PLUGIN_BUNDLED_LIBRARIES) 14 | 15 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 16 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 17 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 19 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 20 | endforeach(plugin) 21 | 22 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 23 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 24 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 25 | endforeach(ffi_plugin) 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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", "my_expense_tracker" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "my_expense_tracker" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "my_expense_tracker.exe" "\0" 98 | VALUE "ProductName", "my_expense_tracker" "\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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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"my_expense_tracker", 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /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 | unsigned 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------