138 Commits

Author SHA1 Message Date
70c26be8e3 Stop showing errors if the default exists and all options have already been covered. 2025-03-27 15:17:14 -07:00
b177b6317e Adjust settings page formatting and description text. 2025-03-27 15:15:33 -07:00
3c52239efc Add header to Settings page. 2025-03-27 15:14:10 -07:00
de4f8306d7 Remove TODO note. 2025-03-27 15:12:31 -07:00
f77cf7bd38 Refactor to use value of variable. 2025-03-27 15:12:23 -07:00
716d40c694 Remove unneeded dependency. 2025-03-27 15:12:07 -07:00
d9f8536f26 Use a consistent header on each page. 2025-03-27 14:48:07 -07:00
1966f72c93 Add item descriptions to Help information and clarify the updating section. 2025-03-27 14:39:52 -07:00
e896611bd1 Refactor to add header to the Tracked Item pages. 2025-03-27 14:31:34 -07:00
2d9c93fec4 Fix typo. 2025-03-27 14:26:51 -07:00
c5f1a4e9ba Add app version to menu. 2025-03-27 14:02:18 -07:00
538a298acd Made the input dialog fully item type aware. Fixes #1, now all item types can have an item of the same name (such as "Test") rather than only checking the Expense table for the name. 2025-03-27 12:22:57 -07:00
cce878ccaa Made the input dialog fully item type aware. Fixes issue #1, now all item types can have an item of the same name (such as "Test") rather than only checking the Expense table for the name. 2025-03-27 12:15:55 -07:00
147178e4dd Fix the projections not loading if any item type was left unused. 2025-03-27 11:58:26 -07:00
c39e09b2b6 Increment the version. 2025-03-27 11:57:22 -07:00
fa852faadc Use the dispose() method to reset the global values. Works excellently!! :D 2025-03-26 13:17:08 -07:00
6f9d0d8afb The initial page load now delays properly and shows the correct value rather than 0. Updating an item and going back to the page still shows old data though. 2025-03-26 12:39:09 -07:00
75cc72678b Add TBD of where the Income duplicate name validator is going wrong. 2025-03-25 08:45:44 -07:00
2970431b91 Increment the version. 2025-03-21 15:03:16 -07:00
4d0dc03c69 Create script which builds the delverables and places them in a convenient place with convenient names. 2025-03-21 14:58:56 -07:00
bdb3fc5c7a Ensure the README is up to date. Reword slightly. 2025-03-21 14:22:14 -07:00
da2ae6206a Add additional Help content and modify the Code Repo button to more specifically be Source Code for the project. 2025-03-21 14:21:43 -07:00
f5b22c6c1c Create and use a better app icon. 2025-03-21 14:08:26 -07:00
9f7773f724 Add functionality to Help buttons for opening URLs. 2025-03-21 13:40:11 -07:00
dec343af09 Add plugin url_launcher for Help page. 2025-03-21 12:17:10 -07:00
0890e15bfb Change the double SafeArea for Linux. 2025-03-21 12:05:59 -07:00
55fd4092ac Prevent the Help buttons from being underneath the Nav Bar on Android. 2025-03-21 12:05:03 -07:00
247b88daa2 Replace placeholder with a basic message for now. 2025-03-21 12:04:48 -07:00
d604a59ad1 Accept a recommended change since null is now handled properly. 2025-03-21 11:39:23 -07:00
5bf6a0889c Finish changing the database name. 2025-03-21 11:38:50 -07:00
f3d6b8abbe Increment the app version slightly now that it's working and has an icon. 2025-03-21 11:03:02 -07:00
8ea85586d5 Begin using the old project's icon. 2025-03-21 11:02:57 -07:00
d1633fd155 Add media from android-expense-tracker project. 2025-03-21 10:54:29 -07:00
26718a41e0 Shorten app name for home screen. 2025-03-21 10:54:14 -07:00
2c241d9113 Change database file name to be more user-friendly. 2025-03-21 10:53:48 -07:00
adea1eeb02 Use the non-KTS files after all, the KTS versions do not launch successfully. 2025-03-21 10:34:56 -07:00
f441caebf7 Unimplemented got thrown for null. Add it to the list so that lists with no items still work properly. 2025-03-21 10:34:25 -07:00
48615b3438 Hide another non-KTS gradle file to ensure that Android config is done properly. 2025-03-21 10:27:41 -07:00
bc3b3a4109 Ensure project is properly named across all files. 2025-03-21 10:24:46 -07:00
5ec246228e Ran flutter create . to create new files based on changed package name. 2025-03-21 09:32:45 -07:00
579a611bc2 Update packages. 2025-03-21 09:32:26 -07:00
62379f54b0 Pull the basic card types out into their own file for possible reuse. 2025-03-08 08:40:08 -07:00
e7dc369c4f Projections are working! Just 1 issue noticed so far. 2025-03-08 08:33:31 -07:00
15b713215f Add item count per category. 2025-03-08 07:51:23 -07:00
e57432767d Formatting improvement. 2025-03-08 07:41:59 -07:00
8b9e0bfe54 Assets are now being displayed properly. 2025-03-08 07:40:03 -07:00
a92cc00291 Flutter pub get updated more things. 2025-03-08 07:24:49 -07:00
b4aa72440d Futter changed this. 2025-03-08 06:57:47 -07:00
b8096833b3 Turn off testing / debug. 2025-03-08 06:57:26 -07:00
777d263efe Start loading incomes and assets for reporting. 2025-03-07 13:36:24 -07:00
84f1ec2f70 Add, update, and delete are all working for each item type!! 2025-03-07 13:32:11 -07:00
8d7591b766 Continue trying to fix issue with edited items not notifying parent. 2025-03-07 13:26:02 -07:00
88d57e217f Fix the Monthly text for assets now that it has its proper type. 2025-03-07 13:25:55 -07:00
7d08f6c5cf Add type to objects. 2025-03-07 13:24:17 -07:00
e5e85b68ff Income and Assets are successfully being inserted and selected! Still needs tweaking, such as Asset saying "monthly". 2025-03-07 13:06:26 -07:00
f6f167dd83 Fix local variable warning. 2025-03-07 12:56:14 -07:00
8ef8e0dad9 Start filling in TODO's with Income and Asset code.. 2025-03-07 12:55:34 -07:00
0b937186aa Add break statements, even if unnecessary. 2025-03-07 12:54:01 -07:00
a5e10e26cc Move testing to be a global variable and use it to control the debug output. 2025-03-07 12:47:40 -07:00
029ee8e9a8 Add DEBUG messages to get methods. Fix the income and asset tables not existing if db upgrade is not run. 2025-03-07 12:43:26 -07:00
7947d64b3b Allow the Income and Asset pages to start using the generic item creation page. 2025-03-07 12:41:52 -07:00
778b4e2f39 Add methods for accessing Incomes and Assets in the database. 2025-03-07 12:20:38 -07:00
02f08093c9 Fix formatting. 2025-03-07 08:37:26 -07:00
c2939f81c0 Fix whitespace. 2025-03-07 08:30:29 -07:00
8ae728f6bf Prevent new DEBUG folder from getting into git repo. Must have gotten created when compiling for Android emulator. 2025-02-26 10:06:09 -07:00
f6179d1fae Upgraded Flutter. 2025-02-26 09:53:09 -07:00
52fdd5c471 Fix username. 2025-02-26 09:52:59 -07:00
42548d437c Add ItemType for determining which DB methods should be called based on the screen data. Begin implementing it. 2025-02-21 09:30:46 -07:00
be66f52cbf Move down to only 1 abstract data type with a nully Frequency. Should aid in making the Expense page usable for all 3 data type. 2025-02-21 08:56:50 -07:00
992195b9a0 Style the report a little better. Still need to get the row's middle text properly aligned with the column header somehow, as if everything were in a grid. 2025-02-21 08:30:54 -07:00
991eea4d54 Add estimate tildes. 2025-02-21 08:10:38 -07:00
064884dc07 Fix code warnings by using Description variable and temporarily assuming that Assets are a RecurringTrackedType. 2025-02-19 17:40:43 -07:00
98e511056c Remove no longer necessary includes. 2025-02-19 17:26:28 -07:00
58102bab02 Add the expense totals! Yay! Still trying to center the totals part with the monthly part across all sections properly. 2025-02-19 17:25:36 -07:00
0b4937b141 Start creating the reports as a break from making the Expense page usable for all 3. Still considering whether to just have 1 main abstract class with a nully Frequency. 2025-02-14 08:49:44 -07:00
d35c250913 Add note for next steps in making the ListView more abstract. 2025-02-12 12:55:10 -07:00
21f67b8a36 Added a monthly runner. Moved field values to be in variables outside of the Widget being returned. 2025-02-12 12:53:34 -07:00
9924860181 Create the beginning of a DB backup system. 2025-02-12 08:22:16 -07:00
60363c29f2 Rename recurring class filename. Move around some libraries to be under the proper comments. 2025-02-12 07:55:22 -07:00
83dece8749 Add TODO's. 2025-02-12 07:52:05 -07:00
737264fa2f More work to make the Expense page arbitrary for re-use. 2025-02-11 15:16:46 -07:00
bea9a4bc36 Update TODO wording. 2025-02-11 14:52:57 -07:00
8e6b574023 Create the income and asset tables. 2025-02-11 14:49:50 -07:00
fcf4fca33b Add the Income and Asset classes. 2025-02-11 14:49:42 -07:00
0aa8fdf271 Properly map all fields (cost/amount). 2025-02-11 14:49:23 -07:00
3bf3dd3190 Fix order of cost validations to properly show error for character data. 2025-02-11 14:48:32 -07:00
ab9b3e0bf9 Begin making the Expense page more arbitrary for future re-use by creating superclasses which will fit all 3 object use cases. 2025-02-11 14:33:38 -07:00
7a3eaf70b5 Add TODO, format code. 2025-02-11 09:57:10 -07:00
f5f153f692 Turn off the database delete, schema is currently stable. 2025-02-11 09:53:15 -07:00
362f1214e8 Fix ability to edit an item. 2025-02-11 09:52:52 -07:00
d77e732551 Change desktop version to use the original style of navigation, keep using drawer for mobile devices. Flutter is so cool!! :) 2025-02-11 09:26:59 -07:00
cc33458457 Remove unused imports. 2025-02-11 09:25:50 -07:00
f5635d6120 Add unique name check to entry form. 2025-02-07 15:40:07 -07:00
ef58a06dfa Successfully migrate the application to SQLIte! 2025-02-07 15:29:20 -07:00
5425b22ba2 Add a Help page. 2025-02-07 10:53:29 -07:00
305012ffd4 Reset income page to just a placeholder for consistency. 2025-02-07 10:53:16 -07:00
8c31d868b9 Add a daily estimate. Move estimates to the right of the list item. 2025-02-07 10:24:20 -07:00
9d478b9cbf Fix bug which sometimes set an edited expense to Monthly. Fix typo in enum monthly. 2025-02-07 05:10:22 -07:00
452eb73773 Code formatting and dialog text enhancements. 2025-02-07 05:00:57 -07:00
631555af59 Add a dark theme. 2025-02-07 04:54:59 -07:00
0f65166123 Properly implement the dismissable backgrounds. 2025-02-07 04:41:53 -07:00
595aaefedc Added editing an expense. 2025-02-07 04:29:10 -07:00
9d8a5e6685 Add more frequencies. 2025-02-06 11:50:23 -07:00
2acabf4d3b Huge improvements to UI, very interactive now! 2025-02-06 11:50:10 -07:00
15fa4aadbd Add method which puts all expenses on equal comparison grounds. 2025-02-06 11:50:02 -07:00
78a407d0ec Always show the page names. 2025-02-06 11:48:51 -07:00
54cd86c34b Shorten the title. 2025-02-05 16:45:55 -07:00
66fd966de8 Redo how the frequencies are generated. Clean up old print statements. 2025-02-05 16:41:58 -07:00
5561f50736 Begin working on the database. 2025-02-05 14:43:06 -07:00
6b25e6e552 Add sqlite dependency with flutter pub add sqflite. 2025-02-05 14:17:29 -07:00
360a36f024 Fix the layout when there are no items. 2025-02-05 14:16:49 -07:00
ecbac615e9 Finished drawer and also now showing expenses as they are added! 2025-02-05 14:04:35 -07:00
87392cc73c Break code into multiple files. 2025-02-05 13:33:07 -07:00
1b95feb5d4 Forms is working, items show if hot reloaded, need to refactor code and get the global variable into the state of the expense page. 2025-02-05 13:20:04 -07:00
99b1ec82e6 Add the Form widget and get the fields working with it. Still need to pull the values into a usable object for saving. 2025-02-05 04:20:10 -07:00
7ead0e5ebb Format dialog a little better. 2025-02-05 03:39:12 -07:00
bcae40e0e2 Buttons now close the dialog properly. 2025-02-05 03:27:11 -07:00
aa3c2f9304 Expense input form is now in a popup! Still needs to look and function properly though. 2025-02-05 03:20:29 -07:00
96811f5bfd Make the name of the function more agnostic. 2025-02-01 16:51:12 -07:00
c44d4553c3 Attempting to move the input form to a popup. 2025-02-01 16:50:37 -07:00
d722172cb3 Add buttons and fake items. 2025-02-01 04:17:56 -07:00
43dd151cb4 Refactor the order of the classes based on the UI. 2025-02-01 03:54:53 -07:00
c2cc71eae0 Add widgets for each of the future pages. 2025-02-01 03:48:26 -07:00
0c9b365f7f Added navigation. Very nice!! ;D 2025-02-01 03:42:14 -07:00
f820265dba Found the correct place to set the title info on Linux. 2025-02-01 03:21:12 -07:00
85b7a0a3e6 Add reminder script for how to run with no Internet connection. 2025-02-01 03:20:44 -07:00
861e8bf904 Change project name and ID. 2025-02-01 03:15:01 -07:00
aaa95bd3a6 Break the form into its own widget, since eventually it'll likely be a pop-up. 2025-02-01 03:14:34 -07:00
45b5e33491 Add license. 2025-01-30 21:06:25 -07:00
8a2dfc2131 More messing around. 2025-01-30 20:56:01 -07:00
3b7313ca10 Made the UI fancier but still unable to get the controller to work. 2025-01-30 20:48:30 -07:00
b07a8f9c0a Add the enum properly. 2025-01-30 20:32:04 -07:00
19e10e6792 Test UI done enough, start working on how to get the input into an Expense. 2025-01-30 20:27:03 -07:00
811313dc2c Actually starting to look pretty good!? 2025-01-30 20:08:34 -07:00
26f688cfe9 Random files, maybe due to flutter run. 2025-01-30 20:01:05 -07:00
a4ad5823aa A lot of messing around until something actually finally sorta worked! 2025-01-30 20:00:46 -07:00
b227ddc849 Take the main file from an empty project. (flutter create -e). 2025-01-30 19:23:12 -07:00
92ebe3c4e0 Remove extra sentence. 2025-01-30 19:22:31 -07:00
61 changed files with 2478 additions and 330 deletions

