Compare commits
	
		
			26 Commits
		
	
	
		
			last_versi
			...
			ab9b3e0bf9
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | 
							
								
								
									
										98
									
								
								lib/db.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								lib/db.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | |||||||
|  | // 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'; | ||||||
|  |  | ||||||
|  | // 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: 1, | ||||||
|  |       onCreate: _onCreate, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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 | ||||||
|  |         ) | ||||||
|  |         """); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /// 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(Expense 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(Expense 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, |  | ||||||
|       ); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -1,18 +1,24 @@ | |||||||
|  | import '/models/recurring_tracked_type.dart'; | ||||||
| import '/models/frequency.dart'; | import '/models/frequency.dart'; | ||||||
|  |  | ||||||
| class Expense { | class Expense extends RecurringTrackedType { | ||||||
|   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, |     required super.name, | ||||||
|       required this.frequency, |     required super.amount, | ||||||
|       required this.description}); |     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'], | ||||||
|  |       ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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; | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								lib/models/recurring_tracked_type.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								lib/models/recurring_tracked_type.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | // Local | ||||||
|  | import '/models/tracked_type.dart'; | ||||||
|  | import '/models/frequency.dart'; | ||||||
|  |  | ||||||
|  | abstract class RecurringTrackedType extends TrackedType { | ||||||
|  |   Frequency frequency; | ||||||
|  |  | ||||||
|  |   RecurringTrackedType({ | ||||||
|  |     super.id, | ||||||
|  |     required super.name, | ||||||
|  |     required super.amount, | ||||||
|  |     required this.frequency, | ||||||
|  |     required super.description, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   double calcComparableAmountYearly() { | ||||||
|  |     return amount * frequency.timesPerYear; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   double calcComparableAmountDaily() { | ||||||
|  |     return amount / frequency.numDays; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Map<String, dynamic> toMap() { | ||||||
|  |     return { | ||||||
|  |       'id': id, | ||||||
|  |       'name': name, | ||||||
|  |       'amount': amount, | ||||||
|  |       'frequency': frequency.title, | ||||||
|  |       'description': description, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								lib/models/tracked_type.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								lib/models/tracked_type.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | abstract class TrackedType { | ||||||
|  |   int? id; | ||||||
|  |   String name; | ||||||
|  |   double amount; | ||||||
|  |   String description; | ||||||
|  |  | ||||||
|  |   TrackedType({ | ||||||
|  |     this.id, | ||||||
|  |     required this.name, | ||||||
|  |     required this.amount, | ||||||
|  |     required this.description, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   String toString() { | ||||||
|  |     return toMap().toString(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Map<String, dynamic> toMap() { | ||||||
|  |     return { | ||||||
|  |       'id': id, | ||||||
|  |       'name': name, | ||||||
|  |       'amount': amount, | ||||||
|  |       'description': description, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,59 +1,192 @@ | |||||||
|  | // Flutter | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_expense_tracker/db.dart'; | ||||||
|  |  | ||||||
|  | // Local | ||||||
| import '/models/expense.dart'; | import '/models/expense.dart'; | ||||||
| import '/models/frequency.dart'; | import '/models/frequency.dart'; | ||||||
|  |  | ||||||
| List<Expense> expenses = []; | // TODO: Make this a generic class based on a superclass of Expense, Income, and Assets? | ||||||
|  |  | ||||||
| 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> { | ||||||
|  |   refresh() { | ||||||
|  |     setState(() {}); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return ListView.builder( |     final theme = Theme.of(context); | ||||||
|         itemCount: expenses.length, |  | ||||||
|  |     return FutureBuilder<List<Expense>>( | ||||||
|  |         future: DatabaseHelper.instance.getExpenses(), | ||||||
|  |         builder: (BuildContext context, AsyncSnapshot<List<Expense>> snapshot) { | ||||||
|  |           if (!snapshot.hasData) { | ||||||
|  |             return Text('Loading...'); | ||||||
|  |           } | ||||||
|  |           snapshot.data!.sort( | ||||||
|  |             (a, b) => (b.calcComparableAmountYearly() - | ||||||
|  |                     a.calcComparableAmountYearly()) | ||||||
|  |                 .toInt(), | ||||||
|  |           ); | ||||||
|  |           return snapshot.data!.isEmpty | ||||||
|  |               ? Text( | ||||||
|  |                   "Add expenses to get started.", | ||||||
|  |                   softWrap: true, | ||||||
|  |                 ) | ||||||
|  |               : ListView.builder( | ||||||
|  |                   itemCount: snapshot.data!.length, | ||||||
|                   itemBuilder: (_, index) { |                   itemBuilder: (_, index) { | ||||||
|  |                     List<Expense> expenses = snapshot.data!; | ||||||
|                     final Expense curr = expenses[index]; |                     final Expense curr = expenses[index]; | ||||||
|           return Center( |                     final String estimateSymbolYearly = curr | ||||||
|             child: Padding( |                                 .frequency.timesPerYear | ||||||
|  |                                 .toStringAsFixed(2) | ||||||
|  |                                 .endsWith(".00") && | ||||||
|  |                             curr | ||||||
|  |                                 .calcComparableAmountYearly() | ||||||
|  |                                 .toStringAsFixed(3) | ||||||
|  |                                 .endsWith("0") | ||||||
|  |                         ? "" | ||||||
|  |                         : "~"; | ||||||
|  |                     final String estimateSymbolDaily = curr.frequency.numDays | ||||||
|  |                                 .toStringAsFixed(2) | ||||||
|  |                                 .endsWith(".00") && | ||||||
|  |                             curr | ||||||
|  |                                 .calcComparableAmountDaily() | ||||||
|  |                                 .toStringAsFixed(3) | ||||||
|  |                                 .endsWith("0") | ||||||
|  |                         ? "" | ||||||
|  |                         : "~"; | ||||||
|  |                     return Padding( | ||||||
|                       padding: const EdgeInsets.all(4.0), |                       padding: const EdgeInsets.all(4.0), | ||||||
|  |                       child: Dismissible( | ||||||
|  |                         key: Key(curr.id!.toString()), | ||||||
|  |                         background: Container( | ||||||
|  |                           color: Colors.red, | ||||||
|  |                           child: Row( | ||||||
|  |                             mainAxisAlignment: MainAxisAlignment.start, | ||||||
|  |                             children: [ | ||||||
|  |                               Icon(Icons.delete), | ||||||
|  |                               Text("Delete"), | ||||||
|  |                             ], | ||||||
|  |                           ), | ||||||
|  |                         ), | ||||||
|  |                         secondaryBackground: Container( | ||||||
|  |                           color: Colors.orange, | ||||||
|  |                           child: Row( | ||||||
|  |                             mainAxisAlignment: MainAxisAlignment.end, | ||||||
|  |                             children: [ | ||||||
|  |                               Text("Edit"), | ||||||
|  |                               Icon(Icons.edit), | ||||||
|  |                             ], | ||||||
|  |                           ), | ||||||
|  |                         ), | ||||||
|  |                         onDismissed: (direction) { | ||||||
|  |                           setState(() { | ||||||
|  |                             expenses.remove(curr); | ||||||
|  |                             switch (direction) { | ||||||
|  |                               case DismissDirection.startToEnd: | ||||||
|  |                                 DatabaseHelper.instance.removeExpense(curr.id!); | ||||||
|  |                                 break; | ||||||
|  |                               case DismissDirection.endToStart: | ||||||
|  |                                 // Open an edit dialog, then remove the item from the list. | ||||||
|  |                                 showDialog( | ||||||
|  |                                   context: context, | ||||||
|  |                                   builder: (_) => AlertDialog( | ||||||
|  |                                     content: ExpenseInputDialog( | ||||||
|  |                                       notifyParent: refresh, | ||||||
|  |                                       expense: curr, | ||||||
|  |                                     ), | ||||||
|  |                                   ), | ||||||
|  |                                 ); | ||||||
|  |                                 break; | ||||||
|  |                               default: | ||||||
|  |                                 UnimplementedError( | ||||||
|  |                                   "Direction ${direction.toString()} not recognized.", | ||||||
|  |                                 ); | ||||||
|  |                             } | ||||||
|  |                           }); | ||||||
|  |                         }, | ||||||
|                         child: Container( |                         child: Container( | ||||||
|                           decoration: BoxDecoration( |                           decoration: BoxDecoration( | ||||||
|                             borderRadius: BorderRadius.circular(4), |                             borderRadius: BorderRadius.circular(4), | ||||||
|                   color: Colors.greenAccent, |                             color: theme.colorScheme.onPrimary, | ||||||
|                           ), |                           ), | ||||||
|                 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.amount.toStringAsFixed(2)} ${curr.frequency.title}", | ||||||
|  |                                       style: TextStyle(fontSize: 12.0), | ||||||
|  |                                     ), | ||||||
|  |                                   ], | ||||||
|  |                                 ), | ||||||
|  |                                 Expanded( | ||||||
|  |                                   child: Center( | ||||||
|  |                                     child: Text( | ||||||
|  |                                       curr.description, | ||||||
|  |                                       style: TextStyle( | ||||||
|  |                                         fontSize: 12.0, | ||||||
|  |                                       ), | ||||||
|  |                                       softWrap: true, | ||||||
|  |                                       textAlign: TextAlign.center, | ||||||
|  |                                     ), | ||||||
|  |                                   ), | ||||||
|  |                                 ), | ||||||
|  |                                 Column( | ||||||
|  |                                   crossAxisAlignment: CrossAxisAlignment.end, | ||||||
|  |                                   children: [ | ||||||
|  |                                     //if (curr.frequency != Frequency.daily) | ||||||
|  |                                     Text( | ||||||
|  |                                       "$estimateSymbolDaily${curr.calcComparableAmountDaily().toStringAsFixed(2)} ${Frequency.daily.title}", | ||||||
|  |                                       style: TextStyle(fontSize: 12.0), | ||||||
|  |                                     ), | ||||||
|  |                                     //if (curr.frequency != Frequency.yearly) | ||||||
|  |                                     Text( | ||||||
|  |                                       "$estimateSymbolYearly${curr.calcComparableAmountYearly().toStringAsFixed(2)} ${Frequency.yearly.title}", | ||||||
|  |                                       style: TextStyle(fontSize: 12.0), | ||||||
|  |                                     ), | ||||||
|  |                                   ], | ||||||
|  |                                 ), | ||||||
|                               ], |                               ], | ||||||
|                             ), |                             ), | ||||||
|                           ), |                           ), | ||||||
|                         ), |                         ), | ||||||
|  |                       ), | ||||||
|  |                     ); | ||||||
|  |                   }, | ||||||
|                 ); |                 ); | ||||||
|         }); |         }); | ||||||
|     /* |  | ||||||
|     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 { | class ExpenseInputDialog extends StatefulWidget { | ||||||
|  |   final Function() notifyParent; | ||||||
|  |   final Expense? expense; | ||||||
|  |  | ||||||
|   const ExpenseInputDialog({ |   const ExpenseInputDialog({ | ||||||
|     super.key, |     super.key, | ||||||
|  |     required this.notifyParent, | ||||||
|  |     this.expense, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -63,76 +196,141 @@ class ExpenseInputDialog extends StatefulWidget { | |||||||
| class _ExpenseInputDialogState extends State<ExpenseInputDialog> { | class _ExpenseInputDialogState extends State<ExpenseInputDialog> { | ||||||
|   final _expenseFormKey = GlobalKey<FormState>(); |   final _expenseFormKey = GlobalKey<FormState>(); | ||||||
|  |  | ||||||
|  |   int? _id; | ||||||
|   String _name = ""; |   String _name = ""; | ||||||
|   double _cost = 0.0; |   double _amount = 0; | ||||||
|   Frequency _freq = Frequency.montly; |   Frequency _freq = Frequency.monthly; | ||||||
|   String _desc = ""; |   String _desc = ""; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     const inputWidth = 300.0; |     if (widget.expense != null) { | ||||||
|     const inputHeight = 50.0; |       _id = widget.expense!.id; | ||||||
|  |       _name = widget.expense!.name; | ||||||
|     List<DropdownMenuItem> freqValues = []; |       _amount = widget.expense!.amount; | ||||||
|     for (var freq in Frequency.values) { |       _freq = widget.expense!.frequency; | ||||||
|       freqValues.add(DropdownMenuItem(value: freq, child: Text(freq.title))); |       _desc = widget.expense!.description; | ||||||
|     } |     } | ||||||
|     ; |  | ||||||
|  |  | ||||||
|     return AlertDialog( |     return Column( | ||||||
|       title: Center(child: Text("Add New Expense")), |       // prevent AlertDialog from taking full vertical height. | ||||||
|       content: Form( |       mainAxisSize: MainAxisSize.min, | ||||||
|  |       children: [ | ||||||
|  |         Container( | ||||||
|  |           alignment: FractionalOffset.topRight, | ||||||
|  |           child: IconButton( | ||||||
|  |             onPressed: () { | ||||||
|  |               if (widget.expense != null) { | ||||||
|  |                 setState(() { | ||||||
|  |                   DatabaseHelper.instance.addExpense(widget.expense!); | ||||||
|  |                   widget.notifyParent(); | ||||||
|  |                 }); | ||||||
|  |               } | ||||||
|  |               Navigator.of(context).pop(); | ||||||
|  |             }, | ||||||
|  |             icon: Icon(Icons.clear), | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |         AlertDialog( | ||||||
|  |           insetPadding: EdgeInsets.all(0), | ||||||
|  |           title: Center( | ||||||
|  |             child: widget.expense == null | ||||||
|  |                 ? Text("New Expense") | ||||||
|  |                 : Text("Edit Expense"), | ||||||
|  |           ), | ||||||
|  |           content: 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: _expenseFormKey, |                   key: _expenseFormKey, | ||||||
|         autovalidateMode: AutovalidateMode.onUserInteraction, |                   child: Column( | ||||||
|         child: Column(mainAxisSize: MainAxisSize.min, spacing: 10, children: [ |                     mainAxisSize: MainAxisSize.min, | ||||||
|           SizedBox( |                     children: [ | ||||||
|             width: inputWidth, |                       TextFormField( | ||||||
|             height: inputHeight, |  | ||||||
|             child: TextFormField( |  | ||||||
|                         keyboardType: TextInputType.text, |                         keyboardType: TextInputType.text, | ||||||
|  |                         textCapitalization: TextCapitalization.words, | ||||||
|                         decoration: InputDecoration( |                         decoration: InputDecoration( | ||||||
|                           labelText: "Name", |                           labelText: "Name", | ||||||
|                 hintText: "Example: Red Pocket Phone Bill", |                           hintText: "Example: Red Pocket", | ||||||
|  |                           hintStyle: TextStyle(fontSize: 10.0), | ||||||
|  |                           errorStyle: TextStyle(fontSize: 10.0), | ||||||
|                         ), |                         ), | ||||||
|  |                         initialValue: _name, | ||||||
|                         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 || expense.id == _id)) { | ||||||
|  |                             return "Name must be unique, already in use."; | ||||||
|  |                           } | ||||||
|                           return null; |                           return null; | ||||||
|                         }, |                         }, | ||||||
|               onSaved: (newValue) { |                         onSaved: (value) { | ||||||
|                 _name = newValue!; |                           _name = value!; | ||||||
|                         }, |                         }, | ||||||
|                       ), |                       ), | ||||||
|           ), |                       TextFormField( | ||||||
|           SizedBox( |                         keyboardType: | ||||||
|             width: inputWidth, |                             TextInputType.numberWithOptions(decimal: true), | ||||||
|             height: inputHeight, |  | ||||||
|             child: TextFormField( |  | ||||||
|               keyboardType: TextInputType.numberWithOptions(decimal: true), |  | ||||||
|                         decoration: InputDecoration( |                         decoration: InputDecoration( | ||||||
|                   labelText: "Cost", hintText: "Example: 10.00"), |                           labelText: "${Expense.amountText}", | ||||||
|  |                           hintText: "Example: 10.00", | ||||||
|  |                           hintStyle: TextStyle(fontSize: 10.0), | ||||||
|  |                           errorStyle: TextStyle(fontSize: 10.0), | ||||||
|  |                         ), | ||||||
|  |                         initialValue: _amount != 0 ? _amount.toString() : "", | ||||||
|                         validator: (value) { |                         validator: (value) { | ||||||
|                 if (value!.isEmpty) { |                           if (value == null || value.isEmpty) { | ||||||
|                   return "Cost must be provided."; |                             return "${Expense.amountText} 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 "${Expense.amountText} 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 "${Expense.amountText} must be a valid number."; | ||||||
|                           } |                           } | ||||||
|                           return null; |                           return null; | ||||||
|                         }, |                         }, | ||||||
|               onSaved: (newValue) { |                         onSaved: (value) { | ||||||
|                 _cost = double.parse(newValue!); |                           _amount = double.parse(value!); | ||||||
|                         }, |                         }, | ||||||
|                       ), |                       ), | ||||||
|  |                       DropdownButtonFormField( | ||||||
|  |                         items: Frequency.values | ||||||
|  |                             .map( | ||||||
|  |                               (freq) => DropdownMenuItem( | ||||||
|  |                                 value: freq, | ||||||
|  |                                 child: Row( | ||||||
|  |                                   children: [ | ||||||
|  |                                     Text( | ||||||
|  |                                       freq.title, | ||||||
|                                     ), |                                     ), | ||||||
|           SizedBox( |                                     Padding( | ||||||
|             width: inputWidth, |                                       padding: EdgeInsets.all(1.0), | ||||||
|             height: inputHeight, |                                       child: Text( | ||||||
|             child: DropdownButtonFormField( |                                         " (${freq.hint})", | ||||||
|               items: freqValues, |                                         style: TextStyle(fontSize: 10.0), | ||||||
|  |                                       ), | ||||||
|  |                                     ), | ||||||
|  |                                   ], | ||||||
|  |                                 ), | ||||||
|  |                               ), | ||||||
|  |                             ) | ||||||
|  |                             .toList(), | ||||||
|  |                         value: _freq, | ||||||
|                         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."; | ||||||
| @@ -142,72 +340,66 @@ class _ExpenseInputDialogState extends State<ExpenseInputDialog> { | |||||||
|                           } |                           } | ||||||
|                           return null; |                           return null; | ||||||
|                         }, |                         }, | ||||||
|               onChanged: (newValue) { |                         onChanged: (value) { | ||||||
|                 _freq = newValue; |                           _freq = value!; | ||||||
|                         }, |                         }, | ||||||
|                       ), |                       ), | ||||||
|           ), |                       TextFormField( | ||||||
|           SizedBox( |  | ||||||
|             width: inputWidth, |  | ||||||
|             height: inputHeight, |  | ||||||
|             child: TextFormField( |  | ||||||
|                         keyboardType: TextInputType.text, |                         keyboardType: TextInputType.text, | ||||||
|  |                         textCapitalization: TextCapitalization.sentences, | ||||||
|                         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: 8.0), | ||||||
|  |                           errorStyle: TextStyle(fontSize: 10.0), | ||||||
|  |                         ), | ||||||
|  |                         initialValue: _desc, | ||||||
|                         validator: (value) { |                         validator: (value) { | ||||||
|                           return null; |                           return null; | ||||||
|                         }, |                         }, | ||||||
|               onSaved: (newValue) { |                         onSaved: (value) { | ||||||
|                 _desc = newValue!; |                           _desc = value!; | ||||||
|                         }, |                         }, | ||||||
|                       ), |                       ), | ||||||
|  |                     ], | ||||||
|                   ), |                   ), | ||||||
|         ]), |                 ); | ||||||
|       ), |               }), | ||||||
|           actions: [ |           actions: [ | ||||||
|         SizedBox( |             Center( | ||||||
|           width: inputWidth, |               child: ElevatedButton.icon( | ||||||
|           height: inputHeight, |  | ||||||
|           child: Row( |  | ||||||
|             mainAxisAlignment: MainAxisAlignment.spaceEvenly, |  | ||||||
|             crossAxisAlignment: CrossAxisAlignment.center, |  | ||||||
|             children: [ |  | ||||||
|               ElevatedButton.icon( |  | ||||||
|                 onPressed: () { |                 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()) { |                   if (_expenseFormKey.currentState!.validate()) { | ||||||
|                     _expenseFormKey.currentState!.save(); |                     _expenseFormKey.currentState!.save(); | ||||||
|                     setState(() { |                     setState(() { | ||||||
|                       expenses.add( |                       Expense expense = Expense( | ||||||
|                         Expense( |                         id: _id, | ||||||
|                         name: _name, |                         name: _name, | ||||||
|                             cost: _cost, |                         amount: _amount, | ||||||
|                         frequency: _freq, |                         frequency: _freq, | ||||||
|                             description: _desc), |                         description: _desc, | ||||||
|  |                       ); | ||||||
|  |                       if (_id != null) { | ||||||
|  |                         DatabaseHelper.instance.updateExpense( | ||||||
|  |                           expense, | ||||||
|  |                         ); | ||||||
|  |                       } else { | ||||||
|  |                         DatabaseHelper.instance.addExpense( | ||||||
|  |                           expense, | ||||||
|                         ); |                         ); | ||||||
|                     }); |  | ||||||
|                     print(expenses.toString()); |  | ||||||
|                     for (var expense in expenses) { |  | ||||||
|                       print(expense.toString()); |  | ||||||
|                       } |                       } | ||||||
|  |                     }); | ||||||
|  |                     widget.notifyParent(); | ||||||
|                     Navigator.of(context).pop(); |                     Navigator.of(context).pop(); | ||||||
|                   } |                   } | ||||||
|                 }, |                 }, | ||||||
|                 icon: Icon(Icons.save), |                 icon: Icon(Icons.save), | ||||||
|                 label: Text('Submit'), |                 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"), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ) | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										140
									
								
								lib/pages/home.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								lib/pages/home.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | |||||||
|  | // Flutter | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'dart:io'; | ||||||
|  |  | ||||||
|  | // Local | ||||||
|  | import '/pages/expense.dart'; | ||||||
|  | import '/pages/income.dart'; | ||||||
|  | import '/pages/asset.dart'; | ||||||
|  | import '/pages/report.dart'; | ||||||
|  | import '/pages/settings.dart'; | ||||||
|  | import '/pages/help.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 = ExpensePage(); | ||||||
|  |         dialog = ExpenseInputDialog( | ||||||
|  |           notifyParent: refresh, | ||||||
|  |         ); | ||||||
|  |       case 1: | ||||||
|  |         page = IncomePage(); | ||||||
|  |       case 2: | ||||||
|  |         page = AssetPage(); | ||||||
|  |       case 3: | ||||||
|  |         page = ProjectionPage(); | ||||||
|  |       case 4: | ||||||
|  |         page = SettingsPage(); | ||||||
|  |       case 5: | ||||||
|  |         page = HelpPage(); | ||||||
|  |       default: | ||||||
|  |         throw UnimplementedError('No widget for page $pageSelected yet!'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Future<void> addNewValue(BuildContext context) { | ||||||
|  |       return showDialog( | ||||||
|  |         context: context, | ||||||
|  |         builder: (_) => AlertDialog(content: dialog), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Widget? floatingButton; | ||||||
|  |     if (dialog != null) { | ||||||
|  |       floatingButton = IconButton( | ||||||
|  |         onPressed: () { | ||||||
|  |           addNewValue(context); | ||||||
|  |         }, | ||||||
|  |         icon: Icon(Icons.add), | ||||||
|  |         color: Theme.of(context).colorScheme.onSurface, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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, | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -7,12 +7,6 @@ class IncomePage extends StatelessWidget { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return Center( |     return Placeholder(); | ||||||
|         child: Column( |  | ||||||
|       children: [ |  | ||||||
|         Text("TBD"), |  | ||||||
|         Placeholder(), |  | ||||||
|       ], |  | ||||||
|     )); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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")) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										164
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										164
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -49,6 +49,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.3.1" |     version: "1.3.1" | ||||||
|  |   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 | ||||||
| @@ -124,13 +132,77 @@ packages: | |||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.15.0" |     version: "1.15.0" | ||||||
|   path: |   path: | ||||||
|     dependency: transitive |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: path |       name: path | ||||||
|       sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" |       sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.9.0" |     version: "1.9.0" | ||||||
|  |   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 | ||||||
| @@ -144,6 +216,62 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.10.0" |     version: "1.10.0" | ||||||
|  |   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: | ||||||
| @@ -168,6 +296,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.3.0" |     version: "1.3.0" | ||||||
|  |   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: | ||||||
| @@ -184,6 +320,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.7.3" |     version: "0.7.3" | ||||||
|  |   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: | ||||||
| @@ -200,6 +344,22 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "14.3.0" |     version: "14.3.0" | ||||||
|  |   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.6.1 <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