46 Commits

Author SHA1 Message Date
fa852faadc Use the dispose() method to reset the global values. Works excellently!! :D 2025-03-26 13:17:08 -07:00
6f9d0d8afb The initial page load now delays properly and shows the correct value rather than 0. Updating an item and going back to the page still shows old data though. 2025-03-26 12:39:09 -07:00
75cc72678b Add TBD of where the Income duplicate name validator is going wrong. 2025-03-25 08:45:44 -07:00
2970431b91 Increment the version. 2025-03-21 15:03:16 -07:00
4d0dc03c69 Create script which builds the delverables and places them in a convenient place with convenient names. 2025-03-21 14:58:56 -07:00
bdb3fc5c7a Ensure the README is up to date. Reword slightly. 2025-03-21 14:22:14 -07:00
da2ae6206a Add additional Help content and modify the Code Repo button to more specifically be Source Code for the project. 2025-03-21 14:21:43 -07:00
f5b22c6c1c Create and use a better app icon. 2025-03-21 14:08:26 -07:00
9f7773f724 Add functionality to Help buttons for opening URLs. 2025-03-21 13:40:11 -07:00
dec343af09 Add plugin url_launcher for Help page. 2025-03-21 12:17:10 -07:00
0890e15bfb Change the double SafeArea for Linux. 2025-03-21 12:05:59 -07:00
55fd4092ac Prevent the Help buttons from being underneath the Nav Bar on Android. 2025-03-21 12:05:03 -07:00
247b88daa2 Replace placeholder with a basic message for now. 2025-03-21 12:04:48 -07:00
d604a59ad1 Accept a recommended change since null is now handled properly. 2025-03-21 11:39:23 -07:00
5bf6a0889c Finish changing the database name. 2025-03-21 11:38:50 -07:00
f3d6b8abbe Increment the app version slightly now that it's working and has an icon. 2025-03-21 11:03:02 -07:00
8ea85586d5 Begin using the old project's icon. 2025-03-21 11:02:57 -07:00
d1633fd155 Add media from android-expense-tracker project. 2025-03-21 10:54:29 -07:00
26718a41e0 Shorten app name for home screen. 2025-03-21 10:54:14 -07:00
2c241d9113 Change database file name to be more user-friendly. 2025-03-21 10:53:48 -07:00
adea1eeb02 Use the non-KTS files after all, the KTS versions do not launch successfully. 2025-03-21 10:34:56 -07:00
f441caebf7 Unimplemented got thrown for null. Add it to the list so that lists with no items still work properly. 2025-03-21 10:34:25 -07:00
48615b3438 Hide another non-KTS gradle file to ensure that Android config is done properly. 2025-03-21 10:27:41 -07:00
bc3b3a4109 Ensure project is properly named across all files. 2025-03-21 10:24:46 -07:00
5ec246228e Ran flutter create . to create new files based on changed package name. 2025-03-21 09:32:45 -07:00
579a611bc2 Update packages. 2025-03-21 09:32:26 -07:00
62379f54b0 Pull the basic card types out into their own file for possible reuse. 2025-03-08 08:40:08 -07:00
e7dc369c4f Projections are working! Just 1 issue noticed so far. 2025-03-08 08:33:31 -07:00
15b713215f Add item count per category. 2025-03-08 07:51:23 -07:00
e57432767d Formatting improvement. 2025-03-08 07:41:59 -07:00
8b9e0bfe54 Assets are now being displayed properly. 2025-03-08 07:40:03 -07:00
a92cc00291 Flutter pub get updated more things. 2025-03-08 07:24:49 -07:00
b4aa72440d Futter changed this. 2025-03-08 06:57:47 -07:00
b8096833b3 Turn off testing / debug. 2025-03-08 06:57:26 -07:00
777d263efe Start loading incomes and assets for reporting. 2025-03-07 13:36:24 -07:00
84f1ec2f70 Add, update, and delete are all working for each item type!! 2025-03-07 13:32:11 -07:00
8d7591b766 Continue trying to fix issue with edited items not notifying parent. 2025-03-07 13:26:02 -07:00
88d57e217f Fix the Monthly text for assets now that it has its proper type. 2025-03-07 13:25:55 -07:00
7d08f6c5cf Add type to objects. 2025-03-07 13:24:17 -07:00
e5e85b68ff Income and Assets are successfully being inserted and selected! Still needs tweaking, such as Asset saying "monthly". 2025-03-07 13:06:26 -07:00
f6f167dd83 Fix local variable warning. 2025-03-07 12:56:14 -07:00
8ef8e0dad9 Start filling in TODO's with Income and Asset code.. 2025-03-07 12:55:34 -07:00
0b937186aa Add break statements, even if unnecessary. 2025-03-07 12:54:01 -07:00
a5e10e26cc Move testing to be a global variable and use it to control the debug output. 2025-03-07 12:47:40 -07:00
029ee8e9a8 Add DEBUG messages to get methods. Fix the income and asset tables not existing if db upgrade is not run. 2025-03-07 12:43:26 -07:00
7947d64b3b Allow the Income and Asset pages to start using the generic item creation page. 2025-03-07 12:41:52 -07:00
42 changed files with 878 additions and 264 deletions

