Compare commits
35 Commits
last_versi
...
029ee8e9a8
Author | SHA1 | Date | |
---|---|---|---|
029ee8e9a8 | |||
7947d64b3b | |||
778b4e2f39 | |||
02f08093c9 | |||
c2939f81c0 | |||
8ae728f6bf | |||
f6179d1fae | |||
52fdd5c471 | |||
42548d437c | |||
be66f52cbf | |||
992195b9a0 | |||
991eea4d54 | |||
064884dc07 | |||
98e511056c | |||
58102bab02 | |||
0b4937b141 | |||
d35c250913 | |||
21f67b8a36 | |||
9924860181 | |||
60363c29f2 | |||
83dece8749 | |||
737264fa2f | |||
bea9a4bc36 | |||
8e6b574023 | |||
fcf4fca33b | |||
0aa8fdf271 | |||
3bf3dd3190 | |||
ab9b3e0bf9 | |||
7a3eaf70b5 | |||
f5f153f692 | |||
362f1214e8 | |||
d77e732551 | |||
cc33458457 | |||
f5635d6120 | |||
ef58a06dfa |
3
.gitignore
vendored
3
.gitignore
vendored
@ -32,7 +32,6 @@ migrate_working_dir/
|
|||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
/build/
|
/build/
|
||||||
|
|
||||||
# Symbolication related
|
# Symbolication related
|
||||||
app.*.symbols
|
app.*.symbols
|
||||||
|
|
||||||
@ -43,3 +42,5 @@ app.*.map.json
|
|||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
/android/app/.*
|
||||||
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
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:
|
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:
|
||||||
|
|
||||||
|
273
lib/db.dart
273
lib/db.dart
@ -1,45 +1,256 @@
|
|||||||
// https://docs.flutter.dev/cookbook/persistence/sqlite
|
// https://docs.flutter.dev/cookbook/persistence/sqlite
|
||||||
|
|
||||||
// SQLite
|
// SQLite
|
||||||
|
import 'dart:io';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_expense_tracker/models/frequency.dart';
|
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
|
||||||
// Local
|
// Local
|
||||||
import '/models/expense.dart';
|
import '/models/expense.dart';
|
||||||
|
import '/models/income.dart';
|
||||||
|
import '/models/asset.dart';
|
||||||
|
import '/models/tracked_item.dart';
|
||||||
|
|
||||||
void loadDB() async {
|
// Leaned on this example:
|
||||||
// Avoid errors caused by flutter upgrade.
|
// https://learnflutterwithme.com/sqlite
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
class DatabaseHelper {
|
||||||
|
DatabaseHelper._privateConstructor();
|
||||||
|
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
|
||||||
|
|
||||||
final String frequencies =
|
static Database? _db;
|
||||||
"'${Frequency.values.map((freq) => freq.title).join("','")}'";
|
Future<Database> get db async => _db ??= await _initDatabase();
|
||||||
print(frequencies);
|
|
||||||
|
|
||||||
// Open the database and store the reference.
|
Future<Database> _initDatabase() async {
|
||||||
final database = openDatabase(
|
Directory documentsDirectory = await getApplicationDocumentsDirectory();
|
||||||
// Set the path to the database. Note: Using the `join` function from the
|
String path = join(documentsDirectory.path, "com_hyperling_expense.db");
|
||||||
// `path` package is best practice to ensure the path is correctly
|
return await openDatabase(
|
||||||
// constructed for each platform.
|
path,
|
||||||
join(await getDatabasesPath(), 'expense_tracker.db'),
|
version: 2,
|
||||||
|
onCreate: _onCreate,
|
||||||
|
onUpgrade: _onUpgrade,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
onCreate: (db, version) {
|
Future _onCreate(Database db, int version) async {
|
||||||
// Run the CREATE TABLE statement on the database.
|
debugPrint(
|
||||||
return db.execute(
|
"_onCreate(): version=$version",
|
||||||
"""
|
);
|
||||||
CREATE TABLE expense
|
_createExpense(db);
|
||||||
( id INTEGER PRIMARY KEY
|
_createIncome(db);
|
||||||
, name TEXT
|
_createAsset(db);
|
||||||
, cost DOUBLE
|
}
|
||||||
, frequency TEXT CHECK(frequency IN ($frequencies) )
|
|
||||||
, description TEXT
|
Future _onUpgrade(Database db, int previousVersion, int newVersion) async {
|
||||||
)""",
|
debugPrint(
|
||||||
);
|
"_onUpgrade(): previousVersion=$previousVersion, newVersion=$newVersion",
|
||||||
},
|
);
|
||||||
// Set the version. This executes the onCreate function and provides a
|
// Added in DB version 2.
|
||||||
// path to perform database upgrades and downgrades.
|
if (previousVersion < 2 && newVersion >= 2) {
|
||||||
version: 1,
|
_createIncome(db);
|
||||||
);
|
_createAsset(db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _createExpense(Database db) async {
|
||||||
|
await db.execute("""
|
||||||
|
CREATE TABLE expense
|
||||||
|
( id INTEGER PRIMARY KEY
|
||||||
|
, name TEXT NOT NULL UNIQUE
|
||||||
|
, cost DOUBLE NOT NULL
|
||||||
|
, frequency TEXT NOT NULL
|
||||||
|
, description TEXT
|
||||||
|
)""");
|
||||||
|
}
|
||||||
|
|
||||||
|
void _createIncome(Database db) async {
|
||||||
|
await db.execute("""
|
||||||
|
CREATE TABLE income
|
||||||
|
( id INTEGER PRIMARY KEY
|
||||||
|
, name TEXT NOT NULL UNIQUE
|
||||||
|
, revenue DOUBLE NOT NULL
|
||||||
|
, frequency TEXT NOT NULL
|
||||||
|
, description TEXT
|
||||||
|
)""");
|
||||||
|
}
|
||||||
|
|
||||||
|
void _createAsset(Database db) async {
|
||||||
|
await db.execute("""
|
||||||
|
CREATE TABLE asset
|
||||||
|
( id INTEGER PRIMARY KEY
|
||||||
|
, name TEXT NOT NULL UNIQUE
|
||||||
|
, amount DOUBLE NOT NULL
|
||||||
|
, description TEXT
|
||||||
|
)""");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expense Section
|
||||||
|
///
|
||||||
|
Future<List<Expense>> getExpenses() async {
|
||||||
|
debugPrint("getExpenses(): Accessing db.");
|
||||||
|
Database db = await instance.db;
|
||||||
|
|
||||||
|
debugPrint("getExpenses(): Querying table.");
|
||||||
|
var expenses = await db.query("expense", orderBy: "name");
|
||||||
|
|
||||||
|
debugPrint("getExpenses(): Putting into object list.");
|
||||||
|
List<Expense> expenseList = expenses.isNotEmpty
|
||||||
|
? expenses.map((c) => Expense.fromMap(c)).toList()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
debugPrint("getExpenses(): Returning!");
|
||||||
|
return expenseList;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> addExpense(TrackedItem expense) async {
|
||||||
|
Database db = await instance.db;
|
||||||
|
return await db.insert(
|
||||||
|
"expense",
|
||||||
|
expense.toMap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> removeExpense(int id) async {
|
||||||
|
Database db = await instance.db;
|
||||||
|
return await db.delete(
|
||||||
|
"expense",
|
||||||
|
where: "id = ?",
|
||||||
|
whereArgs: [id],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> updateExpense(TrackedItem expense) async {
|
||||||
|
Database db = await instance.db;
|
||||||
|
return await db.update(
|
||||||
|
"expense",
|
||||||
|
expense.toMap(),
|
||||||
|
where: "id = ?",
|
||||||
|
whereArgs: [expense.id],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> checkExpenseNameExists(String name) async {
|
||||||
|
Database db = await instance.db;
|
||||||
|
var expenses = await db.query(
|
||||||
|
"expense",
|
||||||
|
where: "name = ?",
|
||||||
|
whereArgs: [name],
|
||||||
|
);
|
||||||
|
return expenses.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
|
||||||
|
/// Income Section
|
||||||
|
///
|
||||||
|
Future<List<Income>> getIncomes() async {
|
||||||
|
debugPrint("getIncomes(): Accessing db.");
|
||||||
|
Database db = await instance.db;
|
||||||
|
|
||||||
|
debugPrint("getIncomes(): Querying table.");
|
||||||
|
var incomes = await db.query("income", orderBy: "name");
|
||||||
|
|
||||||
|
debugPrint("getIncomes(): Putting into object list.");
|
||||||
|
List<Income> incomeList = incomes.isNotEmpty
|
||||||
|
? incomes.map((c) => Income.fromMap(c)).toList()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
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 {
|
||||||
|
debugPrint("getAssets(): Accessing db.");
|
||||||
|
Database db = await instance.db;
|
||||||
|
|
||||||
|
debugPrint("getAssets(): Querying table.");
|
||||||
|
var assets = await db.query("asset", orderBy: "name");
|
||||||
|
|
||||||
|
debugPrint("getAssets(): Putting into object list.");
|
||||||
|
List<Asset> assetList =
|
||||||
|
assets.isNotEmpty ? assets.map((c) => Asset.fromMap(c)).toList() : [];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
}
|
}
|
||||||
|
143
lib/main.dart
143
lib/main.dart
@ -2,16 +2,36 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
// Local
|
// Local
|
||||||
import '/pages/expense.dart';
|
import '/pages/home.dart';
|
||||||
import '/pages/income.dart';
|
|
||||||
import '/pages/asset.dart';
|
// SQLite
|
||||||
import '/pages/report.dart';
|
import 'dart:io';
|
||||||
import '/pages/settings.dart';
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||||
import '/pages/help.dart';
|
import 'package:path/path.dart';
|
||||||
import '/db.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
const bool testing = false;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
loadDB();
|
// I see no good explanations of why to use this other package yet, but
|
||||||
|
// trying this to see if it fixes the DB factory errors.
|
||||||
|
// "Unhandled Exception: Bad state: databaseFactory not initialized databaseFactory is only initialized when using sqflite. When using `sqflite_common_ffi`You must call `databaseFactory = databaseFactoryFfi;` before using global openDatabase API
|
||||||
|
// https://stackoverflow.com/questions/76158800/databasefactory-not-initialized-when-using-sqflite-in-flutter
|
||||||
|
if (Platform.isWindows || Platform.isLinux) {
|
||||||
|
// Initialize FFI
|
||||||
|
sqfliteFfiInit();
|
||||||
|
databaseFactory = databaseFactoryFfi;
|
||||||
|
}
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
if (testing) {
|
||||||
|
() async {
|
||||||
|
Directory documentsDirectory = await getApplicationDocumentsDirectory();
|
||||||
|
String path = join(documentsDirectory.path, 'com_hyperling_expense.db');
|
||||||
|
await deleteDatabase(path);
|
||||||
|
}();
|
||||||
|
}
|
||||||
|
|
||||||
runApp(const MainApp());
|
runApp(const MainApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,110 +56,3 @@ class MainApp extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
|
||||||
const HomePage({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<HomePage> createState() => _HomePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _HomePageState extends State<HomePage> {
|
|
||||||
var pageSelected = 0;
|
|
||||||
|
|
||||||
refresh() {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
Widget page;
|
|
||||||
Widget? dialog;
|
|
||||||
switch (pageSelected) {
|
|
||||||
case 0:
|
|
||||||
page = ExpensePage();
|
|
||||||
dialog = ExpenseInputDialog(
|
|
||||||
notifyParent: refresh,
|
|
||||||
);
|
|
||||||
case 1:
|
|
||||||
page = IncomePage();
|
|
||||||
case 2:
|
|
||||||
page = AssetPage();
|
|
||||||
case 3:
|
|
||||||
page = ProjectionPage();
|
|
||||||
case 4:
|
|
||||||
page = SettingsPage();
|
|
||||||
case 5:
|
|
||||||
page = HelpPage();
|
|
||||||
default:
|
|
||||||
throw UnimplementedError('No widget for page $pageSelected yet!');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> addNewValue(BuildContext context) {
|
|
||||||
return showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => AlertDialog(content: dialog),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget? floatingButton;
|
|
||||||
if (dialog != null) {
|
|
||||||
floatingButton = IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
addNewValue(context);
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.add),
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return LayoutBuilder(builder: (context, constraints) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(title: Text("Expense Tracker")),
|
|
||||||
drawer: NavigationRail(
|
|
||||||
extended: true,
|
|
||||||
destinations: [
|
|
||||||
NavigationRailDestination(
|
|
||||||
icon: Icon(Icons.payment),
|
|
||||||
label: Text('Expenses'),
|
|
||||||
),
|
|
||||||
NavigationRailDestination(
|
|
||||||
icon: Icon(Icons.account_balance),
|
|
||||||
label: Text('Income'),
|
|
||||||
),
|
|
||||||
NavigationRailDestination(
|
|
||||||
icon: Icon(Icons.attach_money),
|
|
||||||
label: Text('Liquid Assets'),
|
|
||||||
),
|
|
||||||
NavigationRailDestination(
|
|
||||||
icon: Icon(Icons.bar_chart),
|
|
||||||
label: Text('Reports'),
|
|
||||||
),
|
|
||||||
NavigationRailDestination(
|
|
||||||
icon: Icon(Icons.settings),
|
|
||||||
label: Text('Settings'),
|
|
||||||
),
|
|
||||||
NavigationRailDestination(
|
|
||||||
icon: Icon(Icons.help),
|
|
||||||
label: Text('Help'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
selectedIndex: pageSelected,
|
|
||||||
onDestinationSelected: (value) {
|
|
||||||
setState(() {
|
|
||||||
pageSelected = value;
|
|
||||||
Navigator.pop(context);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
body: Container(
|
|
||||||
color: Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
child: Center(child: page),
|
|
||||||
),
|
|
||||||
floatingActionButton: floatingButton,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
20
lib/models/asset.dart
Normal file
20
lib/models/asset.dart
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// Local
|
||||||
|
import '/models/tracked_item.dart';
|
||||||
|
|
||||||
|
class Asset extends TrackedItem {
|
||||||
|
static String amountText = "Amount";
|
||||||
|
|
||||||
|
Asset({
|
||||||
|
super.id,
|
||||||
|
required super.name,
|
||||||
|
required super.amount,
|
||||||
|
required super.description,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Asset.fromMap(Map<String, dynamic> json) => Asset(
|
||||||
|
id: json['id'],
|
||||||
|
name: json['name'],
|
||||||
|
amount: json['amount'],
|
||||||
|
description: json['description'],
|
||||||
|
);
|
||||||
|
}
|
57
lib/models/database_backup.dart
Normal file
57
lib/models/database_backup.dart
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// Local
|
||||||
|
import '/models/expense.dart';
|
||||||
|
import '/models/income.dart';
|
||||||
|
import '/models/asset.dart';
|
||||||
|
import '/db.dart';
|
||||||
|
|
||||||
|
/// TODO:
|
||||||
|
/// - Test the JSON data to ensure it's formed properly.
|
||||||
|
/// - Enable the functions for income and assets once they are implemented.
|
||||||
|
/// - Create a method (factory?) to load JSON data and delete/insert the data.
|
||||||
|
|
||||||
|
class DatabaseBackup {
|
||||||
|
List<Expense> expenseTable = [];
|
||||||
|
List<Income> incomeTable = [];
|
||||||
|
List<Asset> assetTable = [];
|
||||||
|
|
||||||
|
void loadTables() async {
|
||||||
|
expenseTable = await DatabaseHelper.instance.getExpenses();
|
||||||
|
//incomeTable = await DatabaseHelper.instance.getIncomes();
|
||||||
|
//assetTable = await DatabaseHelper.instance.getAssets();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
loadTables(); // TODO: Do we somehow need to wait for this to finish?
|
||||||
|
return {
|
||||||
|
'expense_table': [
|
||||||
|
for (Expense e in expenseTable)
|
||||||
|
{
|
||||||
|
'id': e.id,
|
||||||
|
'name': e.name,
|
||||||
|
'cost': e.amount,
|
||||||
|
'frequency': e.frequency!.title,
|
||||||
|
'description': e.description,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'income_table': [
|
||||||
|
for (Income i in incomeTable)
|
||||||
|
{
|
||||||
|
'id': i.id,
|
||||||
|
'name': i.name,
|
||||||
|
'revenue': i.amount,
|
||||||
|
'frequency': i.frequency!.title,
|
||||||
|
'description': i.description,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'asset_table': [
|
||||||
|
for (Asset a in assetTable)
|
||||||
|
{
|
||||||
|
'id': a.id,
|
||||||
|
'name': a.name,
|
||||||
|
'amount': a.amount,
|
||||||
|
'description': a.description,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +1,39 @@
|
|||||||
|
// Local
|
||||||
|
import 'package:flutter_expense_tracker/models/item_type.dart';
|
||||||
|
|
||||||
|
import '/models/tracked_item.dart';
|
||||||
import '/models/frequency.dart';
|
import '/models/frequency.dart';
|
||||||
|
|
||||||
class Expense {
|
class Expense extends TrackedItem {
|
||||||
final String name;
|
static String amountText = "Cost";
|
||||||
final double cost;
|
|
||||||
final Frequency frequency;
|
|
||||||
final String description;
|
|
||||||
|
|
||||||
const Expense(
|
Expense({
|
||||||
{required this.name,
|
super.id,
|
||||||
required this.cost,
|
super.type = ItemType.expense,
|
||||||
required this.frequency,
|
required super.name,
|
||||||
required this.description});
|
required super.amount,
|
||||||
|
required super.frequency,
|
||||||
|
required super.description,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Expense.fromMap(Map<String, dynamic> json) => Expense(
|
||||||
|
id: json['id'],
|
||||||
|
name: json['name'],
|
||||||
|
amount: json['cost'],
|
||||||
|
frequency: Frequency.values
|
||||||
|
.where((freq) => freq.title == json['frequency'])
|
||||||
|
.first,
|
||||||
|
description: json['description'],
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
Map<String, dynamic> toMap() {
|
||||||
return "$name, $cost, ${frequency.title}, $description";
|
return {
|
||||||
}
|
'id': id,
|
||||||
|
'name': name,
|
||||||
double calcComparableCost() {
|
'cost': amount,
|
||||||
return cost * frequency.timesPerYear;
|
'frequency': frequency!.title,
|
||||||
}
|
'description': description,
|
||||||
|
};
|
||||||
double calcComparableCostDaily() {
|
|
||||||
return cost / frequency.numDays;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
36
lib/models/income.dart
Normal file
36
lib/models/income.dart
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Local
|
||||||
|
import '/models/tracked_item.dart';
|
||||||
|
import '/models/frequency.dart';
|
||||||
|
|
||||||
|
class Income extends TrackedItem {
|
||||||
|
static String amountText = "Revenue";
|
||||||
|
|
||||||
|
Income({
|
||||||
|
super.id,
|
||||||
|
required super.name,
|
||||||
|
required super.amount,
|
||||||
|
required super.frequency,
|
||||||
|
required super.description,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Income.fromMap(Map<String, dynamic> json) => Income(
|
||||||
|
id: json['id'],
|
||||||
|
name: json['name'],
|
||||||
|
amount: json['revenue'],
|
||||||
|
frequency: Frequency.values
|
||||||
|
.where((freq) => freq.title == json['frequency'])
|
||||||
|
.first,
|
||||||
|
description: json['description'],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'revenue': amount,
|
||||||
|
'frequency': frequency!.title,
|
||||||
|
'description': description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
22
lib/models/item_type.dart
Normal file
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;
|
||||||
|
}
|
53
lib/models/tracked_item.dart
Normal file
53
lib/models/tracked_item.dart
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// Local
|
||||||
|
import 'package:flutter_expense_tracker/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,12 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class AssetPage extends StatelessWidget {
|
|
||||||
const AssetPage({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Placeholder();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,365 +0,0 @@
|
|||||||
// Flutter
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
// Local
|
|
||||||
import '/models/expense.dart';
|
|
||||||
import '/models/frequency.dart';
|
|
||||||
|
|
||||||
List<Expense> expenses = [];
|
|
||||||
|
|
||||||
class ExpensePage extends StatefulWidget {
|
|
||||||
const ExpensePage({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ExpensePage> createState() => _ExpensePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ExpensePageState extends State<ExpensePage> {
|
|
||||||
refresh() {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
expenses.sort(
|
|
||||||
(a, b) => (b.calcComparableCost() - a.calcComparableCost()).toInt(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return expenses.isEmpty
|
|
||||||
? Text("Add expenses to get started!")
|
|
||||||
: ListView.builder(
|
|
||||||
itemCount: expenses.length,
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
final Expense curr = expenses[index];
|
|
||||||
final String estimateSymbolYearly = curr.frequency.timesPerYear
|
|
||||||
.toStringAsFixed(2)
|
|
||||||
.endsWith(".00") &&
|
|
||||||
curr.calcComparableCost().toStringAsFixed(3).endsWith("0")
|
|
||||||
? ""
|
|
||||||
: "~";
|
|
||||||
final String estimateSymbolDaily =
|
|
||||||
curr.frequency.numDays.toStringAsFixed(2).endsWith(".00") &&
|
|
||||||
curr
|
|
||||||
.calcComparableCostDaily()
|
|
||||||
.toStringAsFixed(3)
|
|
||||||
.endsWith("0")
|
|
||||||
? ""
|
|
||||||
: "~";
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(4.0),
|
|
||||||
child: Dismissible(
|
|
||||||
key: Key(curr.toString()),
|
|
||||||
background: Container(
|
|
||||||
color: Colors.red,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.delete),
|
|
||||||
Text("Delete"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
secondaryBackground: Container(
|
|
||||||
color: Colors.orange,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Text("Edit"),
|
|
||||||
Icon(Icons.edit),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onDismissed: (direction) {
|
|
||||||
setState(() {
|
|
||||||
expenses.remove(curr);
|
|
||||||
});
|
|
||||||
switch (direction) {
|
|
||||||
case DismissDirection.startToEnd:
|
|
||||||
// Only remove the item from the list.
|
|
||||||
break;
|
|
||||||
case DismissDirection.endToStart:
|
|
||||||
// Open an edit dialog, then remove the item from the list.
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => AlertDialog(
|
|
||||||
content: ExpenseInputDialog(
|
|
||||||
notifyParent: refresh,
|
|
||||||
expense: curr,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
UnimplementedError(
|
|
||||||
"Direction ${direction.toString()} not recognized.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
color: theme.colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(4.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
curr.name,
|
|
||||||
style: TextStyle(fontSize: 20.0),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"${curr.cost.toStringAsFixed(2)} ${curr.frequency.title}",
|
|
||||||
style: TextStyle(fontSize: 12.0),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
curr.description,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12.0,
|
|
||||||
),
|
|
||||||
softWrap: true,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
//if (curr.frequency != Frequency.daily)
|
|
||||||
Text(
|
|
||||||
"$estimateSymbolDaily${curr.calcComparableCostDaily().toStringAsFixed(2)} ${Frequency.daily.title}",
|
|
||||||
style: TextStyle(fontSize: 12.0),
|
|
||||||
),
|
|
||||||
//if (curr.frequency != Frequency.yearly)
|
|
||||||
Text(
|
|
||||||
"$estimateSymbolYearly${curr.calcComparableCost().toStringAsFixed(2)} ${Frequency.yearly.title}",
|
|
||||||
style: TextStyle(fontSize: 12.0),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExpenseInputDialog extends StatefulWidget {
|
|
||||||
final Function() notifyParent;
|
|
||||||
final Expense? expense;
|
|
||||||
|
|
||||||
const ExpenseInputDialog({
|
|
||||||
super.key,
|
|
||||||
required this.notifyParent,
|
|
||||||
this.expense,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ExpenseInputDialog> createState() => _ExpenseInputDialogState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ExpenseInputDialogState extends State<ExpenseInputDialog> {
|
|
||||||
final _expenseFormKey = GlobalKey<FormState>();
|
|
||||||
|
|
||||||
String _name = "";
|
|
||||||
double _cost = 0;
|
|
||||||
Frequency _freq = Frequency.monthly;
|
|
||||||
String _desc = "";
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (widget.expense != null) {
|
|
||||||
_name = widget.expense!.name;
|
|
||||||
_cost = widget.expense!.cost;
|
|
||||||
_freq = widget.expense!.frequency;
|
|
||||||
_desc = widget.expense!.description;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
// prevent AlertDialog from taking full vertical height.
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
alignment: FractionalOffset.topRight,
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (widget.expense != null) {
|
|
||||||
setState(() {
|
|
||||||
expenses.add(widget.expense!);
|
|
||||||
widget.notifyParent();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.clear),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
AlertDialog(
|
|
||||||
insetPadding: EdgeInsets.all(0),
|
|
||||||
title: Center(
|
|
||||||
child: widget.expense == null
|
|
||||||
? Text("New Expense")
|
|
||||||
: Text("Edit Expense"),
|
|
||||||
),
|
|
||||||
content: Form(
|
|
||||||
key: _expenseFormKey,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
TextFormField(
|
|
||||||
keyboardType: TextInputType.text,
|
|
||||||
textCapitalization: TextCapitalization.words,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Name",
|
|
||||||
hintText: "Example: Red Pocket",
|
|
||||||
hintStyle: TextStyle(fontSize: 10.0),
|
|
||||||
errorStyle: TextStyle(fontSize: 10.0),
|
|
||||||
),
|
|
||||||
initialValue: _name,
|
|
||||||
validator: (value) {
|
|
||||||
if (value!.isEmpty) {
|
|
||||||
return "Name must be provided.";
|
|
||||||
}
|
|
||||||
if (!expenses.every((expense) => expense.name != value)) {
|
|
||||||
return "Name must be unique, already in use.";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onSaved: (value) {
|
|
||||||
_name = value!;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Cost",
|
|
||||||
hintText: "Example: 10.00",
|
|
||||||
hintStyle: TextStyle(fontSize: 10.0),
|
|
||||||
errorStyle: TextStyle(fontSize: 10.0),
|
|
||||||
),
|
|
||||||
initialValue: _cost != 0 ? _cost.toString() : "",
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return "Cost must be provided.";
|
|
||||||
}
|
|
||||||
if (double.parse(value) < 0) {
|
|
||||||
return "Please use the Income page rather than having negative expenses.";
|
|
||||||
}
|
|
||||||
if (double.parse(value) < 0.01) {
|
|
||||||
return "Cost must be one hundreth (0.01) or higher.";
|
|
||||||
}
|
|
||||||
if (double.tryParse(value) == null) {
|
|
||||||
return "Cost must be a valid number.";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onSaved: (value) {
|
|
||||||
_cost = 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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
value: _freq,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Frequency",
|
|
||||||
errorStyle: TextStyle(fontSize: 10.0),
|
|
||||||
),
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null) {
|
|
||||||
return "Frequency must be provided.";
|
|
||||||
}
|
|
||||||
if (!Frequency.values.contains(value)) {
|
|
||||||
return "Value not valid.";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onChanged: (value) {
|
|
||||||
_freq = value!;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
keyboardType: TextInputType.text,
|
|
||||||
textCapitalization: TextCapitalization.sentences,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Description",
|
|
||||||
hintText: "Example: 1GB data with unlimited talk & text.",
|
|
||||||
hintStyle: TextStyle(fontSize: 8.0),
|
|
||||||
errorStyle: TextStyle(fontSize: 10.0),
|
|
||||||
),
|
|
||||||
initialValue: _desc,
|
|
||||||
validator: (value) {
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onSaved: (value) {
|
|
||||||
_desc = value!;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
Center(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
if (_expenseFormKey.currentState!.validate()) {
|
|
||||||
_expenseFormKey.currentState!.save();
|
|
||||||
setState(() {
|
|
||||||
expenses.add(
|
|
||||||
Expense(
|
|
||||||
name: _name,
|
|
||||||
cost: _cost,
|
|
||||||
frequency: _freq,
|
|
||||||
description: _desc),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
widget.notifyParent();
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.save),
|
|
||||||
label: Text('Submit'),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
155
lib/pages/home.dart
Normal file
155
lib/pages/home.dart
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
// Flutter
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_expense_tracker/models/item_type.dart';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
// Local
|
||||||
|
import '/pages/tracked_item.dart';
|
||||||
|
import '/pages/report.dart';
|
||||||
|
import '/pages/settings.dart';
|
||||||
|
import '/pages/help.dart';
|
||||||
|
import '/db.dart';
|
||||||
|
|
||||||
|
class HomePage extends StatefulWidget {
|
||||||
|
const HomePage({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HomePage> createState() => _HomePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomePageState extends State<HomePage> {
|
||||||
|
var pageSelected = 0;
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget page;
|
||||||
|
Widget? dialog;
|
||||||
|
switch (pageSelected) {
|
||||||
|
case 0:
|
||||||
|
page = TrackedItemPage(
|
||||||
|
assetsToLoad: DatabaseHelper.instance.getExpenses(),
|
||||||
|
);
|
||||||
|
dialog = TrackedItemInputDialog(
|
||||||
|
notifyParent: refresh,
|
||||||
|
type: ItemType.expense,
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
page = TrackedItemPage(
|
||||||
|
assetsToLoad: DatabaseHelper.instance.getIncomes(),
|
||||||
|
);
|
||||||
|
dialog = TrackedItemInputDialog(
|
||||||
|
notifyParent: refresh,
|
||||||
|
type: ItemType.income,
|
||||||
|
);
|
||||||
|
case 2:
|
||||||
|
page = TrackedItemPage(
|
||||||
|
assetsToLoad: DatabaseHelper.instance.getAssets(),
|
||||||
|
);
|
||||||
|
dialog = TrackedItemInputDialog(
|
||||||
|
notifyParent: refresh,
|
||||||
|
type: ItemType.asset,
|
||||||
|
);
|
||||||
|
case 3:
|
||||||
|
page = ProjectionPage();
|
||||||
|
case 4:
|
||||||
|
page = SettingsPage();
|
||||||
|
case 5:
|
||||||
|
page = HelpPage();
|
||||||
|
default:
|
||||||
|
throw UnimplementedError('No widget for page $pageSelected yet!');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addNewValue(BuildContext context) {
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(content: dialog),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget? floatingButton;
|
||||||
|
if (dialog != null) {
|
||||||
|
floatingButton = IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
addNewValue(context);
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.add),
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget navigation = NavigationRail(
|
||||||
|
extended: true,
|
||||||
|
destinations: [
|
||||||
|
NavigationRailDestination(
|
||||||
|
icon: Icon(Icons.payment),
|
||||||
|
label: Text('Expenses'),
|
||||||
|
),
|
||||||
|
NavigationRailDestination(
|
||||||
|
icon: Icon(Icons.account_balance),
|
||||||
|
label: Text('Income'),
|
||||||
|
),
|
||||||
|
NavigationRailDestination(
|
||||||
|
icon: Icon(Icons.attach_money),
|
||||||
|
label: Text('Liquid Assets'),
|
||||||
|
),
|
||||||
|
NavigationRailDestination(
|
||||||
|
icon: Icon(Icons.bar_chart),
|
||||||
|
label: Text('Reports'),
|
||||||
|
),
|
||||||
|
NavigationRailDestination(
|
||||||
|
icon: Icon(Icons.settings),
|
||||||
|
label: Text('Settings'),
|
||||||
|
),
|
||||||
|
NavigationRailDestination(
|
||||||
|
icon: Icon(Icons.help),
|
||||||
|
label: Text('Help'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selectedIndex: pageSelected,
|
||||||
|
onDestinationSelected: (value) {
|
||||||
|
setState(() {
|
||||||
|
pageSelected = value;
|
||||||
|
if (Platform.isAndroid || Platform.isIOS) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget main = Container(
|
||||||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
child: Center(child: page),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget? drawer, body;
|
||||||
|
if (Platform.isAndroid || Platform.isIOS) {
|
||||||
|
drawer = navigation;
|
||||||
|
body = main;
|
||||||
|
} else {
|
||||||
|
drawer = null;
|
||||||
|
body = Row(
|
||||||
|
children: [
|
||||||
|
SafeArea(child: navigation),
|
||||||
|
Expanded(child: main),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LayoutBuilder(builder: (context, constraints) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text("Expense Tracker"),
|
||||||
|
),
|
||||||
|
drawer: drawer,
|
||||||
|
body: body,
|
||||||
|
floatingActionButton: floatingButton,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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,12 +1,154 @@
|
|||||||
|
// Flutter
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ProjectionPage extends StatelessWidget {
|
// Local
|
||||||
|
import '/db.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?
|
||||||
|
|
||||||
|
class ProjectionPage extends StatefulWidget {
|
||||||
const ProjectionPage({
|
const ProjectionPage({
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ProjectionPage> createState() => _ProjectionPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProjectionPageState extends State<ProjectionPage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Placeholder();
|
Widget expenseSummary = SummaryCardForTotals(
|
||||||
|
list: DatabaseHelper.instance.getExpenses(),
|
||||||
|
summaryTypeLabel: "Expense",
|
||||||
|
);
|
||||||
|
|
||||||
|
return ListView(
|
||||||
|
children: [
|
||||||
|
expenseSummary,
|
||||||
|
SummaryCard(
|
||||||
|
name: "Income Totals",
|
||||||
|
leftText: "left",
|
||||||
|
middleText: "middle",
|
||||||
|
rightText: "right",
|
||||||
|
),
|
||||||
|
SummaryCard(
|
||||||
|
name: "Asset Totals",
|
||||||
|
leftText: "left",
|
||||||
|
middleText: "middle",
|
||||||
|
rightText: "right",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SummaryCardForTotals extends StatelessWidget {
|
||||||
|
const SummaryCardForTotals({
|
||||||
|
super.key,
|
||||||
|
required this.list,
|
||||||
|
required this.summaryTypeLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Future<List<TrackedItem>> list;
|
||||||
|
final String summaryTypeLabel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<List<TrackedItem>>(
|
||||||
|
future: list,
|
||||||
|
builder: (
|
||||||
|
BuildContext context,
|
||||||
|
AsyncSnapshot<List<TrackedItem>> snapshot,
|
||||||
|
) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return Text('Loading $summaryTypeLabel Section...');
|
||||||
|
}
|
||||||
|
double dailyTotal = 0, monthlyTotal = 0, yearlyTotal = 0;
|
||||||
|
for (TrackedItem e in snapshot.data!) {
|
||||||
|
dailyTotal += e.calcComparableAmountDaily();
|
||||||
|
monthlyTotal += e.calcComparableAmountYearly() / 12;
|
||||||
|
yearlyTotal += e.calcComparableAmountYearly();
|
||||||
|
}
|
||||||
|
String dailyEstimate =
|
||||||
|
dailyTotal.toStringAsFixed(3).endsWith("0") ? "" : "~",
|
||||||
|
monthlyEstimate =
|
||||||
|
monthlyTotal.toStringAsFixed(3).endsWith("0") ? "" : "~",
|
||||||
|
yearlyEstimate =
|
||||||
|
yearlyTotal.toStringAsFixed(3).endsWith("0") ? "" : "~";
|
||||||
|
return SummaryCard(
|
||||||
|
name: "$summaryTypeLabel Totals",
|
||||||
|
leftText: "$dailyEstimate${dailyTotal.toStringAsFixed(2)} Daily",
|
||||||
|
middleText:
|
||||||
|
"$monthlyEstimate${monthlyTotal.toStringAsFixed(2)} Monthly",
|
||||||
|
rightText:
|
||||||
|
"$yearlyEstimate${yearlyTotal.toStringAsFixed(2)} Yearly",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// TODO:
|
||||||
|
/// - Export DB (JSON?)
|
||||||
|
/// - Import DB (JSON?)
|
||||||
|
/// - Choose color scheme
|
||||||
|
/// - Delete / Reset DB (pull code from `main`'s `if (testing)`)
|
||||||
|
|
||||||
class SettingsPage extends StatelessWidget {
|
class SettingsPage extends StatelessWidget {
|
||||||
const SettingsPage({
|
const SettingsPage({
|
||||||
super.key,
|
super.key,
|
||||||
|
496
lib/pages/tracked_item.dart
Normal file
496
lib/pages/tracked_item.dart
Normal file
@ -0,0 +1,496 @@
|
|||||||
|
// Flutter
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_expense_tracker/models/asset.dart';
|
||||||
|
import 'package:flutter_expense_tracker/models/income.dart';
|
||||||
|
|
||||||
|
// Local
|
||||||
|
import '/models/tracked_item.dart';
|
||||||
|
import '/models/item_type.dart';
|
||||||
|
import '/models/expense.dart';
|
||||||
|
import '/models/frequency.dart';
|
||||||
|
import '/db.dart';
|
||||||
|
|
||||||
|
// TODO: Make this a generic UI based on a superclass of Expense, Income, and Assets.
|
||||||
|
|
||||||
|
class TrackedItemPage extends StatefulWidget {
|
||||||
|
final Future<List<TrackedItem>> assetsToLoad;
|
||||||
|
|
||||||
|
const TrackedItemPage({
|
||||||
|
super.key,
|
||||||
|
required this.assetsToLoad,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TrackedItemPage> createState() => _TrackedItemPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TrackedItemPageState extends State<TrackedItemPage> {
|
||||||
|
refresh() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return FutureBuilder<List<TrackedItem>>(
|
||||||
|
future: widget.assetsToLoad,
|
||||||
|
builder:
|
||||||
|
(BuildContext context, AsyncSnapshot<List<TrackedItem>> snapshot) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return Text('Loading...');
|
||||||
|
}
|
||||||
|
snapshot.data!.sort(
|
||||||
|
(a, b) => (b.calcComparableAmountYearly() -
|
||||||
|
a.calcComparableAmountYearly())
|
||||||
|
.toInt(),
|
||||||
|
);
|
||||||
|
return snapshot.data!.isEmpty
|
||||||
|
? Text(
|
||||||
|
"Add items to get started.",
|
||||||
|
softWrap: true,
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: snapshot.data!.length,
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
final TrackedItem curr = snapshot.data![index];
|
||||||
|
|
||||||
|
final itemKey = Key(curr.id!.toString());
|
||||||
|
final String itemTitle = curr.name;
|
||||||
|
|
||||||
|
final String itemAmount;
|
||||||
|
if (curr.frequency != null) {
|
||||||
|
itemAmount =
|
||||||
|
"${curr.amount.toStringAsFixed(2)} ${curr.frequency!.title}";
|
||||||
|
} else {
|
||||||
|
itemAmount = curr.amount.toStringAsFixed(2);
|
||||||
|
}
|
||||||
|
final String itemDescription = curr.description;
|
||||||
|
|
||||||
|
final double itemDayAmount, itemMonthAmount, itemYearAmount;
|
||||||
|
final String estimateSymbolDaily,
|
||||||
|
estimateSymbolMonthly,
|
||||||
|
estimateSymbolYearly;
|
||||||
|
|
||||||
|
if (curr.frequency != null) {
|
||||||
|
itemDayAmount = curr.calcComparableAmountDaily();
|
||||||
|
estimateSymbolDaily = curr.frequency!.numDays
|
||||||
|
.toStringAsFixed(2)
|
||||||
|
.endsWith(".00") &&
|
||||||
|
itemDayAmount.toStringAsFixed(3).endsWith("0")
|
||||||
|
? ""
|
||||||
|
: "~";
|
||||||
|
|
||||||
|
itemMonthAmount =
|
||||||
|
(curr.calcComparableAmountYearly() / 12);
|
||||||
|
estimateSymbolMonthly = curr.frequency!.timesPerYear
|
||||||
|
.toStringAsFixed(2)
|
||||||
|
.endsWith(".00") &&
|
||||||
|
itemMonthAmount.toStringAsFixed(3).endsWith("0")
|
||||||
|
? ""
|
||||||
|
: "~";
|
||||||
|
|
||||||
|
itemYearAmount = curr.calcComparableAmountYearly();
|
||||||
|
estimateSymbolYearly = curr.frequency!.timesPerYear
|
||||||
|
.toStringAsFixed(2)
|
||||||
|
.endsWith(".00") &&
|
||||||
|
itemYearAmount.toStringAsFixed(3).endsWith("0")
|
||||||
|
? ""
|
||||||
|
: "~";
|
||||||
|
} else {
|
||||||
|
itemDayAmount = -1;
|
||||||
|
estimateSymbolDaily = "";
|
||||||
|
itemMonthAmount = curr.amount;
|
||||||
|
estimateSymbolMonthly = "";
|
||||||
|
itemYearAmount = -1;
|
||||||
|
estimateSymbolYearly = "";
|
||||||
|
}
|
||||||
|
final String itemTopText = itemDayAmount < 0
|
||||||
|
? ""
|
||||||
|
: "$estimateSymbolDaily${itemDayAmount.toStringAsFixed(2)} ${Frequency.daily.title}";
|
||||||
|
final String itemMiddleText = itemMonthAmount < 0
|
||||||
|
? ""
|
||||||
|
: "$estimateSymbolMonthly${itemMonthAmount.toStringAsFixed(2)} ${Frequency.monthly.title}";
|
||||||
|
final String itemBottomText = itemYearAmount < 0
|
||||||
|
? ""
|
||||||
|
: "$estimateSymbolYearly${itemYearAmount.toStringAsFixed(2)} ${Frequency.yearly.title}";
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(4.0),
|
||||||
|
child: Dismissible(
|
||||||
|
key: itemKey,
|
||||||
|
background: Container(
|
||||||
|
color: Colors.red,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.delete),
|
||||||
|
Text("Delete"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
secondaryBackground: Container(
|
||||||
|
color: Colors.orange,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text("Edit"),
|
||||||
|
Icon(Icons.edit),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDismissed: (direction) {
|
||||||
|
setState(() {
|
||||||
|
snapshot.data!.remove(curr);
|
||||||
|
switch (direction) {
|
||||||
|
case DismissDirection.startToEnd:
|
||||||
|
if (curr is Expense) {
|
||||||
|
DatabaseHelper.instance
|
||||||
|
.removeExpense(curr.id!);
|
||||||
|
} else if (curr is Income) {
|
||||||
|
// TODO
|
||||||
|
} else if (curr is Asset) {
|
||||||
|
// TODO
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError(
|
||||||
|
"Cannot remove unimplemented item type.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DismissDirection.endToStart:
|
||||||
|
// Open an edit dialog, then remove the item from the list.
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
content: TrackedItemInputDialog(
|
||||||
|
notifyParent: refresh,
|
||||||
|
entry: curr,
|
||||||
|
amountText: curr.getAmountText(),
|
||||||
|
type: curr.type!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
UnimplementedError(
|
||||||
|
"Direction ${direction.toString()} not recognized.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(4.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
itemTitle,
|
||||||
|
style: TextStyle(fontSize: 20.0),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
itemAmount,
|
||||||
|
style: TextStyle(fontSize: 12.0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
itemDescription,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.0,
|
||||||
|
),
|
||||||
|
softWrap: true,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
itemTopText,
|
||||||
|
style: TextStyle(fontSize: 12.0),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
itemMiddleText,
|
||||||
|
style: TextStyle(fontSize: 12.0),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
itemBottomText,
|
||||||
|
style: TextStyle(fontSize: 12.0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrackedItemInputDialog extends StatefulWidget {
|
||||||
|
final Function() notifyParent;
|
||||||
|
final TrackedItem? entry;
|
||||||
|
final String? amountText;
|
||||||
|
final ItemType? type;
|
||||||
|
|
||||||
|
const TrackedItemInputDialog({
|
||||||
|
super.key,
|
||||||
|
required this.notifyParent,
|
||||||
|
this.entry,
|
||||||
|
this.amountText,
|
||||||
|
this.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TrackedItemInputDialog> createState() => _TrackedItemInputDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
int? _id;
|
||||||
|
String _name = "";
|
||||||
|
double _amount = 0;
|
||||||
|
Frequency _freq = Frequency.monthly;
|
||||||
|
String _desc = "";
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.type == null &&
|
||||||
|
(widget.entry != null && widget.entry!.type == null)) {
|
||||||
|
throw FlutterError("No ItemType provided for TrackedItemInputDialog.");
|
||||||
|
}
|
||||||
|
ItemType? _type = widget.type;
|
||||||
|
|
||||||
|
if (widget.entry != null) {
|
||||||
|
_id = widget.entry!.id;
|
||||||
|
_name = widget.entry!.name;
|
||||||
|
_amount = widget.entry!.amount;
|
||||||
|
widget.entry!.frequency == null ? null : _freq = widget.entry!.frequency!;
|
||||||
|
_desc = widget.entry!.description;
|
||||||
|
_type = widget.entry!.type!;
|
||||||
|
}
|
||||||
|
|
||||||
|
String amountText =
|
||||||
|
widget.amountText != null ? widget.amountText! : TrackedItem.amountText;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
// prevent AlertDialog from taking full vertical height.
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
alignment: FractionalOffset.topRight,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (widget.entry != null) {
|
||||||
|
setState(() {
|
||||||
|
switch (_type) {
|
||||||
|
case ItemType.expense:
|
||||||
|
DatabaseHelper.instance.addExpense(widget.entry!);
|
||||||
|
break;
|
||||||
|
case ItemType.income:
|
||||||
|
// TODO
|
||||||
|
break;
|
||||||
|
case ItemType.asset:
|
||||||
|
// TODO
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw UnimplementedError(
|
||||||
|
"Cannot add unimplemented type.");
|
||||||
|
}
|
||||||
|
widget.notifyParent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.clear),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AlertDialog(
|
||||||
|
insetPadding: EdgeInsets.all(0),
|
||||||
|
title: Center(
|
||||||
|
child: widget.entry == null
|
||||||
|
? Text("New ${_type!.title}")
|
||||||
|
: Text("Edit Expense"),
|
||||||
|
),
|
||||||
|
content: FutureBuilder<List<Expense>>(
|
||||||
|
future: DatabaseHelper.instance.getExpenses(),
|
||||||
|
builder: (BuildContext context,
|
||||||
|
AsyncSnapshot<List<Expense>> snapshot) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return Center(child: Text('Loading...'));
|
||||||
|
}
|
||||||
|
List<Expense> expenses = snapshot.data!;
|
||||||
|
return Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
textCapitalization: TextCapitalization.words,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Name",
|
||||||
|
hintText: "Example: Red Pocket",
|
||||||
|
hintStyle: TextStyle(fontSize: 10.0),
|
||||||
|
errorStyle: TextStyle(fontSize: 10.0),
|
||||||
|
),
|
||||||
|
initialValue: _name,
|
||||||
|
validator: (value) {
|
||||||
|
if (value!.isEmpty) {
|
||||||
|
return "Name must be provided.";
|
||||||
|
}
|
||||||
|
if (!expenses.every((expense) =>
|
||||||
|
expense.name != value || expense.id == _id)) {
|
||||||
|
return "Name must be unique, already in use.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onSaved: (value) {
|
||||||
|
_name = value!;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
keyboardType:
|
||||||
|
TextInputType.numberWithOptions(decimal: true),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: amountText,
|
||||||
|
hintText: "Example: 10.00",
|
||||||
|
hintStyle: TextStyle(fontSize: 10.0),
|
||||||
|
errorStyle: TextStyle(fontSize: 10.0),
|
||||||
|
),
|
||||||
|
initialValue: _amount != 0 ? _amount.toString() : "",
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return "$amountText must be provided.";
|
||||||
|
}
|
||||||
|
if (double.tryParse(value) == null) {
|
||||||
|
return "$amountText must be a valid number.";
|
||||||
|
}
|
||||||
|
if (double.parse(value) < 0) {
|
||||||
|
return "Please use the Income page rather than having negative expenses.";
|
||||||
|
}
|
||||||
|
if (double.parse(value) < 0.01) {
|
||||||
|
return "$amountText must be one hundreth (0.01) or higher.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onSaved: (value) {
|
||||||
|
_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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
value: _freq,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Frequency",
|
||||||
|
errorStyle: TextStyle(fontSize: 10.0),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "Frequency must be provided.";
|
||||||
|
}
|
||||||
|
if (!Frequency.values.contains(value)) {
|
||||||
|
return "Value not valid.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onChanged: (value) {
|
||||||
|
_freq = value!;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Description",
|
||||||
|
hintText:
|
||||||
|
"Example: 1GB data with unlimited talk & text.",
|
||||||
|
hintStyle: TextStyle(fontSize: 8.0),
|
||||||
|
errorStyle: TextStyle(fontSize: 10.0),
|
||||||
|
),
|
||||||
|
initialValue: _desc,
|
||||||
|
validator: (value) {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onSaved: (value) {
|
||||||
|
_desc = value!;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
actions: [
|
||||||
|
Center(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
_formKey.currentState!.save();
|
||||||
|
setState(() {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.save),
|
||||||
|
label: Text('Submit'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -5,8 +5,10 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import path_provider_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
}
|
}
|
||||||
|
172
pubspec.lock
172
pubspec.lock
@ -5,50 +5,58 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.11.0"
|
version: "2.12.0"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: boolean_selector
|
name: boolean_selector
|
||||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.2"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.4.0"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: clock
|
name: clock
|
||||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.2"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.0"
|
version: "1.19.1"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: fake_async
|
name: fake_async
|
||||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.2"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -71,18 +79,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.7"
|
version: "10.0.8"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.8"
|
version: "3.0.9"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -103,10 +111,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.16+1"
|
version: "0.12.17"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -119,18 +127,66 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.15.0"
|
version: "1.16.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.9.1"
|
||||||
|
path_provider:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: path_provider
|
||||||
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.5"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.15"
|
||||||
|
path_provider_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_foundation
|
||||||
|
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
path_provider_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_linux
|
||||||
|
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.1"
|
||||||
|
path_provider_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_platform_interface
|
||||||
|
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
path_provider_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_windows
|
||||||
|
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -156,10 +212,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_span
|
name: source_span
|
||||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.0"
|
version: "1.10.1"
|
||||||
sqflite:
|
sqflite:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -184,6 +240,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.4+6"
|
version: "2.5.4+6"
|
||||||
|
sqflite_common_ffi:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: sqflite_common_ffi
|
||||||
|
sha256: "883dd810b2b49e6e8c3b980df1829ef550a94e3f87deab5d864917d27ca6bf36"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.4+4"
|
||||||
sqflite_darwin:
|
sqflite_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -200,30 +264,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.0"
|
version: "2.4.0"
|
||||||
|
sqlite3:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqlite3
|
||||||
|
sha256: "35d3726fe18ab1463403a5cc8d97dbc81f2a0b08082e8173851363fcc97b6627"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.2"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stack_trace
|
name: stack_trace
|
||||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.0"
|
version: "1.12.1"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stream_channel
|
name: stream_channel
|
||||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.4"
|
||||||
string_scanner:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: string_scanner
|
name: string_scanner
|
||||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.4.1"
|
||||||
synchronized:
|
synchronized:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -236,18 +308,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: term_glyph
|
name: term_glyph
|
||||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.2.2"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.3"
|
version: "0.7.4"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -260,10 +340,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.3.0"
|
version: "14.3.1"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
xdg_directories:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xdg_directories
|
||||||
|
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.6.1 <4.0.0"
|
dart: ">=3.7.0-0 <4.0.0"
|
||||||
flutter: ">=3.24.0"
|
flutter: ">=3.24.0"
|
||||||
|
@ -9,7 +9,10 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
path: ^1.9.0
|
||||||
|
path_provider: ^2.1.5
|
||||||
sqflite: ^2.4.1
|
sqflite: ^2.4.1
|
||||||
|
sqflite_common_ffi: ^2.3.4+4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Reference in New Issue
Block a user