├── lib ├── test.dart ├── data │ ├── models │ │ ├── category_details.dart │ │ ├── home_tab.dart │ │ ├── entry_list.dart │ │ ├── category_with_entry_list.dart │ │ ├── history.dart │ │ ├── category_with_sum.dart │ │ ├── entry_with_category.dart │ │ ├── entry.dart │ │ └── category.dart │ ├── datasource │ │ ├── sharedpref │ │ │ ├── preferences.dart │ │ │ └── shared_preference_helper.dart │ │ ├── language_data.dart │ │ ├── entry_dataSource.dart │ │ └── local │ │ │ └── moor │ │ │ └── new_app_database.dart │ └── repository │ │ ├── entry_repository.dart │ │ └── entry_repository_imp.dart ├── core │ ├── routes.dart │ ├── custom_scroll_physics.dart │ ├── Logger.dart │ ├── CrashReportingTree.dart │ ├── color_scheme.dart │ ├── date_time_util.dart │ ├── currency_text_input_formatter.dart │ ├── app_localization.dart │ ├── theme.dart │ └── constants.dart ├── extension │ ├── icon_data_extension.dart │ ├── string_extension.dart │ ├── list_extension.dart │ └── datetime_extension.dart ├── ui │ ├── home │ │ ├── home_state.dart │ │ └── home.dart │ ├── dialog │ │ ├── common_alert_dialog.dart │ │ ├── category_details_filter_dialog.dart │ │ ├── history_filter_dialog.dart │ │ ├── currency_dialog.dart │ │ ├── language_dialog.dart │ │ ├── theme_dialog.dart │ │ └── monthly_cycle_date_dialog.dart │ ├── setting │ │ └── setting_view_model.dart │ ├── category_list │ │ ├── category_list_state.dart │ │ └── category_list.dart │ ├── dashboard │ │ ├── chart_painter.dart │ │ ├── category_list_view.dart │ │ ├── pie_chart.dart │ │ └── dashboard_state.dart │ ├── history │ │ ├── history_view_model.dart │ │ ├── history.dart │ │ ├── month_list.dart │ │ ├── year_list.dart │ │ └── entry_type.dart │ ├── app │ │ ├── app_state.dart │ │ └── app.dart │ ├── addCategory │ │ └── addCategory_state.dart │ └── addEntry │ │ └── addEntry_state.dart └── main.dart ├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── Gemfile ├── fastlane │ ├── Appfile │ ├── Matchfile │ └── Fastfile ├── .gitignore └── Podfile ├── images ├── page_1.png ├── page_2.png ├── page_3.png ├── page_4.png └── onboarding.gif ├── assets ├── images │ ├── page_1.png │ ├── page_2.png │ ├── page_3.png │ ├── expense_manage.png │ └── add_expense_arrow.png └── language │ ├── en.json │ ├── es.json │ └── pt.json ├── android ├── android_keys.zip.gpg ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ └── values │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── nividata │ │ │ │ │ └── expense_manager │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── fastlane │ ├── Appfile │ └── Fastfile ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── Gemfile ├── settings.gradle └── build.gradle ├── .metadata ├── .github ├── scripts │ └── decrypt_android_secrets.sh └── workflows │ ├── ios-build.yml │ ├── android-build.yml │ ├── ios-deployment.yml │ └── android-deployment.yml ├── LICENSE ├── test └── widget_test.dart ├── README.md ├── .gitignore └── pubspec.yaml /lib/test.dart: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/data/models/category_details.dart: -------------------------------------------------------------------------------- 1 | class CategoryDetails {} 2 | -------------------------------------------------------------------------------- /lib/data/models/home_tab.dart: -------------------------------------------------------------------------------- 1 | enum HomeTab { dashboard, history } 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /images/page_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/images/page_1.png -------------------------------------------------------------------------------- /images/page_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/images/page_2.png -------------------------------------------------------------------------------- /images/page_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/images/page_3.png -------------------------------------------------------------------------------- /images/page_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/images/page_4.png -------------------------------------------------------------------------------- /images/onboarding.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/images/onboarding.gif -------------------------------------------------------------------------------- /assets/images/page_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/assets/images/page_1.png -------------------------------------------------------------------------------- /assets/images/page_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/assets/images/page_2.png -------------------------------------------------------------------------------- /assets/images/page_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/assets/images/page_3.png -------------------------------------------------------------------------------- /android/android_keys.zip.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/android/android_keys.zip.gpg -------------------------------------------------------------------------------- /assets/images/expense_manage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/assets/images/expense_manage.png -------------------------------------------------------------------------------- /assets/images/add_expense_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/assets/images/add_expense_arrow.png -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.enableR8=true 5 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/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/jaysavsani07/expense-manager/HEAD/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/jaysavsani07/expense-manager/HEAD/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/jaysavsani07/expense-manager/HEAD/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/jaysavsani07/expense-manager/HEAD/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/jaysavsani07/expense-manager/HEAD/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/jaysavsani07/expense-manager/HEAD/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/jaysavsani07/expense-manager/HEAD/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/jaysavsani07/expense-manager/HEAD/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/jaysavsani07/expense-manager/HEAD/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/jaysavsani07/expense-manager/HEAD/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/jaysavsani07/expense-manager/HEAD/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/jaysavsani07/expense-manager/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaysavsani07/expense-manager/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/nividata/expense_manager/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.nividata.expense_manager 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | json_key_file("service_account_key.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one 2 | package_name("com.nividata.expense_manager") # e.g. com.krausefx.app 3 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 7 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /lib/data/datasource/sharedpref/preferences.dart: -------------------------------------------------------------------------------- 1 | class Preferences { 2 | static const USER_NAME = "userName"; 3 | static const IS_DARK_MODE = "isDarkMode"; 4 | static const DEFAULT_LANGUAGE = "defaultLanguage"; 5 | static const DEFAULT_CURRENCY = "defaultCurrency"; 6 | static const MONTH_CYCLE_DATE = "monthCycleDate"; 7 | } 8 | -------------------------------------------------------------------------------- /ios/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | 5 | # Add this plugin 6 | gem "fastlane-plugin-flutter_version", git: "https://github.com/tianhaoz95/fastlane-plugin-flutter-version" 7 | plugins_path = File.join(File.dirname(__FILE__), '.', 'Pluginfile') 8 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 84f3d28555368a70270e9ac8390a9441df95e752 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /android/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | 5 | # Add this plugin to retrieve version code for flutter 6 | gem "fastlane-plugin-flutter_version", git: "https://github.com/tianhaoz95/fastlane-plugin-flutter-version" 7 | plugins_path = File.join(File.dirname(__FILE__), '.', 'Pluginfile') 8 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 9 | -------------------------------------------------------------------------------- /ios/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | app_identifier("com.nividata.expensemanager") # The bundle identifier of your app 2 | apple_id("nividataapps@gmail.com") # Your Apple email address 3 | 4 | itc_team_id("120740426") # App Store Connect Team ID 5 | team_id("UJMS4XPZSS") # Developer Portal Team ID 6 | 7 | # For more information about the Appfile, see: 8 | # https://docs.fastlane.tools/advanced/#appfile 9 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /lib/core/routes.dart: -------------------------------------------------------------------------------- 1 | class AppRoutes { 2 | static final welcome = '/welcome'; 3 | static final onBoarding = '/onBoarding'; 4 | static final home = '/home'; 5 | static final addEntry = '/addEntry'; 6 | static final categoryList = '/categoryList'; 7 | static final addCategory = '/addCategory'; 8 | static final categoryDetails = '/categoryDetails'; 9 | static final setting = '/setting'; 10 | } 11 | -------------------------------------------------------------------------------- /lib/extension/icon_data_extension.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | extension IconDataExtension on IconData { 6 | String iconDataToJson() => jsonEncode({ 7 | 'codePoint': this.codePoint, 8 | 'fontFamily': this.fontFamily, 9 | 'fontPackage': this.fontPackage, 10 | 'matchTextDirection': this.matchTextDirection 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /lib/extension/string_extension.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | 5 | extension StringExtension on String { 6 | IconData jsonToIconData() { 7 | Map map = jsonDecode(this); 8 | return IconData( 9 | map['codePoint'], 10 | fontFamily: map['fontFamily'], 11 | fontPackage: map['fontPackage'], 12 | matchTextDirection: map['matchTextDirection'], 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/data/models/entry_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/data/models/entry.dart'; 2 | 3 | class EntryList { 4 | final String title; 5 | final List list; 6 | 7 | EntryList({ 8 | required this.title, 9 | required this.list, 10 | }); 11 | 12 | EntryList copyWith({ 13 | String? title, 14 | List? list, 15 | }) { 16 | return EntryList(title: title ?? this.title, list: list ?? this.list); 17 | } 18 | 19 | @override 20 | String toString() { 21 | return 'EntryList{title: $title, list: $list}'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/fastlane/Matchfile: -------------------------------------------------------------------------------- 1 | git_url("https://github.com/nividata-consultancy/certificates-ios.git") 2 | 3 | storage_mode("git") 4 | 5 | type("development") # The default type, can be: appstore, adhoc, enterprise or development 6 | type("appstore") 7 | 8 | # app_identifier(["tools.fastlane.app", "tools.fastlane.app2"]) 9 | # username("user@fastlane.tools") # Your Apple Developer Portal username 10 | 11 | # For all available options run `fastlane match --help` 12 | # Remove the # in the beginning of the line to enable the other options 13 | 14 | # The docs are available on https://docs.fastlane.tools/actions/match 15 | -------------------------------------------------------------------------------- /lib/ui/home/home_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/data/models/home_tab.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | 5 | final signInModelProvider = ChangeNotifierProvider( 6 | (ref) => HomeViewModel(), 7 | ); 8 | 9 | class HomeViewModel with ChangeNotifier { 10 | HomeTab activeTab = HomeTab.dashboard; 11 | 12 | changeTab(int index) { 13 | if (index == 0) { 14 | activeTab = HomeTab.dashboard; 15 | } else { 16 | activeTab = HomeTab.history; 17 | } 18 | notifyListeners(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/extension/list_extension.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/data/models/entry_with_category.dart'; 2 | 3 | extension ListExtension on List { 4 | Map> groupListsByMonth(int currentMonth) { 5 | var result = >{}; 6 | 7 | for (int i = 1; i <= currentMonth; i++) { 8 | result[i] = []; 9 | } 10 | for (var element in this) { 11 | if (element.entry.modifiedDate.month <= currentMonth) 12 | (result[element.entry.modifiedDate.month])!..add(element); 13 | } 14 | return result; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/core/custom_scroll_physics.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomScrollPhysics extends PageScrollPhysics { 4 | const CustomScrollPhysics({ScrollPhysics? parent}) : super(parent: parent); 5 | 6 | @override 7 | double get minFlingVelocity => 0.01; 8 | 9 | @override 10 | double get maxFlingVelocity => 0.7; 11 | 12 | @override 13 | double get minFlingDistance => 12; 14 | 15 | @override 16 | double get dragStartDistanceMotionThreshold => 40; 17 | 18 | @override 19 | CustomScrollPhysics applyTo(ScrollPhysics? ancestor) { 20 | return CustomScrollPhysics(parent: buildParent(ancestor)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/scripts/decrypt_android_secrets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # --batch to prevent interactive command 4 | # --yes to assume "yes" for questions 5 | gpg --quiet --batch --yes --decrypt --passphrase="$ANDROID_KEYS_SECRET_PASSPHRASE" \ 6 | --output android/android_keys.zip android/android_keys.zip.gpg && cd android && jar xvf android_keys.zip && cd - 7 | echo $PWD 8 | 9 | echo "********1" 10 | ls -d $PWD/android/* 11 | mv ./android/expensemanager.jks ./android/app 12 | mv ./android/google-services.json ./android/app 13 | echo ./ 14 | echo "********2" 15 | FILE=./android/app/expensemanager.jks 16 | if test -f "$FILE"; then 17 | echo "$FILE exists." 18 | fi 19 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /lib/ui/dialog/common_alert_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CommonAlertDialog extends AlertDialog { 4 | final Widget child; 5 | 6 | CommonAlertDialog({required this.child}) : super(); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Align( 11 | alignment: Alignment.topCenter, 12 | child: Padding( 13 | padding: const EdgeInsets.only( 14 | left: 24, right: 24, top: kToolbarHeight + 12), 15 | child: Material( 16 | shape: RoundedRectangleBorder( 17 | borderRadius: BorderRadius.all(Radius.circular(14))), 18 | child: child, 19 | ), 20 | ), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/data/datasource/language_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | class Language { 4 | final int id; 5 | final String name; 6 | final String flag; 7 | final Locale locale; 8 | 9 | Language({ 10 | required this.id, 11 | required this.name, 12 | required this.flag, 13 | required this.locale, 14 | }); 15 | 16 | static List languageList() { 17 | return [ 18 | Language( 19 | id: 1, name: "English", flag: "🇺🇸", locale: Locale('en', 'US')), 20 | Language( 21 | id: 2, name: "Spanish", flag: "🇪🇸", locale: Locale('es', 'ES')), 22 | Language( 23 | id: 3, name: "Portuguese", flag: "🇧🇷", locale: Locale('pt', 'BR')), 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/core/Logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:fimber/fimber.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | 4 | class Logger extends ProviderObserver { 5 | @override 6 | void didUpdateProvider( 7 | ProviderBase provider, 8 | Object? previousValue, 9 | Object? newValue, 10 | ProviderContainer container, 11 | ) { 12 | // print(''' 13 | // { 14 | // "provider": "${provider.name ?? provider.runtimeType}", 15 | // "newValue": "$newValue" 16 | // }'''); 17 | } 18 | 19 | @override 20 | void providerDidFail(ProviderBase provider, Object error, 21 | StackTrace stackTrace, ProviderContainer container) { 22 | Fimber.e(provider.name??"Ok", ex: error, stacktrace: stackTrace); 23 | super.providerDidFail(provider, error, stackTrace, container); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/core/CrashReportingTree.dart: -------------------------------------------------------------------------------- 1 | import 'package:fimber/fimber.dart'; 2 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 3 | 4 | class CrashReportingTree extends LogTree { 5 | //Only Log Warnings and Exceptions 6 | static const List defaultLevels = ["W", "E"]; 7 | final List logLevels; 8 | 9 | @override 10 | List getLevels() => logLevels; 11 | 12 | CrashReportingTree({this.logLevels = defaultLevels}); 13 | 14 | @override 15 | void log( 16 | String level, 17 | String message, { 18 | String? tag, 19 | dynamic ex, 20 | StackTrace? stacktrace, 21 | }) { 22 | final crashlytics = FirebaseCrashlytics.instance; 23 | crashlytics.log(message); 24 | 25 | if (ex != null) { 26 | crashlytics.recordError(ex, stacktrace); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/ui/dialog/category_details_filter_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/ui/category_details/category_details.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | 5 | class CategoryDetailsFilterDialog extends StatelessWidget { 6 | @override 7 | Widget build(BuildContext context) { 8 | return Consumer( 9 | builder: (context, ref, child) { 10 | return Padding( 11 | padding: const EdgeInsets.symmetric(vertical: 24), 12 | child: Column( 13 | crossAxisAlignment: CrossAxisAlignment.center, 14 | mainAxisSize: MainAxisSize.min, 15 | children: [ 16 | YearListView(), 17 | QuarterListView(), 18 | ], 19 | ), 20 | ); 21 | }, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/ui/dialog/history_filter_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/ui/history/entry_type.dart'; 2 | import 'package:expense_manager/ui/history/year_list.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | 6 | class HistoryFilterDialog extends StatelessWidget { 7 | @override 8 | Widget build(BuildContext context) { 9 | return Consumer( 10 | builder: (context, ref, child) { 11 | return Padding( 12 | padding: const EdgeInsets.symmetric(vertical: 24), 13 | child: Column( 14 | crossAxisAlignment: CrossAxisAlignment.center, 15 | mainAxisSize: MainAxisSize.min, 16 | children: [ 17 | EntryTypeView(), 18 | YearList(), 19 | ], 20 | ), 21 | ); 22 | }, 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.3.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath 'com.google.gms:google-services:4.3.8' 12 | classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' 13 | } 14 | } 15 | 16 | allprojects { 17 | repositories { 18 | google() 19 | jcenter() 20 | } 21 | } 22 | 23 | rootProject.buildDir = '../build' 24 | subprojects { 25 | project.buildDir = "${rootProject.buildDir}/${project.name}" 26 | } 27 | subprojects { 28 | project.evaluationDependsOn(':app') 29 | } 30 | 31 | tasks.register("clean", Delete) { 32 | delete rootProject.buildDir 33 | } 34 | -------------------------------------------------------------------------------- /lib/data/models/category_with_entry_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/data/models/category.dart'; 2 | import 'package:expense_manager/data/models/entry.dart'; 3 | 4 | class CategoryWithEntryList { 5 | final Category category; 6 | final double total; 7 | final List entry; 8 | 9 | CategoryWithEntryList({ 10 | required this.category, 11 | required this.total, 12 | required this.entry, 13 | }); 14 | 15 | CategoryWithEntryList copyWith({ 16 | List? entry, 17 | double? total, 18 | Category? category, 19 | }) { 20 | return CategoryWithEntryList( 21 | category: category ?? this.category, 22 | total: total ?? this.total, 23 | entry: entry ?? this.entry, 24 | ); 25 | } 26 | 27 | @override 28 | String toString() { 29 | return 'CategoryWithEntry{category: $category, total: $total, entry: $entry}'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/core/color_scheme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | extension CustomColorScheme on ColorScheme { 4 | Color get baseColor => 5 | brightness == Brightness.light ? Colors.white : Colors.black; 6 | // 7 | Color get baseLightColor => 8 | brightness == Brightness.light ? Colors.white60 : Colors.black54; 9 | // 10 | Color get crossColor => 11 | brightness == Brightness.light ? Colors.black : Colors.white; 12 | // 13 | Color get crossLightColor => 14 | brightness == Brightness.light ? Colors.black54 : Colors.white60; 15 | 16 | Color get paiChartColor => 17 | brightness == Brightness.light ? Color(0xFFBDCDE0) : Color(0xFF292D32); 18 | 19 | Color get paiChartShadowLightColor => 20 | brightness == Brightness.light ? Colors.black : Colors.white; 21 | 22 | Color get paiChartShadowDarkColor => 23 | brightness == Brightness.light ? Colors.white : Colors.black; 24 | } 25 | -------------------------------------------------------------------------------- /lib/data/models/history.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/data/models/entry_with_category.dart'; 2 | 3 | class History { 4 | final String title; 5 | final List list; 6 | 7 | History({ 8 | required this.title, 9 | required this.list, 10 | }); 11 | 12 | History copyWith({ 13 | String? title, 14 | List? list, 15 | }) { 16 | return History(title: title ?? this.title, list: list ?? this.list); 17 | } 18 | 19 | @override 20 | String toString() { 21 | return 'History{title: $title, list: $list}'; 22 | } 23 | 24 | /*factory History.fromEntryEntity(EntryEntityData entityData) { 25 | return History(id: entityData.id, amount: entityData.amount); 26 | } 27 | 28 | EntryEntityCompanion toEntryEntityCompanion() { 29 | return EntryEntityCompanion( 30 | amount: Value(amount), 31 | categoryName: Value(categoryName), 32 | modifiedDate: Value(modifiedDate)); 33 | }*/ 34 | 35 | } 36 | -------------------------------------------------------------------------------- /lib/ui/setting/setting_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/data/datasource/sharedpref/preferences.dart'; 2 | import 'package:expense_manager/data/datasource/sharedpref/shared_preference_helper.dart'; 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | 6 | final monthStartDateStateNotifier = 7 | ChangeNotifierProvider((ref) => MonthStartDateState(ref)); 8 | 9 | class MonthStartDateState extends ChangeNotifier { 10 | Ref ref; 11 | String date = "1"; 12 | 13 | MonthStartDateState(this.ref) { 14 | _loadFromPrefs(); 15 | } 16 | 17 | _loadFromPrefs() async { 18 | date = ref 19 | .read(sharedPreferencesProvider) 20 | .getString(Preferences.MONTH_CYCLE_DATE, defValue: "1"); 21 | notifyListeners(); 22 | } 23 | 24 | setDate(String date) async { 25 | this.date = date; 26 | notifyListeners(); 27 | await ref 28 | .read(sharedPreferencesProvider) 29 | .putString(Preferences.MONTH_CYCLE_DATE, date); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nividata Consultancy 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. -------------------------------------------------------------------------------- /.github/workflows/ios-build.yml: -------------------------------------------------------------------------------- 1 | name: iOS build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | # - development 8 | 9 | jobs: 10 | build_ios: 11 | name: Build iOS Runner 12 | runs-on: macOS-latest 13 | steps: 14 | - name: Checkout code from ref 15 | uses: actions/checkout@v2 16 | with: 17 | ref: ${{ github.ref }} 18 | - name: Run Flutter tasks 19 | uses: subosito/flutter-action@v1 20 | with: 21 | flutter-version: '2.5.3' 22 | - run: flutter clean 23 | - run: flutter pub get 24 | - run: flutter build ios --release --no-codesign --no-tree-shake-icons 25 | - name: Fleep Updates 26 | if: always() 27 | uses: dawidd6/action-send-mail@v2 28 | with: 29 | server_address: smtp.gmail.com 30 | server_port: 465 31 | username: ${{secrets.MAIL_USERNAME}} 32 | password: ${{secrets.MAIL_PASSWORD}} 33 | subject: Github Actions Job Results 34 | body: Build - iOS for job ${{github.job}} has ${{job.status}}! 35 | to: conv.0asu9evzgxz6tn@fleep.io 36 | from: Nividata Apps -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. 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 | import 'package:expense_manager/ui/app/app.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter_test/flutter_test.dart'; 11 | 12 | void main() { 13 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 14 | // Build our app and trigger a frame. 15 | await tester.pumpWidget(MyApp()); 16 | 17 | // Verify that our counter starts at 0. 18 | expect(find.text('0'), findsOneWidget); 19 | expect(find.text('1'), findsNothing); 20 | 21 | // Tap the '+' icon and trigger a frame. 22 | await tester.tap(find.byIcon(Icons.add)); 23 | await tester.pump(); 24 | 25 | // Verify that our counter has incremented. 26 | expect(find.text('0'), findsNothing); 27 | expect(find.text('1'), findsOneWidget); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /lib/core/date_time_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/extension/datetime_extension.dart'; 2 | 3 | class DateTimeUtil { 4 | static getStartDateTime(int cycleDate) { 5 | DateTime currentDate = 6 | DateTime.now().copyWith(hour: 0, minute: 0, second: 0); 7 | 8 | if (currentDate.day == 1) { 9 | return currentDate.copyWith(day: cycleDate); 10 | } else { 11 | if (currentDate.day > cycleDate) { 12 | return currentDate.copyWith(day: cycleDate); 13 | } else { 14 | return currentDate.copyWith( 15 | month: currentDate.month - 1, day: cycleDate); 16 | } 17 | } 18 | } 19 | 20 | static getEndDateTime(int cycleDate) { 21 | DateTime currentDate = 22 | DateTime.now().copyWith(hour: 23, minute: 59, second: 59); 23 | 24 | if (currentDate.day == 1) { 25 | return currentDate.copyWith( 26 | day: currentDate.copyWith(month: currentDate.month + 1, day: 0).day); 27 | } else { 28 | if (currentDate.day > cycleDate) { 29 | return currentDate.copyWith( 30 | month: currentDate.month + 1, day: cycleDate - 1); 31 | } else { 32 | return currentDate.copyWith( 33 | month: currentDate.month, day: cycleDate - 1); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/extension/datetime_extension.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | extension DateTimeExtension on DateTime { 4 | DateTime copyWith({ 5 | int? year, 6 | int? month, 7 | int? day, 8 | int? hour, 9 | int? minute, 10 | int? second, 11 | int? millisecond, 12 | int? microsecond, 13 | }) { 14 | return DateTime( 15 | year ?? this.year, 16 | month ?? this.month, 17 | day ?? this.day, 18 | hour ?? this.hour, 19 | minute ?? this.minute, 20 | second ?? this.second, 21 | millisecond ?? this.millisecond, 22 | microsecond ?? this.microsecond, 23 | ); 24 | } 25 | 26 | String toTitle() { 27 | if (this.isToday()) { 28 | return "recent_expanse"; 29 | } else if (this.isYesterday()) { 30 | return "yesterday"; 31 | } else { 32 | return DateFormat.yMMMd().format(this); 33 | } 34 | } 35 | 36 | bool isToday() { 37 | final now = DateTime.now(); 38 | return now.day == this.day && 39 | now.month == this.month && 40 | now.year == this.year; 41 | } 42 | 43 | bool isYesterday() { 44 | final yesterday = DateTime.now().subtract(Duration(days: 1)); 45 | return yesterday.day == this.day && 46 | yesterday.month == this.month && 47 | yesterday.year == this.year; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '10.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | target.build_configurations.each do |config| 41 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '10.0' 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/data/models/category_with_sum.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/data/datasource/local/moor/app_database.dart'; 2 | import 'package:expense_manager/data/models/category.dart'; 3 | 4 | class CategoryWithSum { 5 | final double total; 6 | final Category? category; 7 | 8 | CategoryWithSum({ 9 | required this.total, 10 | required this.category, 11 | }); 12 | 13 | CategoryWithSum copyWith({ 14 | double? total, 15 | Category? category, 16 | }) { 17 | return CategoryWithSum( 18 | total: total ?? this.total, category: category ?? this.category); 19 | } 20 | 21 | factory CategoryWithSum.initial() { 22 | return CategoryWithSum(total: 0, category: null); 23 | } 24 | 25 | @override 26 | String toString() { 27 | return 'CategoryWithSum{total: $total, category: $category}'; 28 | } 29 | 30 | factory CategoryWithSum.fromCategoryWithSumEntity( 31 | CategoryWithSumData entityData) { 32 | return CategoryWithSum( 33 | total: entityData.total, 34 | category: Category.fromExpenseCategoryEntity(entityData.category)); 35 | } 36 | 37 | /*EntryEntityCompanion toEntryEntityCompanion() { 38 | return EntryEntityCompanion( 39 | amount: Value(amount), 40 | categoryName: Value(categoryName), 41 | modifiedDate: Value(modifiedDate)); 42 | }*/ 43 | } 44 | 45 | class CategoryWithSumData { 46 | final double total; 47 | final CategoryEntityData? category; 48 | 49 | CategoryWithSumData({required this.total, required this.category}); 50 | 51 | @override 52 | String toString() { 53 | return 'CategoryWithSumData{total: $total, category: $category}'; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/android-build.yml: -------------------------------------------------------------------------------- 1 | name: android build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | # - development 8 | 9 | jobs: 10 | build_android: 11 | name: Android Build & Upload Artifact 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | - name: Setup Java 17 | uses: actions/setup-java@v1 18 | with: 19 | java-version: 12.x 20 | - name: Decrypt Android keys 21 | run: sh ./.github/scripts/decrypt_android_secrets.sh 22 | env: 23 | ANDROID_KEYS_SECRET_PASSPHRASE: ${{ secrets.ANDROID_KEYS_SECRET_PASSPHRASE }} 24 | - name: Setup Flutter 25 | uses: subosito/flutter-action@v1 26 | with: 27 | flutter-version: 2.5.3 28 | - name: Install Flutter dependencies 29 | run: flutter pub get 30 | # Add build runner commands here if you have any 31 | - name: Build the APK 32 | run: flutter build apk --release --no-tree-shake-icons 33 | - name: Upload artifact to Github 34 | uses: actions/upload-artifact@v1 35 | with: 36 | name: release-apk 37 | path: build/app/outputs/apk/release/app-release.apk 38 | - name: Fleep Updates 39 | if: always() 40 | uses: dawidd6/action-send-mail@v2 41 | with: 42 | server_address: smtp.gmail.com 43 | server_port: 465 44 | username: ${{secrets.MAIL_USERNAME}} 45 | password: ${{secrets.MAIL_PASSWORD}} 46 | subject: Github Actions Job Results 47 | body: Build - Android for job ${{github.job}} has ${{job.status}} ! 48 | to: conv.0asu9evzgxz6tn@fleep.io 49 | from: Nividata Apps 50 | -------------------------------------------------------------------------------- /android/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | # update_fastlane 15 | 16 | #default_platform(:android) 17 | 18 | #platform :android do 19 | # desc "Runs all the tests" 20 | # lane :test do 21 | # gradle(task: "test") 22 | # end 23 | 24 | # desc "Submit a new Beta Build to Crashlytics Beta" 25 | # lane :beta do 26 | # gradle(task: "clean assembleRelease") 27 | # crashlytics 28 | 29 | # sh "your_script.sh" 30 | # You can also use other beta testing services here 31 | # end 32 | 33 | # desc "Deploy a new version to the Google Play" 34 | # lane :deploy do 35 | # gradle(task: "clean assembleRelease") 36 | # upload_to_play_store 37 | # end 38 | #end 39 | 40 | default_platform(:android) 41 | 42 | platform :android do 43 | desc "Deploy to closed beta track" 44 | lane :closed_beta do 45 | begin 46 | gradle(task: "clean") 47 | gradle( 48 | task: "bundle", 49 | build_type: 'Release' 50 | ) 51 | upload_to_play_store( 52 | track: 'production', 53 | aab: '../build/app/outputs/bundle/release/app-release.aab', 54 | skip_upload_metadata: true, 55 | skip_upload_images: true, 56 | skip_upload_screenshots: true, 57 | release_status: "draft", 58 | version_code: flutter_version()["version_code"], 59 | ) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Expense Manager 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | NSAppTransportSecurity 45 | 46 | NSAllowsLocalNetworking 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 18 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/core/currency_text_input_formatter.dart: -------------------------------------------------------------------------------- 1 | library currency_text_input_formatter; 2 | 3 | import 'package:flutter/services.dart'; 4 | 5 | class CurrencyTextInputFormatter extends TextInputFormatter { 6 | @override 7 | TextEditingValue formatEditUpdate( 8 | TextEditingValue oldValue, TextEditingValue newValue) { 9 | bool isInsertedCharacter = 10 | oldValue.text.length + 1 == newValue.text.length && 11 | newValue.text.startsWith(oldValue.text); 12 | bool isRemovedCharacter = 13 | oldValue.text.length - 1 == newValue.text.length && 14 | oldValue.text.startsWith(newValue.text); 15 | String newString = oldValue.text; 16 | if (!isInsertedCharacter && !isRemovedCharacter) { 17 | return oldValue; 18 | } else if (isInsertedCharacter) { 19 | print(newValue.text.substring(newValue.text.length - 1) == "."); 20 | print(oldValue.text.contains(".")); 21 | if (newValue.text.substring(newValue.text.length - 1) == "." && 22 | oldValue.text.contains(".")) { 23 | newString = oldValue.text; 24 | } else if (newValue.text.substring(newValue.text.length - 1) == " ") { 25 | newString = oldValue.text; 26 | } else { 27 | if (newValue.text.contains(".")) { 28 | List list = newValue.text.split("."); 29 | if (list[1].length < 3) { 30 | newString = newValue.text; 31 | } else { 32 | newString = oldValue.text; 33 | } 34 | } else { 35 | if (oldValue.text.length < 6) { 36 | newString = newValue.text; 37 | } else { 38 | newString = oldValue.text; 39 | } 40 | } 41 | } 42 | } else { 43 | newString = newValue.text; 44 | } 45 | return TextEditingValue( 46 | text: newString, 47 | selection: TextSelection.collapsed(offset: newString.length), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/core/CrashReportingTree.dart'; 2 | import 'package:expense_manager/core/Logger.dart'; 3 | import 'package:expense_manager/data/datasource/sharedpref/shared_preference_helper.dart'; 4 | import 'package:expense_manager/ui/app/app.dart'; 5 | import 'package:fimber/fimber.dart'; 6 | import 'package:firebase_core/firebase_core.dart'; 7 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 8 | import 'package:flutter/foundation.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 11 | import 'package:shared_preferences/shared_preferences.dart'; 12 | import 'package:stack_trace/stack_trace.dart' as stack_trace; 13 | 14 | Future main() async { 15 | WidgetsFlutterBinding.ensureInitialized(); 16 | FlutterError.demangleStackTrace = (StackTrace stack) { 17 | if (stack is stack_trace.Trace) return stack.vmTrace; 18 | if (stack is stack_trace.Chain) return stack.toTrace().vmTrace; 19 | return stack; 20 | }; 21 | 22 | await Firebase.initializeApp(); 23 | 24 | FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError; 25 | 26 | if (kDebugMode) { 27 | Fimber.plantTree(DebugTree(useColors: true)); 28 | await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(false); 29 | } else { 30 | Fimber.plantTree(CrashReportingTree()); 31 | } 32 | 33 | // Errors outside of Flutter 34 | PlatformDispatcher.instance.onError = (error, stack) { 35 | FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); 36 | return true; 37 | }; 38 | 39 | final sharedPreferences = await SharedPreferences.getInstance(); 40 | 41 | runApp(ProviderScope( 42 | observers: [Logger()], 43 | overrides: [ 44 | sharedPreferencesProvider 45 | .overrideWithValue(SharedPreferencesHelper(prefs: sharedPreferences)) 46 | ], 47 | child: MyApp(), 48 | )); 49 | } 50 | 51 | 52 | -------------------------------------------------------------------------------- /lib/data/repository/entry_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/core/constants.dart'; 2 | import 'package:expense_manager/data/models/category.dart'; 3 | import 'package:expense_manager/data/models/category_with_entry_list.dart'; 4 | import 'package:expense_manager/data/models/category_with_sum.dart'; 5 | import 'package:expense_manager/data/models/entry.dart'; 6 | import 'package:expense_manager/data/models/entry_with_category.dart'; 7 | import 'package:expense_manager/data/models/history.dart'; 8 | import 'package:tuple/tuple.dart'; 9 | 10 | abstract class EntryRepository { 11 | Stream> getMonthListByYear(EntryType entryType, int year); 12 | 13 | Stream> getYearList(EntryType entryType); 14 | 15 | Stream addEntry(EntryType entryType, Entry entry); 16 | 17 | Stream updateEntry(EntryType entryType, Entry entry); 18 | 19 | Stream deleteEntry(EntryType entryType, int id); 20 | 21 | Stream> getAllEntryWithCategory( 22 | DateTime start, DateTime end); 23 | Stream getExpanseSumByDateRange(DateTime start, DateTime end); 24 | Stream getIncomeSumByDateRange(DateTime start, DateTime end); 25 | Stream getTodayExpense(); 26 | 27 | Stream> getAllEntryWithCategoryDateWiseByMonthAndYear( 28 | EntryType entryType, int month, int year); 29 | 30 | Stream addCategory(EntryType entryType, Category category); 31 | 32 | Stream updateCategory(EntryType entryType, Category category); 33 | 34 | Stream deleteCategory(EntryType entryType, int id); 35 | 36 | Stream reorderCategory(int oldIndex, int newIndex); 37 | 38 | Stream> getAllCategory(EntryType entryType); 39 | 40 | Stream> getCategoryDetails( 41 | Tuple2 filterType); 42 | 43 | Stream, List>>> 44 | getAllEntryWithCategoryByYear(int year, int currentMonth); 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/ios-deployment.yml: -------------------------------------------------------------------------------- 1 | name: Appstore Deployment 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | deploy_ios: 10 | name: Deploy build to TestFlight 11 | runs-on: macOS-latest 12 | steps: 13 | - name: Checkout code from ref 14 | uses: actions/checkout@v2 15 | with: 16 | ref: ${{ github.ref }} 17 | - name: Run Flutter tasks 18 | uses: subosito/flutter-action@v1 19 | with: 20 | flutter-version: '2.5.3' 21 | - run: flutter clean 22 | - run: flutter pub get 23 | - run: flutter build ios --release --no-codesign --no-tree-shake-icons 24 | - name: Deploy iOS Beta to TestFlight via Fastlane 25 | uses: maierj/fastlane-action@v1.4.0 26 | with: 27 | lane: closed_beta 28 | subdirectory: ios 29 | env: 30 | APP_STORE_CONNECT_TEAM_ID: '${{ secrets.APP_STORE_CONNECT_TEAM_ID }}' 31 | DEVELOPER_APP_ID: '${{ secrets.DEVELOPER_APP_ID }}' 32 | DEVELOPER_APP_IDENTIFIER: '${{ secrets.DEVELOPER_APP_IDENTIFIER }}' 33 | DEVELOPER_PORTAL_TEAM_ID: '${{ secrets.DEVELOPER_PORTAL_TEAM_ID }}' 34 | FASTLANE_APPLE_ID: '${{ secrets.FASTLANE_APPLE_ID }}' 35 | FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: '${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}' 36 | MATCH_PASSWORD: '${{ secrets.MATCH_PASSWORD }}' 37 | GIT_AUTHORIZATION: '${{ secrets.GIT_AUTHORIZATION }}' 38 | PROVISIONING_PROFILE_SPECIFIER: '${{ secrets.PROVISIONING_PROFILE_SPECIFIER }}' 39 | TEMP_KEYCHAIN_PASSWORD: '${{ secrets.TEMP_KEYCHAIN_PASSWORD }}' 40 | TEMP_KEYCHAIN_USER: '${{ secrets.TEMP_KEYCHAIN_USER }}' 41 | - name: Fleep Updates 42 | if: always() 43 | uses: dawidd6/action-send-mail@v2 44 | with: 45 | server_address: smtp.gmail.com 46 | server_port: 465 47 | username: ${{secrets.MAIL_USERNAME}} 48 | password: ${{secrets.MAIL_PASSWORD}} 49 | subject: Github Actions Job Results 50 | body: Deployment - iOS (AppStore) for job ${{github.job}} has ${{job.status}} ! 51 | to: conv.0asu9evzgxz6tn@fleep.io 52 | from: Nividata Apps -------------------------------------------------------------------------------- /lib/ui/dialog/currency_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/core/app_localization.dart'; 2 | import 'package:expense_manager/core/color_scheme.dart'; 3 | import 'package:expense_manager/core/constants.dart'; 4 | import 'package:expense_manager/ui/app/app_state.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 | import 'package:tuple/tuple.dart'; 8 | import 'package:intl/intl.dart'; 9 | 10 | class CurrencyDialog extends AlertDialog { 11 | @override 12 | Widget build(BuildContext context) { 13 | return Column( 14 | mainAxisSize: MainAxisSize.min, 15 | children: [ 16 | SizedBox(height: 24), 17 | Text( 18 | AppLocalization.of(context).getTranslatedVal("currency"), 19 | style: Theme.of(context).textTheme.titleMedium, 20 | ), 21 | SizedBox(height: 8), 22 | Divider(color: Theme.of(context).colorScheme.crossLightColor), 23 | Consumer( 24 | builder: (context, ref, child) { 25 | Tuple2 selected = 26 | ref.watch(appStateNotifier.notifier).currency; 27 | return Column( 28 | mainAxisSize: MainAxisSize.min, 29 | children: AppConstants.currencyList 30 | .map((e) => RadioListTile( 31 | groupValue: selected, 32 | value: e, 33 | onChanged: (val) { 34 | ref 35 | .watch(appStateNotifier.notifier) 36 | .changeCurrency(currency: e); 37 | Navigator.of(context).pop(); 38 | }, 39 | title: Text( 40 | "${NumberFormat.simpleCurrency(locale: e.item1).currencySymbol} ${e.item2}", 41 | style: Theme.of(context) 42 | .textTheme 43 | .titleSmall! 44 | .copyWith(fontSize: 14), 45 | ), 46 | )) 47 | .toList(), 48 | ); 49 | }, 50 | ), 51 | SizedBox(height: 8), 52 | ], 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/ui/category_list/category_list_state.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:expense_manager/core/constants.dart'; 4 | import 'package:expense_manager/data/models/category.dart' as cat; 5 | import 'package:expense_manager/data/repository/entry_repository_imp.dart'; 6 | import 'package:flutter/foundation.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 9 | 10 | final categoryListModelProvider = 11 | ChangeNotifierProvider.autoDispose.family( 12 | (ref, entryType) => CategoryListViewModel( 13 | entryDataSourceImp: ref.read(repositoryProvider), entryType: entryType), 14 | ); 15 | 16 | class CategoryListViewModel with ChangeNotifier { 17 | final EntryRepositoryImp entryDataSourceImp; 18 | List expenseCategoryList = []; 19 | List incomeCategoryList = []; 20 | late final StreamSubscription _expenseSubscription; 21 | late final StreamSubscription _incomeSubscription; 22 | EntryType? entryType; 23 | 24 | CategoryListViewModel({ 25 | required this.entryDataSourceImp, 26 | this.entryType, 27 | }) { 28 | _expenseSubscription = 29 | entryDataSourceImp.getAllCategory(EntryType.expense).listen((event) { 30 | expenseCategoryList = event; 31 | notifyListeners(); 32 | }); 33 | _incomeSubscription = 34 | entryDataSourceImp.getAllCategory(EntryType.income).listen((event) { 35 | incomeCategoryList = event; 36 | notifyListeners(); 37 | }); 38 | } 39 | 40 | void entryTypeChange(EntryType entryType) { 41 | this.entryType = entryType; 42 | notifyListeners(); 43 | } 44 | 45 | void reorder(int oldIndex, int newIndex) { 46 | if (newIndex > oldIndex) { 47 | newIndex -= 1; 48 | } 49 | if (oldIndex != newIndex) { 50 | var x = expenseCategoryList[oldIndex]; 51 | expenseCategoryList.removeAt(oldIndex); 52 | expenseCategoryList.insert(newIndex, x); 53 | notifyListeners(); 54 | entryDataSourceImp 55 | .reorderCategory(oldIndex + 1, newIndex + 1) 56 | .listen((event) {}); 57 | } 58 | } 59 | 60 | @override 61 | void dispose() { 62 | expenseCategoryList = []; 63 | incomeCategoryList = []; 64 | _expenseSubscription.cancel(); 65 | _incomeSubscription.cancel(); 66 | super.dispose(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/ui/dashboard/chart_painter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class PieChartPainter extends CustomPainter { 6 | List _paintList = []; 7 | late List _subParts; 8 | double _total = 0; 9 | double _totalAngle = math.pi * 2; 10 | 11 | final double initialAngle; 12 | final double strokeWidth; 13 | final Color emptyColor; 14 | 15 | double _prevAngle = 0; 16 | 17 | PieChartPainter( 18 | double angleFactor, 19 | List colorList, { 20 | required List values, 21 | required this.initialAngle, 22 | required this.strokeWidth, 23 | required this.emptyColor, 24 | }) { 25 | _total = values.fold(0, (v1, v2) => v1 + v2); 26 | for (int i = 0; i < values.length; i++) { 27 | final paint = Paint()..color = colorList[i]; 28 | paint.style = PaintingStyle.stroke; 29 | paint.strokeWidth = strokeWidth; 30 | paint.strokeCap = StrokeCap.round; 31 | _paintList.add(paint); 32 | } 33 | _subParts = values; 34 | } 35 | 36 | @override 37 | void paint(Canvas canvas, Size size) { 38 | final side = size.width < size.height ? size.width : size.height; 39 | if (_total == 0) { 40 | final paint = Paint()..color = emptyColor; 41 | paint.style = PaintingStyle.stroke; 42 | paint.strokeWidth = strokeWidth; 43 | canvas.drawArc( 44 | new Rect.fromLTWH(0.0, 0.0, side, size.height), 45 | _prevAngle, 46 | 360, 47 | false, 48 | paint, 49 | ); 50 | } else if (_subParts.length == 1) { 51 | canvas.drawArc( 52 | new Rect.fromLTWH(0.0, 0.0, side, size.height), 53 | _prevAngle, 54 | 360, 55 | false, 56 | _paintList[0], 57 | ); 58 | } else { 59 | _prevAngle = this.initialAngle * math.pi / 180; 60 | for (int i = 0; i < _subParts.length; i++) { 61 | canvas.drawArc( 62 | new Rect.fromLTWH(0.0, 0.0, side, size.height), 63 | _prevAngle, 64 | (((_totalAngle) / _total) * _subParts[i] - 0.4), 65 | false, 66 | _paintList[i], 67 | ); 68 | _prevAngle = _prevAngle + (((_totalAngle) / _total) * _subParts[i]); 69 | } 70 | } 71 | } 72 | 73 | @override 74 | bool shouldRepaint(PieChartPainter oldDelegate) => 75 | oldDelegate._totalAngle != _totalAngle; 76 | } 77 | -------------------------------------------------------------------------------- /lib/ui/history/history_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/core/constants.dart'; 2 | import 'package:expense_manager/data/models/history.dart'; 3 | import 'package:expense_manager/data/repository/entry_repository_imp.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | 6 | final entryTypeProvider = StateProvider((ref) => EntryType.all); 7 | 8 | final yearListProvider = StreamProvider>((ref) { 9 | EntryType entryType = ref.watch(entryTypeProvider.notifier).state; 10 | return ref.read(repositoryProvider).getYearList(entryType).map((event) { 11 | if (event.isNotEmpty) { 12 | if (event.contains(DateTime.now().year)) { 13 | ref.read(yearProvider.notifier).state = DateTime.now().year; 14 | } else { 15 | ref.read(yearProvider.notifier).state = event.first; 16 | } 17 | } 18 | return event; 19 | }); 20 | }); 21 | 22 | final yearProvider = StateProvider((ref) => DateTime.now().year); 23 | 24 | final monthListProvider = StreamProvider>((ref) { 25 | int year = ref.watch(yearProvider.notifier).state; 26 | EntryType entryType = ref.watch(entryTypeProvider.notifier).state; 27 | return ref 28 | .read(repositoryProvider) 29 | .getMonthListByYear(entryType, year) 30 | .map((event) { 31 | if (year == DateTime.now().year) { 32 | if (event.isNotEmpty) { 33 | if (event.contains(DateTime.now().month)) { 34 | ref.read(monthProvider.notifier).state = 35 | AppConstants.monthList[DateTime.now().month]!; 36 | } else { 37 | ref.read(monthProvider.notifier).state = event.first; 38 | } 39 | } 40 | } else { 41 | ref.read(monthProvider.notifier).state = event.first; 42 | } 43 | return event; 44 | }); 45 | }); 46 | 47 | final monthProvider = StateProvider( 48 | (ref) => AppConstants.monthList[DateTime.now().month]!); 49 | 50 | final historyListProvider = StreamProvider>((ref) { 51 | String month = ref.watch(monthProvider); 52 | int year = ref.watch(yearProvider); 53 | EntryType entryType = ref.watch(entryTypeProvider); 54 | return ref 55 | .read(repositoryProvider) 56 | .getAllEntryWithCategoryDateWiseByMonthAndYear( 57 | entryType, 58 | AppConstants.monthList.keys.firstWhere( 59 | (element) => AppConstants.monthList[element] == month), 60 | year); 61 | }); 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /assets/language/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome_to": "Welcome to", 3 | "expense_manager": "Expense Manager", 4 | "enter_your_name": "Enter your name", 5 | "pls_enter_user_name": "Pls enter name", 6 | "user_name_allowed_from_3_to_20_characters": "Name allowed from 3 to 10 characters", 7 | "next": "Next", 8 | "dashboard": "Dashboard", 9 | "history": "History", 10 | "no_entry_added": "No Entry added", 11 | "no_expense_yet": "No expense yet", 12 | "no_expense_yet_2": "After your first expense\nyou will be able to view it here", 13 | "hello": "Hello, ", 14 | "today_expanse": "TODAY'S EXPENSE", 15 | "last_month": "Monthly", 16 | "quick_add": "Quick Add", 17 | "manage_category": "Manage Category", 18 | "add_expense": "Add Expense", 19 | "save": "Save", 20 | "category": "Category", 21 | "edit": "Edit", 22 | "date": "Date", 23 | "time": "Time", 24 | "note": "Note", 25 | "enter_note_here": "Enter note here", 26 | "pls_enter_amount": "Pls enter amount", 27 | "amount_should_grater_then_zero": "Amount should grater then zero", 28 | "category_list": "Category list", 29 | "add_new": "Add New", 30 | "new_category": "New Category", 31 | "tea": "Tea", 32 | "icon": "Icon", 33 | "color": "Color", 34 | "pls_enter_category_name": "Pls enter category name", 35 | "category_name_allowed_from_3_to_20_characters": "Category name allowed from 3 to 20 characters", 36 | "total_expense": "Total Expense", 37 | "this_month": "This Month", 38 | "settings": "Settings", 39 | "appearance": "Appearance", 40 | "choose_your_light_or_dark_theme_preference": "Choose your light or dark theme preference", 41 | "use_device_theme": "Use Device Theme", 42 | "light_theme": "Light Theme", 43 | "dark_theme": "Dark Theme", 44 | "cancel": "Cancel", 45 | "month_cycle_date": "Month Start Date", 46 | "language": "Language", 47 | "currency": "Currency", 48 | "expense_manager_by_nividata": "Expense Manager by Nividata", 49 | "app_version": "App version:", 50 | "jan": "Jan", 51 | "feb": "Feb", 52 | "mar": "Mar", 53 | "apr": "Apr", 54 | "may": "May", 55 | "jun": "Jun", 56 | "jul": "Jul", 57 | "aug": "Aug", 58 | "sep": "Sep", 59 | "oct": "Oct", 60 | "nov": "Nov", 61 | "dec": "Dec", 62 | "recent_expanse": "Recent Expense", 63 | "yesterday": "Yesterday", 64 | 65 | "type": "Type", 66 | "expense": "Expense", 67 | "income": "Income", 68 | "all": "All", 69 | "add_income": "Add Income", 70 | "no_entry": "No entry for this month", 71 | "statistics": "Statistics", 72 | "expense_meter" : "Monthly Expense Meter" 73 | } -------------------------------------------------------------------------------- /lib/ui/dialog/language_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/core/color_scheme.dart'; 2 | import 'package:expense_manager/data/datasource/language_data.dart'; 3 | import 'package:expense_manager/core/app_localization.dart'; 4 | import 'package:expense_manager/ui/app/app_state.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 | 8 | class LanguageDialog extends AlertDialog { 9 | @override 10 | Widget build(BuildContext context) { 11 | return Column( 12 | mainAxisSize: MainAxisSize.min, 13 | children: [ 14 | SizedBox(height: 24), 15 | Text( 16 | AppLocalization.of(context).getTranslatedVal("language"), 17 | style: Theme.of(context).textTheme.titleMedium, 18 | ), 19 | SizedBox(height: 8), 20 | Divider(color: Theme.of(context).colorScheme.crossLightColor), 21 | Consumer( 22 | builder: (context, ref, child) { 23 | Locale selected = 24 | ref.watch(appStateNotifier.notifier).currentLocale; 25 | return Column( 26 | mainAxisSize: MainAxisSize.min, 27 | children: Language.languageList() 28 | .map((e) => RadioListTile( 29 | groupValue: selected, 30 | value: e.locale, 31 | onChanged: (val) { 32 | ref 33 | .watch(appStateNotifier.notifier) 34 | .changeLocale(switchToLocale: e.locale); 35 | Navigator.of(context).pop(); 36 | }, 37 | title: Row( 38 | children: [ 39 | Text( 40 | e.flag, 41 | style: Theme.of(context).textTheme.titleMedium, 42 | ), 43 | SizedBox(width: 4), 44 | Text( 45 | e.name, 46 | style: Theme.of(context) 47 | .textTheme 48 | .titleSmall! 49 | .copyWith(fontSize: 14), 50 | ), 51 | ], 52 | ), 53 | )) 54 | .toList(), 55 | ); 56 | }, 57 | ), 58 | SizedBox(height: 8), 59 | ], 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/ui/dialog/theme_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/core/app_localization.dart'; 2 | import 'package:expense_manager/ui/app/app_state.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:tuple/tuple.dart'; 5 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 | import 'package:expense_manager/core/color_scheme.dart'; 7 | 8 | class ThemeDialog extends StatelessWidget { 9 | @override 10 | Widget build(BuildContext context) { 11 | return Column( 12 | mainAxisSize: MainAxisSize.min, 13 | children: [ 14 | SizedBox(height: 24), 15 | Text( 16 | AppLocalization.of(context).getTranslatedVal("appearance"), 17 | style: Theme.of(context).textTheme.titleMedium, 18 | ), 19 | SizedBox(height: 8), 20 | Divider(color: Theme.of(context).colorScheme.crossLightColor), 21 | Consumer( 22 | builder: (context, ref, child) { 23 | ThemeMode selected = ref.watch(appStateNotifier.notifier).themeMode; 24 | return Column( 25 | mainAxisSize: MainAxisSize.min, 26 | children: [ 27 | Tuple2( 28 | ThemeMode.system, 29 | AppLocalization.of(context) 30 | .getTranslatedVal("use_device_theme")), 31 | Tuple2( 32 | ThemeMode.light, 33 | AppLocalization.of(context) 34 | .getTranslatedVal("light_theme")), 35 | Tuple2(ThemeMode.dark, 36 | AppLocalization.of(context).getTranslatedVal("dark_theme")) 37 | ] 38 | .map((e) => RadioListTile( 39 | groupValue: selected, 40 | value: e.item1, 41 | onChanged: (val) { 42 | ref 43 | .watch(appStateNotifier.notifier) 44 | .changeTheme(e.item1); 45 | Navigator.of(context).pop(); 46 | }, 47 | title: Text( 48 | e.item2, 49 | style: Theme.of(context) 50 | .textTheme 51 | .titleSmall! 52 | .copyWith(fontSize: 14), 53 | ), 54 | )) 55 | .toList(), 56 | ); 57 | }, 58 | ), 59 | SizedBox(height: 8), 60 | ], 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/ui/dashboard/category_list_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/core/routes.dart'; 2 | import 'package:expense_manager/data/models/category.dart'; 3 | import 'package:expense_manager/ui/dashboard/dashboard_state.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 | import 'package:tuple/tuple.dart'; 7 | 8 | final _currentCategory = 9 | Provider((ref) => throw UnimplementedError()); 10 | 11 | class CategoryListView extends ConsumerWidget { 12 | const CategoryListView({ 13 | Key? key, 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context, WidgetRef ref) { 18 | return GridView( 19 | padding: const EdgeInsets.symmetric(horizontal: 24), 20 | shrinkWrap: true, 21 | gridDelegate: 22 | SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4), 23 | physics: NeverScrollableScrollPhysics(), 24 | children: [ 25 | ...ref 26 | .watch(categoryListProvider) 27 | .list 28 | .map((category) => ProviderScope( 29 | overrides: [_currentCategory.overrideWithValue(category)], 30 | child: const CategoryItem())) 31 | .toList() 32 | ], 33 | ); 34 | } 35 | } 36 | 37 | class CategoryItem extends ConsumerWidget { 38 | const CategoryItem({Key? key}) : super(key: key); 39 | 40 | @override 41 | Widget build(BuildContext context, WidgetRef ref) { 42 | final category = ref.watch(_currentCategory); 43 | return InkWell( 44 | onTap: () { 45 | Navigator.pushNamed(context, AppRoutes.addEntry, 46 | arguments: Tuple3(category.entryType, null, category)); 47 | }, 48 | borderRadius: BorderRadius.circular(6), 49 | child: Card( 50 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), 51 | child: Column( 52 | mainAxisAlignment: MainAxisAlignment.center, 53 | children: [ 54 | Icon( 55 | category.icon, 56 | color: category.iconColor, 57 | size: 20, 58 | ), 59 | Padding( 60 | padding: 61 | const EdgeInsets.only(left: 4, right: 4, bottom: 4, top: 8), 62 | child: Text( 63 | category.name, 64 | style: Theme.of(context) 65 | .textTheme 66 | .bodySmall! 67 | .copyWith(overflow: TextOverflow.ellipsis), 68 | ), 69 | ), 70 | ], 71 | ), 72 | ), 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/ui/history/history.dart: -------------------------------------------------------------------------------- 1 | import 'package:dotted_border/dotted_border.dart'; 2 | import 'package:expense_manager/core/app_localization.dart'; 3 | import 'package:expense_manager/ui/dialog/common_alert_dialog.dart'; 4 | import 'package:expense_manager/ui/dialog/history_filter_dialog.dart'; 5 | import 'package:expense_manager/ui/history/history_list.dart'; 6 | import 'package:expense_manager/ui/history/month_list.dart'; 7 | import 'package:flutter/material.dart'; 8 | 9 | class History extends StatelessWidget { 10 | @override 11 | Widget build(BuildContext context) { 12 | return Scaffold( 13 | appBar: AppBar( 14 | title: Padding( 15 | padding: const EdgeInsets.only(left: 24), 16 | child: DottedBorder( 17 | color: Theme.of(context).appBarTheme.titleTextStyle!.color!, 18 | dashPattern: [5, 5], 19 | radius: Radius.circular(12), 20 | borderType: BorderType.RRect, 21 | child: Padding( 22 | padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 23 | child: Text( 24 | AppLocalization.of(context).getTranslatedVal("history"), 25 | style: Theme.of(context).appBarTheme.titleTextStyle, 26 | ), 27 | ), 28 | ), 29 | ), 30 | actions: [ 31 | InkWell( 32 | onTap: () { 33 | showGeneralDialog( 34 | context: context, 35 | barrierDismissible: true, 36 | barrierLabel: "history", 37 | transitionDuration: Duration(milliseconds: 200), 38 | pageBuilder: (context, animation, secondaryAnimation) => 39 | CommonAlertDialog(child: HistoryFilterDialog()), 40 | transitionBuilder: 41 | (context, animation, secondaryAnimation, child) => 42 | Transform.scale( 43 | scale: animation.value, 44 | alignment: Alignment(0.83, -0.83), 45 | child: child, 46 | )); 47 | }, 48 | child: Padding( 49 | padding: const EdgeInsets.all(24), 50 | child: Icon( 51 | Icons.filter_list_rounded, 52 | ), 53 | ), 54 | ) 55 | ], 56 | ), 57 | body: Column( 58 | crossAxisAlignment: CrossAxisAlignment.start, 59 | children: [ 60 | SizedBox(height: 8), 61 | MonthList(), 62 | HistoryList(), 63 | ], 64 | ), 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/ui/history/month_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/core/app_localization.dart'; 2 | import 'package:expense_manager/ui/history/history_view_model.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | 6 | class MonthList extends ConsumerWidget { 7 | @override 8 | Widget build(BuildContext context, WidgetRef ref) { 9 | final vm = ref.watch(monthListProvider); 10 | return SingleChildScrollView( 11 | child: SizedBox( 12 | height: 48, 13 | child: vm.when( 14 | data: (monthList) => ListView( 15 | shrinkWrap: false, 16 | padding: EdgeInsets.only(left: 24), 17 | scrollDirection: Axis.horizontal, 18 | children: monthList 19 | .map( 20 | (e) => Padding( 21 | padding: 22 | const EdgeInsets.symmetric(vertical: 8, horizontal: 4), 23 | child: InkWell( 24 | onTap: () { 25 | ref.read(monthProvider.notifier).state = e; 26 | }, 27 | borderRadius: BorderRadius.circular(15), 28 | child: Container( 29 | padding: const EdgeInsets.symmetric( 30 | vertical: 9, horizontal: 14), 31 | decoration: BoxDecoration( 32 | color: ref.watch(monthProvider) == e 33 | ? Color(0xff2196F3) 34 | : Theme.of(context).dividerColor, 35 | borderRadius: BorderRadius.circular(15), 36 | ), 37 | child: Text( 38 | AppLocalization.of(context).getTranslatedVal(e), 39 | style: Theme.of(context) 40 | .textTheme 41 | .titleSmall! 42 | .copyWith( 43 | fontSize: 12, 44 | color: 45 | ref.watch(monthProvider) == e 46 | ? Colors.white 47 | : Color(0xff2196F3), 48 | ), 49 | ), 50 | ), 51 | ), 52 | ), 53 | ) 54 | .toList(), 55 | ), 56 | loading: () => SizedBox(), 57 | error: (e, str) => Text(e.toString()), 58 | ), 59 | ), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/data/datasource/entry_dataSource.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/data/models/category.dart'; 2 | import 'package:expense_manager/data/models/category_with_entry_list.dart'; 3 | import 'package:expense_manager/data/models/category_with_sum.dart'; 4 | import 'package:expense_manager/data/models/entry.dart'; 5 | import 'package:expense_manager/data/models/entry_with_category.dart'; 6 | import 'package:expense_manager/data/models/history.dart'; 7 | 8 | abstract class EntryDataSource { 9 | Stream> getExpenseMonthListByYear(int year); 10 | 11 | Stream> getIncomeMonthListByYear(int year); 12 | 13 | Stream> getAllMonthListByYear(int year); 14 | 15 | Stream> getExpenseYearList(); 16 | 17 | Stream> getIncomeYearList(); 18 | 19 | Stream> getAllYearList(); 20 | 21 | Stream addExpenseEntry(Entry entry); 22 | 23 | Stream addIncomeEntry(Entry entry); 24 | 25 | Stream updateExpenseEntry(Entry entry); 26 | 27 | Stream updateIncomeEntry(Entry entry); 28 | 29 | Stream deleteExpenseEntry(int id); 30 | 31 | Stream deleteIncomeEntry(int id); 32 | 33 | Stream> getAllEntryWithCategory( 34 | DateTime start, DateTime end); 35 | Stream getExpanseSumByDateRange(DateTime start, DateTime end); 36 | Stream getIncomeSumByDateRange(DateTime start, DateTime end); 37 | Stream getTodayExpense(); 38 | 39 | Stream> getExpenseEntryWithCategoryDateWiseByMonthAndYear( 40 | int month, int year); 41 | 42 | Stream> getIncomeEntryWithCategoryDateWiseByMonthAndYear( 43 | int month, int year); 44 | 45 | Stream> getAllEntryWithCategoryDateWiseByMonthAndYear( 46 | int month, int year); 47 | 48 | Stream addExpenseCategory(Category category); 49 | 50 | Stream addIncomeCategory(Category category); 51 | 52 | Stream updateExpenseCategory(Category category); 53 | 54 | Stream updateIncomeCategory(Category category); 55 | 56 | Stream deleteExpenseCategory(int id); 57 | 58 | Stream deleteIncomeCategory(int id); 59 | 60 | Stream reorderCategory(int oldIndex, int newIndex); 61 | 62 | Stream> getAllExpenseCategory(); 63 | 64 | Stream> getAllIncomeCategory(); 65 | 66 | Stream> getAllCategory(); 67 | 68 | Stream> getAllCategoryWithSumByMonth( 69 | int month, int year); 70 | 71 | Stream> getAllCategoryWithSumByYear(int year); 72 | Stream> getAllEntryWithCategoryByYear(int year); 73 | } 74 | -------------------------------------------------------------------------------- /.github/workflows/android-deployment.yml: -------------------------------------------------------------------------------- 1 | name: Playstore deployment 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | # CI 10 | build_android: 11 | name: Building Android 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | - name: Setup Java 17 | uses: actions/setup-java@v1 18 | with: 19 | java-version: 12.x 20 | - name: Decrypt Android keys 21 | run: sh ./.github/scripts/decrypt_android_secrets.sh 22 | env: 23 | ANDROID_KEYS_SECRET_PASSPHRASE: ${{ secrets.ANDROID_KEYS_SECRET_PASSPHRASE }} 24 | - name: Setup Flutter 25 | uses: subosito/flutter-action@v1 26 | with: 27 | flutter-version: 2.5.3 28 | - name: Install Flutter dependencies 29 | run: flutter pub get 30 | # Add build runner commands here if you have any 31 | - name: Build the APK 32 | run: flutter build apk --release --no-tree-shake-icons 33 | - name: Upload artifact to Github 34 | uses: actions/upload-artifact@v1 35 | with: 36 | name: release-apk 37 | path: build/app/outputs/apk/release/app-release.apk 38 | # CD 39 | deploy_android: 40 | name: Deploying to playstore 41 | runs-on: ubuntu-latest 42 | needs: [build_android] 43 | steps: 44 | - name: Checkout code 45 | uses: actions/checkout@v2 46 | - name: Setup Java 47 | uses: actions/setup-java@v1 48 | with: 49 | java-version: 12.x 50 | - name: Decrypt Android keys 51 | run: sh ./.github/scripts/decrypt_android_secrets.sh 52 | env: 53 | ANDROID_KEYS_SECRET_PASSPHRASE: ${{ secrets.ANDROID_KEYS_SECRET_PASSPHRASE }} 54 | - name: Setup Flutter 55 | uses: subosito/flutter-action@v1 56 | with: 57 | flutter-version: 2.5.3 58 | - run: flutter pub get 59 | - run: flutter build apk --release --no-tree-shake-icons 60 | - name: Run Fastlane 61 | uses: maierj/fastlane-action@v1.4.0 62 | with: 63 | lane: closed_beta 64 | subdirectory: android 65 | - name: Fleep Updates 66 | if: always() 67 | uses: dawidd6/action-send-mail@v2 68 | with: 69 | server_address: smtp.gmail.com 70 | server_port: 465 71 | username: ${{secrets.MAIL_USERNAME}} 72 | password: ${{secrets.MAIL_PASSWORD}} 73 | subject: Github Actions Job Results 74 | body: Deployment - Android (Playstore) for job ${{github.job}} has ${{job.status}} ! 75 | to: conv.0asu9evzgxz6tn@fleep.io 76 | from: Nividata Apps -------------------------------------------------------------------------------- /assets/language/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome_to": "Welcome to", 3 | "expense_manager": "Expense Manager", 4 | "enter_your_name": "Enter your name", 5 | "pls_enter_user_name": "Pls enter name", 6 | "user_name_allowed_from_3_to_20_characters": "Name allowed from 3 to 10 characters", 7 | "next": "Next", 8 | "dashboard": "tablero", 9 | "history": "historia", 10 | "no_entry_added": "No se agregó ninguna entrada", 11 | "no_expense_yet": "Sin gastos todavía", 12 | "no_expense_yet_2": "Después de tu primer gasto\npodrás verlo aquí", 13 | "hello": "Hola, ", 14 | "today_expanse": "gasto de hoy", 15 | "last_month": "Mensual", 16 | "quick_add": "adición rápida", 17 | "manage_category": "administrar categoría", 18 | "add_expense": "agregar gastos", 19 | "save": "salvar", 20 | "category": "categoría", 21 | "edit": "editar", 22 | "date": "fecha", 23 | "time": "fecha", 24 | "note": "Nota", 25 | "enter_note_here": "ingrese nota aquí", 26 | "pls_enter_amount": "por favor ingrese la cantidad", 27 | "amount_should_grater_then_zero": "la cantidad debe ser mayor que cero", 28 | "category_list": "Lista de categoría", 29 | "add_new": "agregar nuevo", 30 | "new_category": "nueva categoría", 31 | "tea": "té", 32 | "icon": "icono", 33 | "color": "color", 34 | "pls_enter_category_name": "por favor ingrese el nombre de la categoría", 35 | "category_name_allowed_from_3_to_20_characters": "Nombre de categoría permitido de 3 a 20 caracteres", 36 | "total_expense": "gasto total", 37 | "this_month": "este mes", 38 | "settings": "ajustes", 39 | "appearance": "Apariencia", 40 | "choose_your_light_or_dark_theme_preference": "Elija su preferencia de tema claro u oscuro", 41 | "use_device_theme": "Usar tema del dispositivo", 42 | "light_theme": "Tema de luz", 43 | "dark_theme": "Tema oscuro", 44 | "cancel": "cancelar", 45 | "month_cycle_date": "Mes Fecha de inicio", 46 | "language": "Idioma", 47 | "currency": "Divisa", 48 | "expense_manager_by_nividata": "Gestora de gastos por nividata", 49 | "app_version": "Version de aplicacion:", 50 | "jan": "enero", 51 | "feb": "febrero", 52 | "mar": "marzo", 53 | "apr": "abril", 54 | "may": "mayo", 55 | "jun": "junio", 56 | "jul": "julio", 57 | "aug": "agosto", 58 | "sep": "septiembre", 59 | "oct": "octubre", 60 | "nov": "noviembre", 61 | "dec": "diciembre", 62 | "recent_expanse": "Expansión reciente", 63 | "yesterday": "Ayer", 64 | 65 | "type": "Tipo", 66 | "expense": "Gastos", 67 | "income": "Ingreso", 68 | "all": "Toda", 69 | "add_income": "Añadir ingresos", 70 | "no_entry": "No hay entradas para este mes", 71 | "statistics": "Estadísticas", 72 | "expense_meter" : "Contador de gastos mensuales" 73 | } -------------------------------------------------------------------------------- /assets/language/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome_to": "Welcome to", 3 | "expense_manager": "Expense Manager", 4 | "enter_your_name": "Enter your name", 5 | "pls_enter_user_name": "Pls enter name", 6 | "user_name_allowed_from_3_to_20_characters": "Name allowed from 3 to 10 characters", 7 | "next": "Next", 8 | "dashboard": "Painel", 9 | "history": "História", 10 | "no_entry_added": "Nenhuma entrada adicionada", 11 | "no_expense_yet": "Sem despesas ainda", 12 | "no_expense_yet_2": "Depois de sua primeira despesa\nvocê poderá vê-lo aqui", 13 | "hello": "Olá, ", 14 | "today_expanse": "EXPANSÃO DE HOJE", 15 | "last_month": "Por mês", 16 | "quick_add": "Quick Add", 17 | "manage_category": "Gerenciar categoria", 18 | "add_expense": "Adicionar Despesa", 19 | "save": "Salve", 20 | "category": "Categoria", 21 | "edit": "Editar", 22 | "date": "Encontro", 23 | "time": "Tempo", 24 | "note": "Observação", 25 | "enter_note_here": "Insira a nota aqui", 26 | "pls_enter_amount": "Por favor, insira o valor", 27 | "amount_should_grater_then_zero": "Quantidade deve ser maior que zero", 28 | "category_list": "Lista de categorias", 29 | "add_new": "Adicionar novo", 30 | "new_category": "Nova categoria", 31 | "tea": "Chá", 32 | "icon": "Ícone", 33 | "color": "Cor", 34 | "pls_enter_category_name": "Por favor, insira o nome da categoria", 35 | "category_name_allowed_from_3_to_20_characters": "Nome da categoria permitido de 3 a 20 caracteres", 36 | "total_expense": "Custo total", 37 | "this_month": "Este mês", 38 | "settings": "Settings", 39 | "appearance": "Aparência", 40 | "choose_your_light_or_dark_theme_preference": "Escolha sua preferência de tema claro ou escuro", 41 | "use_device_theme": "Use o tema do dispositivo", 42 | "light_theme": "Tema claro", 43 | "dark_theme": "Tema escuro", 44 | "cancel": "Cancelar", 45 | "month_cycle_date": "Data de início do mês", 46 | "language": "Língua", 47 | "currency": "Moeda", 48 | "expense_manager_by_nividata": "Gerente de despesas por Nividata", 49 | "app_version": "Versão do aplicativo:", 50 | "jan": "Janeiro", 51 | "feb": "fevereiro", 52 | "mar": "Março", 53 | "apr": "abril", 54 | "may": "maio", 55 | "jun": "Junho", 56 | "jul": "julho", 57 | "aug": "agosto", 58 | "sep": "setembro", 59 | "oct": "Outubro", 60 | "nov": "novembro", 61 | "dec": "dezembro", 62 | "recent_expanse": "Expansão recente", 63 | "yesterday": "Ontem", 64 | 65 | "type": "Tipo", 66 | "expense": "Despesa", 67 | "income": "Renda", 68 | "all": "Todos", 69 | "add_income": "Adicionar Rendimento", 70 | "no_entry": "Sem entrada para este mês", 71 | "statistics": "Estatisticas", 72 | "expense_meter" : "Medidor de despesas mensais" 73 | } -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | def keystoreProperties = new Properties() 25 | def keystorePropertiesFile = rootProject.file('key.properties') 26 | if (keystorePropertiesFile.exists()) { 27 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 28 | } 29 | 30 | apply plugin: 'com.android.application' 31 | apply plugin: 'kotlin-android' 32 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 33 | 34 | android { 35 | compileSdkVersion flutter.compileSdkVersion 36 | ndkVersion flutter.ndkVersion 37 | 38 | compileOptions { 39 | sourceCompatibility JavaVersion.VERSION_1_8 40 | targetCompatibility JavaVersion.VERSION_1_8 41 | } 42 | 43 | kotlinOptions { 44 | jvmTarget = '1.8' 45 | } 46 | 47 | sourceSets { 48 | main.java.srcDirs += 'src/main/kotlin' 49 | } 50 | 51 | lintOptions { 52 | disable 'InvalidPackage' 53 | } 54 | 55 | defaultConfig { 56 | applicationId "com.nividata.expense_manager" 57 | minSdkVersion 21 58 | targetSdkVersion flutter.targetSdkVersion 59 | versionCode flutterVersionCode.toInteger() 60 | versionName flutterVersionName 61 | } 62 | 63 | signingConfigs { 64 | release { 65 | keyAlias keystoreProperties['keyAlias'] 66 | keyPassword keystoreProperties['keyPassword'] 67 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 68 | storePassword keystoreProperties['storePassword'] 69 | } 70 | } 71 | 72 | buildTypes { 73 | release { 74 | signingConfig signingConfigs.release 75 | } 76 | } 77 | } 78 | 79 | flutter { 80 | source '../..' 81 | } 82 | 83 | dependencies { 84 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 85 | } 86 | 87 | apply plugin: 'com.google.gms.google-services' 88 | apply plugin: 'com.google.firebase.crashlytics' -------------------------------------------------------------------------------- /ios/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | # update_fastlane 15 | 16 | #default_platform(:ios) 17 | 18 | #platform :ios do 19 | # desc "Push a new beta build to TestFlight" 20 | # lane :beta do 21 | # build_app(workspace: "Runner.xcworkspace", scheme: "Runner") 22 | # upload_to_testflight 23 | # end 24 | #end 25 | 26 | default_platform(:ios) 27 | 28 | DEVELOPER_APP_ID = ENV["DEVELOPER_APP_ID"] 29 | DEVELOPER_APP_IDENTIFIER = ENV["DEVELOPER_APP_IDENTIFIER"] 30 | PROVISIONING_PROFILE_SPECIFIER = ENV["PROVISIONING_PROFILE_SPECIFIER"] 31 | TEMP_KEYCHAIN_USER = ENV["TEMP_KEYCHAIN_USER"] 32 | TEMP_KEYCHAIN_PASSWORD = ENV["TEMP_KEYCHAIN_PASSWORD"] 33 | 34 | def delete_temp_keychain(name) 35 | delete_keychain( 36 | name: name 37 | ) if File.exist? File.expand_path("~/Library/Keychains/#{name}-db") 38 | end 39 | 40 | def create_temp_keychain(name, password) 41 | create_keychain( 42 | name: name, 43 | password: password, 44 | unlock: false, 45 | timeout: false 46 | ) 47 | end 48 | 49 | def ensure_temp_keychain(name, password) 50 | delete_temp_keychain(name) 51 | create_temp_keychain(name, password) 52 | end 53 | 54 | platform :ios do 55 | lane :closed_beta do 56 | keychain_name = TEMP_KEYCHAIN_USER 57 | keychain_password = TEMP_KEYCHAIN_PASSWORD 58 | ensure_temp_keychain(keychain_name, keychain_password) 59 | 60 | match( 61 | type: 'appstore', 62 | app_identifier: "#{DEVELOPER_APP_IDENTIFIER}", 63 | git_basic_authorization: Base64.strict_encode64(ENV["GIT_AUTHORIZATION"]), 64 | readonly: true, 65 | keychain_name: keychain_name, 66 | keychain_password: keychain_password 67 | ) 68 | 69 | gym( 70 | configuration: "Release", 71 | workspace: "Runner.xcworkspace", 72 | scheme: "Runner", 73 | export_method: "app-store", 74 | export_options: { 75 | provisioningProfiles: { 76 | DEVELOPER_APP_ID => PROVISIONING_PROFILE_SPECIFIER 77 | } 78 | } 79 | ) 80 | 81 | pilot( 82 | apple_id: "#{DEVELOPER_APP_ID}", 83 | app_identifier: "#{DEVELOPER_APP_IDENTIFIER}", 84 | skip_waiting_for_build_processing: true, 85 | skip_submission: true, 86 | distribute_external: false, 87 | notify_external_testers: false, 88 | ipa: "./Runner.ipa" 89 | ) 90 | 91 | delete_temp_keychain(keychain_name) 92 | end 93 | end 94 | 95 | 96 | -------------------------------------------------------------------------------- /lib/ui/app/app_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/data/datasource/sharedpref/preferences.dart'; 2 | import 'package:expense_manager/data/datasource/sharedpref/shared_preference_helper.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | import 'package:package_info/package_info.dart'; 6 | import 'package:tuple/tuple.dart'; 7 | 8 | final appStateNotifier = ChangeNotifierProvider((ref) => AppThemeState(ref)); 9 | 10 | class AppThemeState extends ChangeNotifier { 11 | Ref ref; 12 | ThemeMode themeMode = ThemeMode.system; 13 | Locale currentLocale = Locale('en', 'US'); 14 | Tuple2 currency = Tuple2("en", "Dollar"); 15 | String userName = ""; 16 | String appVersion = ""; 17 | 18 | AppThemeState(this.ref) { 19 | userName = ref 20 | .read(sharedPreferencesProvider) 21 | .getString(Preferences.USER_NAME, defValue: ""); 22 | themeMode = ThemeMode.values[ref 23 | .read(sharedPreferencesProvider) 24 | .getInt(Preferences.IS_DARK_MODE, defValue: 0)]; 25 | currentLocale = ref.read(sharedPreferencesProvider).getObj( 26 | Preferences.DEFAULT_LANGUAGE, (v) => Locale(v["lc"], v["cc"]), 27 | defValue: Locale('en', 'US')); 28 | currency = ref.read(sharedPreferencesProvider).getObj( 29 | Preferences.DEFAULT_CURRENCY, (v) => Tuple2(v["item1"], v["item2"]), 30 | defValue: Tuple2("en", "Dollar")); 31 | 32 | PackageInfo.fromPlatform().then((value) { 33 | appVersion = value.version; 34 | }); 35 | } 36 | 37 | void changeUserName(String name) async { 38 | this.userName = name; 39 | notifyListeners(); 40 | await ref 41 | .read(sharedPreferencesProvider) 42 | .putString(Preferences.USER_NAME, name); 43 | } 44 | 45 | void changeTheme(ThemeMode themeMode) async { 46 | this.themeMode = themeMode; 47 | notifyListeners(); 48 | await ref 49 | .read(sharedPreferencesProvider) 50 | .putInt(Preferences.IS_DARK_MODE, themeMode.index); 51 | } 52 | 53 | void changeLocale({required Locale switchToLocale}) async { 54 | currentLocale = switchToLocale; 55 | notifyListeners(); 56 | await ref 57 | .read(sharedPreferencesProvider) 58 | .putObjectNew(Preferences.DEFAULT_LANGUAGE, () { 59 | return "{\"lc\" : \"${currentLocale.languageCode}\" , \"cc\" : \"${currentLocale.countryCode}\"}"; 60 | }); 61 | } 62 | 63 | void changeCurrency({required Tuple2 currency}) async { 64 | this.currency = currency; 65 | notifyListeners(); 66 | await ref 67 | .read(sharedPreferencesProvider) 68 | .putObjectNew(Preferences.DEFAULT_CURRENCY, () { 69 | return "{\"item1\" : \"${currency.item1}\" , \"item2\" : \"${currency.item2}\"}"; 70 | }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/core/app_localization.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/services.dart'; 7 | 8 | class AppLocalization { 9 | final Locale locale; 10 | 11 | AppLocalization(this.locale); 12 | 13 | static AppLocalization of(BuildContext context) { 14 | return Localizations.of(context, AppLocalization)!; 15 | } 16 | 17 | late Map _localizedValues; 18 | 19 | Future load() async { 20 | String languageJsonValues = await rootBundle 21 | .loadString('assets/language/${locale.languageCode}.json'); 22 | 23 | Map mappedJson = json.decode(languageJsonValues); 24 | 25 | _localizedValues = mappedJson.map((key, value) => MapEntry(key, value)); 26 | } 27 | 28 | String getTranslatedVal(String key) { 29 | return _localizedValues[key] ?? "No text"; 30 | } 31 | 32 | static const LocalizationsDelegate delegate = 33 | _AppLocalizationDelegate(); 34 | } 35 | 36 | class _AppLocalizationDelegate extends LocalizationsDelegate { 37 | const _AppLocalizationDelegate(); 38 | 39 | @override 40 | bool isSupported(Locale locale) { 41 | return ['en', 'es', 'pt'].contains(locale.languageCode); 42 | } 43 | 44 | @override 45 | Future load(Locale locale) async { 46 | AppLocalization localization = new AppLocalization(locale); 47 | await localization.load(); 48 | return localization; 49 | } 50 | 51 | @override 52 | bool shouldReload(covariant LocalizationsDelegate old) => 53 | false; 54 | } 55 | 56 | // class AppLocalizations { 57 | // static final AppLocalizations _singleton = new AppLocalizations._internal(); 58 | // 59 | // AppLocalizations._internal(); 60 | // 61 | // static AppLocalizations get instance => _singleton; 62 | // 63 | // late Map _localisedValues; 64 | // 65 | // Future load(Locale locale) async { 66 | // String jsonContent = await rootBundle 67 | // .loadString('assets/language/${locale.languageCode}.json'); 68 | // _localisedValues = json.decode(jsonContent); 69 | // return this; 70 | // } 71 | // 72 | // String getTranslatedVal(String key) { 73 | // return _localisedValues[key] ?? "$key not found"; 74 | // } 75 | // } 76 | // 77 | // class AppLocalizationsDelegate extends LocalizationsDelegate { 78 | // const AppLocalizationsDelegate(); 79 | // 80 | // @override 81 | // bool isSupported(Locale locale) => 82 | // ['en', 'es', 'pt'].contains(locale.languageCode); 83 | // 84 | // @override 85 | // Future load(Locale locale) { 86 | // return AppLocalizations.instance.load(locale); 87 | // } 88 | // 89 | // @override 90 | // bool shouldReload(AppLocalizationsDelegate old) => true; 91 | // } 92 | -------------------------------------------------------------------------------- /lib/ui/history/year_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/ui/history/history_view_model.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | 5 | class YearList extends ConsumerWidget { 6 | @override 7 | Widget build(BuildContext context, WidgetRef ref) { 8 | final vm = ref.watch(yearListProvider); 9 | 10 | return vm.when( 11 | data: (yearList) => yearList.isEmpty 12 | ? SizedBox() 13 | : SizedBox( 14 | height: 48, 15 | child: ListView( 16 | shrinkWrap: false, 17 | padding: EdgeInsets.only(left: 24), 18 | scrollDirection: Axis.horizontal, 19 | children: yearList 20 | .map( 21 | (e) => Padding( 22 | padding: const EdgeInsets.symmetric( 23 | vertical: 8, horizontal: 4), 24 | child: InkWell( 25 | onTap: () { 26 | ref.read(yearProvider.notifier).state = e; 27 | }, 28 | borderRadius: BorderRadius.circular(15), 29 | child: Container( 30 | padding: const EdgeInsets.symmetric( 31 | vertical: 9, horizontal: 14), 32 | decoration: BoxDecoration( 33 | color: ref.watch(yearProvider.notifier).state == e 34 | ? Color(0xff2196F3) 35 | : Theme.of(context).dividerColor, 36 | borderRadius: BorderRadius.circular(15), 37 | ), 38 | child: Text( 39 | e.toString(), 40 | style: Theme.of(context) 41 | .textTheme 42 | .titleSmall! 43 | .copyWith( 44 | fontSize: 12, 45 | color: ref 46 | .watch(yearProvider.notifier) 47 | .state == 48 | e 49 | ? Colors.white 50 | : Color(0xff2196F3), 51 | ), 52 | ), 53 | ), 54 | ), 55 | ), 56 | ) 57 | .toList(), 58 | ), 59 | ), 60 | loading: () => SizedBox(height: 48, child: CircularProgressIndicator()), 61 | error: (e, str) => Center(child: Text(e.toString())), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Expense Manager 3 |

4 | 5 | Expense Manager makes it easy to manage your personal finances. With this app you can record your personal and business financial transactions, generate spending reports, review your daily, weekly and monthly financial data. 6 | 7 |

8 | Expense Manager 9 | Expense Manager 10 |

11 | 12 |

13 | 14 | 15 | 16 |

17 | 18 |

19 | 20 | 21 |

22 | 23 | ## Expense Manager - Money Tracker 24 | 25 | You can instantly see your expense by category and how it changes between each month, based on the data you've entered. Dashboard will show you your expense represented into line chart and pie charts on monthly bases. 26 | 27 | # Feature Highlights 28 | - Simple Design 29 | - Ad-Free 30 | - Expense Recording 31 | - Attach Categories 32 | - Delete modify expense 33 | - Create categories 34 | - Dashboard for overview 35 | - Expense History 36 | - Expense Grouping with monthly & yearly filter 37 | 38 | # Customization 39 | - User can customize categories and their icons or colors 40 | - User can add custom category 41 | - Option for Dark Theme and Light Theme 42 | - Custom day selection for monthly cycle 43 | - Multiple Language selection 44 | 45 | # Languages 46 | - English 47 | - Spanish 48 | - Portuguese 49 | 50 | # Dependencies 51 | - moor_flutter 52 | - flutter_riverpod 53 | - fl_chart 54 | - shared_preferences 55 | - rxdart 56 | - firebase_core 57 | - smooth_page_indicator 58 | 59 | # Contribution 60 | This is just a basic version of expense manager that we've developed. We were basically exploring riverpod and intetintentionally made this app for a showcase in our portfolio. There are limitless features that we can add in expense manager, so if you're just like us exploring various libraries of flutter then feel free to fork and create pull request for new features. 61 | 62 | # License 63 | This code is distributed under the terms and conditions of the [MIT](LICENSE). 64 | -------------------------------------------------------------------------------- /lib/data/models/entry_with_category.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/core/constants.dart'; 2 | import 'package:expense_manager/data/datasource/local/moor/app_database.dart'; 3 | import 'package:expense_manager/data/models/category.dart'; 4 | import 'package:expense_manager/data/models/entry.dart'; 5 | 6 | class EntryWithCategory { 7 | final Entry entry; 8 | final Category category; 9 | final EntryType entryType; 10 | 11 | EntryWithCategory({ 12 | required this.entry, 13 | required this.category, 14 | required this.entryType, 15 | }); 16 | 17 | factory EntryWithCategory.fromExpenseEntryWithCategoryEntity( 18 | EntryWithCategoryExpenseData entityData) { 19 | return EntryWithCategory( 20 | entry: Entry.fromEntryEntity(entityData.entry!), 21 | category: Category.fromExpenseCategoryEntity(entityData.category), 22 | entryType: EntryType.expense, 23 | ); 24 | } 25 | 26 | factory EntryWithCategory.fromIncomeEntryWithCategoryEntity( 27 | EntryWithCategoryIncomeData entityData) { 28 | return EntryWithCategory( 29 | entry: Entry.fromIncomeEntryEntity(entityData.entry!), 30 | category: Category.fromIncomeCategoryEntity(entityData.category), 31 | entryType: EntryType.income, 32 | ); 33 | } 34 | 35 | factory EntryWithCategory.fromAllEntryWithCategoryEntity( 36 | EntryWithCategoryAllData entityData, int entryType) { 37 | return EntryWithCategory( 38 | entry: Entry.fromEntryEntity(entityData.entry), 39 | category: Category.fromExpenseCategoryEntity(entityData.category), 40 | entryType: EntryType.values[entryType], 41 | ); 42 | } 43 | 44 | @override 45 | String toString() { 46 | return 'EntryWithCategory{entryType: $entryType, entry: ${entry.toString()}, category: ${category.toString()}'; 47 | } 48 | } 49 | 50 | class EntryWithCategoryAllData { 51 | final EntryEntityData entry; 52 | final CategoryEntityData? category; 53 | final int entryType; 54 | 55 | EntryWithCategoryAllData({ 56 | required this.entry, 57 | required this.category, 58 | required this.entryType, 59 | }); 60 | 61 | @override 62 | String toString() { 63 | return 'EntryWithCategoryAllData{entry: ${entry.toString()}, category: ${category.toString()}, entryType: $entryType'; 64 | } 65 | } 66 | 67 | class EntryWithCategoryExpenseData { 68 | final EntryEntityData? entry; 69 | final CategoryEntityData? category; 70 | 71 | EntryWithCategoryExpenseData({required this.entry, required this.category}); 72 | 73 | @override 74 | String toString() { 75 | return 'EntryWithCategoryExpenseData{entry: ${entry.toString()}, category: ${category.toString()}'; 76 | } 77 | } 78 | 79 | class EntryWithCategoryIncomeData { 80 | final IncomeEntryEntityData? entry; 81 | final IncomeCategoryEntityData? category; 82 | 83 | EntryWithCategoryIncomeData({required this.entry, required this.category}); 84 | 85 | @override 86 | String toString() { 87 | return 'EntryWithCategoryIncomeData{entry: ${entry.toString()}, category: ${category.toString()}'; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/data/models/entry.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/data/datasource/local/moor/app_database.dart'; 2 | import 'package:meta/meta.dart'; 3 | import 'package:drift/drift.dart'; 4 | 5 | @immutable 6 | class Entry { 7 | final double amount; 8 | final int? id; 9 | final int? categoryId; 10 | final DateTime modifiedDate; 11 | final String description; 12 | 13 | Entry({ 14 | this.id, 15 | required this.amount, 16 | this.categoryId, 17 | required this.modifiedDate, 18 | required this.description, 19 | }); 20 | 21 | Entry copyWith({ 22 | double? amount, 23 | int? categoryId, 24 | DateTime? modifiedDate, 25 | String? description, 26 | }) { 27 | return Entry( 28 | amount: amount ?? this.amount, 29 | categoryId: categoryId ?? this.categoryId, 30 | modifiedDate: modifiedDate ?? this.modifiedDate, 31 | description: description ?? this.description); 32 | } 33 | 34 | factory Entry.fromEntryEntity(EntryEntityData entityData) { 35 | return Entry( 36 | id: entityData.id, 37 | amount: entityData.amount, 38 | categoryId: entityData.categoryId, 39 | modifiedDate: entityData.modifiedDate, 40 | description: entityData.description); 41 | } 42 | 43 | factory Entry.fromIncomeEntryEntity(IncomeEntryEntityData incomeEntityData) { 44 | return Entry( 45 | id: incomeEntityData.id, 46 | amount: incomeEntityData.amount, 47 | categoryId: incomeEntityData.categoryId, 48 | modifiedDate: incomeEntityData.modifiedDate, 49 | description: incomeEntityData.description); 50 | } 51 | 52 | EntryEntityCompanion toEntryEntityCompanion() { 53 | return EntryEntityCompanion( 54 | id: id == null ? Value.absent() : Value(id!), 55 | amount: Value(amount), 56 | categoryId: categoryId == null ? Value.absent() : Value(categoryId!), 57 | modifiedDate: Value(modifiedDate), 58 | description: Value(description)); 59 | } 60 | 61 | IncomeEntryEntityCompanion toIncomeEntryEntityCompanion() { 62 | return IncomeEntryEntityCompanion( 63 | id: id == null ? Value.absent() : Value(id!), 64 | amount: Value(amount), 65 | categoryId: categoryId == null ? Value.absent() : Value(categoryId), 66 | modifiedDate: Value(modifiedDate), 67 | description: Value(description)); 68 | } 69 | 70 | @override 71 | bool operator ==(Object other) => 72 | identical(this, other) || 73 | other is Entry && 74 | runtimeType == other.runtimeType && 75 | amount == other.amount && 76 | id == other.id && 77 | categoryId == other.categoryId && 78 | modifiedDate == other.modifiedDate && 79 | description == other.description; 80 | 81 | @override 82 | int get hashCode => 83 | amount.hashCode ^ 84 | id.hashCode ^ 85 | categoryId.hashCode ^ 86 | modifiedDate.hashCode ^ 87 | description.hashCode; 88 | 89 | @override 90 | String toString() { 91 | return 'Entry{amount: $amount, id: $id, categoryId: $categoryId, modifiedDate: $modifiedDate, description: $description}'; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/ui/dialog/monthly_cycle_date_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/core/app_localization.dart'; 2 | import 'package:expense_manager/core/color_scheme.dart'; 3 | import 'package:expense_manager/ui/setting/setting_view_model.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 | 7 | class MonthlyCycleDateDialog extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | return Column( 11 | mainAxisSize: MainAxisSize.min, 12 | children: [ 13 | SizedBox(height: 24), 14 | Text( 15 | AppLocalization.of(context).getTranslatedVal("month_cycle_date"), 16 | style: Theme.of(context).textTheme.titleMedium, 17 | ), 18 | SizedBox(height: 16), 19 | Divider(color: Theme.of(context).colorScheme.crossLightColor), 20 | SizedBox( 21 | height: 250, 22 | child: Consumer( 23 | builder: (context, ref, child) { 24 | String selected = 25 | ref.watch(monthStartDateStateNotifier.notifier).date; 26 | return ListView( 27 | padding: const EdgeInsets.symmetric(horizontal: 16), 28 | shrinkWrap: true, 29 | children: [ 30 | "1", 31 | "2", 32 | "3", 33 | "4", 34 | "5", 35 | "6", 36 | "7", 37 | "8", 38 | "9", 39 | "10", 40 | "11", 41 | "12", 42 | "13", 43 | "14", 44 | "15", 45 | "16", 46 | "17", 47 | "18", 48 | "19", 49 | "20", 50 | ] 51 | .map((e) => InkWell( 52 | onTap: () { 53 | ref 54 | .watch(monthStartDateStateNotifier.notifier) 55 | .setDate(e); 56 | Navigator.pop(context); 57 | }, 58 | child: Padding( 59 | padding: const EdgeInsets.all(8), 60 | child: Center( 61 | child: Text( 62 | e, 63 | style: Theme.of(context) 64 | .textTheme 65 | .titleSmall! 66 | .copyWith( 67 | fontSize: e == selected ? 24 : 14, 68 | fontWeight: e == selected 69 | ? FontWeight.bold 70 | : FontWeight.w500), 71 | ), 72 | ), 73 | ), 74 | )) 75 | .toList()); 76 | }, 77 | ), 78 | ), 79 | SizedBox(height: 16), 80 | ], 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/ui/app/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/core/app_localization.dart'; 2 | import 'package:expense_manager/core/constants.dart'; 3 | import 'package:expense_manager/core/routes.dart'; 4 | import 'package:expense_manager/core/theme.dart'; 5 | import 'package:expense_manager/data/models/category.dart'; 6 | import 'package:expense_manager/data/models/entry_with_category.dart'; 7 | import 'package:expense_manager/ui/addCategory/addCategory.dart'; 8 | import 'package:expense_manager/ui/addEntry/addEntry.dart'; 9 | import 'package:expense_manager/ui/category_details/category_details.dart'; 10 | import 'package:expense_manager/ui/category_list/category_list.dart'; 11 | import 'package:expense_manager/ui/home/home.dart'; 12 | import 'package:expense_manager/ui/landing/onboarding_pageview.dart'; 13 | import 'package:expense_manager/ui/setting/setting.dart'; 14 | import 'package:expense_manager/ui/welcome/welcome.dart'; 15 | import 'package:flutter/material.dart'; 16 | import 'package:flutter_localizations/flutter_localizations.dart'; 17 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 18 | import 'package:tuple/tuple.dart'; 19 | 20 | import 'app_state.dart'; 21 | 22 | class MyApp extends ConsumerWidget { 23 | const MyApp({Key? key}) : super(key: key); 24 | 25 | @override 26 | Widget build(BuildContext context, WidgetRef ref) { 27 | final appState = ref.watch(appStateNotifier); 28 | return MaterialApp( 29 | theme: AppTheme.theme, 30 | darkTheme: AppTheme.darkTheme, 31 | supportedLocales: [ 32 | Locale('en', 'US'), 33 | Locale('es', 'ES'), 34 | Locale('pt', 'BR') 35 | ], 36 | localizationsDelegates: [ 37 | AppLocalization.delegate, 38 | GlobalMaterialLocalizations.delegate, 39 | GlobalWidgetsLocalizations.delegate, 40 | GlobalCupertinoLocalizations.delegate 41 | ], 42 | localeResolutionCallback: (deviceLocale, supportedLocales) { 43 | for (var locale in supportedLocales) { 44 | if (locale.languageCode == deviceLocale?.languageCode && 45 | locale.countryCode == deviceLocale?.countryCode) { 46 | return deviceLocale; 47 | } 48 | } 49 | return supportedLocales.first; 50 | }, 51 | locale: appState.currentLocale, 52 | themeMode: appState.themeMode, 53 | debugShowCheckedModeBanner: false, 54 | initialRoute: (ref.read(appStateNotifier)).userName.isEmpty 55 | ? AppRoutes.onBoarding 56 | : AppRoutes.home, 57 | routes: { 58 | AppRoutes.welcome: (context) => Welcome(), 59 | AppRoutes.onBoarding: (context) => CustomScrollOnboarding(), 60 | AppRoutes.home: (context) => HomeScreen(), 61 | AppRoutes.addEntry: (context) => AddEntry( 62 | entryWithCategory: ModalRoute.of(context)?.settings.arguments 63 | as Tuple3), 64 | AppRoutes.categoryList: (context) => CategoryList( 65 | entryType: ModalRoute.of(context)?.settings.arguments as EntryType), 66 | AppRoutes.addCategory: (context) => AddCategory( 67 | tuple2: ModalRoute.of(context)?.settings.arguments 68 | as Tuple2), 69 | AppRoutes.categoryDetails: (context) => CategoryDetails(), 70 | AppRoutes.setting: (context) => Setting(), 71 | }, 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Miscellaneous 44 | *.lock 45 | 46 | # Visual Studio Code related 47 | .classpath 48 | .project 49 | .settings/ 50 | .vscode/ 51 | 52 | # Flutter repo-specific 53 | /bin/cache/ 54 | /bin/internal/bootstrap.bat 55 | /bin/internal/bootstrap.sh 56 | /bin/mingit/ 57 | /dev/benchmarks/mega_gallery/ 58 | /dev/bots/.recipe_deps 59 | /dev/bots/android_tools/ 60 | /dev/devicelab/ABresults*.json 61 | /dev/docs/doc/ 62 | /dev/docs/flutter.docs.zip 63 | /dev/docs/lib/ 64 | /dev/docs/pubspec.yaml 65 | /dev/integration_tests/**/xcuserdata 66 | /dev/integration_tests/**/Pods 67 | /packages/flutter/coverage/ 68 | version 69 | analysis_benchmark.json 70 | 71 | # packages file containing multi-root paths 72 | .packages.generated 73 | 74 | # Flutter/Dart/Pub related 75 | **/generated_plugin_registrant.dart 76 | build/ 77 | flutter_*.png 78 | linked_*.ds 79 | unlinked.ds 80 | unlinked_spec.ds 81 | 82 | # Android related 83 | **/android/**/gradle-wrapper.jar 84 | **/android/.gradle 85 | **/android/captures/ 86 | **/android/gradlew 87 | **/android/gradlew.bat 88 | **/android/local.properties 89 | **/android/**/GeneratedPluginRegistrant.java 90 | **/android/key.properties 91 | *.jks 92 | **/android/android_keys.zip 93 | 94 | # iOS/XCode related 95 | **/ios/**/*.mode1v3 96 | **/ios/**/*.mode2v3 97 | **/ios/**/*.moved-aside 98 | **/ios/**/*.pbxuser 99 | **/ios/**/*.perspectivev3 100 | **/ios/**/*sync/ 101 | **/ios/**/.sconsign.dblite 102 | **/ios/**/.tags* 103 | **/ios/**/.vagrant/ 104 | **/ios/**/DerivedData/ 105 | **/ios/**/Icon? 106 | **/ios/**/Pods/ 107 | **/ios/**/.symlinks/ 108 | **/ios/**/profile 109 | **/ios/**/xcuserdata 110 | **/ios/.generated/ 111 | **/ios/Flutter/App.framework 112 | **/ios/Flutter/Flutter.framework 113 | **/ios/Flutter/Flutter.podspec 114 | **/ios/Flutter/Generated.xcconfig 115 | **/ios/Flutter/app.flx 116 | **/ios/Flutter/app.zip 117 | **/ios/Flutter/flutter_assets/ 118 | **/ios/Flutter/flutter_export_environment.sh 119 | **/ios/ServiceDefinitions.json 120 | **/ios/Runner/GeneratedPluginRegistrant.* 121 | 122 | # macOS 123 | **/macos/Flutter/GeneratedPluginRegistrant.swift 124 | **/macos/Flutter/Flutter-Debug.xcconfig 125 | **/macos/Flutter/Flutter-Release.xcconfig 126 | **/macos/Flutter/Flutter-Profile.xcconfig 127 | 128 | # Coverage 129 | coverage/ 130 | 131 | # Exceptions to above rules. 132 | !**/ios/**/default.mode1v3 133 | !**/ios/**/default.mode2v3 134 | !**/ios/**/default.pbxuser 135 | !**/ios/**/default.perspectivev3 136 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 137 | !/dev/ci/**/Gemfile.lock 138 | /android/app/google-services.json 139 | /ios/Runner/GoogleService-Info.plist 140 | -------------------------------------------------------------------------------- /lib/ui/addCategory/addCategory_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/core/constants.dart'; 2 | import 'package:expense_manager/data/models/category.dart' as cat; 3 | import 'package:expense_manager/data/repository/entry_repository_imp.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 | import 'package:tuple/tuple.dart'; 7 | 8 | // 9 | // part 'addCategory_state.freezed.dart'; 10 | // 11 | // @freezed 12 | // class AddCategoryEntity with _$AddCategoryEntity { 13 | // factory AddCategoryEntity({ 14 | // @required cat.Category category, 15 | // @required String name, 16 | // @required IconData iconData, 17 | // @required Color color, 18 | // @required EntryType entryType, 19 | // }) = _AddCategoryEntity; 20 | // 21 | // factory AddCategoryEntity.initial({@required cat.Category category,}) => 22 | // AddCategoryEntity( 23 | // category:category, 24 | // name: 25 | // ); 26 | // } 27 | 28 | final addCategoryModelProvider = ChangeNotifierProvider.autoDispose 29 | .family>( 30 | (ref, tuple2) => AddCategoryViewModel( 31 | entryDataSourceImp: ref.read(repositoryProvider), 32 | entryType: tuple2.item1, 33 | category: tuple2.item2, 34 | )); 35 | 36 | class AddCategoryViewModel with ChangeNotifier { 37 | final EntryRepositoryImp entryDataSourceImp; 38 | cat.Category? category; 39 | final EntryType entryType; 40 | late String name; 41 | late IconData iconData; 42 | late Color color; 43 | 44 | AddCategoryViewModel({ 45 | required this.entryDataSourceImp, 46 | this.category, 47 | required this.entryType, 48 | }) { 49 | if (category == null) { 50 | name = ""; 51 | iconData = AppConstants.otherCategory.icon; 52 | color = AppConstants.otherCategory.iconColor; 53 | } else { 54 | name = category!.name; 55 | iconData = category!.icon; 56 | color = category!.iconColor; 57 | } 58 | notifyListeners(); 59 | } 60 | 61 | void changeColor(Color color) { 62 | this.color = color; 63 | notifyListeners(); 64 | } 65 | 66 | void changeIcon(IconData iconData) { 67 | this.iconData = iconData; 68 | notifyListeners(); 69 | } 70 | 71 | void changeName(String name) { 72 | this.name = name; 73 | } 74 | 75 | void addUpdateCategory() { 76 | if (category == null) { 77 | entryDataSourceImp 78 | .addCategory( 79 | entryType, 80 | cat.Category( 81 | name: name.trim(), 82 | icon: iconData, 83 | iconColor: color, 84 | entryType: entryType, 85 | )) 86 | .listen((event) {}); 87 | } else { 88 | entryDataSourceImp 89 | .updateCategory( 90 | entryType, 91 | cat.Category( 92 | id: category!.id, 93 | position: category!.position, 94 | name: name, 95 | icon: iconData, 96 | iconColor: color, 97 | entryType: entryType, 98 | )) 99 | .listen((event) {}); 100 | } 101 | } 102 | 103 | void delete() { 104 | entryDataSourceImp 105 | .deleteCategory(entryType, category!.id!) 106 | .listen((event) {}); 107 | } 108 | 109 | @override 110 | void dispose() { 111 | category = null; 112 | super.dispose(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/ui/dashboard/pie_chart.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tuple/tuple.dart'; 3 | 4 | import 'chart_painter.dart'; 5 | 6 | class PieChart extends StatefulWidget { 7 | PieChart({ 8 | required this.dataMap, 9 | required this.chartRadius, 10 | required this.animationDuration, 11 | this.chartLegendSpacing = 48, 12 | this.initialAngleInDegree = 0.0, 13 | required this.centerText, 14 | this.ringStrokeWidth = 20.0, 15 | this.emptyColor = Colors.grey, 16 | Key? key, 17 | }) : super(key: key); 18 | 19 | final List> dataMap; 20 | final double chartRadius; 21 | final Duration animationDuration; 22 | final double chartLegendSpacing; 23 | final double initialAngleInDegree; 24 | final Widget centerText; 25 | final double ringStrokeWidth; 26 | final Color emptyColor; 27 | 28 | @override 29 | _PieChartState createState() => _PieChartState(); 30 | } 31 | 32 | class _PieChartState extends State 33 | with SingleTickerProviderStateMixin { 34 | late final Animation animation; 35 | late final AnimationController controller; 36 | double _animFraction = 0.0; 37 | 38 | late List legendValues; 39 | late List colorList; 40 | 41 | void initValues() { 42 | this.legendValues = 43 | widget.dataMap.map((e) => e.item1).toList(growable: false); 44 | this.colorList = widget.dataMap.map((e) => e.item2).toList(growable: false); 45 | } 46 | 47 | void initData() { 48 | assert( 49 | widget.dataMap.isNotEmpty, 50 | "dataMap passed to pie chart cant be null or empty", 51 | ); 52 | initValues(); 53 | } 54 | 55 | @override 56 | void initState() { 57 | super.initState(); 58 | initData(); 59 | controller = AnimationController( 60 | duration: widget.animationDuration, 61 | vsync: this, 62 | ); 63 | final Animation curve = CurvedAnimation( 64 | parent: controller, 65 | curve: Curves.decelerate, 66 | ); 67 | animation = 68 | Tween(begin: 0, end: 1).animate(curve as Animation) 69 | ..addListener(() { 70 | setState(() { 71 | _animFraction = animation.value; 72 | }); 73 | }); 74 | controller.forward(); 75 | } 76 | 77 | Widget _getChart() { 78 | return LayoutBuilder( 79 | builder: (_, c) => Container( 80 | height: widget.chartRadius != null 81 | ? c.maxWidth < widget.chartRadius 82 | ? c.maxWidth 83 | : widget.chartRadius 84 | : null, 85 | child: CustomPaint( 86 | painter: PieChartPainter( 87 | _animFraction, 88 | colorList, 89 | values: legendValues, 90 | initialAngle: widget.initialAngleInDegree, 91 | strokeWidth: widget.ringStrokeWidth, 92 | emptyColor: widget.emptyColor, 93 | ), 94 | child: Center(child: widget.centerText), 95 | ), 96 | ), 97 | ); 98 | } 99 | 100 | @override 101 | Widget build(BuildContext context) { 102 | return Container( 103 | alignment: Alignment.center, 104 | padding: EdgeInsets.all(8.0), 105 | child: _getChart(), 106 | ); 107 | } 108 | 109 | @override 110 | void didUpdateWidget(PieChart oldWidget) { 111 | initData(); 112 | super.didUpdateWidget(oldWidget); 113 | } 114 | 115 | @override 116 | void dispose() { 117 | controller.dispose(); 118 | super.dispose(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/ui/history/entry_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/core/app_localization.dart'; 2 | import 'package:expense_manager/core/constants.dart'; 3 | import 'package:expense_manager/ui/history/history_view_model.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 | 7 | class EntryTypeView extends ConsumerWidget { 8 | const EntryTypeView({Key? key}) : super(key: key); 9 | 10 | @override 11 | Widget build(BuildContext context, WidgetRef ref) { 12 | final vm = ref.watch(entryTypeProvider); 13 | 14 | return Padding( 15 | padding: const EdgeInsets.symmetric(horizontal: 24), 16 | child: Row( 17 | children: [ 18 | Expanded( 19 | child: InkWell( 20 | onTap: () { 21 | ref.read(entryTypeProvider.notifier).state = EntryType.all; 22 | }, 23 | child: Card( 24 | shape: RoundedRectangleBorder( 25 | borderRadius: BorderRadius.circular(4), 26 | side: vm == EntryType.all 27 | ? BorderSide( 28 | width: 1, 29 | color: Color(0xff2196F3), 30 | ) 31 | : BorderSide.none, 32 | ), 33 | child: Padding( 34 | padding: const EdgeInsets.all(12), 35 | child: Text( 36 | AppLocalization.of(context).getTranslatedVal("all"), 37 | textAlign: TextAlign.center, 38 | style: Theme.of(context).textTheme.bodySmall, 39 | ), 40 | ), 41 | ), 42 | ), 43 | ), 44 | Expanded( 45 | child: InkWell( 46 | onTap: () { 47 | ref.read(entryTypeProvider.notifier).state = EntryType.expense; 48 | }, 49 | child: Card( 50 | shape: RoundedRectangleBorder( 51 | borderRadius: BorderRadius.circular(4), 52 | side: vm == EntryType.expense 53 | ? BorderSide( 54 | width: 1, 55 | color: Color(0xff2196F3), 56 | ) 57 | : BorderSide.none, 58 | ), 59 | child: Padding( 60 | padding: const EdgeInsets.all(12), 61 | child: Text( 62 | AppLocalization.of(context).getTranslatedVal("expense"), 63 | textAlign: TextAlign.center, 64 | style: Theme.of(context).textTheme.bodySmall, 65 | ), 66 | ), 67 | ), 68 | ), 69 | ), 70 | Expanded( 71 | child: InkWell( 72 | onTap: () { 73 | ref.read(entryTypeProvider.notifier).state = EntryType.income; 74 | }, 75 | child: Card( 76 | shape: RoundedRectangleBorder( 77 | borderRadius: BorderRadius.circular(4), 78 | side: vm == EntryType.income 79 | ? BorderSide( 80 | width: 1, 81 | color: Color(0xff2196F3), 82 | ) 83 | : BorderSide.none, 84 | ), 85 | child: Padding( 86 | padding: const EdgeInsets.all(12), 87 | child: Text( 88 | AppLocalization.of(context).getTranslatedVal("income"), 89 | textAlign: TextAlign.center, 90 | style: Theme.of(context).textTheme.bodySmall, 91 | ), 92 | ), 93 | ), 94 | ), 95 | ), 96 | ], 97 | ), 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: expense_manager 2 | description: A new Flutter application. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.1.0+19 19 | 20 | environment: 21 | sdk: '>=3.0.1 <4.0.0' 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | 27 | cupertino_icons: ^1.0.5 28 | 29 | flutter_riverpod: ^2.1.3 30 | 31 | drift_sqflite: ^2.0.0 32 | shared_preferences: ^2.0.15 33 | flutter_localization: ^0.1.11 34 | 35 | intl: ^0.18.0 36 | rxdart: ^0.27.7 37 | tuple: ^2.0.1 38 | collection: ^1.16.0 39 | freezed_annotation: ^2.2.0 40 | numeral: ^2.0.1 41 | package_info: ^2.0.2 42 | fimber: ^0.6.6 43 | path_provider: ^2.0.9 44 | path: ^1.8.0 45 | 46 | fl_chart: ^0.55.2 47 | dotted_border: ^2.0.0+3 48 | percent_indicator: ^4.2.2 49 | smooth_page_indicator: ^1.0.0+2 50 | syncfusion_flutter_gauges: ^20.3.58 51 | 52 | firebase_core: ^2.4.1 53 | firebase_crashlytics: ^3.0.11 54 | firebase_analytics: ^10.1.0 55 | 56 | dev_dependencies: 57 | flutter_test: 58 | sdk: flutter 59 | flutter_lints: ^2.0.1 60 | drift_dev: ^2.8.3 61 | freezed: ^2.2.0 62 | build_runner: ^2.4.5 63 | flutter_launcher_icons: ^0.11.0 64 | 65 | flutter_icons: 66 | android: "ic_launcher" 67 | ios: true 68 | image_path: "assets/images/expense_manage.png" 69 | 70 | # For information on the generic Dart part of this file, see the 71 | # following page: https://dart.dev/tools/pub/pubspec 72 | 73 | # The following section is specific to Flutter. 74 | flutter: 75 | 76 | # The following line ensures that the Material Icons font is 77 | # included with your application, so that you can use the icons in 78 | # the material Icons class. 79 | uses-material-design: true 80 | 81 | # To add assets to your application, add an assets section, like this: 82 | assets: 83 | - assets/images/ 84 | - assets/language/ 85 | # - images/a_dot_ham.jpeg 86 | 87 | # An image asset can refer to one or more resolution-specific "variants", see 88 | # https://flutter.dev/assets-and-images/#resolution-aware. 89 | 90 | # For details regarding adding assets from package dependencies, see 91 | # https://flutter.dev/assets-and-images/#from-packages 92 | 93 | # To add custom fonts to your application, add a fonts section here,zz 94 | # in this "flutter" section. Each entry in this list should have a 95 | # "family" key with the font family name, and a "fonts" key with a 96 | # list giving the asset and other descriptors for the font. For 97 | # example: 98 | # fonts: 99 | # - family: Schyler 100 | # fonts: 101 | # - asset: fonts/Schyler-Regular.ttf 102 | # - asset: fonts/Schyler-Italic.ttf 103 | # style: italic 104 | # - family: Trajan Pro 105 | # fonts: 106 | # - asset: fonts/TrajanPro.ttf 107 | # - asset: fonts/TrajanPro_Bold.ttf 108 | # weight: 700 109 | # 110 | # For details regarding fonts from package dependencies, 111 | # see https://flutter.dev/custom-fonts/#from-packages 112 | -------------------------------------------------------------------------------- /lib/core/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppTheme { 4 | static ThemeData get theme { 5 | return ThemeData( 6 | brightness: Brightness.light, 7 | scaffoldBackgroundColor: Colors.white, 8 | backgroundColor: Colors.white, 9 | primaryColor: Color(0xff2196F3), 10 | dividerColor: Color(0xffeeeeee), 11 | appBarTheme: AppBarTheme( 12 | centerTitle: false, 13 | color: Colors.white, 14 | elevation: 0, 15 | iconTheme: IconThemeData(color: Colors.black, size: 20), 16 | titleTextStyle: TextStyle( 17 | color: Color(0xff2196F3), 18 | fontSize: 16, 19 | fontWeight: FontWeight.w500, 20 | )), 21 | floatingActionButtonTheme: FloatingActionButtonThemeData( 22 | backgroundColor: Colors.white, 23 | foregroundColor: Colors.black, 24 | ), 25 | bottomAppBarTheme: BottomAppBarTheme( 26 | color: Colors.white, 27 | ), 28 | cardTheme: CardTheme( 29 | color: Colors.white, 30 | ), 31 | textSelectionTheme: TextSelectionThemeData( 32 | cursorColor: Colors.black, 33 | ), 34 | inputDecorationTheme: InputDecorationTheme( 35 | fillColor: Colors.black, 36 | labelStyle: TextStyle( 37 | color: Color(0xff2196F3), 38 | fontSize: 16, 39 | fontWeight: FontWeight.normal), 40 | enabledBorder: UnderlineInputBorder( 41 | borderSide: BorderSide(color: Color(0xff2196F3))), 42 | focusedBorder: UnderlineInputBorder( 43 | borderSide: BorderSide(color: Color(0xff2196F3))), 44 | ), 45 | textTheme: TextTheme( 46 | bodySmall: TextStyle( 47 | fontSize: 12, 48 | color: Color(0xff212121), 49 | ), 50 | ), 51 | ); 52 | } 53 | 54 | static ThemeData get darkTheme { 55 | return ThemeData( 56 | brightness: Brightness.dark, 57 | scaffoldBackgroundColor: Colors.black, 58 | backgroundColor: Colors.black, 59 | primaryColor: Color(0xff212121), 60 | dividerColor: Color(0xff121212), 61 | appBarTheme: AppBarTheme( 62 | centerTitle: false, 63 | color: Colors.black, 64 | elevation: 0, 65 | iconTheme: IconThemeData(color: Colors.white, size: 20), 66 | titleTextStyle: TextStyle( 67 | color: Colors.white, 68 | fontSize: 16, 69 | fontWeight: FontWeight.w500, 70 | )), 71 | floatingActionButtonTheme: FloatingActionButtonThemeData( 72 | backgroundColor: Color(0xff212121), 73 | foregroundColor: Colors.white, 74 | ), 75 | bottomAppBarTheme: BottomAppBarTheme( 76 | color: Color(0xff212121), 77 | ), 78 | cardTheme: CardTheme( 79 | color: Color(0xff212121), 80 | shadowColor: Color(0x000000DE), 81 | ), 82 | textSelectionTheme: TextSelectionThemeData( 83 | cursorColor: Colors.white, 84 | ), 85 | inputDecorationTheme: InputDecorationTheme( 86 | fillColor: Colors.black, 87 | labelStyle: TextStyle( 88 | color: Color(0xff2196F3), 89 | fontSize: 16, 90 | fontWeight: FontWeight.normal), 91 | enabledBorder: UnderlineInputBorder( 92 | borderSide: BorderSide(color: Color(0xff2196F3))), 93 | focusedBorder: UnderlineInputBorder( 94 | borderSide: BorderSide(color: Color(0xff2196F3))), 95 | disabledBorder: UnderlineInputBorder( 96 | borderSide: BorderSide(color: Color(0xff2196F3))), 97 | ), 98 | textTheme: TextTheme( 99 | bodySmall: TextStyle( 100 | fontSize: 12, 101 | color: Color(0xffc9c9c9), 102 | fontWeight: FontWeight.normal, 103 | ), 104 | ), 105 | elevatedButtonTheme: ElevatedButtonThemeData( 106 | style: ButtonStyle( 107 | backgroundColor: 108 | MaterialStateColor.resolveWith((states) => Color(0xff212121)), 109 | ), 110 | ), 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/data/datasource/sharedpref/shared_preference_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | 6 | final sharedPreferencesProvider = 7 | Provider((ref) => throw UnimplementedError()); 8 | 9 | class SharedPreferencesHelper { 10 | final SharedPreferences prefs; 11 | 12 | SharedPreferencesHelper({required this.prefs}); 13 | 14 | // put object 15 | Future putObject(String key, Map value) { 16 | return prefs.setString(key, json.encode(value)); 17 | } 18 | 19 | // put object 20 | Future putObjectNew(String key, String Function() toString) { 21 | return prefs.setString(key, toString()); 22 | } 23 | 24 | // get obj 25 | T getObj(String key, T Function(Map v) f, 26 | {required T defValue}) { 27 | Map? map = getObject(key); 28 | return map == null ? defValue : f(map); 29 | } 30 | 31 | // get not null obj 32 | T getNotNullObj(String key, T Function(Map v) f) { 33 | Map map = getObject(key)!; 34 | return f(map); 35 | } 36 | 37 | // get object 38 | Map? getObject(String key) { 39 | String? data = prefs.getString(key); 40 | return (data == null || data.isEmpty) 41 | ? null 42 | : json.decode(data) as Map; 43 | } 44 | 45 | // put object list 46 | Future putObjectList(String key, List list) { 47 | List dataList = list.map((value) { 48 | return json.encode(value); 49 | }).toList(); 50 | return prefs.setStringList(key, dataList); 51 | } 52 | 53 | // get obj list 54 | List getObjList(String key, T Function(Map v) f, 55 | {List defValue = const []}) { 56 | List? dataList = getObjectList(key); 57 | List? list = dataList?.map((value) { 58 | return f(value); 59 | }).toList(); 60 | return list ?? defValue; 61 | } 62 | 63 | // get object list 64 | List? getObjectList(String key) { 65 | List? dataLis = prefs.getStringList(key); 66 | return dataLis?.map((value) { 67 | Map dataMap = json.decode(value); 68 | return dataMap; 69 | }).toList(); 70 | } 71 | 72 | // get string 73 | String getString(String key, {String defValue = ''}) { 74 | return prefs.getString(key) ?? defValue; 75 | } 76 | 77 | // put string 78 | Future putString(String key, String value) { 79 | return prefs.setString(key, value); 80 | } 81 | 82 | // get bool 83 | bool getBool(String key, {bool defValue = false}) { 84 | return prefs.getBool(key) ?? defValue; 85 | } 86 | 87 | // put bool 88 | Future putBool(String key, bool value) { 89 | return prefs.setBool(key, value); 90 | } 91 | 92 | // get int 93 | int getInt(String key, {int defValue = -1}) { 94 | return prefs.getInt(key) ?? defValue; 95 | } 96 | 97 | // put int. 98 | Future putInt(String key, int value) { 99 | return prefs.setInt(key, value); 100 | } 101 | 102 | // get double 103 | double getDouble(String key, {double defValue = -1}) { 104 | return prefs.getDouble(key) ?? defValue; 105 | } 106 | 107 | // put double 108 | Future putDouble(String key, double value) { 109 | return prefs.setDouble(key, value); 110 | } 111 | 112 | // get string list 113 | List getStringList(String key, {List defValue = const []}) { 114 | return prefs.getStringList(key) ?? defValue; 115 | } 116 | 117 | // put string list 118 | Future putStringList(String key, List value) { 119 | return prefs.setStringList(key, value); 120 | } 121 | 122 | // get dynamic 123 | dynamic getDynamic(String key, {required Object defValue}) { 124 | return prefs.get(key) ?? defValue; 125 | } 126 | 127 | // have key 128 | bool haveKey(String key) { 129 | return prefs.getKeys().contains(key); 130 | } 131 | 132 | // get keys 133 | Set getKeys() { 134 | return prefs.getKeys(); 135 | } 136 | 137 | // remove 138 | Future remove(String key) { 139 | return prefs.remove(key); 140 | } 141 | 142 | // clear 143 | Future clear() { 144 | return prefs.clear(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /lib/ui/addEntry/addEntry_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/core/constants.dart'; 2 | import 'package:expense_manager/data/models/category.dart' as cat; 3 | import 'package:expense_manager/data/models/entry.dart'; 4 | import 'package:expense_manager/data/models/entry_with_category.dart'; 5 | import 'package:expense_manager/data/repository/entry_repository_imp.dart'; 6 | import 'package:expense_manager/ui/history/history_view_model.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 9 | import 'package:tuple/tuple.dart'; 10 | 11 | final addEntryModelProvider = ChangeNotifierProvider.autoDispose.family< 12 | AddEntryViewModel, Tuple3>( 13 | (ref, entryWithCategory) => AddEntryViewModel( 14 | entryDataSourceImp: ref.read(repositoryProvider), 15 | entryType: entryWithCategory.item1, 16 | entryWithCategory: entryWithCategory.item2, 17 | category: entryWithCategory.item3, 18 | ref: ref, 19 | ), 20 | ); 21 | 22 | class AddEntryViewModel with ChangeNotifier { 23 | EntryRepositoryImp entryDataSourceImp; 24 | EntryWithCategory? entryWithCategory; 25 | 26 | List expenseCategoryList = []; 27 | List incomeCategoryList = []; 28 | String amount = ""; 29 | cat.Category? category; 30 | DateTime date = DateTime.now(); 31 | String description = ""; 32 | EntryType entryType; 33 | final Ref ref; 34 | 35 | AddEntryViewModel({ 36 | required this.entryDataSourceImp, 37 | this.entryWithCategory, 38 | required this.category, 39 | required this.entryType, 40 | required this.ref, 41 | }) { 42 | this.entryWithCategory = entryWithCategory; 43 | if (entryWithCategory != null) { 44 | amount = entryWithCategory!.entry.amount.toString(); 45 | date = entryWithCategory!.entry.modifiedDate; 46 | category = entryWithCategory!.category; 47 | description = entryWithCategory!.entry.description; 48 | } 49 | 50 | entryDataSourceImp.getAllCategory(EntryType.expense).listen((event) { 51 | expenseCategoryList = event; 52 | notifyListeners(); 53 | }); 54 | 55 | entryDataSourceImp.getAllCategory(EntryType.income).listen((event) { 56 | incomeCategoryList = event; 57 | notifyListeners(); 58 | }); 59 | } 60 | 61 | void addUpdateEntry() { 62 | if (entryWithCategory != null) { 63 | entryDataSourceImp 64 | .updateEntry( 65 | entryType, 66 | Entry( 67 | id: entryWithCategory!.entry.id, 68 | amount: double.parse(amount), 69 | categoryId: category?.id, 70 | modifiedDate: date, 71 | description: description)) 72 | .listen((event) { 73 | ref.invalidate(historyListProvider); 74 | }); 75 | } else { 76 | entryDataSourceImp 77 | .addEntry( 78 | entryType, 79 | Entry( 80 | amount: double.parse(amount), 81 | categoryId: category?.id, 82 | modifiedDate: date, 83 | description: description)) 84 | .listen((event) { 85 | ref.invalidate(historyListProvider); 86 | }); 87 | } 88 | } 89 | 90 | void categoryChange(cat.Category category) { 91 | this.category = category; 92 | notifyListeners(); 93 | } 94 | 95 | void amountChange(String amount) { 96 | this.amount = amount; 97 | notifyListeners(); 98 | } 99 | 100 | void entryTypeChange(EntryType entryType) { 101 | this.entryType = entryType; 102 | notifyListeners(); 103 | } 104 | 105 | void changeDate(DateTime dateTime) { 106 | this.date = DateTime( 107 | dateTime.year, dateTime.month, dateTime.day, date.hour, date.minute); 108 | notifyListeners(); 109 | } 110 | 111 | void changeDescription(String description) { 112 | this.description = description; 113 | notifyListeners(); 114 | } 115 | 116 | void changeTime(TimeOfDay timeOfDay) { 117 | this.date = DateTime( 118 | date.year, date.month, date.day, timeOfDay.hour, timeOfDay.minute); 119 | notifyListeners(); 120 | } 121 | 122 | @override 123 | void dispose() { 124 | expenseCategoryList = []; 125 | amount = ""; 126 | category = null; 127 | date = DateTime.now(); 128 | description = ""; 129 | super.dispose(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/ui/home/home.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/core/constants.dart'; 2 | import 'package:expense_manager/core/routes.dart'; 3 | import 'package:expense_manager/core/app_localization.dart'; 4 | import 'package:expense_manager/data/models/home_tab.dart'; 5 | import 'package:expense_manager/ui/dashboard/dashboard.dart'; 6 | import 'package:expense_manager/ui/history/history.dart'; 7 | import 'package:expense_manager/ui/home/home_state.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 10 | import 'package:tuple/tuple.dart'; 11 | 12 | class HomeScreen extends ConsumerWidget { 13 | @override 14 | Widget build(BuildContext context, WidgetRef ref) { 15 | final homeViewModel = ref.watch(signInModelProvider); 16 | return Scaffold( 17 | body: homeViewModel.activeTab == HomeTab.dashboard 18 | ? Dashboard() 19 | : History(), 20 | floatingActionButton: FloatingActionButton( 21 | onPressed: () { 22 | Navigator.pushNamed(context, AppRoutes.addEntry, 23 | arguments: Tuple3(EntryType.expense, null, null)); 24 | }, 25 | child: Icon(Icons.add), 26 | ), 27 | floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, 28 | bottomNavigationBar: BottomAppBar( 29 | notchMargin: 8, 30 | shape: CircularNotchedRectangle(), 31 | child: Row( 32 | children: [ 33 | Expanded( 34 | child: SizedBox( 35 | height: 60, 36 | child: InkWell( 37 | onTap: () => homeViewModel.changeTab(0), 38 | borderRadius: 39 | BorderRadius.only(topRight: Radius.circular(24)), 40 | child: Row( 41 | mainAxisSize: MainAxisSize.min, 42 | mainAxisAlignment: MainAxisAlignment.center, 43 | children: [ 44 | Icon( 45 | Icons.dashboard, 46 | size: 20, 47 | color: homeViewModel.activeTab == HomeTab.dashboard 48 | ? Color(0xff2196F3) 49 | : null, 50 | ), 51 | SizedBox(width: 8), 52 | Text( 53 | AppLocalization.of(context) 54 | .getTranslatedVal("dashboard"), 55 | style: Theme.of(context).textTheme.titleSmall!.copyWith( 56 | fontSize: 12, 57 | color: homeViewModel.activeTab == HomeTab.dashboard 58 | ? Color(0xff2196F3) 59 | : null), 60 | ), 61 | ], 62 | ), 63 | ), 64 | ), 65 | ), 66 | SizedBox(width: 60), 67 | Expanded( 68 | child: SizedBox( 69 | height: 60, 70 | child: InkWell( 71 | borderRadius: BorderRadius.only(topLeft: Radius.circular(24)), 72 | onTap: () => homeViewModel.changeTab(1), 73 | child: Row( 74 | mainAxisSize: MainAxisSize.min, 75 | mainAxisAlignment: MainAxisAlignment.center, 76 | children: [ 77 | Icon( 78 | Icons.history, 79 | size: 20, 80 | color: homeViewModel.activeTab == HomeTab.history 81 | ? Color(0xff2196F3) 82 | : null, 83 | ), 84 | SizedBox(width: 8), 85 | Text( 86 | AppLocalization.of(context).getTranslatedVal("history"), 87 | style: Theme.of(context).textTheme.titleSmall!.copyWith( 88 | fontSize: 12, 89 | color: homeViewModel.activeTab == HomeTab.history 90 | ? Color(0xff2196F3) 91 | : null), 92 | ), 93 | ], 94 | ), 95 | ), 96 | ), 97 | ) 98 | ], 99 | ), 100 | ), 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/data/models/category.dart: -------------------------------------------------------------------------------- 1 | import 'package:drift/drift.dart'; 2 | import 'package:expense_manager/core/constants.dart'; 3 | import 'package:expense_manager/data/datasource/local/moor/app_database.dart'; 4 | import 'package:expense_manager/extension/icon_data_extension.dart'; 5 | import 'package:expense_manager/extension/string_extension.dart'; 6 | import 'package:flutter/cupertino.dart'; 7 | 8 | @immutable 9 | class Category { 10 | final int? id; 11 | final int? position; 12 | final String name; 13 | final IconData icon; 14 | final Color iconColor; 15 | final EntryType entryType; 16 | 17 | Category({ 18 | this.id, 19 | this.position, 20 | required this.name, 21 | required this.icon, 22 | required this.iconColor, 23 | required this.entryType, 24 | }); 25 | 26 | Category copyWith({ 27 | int? id, 28 | int? position, 29 | String? name, 30 | IconData? icon, 31 | Color? iconColor, 32 | EntryType? entryType, 33 | }) { 34 | return Category( 35 | id: id ?? this.id, 36 | position: position ?? this.position, 37 | name: name ?? this.name, 38 | icon: icon ?? this.icon, 39 | iconColor: iconColor ?? this.iconColor, 40 | entryType: entryType ?? this.entryType, 41 | ); 42 | } 43 | 44 | factory Category.fromExpenseCategoryEntity( 45 | CategoryEntityData? categoryEntityData) { 46 | return Category( 47 | id: categoryEntityData?.id, 48 | position: categoryEntityData?.position, 49 | name: categoryEntityData?.name ?? AppConstants.otherCategory.name, 50 | icon: categoryEntityData?.icon?.jsonToIconData() ?? 51 | AppConstants.otherCategory.icon, 52 | iconColor: categoryEntityData?.iconColor != null 53 | ? Color(int.parse(categoryEntityData!.iconColor)) 54 | : AppConstants.otherCategory.iconColor, 55 | entryType: EntryType.expense, 56 | ); 57 | } 58 | 59 | factory Category.fromIncomeCategoryEntity( 60 | IncomeCategoryEntityData? incomeCategoryEntityData, 61 | ) { 62 | return Category( 63 | id: incomeCategoryEntityData?.id, 64 | position: incomeCategoryEntityData?.position, 65 | name: incomeCategoryEntityData?.name ?? AppConstants.otherCategory.name, 66 | icon: incomeCategoryEntityData?.icon?.jsonToIconData() ?? 67 | AppConstants.otherCategory.icon, 68 | iconColor: incomeCategoryEntityData?.iconColor != null 69 | ? Color(int.parse(incomeCategoryEntityData!.iconColor)) 70 | : AppConstants.otherCategory.iconColor, 71 | entryType: EntryType.income, 72 | ); 73 | } 74 | 75 | factory Category.fromAllCategoryEntity( 76 | CategoryEntityData? categoryEntityData, 77 | int entryType, 78 | ) { 79 | return Category( 80 | id: categoryEntityData?.id, 81 | position: categoryEntityData?.position, 82 | name: categoryEntityData?.name ?? AppConstants.otherCategory.name, 83 | icon: categoryEntityData?.icon?.jsonToIconData() ?? 84 | AppConstants.otherCategory.icon, 85 | iconColor: categoryEntityData?.iconColor != null 86 | ? Color(int.parse(categoryEntityData!.iconColor)) 87 | : AppConstants.otherCategory.iconColor, 88 | entryType: EntryType.values[entryType], 89 | ); 90 | } 91 | 92 | CategoryEntityCompanion toCategoryEntityCompanion() { 93 | return CategoryEntityCompanion( 94 | id: id == null ? Value.absent() : Value(id!), 95 | position: position == null ? Value.absent() : Value(position!), 96 | name: Value(name), 97 | icon: Value(icon.iconDataToJson()), 98 | iconColor: 99 | Value("0x${iconColor.value.toRadixString(16).padLeft(8, '0')}")); 100 | } 101 | 102 | IncomeCategoryEntityCompanion toIncomeCategoryEntityCompanion() { 103 | return IncomeCategoryEntityCompanion( 104 | id: id == null ? Value.absent() : Value(id!), 105 | position: position == null ? Value.absent() : Value(position!), 106 | name: Value(name), 107 | icon: Value(icon.iconDataToJson()), 108 | iconColor: 109 | Value("0x${iconColor.value.toRadixString(16).padLeft(8, '0')}")); 110 | } 111 | 112 | CategoryEntityData toCategoryEntityData() { 113 | return CategoryEntityData( 114 | id: id!, 115 | position: position!, 116 | name: name, 117 | icon: icon.iconDataToJson(), 118 | iconColor: "0x${iconColor.value.toRadixString(16).padLeft(8, '0')}"); 119 | } 120 | 121 | @override 122 | bool operator ==(Object other) => 123 | identical(this, other) || 124 | other is Category && 125 | runtimeType == other.runtimeType && 126 | name == other.name && 127 | icon == other.icon && 128 | iconColor == other.iconColor; 129 | 130 | @override 131 | int get hashCode => name.hashCode ^ icon.hashCode ^ iconColor.hashCode; 132 | 133 | @override 134 | String toString() { 135 | return 'Category{id: $id,name: $position,position: $name, icon: $icon, iconColor: $iconColor}'; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /lib/ui/dashboard/dashboard_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/core/constants.dart'; 2 | import 'package:expense_manager/core/date_time_util.dart'; 3 | import 'package:expense_manager/data/models/category.dart' as cat; 4 | import 'package:expense_manager/data/models/category_with_entry_list.dart'; 5 | import 'package:expense_manager/data/repository/entry_repository_imp.dart'; 6 | import 'package:expense_manager/ui/setting/setting_view_model.dart'; 7 | import 'package:fl_chart/fl_chart.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 10 | 11 | final totalExpenseProvider = StateProvider((ref) { 12 | return ref 13 | .watch(totalExpenseStreamProvider) 14 | .whenOrNull(data: (value) => value, loading: () => 0); 15 | }); 16 | 17 | final totalExpenseStreamProvider = StreamProvider((ref) { 18 | var cycleDate = int.parse(ref.watch(monthStartDateStateNotifier).date); 19 | return ref.read(repositoryProvider).getExpanseSumByDateRange( 20 | DateTimeUtil.getStartDateTime(cycleDate), 21 | DateTimeUtil.getEndDateTime(cycleDate)); 22 | }); 23 | 24 | final totalIncomeProvider = StateProvider((ref) { 25 | return ref 26 | .watch(totalIncomeStreamProvider) 27 | .whenOrNull(data: (value) => value, loading: () => 0); 28 | }); 29 | 30 | final totalIncomeStreamProvider = StreamProvider((ref) { 31 | var cycleDate = int.parse(ref.watch(monthStartDateStateNotifier).date); 32 | return ref.read(repositoryProvider).getIncomeSumByDateRange( 33 | DateTimeUtil.getStartDateTime(cycleDate), 34 | DateTimeUtil.getEndDateTime(cycleDate)); 35 | }); 36 | 37 | final todayExpenseProvider = StateProvider((ref) { 38 | return ref 39 | .watch(todayExpenseStreamProvider) 40 | .whenOrNull(data: (value) => value, loading: () => 0); 41 | }); 42 | 43 | final todayExpenseStreamProvider = StreamProvider((ref) { 44 | return ref.read(repositoryProvider).getTodayExpense(); 45 | }); 46 | 47 | final totalIncomeExpenseRatioProvider = StateProvider((ref) { 48 | var expense = ref.watch(totalExpenseProvider); 49 | var income = ref.watch(totalIncomeProvider); 50 | if (income == 0 && expense == 0) { 51 | return 0.0; 52 | } else if (income == 0) { 53 | return 1.0; 54 | } else if (expense == 0) { 55 | return 0.0; 56 | } else { 57 | return expense! / income!; 58 | } 59 | }); 60 | 61 | final categoryPieChartTeachItemProvider = StateProvider((_) => -1); 62 | 63 | final categoryPieChartVisibilityProvider = 64 | StateProvider((ref) => ref.watch(dashboardProvider).list.isEmpty); 65 | 66 | final categoryPieChartProvider = Provider>((ref) { 67 | int touchedIndex = 68 | ref.watch(categoryPieChartTeachItemProvider.notifier).state; 69 | double? totalAmount = ref.read(totalExpenseStreamProvider).value; 70 | return ref.watch(dashboardProvider).list.asMap().entries.map((e) { 71 | return PieChartSectionData( 72 | color: e.value.category.iconColor, 73 | value: e.value.total, 74 | title: '${100 * e.value.total ~/ totalAmount!}%', 75 | radius: e.key == touchedIndex ? 60 : 50, 76 | titleStyle: TextStyle( 77 | fontSize: e.key == touchedIndex ? 20 : 16, 78 | fontWeight: FontWeight.bold, 79 | color: const Color(0xffffffff)), 80 | ); 81 | }).toList(); 82 | }); 83 | 84 | final categoryPieChartListProvider = Provider>((ref) { 85 | return ref.watch(dashboardProvider).list.map((e) => e.category).toList(); 86 | }); 87 | 88 | final dashboardProvider = ChangeNotifierProvider((ref) { 89 | return DashboardViewModel( 90 | entryDataSourceImp: ref.read(repositoryProvider), 91 | cycleDate: int.parse(ref.watch(monthStartDateStateNotifier).date)); 92 | }); 93 | 94 | class DashboardViewModel with ChangeNotifier { 95 | EntryRepositoryImp entryDataSourceImp; 96 | List list = []; 97 | int cycleDate = 1; 98 | 99 | DashboardViewModel({ 100 | required this.entryDataSourceImp, 101 | required this.cycleDate, 102 | }) { 103 | entryDataSourceImp 104 | .getAllEntryWithCategory(DateTimeUtil.getStartDateTime(cycleDate), 105 | DateTimeUtil.getEndDateTime(cycleDate)) 106 | .listen((event) { 107 | list = event 108 | ..sort((a, b) { 109 | if (a.total > b.total) { 110 | return -1; 111 | } else if (a.total < b.total) { 112 | return 1; 113 | } else { 114 | return 0; 115 | } 116 | }); 117 | notifyListeners(); 118 | }); 119 | } 120 | } 121 | 122 | final categoryListProvider = ChangeNotifierProvider((ref) { 123 | return CategoryModel(entryDataSourceImp: ref.read(repositoryProvider)); 124 | }); 125 | 126 | class CategoryModel with ChangeNotifier { 127 | EntryRepositoryImp entryDataSourceImp; 128 | List list = []; 129 | 130 | CategoryModel({required this.entryDataSourceImp}) { 131 | entryDataSourceImp.getAllCategory(EntryType.all).listen((event) { 132 | list = event; 133 | notifyListeners(); 134 | }); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /lib/data/datasource/local/moor/new_app_database.dart: -------------------------------------------------------------------------------- 1 | // import 'dart:io'; 2 | // 3 | // import 'package:drift/drift.dart'; 4 | // import 'package:drift/native.dart'; 5 | // import 'package:expense_manager/core/constants.dart'; 6 | // import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 | // import 'package:path/path.dart' as p; 8 | // import 'package:path_provider/path_provider.dart'; 9 | // 10 | // part 'new_app_database.g.dart'; 11 | // 12 | // class EntryEntity extends Table { 13 | // IntColumn get id => integer().autoIncrement()(); 14 | // 15 | // RealColumn get amount => real()(); 16 | // 17 | // IntColumn get categoryId => 18 | // integer().nullable().references(CategoryEntity, #id)(); 19 | // 20 | // DateTimeColumn get modifiedDate => dateTime()(); 21 | // 22 | // TextColumn get description => text().withLength(max: 100)(); 23 | // } 24 | // 25 | // class CategoryEntity extends Table { 26 | // IntColumn get id => integer().autoIncrement()(); 27 | // 28 | // IntColumn get position => integer()(); 29 | // 30 | // TextColumn get name => text().withLength(min: 3, max: 20)(); 31 | // 32 | // TextColumn get icon => text().nullable()(); 33 | // 34 | // TextColumn get iconColor => text()(); 35 | // } 36 | // 37 | // class IncomeEntryEntity extends Table { 38 | // IntColumn get id => integer().autoIncrement()(); 39 | // 40 | // RealColumn get amount => real()(); 41 | // 42 | // IntColumn get categoryId => 43 | // integer().nullable().references(CategoryEntity, #id)(); 44 | // 45 | // // integer().nullable().customConstraint( 46 | // // 'NULL REFERENCES income_category_entity(id) ON DELETE SET NULL')(); 47 | // 48 | // DateTimeColumn get modifiedDate => dateTime()(); 49 | // 50 | // TextColumn get description => text().withLength(max: 100)(); 51 | // } 52 | // 53 | // class IncomeCategoryEntity extends Table { 54 | // IntColumn get id => integer().autoIncrement()(); 55 | // 56 | // IntColumn get position => integer()(); 57 | // 58 | // TextColumn get name => text().withLength(min: 3, max: 20)(); 59 | // 60 | // TextColumn get icon => text().nullable()(); 61 | // 62 | // TextColumn get iconColor => text()(); 63 | // } 64 | // 65 | // final appDatabaseProvider = Provider((ref) => AppDatabase()); 66 | // 67 | // Future get databaseFile async { 68 | // // We use `path_provider` to find a suitable path to store our data in. 69 | // final appDir = await getApplicationDocumentsDirectory(); 70 | // final dbPath = p.join(appDir.path, 'todos.db'); 71 | // return File(dbPath); 72 | // } 73 | // 74 | // /// Obtains a database connection for running drift in a Dart VM. 75 | // DatabaseConnection connect() { 76 | // return DatabaseConnection.delayed(Future(() async { 77 | // return NativeDatabase.createBackgroundConnection(await databaseFile); 78 | // })); 79 | // } 80 | // 81 | // @DriftDatabase(tables: [ 82 | // EntryEntity, 83 | // CategoryEntity, 84 | // IncomeCategoryEntity, 85 | // IncomeEntryEntity 86 | // ]) 87 | // class AppDatabase extends _$AppDatabase { 88 | // // AppDatabase() 89 | // // : super((SqfliteQueryExecutor.inDatabaseFolder( 90 | // // path: 'db.sqlite', 91 | // // logStatements: true, 92 | // // ))); 93 | // 94 | // AppDatabase() : super(connect()); 95 | // 96 | // @override 97 | // int get schemaVersion => 3; 98 | // 99 | // @override 100 | // MigrationStrategy get migration { 101 | // return MigrationStrategy( 102 | // onCreate: (Migrator m) async { 103 | // await m.createAll(); 104 | // AppConstants.defaultCategoryList.forEach((category) async { 105 | // await addExpenseCategory(category.toCategoryEntityCompanion()).single; 106 | // }); 107 | // AppConstants.defaultIncomeCategoryList.forEach((category) async { 108 | // await addIncomeCategory(category.toIncomeCategoryEntityCompanion()) 109 | // .single; 110 | // }); 111 | // }, 112 | // beforeOpen: (details) async { 113 | // await customStatement('PRAGMA foreign_keys = ON'); 114 | // }, 115 | // onUpgrade: (Migrator m, int from, int to) async { 116 | // if (from == 1) { 117 | // // await m.createTable(incomeCategoryEntity); 118 | // // await m.createTable(incomeEntryEntity); 119 | // // AppConstants.defaultIncomeCategoryList.forEach((category) async { 120 | // // await addIncomeCategory(category.toIncomeCategoryEntityCompanion()) 121 | // // .single; 122 | // // }); 123 | // } 124 | // }, 125 | // ); 126 | // } 127 | // 128 | // Stream addExpenseCategory(CategoryEntityCompanion category) => customInsert( 129 | // "INSERT INTO category_entity (position, name, icon, icon_color) VALUES ((SELECT IFNULL(MAX(position), 0) + 1 FROM category_entity), '${category.name.value}', '${category.icon.value}', '${category.iconColor.value}');", 130 | // updates: {categoryEntity}).asStream(); 131 | // 132 | // Stream addIncomeCategory(IncomeCategoryEntityCompanion incomeCategory) => 133 | // customInsert( 134 | // "INSERT INTO income_category_entity (position, name, icon, icon_color) VALUES ((SELECT IFNULL(MAX(position), 0) + 1 FROM income_category_entity), '${incomeCategory.name.value}', '${incomeCategory.icon.value}', '${incomeCategory.iconColor.value}');", 135 | // updates: {incomeCategoryEntity}).asStream(); 136 | // 137 | // Stream addExpenseEntry(EntryEntityCompanion entity) => 138 | // into(entryEntity).insert(entity).asStream(); 139 | // 140 | // Stream addIncomeEntry(IncomeEntryEntityCompanion entity) { 141 | // return into(incomeEntryEntity).insert(entity).asStream(); 142 | // } 143 | // 144 | // Stream updateExpenseEntry(EntryEntityCompanion entity) { 145 | // return update(entryEntity).replace(entity).asStream(); 146 | // } 147 | // 148 | // Stream updateIncomeEntry(IncomeEntryEntityCompanion entity) { 149 | // return update(incomeEntryEntity).replace(entity).asStream(); 150 | // } 151 | // 152 | // Stream deleteExpenseEntry(int id) => 153 | // (delete(entryEntity)..where((tbl) => tbl.id.equals(id))).go().asStream(); 154 | // 155 | // Stream deleteIncomeEntry(int id) => 156 | // (delete(incomeEntryEntity)..where((tbl) => tbl.id.equals(id))) 157 | // .go() 158 | // .asStream(); 159 | // 160 | // 161 | // Stream> getExpenseYearList() { 162 | // return (selectOnly(entryEntity, distinct: true) 163 | // ..addColumns([entryEntity.modifiedDate.year])) 164 | // .map((row) => row.read(entryEntity.modifiedDate.year)) 165 | // .watch(); 166 | // } 167 | // 168 | // Stream> getIncomeYearList() { 169 | // return (selectOnly(incomeEntryEntity, distinct: true) 170 | // ..addColumns([incomeEntryEntity.modifiedDate.year])) 171 | // .map((row) => row.read(incomeEntryEntity.modifiedDate.year)) 172 | // .watch(); 173 | // } 174 | // } 175 | -------------------------------------------------------------------------------- /lib/data/repository/entry_repository_imp.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:expense_manager/core/constants.dart'; 3 | import 'package:expense_manager/data/datasource/local/entry_datasource_imp.dart'; 4 | import 'package:expense_manager/data/models/category.dart'; 5 | import 'package:expense_manager/data/models/category_with_entry_list.dart'; 6 | import 'package:expense_manager/data/models/category_with_sum.dart'; 7 | import 'package:expense_manager/data/models/entry.dart'; 8 | import 'package:expense_manager/data/models/entry_with_category.dart'; 9 | import 'package:expense_manager/data/models/history.dart'; 10 | import 'package:expense_manager/data/repository/entry_repository.dart'; 11 | import 'package:expense_manager/extension/list_extension.dart'; 12 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 13 | import 'package:tuple/tuple.dart'; 14 | 15 | final repositoryProvider = Provider((ref) => 16 | EntryRepositoryImp(entryDataSourceImp: ref.read(dataSourceProvider))); 17 | 18 | class EntryRepositoryImp extends EntryRepository { 19 | EntryDataSourceImp entryDataSourceImp; 20 | 21 | EntryRepositoryImp({required this.entryDataSourceImp}); 22 | 23 | @override 24 | Stream> getMonthListByYear(EntryType entryType, int year) { 25 | switch (entryType) { 26 | case EntryType.expense: 27 | return entryDataSourceImp.getExpenseMonthListByYear(year); 28 | case EntryType.income: 29 | return entryDataSourceImp.getIncomeMonthListByYear(year); 30 | default: 31 | return entryDataSourceImp.getAllMonthListByYear(year); 32 | } 33 | } 34 | 35 | @override 36 | Stream> getYearList(EntryType entryType) { 37 | switch (entryType) { 38 | case EntryType.expense: 39 | return entryDataSourceImp 40 | .getExpenseYearList() 41 | .map((event) => event.whereNotNull().toList()); 42 | case EntryType.income: 43 | return entryDataSourceImp 44 | .getIncomeYearList() 45 | .map((event) => event.whereNotNull().toList()); 46 | default: 47 | return entryDataSourceImp.getAllYearList(); 48 | } 49 | } 50 | 51 | @override 52 | Stream addEntry(EntryType entryType, Entry entry) { 53 | if (entryType == EntryType.expense) { 54 | return entryDataSourceImp.addExpenseEntry(entry); 55 | } else { 56 | return entryDataSourceImp.addIncomeEntry(entry); 57 | } 58 | } 59 | 60 | @override 61 | Stream updateEntry(EntryType entryType, Entry entry) { 62 | if (entryType == EntryType.expense) { 63 | return entryDataSourceImp.updateExpenseEntry(entry); 64 | } else { 65 | return entryDataSourceImp.updateIncomeEntry(entry); 66 | } 67 | } 68 | 69 | @override 70 | Stream deleteEntry(EntryType entryType, int id) { 71 | if (entryType == EntryType.expense) { 72 | return entryDataSourceImp.deleteExpenseEntry(id); 73 | } else { 74 | return entryDataSourceImp.deleteIncomeEntry(id); 75 | } 76 | } 77 | 78 | @override 79 | Stream> getAllEntryWithCategory( 80 | DateTime start, DateTime end) { 81 | return entryDataSourceImp.getAllEntryWithCategory(start, end); 82 | } 83 | 84 | @override 85 | Stream getExpanseSumByDateRange(DateTime start, DateTime end) { 86 | return entryDataSourceImp.getExpanseSumByDateRange(start, end); 87 | } 88 | 89 | @override 90 | Stream getIncomeSumByDateRange(DateTime start, DateTime end) { 91 | return entryDataSourceImp.getIncomeSumByDateRange(start, end); 92 | } 93 | 94 | @override 95 | Stream getTodayExpense() { 96 | return entryDataSourceImp.getTodayExpense(); 97 | } 98 | 99 | @override 100 | Stream> getAllEntryWithCategoryDateWiseByMonthAndYear( 101 | EntryType entryType, int month, int year) { 102 | switch (entryType) { 103 | case EntryType.expense: 104 | return entryDataSourceImp 105 | .getExpenseEntryWithCategoryDateWiseByMonthAndYear(month, year); 106 | case EntryType.income: 107 | return entryDataSourceImp 108 | .getIncomeEntryWithCategoryDateWiseByMonthAndYear(month, year); 109 | default: 110 | return entryDataSourceImp.getAllEntryWithCategoryDateWiseByMonthAndYear( 111 | month, year); 112 | } 113 | } 114 | 115 | @override 116 | Stream addCategory(EntryType entryType, Category category) { 117 | if (entryType == EntryType.expense) { 118 | return entryDataSourceImp.addExpenseCategory(category); 119 | } else { 120 | return entryDataSourceImp.addIncomeCategory(category); 121 | } 122 | } 123 | 124 | @override 125 | Stream updateCategory(EntryType entryType, Category category) { 126 | if (entryType == EntryType.expense) { 127 | return entryDataSourceImp.updateExpenseCategory(category); 128 | } else { 129 | return entryDataSourceImp.updateIncomeCategory(category); 130 | } 131 | } 132 | 133 | @override 134 | Stream deleteCategory(EntryType entryType, int id) { 135 | if (entryType == EntryType.expense) { 136 | return entryDataSourceImp.deleteExpenseCategory(id); 137 | } else { 138 | return entryDataSourceImp.deleteIncomeCategory(id); 139 | } 140 | } 141 | 142 | @override 143 | Stream reorderCategory(int oldIndex, int newIndex) { 144 | return entryDataSourceImp.reorderCategory(oldIndex, newIndex); 145 | } 146 | 147 | @override 148 | Stream> getAllCategory(EntryType entryType) { 149 | switch (entryType) { 150 | case EntryType.expense: 151 | return entryDataSourceImp.getAllExpenseCategory(); 152 | case EntryType.income: 153 | return entryDataSourceImp.getAllIncomeCategory(); 154 | default: 155 | return entryDataSourceImp.getAllCategory(); 156 | } 157 | } 158 | 159 | @override 160 | Stream> getCategoryDetails( 161 | Tuple2 filterType) { 162 | if (filterType.item1 == "Month") 163 | return entryDataSourceImp.getAllCategoryWithSumByMonth( 164 | filterType.item2, DateTime.now().year); 165 | return entryDataSourceImp.getAllCategoryWithSumByYear(filterType.item2); 166 | } 167 | 168 | @override 169 | Stream, List>>> 170 | getAllEntryWithCategoryByYear(int year, int currentMonth) { 171 | return entryDataSourceImp.getAllEntryWithCategoryByYear(year).map( 172 | (event) { 173 | return event.groupListsByMonth(currentMonth).entries.map((e) { 174 | var list = e.value.groupListsBy((element) => element.entryType); 175 | return Tuple3, List>( 176 | e.key, 177 | list[EntryType.expense] ?? [], 178 | list[EntryType.income] ?? [], 179 | ); 180 | }).toList(); 181 | }, 182 | ); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /lib/core/constants.dart: -------------------------------------------------------------------------------- 1 | import 'package:expense_manager/data/models/category.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:tuple/tuple.dart'; 4 | 5 | enum EntryType { expense, income, all } 6 | 7 | enum QuarterlyType { Q1, Q2, Q3, Q4 } 8 | 9 | class AppConstants { 10 | static final defaultCategoryList = [ 11 | Category( 12 | name: "Food", 13 | icon: Icons.restaurant, 14 | iconColor: Color(0xFFc03c42), 15 | entryType: EntryType.expense, 16 | ), 17 | Category( 18 | name: "Health", 19 | icon: Icons.local_hospital_rounded, 20 | iconColor: Color(0xFF56717c), 21 | entryType: EntryType.expense, 22 | ), 23 | Category( 24 | name: "Shopping", 25 | icon: Icons.shopping_cart_rounded, 26 | iconColor: Color(0xFFfdbe0d), 27 | entryType: EntryType.expense, 28 | ), 29 | Category( 30 | name: "Transportation", 31 | icon: Icons.directions_bus_rounded, 32 | iconColor: Color(0xFF188976), 33 | entryType: EntryType.expense, 34 | ), 35 | Category( 36 | name: "Utilities", 37 | icon: Icons.build_rounded, 38 | iconColor: Color(0xFFAB3A66), 39 | entryType: EntryType.expense, 40 | ), 41 | ]; 42 | 43 | static final defaultIncomeCategoryList = [ 44 | Category( 45 | name: "Salary", 46 | icon: Icons.attach_money_outlined, 47 | iconColor: Color(0xFFc03c42), 48 | entryType: EntryType.income, 49 | ), 50 | Category( 51 | name: "Allowance", 52 | icon: Icons.security_outlined, 53 | iconColor: Color(0xFF84C03C), 54 | entryType: EntryType.income, 55 | ), 56 | Category( 57 | name: "Bonus", 58 | icon: Icons.sentiment_very_satisfied_outlined, 59 | iconColor: Color(0xFF3CC0BA), 60 | entryType: EntryType.income, 61 | ), 62 | Category( 63 | name: "Petty Cash", 64 | icon: Icons.money_outlined, 65 | iconColor: Color(0xFF783CC0), 66 | entryType: EntryType.income, 67 | ), 68 | ]; 69 | 70 | static final otherCategory = Category( 71 | name: "Other", 72 | icon: Icons.ac_unit_rounded, 73 | iconColor: Color(0xFF798897), 74 | entryType: EntryType.all, 75 | ); 76 | 77 | static final monthList = { 78 | 1: "jan", 79 | 2: "feb", 80 | 3: "mar", 81 | 4: "apr", 82 | 5: "may", 83 | 6: "jun", 84 | 7: "jul", 85 | 8: "aug", 86 | 9: "sep", 87 | 10: "oct", 88 | 11: "nov", 89 | 12: "dec" 90 | }; 91 | 92 | static final quarterlyMonth = { 93 | QuarterlyType.Q1: [1, 2, 3], 94 | QuarterlyType.Q2: [4, 5, 6], 95 | QuarterlyType.Q3: [7, 8, 9], 96 | QuarterlyType.Q4: [10, 11, 12], 97 | }; 98 | 99 | static final currencyList = [ 100 | Tuple2("en", "Dollar"), 101 | Tuple2("eu", "Euro"), 102 | Tuple2("cy", "Pound"), 103 | Tuple2("ja", "Yen"), 104 | Tuple2("en_IN", "Rupee"), 105 | ]; 106 | 107 | static final expenseIconList = [ 108 | Icons.fastfood_rounded, 109 | Icons.local_cafe_rounded, 110 | Icons.local_dining_rounded, 111 | Icons.restaurant_rounded, 112 | Icons.local_bar_rounded, 113 | Icons.lunch_dining, 114 | Icons.cake_rounded, 115 | Icons.directions_bus_rounded, 116 | Icons.directions_car_rounded, 117 | Icons.local_shipping_rounded, 118 | Icons.two_wheeler_rounded, 119 | Icons.train_rounded, 120 | Icons.agriculture_rounded, 121 | Icons.build_rounded, 122 | Icons.local_movies_rounded, 123 | Icons.audiotrack_rounded, 124 | Icons.shopping_cart_rounded, 125 | Icons.laptop_chromebook_rounded, 126 | Icons.local_hospital_rounded, 127 | Icons.home_rounded, 128 | Icons.flight_rounded, 129 | Icons.phone_android_rounded, 130 | Icons.school_rounded, 131 | Icons.smoking_rooms_rounded, 132 | Icons.add_circle_rounded, 133 | Icons.receipt_long_rounded, 134 | Icons.payment_rounded, 135 | Icons.attach_money_rounded, 136 | Icons.trending_up_rounded, 137 | Icons.widgets_rounded, 138 | Icons.security_rounded, 139 | Icons.toys_rounded, 140 | Icons.local_gas_station_rounded, 141 | Icons.hotel_rounded, 142 | Icons.electrical_services_rounded, 143 | Icons.festival, 144 | Icons.local_laundry_service_rounded, 145 | Icons.local_library_rounded, 146 | Icons.child_care_rounded, 147 | Icons.fitness_center_rounded, 148 | Icons.sports_esports_rounded, 149 | Icons.add_moderator, 150 | Icons.science_rounded, 151 | Icons.eco_rounded, 152 | Icons.public_rounded, 153 | ]; 154 | 155 | static final expenseIconColorList = [ 156 | Color(0xFFc03c42), 157 | Color(0xFFF44336), 158 | Color(0xFFE91E63), 159 | Color(0xFF9C27B0), 160 | Color(0xFF673AB7), 161 | Color(0xFF3F51B5), 162 | Color(0xFF2196F3), 163 | Color(0xFF03A9F4), 164 | Color(0xFF00BCD4), 165 | Color(0xFF009688), 166 | Color(0xFF4CAF50), 167 | Color(0xFF8BC34A), 168 | Color(0xFFCDDC39), 169 | Color(0xFFFFEB3B), 170 | Color(0xFFFFC107), 171 | Color(0xFFFF9800), 172 | Color(0xFF9E9E9E), 173 | Color(0xFF607D8B), 174 | Color(0xFF86447C), 175 | Color(0xFF5B4C7E), 176 | Color(0xFF2F4858), 177 | Color(0xFFB9538E), 178 | ]; 179 | 180 | static final incomeIconList = [ 181 | Icons.fastfood_rounded, 182 | Icons.local_cafe_rounded, 183 | Icons.local_dining_rounded, 184 | Icons.restaurant_rounded, 185 | Icons.local_bar_rounded, 186 | Icons.lunch_dining, 187 | Icons.cake_rounded, 188 | // Icons.directions_bus_rounded, 189 | // Icons.directions_car_rounded, 190 | // Icons.local_shipping_rounded, 191 | // Icons.two_wheeler_rounded, 192 | // Icons.train_rounded, 193 | // Icons.agriculture_rounded, 194 | // Icons.build_rounded, 195 | // Icons.local_movies_rounded, 196 | // Icons.audiotrack_rounded, 197 | // Icons.shopping_cart_rounded, 198 | // Icons.laptop_chromebook_rounded, 199 | // Icons.local_hospital_rounded, 200 | // Icons.home_rounded, 201 | // Icons.flight_rounded, 202 | // Icons.phone_android_rounded, 203 | // Icons.school_rounded, 204 | // Icons.smoking_rooms_rounded, 205 | // Icons.add_circle_rounded, 206 | // Icons.receipt_long_rounded, 207 | // Icons.payment_rounded, 208 | // Icons.attach_money_rounded, 209 | // Icons.trending_up_rounded, 210 | // Icons.widgets_rounded, 211 | // Icons.security_rounded, 212 | // Icons.toys_rounded, 213 | // Icons.local_gas_station_rounded, 214 | // Icons.hotel_rounded, 215 | // Icons.electrical_services_rounded, 216 | // Icons.festival, 217 | // Icons.local_laundry_service_rounded, 218 | // Icons.local_library_rounded, 219 | // Icons.child_care_rounded, 220 | // Icons.fitness_center_rounded, 221 | // Icons.sports_esports_rounded, 222 | // Icons.add_moderator, 223 | // Icons.science_rounded, 224 | // Icons.eco_rounded, 225 | // Icons.public_rounded, 226 | ]; 227 | 228 | static final incomeIconColorList = [ 229 | Color(0xFFc03c42), 230 | Color(0xFFF44336), 231 | Color(0xFFE91E63), 232 | Color(0xFF9C27B0), 233 | Color(0xFF673AB7), 234 | // Color(0xFF3F51B5), 235 | // Color(0xFF2196F3), 236 | // Color(0xFF03A9F4), 237 | // Color(0xFF00BCD4), 238 | // Color(0xFF009688), 239 | // Color(0xFF4CAF50), 240 | // Color(0xFF8BC34A), 241 | // Color(0xFFCDDC39), 242 | // Color(0xFFFFEB3B), 243 | // Color(0xFFFFC107), 244 | // Color(0xFFFF9800), 245 | // Color(0xFF9E9E9E), 246 | // Color(0xFF607D8B), 247 | // Color(0xFF86447C), 248 | // Color(0xFF5B4C7E), 249 | // Color(0xFF2F4858), 250 | // Color(0xFFB9538E), 251 | ]; 252 | } 253 | -------------------------------------------------------------------------------- /lib/ui/category_list/category_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:dotted_border/dotted_border.dart'; 2 | import 'package:expense_manager/core/constants.dart'; 3 | import 'package:expense_manager/core/routes.dart'; 4 | import 'package:expense_manager/core/app_localization.dart'; 5 | import 'package:expense_manager/ui/category_list/category_list_state.dart'; 6 | import 'package:fimber/fimber.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 9 | import 'package:tuple/tuple.dart'; 10 | 11 | class CategoryList extends ConsumerWidget { 12 | final EntryType entryType; 13 | 14 | CategoryList({required this.entryType}) : super(); 15 | 16 | @override 17 | Widget build(BuildContext context, WidgetRef ref) { 18 | final vm = ref.watch(categoryListModelProvider(entryType)); 19 | return Scaffold( 20 | appBar: AppBar( 21 | leading: InkWell( 22 | onTap: () { 23 | Navigator.pop(context); 24 | }, 25 | child: Icon(Icons.arrow_back_ios_rounded)), 26 | title: DottedBorder( 27 | color: Theme.of(context).appBarTheme.titleTextStyle!.color!, 28 | dashPattern: [5, 5], 29 | radius: Radius.circular(12), 30 | borderType: BorderType.RRect, 31 | child: Padding( 32 | padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 33 | child: Text( 34 | AppLocalization.of(context).getTranslatedVal("category_list"), 35 | style: Theme.of(context).appBarTheme.titleTextStyle, 36 | ), 37 | ), 38 | ), 39 | actions: [ 40 | InkWell( 41 | onTap: () { 42 | Navigator.pushNamed( 43 | context, 44 | AppRoutes.addCategory, 45 | arguments: Tuple2(vm.entryType!, null), 46 | ); 47 | }, 48 | borderRadius: BorderRadius.circular(20), 49 | child: Padding( 50 | padding: const EdgeInsets.all(20), 51 | child: Text( 52 | AppLocalization.of(context).getTranslatedVal("add_new"), 53 | style: Theme.of(context).textTheme.titleSmall!.copyWith( 54 | fontWeight: FontWeight.bold, color: Color(0xff2196F3)), 55 | ), 56 | ), 57 | ), 58 | ], 59 | ), 60 | body: Column( 61 | children: [ 62 | SizedBox(height: 16), 63 | Padding( 64 | padding: const EdgeInsets.symmetric(horizontal: 24), 65 | child: Row( 66 | children: [ 67 | Expanded( 68 | child: InkWell( 69 | onTap: () { 70 | vm.entryTypeChange(EntryType.expense); 71 | }, 72 | child: Card( 73 | shape: RoundedRectangleBorder( 74 | borderRadius: BorderRadius.circular(4), 75 | side: vm.entryType == EntryType.expense 76 | ? BorderSide( 77 | width: 1, 78 | color: Color(0xff2196F3), 79 | ) 80 | : BorderSide.none, 81 | ), 82 | child: Padding( 83 | padding: const EdgeInsets.all(12), 84 | child: Text( 85 | AppLocalization.of(context) 86 | .getTranslatedVal("expense"), 87 | textAlign: TextAlign.center, 88 | style: Theme.of(context).textTheme.bodySmall, 89 | ), 90 | ), 91 | ), 92 | ), 93 | ), 94 | Expanded( 95 | child: InkWell( 96 | onTap: () { 97 | vm.entryTypeChange(EntryType.income); 98 | }, 99 | child: Card( 100 | shape: RoundedRectangleBorder( 101 | borderRadius: BorderRadius.circular(4), 102 | side: vm.entryType == EntryType.income 103 | ? BorderSide( 104 | width: 1, 105 | color: Color(0xff2196F3), 106 | ) 107 | : BorderSide.none, 108 | ), 109 | child: Padding( 110 | padding: const EdgeInsets.all(12), 111 | child: Text( 112 | AppLocalization.of(context) 113 | .getTranslatedVal("income"), 114 | textAlign: TextAlign.center, 115 | style: Theme.of(context).textTheme.bodySmall, 116 | ), 117 | ), 118 | ), 119 | ), 120 | ), 121 | ], 122 | ), 123 | ), 124 | Expanded( 125 | child: /*Reorderable*/ ListView( 126 | // onReorder: vm.reorder, 127 | padding: const EdgeInsets.only(top: 16), 128 | children: (vm.entryType == EntryType.expense 129 | ? vm.expenseCategoryList 130 | : vm.incomeCategoryList) 131 | .map((e) => InkWell( 132 | key: ValueKey(e.id), 133 | onTap: () { 134 | Navigator.pushNamed( 135 | context, 136 | AppRoutes.addCategory, 137 | arguments: Tuple2(vm.entryType!, e), 138 | ); 139 | }, 140 | child: Padding( 141 | padding: const EdgeInsets.symmetric( 142 | horizontal: 24, vertical: 14), 143 | child: Row( 144 | children: [ 145 | Icon( 146 | e.icon, 147 | size: 20, 148 | color: e.iconColor, 149 | ), 150 | Expanded( 151 | child: Row( 152 | mainAxisAlignment: 153 | MainAxisAlignment.spaceBetween, 154 | children: [ 155 | Padding( 156 | padding: const EdgeInsets.symmetric( 157 | horizontal: 16), 158 | child: Text( 159 | e.name, 160 | style: 161 | Theme.of(context).textTheme.titleSmall, 162 | ), 163 | ), 164 | /*Icon( 165 | Icons.drag_handle_outlined, 166 | size: 20, 167 | )*/ 168 | ], 169 | ), 170 | ), 171 | ], 172 | ), 173 | ), 174 | )) 175 | .toList(), 176 | )) 177 | ], 178 | ), 179 | ); 180 | } 181 | } 182 | --------------------------------------------------------------------------------