6
.gitignore vendored
View File

@ -32,7 +32,6 @@ migrate_working_dir/
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
@ -43,3 +42,8 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
/android/app/.*
# Ignore the releases folder.
releases/*

View File

@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3"
revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf"
channel: "stable"
project_type: app
@ -13,26 +13,26 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: android
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: ios
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: linux
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: macos
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: web
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: windows
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
# User provided section

9
LICENSE Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2025 Hyperling
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,8 +1,6 @@
# flutter_expense_tracker
# Recurring Expense Tracker
Recurring expense tracker app for Android.
Recurring expense tracker app for Linux and Android.
Add a debit as daily, weekly, monthly, etc, then see how it affects your liquid
assets based on your reported income over different time projections.
Useful for many applications!
Add an expense as daily, weekly, monthly, etc, then see how it affects your
liquid assets based on your reported income over different time projections.

View File

@ -1,28 +1,4 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
analyzer:
errors:
unreachable_switch_default: ignore

View File

@ -6,7 +6,7 @@ plugins {
}
android {
namespace = "com.example.flutter_expense_tracker"
namespace = "com.hyperling.expense_tracker"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@ -21,7 +21,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.flutter_expense_tracker"
applicationId = "com.hyperling.expense_tracker"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion

View File

@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.hyperling.expense_tracker"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.hyperling.expense_tracker"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@ -1,8 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="flutter_expense_tracker"
android:label="Expenses"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/icon_v003"
android:roundIcon="@mipmap/icon_v003_round">
<activity
android:name=".MainActivity"
android:exported="true"
@ -41,5 +42,10 @@
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<!-- Allow opening of web URLs -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
</queries>
</manifest>

View File

@ -1,5 +0,0 @@
package com.example.flutter_expense_tracker
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

View File

@ -0,0 +1,5 @@
package com.hyperling.expense_tracker
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

21
android/build.gradle.kts Normal file
View File

@ -0,0 +1,21 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@ -0,0 +1,25 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
}
include(":app")

46
create_release_files.sh Executable file
View File

@ -0,0 +1,46 @@
#!/usr/bin/env bash
DIR="$(dirname -- "${BASH_SOURCE[0]}")"
RELEASE_DIR=$DIR/releases
echo "*** Prepare for Builds ***"
cd $DIR
pwd
mkdir -pv $RELEASE_DIR
echo "*** Get Version ***"
grep 'version:' pubspec.yaml | while read v_text v_number; do
# Skip any newlines found while grepping.
if [[ -z $v_number ]]; then
continue
fi
echo "Creating assets for version '$v_number'."
# Set Up Variables
ANDROID_APK="build/app/outputs/flutter-apk/app-release.apk"
APK_RENAME="$RELEASE_DIR/ExpenseTracker_$v_number.apk"
LINUX_BUNDLE="build/linux/x64/release/bundle"
BUNDLE_RENAME="LinuxBundle_$v_number"
# Build Android App
echo -e "\n*** Android APK ***"
rm -v "$APK_RENAME"
flutter build apk
mv -v $ANDROID_APK "$APK_RENAME"
ls -sh "$APK_RENAME"
# Build Linux Project
echo -e "\n*** Linux Bundle ***"
rm -rv "$RELEASE_DIR/$BUNDLE_RENAME"*
flutter build linux
mv -v $LINUX_BUNDLE "$RELEASE_DIR/$BUNDLE_RENAME"
cd $RELEASE_DIR
zip -r $BUNDLE_RENAME.zip $BUNDLE_RENAME
rm -rv $BUNDLE_RENAME
ls -sh $BUNDLE_RENAME.zip
# Only one version should be found, but just in case, only use the top one!
break
done

View File

@ -368,7 +368,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@ -384,7 +384,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -401,7 +401,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -416,7 +416,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -547,7 +547,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -569,7 +569,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Flutter Expense Tracker</string>
<string>Flutter Empty</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>flutter_expense_tracker</string>
<string>flutter_empty</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>

257
lib/db.dart Normal file
View File

@ -0,0 +1,257 @@
// https://docs.flutter.dev/cookbook/persistence/sqlite
// SQLite
import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
// Local
import "/globals.dart";
import '/models/expense.dart';
import '/models/income.dart';
import '/models/asset.dart';
import '/models/tracked_item.dart';
// Leaned on this example:
// https://learnflutterwithme.com/sqlite
class DatabaseHelper {
DatabaseHelper._privateConstructor();
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
static Database? _db;
Future<Database> get db async => _db ??= await _initDatabase();
Future<Database> _initDatabase() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, "ExpenseTracker.sqlite.db");
return await openDatabase(
path,
version: 2,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
Future _onCreate(Database db, int version) async {
debugPrint(
"_onCreate(): version=$version",
);
_createExpense(db);
_createIncome(db);
_createAsset(db);
}
Future _onUpgrade(Database db, int previousVersion, int newVersion) async {
debugPrint(
"_onUpgrade(): previousVersion=$previousVersion, newVersion=$newVersion",
);
// Added in DB version 2.
if (previousVersion < 2 && newVersion >= 2) {
_createIncome(db);
_createAsset(db);
}
}
void _createExpense(Database db) async {
await db.execute("""
CREATE TABLE expense
( id INTEGER PRIMARY KEY
, name TEXT NOT NULL UNIQUE
, cost DOUBLE NOT NULL
, frequency TEXT NOT NULL
, description TEXT
)""");
}
void _createIncome(Database db) async {
await db.execute("""
CREATE TABLE income
( id INTEGER PRIMARY KEY
, name TEXT NOT NULL UNIQUE
, revenue DOUBLE NOT NULL
, frequency TEXT NOT NULL
, description TEXT
)""");
}
void _createAsset(Database db) async {
await db.execute("""
CREATE TABLE asset
( id INTEGER PRIMARY KEY
, name TEXT NOT NULL UNIQUE
, amount DOUBLE NOT NULL
, description TEXT
)""");
}
/// Expense Section
///
Future<List<Expense>> getExpenses() async {
if (testing) debugPrint("getExpenses(): Accessing db.");
Database db = await instance.db;
if (testing) debugPrint("getExpenses(): Querying table.");
var expenses = await db.query("expense", orderBy: "name");
if (testing) debugPrint("getExpenses(): Putting into object list.");
List<Expense> expenseList = expenses.isNotEmpty
? expenses.map((c) => Expense.fromMap(c)).toList()
: [];
if (testing) debugPrint("getExpenses(): Returning!");
return expenseList;
}
Future<int> addExpense(TrackedItem expense) async {
Database db = await instance.db;
return await db.insert(
"expense",
expense.toMap(),
);
}
Future<int> removeExpense(int id) async {
Database db = await instance.db;
return await db.delete(
"expense",
where: "id = ?",
whereArgs: [id],
);
}
Future<int> updateExpense(TrackedItem expense) async {
Database db = await instance.db;
return await db.update(
"expense",
expense.toMap(),
where: "id = ?",
whereArgs: [expense.id],
);
}
Future<bool> checkExpenseNameExists(String name) async {
Database db = await instance.db;
var expenses = await db.query(
"expense",
where: "name = ?",
whereArgs: [name],
);
return expenses.isNotEmpty;
}
///
/// Income Section
///
Future<List<Income>> getIncomes() async {
if (testing) debugPrint("getIncomes(): Accessing db.");
Database db = await instance.db;
if (testing) debugPrint("getIncomes(): Querying table.");
var incomes = await db.query("income", orderBy: "name");
if (testing) debugPrint("getIncomes(): Putting into object list.");
List<Income> incomeList = incomes.isNotEmpty
? incomes.map((c) => Income.fromMap(c)).toList()
: [];
if (testing) debugPrint("getIncomes(): Returning!");
return incomeList;
}
Future<int> addIncome(TrackedItem income) async {
Database db = await instance.db;
return await db.insert(
"income",
income.toMap(),
);
}
Future<int> removeIncome(int id) async {
Database db = await instance.db;
return await db.delete(
"income",
where: "id = ?",
whereArgs: [id],
);
}
Future<int> updateIncome(TrackedItem income) async {
Database db = await instance.db;
return await db.update(
"income",
income.toMap(),
where: "id = ?",
whereArgs: [income.id],
);
}
Future<bool> checkIncomeNameExists(String name) async {
Database db = await instance.db;
var incomes = await db.query(
"income",
where: "name = ?",
whereArgs: [name],
);
return incomes.isNotEmpty;
}
///
/// Liquid Asset Section
Future<List<Asset>> getAssets() async {
if (testing) debugPrint("getAssets(): Accessing db.");
Database db = await instance.db;
if (testing) debugPrint("getAssets(): Querying table.");
var assets = await db.query("asset", orderBy: "name");
if (testing) debugPrint("getAssets(): Putting into object list.");
List<Asset> assetList =
assets.isNotEmpty ? assets.map((c) => Asset.fromMap(c)).toList() : [];
if (testing) debugPrint("getAssets(): Returning!");
return assetList;
}
Future<int> addAsset(TrackedItem asset) async {
Database db = await instance.db;
return await db.insert(
"asset",
asset.toMap(),
);
}
Future<int> removeAsset(int id) async {
Database db = await instance.db;
return await db.delete(
"asset",
where: "id = ?",
whereArgs: [id],
);
}
Future<int> updateAsset(TrackedItem asset) async {
Database db = await instance.db;
return await db.update(
"asset",
asset.toMap(),
where: "id = ?",
whereArgs: [asset.id],
);
}
Future<bool> checkAssetNameExists(String name) async {
Database db = await instance.db;
var assets = await db.query(
"asset",
where: "name = ?",
whereArgs: [name],
);
return assets.isNotEmpty;
}
///
}

1
lib/globals.dart Normal file
View File

@ -0,0 +1 @@
const bool testing = false;

View File

@ -1,125 +1,58 @@
// Flutter
import 'package:flutter/material.dart';
// Local
import '/globals.dart';
import '/pages/home.dart';
// SQLite
import 'dart:io';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
void main() {
runApp(const MyApp());
// I see no good explanations of why to use this other package yet, but
// trying this to see if it fixes the DB factory errors.
// "Unhandled Exception: Bad state: databaseFactory not initialized databaseFactory is only initialized when using sqflite. When using `sqflite_common_ffi`You must call `databaseFactory = databaseFactoryFfi;` before using global openDatabase API
// https://stackoverflow.com/questions/76158800/databasefactory-not-initialized-when-using-sqflite-in-flutter
if (Platform.isWindows || Platform.isLinux) {
// Initialize FFI
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
}
WidgetsFlutterBinding.ensureInitialized();
// Remove the DB and recreate it to test the Database Helper multiple times.
if (testing) {
() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, 'ExpenseTracker.sqlite.db');
await deleteDatabase(path);
}();
}
runApp(const MainApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
class MainApp extends StatelessWidget {
const MainApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
title: 'Recurring Expense Tracker',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
darkTheme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorSchemeSeed: Colors.green,
),
themeMode: ThemeMode.system,
home: HomePage(),
);
}
}

22
lib/models/asset.dart Normal file
View File

@ -0,0 +1,22 @@
// Local
import '/models/item_type.dart';
import '/models/tracked_item.dart';
class Asset extends TrackedItem {
static String amountText = "Amount";
Asset({
super.id,
super.type = ItemType.asset,
required super.name,
required super.amount,
required super.description,
});
factory Asset.fromMap(Map<String, dynamic> json) => Asset(
id: json['id'],
name: json['name'],
amount: json['amount'],
description: json['description'],
);
}

View File

@ -0,0 +1,57 @@
// Local
import '/models/expense.dart';
import '/models/income.dart';
import '/models/asset.dart';
import '/db.dart';
/// TODO:
/// - Test the JSON data to ensure it's formed properly.
/// - Enable the functions for income and assets once they are implemented.
/// - Create a method (factory?) to load JSON data and delete/insert the data.
class DatabaseBackup {
List<Expense> expenseTable = [];
List<Income> incomeTable = [];
List<Asset> assetTable = [];
void loadTables() async {
expenseTable = await DatabaseHelper.instance.getExpenses();
//incomeTable = await DatabaseHelper.instance.getIncomes();
//assetTable = await DatabaseHelper.instance.getAssets();
}
Map<String, dynamic> toMap() {
loadTables(); // TODO: Do we somehow need to wait for this to finish?
return {
'expense_table': [
for (Expense e in expenseTable)
{
'id': e.id,
'name': e.name,
'cost': e.amount,
'frequency': e.frequency!.title,
'description': e.description,
},
],
'income_table': [
for (Income i in incomeTable)
{
'id': i.id,
'name': i.name,
'revenue': i.amount,
'frequency': i.frequency!.title,
'description': i.description,
},
],
'asset_table': [
for (Asset a in assetTable)
{
'id': a.id,
'name': a.name,
'amount': a.amount,
'description': a.description,
},
],
};
}
}

39
lib/models/expense.dart Normal file
View File

@ -0,0 +1,39 @@
// Local
import '/models/item_type.dart';
import '/models/tracked_item.dart';
import '/models/frequency.dart';
class Expense extends TrackedItem {
static String amountText = "Cost";
Expense({
super.id,
super.type = ItemType.expense,
required super.name,
required super.amount,
required super.frequency,
required super.description,
});
factory Expense.fromMap(Map<String, dynamic> json) => Expense(
id: json['id'],
name: json['name'],
amount: json['cost'],
frequency: Frequency.values
.where((freq) => freq.title == json['frequency'])
.first,
description: json['description'],
);
@override
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'cost': amount,
'frequency': frequency!.title,
'description': description,
};
}
}

63
lib/models/frequency.dart Normal file
View File

@ -0,0 +1,63 @@
// https://www.tutorialspoint.com/dart_programming/dart_programming_enumeration.htm
enum Frequency {
daily(
title: "Daily",
hint: "Once Per Day",
timesPerYear: 364.25,
numDays: 1,
),
weekly(
title: "Weekly",
hint: "Once Per Week",
timesPerYear: (364.25 / 7),
numDays: 7,
),
biweekly(
title: "Biweekly",
hint: "Every Other Week",
timesPerYear: (364.25 / 14),
numDays: 14,
),
bimonthly(
title: "Bimonthly",
hint: "Twice Per Month",
timesPerYear: 24,
numDays: (364.25 / 24),
),
monthly(
title: "Monthly",
hint: "Once Per Month",
timesPerYear: 12,
numDays: (364.25 / 12),
),
quarterly(
title: "Quarterly",
hint: "Every Three Months",
timesPerYear: 4,
numDays: (364.25 / 4),
),
biannual(
title: "Biannual",
hint: "Twice Per Year",
timesPerYear: 2,
numDays: (364.25 / 2),
),
yearly(
title: "Yearly",
hint: "Once Per Year",
timesPerYear: 1,
numDays: 364.25,
);
const Frequency({
required this.title,
required this.hint,
required this.timesPerYear,
required this.numDays,
});
final String title;
final String hint;
final double timesPerYear;
final double numDays;
}

38
lib/models/income.dart Normal file
View File

@ -0,0 +1,38 @@
// Local
import '/models/tracked_item.dart';
import '/models/frequency.dart';
import '/models/item_type.dart';
class Income extends TrackedItem {
static String amountText = "Revenue";
Income({
super.id,
super.type = ItemType.income,
required super.name,
required super.amount,
required super.frequency,
required super.description,
});
factory Income.fromMap(Map<String, dynamic> json) => Income(
id: json['id'],
name: json['name'],
amount: json['revenue'],
frequency: Frequency.values
.where((freq) => freq.title == json['frequency'])
.first,
description: json['description'],
);
@override
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'revenue': amount,
'frequency': frequency!.title,
'description': description,
};
}
}

27
lib/models/item_type.dart Normal file
View File

@ -0,0 +1,27 @@
enum ItemType {
expense(
title: "Expense",
plural: "Expenses",
description: "Items which cost revenue, or decrease asset value.",
),
income(
title: "Income",
plural: "Income",
description: "Items which bring in revenue, or increase asset value.",
),
asset(
title: "Asset",
plural: "Assets",
description: "Value which has been earned and can be spent.",
);
const ItemType({
required this.title,
required this.plural,
required this.description,
});
final String title;
final String plural;
final String description;
}

View File

@ -0,0 +1,55 @@
// Local
import '/models/item_type.dart';
import '/models/frequency.dart';
abstract class TrackedItem {
int? id;
ItemType? type;
String name;
double amount;
Frequency? frequency;
String description;
TrackedItem({
this.id,
this.type,
required this.name,
required this.amount,
this.frequency,
required this.description,
});
static String amountText = "Amount";
String getAmountText() => amountText;
@override
String toString() {
return toMap().toString();
}
double calcComparableAmountYearly() {
return frequency == null ? 0 : amount * frequency!.timesPerYear;
}
double calcComparableAmountDaily() {
return frequency == null ? 0 : amount / frequency!.numDays;
}
Map<String, dynamic> toMap() {
return frequency == null
? {
'id': id,
'name': name,
'amount': amount,
'description': description,
}
: {
'id': id,
'name': name,
'amount': amount,
'frequency': frequency!.title,
'description': description,
};
}
}

127
lib/pages/help.dart Normal file
View File

@ -0,0 +1,127 @@
// Flutter
import '/models/item_type.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher_string.dart';
// Local
import '/widgets/cards.dart';
_launchSite(String url) async {
try {
if (await canLaunchUrlString(url)) {
await launchUrlString(
url,
mode: LaunchMode.externalApplication,
);
} else {
throw "System does not think it can launch '$url'.";
}
} on Exception catch (e) {
throw e.toString();
}
}
class HelpPage extends StatelessWidget {
const HelpPage({
super.key,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: theme.colorScheme.onPrimary,
),
child: Column(
children: [
TitleCard(title: "Help"),
Text(
"\t\t This app is meant to be a simple budgeting tool,"
" allowing you to view your income and expenses at a high"
" level without micro managing specific budget items or"
" adding receipts."
"",
),
Text(
"\n\t\t ${ItemType.expense.plural} are defined as ${ItemType.expense.description.toLowerCase()}"
" ${ItemType.income.title} is defined as ${ItemType.income.description.toLowerCase()}"
" ${ItemType.asset.plural} are defined as ${ItemType.asset.description.toLowerCase()}"
"",
),
Text(
"\n\t\t Tracked items can be swiped left to right for"
" Deletion or right to left for Editing. Items are sorted"
" from highest to lowest so that the biggest impacts are"
" always in view."
"",
),
Text(
"\n\t\t To subscribe to Android updates, install Obtanium,"
" then use the URL from the Source Code button below."
" Otherwise the app needs installed manually by downloading"
" APKs from the Source Code /releases/ page. Linux users"
" currently need to install and update manually."
"",
),
//Text("Another paragraph.")
],
),
),
),
),
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: theme.colorScheme.onPrimary,
),
child: TextButton.icon(
onPressed: () {
_launchSite(
"https://git.hyperling.com/me/flutter-expense-tracker",
);
},
icon: Icon(Icons.code),
label: Text("Source Code"),
),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: theme.colorScheme.onPrimary,
),
child: TextButton.icon(
onPressed: () {
_launchSite(
"https://hyperling.com",
);
},
icon: Icon(Icons.web_asset),
label: Text("Personal Website"),
),
),
),
),
],
)
],
);
}
}

192
lib/pages/home.dart Normal file
View File

@ -0,0 +1,192 @@
// Flutter
import 'package:flutter/material.dart';
import '/models/item_type.dart';
import 'dart:io';
import 'package:package_info_plus/package_info_plus.dart';
// Local
import '/pages/tracked_item.dart';
import '/pages/report.dart';
import '/pages/settings.dart';
import '/pages/help.dart';
class HomePage extends StatefulWidget {
const HomePage({
super.key,
});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
var pageSelected = 0;
refresh() {
setState(() {});
}
PackageInfo _packageInfo = PackageInfo(
appName: 'Unknown',
packageName: 'Unknown',
version: 'Unknown',
buildNumber: 'Unknown',
);
Future _initPackageInfo() async {
final PackageInfo info = await PackageInfo.fromPlatform();
setState(() {
_packageInfo = info;
});
}
@override
void initState() {
super.initState();
// Get package details
_initPackageInfo();
}
@override
Widget build(BuildContext context) {
Widget page;
Widget? dialog;
switch (pageSelected) {
case 0:
page = TrackedItemPage(
assetType: ItemType.expense,
notifyParent: refresh,
);
dialog = TrackedItemInputDialog(
notifyParent: refresh,
type: ItemType.expense,
);
break;
case 1:
page = TrackedItemPage(
assetType: ItemType.income,
notifyParent: refresh,
);
dialog = TrackedItemInputDialog(
notifyParent: refresh,
type: ItemType.income,
);
break;
case 2:
page = TrackedItemPage(
assetType: ItemType.asset,
notifyParent: refresh,
);
dialog = TrackedItemInputDialog(
notifyParent: refresh,
type: ItemType.asset,
);
break;
case 3:
page = ProjectionPage();
break;
case 4:
page = SettingsPage();
break;
case 5:
page = HelpPage();
break;
default:
throw UnimplementedError('No widget for page $pageSelected yet!');
}
Future<void> addNewValue(BuildContext context) {
return showDialog(
context: context,
builder: (_) => AlertDialog(content: dialog),
);
}
Widget? floatingButton;
if (dialog != null) {
floatingButton = IconButton(
onPressed: () {
addNewValue(context);
},
icon: Icon(Icons.add),
color: Theme.of(context).colorScheme.onSurface,
);
}
Widget navigation = NavigationRail(
extended: true,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.payment),
label: Text(ItemType.expense.plural),
),
NavigationRailDestination(
icon: Icon(Icons.account_balance),
label: Text(ItemType.income.plural),
),
NavigationRailDestination(
icon: Icon(Icons.attach_money),
label: Text(ItemType.asset.plural),
),
NavigationRailDestination(
icon: Icon(Icons.bar_chart),
label: Text('Reports'),
),
NavigationRailDestination(
icon: Icon(Icons.settings),
label: Text('Settings'),
),
NavigationRailDestination(
icon: Icon(Icons.help),
label: Text('Help'),
),
],
selectedIndex: pageSelected,
onDestinationSelected: (value) {
setState(() {
pageSelected = value;
if (Platform.isAndroid || Platform.isIOS) {
Navigator.pop(context);
}
});
},
leading: Text("Menu"),
trailing: Text("v${_packageInfo.version}"),
);
Widget main = Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: Center(child: page),
);
Widget? drawer;
Widget body;
if (Platform.isAndroid || Platform.isIOS) {
drawer = navigation;
body = SafeArea(child: main);
} else {
drawer = null;
body = Row(
children: [
SafeArea(child: navigation),
Expanded(child: main),
],
);
}
return LayoutBuilder(builder: (context, constraints) {
return Scaffold(
appBar: AppBar(
title: Text("Expense Tracker"),
),
drawer: drawer,
body: body,
floatingActionButton: floatingButton,
extendBody: false,
extendBodyBehindAppBar: false,
resizeToAvoidBottomInset: true,
);
});
}
}

249
lib/pages/report.dart Normal file
View File

@ -0,0 +1,249 @@
// Flutter
import 'dart:async';
import 'package:flutter/material.dart';
import '/models/item_type.dart';
// Local
import '/db.dart';
import '/widgets/cards.dart';
import '/models/tracked_item.dart';
/// TODO:
/// - Projected Assets:
/// - Allow customization?
double _assetTotal = -1,
_expenseMonthly = -1,
_expenseYearly = -1,
_incomeMonthly = -1,
_incomeYearly = -1;
class ProjectionPage extends StatefulWidget {
const ProjectionPage({
super.key,
});
@override
State<ProjectionPage> createState() => _ProjectionPageState();
}
class _ProjectionPageState extends State<ProjectionPage> {
bool _showProjections = true;
@override
void dispose() {
_assetTotal = -2;
_expenseMonthly = -2;
_expenseYearly = -2;
_incomeMonthly = -2;
_incomeYearly = -2;
super.dispose();
}
@override
Widget build(BuildContext context) {
// Summaries for display as well as calculation of totals for projections.
Widget expenseSummary = SummaryCardForTotals(
list: DatabaseHelper.instance.getExpenses(),
itemType: ItemType.expense,
);
Widget incomeSummary = SummaryCardForTotals(
list: DatabaseHelper.instance.getIncomes(),
itemType: ItemType.income,
);
Widget assetSummary = SummaryCardForTotals(
list: DatabaseHelper.instance.getAssets(),
itemType: ItemType.asset,
);
// Calculations for the projections.
Widget projections;
if (_assetTotal < 0 ||
_incomeMonthly < 0 ||
_incomeYearly < 0 ||
_expenseMonthly < 0 ||
_expenseYearly < 0) {
_showProjections = false;
Future.delayed(Duration(seconds: 1), () {
setState(() {
_showProjections = true;
});
});
}
if (_showProjections) {
double oneMonth = _assetTotal + _incomeMonthly - _expenseMonthly,
threeMonths = _assetTotal + (3 * (_incomeMonthly - _expenseMonthly)),
sixMonths = _assetTotal + (6 * (_incomeMonthly - _expenseMonthly)),
oneYear = _assetTotal + (_incomeYearly - _expenseYearly),
twoYears = _assetTotal + (2 * (_incomeYearly - _expenseYearly)),
fiveYears = _assetTotal + (5 * (_incomeYearly - _expenseYearly));
// Widgets to show the projections.
Widget proj1 = SummaryCard(
name: "One month from now...",
leftText: "",
middleText: oneMonth.toStringAsFixed(2),
rightText: "",
);
Widget proj2 = SummaryCard(
name: "Three months from now...",
leftText: "",
middleText: threeMonths.toStringAsFixed(2),
rightText: "",
);
Widget proj3 = SummaryCard(
name: "Half a year from now...",
leftText: "",
middleText: sixMonths.toStringAsFixed(2),
rightText: "",
);
Widget proj4 = SummaryCard(
name: "One year from now...",
leftText: "",
middleText: oneYear.toStringAsFixed(2),
rightText: "",
);
Widget proj5 = SummaryCard(
name: "Two years from now...",
leftText: "",
middleText: twoYears.toStringAsFixed(2),
rightText: "",
);
Widget proj6 = SummaryCard(
name: "Five years from now...",
leftText: "",
middleText: fiveYears.toStringAsFixed(2),
rightText: "",
);
projections = Column(
children: [
proj1,
proj2,
proj3,
proj4,
proj5,
proj6,
],
);
} else {
projections = Center(
child: SizedBox(
child: CircularProgressIndicator(),
),
);
}
// Return all of the UI elements.
return ListView(
children: [
TitleCard(title: "Summaries"),
expenseSummary,
incomeSummary,
assetSummary,
TitleCard(title: "Projections"),
projections,
],
);
}
}
class SummaryCardForTotals extends StatelessWidget {
const SummaryCardForTotals({
super.key,
required this.list,
required this.itemType,
});
final Future<List<TrackedItem>> list;
final ItemType itemType;
@override
Widget build(BuildContext context) {
String summaryTypeLabel = itemType.title.toString();
return FutureBuilder<List<TrackedItem>>(
future: list,
builder: (
BuildContext context,
AsyncSnapshot<List<TrackedItem>> snapshot,
) {
if (!snapshot.hasData) {
return Text('Loading $summaryTypeLabel Section...');
}
// Calculate the total fields based on item type.
double dailyTotal = 0, monthlyTotal = 0, yearlyTotal = 0;
for (TrackedItem e in snapshot.data!) {
if (e.type != itemType) {
throw "List in SummaryCardForTotals has incorrect item types, abort!";
}
if (e.type == ItemType.asset) {
monthlyTotal += e.amount;
} else {
dailyTotal += e.calcComparableAmountDaily();
monthlyTotal += e.calcComparableAmountYearly() / 12;
yearlyTotal += e.calcComparableAmountYearly();
}
}
/* Load page variables based on calculated totals. */
switch (itemType) {
case ItemType.asset:
_assetTotal = monthlyTotal;
break;
case ItemType.expense:
_expenseMonthly = monthlyTotal;
_expenseYearly = yearlyTotal;
break;
case ItemType.income:
_incomeMonthly = monthlyTotal;
_incomeYearly = yearlyTotal;
break;
default:
throw UnimplementedError(
"Item type ${itemType.title} not handled in SummaryCardForTotals!",
);
}
/* Determine what needs displayed for the item type. */
// Header
String plural = snapshot.data!.length == 1 ? "" : "s";
String header = "$summaryTypeLabel Total";
header += itemType == ItemType.asset ? "" : "s";
header += " (${snapshot.data!.length} Item$plural)";
// Total Fields
String dailyEstimate =
dailyTotal.toStringAsFixed(3).endsWith("0") ? "" : "~",
monthlyEstimate =
monthlyTotal.toStringAsFixed(3).endsWith("0") ? "" : "~",
yearlyEstimate =
yearlyTotal.toStringAsFixed(3).endsWith("0") ? "" : "~";
String leftText = "", middleText = "", rightText = "";
if (itemType == ItemType.asset) {
middleText = "$monthlyEstimate${monthlyTotal.toStringAsFixed(2)}";
} else {
leftText = "$dailyEstimate${dailyTotal.toStringAsFixed(2)} Daily";
middleText =
"$monthlyEstimate${monthlyTotal.toStringAsFixed(2)} Monthly";
rightText =
"$yearlyEstimate${yearlyTotal.toStringAsFixed(2)} Yearly";
}
// Return the UI element.
return SummaryCard(
name: header,
leftText: leftText,
middleText: middleText,
rightText: rightText,
);
});
}
}

