Huge improvements to UI, very interactive now!
This commit is contained in:
		@@ -7,33 +7,109 @@ import '/models/frequency.dart';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
List<Expense> expenses = [];
 | 
					List<Expense> expenses = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ExpensePage extends StatelessWidget {
 | 
					class ExpensePage extends StatefulWidget {
 | 
				
			||||||
  const ExpensePage({
 | 
					  const ExpensePage({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<ExpensePage> createState() => _ExpensePageState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _ExpensePageState extends State<ExpensePage> {
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    expenses.sort(
 | 
				
			||||||
 | 
					      (a, b) => (b.calcComparableCost() - a.calcComparableCost()).toInt(),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
    return expenses.isEmpty
 | 
					    return expenses.isEmpty
 | 
				
			||||||
        ? Text("Add expenses to get started!")
 | 
					        ? Text("Add expenses to get started!")
 | 
				
			||||||
        : ListView.builder(
 | 
					        : ListView.builder(
 | 
				
			||||||
            itemCount: expenses.length,
 | 
					            itemCount: expenses.length,
 | 
				
			||||||
            itemBuilder: (_, index) {
 | 
					            itemBuilder: (_, index) {
 | 
				
			||||||
              final Expense curr = expenses[index];
 | 
					              final Expense curr = expenses[index];
 | 
				
			||||||
              return Center(
 | 
					              final String estimateSymbol = switch (curr.frequency.timesPerYear
 | 
				
			||||||
                child: Padding(
 | 
					                  .toStringAsFixed(2)
 | 
				
			||||||
 | 
					                  .endsWith(".00")) {
 | 
				
			||||||
 | 
					                true => "",
 | 
				
			||||||
 | 
					                false => "~",
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					              return Padding(
 | 
				
			||||||
                padding: const EdgeInsets.all(4.0),
 | 
					                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(
 | 
					                  child: Container(
 | 
				
			||||||
                    decoration: BoxDecoration(
 | 
					                    decoration: BoxDecoration(
 | 
				
			||||||
                      borderRadius: BorderRadius.circular(4),
 | 
					                      borderRadius: BorderRadius.circular(4),
 | 
				
			||||||
                      color: Colors.greenAccent,
 | 
					                      color: Colors.greenAccent,
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    child: Column(
 | 
					                    child: Padding(
 | 
				
			||||||
 | 
					                      padding: const EdgeInsets.all(4.0),
 | 
				
			||||||
 | 
					                      child: Row(
 | 
				
			||||||
 | 
					                        mainAxisSize: MainAxisSize.max,
 | 
				
			||||||
                        children: [
 | 
					                        children: [
 | 
				
			||||||
                        Text(curr.name),
 | 
					                          Column(
 | 
				
			||||||
                        Text("${curr.cost.toString()} ${curr.frequency.title}"),
 | 
					                            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,56 +139,102 @@ class _ExpenseInputDialogState extends State<ExpenseInputDialog> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return AlertDialog(
 | 
					    return Column(
 | 
				
			||||||
      title: Center(
 | 
					      // prevent AlertDialog from taking full vertical height.
 | 
				
			||||||
        child: Text("New Expense"),
 | 
					      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(
 | 
					          content: Form(
 | 
				
			||||||
            key: _expenseFormKey,
 | 
					            key: _expenseFormKey,
 | 
				
			||||||
        //autovalidateMode: AutovalidateMode.onUserInteraction,
 | 
					 | 
				
			||||||
            child: Column(
 | 
					            child: Column(
 | 
				
			||||||
              mainAxisSize: MainAxisSize.min,
 | 
					              mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
          //spacing: 10,
 | 
					 | 
				
			||||||
              children: [
 | 
					              children: [
 | 
				
			||||||
                TextFormField(
 | 
					                TextFormField(
 | 
				
			||||||
                  keyboardType: TextInputType.text,
 | 
					                  keyboardType: TextInputType.text,
 | 
				
			||||||
                  decoration: InputDecoration(
 | 
					                  decoration: InputDecoration(
 | 
				
			||||||
                    labelText: "Name",
 | 
					                    labelText: "Name",
 | 
				
			||||||
                hintText: "Example: Red Pocket Phone Bill",
 | 
					                    hintText: "Example: Red Pocket",
 | 
				
			||||||
 | 
					                    hintStyle: TextStyle(fontSize: 12.0),
 | 
				
			||||||
 | 
					                    errorStyle: TextStyle(fontSize: 10.0),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  validator: (value) {
 | 
					                  validator: (value) {
 | 
				
			||||||
                    if (value!.isEmpty) {
 | 
					                    if (value!.isEmpty) {
 | 
				
			||||||
                      return "Name must be provided.";
 | 
					                      return "Name must be provided.";
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                    if (!expenses.every((expense) => expense.name != value)) {
 | 
				
			||||||
 | 
					                      return "Name must be unique, already in use.";
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                    return null;
 | 
					                    return null;
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
              onSaved: (newValue) {
 | 
					                  onSaved: (value) {
 | 
				
			||||||
                _name = newValue!;
 | 
					                    _name = value!;
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                TextFormField(
 | 
					                TextFormField(
 | 
				
			||||||
                  keyboardType: TextInputType.numberWithOptions(decimal: true),
 | 
					                  keyboardType: TextInputType.numberWithOptions(decimal: true),
 | 
				
			||||||
                  decoration: InputDecoration(
 | 
					                  decoration: InputDecoration(
 | 
				
			||||||
                  labelText: "Cost", hintText: "Example: 10.00"),
 | 
					                    labelText: "Cost",
 | 
				
			||||||
 | 
					                    hintText: "Example: 10.00",
 | 
				
			||||||
 | 
					                    hintStyle: TextStyle(fontSize: 12.0),
 | 
				
			||||||
 | 
					                    errorStyle: TextStyle(fontSize: 10.0),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
                  validator: (value) {
 | 
					                  validator: (value) {
 | 
				
			||||||
                if (value!.isEmpty) {
 | 
					                    if (value == null || value.isEmpty) {
 | 
				
			||||||
                      return "Cost must be provided.";
 | 
					                      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) {
 | 
					                    if (double.tryParse(value) == null) {
 | 
				
			||||||
                      return "Cost must be a valid number.";
 | 
					                      return "Cost must be a valid number.";
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    return null;
 | 
					                    return null;
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
              onSaved: (newValue) {
 | 
					                  onSaved: (value) {
 | 
				
			||||||
                _cost = double.parse(newValue!);
 | 
					                    _cost = double.parse(value!);
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                DropdownButtonFormField(
 | 
					                DropdownButtonFormField(
 | 
				
			||||||
              items: (Frequency.values.map((freq) =>
 | 
					                  items: Frequency.values
 | 
				
			||||||
                      DropdownMenuItem(value: freq, child: Text(freq.title))))
 | 
					                      .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(),
 | 
					                      .toList(),
 | 
				
			||||||
 | 
					                  value: Frequency.montly,
 | 
				
			||||||
                  decoration: InputDecoration(
 | 
					                  decoration: InputDecoration(
 | 
				
			||||||
                  labelText: "Recurrence", hintText: "Example: Monthly"),
 | 
					                    labelText: "Frequency",
 | 
				
			||||||
 | 
					                    errorStyle: TextStyle(fontSize: 10.0),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
                  validator: (value) {
 | 
					                  validator: (value) {
 | 
				
			||||||
                    if (value == null) {
 | 
					                    if (value == null) {
 | 
				
			||||||
                      return "Frequency must be provided.";
 | 
					                      return "Frequency must be provided.";
 | 
				
			||||||
@@ -122,38 +244,31 @@ class _ExpenseInputDialogState extends State<ExpenseInputDialog> {
 | 
				
			|||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    return null;
 | 
					                    return null;
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
              onChanged: (newValue) {
 | 
					                  onChanged: (value) {
 | 
				
			||||||
                _freq = newValue!;
 | 
					                    _freq = value!;
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                TextFormField(
 | 
					                TextFormField(
 | 
				
			||||||
                  keyboardType: TextInputType.text,
 | 
					                  keyboardType: TextInputType.text,
 | 
				
			||||||
                  decoration: InputDecoration(
 | 
					                  decoration: InputDecoration(
 | 
				
			||||||
                    labelText: "Description",
 | 
					                    labelText: "Description",
 | 
				
			||||||
                  hintText: "Example: 1GB data with unlimited talk & text."),
 | 
					                    hintText: "Example: 1GB data with unlimited talk & text.",
 | 
				
			||||||
 | 
					                    hintStyle: TextStyle(fontSize: 12.0),
 | 
				
			||||||
 | 
					                    errorStyle: TextStyle(fontSize: 10.0),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
                  validator: (value) {
 | 
					                  validator: (value) {
 | 
				
			||||||
                    return null;
 | 
					                    return null;
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
              onSaved: (newValue) {
 | 
					                  onSaved: (value) {
 | 
				
			||||||
                _desc = newValue!;
 | 
					                    _desc = value!;
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          actions: [
 | 
					          actions: [
 | 
				
			||||||
        Row(
 | 
					            Center(
 | 
				
			||||||
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
 | 
					              child: ElevatedButton.icon(
 | 
				
			||||||
          crossAxisAlignment: CrossAxisAlignment.center,
 | 
					 | 
				
			||||||
          children: [
 | 
					 | 
				
			||||||
            ElevatedButton.icon(
 | 
					 | 
				
			||||||
              onPressed: () {
 | 
					 | 
				
			||||||
                Navigator.of(context).pop();
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              icon: Icon(Icons.cancel),
 | 
					 | 
				
			||||||
              label: Text('Cancel'),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            ElevatedButton.icon(
 | 
					 | 
				
			||||||
                onPressed: () {
 | 
					                onPressed: () {
 | 
				
			||||||
                  if (_expenseFormKey.currentState!.validate()) {
 | 
					                  if (_expenseFormKey.currentState!.validate()) {
 | 
				
			||||||
                    _expenseFormKey.currentState!.save();
 | 
					                    _expenseFormKey.currentState!.save();
 | 
				
			||||||
@@ -173,9 +288,10 @@ class _ExpenseInputDialogState extends State<ExpenseInputDialog> {
 | 
				
			|||||||
                icon: Icon(Icons.save),
 | 
					                icon: Icon(Icons.save),
 | 
				
			||||||
                label: Text('Submit'),
 | 
					                label: Text('Submit'),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
          ],
 | 
					 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user