Compare commits
47 Commits
last_versi
...
f6179d1fae
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
5425b22ba2 | |||
305012ffd4 | |||
8c31d868b9 | |||
9d478b9cbf | |||
452eb73773 | |||
631555af59 | |||
0f65166123 | |||
595aaefedc | |||
9d8a5e6685 | |||
2acabf4d3b | |||
15fa4aadbd | |||
78a407d0ec | |||
54cd86c34b | |||
66fd966de8 | |||
5561f50736 | |||
6b25e6e552 | |||
360a36f024 | |||
ecbac615e9 |
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:
|
||||||
|
|
||||||
|
126
lib/db.dart
Normal file
126
lib/db.dart
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
// https://docs.flutter.dev/cookbook/persistence/sqlite
|
||||||
|
|
||||||
|
// SQLite
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
|
||||||
|
// Local
|
||||||
|
import '/models/expense.dart';
|
||||||
|
import '/models/tracked_item.dart';
|
||||||
|
|
||||||
|
// Leaned on this example:
|
||||||
|
// https://learnflutterwithme.com/sqlite
|
||||||
|
class DatabaseHelper {
|
||||||
|
DatabaseHelper._privateConstructor();
|
||||||
|
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
|
||||||
|
|
||||||
|
static Database? _db;
|
||||||
|
Future<Database> get db async => _db ??= await _initDatabase();
|
||||||
|
|
||||||
|
Future<Database> _initDatabase() async {
|
||||||
|
Directory documentsDirectory = await getApplicationDocumentsDirectory();
|
||||||
|
String path = join(documentsDirectory.path, "com_hyperling_expense.db");
|
||||||
|
return await openDatabase(
|
||||||
|
path,
|
||||||
|
version: 2,
|
||||||
|
onCreate: _onCreate,
|
||||||
|
onUpgrade: _onUpgrade,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _onCreate(Database db, int version) 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
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _onUpgrade(Database db, int previousVersion, int newVersion) async {
|
||||||
|
// Added in DB version 2.
|
||||||
|
if (previousVersion < 2 && newVersion >= 2) {
|
||||||
|
await db.execute("""
|
||||||
|
CREATE TABLE income
|
||||||
|
( id INTEGER PRIMARY KEY
|
||||||
|
, name TEXT NOT NULL UNIQUE
|
||||||
|
, revenue DOUBLE NOT NULL
|
||||||
|
, frequency TEXT NOT NULL
|
||||||
|
, description TEXT
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
await db.execute("""
|
||||||
|
CREATE TABLE asset
|
||||||
|
( id INTEGER PRIMARY KEY
|
||||||
|
, name TEXT NOT NULL UNIQUE
|
||||||
|
, amount DOUBLE NOT NULL
|
||||||
|
, description TEXT
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expense Section
|
||||||
|
///
|
||||||
|
Future<List<Expense>> getExpenses() async {
|
||||||
|
Database db = await instance.db;
|
||||||
|
var expenses = await db.query("expense", orderBy: "name");
|
||||||
|
List<Expense> expenseList = expenses.isNotEmpty
|
||||||
|
? expenses.map((c) => Expense.fromMap(c)).toList()
|
||||||
|
: [];
|
||||||
|
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
|
||||||
|
///
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Liquid Asset Section
|
||||||
|
|
||||||
|
///
|
||||||
|
}
|
144
lib/main.dart
144
lib/main.dart
@ -1,15 +1,37 @@
|
|||||||
// Helpful guides:
|
// Flutter
|
||||||
// - https://flutter.dev/docs/cookbook/forms/validation
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '/pages/expense.dart';
|
// Local
|
||||||
import '/pages/income.dart';
|
import '/pages/home.dart';
|
||||||
import '/pages/asset.dart';
|
|
||||||
import '/pages/report.dart';
|
// SQLite
|
||||||
import '/pages/settings.dart';
|
import 'dart:io';
|
||||||
|
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() {
|
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.
|
||||||
|
// "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());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,107 +46,13 @@ class MainApp extends StatelessWidget {
|
|||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
|
||||||
),
|
),
|
||||||
|
darkTheme: ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
colorSchemeSeed: Colors.green,
|
||||||
|
),
|
||||||
|
themeMode: ThemeMode.system,
|
||||||
home: HomePage(),
|
home: HomePage(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
|
||||||
const HomePage({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<HomePage> createState() => _HomePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _HomePageState extends State<HomePage> {
|
|
||||||
var pageSelected = 0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
Widget page;
|
|
||||||
Widget? dialog;
|
|
||||||
switch (pageSelected) {
|
|
||||||
case 0:
|
|
||||||
page = ExpensePage();
|
|
||||||
dialog = ExpenseInputDialog();
|
|
||||||
case 1:
|
|
||||||
page = IncomePage();
|
|
||||||
case 2:
|
|
||||||
page = AssetPage();
|
|
||||||
case 3:
|
|
||||||
page = ProjectionPage();
|
|
||||||
case 4:
|
|
||||||
page = SettingsPage();
|
|
||||||
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(
|
|
||||||
// TODO: Add a drawer instead of nav rail.
|
|
||||||
body: Row(
|
|
||||||
children: [
|
|
||||||
SafeArea(
|
|
||||||
child: NavigationRail(
|
|
||||||
extended: constraints.maxWidth >= 800,
|
|
||||||
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('Projections'),
|
|
||||||
),
|
|
||||||
NavigationRailDestination(
|
|
||||||
icon: Icon(Icons.settings),
|
|
||||||
label: Text('Settings'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
selectedIndex: pageSelected,
|
|
||||||
onDestinationSelected: (value) {
|
|
||||||
setState(() {
|
|
||||||
pageSelected = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
color: Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
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,18 +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,
|
||||||
|
});
|
||||||
|
|
||||||
String toString() {
|
factory Expense.fromMap(Map<String, dynamic> json) => Expense(
|
||||||
return "$name, $cost, ${frequency.title}, $description";
|
id: json['id'],
|
||||||
|
name: json['name'],
|
||||||
|
amount: json['cost'],
|
||||||
|
frequency: Frequency.values
|
||||||
|
.where((freq) => freq.title == json['frequency'])
|
||||||
|
.first,
|
||||||
|
description: json['description'],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'cost': amount,
|
||||||
|
'frequency': frequency!.title,
|
||||||
|
'description': description,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,63 @@
|
|||||||
// https://www.tutorialspoint.com/dart_programming/dart_programming_enumeration.htm
|
// https://www.tutorialspoint.com/dart_programming/dart_programming_enumeration.htm
|
||||||
enum Frequency {
|
enum Frequency {
|
||||||
daily(title: "Daily"),
|
daily(
|
||||||
weekly(title: "Weekly"),
|
title: "Daily",
|
||||||
biweekly(title: "Biweekly"),
|
hint: "Once Per Day",
|
||||||
montly(title: "Monthly"),
|
timesPerYear: 364.25,
|
||||||
yearly(title: "Yearly");
|
numDays: 1,
|
||||||
|
),
|
||||||
|
weekly(
|
||||||
|
title: "Weekly",
|
||||||
|
hint: "Once Per Week",
|
||||||
|
timesPerYear: (364.25 / 7),
|
||||||
|
numDays: 7,
|
||||||
|
),
|
||||||
|
biweekly(
|
||||||
|
title: "Biweekly",
|
||||||
|
hint: "Every Other Week",
|
||||||
|
timesPerYear: (364.25 / 14),
|
||||||
|
numDays: 14,
|
||||||
|
),
|
||||||
|
bimonthly(
|
||||||
|
title: "Bimonthly",
|
||||||
|
hint: "Twice Per Month",
|
||||||
|
timesPerYear: 24,
|
||||||
|
numDays: (364.25 / 24),
|
||||||
|
),
|
||||||
|
monthly(
|
||||||
|
title: "Monthly",
|
||||||
|
hint: "Once Per Month",
|
||||||
|
timesPerYear: 12,
|
||||||
|
numDays: (364.25 / 12),
|
||||||
|
),
|
||||||
|
quarterly(
|
||||||
|
title: "Quarterly",
|
||||||
|
hint: "Every Three Months",
|
||||||
|
timesPerYear: 4,
|
||||||
|
numDays: (364.25 / 4),
|
||||||
|
),
|
||||||
|
biannual(
|
||||||
|
title: "Biannual",
|
||||||
|
hint: "Twice Per Year",
|
||||||
|
timesPerYear: 2,
|
||||||
|
numDays: (364.25 / 2),
|
||||||
|
),
|
||||||
|
yearly(
|
||||||
|
title: "Yearly",
|
||||||
|
hint: "Once Per Year",
|
||||||
|
timesPerYear: 1,
|
||||||
|
numDays: 364.25,
|
||||||
|
);
|
||||||
|
|
||||||
const Frequency({required this.title});
|
const Frequency({
|
||||||
|
required this.title,
|
||||||
|
required this.hint,
|
||||||
|
required this.timesPerYear,
|
||||||
|
required this.numDays,
|
||||||
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
|
final String hint;
|
||||||
|
final double timesPerYear;
|
||||||
|
final double 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,214 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '/models/expense.dart';
|
|
||||||
import '/models/frequency.dart';
|
|
||||||
|
|
||||||
List<Expense> expenses = [];
|
|
||||||
|
|
||||||
class ExpensePage extends StatelessWidget {
|
|
||||||
const ExpensePage({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListView.builder(
|
|
||||||
itemCount: expenses.length,
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
final Expense curr = expenses[index];
|
|
||||||
return Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(4.0),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
color: Colors.greenAccent,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(curr.name),
|
|
||||||
Text("${curr.cost.toString()} ${curr.frequency.title}"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
/*
|
|
||||||
return ListView(
|
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
title: Text("Fake Item 1"),
|
|
||||||
subtitle: Text("30.00 / month"),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: Text("Fake Item 2"),
|
|
||||||
subtitle: Text("180.00 / year"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExpenseInputDialog extends StatefulWidget {
|
|
||||||
const ExpenseInputDialog({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ExpenseInputDialog> createState() => _ExpenseInputDialogState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ExpenseInputDialogState extends State<ExpenseInputDialog> {
|
|
||||||
final _expenseFormKey = GlobalKey<FormState>();
|
|
||||||
|
|
||||||
String _name = "";
|
|
||||||
double _cost = 0.0;
|
|
||||||
Frequency _freq = Frequency.montly;
|
|
||||||
String _desc = "";
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
const inputWidth = 300.0;
|
|
||||||
const inputHeight = 50.0;
|
|
||||||
|
|
||||||
List<DropdownMenuItem> freqValues = [];
|
|
||||||
for (var freq in Frequency.values) {
|
|
||||||
freqValues.add(DropdownMenuItem(value: freq, child: Text(freq.title)));
|
|
||||||
}
|
|
||||||
;
|
|
||||||
|
|
||||||
return AlertDialog(
|
|
||||||
title: Center(child: Text("Add New Expense")),
|
|
||||||
content: Form(
|
|
||||||
key: _expenseFormKey,
|
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
|
||||||
child: Column(mainAxisSize: MainAxisSize.min, spacing: 10, children: [
|
|
||||||
SizedBox(
|
|
||||||
width: inputWidth,
|
|
||||||
height: inputHeight,
|
|
||||||
child: TextFormField(
|
|
||||||
keyboardType: TextInputType.text,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Name",
|
|
||||||
hintText: "Example: Red Pocket Phone Bill",
|
|
||||||
),
|
|
||||||
validator: (value) {
|
|
||||||
if (value!.isEmpty) {
|
|
||||||
return "Name must be provided.";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onSaved: (newValue) {
|
|
||||||
_name = newValue!;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: inputWidth,
|
|
||||||
height: inputHeight,
|
|
||||||
child: TextFormField(
|
|
||||||
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Cost", hintText: "Example: 10.00"),
|
|
||||||
validator: (value) {
|
|
||||||
if (value!.isEmpty) {
|
|
||||||
return "Cost must be provided.";
|
|
||||||
}
|
|
||||||
if (double.tryParse(value) == null) {
|
|
||||||
return "Cost must be a valid number.";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onSaved: (newValue) {
|
|
||||||
_cost = double.parse(newValue!);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: inputWidth,
|
|
||||||
height: inputHeight,
|
|
||||||
child: DropdownButtonFormField(
|
|
||||||
items: freqValues,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Recurrence", hintText: "Example: Monthly"),
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null) {
|
|
||||||
return "Frequency must be provided.";
|
|
||||||
}
|
|
||||||
if (!Frequency.values.contains(value)) {
|
|
||||||
return "Value not valid.";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onChanged: (newValue) {
|
|
||||||
_freq = newValue;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: inputWidth,
|
|
||||||
height: inputHeight,
|
|
||||||
child: TextFormField(
|
|
||||||
keyboardType: TextInputType.text,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Description",
|
|
||||||
hintText: "Example: 1GB data with unlimited talk & text."),
|
|
||||||
validator: (value) {
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onSaved: (newValue) {
|
|
||||||
_desc = newValue!;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
SizedBox(
|
|
||||||
width: inputWidth,
|
|
||||||
height: inputHeight,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
print("TODO: Clear fields!");
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.cancel),
|
|
||||||
label: Text('Cancel'),
|
|
||||||
),
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
print("TODO: Save expense!");
|
|
||||||
if (_expenseFormKey.currentState!.validate()) {
|
|
||||||
_expenseFormKey.currentState!.save();
|
|
||||||
setState(() {
|
|
||||||
expenses.add(
|
|
||||||
Expense(
|
|
||||||
name: _name,
|
|
||||||
cost: _cost,
|
|
||||||
frequency: _freq,
|
|
||||||
description: _desc),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
print(expenses.toString());
|
|
||||||
for (var expense in expenses) {
|
|
||||||
print(expense.toString());
|
|
||||||
}
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.save),
|
|
||||||
label: Text('Submit'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
72
lib/pages/help.dart
Normal file
72
lib/pages/help.dart
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class HelpPage extends StatelessWidget {
|
||||||
|
const HelpPage({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
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("Another paragraph.")
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
child: TextButton.icon(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: Icon(Icons.code),
|
||||||
|
label: Text("Code Repository"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
child: TextButton.icon(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: Icon(Icons.web_asset),
|
||||||
|
label: Text("Personal Website"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
141
lib/pages/home.dart
Normal file
141
lib/pages/home.dart
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
// 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 = Placeholder();
|
||||||
|
case 2:
|
||||||
|
page = Placeholder();
|
||||||
|
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,18 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class IncomePage extends StatelessWidget {
|
|
||||||
const IncomePage({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text("TBD"),
|
|
||||||
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,6 +5,10 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import path_provider_foundation
|
||||||
|
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"))
|
||||||
}
|
}
|
||||||
|
238
pubspec.lock
238
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,82 @@ 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:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform
|
||||||
|
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.6"
|
||||||
|
plugin_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: plugin_platform_interface
|
||||||
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.8"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -140,50 +212,122 @@ 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:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: sqflite
|
||||||
|
sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
sqflite_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_android
|
||||||
|
sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
|
sqflite_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_common
|
||||||
|
sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
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:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_darwin
|
||||||
|
sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1+1"
|
||||||
|
sqflite_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_platform_interface
|
||||||
|
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
|
sqlite3:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqlite3
|
||||||
|
sha256: "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:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.0+3"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
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:
|
||||||
@ -196,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.18.0-18.0.pre.54"
|
flutter: ">=3.24.0"
|
||||||
|
@ -9,6 +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_common_ffi: ^2.3.4+4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Reference in New Issue
Block a user