32
lib/pages/settings.dart Normal file
View File

@ -0,0 +1,32 @@
// Flutter
import 'package:flutter/material.dart';
// Local
import '/widgets/cards.dart';
/// TODO:
/// - Export DB (JSON?)
/// - Import DB (JSON?)
/// - Choose color scheme
/// - Delete / Reset DB (pull code from `main`'s `if (testing)`)
class SettingsPage extends StatelessWidget {
const SettingsPage({
super.key,
});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
TitleCard(title: "Settings"),
Text(
"No settings exist yet. :)",
),
],
),
);
}
}

617
lib/pages/tracked_item.dart Normal file
View File

@ -0,0 +1,617 @@
// Flutter
import 'package:flutter/material.dart';
import '/models/asset.dart';
import '/models/income.dart';
// Local
import '/models/tracked_item.dart';
import '/models/item_type.dart';
import '/models/expense.dart';
import '/models/frequency.dart';
import '/db.dart';
import '/widgets/cards.dart';
class TrackedItemPage extends StatefulWidget {
final ItemType assetType;
final Function() notifyParent;
const TrackedItemPage({
super.key,
required this.assetType,
required this.notifyParent,
});
@override
State<TrackedItemPage> createState() => _TrackedItemPageState();
}
class _TrackedItemPageState extends State<TrackedItemPage> {
late Future<List<TrackedItem>> _assetsToLoad;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
switch (widget.assetType) {
case ItemType.expense:
_assetsToLoad = DatabaseHelper.instance.getExpenses();
break;
case ItemType.income:
_assetsToLoad = DatabaseHelper.instance.getIncomes();
break;
case ItemType.asset:
_assetsToLoad = DatabaseHelper.instance.getAssets();
break;
default:
throw UnimplementedError("Unsure whch asset group to load.");
}
return FutureBuilder<List<TrackedItem>>(
future: _assetsToLoad,
builder:
(BuildContext context, AsyncSnapshot<List<TrackedItem>> snapshot) {
if (!snapshot.hasData) {
return Text('Loading...');
}
snapshot.data!.sort(
(a, b) => (b.calcComparableAmountYearly() -
a.calcComparableAmountYearly())
.toInt(),
);
return snapshot.data!.isEmpty
? Text(
"Add items to get started.",
softWrap: true,
)
: Column(
children: [
TitleCard(title: widget.assetType.plural),
/*Text(
"${widget.assetType.description}",
style: TextStyle(
fontSize: 16.0,
decoration: TextDecoration.none,
fontWeight: FontWeight.bold,
),
),*/
Expanded(
child: ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (_, index) {
final TrackedItem curr = snapshot.data![index];
final itemKey = Key(curr.id!.toString());
final String itemTitle = curr.name;
final String itemAmount;
if (curr.frequency != null) {
itemAmount =
"${curr.amount.toStringAsFixed(2)} ${curr.frequency!.title}";
} else {
itemAmount = curr.amount.toStringAsFixed(2);
}
final String itemDescription = curr.description;
final double itemDayAmount,
itemMonthAmount,
itemYearAmount;
final String estimateSymbolDaily,
estimateSymbolMonthly,
estimateSymbolYearly;
if (curr.frequency != null) {
itemDayAmount = curr.calcComparableAmountDaily();
estimateSymbolDaily = curr.frequency!.numDays
.toStringAsFixed(2)
.endsWith(".00") &&
itemDayAmount
.toStringAsFixed(3)
.endsWith("0")
? ""
: "~";
itemMonthAmount =
(curr.calcComparableAmountYearly() / 12);
estimateSymbolMonthly = curr.frequency!.timesPerYear
.toStringAsFixed(2)
.endsWith(".00") &&
itemMonthAmount
.toStringAsFixed(3)
.endsWith("0")
? ""
: "~";
itemYearAmount = curr.calcComparableAmountYearly();
estimateSymbolYearly = curr.frequency!.timesPerYear
.toStringAsFixed(2)
.endsWith(".00") &&
itemYearAmount
.toStringAsFixed(3)
.endsWith("0")
? ""
: "~";
} else {
itemDayAmount = -1;
estimateSymbolDaily = "";
itemMonthAmount = curr.amount;
estimateSymbolMonthly = "";
itemYearAmount = -1;
estimateSymbolYearly = "";
}
final String monthlyTitle =
curr.type == ItemType.asset
? ""
: " ${Frequency.monthly.title}";
final String itemTopText = itemDayAmount < 0
? ""
: "$estimateSymbolDaily${itemDayAmount.toStringAsFixed(2)} ${Frequency.daily.title}";
final String itemMiddleText = itemMonthAmount < 0
? ""
: "$estimateSymbolMonthly${itemMonthAmount.toStringAsFixed(2)}$monthlyTitle";
final String itemBottomText = itemYearAmount < 0
? ""
: "$estimateSymbolYearly${itemYearAmount.toStringAsFixed(2)} ${Frequency.yearly.title}";
return Padding(
padding: const EdgeInsets.all(4.0),
child: Dismissible(
key: itemKey,
background: Container(
color: Colors.red,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Icon(Icons.delete),
Text("Delete"),
],
),
),
secondaryBackground: Container(
color: Colors.orange,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text("Edit"),
Icon(Icons.edit),
],
),
),
onDismissed: (direction) {
setState(() {
snapshot.data!.remove(curr);
switch (direction) {
case DismissDirection.startToEnd:
// Remove the item from the database.
if (curr is Expense) {
DatabaseHelper.instance.removeExpense(
curr.id!,
);
} else if (curr is Income) {
DatabaseHelper.instance.removeIncome(
curr.id!,
);
} else if (curr is Asset) {
DatabaseHelper.instance.removeAsset(
curr.id!,
);
} else {
throw UnimplementedError(
"Cannot remove unimplemented item type.");
}
break;
case DismissDirection.endToStart:
// Open an edit dialog, then remove the item from the list.
showDialog(
context: context,
builder: (_) => AlertDialog(
content: TrackedItemInputDialog(
notifyParent: widget.notifyParent,
entry: curr,
amountText: curr.getAmountText(),
type: curr.type!,
),
),
);
break;
default:
UnimplementedError(
"Direction ${direction.toString()} not recognized.",
);
}
});
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: theme.colorScheme.onPrimary,
),
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
itemTitle,
style: TextStyle(fontSize: 20.0),
),
Text(
itemAmount,
style: TextStyle(fontSize: 12.0),
),
],
),
Expanded(
child: Center(
child: Text(
itemDescription,
style: TextStyle(
fontSize: 12.0,
),
softWrap: true,
textAlign: TextAlign.center,
),
),
),
Column(
crossAxisAlignment:
CrossAxisAlignment.end,
children: [
Text(
itemTopText,
style: TextStyle(fontSize: 12.0),
),
Text(
itemMiddleText,
style: TextStyle(fontSize: 12.0),
),
Text(
itemBottomText,
style: TextStyle(fontSize: 12.0),
),
],
),
],
),
),
),
),
);
},
),
),
],
);
});
}
}
class TrackedItemInputDialog extends StatefulWidget {
final Function() notifyParent;
final TrackedItem? entry;
final String? amountText;
final ItemType? type;
const TrackedItemInputDialog({
super.key,
required this.notifyParent,
this.entry,
this.amountText,
this.type,
});
@override
State<TrackedItemInputDialog> createState() => _TrackedItemInputDialogState();
}
class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> {
final _formKey = GlobalKey<FormState>();
int? _id;
String _name = "";
double _amount = 0;
Frequency _freq = Frequency.monthly;
String _desc = "";
ItemType? _type;
@override
Widget build(BuildContext context) {
if (widget.type == null &&
(widget.entry != null && widget.entry!.type == null)) {
throw FlutterError("No ItemType provided for TrackedItemInputDialog.");
}
_type = widget.type;
if (widget.entry != null) {
_id = widget.entry!.id;
_name = widget.entry!.name;
_amount = widget.entry!.amount;
widget.entry!.frequency == null ? null : _freq = widget.entry!.frequency!;
_desc = widget.entry!.description;
_type = widget.entry!.type!;
}
String amountText =
widget.amountText != null ? widget.amountText! : TrackedItem.amountText;
Future<List<TrackedItem>> items;
switch (_type) {
case ItemType.expense:
items = DatabaseHelper.instance.getExpenses();
break;
case ItemType.income:
items = DatabaseHelper.instance.getIncomes();
break;
case ItemType.asset:
items = DatabaseHelper.instance.getAssets();
break;
default:
throw UnimplementedError("Cannot find unimplemented type.");
}
return Column(
// prevent AlertDialog from taking full vertical height.
mainAxisSize: MainAxisSize.min,
children: [
Container(
alignment: FractionalOffset.topRight,
child: IconButton(
onPressed: () {
if (widget.entry != null) {
setState(() {
switch (_type) {
case ItemType.expense:
DatabaseHelper.instance.addExpense(widget.entry!);
break;
case ItemType.income:
DatabaseHelper.instance.addIncome(widget.entry!);
break;
case ItemType.asset:
DatabaseHelper.instance.addAsset(widget.entry!);
break;
default:
throw UnimplementedError(
"Cannot add unimplemented type.");
}
widget.notifyParent();
});
}
Navigator.of(context).pop();
},
icon: Icon(Icons.clear),
),
),
AlertDialog(
insetPadding: EdgeInsets.all(0),
title: Center(
child: widget.entry == null
? Text("New ${_type!.title}")
: Text("Edit ${_type!.title}"),
),
content: FutureBuilder<List<TrackedItem>>(
future: items,
builder: (BuildContext context,
AsyncSnapshot<List<TrackedItem>> snapshot) {
if (!snapshot.hasData) {
return Center(child: Text('Loading...'));
}
List<TrackedItem> expenses = snapshot.data!;
return Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
keyboardType: TextInputType.text,
textCapitalization: TextCapitalization.words,
decoration: InputDecoration(
labelText: "Name",
hintText: "Example: Red Pocket",
hintStyle: TextStyle(fontSize: 10.0),
errorStyle: TextStyle(fontSize: 10.0),
),
initialValue: _name,
validator: (value) {
if (value!.isEmpty) {
return "Name must be provided.";
}
if (!expenses.every((expense) =>
expense.name != value || expense.id == _id)) {
return "Name must be unique, already in use.";
}
return null;
},
onSaved: (value) {
_name = value!;
},
),
TextFormField(
keyboardType:
TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration(
labelText: amountText,
hintText: "Example: 10.00",
hintStyle: TextStyle(fontSize: 10.0),
errorStyle: TextStyle(fontSize: 10.0),
),
initialValue: _amount != 0 ? _amount.toString() : "",
validator: (value) {
if (value == null || value.isEmpty) {
return "$amountText must be provided.";
}
if (double.tryParse(value) == null) {
return "$amountText must be a valid number.";
}
if (double.parse(value) < 0) {
switch (_type) {
case ItemType.expense:
return "Please use the Income page.";
case ItemType.income:
return "Please use the Expense page.";
default:
break;
}
}
if (double.parse(value) < 0.01) {
return "$amountText must be one hundreth (0.01) or higher.";
}
return null;
},
onSaved: (value) {
_amount = double.parse(value!);
},
),
if (_type != ItemType.asset)
DropdownButtonFormField(
items: Frequency.values
.map(
(freq) => DropdownMenuItem(
value: freq,
child: Row(
children: [
Text(
freq.title,
),
Padding(
padding: EdgeInsets.all(1.0),
child: Text(
" (${freq.hint})",
style: TextStyle(fontSize: 10.0),
),
),
],
),
),
)
.toList(),
value: _freq,
decoration: InputDecoration(
labelText: "Frequency",
errorStyle: TextStyle(fontSize: 10.0),
),
validator: (value) {
if (value == null) {
return "Frequency must be provided.";
}
if (!Frequency.values.contains(value)) {
return "Value not valid.";
}
return null;
},
onChanged: (value) {
_freq = value!;
},
),
TextFormField(
keyboardType: TextInputType.text,
textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(
labelText: "Description",
hintText:
"Example: 1GB data with unlimited talk & text.",
hintStyle: TextStyle(fontSize: 8.0),
errorStyle: TextStyle(fontSize: 10.0),
),
initialValue: _desc,
validator: (value) {
return null;
},
onSaved: (value) {
_desc = value!;
},
),
],
),
);
}),
actions: [
Center(
child: ElevatedButton.icon(
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
setState(
() {
switch (_type) {
case ItemType.expense:
Expense expense = Expense(
id: _id,
name: _name,
amount: _amount,
frequency: _freq,
description: _desc,
);
if (_id != null) {
DatabaseHelper.instance.updateExpense(
expense,
);
} else {
DatabaseHelper.instance.addExpense(
expense,
);
}
break;
case ItemType.income:
Income income = Income(
id: _id,
name: _name,
amount: _amount,
frequency: _freq,
description: _desc,
);
if (_id != null) {
DatabaseHelper.instance.updateIncome(
income,
);
} else {
DatabaseHelper.instance.addIncome(
income,
);
}
break;
case ItemType.asset:
Asset asset = Asset(
id: _id,
name: _name,
amount: _amount,
description: _desc,
);
if (_id != null) {
DatabaseHelper.instance.updateAsset(
asset,
);
} else {
DatabaseHelper.instance.addAsset(
asset,
);
}
break;
default:
throw UnimplementedError(
"No code for type ${_type!.title}",
);
}
widget.notifyParent();
Navigator.of(context).pop();
},
);
}
},
icon: Icon(Icons.save),
label: Text('Submit'),
),
)
],
),
],
);
}
}

