From 2acabf4d3bdb65ccfd2ac9d8c1392223406fa225 Mon Sep 17 00:00:00 2001 From: Hyperling Date: Thu, 6 Feb 2025 11:50:10 -0700 Subject: [PATCH] Huge improvements to UI, very interactive now! --- lib/pages/expense.dart | 352 +++++++++++++++++++++++++++-------------- 1 file changed, 234 insertions(+), 118 deletions(-) diff --git a/lib/pages/expense.dart b/lib/pages/expense.dart index b42a320..5c45ce7 100644 --- a/lib/pages/expense.dart +++ b/lib/pages/expense.dart @@ -7,32 +7,108 @@ import '/models/frequency.dart'; List expenses = []; -class ExpensePage extends StatelessWidget { +class ExpensePage extends StatefulWidget { const ExpensePage({ super.key, }); + @override + State createState() => _ExpensePageState(); +} + +class _ExpensePageState extends State { @override Widget build(BuildContext 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]; - return Center( - child: Padding( - padding: const EdgeInsets.all(4.0), + final String estimateSymbol = switch (curr.frequency.timesPerYear + .toStringAsFixed(2) + .endsWith(".00")) { + true => "", + false => "~", + }; + return Padding( + padding: const EdgeInsets.all(4.0), + child: Dismissible( + key: Key(curr.toString()), + background: Container( + color: Colors.red, + child: Row( + children: [ + Icon(Icons.delete), + Text("Delete!"), + Spacer(), + Text("Delete!"), + Icon(Icons.delete), + ], + ), + ), + onDismissed: (direction) { + setState(() { + expenses.remove(curr); + }); + }, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), color: Colors.greenAccent, ), - child: Column( - children: [ - Text(curr.name), - Text("${curr.cost.toString()} ${curr.frequency.title}"), - ], + 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), + ), + if (curr.frequency != Frequency.yearly) + Text( + "$estimateSymbol${curr.calcComparableCost().toStringAsFixed(2)} Yearly", + style: TextStyle(fontSize: 12.0), + ), + ], + ), + Expanded( + child: Center( + child: Text( + curr.description, + style: TextStyle( + fontSize: 12.0, + ), + softWrap: true, + textAlign: TextAlign.center, + ), + ), + ), + IconButton( + icon: Icon(Icons.edit_off), + onPressed: () { + // TODO: Open the item in the dialog with the NAME field disabled. + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Editing still TBD"), + ), + ); + }, + ), + ], + ), ), ), ), @@ -63,118 +139,158 @@ class _ExpenseInputDialogState extends State { @override Widget build(BuildContext context) { - return AlertDialog( - title: Center( - child: Text("New Expense"), - ), - content: Form( - key: _expenseFormKey, - //autovalidateMode: AutovalidateMode.onUserInteraction, - child: Column( - mainAxisSize: MainAxisSize.min, - //spacing: 10, - children: [ - TextFormField( - keyboardType: TextInputType.text, - decoration: InputDecoration( - labelText: "Name", - hintText: "Example: Red Pocket Phone Bill", + return Column( + // prevent AlertDialog from taking full vertical height. + mainAxisSize: MainAxisSize.min, + children: [ + Container( + alignment: FractionalOffset.topRight, + child: IconButton( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: Icon(Icons.clear), + ), + ), + AlertDialog( + insetPadding: EdgeInsets.all(0), + title: Text("New Expense"), + content: Form( + key: _expenseFormKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + keyboardType: TextInputType.text, + decoration: InputDecoration( + labelText: "Name", + hintText: "Example: Red Pocket", + hintStyle: TextStyle(fontSize: 12.0), + errorStyle: TextStyle(fontSize: 10.0), + ), + 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: 12.0), + errorStyle: TextStyle(fontSize: 10.0), + ), + 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: Frequency.montly, + 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, + decoration: InputDecoration( + labelText: "Description", + hintText: "Example: 1GB data with unlimited talk & text.", + hintStyle: TextStyle(fontSize: 12.0), + errorStyle: TextStyle(fontSize: 10.0), + ), + 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'), ), - validator: (value) { - if (value!.isEmpty) { - return "Name must be provided."; - } - return null; - }, - onSaved: (newValue) { - _name = newValue!; - }, - ), - 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!); - }, - ), - DropdownButtonFormField( - items: (Frequency.values.map((freq) => - DropdownMenuItem(value: freq, child: Text(freq.title)))) - .toList(), - 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!; - }, - ), - 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: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ElevatedButton.icon( - onPressed: () { - Navigator.of(context).pop(); - }, - icon: Icon(Icons.cancel), - label: Text('Cancel'), - ), - 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'), - ), - ], - ) ], ); }