3
.gitignore vendored
View File

@ -44,3 +44,6 @@ app.*.map.json
/android/app/release
/android/app/.*
# Ignore the releases folder.
releases/*

View File

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

View File

@ -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.

View File

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

View File

@ -1,8 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="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>

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

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

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

View File

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

46
create_release_files.sh Executable file
View File

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

View File

@ -3,11 +3,13 @@
// SQLite
import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
// Local
import "/globals.dart";
import '/models/expense.dart';
import '/models/income.dart';
import '/models/asset.dart';
@ -24,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,
@ -34,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
@ -41,13 +63,10 @@ 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) {
void _createIncome(Database db) async {
await db.execute("""
CREATE TABLE income
( id INTEGER PRIMARY KEY
@ -55,27 +74,34 @@ class DatabaseHelper {
, 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;
}
@ -121,11 +147,18 @@ 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;
}
@ -169,10 +202,17 @@ class DatabaseHelper {
///
/// 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;
}

1
lib/globals.dart Normal file
View File

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

View File

@ -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');
String path = join(documentsDirectory.path, 'ExpenseTracker.sqlite.db');
await deleteDatabase(path);
}();
}

View File

@ -1,4 +1,5 @@
// Local
import '/models/item_type.dart';
import '/models/tracked_item.dart';
class Asset extends TrackedItem {
@ -6,6 +7,7 @@ class Asset extends TrackedItem {
Asset({
super.id,
super.type = ItemType.asset,
required super.name,
required super.amount,
required super.description,

View File

@ -1,5 +1,5 @@
// Local
import 'package:flutter_expense_tracker/models/item_type.dart';
import '/models/item_type.dart';
import '/models/tracked_item.dart';
import '/models/frequency.dart';

View File

@ -1,12 +1,14 @@
// Local
import '/models/tracked_item.dart';
import '/models/frequency.dart';
import '/models/item_type.dart';
class Income extends TrackedItem {
static String amountText = "Revenue";
Income({
super.id,
super.type = ItemType.income,
required super.name,
required super.amount,
required super.frequency,

View File

@ -1,5 +1,5 @@
// Local
import 'package:flutter_expense_tracker/models/item_type.dart';
import '/models/item_type.dart';
import '/models/frequency.dart';
@ -37,12 +37,14 @@ abstract class TrackedItem {
}
Map<String, dynamic> toMap() {
return frequency == null ? {
return frequency == null
? {
'id': id,
'name': name,
'amount': amount,
'description': description,
} : {
}
: {
'id': id,
'name': name,
'amount': amount,

View File

@ -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"),
),

View File

@ -1,6 +1,6 @@
// Flutter
import 'package:flutter/material.dart';
import 'package:flutter_expense_tracker/models/item_type.dart';
import '/models/item_type.dart';
import 'dart:io';
// Local
@ -34,21 +34,42 @@ class _HomePageState extends State<HomePage> {
case 0:
page = TrackedItemPage(
assetsToLoad: DatabaseHelper.instance.getExpenses(),
notifyParent: refresh,
);
dialog = TrackedItemInputDialog(
notifyParent: refresh,
type: ItemType.expense,
);
break;
case 1:
page = Placeholder();
page = TrackedItemPage(
assetsToLoad: DatabaseHelper.instance.getIncomes(),
notifyParent: refresh,
);
dialog = TrackedItemInputDialog(
notifyParent: refresh,
type: ItemType.income,
);
break;
case 2:
page = Placeholder();
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!');
}
@ -115,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(
@ -137,6 +159,9 @@ class _HomePageState extends State<HomePage> {
drawer: drawer,
body: body,
floatingActionButton: floatingButton,
extendBody: false,
extendBodyBehindAppBar: false,
resizeToAvoidBottomInset: true,
);
});
}

View File

@ -1,17 +1,25 @@
// Flutter
import 'dart:async';
import 'package:flutter/material.dart';
import '/models/item_type.dart';
// Local
import '/db.dart';
import '/widgets/cards.dart';
import '/models/tracked_item.dart';
/// TODO:
/// - 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 = -1,
_expenseMonthly = -1,
_expenseYearly = -1,
_incomeMonthly = -1,
_incomeYearly = -1;
class ProjectionPage extends StatefulWidget {
const ProjectionPage({
@ -23,28 +31,121 @@ 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: "Expense",
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.
Widget projections;
if (_assetTotal < 0 ||
_incomeMonthly < 0 ||
_incomeYearly < 0 ||
_expenseMonthly < 0 ||
_expenseYearly < 0) {
_showProjections = false;
projections = Center(
child: SizedBox(
child: CircularProgressIndicator(),
),
);
Future.delayed(Duration(seconds: 1), () {
setState(() {
_showProjections = true;
});
});
} else {
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,
],
);
}
// Return all of the UI elements.
return ListView(
children: [
TitleCard(title: "Summaries"),
expenseSummary,
SummaryCard(
name: "Income Totals",
leftText: "left",
middleText: "middle",
rightText: "right",
),
SummaryCard(
name: "Asset Totals",
leftText: "left",
middleText: "middle",
rightText: "right",
),
incomeSummary,
assetSummary,
TitleCard(title: "Projections"),
projections,
],
);
}
@ -71,84 +172,83 @@ class SummaryCardForTotals extends StatelessWidget {
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: "$summaryTypeLabel Totals",
leftText: "$dailyEstimate${dailyTotal.toStringAsFixed(2)} Daily",
middleText:
"$monthlyEstimate${monthlyTotal.toStringAsFixed(2)} Monthly",
rightText:
"$yearlyEstimate${yearlyTotal.toStringAsFixed(2)} Yearly",
name: header,
leftText: leftText,
middleText: middleText,
rightText: rightText,
);
});
}
}
class SummaryCard extends StatelessWidget {
const SummaryCard({
super.key,
required this.name,
required this.leftText,
required this.middleText,
required this.rightText,
});
final String name;
final String leftText;
final String middleText;
final String rightText;
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).cardColor,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
color: Theme.of(context).highlightColor,
child: Column(
children: [
Text(
name,
style: TextStyle(
decoration: TextDecoration.underline,
fontSize: 16
),
),
Row(
children: [
Spacer(
flex: 3,
),
Text(leftText),
Spacer(
flex: 1,
),
Text(middleText),
Spacer(
flex: 1,
),
Text(rightText),
Spacer(
flex: 3,
),
],
),
],
),
),
),
);
}
}

View File

@ -13,6 +13,10 @@ class SettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Placeholder();
return Center(
child: Text(
"No settings yet. :)",
),
);
}
}

View File

@ -1,7 +1,7 @@
// Flutter
import 'package:flutter/material.dart';
import 'package:flutter_expense_tracker/models/asset.dart';
import 'package:flutter_expense_tracker/models/income.dart';
import '/models/asset.dart';
import '/models/income.dart';
// Local
import '/models/tracked_item.dart';
@ -10,14 +10,14 @@ 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;
const TrackedItemPage({
super.key,
required this.assetsToLoad,
required this.notifyParent,
});
@override
@ -25,10 +25,6 @@ class TrackedItemPage extends StatefulWidget {
}
class _TrackedItemPageState extends State<TrackedItemPage> {
refresh() {
setState(() {});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@ -105,12 +101,17 @@ class _TrackedItemPageState extends State<TrackedItemPage> {
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)} ${Frequency.monthly.title}";
: "$estimateSymbolMonthly${itemMonthAmount.toStringAsFixed(2)}$monthlyTitle";
final String itemBottomText = itemYearAmount < 0
? ""
: "$estimateSymbolYearly${itemYearAmount.toStringAsFixed(2)} ${Frequency.yearly.title}";
@ -144,13 +145,19 @@ class _TrackedItemPageState extends State<TrackedItemPage> {
snapshot.data!.remove(curr);
switch (direction) {
case DismissDirection.startToEnd:
// Remove the item from the database.
if (curr is Expense) {
DatabaseHelper.instance
.removeExpense(curr.id!);
DatabaseHelper.instance.removeExpense(
curr.id!,
);
} else if (curr is Income) {
// TODO
DatabaseHelper.instance.removeIncome(
curr.id!,
);
} else if (curr is Asset) {
// TODO
DatabaseHelper.instance.removeAsset(
curr.id!,
);
} else {
throw UnimplementedError(
"Cannot remove unimplemented item type.");
@ -162,7 +169,7 @@ class _TrackedItemPageState extends State<TrackedItemPage> {
context: context,
builder: (_) => AlertDialog(
content: TrackedItemInputDialog(
notifyParent: refresh,
notifyParent: widget.notifyParent,
entry: curr,
amountText: curr.getAmountText(),
type: curr.type!,
@ -268,6 +275,7 @@ class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> {
double _amount = 0;
Frequency _freq = Frequency.monthly;
String _desc = "";
ItemType? _type;
@override
Widget build(BuildContext context) {
@ -275,7 +283,7 @@ class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> {
(widget.entry != null && widget.entry!.type == null)) {
throw FlutterError("No ItemType provided for TrackedItemInputDialog.");
}
ItemType? _type = widget.type;
_type = widget.type;
if (widget.entry != null) {
_id = widget.entry!.id;
@ -304,10 +312,10 @@ class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> {
DatabaseHelper.instance.addExpense(widget.entry!);
break;
case ItemType.income:
// TODO
DatabaseHelper.instance.addIncome(widget.entry!);
break;
case ItemType.asset:
// TODO
DatabaseHelper.instance.addAsset(widget.entry!);
break;
default:
throw UnimplementedError(
@ -326,9 +334,10 @@ class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> {
title: Center(
child: widget.entry == null
? Text("New ${_type!.title}")
: Text("Edit Expense"),
: Text("Edit ${_type!.title}"),
),
content: FutureBuilder<List<Expense>>(
// TODO / TBD -- This should no longer only be Expenses.
future: DatabaseHelper.instance.getExpenses(),
builder: (BuildContext context,
AsyncSnapshot<List<Expense>> snapshot) {
@ -394,6 +403,7 @@ class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> {
_amount = double.parse(value!);
},
),
if (_type != ItemType.asset)
DropdownButtonFormField(
items: Frequency.values
.map(
@ -462,7 +472,10 @@ class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> {
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
setState(() {
setState(
() {
switch (_type) {
case ItemType.expense:
Expense expense = Expense(
id: _id,
name: _name,
@ -479,9 +492,51 @@ class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> {
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
View 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,
),
],
),
],
),
),
),
);
}
}

View File

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

View File

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

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
media/icon_v001.xcf Normal file

Binary file not shown.

BIN
media/icon_v001_round.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
media/icon_v002.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
media/icon_v002.xcf Normal file

Binary file not shown.

BIN
media/icon_v002_round.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
media/icon_v003.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
media/icon_v003.xcf Normal file

Binary file not shown.

BIN
media/icon_v003_round.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -53,10 +53,10 @@ packages:
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,6 +75,11 @@ 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:
@ -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:
@ -220,42 +225,42 @@ packages:
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,10 +273,10 @@ 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:
@ -300,10 +305,10 @@ packages:
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:
@ -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:
@ -348,10 +417,10 @@ packages:
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.7.0-0 <4.0.0"
flutter: ">=3.24.0"
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.27.0"

View File

@ -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.2
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
View 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);
});
}

View File

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

View File

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