92
lib/widgets/cards.dart Normal file
View File

@ -0,0 +1,92 @@
// Flutter
import 'package:flutter/material.dart';
class TitleCard extends StatelessWidget {
const TitleCard({
super.key,
required this.title,
});
final String title;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Text(
title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
decoration: TextDecoration.underline,
),
),
),
);
}
}
class SummaryCard extends StatelessWidget {
const SummaryCard({
super.key,
required this.name,
required this.leftText,
required this.middleText,
required this.rightText,
});
final String name;
final String leftText;
final String middleText;
final String rightText;
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).cardColor,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
color: Theme.of(context).highlightColor,
child: Column(
children: [
Text(
name,
style: TextStyle(
decoration: TextDecoration.underline,
fontSize: 16,
),
),
Row(
children: [
Spacer(
flex: 3,
),
Text(
leftText,
),
Spacer(
flex: 1,
),
Text(
middleText,
),
Spacer(
flex: 1,
),
Text(
rightText,
),
Spacer(
flex: 3,
),
],
),
],
),
),
),
);
}
}

View File

@ -4,10 +4,10 @@ project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "flutter_expense_tracker")
set(BINARY_NAME "expense_tracker")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.flutter_expense_tracker")
set(APPLICATION_ID "com.hyperling.expense_tracker")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.

