Compare commits
55 Commits
0b4937b141
...
0.1.1
Author | SHA1 | Date | |
---|---|---|---|
4d0dc03c69 | |||
bdb3fc5c7a | |||
da2ae6206a | |||
f5b22c6c1c | |||
9f7773f724 | |||
dec343af09 | |||
0890e15bfb | |||
55fd4092ac | |||
247b88daa2 | |||
d604a59ad1 | |||
5bf6a0889c | |||
f3d6b8abbe | |||
8ea85586d5 | |||
d1633fd155 | |||
26718a41e0 | |||
2c241d9113 | |||
adea1eeb02 | |||
f441caebf7 | |||
48615b3438 | |||
bc3b3a4109 | |||
5ec246228e | |||
579a611bc2 | |||
62379f54b0 | |||
e7dc369c4f | |||
15b713215f | |||
e57432767d | |||
8b9e0bfe54 | |||
a92cc00291 | |||
b4aa72440d | |||
b8096833b3 | |||
777d263efe | |||
84f1ec2f70 | |||
8d7591b766 | |||
88d57e217f | |||
7d08f6c5cf | |||
e5e85b68ff | |||
f6f167dd83 | |||
8ef8e0dad9 | |||
0b937186aa | |||
a5e10e26cc | |||
029ee8e9a8 | |||
7947d64b3b | |||
778b4e2f39 | |||
02f08093c9 | |||
c2939f81c0 | |||
8ae728f6bf | |||
f6179d1fae | |||
52fdd5c471 | |||
42548d437c | |||
be66f52cbf | |||
992195b9a0 | |||
991eea4d54 | |||
064884dc07 | |||
98e511056c | |||
58102bab02 |
6
.gitignore
vendored
@ -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/*
|
||||
|
30
.metadata
@ -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
|
||||
|
||||
|
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 me
|
||||
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:
|
||||
|
||||
|
@ -1,6 +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.
|
||||
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.
|
||||
|
44
android/app/build.gradle.kts
Normal 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 = "../.."
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="Recurring 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>
|
||||
|
@ -1,5 +0,0 @@
|
||||
package com.example.flutter_empty
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity()
|
@ -0,0 +1,5 @@
|
||||
package com.hyperling.expense_tracker
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
BIN
android/app/src/main/res/mipmap-hdpi/icon_v003.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/icon_v003_round.png
Normal file
After Width: | Height: | Size: 35 KiB |
21
android/build.gradle.kts
Normal 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)
|
||||
}
|
25
android/settings.gradle.kts
Normal 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
@ -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
|
185
lib/db.dart
@ -3,13 +3,17 @@
|
||||
// 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/tracked_type_recurring.dart';
|
||||
import '/models/income.dart';
|
||||
import '/models/asset.dart';
|
||||
import '/models/tracked_item.dart';
|
||||
|
||||
// Leaned on this example:
|
||||
// https://learnflutterwithme.com/sqlite
|
||||
@ -22,7 +26,7 @@ class DatabaseHelper {
|
||||
|
||||
Future<Database> _initDatabase() async {
|
||||
Directory documentsDirectory = await getApplicationDocumentsDirectory();
|
||||
String path = join(documentsDirectory.path, "com_hyperling_expense.db");
|
||||
String path = join(documentsDirectory.path, "ExpenseTracker.sqlite.db");
|
||||
return await openDatabase(
|
||||
path,
|
||||
version: 2,
|
||||
@ -32,6 +36,26 @@ class DatabaseHelper {
|
||||
}
|
||||
|
||||
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
|
||||
@ -39,45 +63,49 @@ class DatabaseHelper {
|
||||
, cost DOUBLE NOT NULL
|
||||
, frequency TEXT NOT NULL
|
||||
, description TEXT
|
||||
)
|
||||
""");
|
||||
)""");
|
||||
}
|
||||
|
||||
Future _onUpgrade(Database db, int previousVersion, int newVersion) async {
|
||||
// Added in DB version 2.
|
||||
if (previousVersion < 2 && newVersion >= 2) {
|
||||
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
|
||||
)
|
||||
""");
|
||||
await db.execute("""
|
||||
CREATE TABLE asset
|
||||
( id INTEGER PRIMARY KEY
|
||||
, name TEXT NOT NULL UNIQUE
|
||||
, amount DOUBLE 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(RecurringTrackedType expense) async {
|
||||
Future<int> addExpense(TrackedItem expense) async {
|
||||
Database db = await instance.db;
|
||||
return await db.insert(
|
||||
"expense",
|
||||
@ -94,7 +122,7 @@ class DatabaseHelper {
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> updateExpense(RecurringTrackedType expense) async {
|
||||
Future<int> updateExpense(TrackedItem expense) async {
|
||||
Database db = await instance.db;
|
||||
return await db.update(
|
||||
"expense",
|
||||
@ -118,9 +146,112 @@ class DatabaseHelper {
|
||||
|
||||
/// 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
@ -0,0 +1 @@
|
||||
const bool testing = false;
|
@ -2,6 +2,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// Local
|
||||
import '/globals.dart';
|
||||
import '/pages/home.dart';
|
||||
|
||||
// SQLite
|
||||
@ -10,8 +11,6 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
const bool testing = false;
|
||||
|
||||
void main() {
|
||||
// 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.
|
||||
@ -24,10 +23,11 @@ void main() {
|
||||
}
|
||||
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, 'com_hyperling_expense.db');
|
||||
Directory documentsDirectory = await getApplicationDocumentsDirectory();
|
||||
String path = join(documentsDirectory.path, 'ExpenseTracker.sqlite.db');
|
||||
await deleteDatabase(path);
|
||||
}();
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
import '/models/tracked_type.dart';
|
||||
// Local
|
||||
import '/models/item_type.dart';
|
||||
import '/models/tracked_item.dart';
|
||||
|
||||
class Asset extends TrackedType {
|
||||
class Asset extends TrackedItem {
|
||||
static String amountText = "Amount";
|
||||
|
||||
Asset({
|
||||
super.id,
|
||||
super.type = ItemType.asset,
|
||||
required super.name,
|
||||
required super.amount,
|
||||
required super.description,
|
||||
|
@ -29,7 +29,7 @@ class DatabaseBackup {
|
||||
'id': e.id,
|
||||
'name': e.name,
|
||||
'cost': e.amount,
|
||||
'frequency': e.frequency.title,
|
||||
'frequency': e.frequency!.title,
|
||||
'description': e.description,
|
||||
},
|
||||
],
|
||||
@ -39,7 +39,7 @@ class DatabaseBackup {
|
||||
'id': i.id,
|
||||
'name': i.name,
|
||||
'revenue': i.amount,
|
||||
'frequency': i.frequency.title,
|
||||
'frequency': i.frequency!.title,
|
||||
'description': i.description,
|
||||
},
|
||||
],
|
||||
|
@ -1,12 +1,15 @@
|
||||
// Local
|
||||
import '/models/tracked_type_recurring.dart';
|
||||
import '/models/item_type.dart';
|
||||
|
||||
import '/models/tracked_item.dart';
|
||||
import '/models/frequency.dart';
|
||||
|
||||
class Expense extends RecurringTrackedType {
|
||||
class Expense extends TrackedItem {
|
||||
static String amountText = "Cost";
|
||||
|
||||
Expense({
|
||||
super.id,
|
||||
super.type = ItemType.expense,
|
||||
required super.name,
|
||||
required super.amount,
|
||||
required super.frequency,
|
||||
@ -29,7 +32,7 @@ class Expense extends RecurringTrackedType {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'cost': amount,
|
||||
'frequency': frequency.title,
|
||||
'frequency': frequency!.title,
|
||||
'description': description,
|
||||
};
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
// Local
|
||||
import '/models/tracked_type_recurring.dart';
|
||||
import '/models/tracked_item.dart';
|
||||
import '/models/frequency.dart';
|
||||
import '/models/item_type.dart';
|
||||
|
||||
class Income extends RecurringTrackedType {
|
||||
class Income extends TrackedItem {
|
||||
static String amountText = "Revenue";
|
||||
|
||||
Income({
|
||||
super.id,
|
||||
super.type = ItemType.income,
|
||||
required super.name,
|
||||
required super.amount,
|
||||
required super.frequency,
|
||||
@ -29,7 +31,7 @@ class Income extends RecurringTrackedType {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'revenue': amount,
|
||||
'frequency': frequency.title,
|
||||
'frequency': frequency!.title,
|
||||
'description': description,
|
||||
};
|
||||
}
|
||||
|
22
lib/models/item_type.dart
Normal file
@ -0,0 +1,22 @@
|
||||
enum ItemType {
|
||||
expense(
|
||||
title: "Expense",
|
||||
plural: "Expenses",
|
||||
),
|
||||
income(
|
||||
title: "Income",
|
||||
plural: "Incomes",
|
||||
),
|
||||
asset(
|
||||
title: "Asset",
|
||||
plural: "Assets",
|
||||
);
|
||||
|
||||
const ItemType({
|
||||
required this.title,
|
||||
required this.plural,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String plural;
|
||||
}
|
55
lib/models/tracked_item.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
abstract class TrackedType {
|
||||
int? id;
|
||||
String name;
|
||||
double amount;
|
||||
String description;
|
||||
|
||||
TrackedType({
|
||||
this.id,
|
||||
required this.name,
|
||||
required this.amount,
|
||||
required this.description,
|
||||
});
|
||||
|
||||
static String amountText = "Amount";
|
||||
String getAmountText() => amountText;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return toMap().toString();
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'amount': amount,
|
||||
'description': description,
|
||||
};
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
// Local
|
||||
import '/models/tracked_type.dart';
|
||||
import '/models/frequency.dart';
|
||||
|
||||
abstract class RecurringTrackedType extends TrackedType {
|
||||
Frequency frequency;
|
||||
|
||||
RecurringTrackedType({
|
||||
super.id,
|
||||
required super.name,
|
||||
required super.amount,
|
||||
required this.frequency,
|
||||
required super.description,
|
||||
});
|
||||
|
||||
double calcComparableAmountYearly() {
|
||||
return amount * frequency.timesPerYear;
|
||||
}
|
||||
|
||||
double calcComparableAmountDaily() {
|
||||
return amount / frequency.numDays;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'amount': amount,
|
||||
'frequency': frequency.title,
|
||||
'description': description,
|
||||
};
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AssetPage extends StatelessWidget {
|
||||
const AssetPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Placeholder();
|
||||
}
|
||||
}
|
@ -1,4 +1,21 @@
|
||||
// Flutter
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher_string.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({
|
||||
@ -21,9 +38,24 @@ class HelpPage extends StatelessWidget {
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text("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(
|
||||
"\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 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 app updates, install the Obtanium"
|
||||
" app, 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.",
|
||||
),
|
||||
//Text("Another paragraph.")
|
||||
],
|
||||
),
|
||||
@ -41,9 +73,13 @@ class HelpPage extends StatelessWidget {
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
child: TextButton.icon(
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
_launchSite(
|
||||
"https://git.hyperling.com/me/flutter-expense-tracker",
|
||||
);
|
||||
},
|
||||
icon: Icon(Icons.code),
|
||||
label: Text("Code Repository"),
|
||||
label: Text("Source Code"),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -57,7 +93,11 @@ class HelpPage extends StatelessWidget {
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
child: TextButton.icon(
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
_launchSite(
|
||||
"https://hyperling.com",
|
||||
);
|
||||
},
|
||||
icon: Icon(Icons.web_asset),
|
||||
label: Text("Personal Website"),
|
||||
),
|
||||
|
@ -1,14 +1,14 @@
|
||||
// Flutter
|
||||
import 'package:flutter/material.dart';
|
||||
import '/models/item_type.dart';
|
||||
import 'dart:io';
|
||||
|
||||
// Local
|
||||
import '/pages/expense.dart';
|
||||
import '/pages/income.dart';
|
||||
import '/pages/asset.dart';
|
||||
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({
|
||||
@ -32,20 +32,44 @@ class _HomePageState extends State<HomePage> {
|
||||
Widget? dialog;
|
||||
switch (pageSelected) {
|
||||
case 0:
|
||||
page = ExpensePage();
|
||||
dialog = RecurringTrackedTypeInputDialog(
|
||||
page = TrackedItemPage(
|
||||
assetsToLoad: DatabaseHelper.instance.getExpenses(),
|
||||
notifyParent: refresh,
|
||||
);
|
||||
dialog = TrackedItemInputDialog(
|
||||
notifyParent: refresh,
|
||||
type: ItemType.expense,
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
page = IncomePage();
|
||||
page = TrackedItemPage(
|
||||
assetsToLoad: DatabaseHelper.instance.getIncomes(),
|
||||
notifyParent: refresh,
|
||||
);
|
||||
dialog = TrackedItemInputDialog(
|
||||
notifyParent: refresh,
|
||||
type: ItemType.income,
|
||||
);
|
||||
break;
|
||||
case 2:
|
||||
page = AssetPage();
|
||||
page = TrackedItemPage(
|
||||
assetsToLoad: DatabaseHelper.instance.getAssets(),
|
||||
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!');
|
||||
}
|
||||
@ -112,10 +136,11 @@ class _HomePageState extends State<HomePage> {
|
||||
child: Center(child: page),
|
||||
);
|
||||
|
||||
Widget? drawer, body;
|
||||
Widget? drawer;
|
||||
Widget body;
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
drawer = navigation;
|
||||
body = main;
|
||||
body = SafeArea(child: main);
|
||||
} else {
|
||||
drawer = null;
|
||||
body = Row(
|
||||
@ -134,6 +159,9 @@ class _HomePageState extends State<HomePage> {
|
||||
drawer: drawer,
|
||||
body: body,
|
||||
floatingActionButton: floatingButton,
|
||||
extendBody: false,
|
||||
extendBodyBehindAppBar: false,
|
||||
resizeToAvoidBottomInset: true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class IncomePage extends StatelessWidget {
|
||||
const IncomePage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Placeholder();
|
||||
}
|
||||
}
|
@ -1,19 +1,23 @@
|
||||
// Flutter
|
||||
import 'package:flutter/material.dart';
|
||||
import '/models/item_type.dart';
|
||||
|
||||
// Local
|
||||
import 'package:flutter_expense_tracker/db.dart';
|
||||
import 'package:flutter_expense_tracker/models/asset.dart';
|
||||
import 'package:flutter_expense_tracker/models/expense.dart';
|
||||
import 'package:flutter_expense_tracker/models/income.dart';
|
||||
import '/db.dart';
|
||||
import '/widgets/cards.dart';
|
||||
import '/models/tracked_item.dart';
|
||||
|
||||
/// TODO:
|
||||
/// - Expenses (total number, totals by day / month / year)
|
||||
/// - Incomes (total number, totals by day / month / year)
|
||||
/// - Assets (total number, total by day / month / year)
|
||||
/// - Projected Assets in:
|
||||
/// - 1 week, 1 month, 1 quarter, 1 year
|
||||
/// - 1/2 year? 2 years? 5 years? Allow customization?
|
||||
/// - 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;
|
||||
|
||||
class ProjectionPage extends StatefulWidget {
|
||||
const ProjectionPage({
|
||||
@ -25,87 +29,185 @@ class ProjectionPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ProjectionPageState extends State<ProjectionPage> {
|
||||
late Future<List<Expense>> expenses;
|
||||
late Future<List<Income>> incomes;
|
||||
late Future<List<Asset>> assets;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
/***** Load DB Data *****/
|
||||
// Expenses
|
||||
expenses = DatabaseHelper.instance.getExpenses();
|
||||
|
||||
// Income
|
||||
//incomes = DatabaseHelper.instance.getIncomes();
|
||||
|
||||
// Assets
|
||||
//assets = DatabaseHelper.instance.getAssets();
|
||||
}
|
||||
|
||||
@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,
|
||||
);
|
||||
Widget incomeSummary = SummaryCardForTotals(
|
||||
list: DatabaseHelper.instance.getIncomes(),
|
||||
summaryTypeLabel: ItemType.income.title,
|
||||
);
|
||||
Widget assetSummary = SummaryCardForTotals(
|
||||
list: DatabaseHelper.instance.getAssets(),
|
||||
summaryTypeLabel: ItemType.asset.title,
|
||||
);
|
||||
|
||||
// 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));
|
||||
|
||||
// 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: "",
|
||||
);
|
||||
|
||||
// Return all of the UI elements.
|
||||
return ListView(
|
||||
children: [
|
||||
SummaryCard(
|
||||
name: "Expense Totals",
|
||||
leftText: "left",
|
||||
middleText: "middle",
|
||||
rightText: "right",
|
||||
),
|
||||
SummaryCard(
|
||||
name: "Income Totals",
|
||||
leftText: "left",
|
||||
middleText: "middle",
|
||||
rightText: "right",
|
||||
),
|
||||
SummaryCard(
|
||||
name: "Asset Totals",
|
||||
leftText: "left",
|
||||
middleText: "middle",
|
||||
rightText: "right",
|
||||
),
|
||||
TitleCard(title: "Summaries"),
|
||||
expenseSummary,
|
||||
incomeSummary,
|
||||
assetSummary,
|
||||
TitleCard(title: "Projections"),
|
||||
proj1,
|
||||
proj2,
|
||||
proj3,
|
||||
proj4,
|
||||
proj5,
|
||||
proj6,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SummaryCard extends StatelessWidget {
|
||||
const SummaryCard({
|
||||
class SummaryCardForTotals extends StatelessWidget {
|
||||
const SummaryCardForTotals({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.leftText,
|
||||
required this.middleText,
|
||||
required this.rightText,
|
||||
required this.list,
|
||||
required this.summaryTypeLabel,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String leftText;
|
||||
final String middleText;
|
||||
final String rightText;
|
||||
final Future<List<TrackedItem>> list;
|
||||
final String summaryTypeLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Center(
|
||||
child: Text(name),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(leftText),
|
||||
Spacer(),
|
||||
Text(middleText),
|
||||
Spacer(),
|
||||
Text(rightText),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
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;
|
||||
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.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 null:
|
||||
break;
|
||||
|
||||
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,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,10 @@ class SettingsPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Placeholder();
|
||||
return Center(
|
||||
child: Text(
|
||||
"No settings yet. :)",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +1,38 @@
|
||||
// Flutter
|
||||
import 'package:flutter/material.dart';
|
||||
import '/models/asset.dart';
|
||||
import '/models/income.dart';
|
||||
|
||||
// Local
|
||||
import '/models/tracked_type.dart';
|
||||
import '/models/tracked_type_recurring.dart';
|
||||
import '/models/tracked_item.dart';
|
||||
import '/models/item_type.dart';
|
||||
import '/models/expense.dart';
|
||||
import '/models/frequency.dart';
|
||||
import '/db.dart';
|
||||
|
||||
// TODO: Make this a generic UI based on a superclass of Expense, Income, and Assets.
|
||||
class TrackedItemPage extends StatefulWidget {
|
||||
final Future<List<TrackedItem>> assetsToLoad;
|
||||
final Function() notifyParent;
|
||||
|
||||
class ExpensePage extends StatefulWidget {
|
||||
const ExpensePage({
|
||||
const TrackedItemPage({
|
||||
super.key,
|
||||
required this.assetsToLoad,
|
||||
required this.notifyParent,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ExpensePage> createState() => _ExpensePageState();
|
||||
State<TrackedItemPage> createState() => _TrackedItemPageState();
|
||||
}
|
||||
|
||||
class _ExpensePageState extends State<ExpensePage> {
|
||||
refresh() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
class _TrackedItemPageState extends State<TrackedItemPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return FutureBuilder<List<RecurringTrackedType>>(
|
||||
future: DatabaseHelper.instance.getExpenses(),
|
||||
builder: (BuildContext context,
|
||||
AsyncSnapshot<List<RecurringTrackedType>> snapshot) {
|
||||
return FutureBuilder<List<TrackedItem>>(
|
||||
future: widget.assetsToLoad,
|
||||
builder:
|
||||
(BuildContext context, AsyncSnapshot<List<TrackedItem>> snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Text('Loading...');
|
||||
}
|
||||
@ -42,65 +43,78 @@ class _ExpensePageState extends State<ExpensePage> {
|
||||
);
|
||||
return snapshot.data!.isEmpty
|
||||
? Text(
|
||||
"Add expenses to get started.",
|
||||
"Add items to get started.",
|
||||
softWrap: true,
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: snapshot.data!.length,
|
||||
itemBuilder: (_, index) {
|
||||
//List<Expense> expenses = snapshot.data!;
|
||||
final RecurringTrackedType curr = snapshot.data![index];
|
||||
final TrackedItem curr = snapshot.data![index];
|
||||
|
||||
final itemKey = Key(curr.id!.toString());
|
||||
final String itemTitle = curr.name;
|
||||
|
||||
final String itemAmount;
|
||||
// TODO: How can we do RecurringTrackedType vs TrackedType here
|
||||
// if the Widgets are expecting RecurringTrackedType, but we
|
||||
// need to be using Frequency? Change to only have one abstract
|
||||
// class and make it nully again? Hmmm...
|
||||
if (curr is RecurringTrackedType) {
|
||||
if (curr.frequency != null) {
|
||||
itemAmount =
|
||||
"${curr.amount.toStringAsFixed(2)} ${curr.frequency.title}";
|
||||
"${curr.amount.toStringAsFixed(2)} ${curr.frequency!.title}";
|
||||
} else {
|
||||
itemAmount = curr.amount.toStringAsFixed(2);
|
||||
}
|
||||
final String itemDescription = curr.description;
|
||||
|
||||
final double itemDayAmount =
|
||||
curr.calcComparableAmountDaily();
|
||||
final String estimateSymbolDaily = curr.frequency.numDays
|
||||
.toStringAsFixed(2)
|
||||
.endsWith(".00") &&
|
||||
itemDayAmount.toStringAsFixed(3).endsWith("0")
|
||||
? ""
|
||||
: "~";
|
||||
final double itemDayAmount, itemMonthAmount, itemYearAmount;
|
||||
final String estimateSymbolDaily,
|
||||
estimateSymbolMonthly,
|
||||
estimateSymbolYearly;
|
||||
|
||||
final double itemMonthAmount =
|
||||
(curr.calcComparableAmountYearly() / 12);
|
||||
final String estimateSymbolMonthly = curr
|
||||
.frequency.timesPerYear
|
||||
.toStringAsFixed(2)
|
||||
.endsWith(".00") &&
|
||||
itemMonthAmount.toStringAsFixed(3).endsWith("0")
|
||||
? ""
|
||||
: "~";
|
||||
if (curr.frequency != null) {
|
||||
itemDayAmount = curr.calcComparableAmountDaily();
|
||||
estimateSymbolDaily = curr.frequency!.numDays
|
||||
.toStringAsFixed(2)
|
||||
.endsWith(".00") &&
|
||||
itemDayAmount.toStringAsFixed(3).endsWith("0")
|
||||
? ""
|
||||
: "~";
|
||||
|
||||
final double itemYearAmount =
|
||||
curr.calcComparableAmountYearly();
|
||||
final String estimateSymbolYearly = curr
|
||||
.frequency.timesPerYear
|
||||
.toStringAsFixed(2)
|
||||
.endsWith(".00") &&
|
||||
itemYearAmount.toStringAsFixed(3).endsWith("0")
|
||||
? ""
|
||||
: "~";
|
||||
itemMonthAmount =
|
||||
(curr.calcComparableAmountYearly() / 12);
|
||||
estimateSymbolMonthly = curr.frequency!.timesPerYear
|
||||
.toStringAsFixed(2)
|
||||
.endsWith(".00") &&
|
||||
itemMonthAmount.toStringAsFixed(3).endsWith("0")
|
||||
? ""
|
||||
: "~";
|
||||
|
||||
final String itemTopText =
|
||||
"$estimateSymbolDaily${itemDayAmount.toStringAsFixed(2)} ${Frequency.daily.title}";
|
||||
final String itemMiddleText =
|
||||
"$estimateSymbolMonthly${itemMonthAmount.toStringAsFixed(2)} ${Frequency.monthly.title}";
|
||||
final String itemBottomText =
|
||||
"$estimateSymbolYearly${itemYearAmount.toStringAsFixed(2)} ${Frequency.yearly.title}";
|
||||
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),
|
||||
@ -131,17 +145,34 @@ class _ExpensePageState extends State<ExpensePage> {
|
||||
snapshot.data!.remove(curr);
|
||||
switch (direction) {
|
||||
case DismissDirection.startToEnd:
|
||||
DatabaseHelper.instance.removeExpense(curr.id!);
|
||||
// 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: RecurringTrackedTypeInputDialog(
|
||||
notifyParent: refresh,
|
||||
content: TrackedItemInputDialog(
|
||||
notifyParent: widget.notifyParent,
|
||||
entry: curr,
|
||||
amountText: curr.getAmountText(),
|
||||
type: curr.type!,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -180,7 +211,7 @@ class _ExpensePageState extends State<ExpensePage> {
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
curr.description,
|
||||
itemDescription,
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
),
|
||||
@ -218,45 +249,53 @@ class _ExpensePageState extends State<ExpensePage> {
|
||||
}
|
||||
}
|
||||
|
||||
class RecurringTrackedTypeInputDialog extends StatefulWidget {
|
||||
class TrackedItemInputDialog extends StatefulWidget {
|
||||
final Function() notifyParent;
|
||||
final RecurringTrackedType? entry;
|
||||
final TrackedItem? entry;
|
||||
final String? amountText;
|
||||
final ItemType? type;
|
||||
|
||||
const RecurringTrackedTypeInputDialog({
|
||||
const TrackedItemInputDialog({
|
||||
super.key,
|
||||
required this.notifyParent,
|
||||
this.entry,
|
||||
this.amountText,
|
||||
this.type,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RecurringTrackedTypeInputDialog> createState() =>
|
||||
_RecurringTrackedTypeInputDialogState();
|
||||
State<TrackedItemInputDialog> createState() => _TrackedItemInputDialogState();
|
||||
}
|
||||
|
||||
class _RecurringTrackedTypeInputDialogState
|
||||
extends State<RecurringTrackedTypeInputDialog> {
|
||||
final _expenseFormKey = GlobalKey<FormState>();
|
||||
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;
|
||||
_freq = widget.entry!.frequency;
|
||||
widget.entry!.frequency == null ? null : _freq = widget.entry!.frequency!;
|
||||
_desc = widget.entry!.description;
|
||||
_type = widget.entry!.type!;
|
||||
}
|
||||
|
||||
String amountText =
|
||||
widget.amountText != null ? widget.amountText! : TrackedType.amountText;
|
||||
widget.amountText != null ? widget.amountText! : TrackedItem.amountText;
|
||||
|
||||
return Column(
|
||||
// prevent AlertDialog from taking full vertical height.
|
||||
@ -268,7 +307,20 @@ class _RecurringTrackedTypeInputDialogState
|
||||
onPressed: () {
|
||||
if (widget.entry != null) {
|
||||
setState(() {
|
||||
DatabaseHelper.instance.addExpense(widget.entry!);
|
||||
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();
|
||||
});
|
||||
}
|
||||
@ -281,8 +333,8 @@ class _RecurringTrackedTypeInputDialogState
|
||||
insetPadding: EdgeInsets.all(0),
|
||||
title: Center(
|
||||
child: widget.entry == null
|
||||
? Text("New Expense")
|
||||
: Text("Edit Expense"),
|
||||
? Text("New ${_type!.title}")
|
||||
: Text("Edit ${_type!.title}"),
|
||||
),
|
||||
content: FutureBuilder<List<Expense>>(
|
||||
future: DatabaseHelper.instance.getExpenses(),
|
||||
@ -293,7 +345,7 @@ class _RecurringTrackedTypeInputDialogState
|
||||
}
|
||||
List<Expense> expenses = snapshot.data!;
|
||||
return Form(
|
||||
key: _expenseFormKey,
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@ -350,46 +402,47 @@ class _RecurringTrackedTypeInputDialogState
|
||||
_amount = double.parse(value!);
|
||||
},
|
||||
),
|
||||
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),
|
||||
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),
|
||||
)
|
||||
.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!;
|
||||
},
|
||||
),
|
||||
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,
|
||||
@ -416,28 +469,73 @@ class _RecurringTrackedTypeInputDialogState
|
||||
Center(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
if (_expenseFormKey.currentState!.validate()) {
|
||||
_expenseFormKey.currentState!.save();
|
||||
setState(() {
|
||||
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,
|
||||
);
|
||||
}
|
||||
});
|
||||
widget.notifyParent();
|
||||
Navigator.of(context).pop();
|
||||
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),
|
87
lib/widgets/cards.dart
Normal file
@ -0,0 +1,87 @@
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
@ -7,8 +7,10 @@ import Foundation
|
||||
|
||||
import path_provider_foundation
|
||||
import sqflite_darwin
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
BIN
media/icon_v001.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
media/icon_v001.xcf
Normal file
BIN
media/icon_v001_round.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
media/icon_v002.png
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
media/icon_v002.xcf
Normal file
BIN
media/icon_v002_round.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
media/icon_v003.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
media/icon_v003.xcf
Normal file
BIN
media/icon_v003_round.png
Normal file
After Width: | Height: | Size: 35 KiB |
185
pubspec.lock
@ -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"
|
||||
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: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
|
||||
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.4"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -75,22 +75,27 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
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 +116,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 +132,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
version: "1.16.0"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -151,10 +156,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
|
||||
sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.15"
|
||||
version: "2.2.16"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -212,50 +217,50 @@ 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: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb"
|
||||
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "2.4.2"
|
||||
sqflite_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_android
|
||||
sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
|
||||
sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.4.1"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709"
|
||||
sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.4+6"
|
||||
version: "2.5.5"
|
||||
sqflite_common_ffi:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite_common_ffi
|
||||
sha256: "883dd810b2b49e6e8c3b980df1829ef550a94e3f87deab5d864917d27ca6bf36"
|
||||
sha256: "1f3ef3888d3bfbb47785cc1dda0dc7dd7ebd8c1955d32a9e8e9dae1e38d1c4c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.4+4"
|
||||
version: "2.3.5"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_darwin
|
||||
sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c"
|
||||
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1+1"
|
||||
version: "2.4.2"
|
||||
sqflite_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -268,58 +273,58 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "35d3726fe18ab1463403a5cc8d97dbc81f2a0b08082e8173851363fcc97b6627"
|
||||
sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.2"
|
||||
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: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
|
||||
sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.0+3"
|
||||
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:
|
||||
@ -328,6 +333,70 @@ packages:
|
||||
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:
|
||||
@ -340,18 +409,18 @@ 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: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -361,5 +430,5 @@ packages:
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
sdks:
|
||||
dart: ">=3.6.1 <4.0.0"
|
||||
flutter: ">=3.24.0"
|
||||
dart: ">=3.7.0 <4.0.0"
|
||||
flutter: ">=3.27.0"
|
||||
|
@ -1,7 +1,7 @@
|
||||
name: flutter_expense_tracker
|
||||
name: expense_tracker
|
||||
description: Track recurring expenses against income and liquid assets.
|
||||
publish_to: 'none'
|
||||
version: 0.1.0
|
||||
version: 0.1.1
|
||||
|
||||
environment:
|
||||
sdk: ^3.6.1
|
||||
@ -13,6 +13,7 @@ dependencies:
|
||||
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:
|
||||
|
30
test/widget_test.dart
Normal file
@ -0,0 +1,30 @@
|
||||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.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 MainApp());
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsNothing);
|
||||
|
||||
// Tap the '+' icon and trigger a frame.
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pump();
|
||||
|
||||
// Verify that our counter has incremented.
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
});
|
||||
}
|
@ -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"));
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|