41 Commits
Author SHA1 Message Date
me 3b71cc0267 Merge pull request '0.1.4' (#3) from dev into main
Reviewed-on: #3
2026-11-01 06:32:34 -07:00
me c03062291a pubspec, update the version in case it hasn't already been done. 2026-10-30 19:58:15 -07:00
me c90a2cc437 manifest, use the proper icon. 2026-10-30 19:51:19 -07:00
me b9eb13a42d icon, use Android Studio to install it properly. 2026-10-30 19:48:03 -07:00
me 1d9fdcb6fe Allow the app to compile again. 2026-10-30 19:45:04 -07:00
me d523886659 gitignore, add the new location of build files. 2026-10-30 19:44:41 -07:00
me 8b51114165 More changes by Flutter, seems like the opposite of what was just done? 2026-10-24 20:42:25 -07:00
me 6e354eb908 Flutter keeps making these changes, go ahead and commit them. 2026-10-24 17:34:47 -07:00
me f48ba337a7 gradle, remove supposedly now unnecessary kotlin-android call. 2026-10-24 17:34:19 -07:00
me 430127d040 Remove old Gradle files (non-Kotlin). 2026-10-22 17:54:02 -07:00
me 6b33431613 Update Kotlin. 2026-10-22 17:53:46 -07:00
me d5985b4a11 Update AGP, add Gradle Daemon. 2026-10-22 17:44:03 -07:00
me 23974ea4ae gitignore, ignore Android build files. 2026-10-22 17:33:44 -07:00
me 7e25ce1c06 Update Gradle. 2026-10-22 17:32:38 -07:00
me 3711a888f9 Update Java. 2026-10-22 17:32:33 -07:00
me 48a8ac93ca Changes via Flutter plugin in Codium, Code-OSS version was very outdated, 2026-09-29 11:51:56 -07:00
me 1900a1378a fdroid, add specifier to email and put the field in the app-specific section. 2026-09-29 07:16:51 -07:00
me bdebda9c83 Add more fdroid details while refactoring file. 2026-07-04 14:57:28 -07:00
me 672d34fa43 Fix space at beginning of fdroid manifest description. 2026-07-04 10:58:13 -07:00
me 0aff0208d4 Update issue tracker to be proper HTTP link, use contact page. Change website page to go directly to the app. 2026-07-04 10:47:43 -07:00
me 097a9ea4ed Merge pull request 'Add manifest file for self-hosted F-Droid repo.' (#2) from dev into main
Reviewed-on: #2
2026-07-04 10:17:23 -07:00
me f86d78a969 Add manifest file for self-hosted F-Droid repo. 2026-07-04 10:16:23 -07:00
me 70c26be8e3 Stop showing errors if the default exists and all options have already been covered. 2025-03-27 15:17:14 -07:00
me b177b6317e Adjust settings page formatting and description text. 2025-03-27 15:15:33 -07:00
me 3c52239efc Add header to Settings page. 2025-03-27 15:14:10 -07:00
me de4f8306d7 Remove TODO note. 2025-03-27 15:12:31 -07:00
me f77cf7bd38 Refactor to use value of variable. 2025-03-27 15:12:23 -07:00
me 716d40c694 Remove unneeded dependency. 2025-03-27 15:12:07 -07:00
me d9f8536f26 Use a consistent header on each page. 2025-03-27 14:48:07 -07:00
me 1966f72c93 Add item descriptions to Help information and clarify the updating section. 2025-03-27 14:39:52 -07:00
me e896611bd1 Refactor to add header to the Tracked Item pages. 2025-03-27 14:31:34 -07:00
me 2d9c93fec4 Fix typo. 2025-03-27 14:26:51 -07:00
me c5f1a4e9ba Add app version to menu. 2025-03-27 14:02:18 -07:00
me 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
me 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
me 147178e4dd Fix the projections not loading if any item type was left unused. 2025-03-27 11:58:26 -07:00
me c39e09b2b6 Increment the version. 2025-03-27 11:57:22 -07:00
me fa852faadc Use the dispose() method to reset the global values. Works excellently!! :D 2025-03-26 13:17:08 -07:00
me 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
me 75cc72678b Add TBD of where the Income duplicate name validator is going wrong. 2025-03-25 08:45:44 -07:00
me 2970431b91 Increment the version. 2025-03-21 15:03:16 -07:00
40 changed files with 592 additions and 416 deletions

No files matched your search

+4
View File
@@ -47,3 +47,7 @@ app.*.map.json
# Ignore the releases folder.
releases/*
# Ignore build files from Android.
android/build/*
android/app/build/*
+11
View File
@@ -1 +1,12 @@
include: package:flutter_lints/flutter.yaml
analyzer:
errors:
unreachable_switch_default: ignore
exclude:
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**
-44
View File
@@ -1,44 +0,0 @@
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_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
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.debug
}
}
}
flutter {
source = "../.."
}
+10 -11
View File
@@ -1,7 +1,8 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
// AGP 9 bundles Kotlin natively - no explicit plugin needed here
id("dev.flutter.flutter-gradle-plugin")
}
@@ -11,19 +12,19 @@ android {
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
// AGP 9's bundled Kotlin - use the native DSL
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
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
@@ -32,8 +33,6 @@ android {
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")
}
}
+1 -2
View File
@@ -2,8 +2,7 @@
<application
android:label="Expenses"
android:name="${applicationName}"
android:icon="@mipmap/icon_v003"
android:roundIcon="@mipmap/icon_v003_round">
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
+2 -18
View File
@@ -1,18 +1,2 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
// Expose Kotlin version for Flutter's plugin validation (AGP 9 bundles Kotlin natively)
ext.kotlin_version = "2.4.20"
+6
View File
@@ -1,3 +1,9 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=true
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
# Force the Kotlin version resolver in AGP 9 to use our target version
kotlin.version=2.4.20
+1 -1
View File
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip
-25
View File
@@ -1,25 +0,0 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return 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.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}
include ":app"
+6 -2
View File
@@ -18,8 +18,12 @@ pluginManagement {
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
id("com.android.application") version "9.4.1" apply false
// This must exist to satisfy Flutter's Gradle plugin validation for AGP 9 bundled Kotlin:
// "Error: Your project's Kotlin version (2.2.10) is lower than Flutter's minimum supported version of 2.2.20."
id("org.jetbrains.kotlin.android") version "2.4.20" apply false
}
include(":app")
+16
View File
@@ -0,0 +1,16 @@
AuthorName: 'Chad Michael "Hyperling" Greenwood'
AuthorWebSite: 'https://hyperling.com/about/'
Donate: 'https://hyperling.com/donate/'
Liberapay: 'HyperVegan'
Bitcoin: 'bc1qsfe8dkvry3d34kztz449gkq67wq8fu2nkgfkh0'
Litecoin: 'ltc1qavmpu5d6ljntxsd6jj548m4yys83zwscl0dzkx'
IssueTracker: 'https://hyperling.com/contact/'
Categories:
- fdroid
- hyperling
- utilities
Name: Expense Tracker
AuthorEmail: 'me+expense@hyperling.com'
Summary: 'Keep track of repeating expenses and view projections of their effect on your finances. First app written with Flutter.'
SourceCode: 'https://git.hyperling.com/me/flutter-expense-tracker/'
WebSite: 'https://hyperling.com/apps/#expense'
+6 -1
View File
@@ -2,21 +2,26 @@ enum ItemType {
expense(
title: "Expense",
plural: "Expenses",
description: "Items which cost revenue, or decrease asset value.",
),
income(
title: "Income",
plural: "Incomes",
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;
}
+21 -6
View File
@@ -1,7 +1,11 @@
// 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)) {
@@ -38,23 +42,34 @@ class HelpPage extends StatelessWidget {
),
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.",
" adding receipts."
"",
),
Text(
"\n\t\t Tracked items can be swiped left to right for ,"
"\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.",
" always in view."
"",
),
Text(
"\n\t\t To subscribe to app updates, install the Obtanium"
" app, then use the URL from the Source Code button below."
"\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.",
" APKs from the Source Code /releases/ page. Linux users"
" currently need to install and update manually."
"",
),
//Text("Another paragraph.")
],
+31 -7
View File
@@ -2,13 +2,13 @@
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';
import '/db.dart';
class HomePage extends StatefulWidget {
const HomePage({
@@ -26,14 +26,36 @@ class _HomePageState extends State<HomePage> {
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(
assetsToLoad: DatabaseHelper.instance.getExpenses(),
assetType: ItemType.expense,
notifyParent: refresh,
);
dialog = TrackedItemInputDialog(
@@ -43,7 +65,7 @@ class _HomePageState extends State<HomePage> {
break;
case 1:
page = TrackedItemPage(
assetsToLoad: DatabaseHelper.instance.getIncomes(),
assetType: ItemType.income,
notifyParent: refresh,
);
dialog = TrackedItemInputDialog(
@@ -53,7 +75,7 @@ class _HomePageState extends State<HomePage> {
break;
case 2:
page = TrackedItemPage(
assetsToLoad: DatabaseHelper.instance.getAssets(),
assetType: ItemType.asset,
notifyParent: refresh,
);
dialog = TrackedItemInputDialog(
@@ -97,15 +119,15 @@ class _HomePageState extends State<HomePage> {
destinations: [
NavigationRailDestination(
icon: Icon(Icons.payment),
label: Text('Expenses'),
label: Text(ItemType.expense.plural),
),
NavigationRailDestination(
icon: Icon(Icons.account_balance),
label: Text('Income'),
label: Text(ItemType.income.plural),
),
NavigationRailDestination(
icon: Icon(Icons.attach_money),
label: Text('Liquid Assets'),
label: Text(ItemType.asset.plural),
),
NavigationRailDestination(
icon: Icon(Icons.bar_chart),
@@ -129,6 +151,8 @@ class _HomePageState extends State<HomePage> {
}
});
},
leading: Text("Menu"),
trailing: Text("v${_packageInfo.version}"),
);
Widget main = Container(
+105 -69
View File
@@ -1,4 +1,5 @@
// Flutter
import 'dart:async';
import 'package:flutter/material.dart';
import '/models/item_type.dart';
@@ -10,14 +11,12 @@ import '/models/tracked_item.dart';
/// TODO:
/// - Projected Assets:
/// - Allow customization?
/// - Fix bug where editing an item does not reflect immediately when returning to Reports page.
/// - Currently reflects after going back to Reports the 2nd time.
double _assetTotal = 0,
_expenseMonthly = 0,
_expenseYearly = 0,
_incomeMonthly = 0,
_incomeYearly = 0;
double _assetTotal = -1,
_expenseMonthly = -1,
_expenseYearly = -1,
_incomeMonthly = -1,
_incomeYearly = -1;
class ProjectionPage extends StatefulWidget {
const ProjectionPage({
@@ -29,67 +28,113 @@ class ProjectionPage extends StatefulWidget {
}
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(),
summaryTypeLabel: ItemType.expense.title,
itemType: ItemType.expense,
);
Widget incomeSummary = SummaryCardForTotals(
list: DatabaseHelper.instance.getIncomes(),
summaryTypeLabel: ItemType.income.title,
itemType: ItemType.income,
);
Widget assetSummary = SummaryCardForTotals(
list: DatabaseHelper.instance.getAssets(),
summaryTypeLabel: ItemType.asset.title,
itemType: ItemType.asset,
);
// Calculations for the projections.
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));
Widget projections;
if (_assetTotal < 0 ||
_incomeMonthly < 0 ||
_incomeYearly < 0 ||
_expenseMonthly < 0 ||
_expenseYearly < 0) {
_showProjections = false;
// 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: "",
);
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(
@@ -99,12 +144,7 @@ class _ProjectionPageState extends State<ProjectionPage> {
incomeSummary,
assetSummary,
TitleCard(title: "Projections"),
proj1,
proj2,
proj3,
proj4,
proj5,
proj6,
projections,
],
);
}
@@ -114,14 +154,16 @@ class SummaryCardForTotals extends StatelessWidget {
const SummaryCardForTotals({
super.key,
required this.list,
required this.summaryTypeLabel,
required this.itemType,
});
final Future<List<TrackedItem>> list;
final String summaryTypeLabel;
final ItemType itemType;
@override
Widget build(BuildContext context) {
String summaryTypeLabel = itemType.title.toString();
return FutureBuilder<List<TrackedItem>>(
future: list,
builder: (
@@ -134,12 +176,9 @@ class SummaryCardForTotals extends StatelessWidget {
// Calculate the total fields based on item type.
double dailyTotal = 0, monthlyTotal = 0, yearlyTotal = 0;
ItemType? itemType;
for (TrackedItem e in snapshot.data!) {
if (itemType == null) {
itemType = e.type!;
} else if (itemType != e.type) {
throw "List in SummaryCardForTotals has multiple item types, abort!";
if (e.type != itemType) {
throw "List in SummaryCardForTotals has incorrect item types, abort!";
}
if (e.type == ItemType.asset) {
@@ -153,9 +192,6 @@ class SummaryCardForTotals extends StatelessWidget {
/* Load page variables based on calculated totals. */
switch (itemType) {
case null:
break;
case ItemType.asset:
_assetTotal = monthlyTotal;
break;
+12 -2
View File
@@ -1,5 +1,9 @@
// Flutter
import 'package:flutter/material.dart';
// Local
import '/widgets/cards.dart';
/// TODO:
/// - Export DB (JSON?)
/// - Import DB (JSON?)
@@ -14,8 +18,14 @@ class SettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Text(
"No settings yet. :)",
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
TitleCard(title: "Settings"),
Text(
"No settings exist yet. :)",
),
],
),
);
}
+272 -205
View File
@@ -9,14 +9,15 @@ 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 Future<List<TrackedItem>> assetsToLoad;
final ItemType assetType;
final Function() notifyParent;
const TrackedItemPage({
super.key,
required this.assetsToLoad,
required this.assetType,
required this.notifyParent,
});
@@ -25,12 +26,28 @@ class TrackedItemPage extends StatefulWidget {
}
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: widget.assetsToLoad,
future: _assetsToLoad,
builder:
(BuildContext context, AsyncSnapshot<List<TrackedItem>> snapshot) {
if (!snapshot.hasData) {
@@ -46,204 +63,230 @@ class _TrackedItemPageState extends State<TrackedItemPage> {
"Add items to get started.",
softWrap: true,
)
: 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),
),
],
),
],
),
),
),
: 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),
),
],
),
],
),
),
),
),
);
},
),
),
],
);
});
}
@@ -297,6 +340,21 @@ class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> {
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,
@@ -336,14 +394,14 @@ class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> {
? Text("New ${_type!.title}")
: Text("Edit ${_type!.title}"),
),
content: FutureBuilder<List<Expense>>(
future: DatabaseHelper.instance.getExpenses(),
content: FutureBuilder<List<TrackedItem>>(
future: items,
builder: (BuildContext context,
AsyncSnapshot<List<Expense>> snapshot) {
AsyncSnapshot<List<TrackedItem>> snapshot) {
if (!snapshot.hasData) {
return Center(child: Text('Loading...'));
}
List<Expense> expenses = snapshot.data!;
List<TrackedItem> expenses = snapshot.data!;
return Form(
key: _formKey,
child: Column(
@@ -391,7 +449,16 @@ class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> {
return "$amountText must be a valid number.";
}
if (double.parse(value) < 0) {
return "Please use the Income page rather than having negative expenses.";
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.";
+6 -1
View File
@@ -1,3 +1,4 @@
// Flutter
import 'package:flutter/material.dart';
class TitleCard extends StatelessWidget {
@@ -15,7 +16,11 @@ class TitleCard extends StatelessWidget {
child: Center(
child: Text(
title,
style: TextStyle(fontSize: 20),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
decoration: TextDecoration.underline,
),
),
),
);
@@ -5,11 +5,13 @@
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"))
+61 -21
View File
@@ -21,10 +21,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
clock:
dependency: transitive
description:
@@ -45,10 +45,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "1.3.3"
ffi:
dependency: transitive
description:
@@ -80,30 +80,46 @@ packages:
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: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "10.0.8"
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.9"
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
lints:
dependency: transitive
description:
@@ -116,26 +132,42 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.20"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
version: "1.19.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: package_info_plus_platform_interface
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
path:
dependency: "direct main"
description:
@@ -321,10 +353,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
url: "https://pub.dev"
source: hosted
version: "0.7.4"
version: "0.7.12"
typed_data:
dependency: transitive
description:
@@ -401,10 +433,10 @@ packages:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
sha256: "92b9910f66ed1057fd4da7b040ae7c74cafacf885bdc81be496928d5049b032d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.4.3"
vm_service:
dependency: transitive
description:
@@ -421,6 +453,14 @@ packages:
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:
@@ -430,5 +470,5 @@ packages:
source: hosted
version: "1.1.0"
sdks:
dart: ">=3.7.0 <4.0.0"
dart: ">=3.11.0-0 <4.0.0"
flutter: ">=3.27.0"
+3 -1
View File
@@ -1,7 +1,8 @@
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.1
version: 0.1.4
environment:
sdk: ^3.6.1
@@ -9,6 +10,7 @@ environment:
dependencies:
flutter:
sdk: flutter
package_info_plus: ^8.3.0
path: ^1.9.0
path_provider: ^2.1.5
sqflite: ^2.4.1