View File

@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h"
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}

View File

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -40,11 +40,11 @@ static void my_application_activate(GApplication* application) {
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "flutter_expense_tracker");
gtk_header_bar_set_title(header_bar, "Recurring Expense Tracker");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "flutter_expense_tracker");
gtk_window_set_title(window, "Recurring Expense Tracker");
}
gtk_window_set_default_size(window, 1280, 720);

View File

@ -5,6 +5,14 @@
import FlutterMacOS
import Foundation
import package_info_plus
import path_provider_foundation
import sqflite_darwin
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View File

@ -64,7 +64,7 @@
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* flutter_expense_tracker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "flutter_expense_tracker.app"; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10ED2044A3C60003C045 /* flutter_empty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "flutter_empty.app"; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
@ -131,7 +131,7 @@
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* flutter_expense_tracker.app */,
33CC10ED2044A3C60003C045 /* flutter_empty.app */,
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
);
name = Products;
@ -217,7 +217,7 @@
);
name = Runner;
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* flutter_expense_tracker.app */;
productReference = 33CC10ED2044A3C60003C045 /* flutter_empty.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
@ -385,10 +385,10 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_expense_tracker.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_expense_tracker";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_empty.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_empty";
};
name = Debug;
};
@ -399,10 +399,10 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_expense_tracker.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_expense_tracker";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_empty.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_empty";
};
name = Release;
};
@ -413,10 +413,10 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_expense_tracker.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_expense_tracker";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_empty.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_empty";
};
name = Profile;
};

View File

@ -15,7 +15,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "flutter_expense_tracker.app"
BuildableName = "flutter_empty.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
@ -31,7 +31,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "flutter_expense_tracker.app"
BuildableName = "flutter_empty.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
@ -65,7 +65,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "flutter_expense_tracker.app"
BuildableName = "flutter_empty.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
@ -82,7 +82,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "flutter_expense_tracker.app"
BuildableName = "flutter_empty.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>

View File

@ -5,10 +5,10 @@
// 'flutter create' template.
// The application's name. By default this is also the title of the Flutter window.
PRODUCT_NAME = flutter_expense_tracker
PRODUCT_NAME = flutter_empty
// The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExpenseTracker
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterEmpty
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved.

BIN
media/icon_v001.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
media/icon_v001.xcf Normal file

Binary file not shown.

BIN
media/icon_v001_round.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
media/icon_v002.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
media/icon_v002.xcf Normal file

Binary file not shown.

BIN
media/icon_v002_round.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
media/icon_v003.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
media/icon_v003.xcf Normal file

Binary file not shown.

BIN
media/icon_v003_round.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -5,58 +5,58 @@ packages:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev"
source: hosted
version: "2.11.0"
version: "2.12.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.0"
clock:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.0"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.8"
version: "1.19.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
version: "1.3.2"
ffi:
dependency: transitive
description:
name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
flutter:
dependency: "direct main"
description: flutter
@ -75,22 +75,43 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
http:
dependency: transitive
description:
name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
url: "https://pub.dev"
source: hosted
version: "1.3.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
url: "https://pub.dev"
source: hosted
version: "10.0.7"
version: "10.0.8"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://pub.dev"
source: hosted
version: "3.0.8"
version: "3.0.9"
leak_tracker_testing:
dependency: transitive
description:
@ -111,10 +132,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
@ -127,18 +148,98 @@ packages:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.15.0"
path:
version: "1.16.0"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
url: "https://pub.dev"
source: hosted
version: "8.3.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
name: package_info_plus_platform_interface
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
version: "3.2.0"
path:
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12"
url: "https://pub.dev"
source: hosted
version: "2.2.16"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
sky_engine:
dependency: transitive
description: flutter
@ -148,50 +249,186 @@ packages:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
version: "1.10.1"
sqflite:
dependency: "direct main"
description:
name: sqflite
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b"
url: "https://pub.dev"
source: hosted
version: "2.5.5"
sqflite_common_ffi:
dependency: "direct main"
description:
name: sqflite_common_ffi
sha256: "1f3ef3888d3bfbb47785cc1dda0dc7dd7ebd8c1955d32a9e8e9dae1e38d1c4c1"
url: "https://pub.dev"
source: hosted
version: "2.3.5"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e"
url: "https://pub.dev"
source: hosted
version: "2.7.5"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.3"
version: "0.7.4"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
url: "https://pub.dev"
source: hosted
version: "6.3.1"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4"
url: "https://pub.dev"
source: hosted
version: "6.3.15"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
vector_math:
dependency: transitive
description:
@ -204,10 +441,34 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
url: "https://pub.dev"
source: hosted
version: "14.3.0"
version: "14.3.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f
url: "https://pub.dev"
source: hosted
version: "5.12.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
sdks:
dart: ">=3.6.1 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.27.0"

View File

@ -1,89 +1,26 @@
name: flutter_expense_tracker
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
name: expense_tracker
homepage: https://git.hyperling.com/me/flutter-expense-tracker
description: Track recurring expenses against income and liquid assets.
publish_to: 'none'
version: 0.1.3
environment:
sdk: ^3.6.1
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
package_info_plus: ^8.3.0
path: ^1.9.0
path_provider: ^2.1.5
sqflite: ^2.4.1
sqflite_common_ffi: ^2.3.4+4
url_launcher: ^6.3.1
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package

3
run_offline.sh Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
flutter run --no-pub

View File

@ -8,12 +8,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_expense_tracker/main.dart';
import 'package:expense_tracker/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
await tester.pumpWidget(const MainApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);

View File

@ -23,13 +23,13 @@
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="flutter_expense_tracker">
<meta name="apple-mobile-web-app-title" content="flutter_empty">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>flutter_expense_tracker</title>
<title>flutter_empty</title>
<link rel="manifest" href="manifest.json">
</head>
<body>

View File

@ -1,6 +1,6 @@
{
"name": "flutter_expense_tracker",
"short_name": "flutter_expense_tracker",
"name": "flutter_empty",
"short_name": "flutter_empty",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",

View File

@ -1,10 +1,10 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.14)
project(flutter_expense_tracker LANGUAGES CXX)
project(flutter_empty LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "flutter_expense_tracker")
set(BINARY_NAME "flutter_empty")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.

View File

@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h"
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -90,12 +90,12 @@ BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "com.example" "\0"
VALUE "FileDescription", "flutter_expense_tracker" "\0"
VALUE "FileDescription", "flutter_empty" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "flutter_expense_tracker" "\0"
VALUE "InternalName", "flutter_empty" "\0"
VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0"
VALUE "OriginalFilename", "flutter_expense_tracker.exe" "\0"
VALUE "ProductName", "flutter_expense_tracker" "\0"
VALUE "OriginalFilename", "flutter_empty.exe" "\0"
VALUE "ProductName", "flutter_empty" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
END
END

View File

@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
if (!window.Create(L"flutter_expense_tracker", origin, size)) {
if (!window.Create(L"flutter_empty", origin, size)) {
return EXIT_FAILURE;
}
window.SetQuitOnClose(true);