Compare commits
16 Commits
45b5e33491
...
last_versi
Author | SHA1 | Date | |
---|---|---|---|
87392cc73c | |||
1b95feb5d4 | |||
99b1ec82e6 | |||
7ead0e5ebb | |||
bcae40e0e2 | |||
aa3c2f9304 | |||
96811f5bfd | |||
c44d4553c3 | |||
d722172cb3 | |||
43dd151cb4 | |||
c2cc71eae0 | |||
0c9b365f7f | |||
f820265dba | |||
85b7a0a3e6 | |||
861e8bf904 | |||
aaa95bd3a6 |
@ -6,7 +6,7 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.flutter_empty"
|
||||
namespace = "com.hyperling.expense_tracker"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
@ -21,7 +21,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.example.flutter_empty"
|
||||
applicationId = "com.hyperling.expense_tracker"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
|
@ -1,6 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="flutter_empty"
|
||||
android:label="Recurring Expense Tracker"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
|
190
lib/main.dart
190
lib/main.dart
@ -3,98 +3,128 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '/pages/expense.dart';
|
||||
import '/pages/income.dart';
|
||||
import '/pages/asset.dart';
|
||||
import '/pages/report.dart';
|
||||
import '/pages/settings.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MainApp());
|
||||
}
|
||||
|
||||
class MainApp extends StatefulWidget {
|
||||
class MainApp extends StatelessWidget {
|
||||
const MainApp({super.key});
|
||||
|
||||
@override
|
||||
State<MainApp> createState() => _MainAppState();
|
||||
}
|
||||
|
||||
class _MainAppState extends State<MainApp> {
|
||||
final nameFieldController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Clean up the controller when the widget is disposed.
|
||||
nameFieldController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const inputWidth = 400.0;
|
||||
const inputHeight = 50.0;
|
||||
|
||||
const recurrenceValues = <DropdownMenuEntry>[
|
||||
DropdownMenuEntry(value: Recurrence.daily, label: "Daily"),
|
||||
DropdownMenuEntry(value: Recurrence.weekly, label: "Weekly"),
|
||||
DropdownMenuEntry(value: Recurrence.biweekly, label: "Biweekly"),
|
||||
DropdownMenuEntry(value: Recurrence.montly, label: "Monthly"),
|
||||
DropdownMenuEntry(value: Recurrence.yearly, label: "Yearly"),
|
||||
];
|
||||
|
||||
return const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, spacing: 10, children: [
|
||||
Text('Input an expense below!'),
|
||||
SizedBox(
|
||||
width: inputWidth,
|
||||
height: inputHeight,
|
||||
child: TextField(
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Name",
|
||||
hintText: "Example: Red Pocket Phone Bill",
|
||||
),
|
||||
// https://docs.flutter.dev/cookbook/forms/retrieve-input
|
||||
//controller: nameFieldController,
|
||||
)),
|
||||
SizedBox(
|
||||
width: inputWidth,
|
||||
height: inputHeight,
|
||||
child: TextField(
|
||||
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: InputDecoration(
|
||||
labelText: "Cost", hintText: "Example: 10.00"),
|
||||
),
|
||||
),
|
||||
DropdownMenu(
|
||||
dropdownMenuEntries: recurrenceValues,
|
||||
width: inputWidth,
|
||||
label: Text("Recurrence"),
|
||||
hintText: "Example: Monthly",
|
||||
),
|
||||
SizedBox(
|
||||
width: inputWidth,
|
||||
height: inputHeight,
|
||||
child: TextField(
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Description",
|
||||
hintText:
|
||||
"Example: 1GB data with unlimited talk & text."
|
||||
),
|
||||
)),
|
||||
]),
|
||||
),
|
||||
return MaterialApp(
|
||||
title: 'Recurring Expense Tracker',
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
|
||||
),
|
||||
home: HomePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// https://www.tutorialspoint.com/dart_programming/dart_programming_enumeration.htm
|
||||
enum Recurrence { daily, weekly, biweekly, montly, yearly }
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
class Expense {
|
||||
String name;
|
||||
double cost;
|
||||
Recurrence recurrence;
|
||||
String description;
|
||||
|
||||
Expense(this.name, this.cost, this.recurrence, this.description);
|
||||
@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,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
18
lib/models/expense.dart
Normal file
18
lib/models/expense.dart
Normal file
@ -0,0 +1,18 @@
|
||||
import '/models/frequency.dart';
|
||||
|
||||
class Expense {
|
||||
final String name;
|
||||
final double cost;
|
||||
final Frequency frequency;
|
||||
final String description;
|
||||
|
||||
const Expense(
|
||||
{required this.name,
|
||||
required this.cost,
|
||||
required this.frequency,
|
||||
required this.description});
|
||||
|
||||
String toString() {
|
||||
return "$name, $cost, ${frequency.title}, $description";
|
||||
}
|
||||
}
|
12
lib/models/frequency.dart
Normal file
12
lib/models/frequency.dart
Normal file
@ -0,0 +1,12 @@
|
||||
// https://www.tutorialspoint.com/dart_programming/dart_programming_enumeration.htm
|
||||
enum Frequency {
|
||||
daily(title: "Daily"),
|
||||
weekly(title: "Weekly"),
|
||||
biweekly(title: "Biweekly"),
|
||||
montly(title: "Monthly"),
|
||||
yearly(title: "Yearly");
|
||||
|
||||
const Frequency({required this.title});
|
||||
|
||||
final String title;
|
||||
}
|
12
lib/pages/asset.dart
Normal file
12
lib/pages/asset.dart
Normal file
@ -0,0 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AssetPage extends StatelessWidget {
|
||||
const AssetPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Placeholder();
|
||||
}
|
||||
}
|
214
lib/pages/expense.dart
Normal file
214
lib/pages/expense.dart
Normal file
@ -0,0 +1,214 @@
|
||||
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
18
lib/pages/income.dart
Normal file
18
lib/pages/income.dart
Normal file
@ -0,0 +1,18 @@
|
||||
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(),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
12
lib/pages/report.dart
Normal file
12
lib/pages/report.dart
Normal file
@ -0,0 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ProjectionPage extends StatelessWidget {
|
||||
const ProjectionPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Placeholder();
|
||||
}
|
||||
}
|
12
lib/pages/settings.dart
Normal file
12
lib/pages/settings.dart
Normal file
@ -0,0 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsPage extends StatelessWidget {
|
||||
const SettingsPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Placeholder();
|
||||
}
|
||||
}
|
@ -4,10 +4,10 @@ project(runner LANGUAGES CXX)
|
||||
|
||||
# The name of the executable created for the application. Change this to change
|
||||
# the on-disk name of your application.
|
||||
set(BINARY_NAME "flutter_empty")
|
||||
set(BINARY_NAME "expense_tracker")
|
||||
# The unique GTK application identifier for this application. See:
|
||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||
set(APPLICATION_ID "com.example.flutter_empty")
|
||||
set(APPLICATION_ID "com.hyperling.expense_tracker")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
|
@ -40,11 +40,11 @@ static void my_application_activate(GApplication* application) {
|
||||
if (use_header_bar) {
|
||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "flutter_empty");
|
||||
gtk_header_bar_set_title(header_bar, "Recurring Expense Tracker");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
} else {
|
||||
gtk_window_set_title(window, "flutter_empty");
|
||||
gtk_window_set_title(window, "Recurring Expense Tracker");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
|
@ -1,5 +1,5 @@
|
||||
name: flutter_empty
|
||||
description: "A new Flutter project."
|
||||
name: flutter_expense_tracker
|
||||
description: Track recurring expenses against income and liquid assets.
|
||||
publish_to: 'none'
|
||||
version: 0.1.0
|
||||
|
||||
|
3
run_offline.sh
Executable file
3
run_offline.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
flutter run --no-pub
|
Reference in New Issue
Block a user