Compare commits
	
		
			8 Commits
		
	
	
		
			last_versi
			...
			ab9b3e0bf9
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ab9b3e0bf9 | |||
| 7a3eaf70b5 | |||
| f5f153f692 | |||
| 362f1214e8 | |||
| d77e732551 | |||
| cc33458457 | |||
| f5635d6120 | |||
| ef58a06dfa | 
							
								
								
									
										109
									
								
								lib/db.dart
									
									
									
									
									
								
							
							
						
						
									
										109
									
								
								lib/db.dart
									
									
									
									
									
								
							| @@ -1,45 +1,98 @@ | |||||||
| // https://docs.flutter.dev/cookbook/persistence/sqlite | // https://docs.flutter.dev/cookbook/persistence/sqlite | ||||||
|  |  | ||||||
| // SQLite | // SQLite | ||||||
|  | import 'dart:io'; | ||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
| import 'package:flutter/widgets.dart'; |  | ||||||
| import 'package:flutter_expense_tracker/models/frequency.dart'; |  | ||||||
| import 'package:path/path.dart'; | import 'package:path/path.dart'; | ||||||
|  | import 'package:path_provider/path_provider.dart'; | ||||||
| import 'package:sqflite/sqflite.dart'; | import 'package:sqflite/sqflite.dart'; | ||||||
|  |  | ||||||
| // Local | // Local | ||||||
| import '/models/expense.dart'; | import '/models/expense.dart'; | ||||||
|  |  | ||||||
| void loadDB() async { | // Leaned on this example: | ||||||
|   // Avoid errors caused by flutter upgrade. | //   https://learnflutterwithme.com/sqlite | ||||||
|   WidgetsFlutterBinding.ensureInitialized(); | class DatabaseHelper { | ||||||
|  |   DatabaseHelper._privateConstructor(); | ||||||
|  |   static final DatabaseHelper instance = DatabaseHelper._privateConstructor(); | ||||||
|  |  | ||||||
|   final String frequencies = |   static Database? _db; | ||||||
|       "'${Frequency.values.map((freq) => freq.title).join("','")}'"; |   Future<Database> get db async => _db ??= await _initDatabase(); | ||||||
|   print(frequencies); |  | ||||||
|  |  | ||||||
|   // Open the database and store the reference. |   Future<Database> _initDatabase() async { | ||||||
|   final database = openDatabase( |     Directory documentsDirectory = await getApplicationDocumentsDirectory(); | ||||||
|     // Set the path to the database. Note: Using the `join` function from the |     String path = join(documentsDirectory.path, "com_hyperling_expense.db"); | ||||||
|     // `path` package is best practice to ensure the path is correctly |     return await openDatabase( | ||||||
|     // constructed for each platform. |       path, | ||||||
|     join(await getDatabasesPath(), 'expense_tracker.db'), |       version: 1, | ||||||
|  |       onCreate: _onCreate, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|     onCreate: (db, version) { |   Future _onCreate(Database db, int version) async { | ||||||
|       // Run the CREATE TABLE statement on the database. |     await db.execute(""" | ||||||
|       return db.execute( |  | ||||||
|         """ |  | ||||||
|         CREATE TABLE expense |         CREATE TABLE expense | ||||||
|           ( id INTEGER PRIMARY KEY |           ( id INTEGER PRIMARY KEY | ||||||
|           , name TEXT |           , name TEXT NOT NULL UNIQUE | ||||||
|           , cost DOUBLE |           , cost DOUBLE NOT NULL | ||||||
|           , frequency TEXT CHECK(frequency IN ($frequencies) ) |           , frequency TEXT NOT NULL | ||||||
|           , description TEXT |           , description TEXT | ||||||
|         )""", |         ) | ||||||
|       ); |         """); | ||||||
|     }, |   } | ||||||
|     // Set the version. This executes the onCreate function and provides a |  | ||||||
|     // path to perform database upgrades and downgrades. |   /// Expense Section | ||||||
|     version: 1, |   /// | ||||||
|   ); |   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 | ||||||
|  |  | ||||||
|  |   /// | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										143
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						
									
										143
									
								
								lib/main.dart
									
									
									
									
									
								
							| @@ -2,16 +2,36 @@ | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  |  | ||||||
| // Local | // Local | ||||||
| import '/pages/expense.dart'; | import '/pages/home.dart'; | ||||||
| import '/pages/income.dart'; |  | ||||||
| import '/pages/asset.dart'; | // SQLite | ||||||
| import '/pages/report.dart'; | import 'dart:io'; | ||||||
| import '/pages/settings.dart'; | import 'package:sqflite_common_ffi/sqflite_ffi.dart'; | ||||||
| import '/pages/help.dart'; | import 'package:path/path.dart'; | ||||||
| import '/db.dart'; | import 'package:path_provider/path_provider.dart'; | ||||||
|  |  | ||||||
|  | const bool testing = false; | ||||||
|  |  | ||||||
| void main() { | void main() { | ||||||
|   loadDB(); |   // 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()); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -36,110 +56,3 @@ class MainApp extends StatelessWidget { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| 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, |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return LayoutBuilder(builder: (context, constraints) { |  | ||||||
|       return Scaffold( |  | ||||||
|         appBar: AppBar(title: Text("Expense Tracker")), |  | ||||||
|         drawer: 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; |  | ||||||
|               Navigator.pop(context); |  | ||||||
|             }); |  | ||||||
|           }, |  | ||||||
|         ), |  | ||||||
|         body: Container( |  | ||||||
|           color: Theme.of(context).colorScheme.primaryContainer, |  | ||||||
|           child: Center(child: page), |  | ||||||
|         ), |  | ||||||
|         floatingActionButton: floatingButton, |  | ||||||
|       ); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -1,27 +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, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|   @override |   factory Expense.fromMap(Map<String, dynamic> json) => Expense( | ||||||
|   String toString() { |         id: json['id'], | ||||||
|     return "$name, $cost, ${frequency.title}, $description"; |         name: json['name'], | ||||||
|   } |         amount: json['cost'], | ||||||
|  |         frequency: Frequency.values | ||||||
|   double calcComparableCost() { |             .where((freq) => freq.title == json['frequency']) | ||||||
|     return cost * frequency.timesPerYear; |             .first, | ||||||
|   } |         description: json['description'], | ||||||
|  |       ); | ||||||
|   double calcComparableCostDaily() { |  | ||||||
|     return cost / frequency.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,11 +1,12 @@ | |||||||
| // Flutter | // Flutter | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_expense_tracker/db.dart'; | ||||||
|  |  | ||||||
| // Local | // 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 StatefulWidget { | class ExpensePage extends StatefulWidget { | ||||||
|   const ExpensePage({ |   const ExpensePage({ | ||||||
| @@ -25,139 +26,156 @@ class _ExpensePageState extends State<ExpensePage> { | |||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     final theme = Theme.of(context); |     final theme = Theme.of(context); | ||||||
|  |  | ||||||
|     expenses.sort( |     return FutureBuilder<List<Expense>>( | ||||||
|       (a, b) => (b.calcComparableCost() - a.calcComparableCost()).toInt(), |         future: DatabaseHelper.instance.getExpenses(), | ||||||
|     ); |         builder: (BuildContext context, AsyncSnapshot<List<Expense>> snapshot) { | ||||||
|  |           if (!snapshot.hasData) { | ||||||
|     return expenses.isEmpty |             return Text('Loading...'); | ||||||
|         ? Text("Add expenses to get started!") |           } | ||||||
|         : ListView.builder( |           snapshot.data!.sort( | ||||||
|             itemCount: expenses.length, |             (a, b) => (b.calcComparableAmountYearly() - | ||||||
|             itemBuilder: (_, index) { |                     a.calcComparableAmountYearly()) | ||||||
|               final Expense curr = expenses[index]; |                 .toInt(), | ||||||
|               final String estimateSymbolYearly = curr.frequency.timesPerYear |  | ||||||
|                           .toStringAsFixed(2) |  | ||||||
|                           .endsWith(".00") && |  | ||||||
|                       curr.calcComparableCost().toStringAsFixed(3).endsWith("0") |  | ||||||
|                   ? "" |  | ||||||
|                   : "~"; |  | ||||||
|               final String estimateSymbolDaily = |  | ||||||
|                   curr.frequency.numDays.toStringAsFixed(2).endsWith(".00") && |  | ||||||
|                           curr |  | ||||||
|                               .calcComparableCostDaily() |  | ||||||
|                               .toStringAsFixed(3) |  | ||||||
|                               .endsWith("0") |  | ||||||
|                       ? "" |  | ||||||
|                       : "~"; |  | ||||||
|               return Padding( |  | ||||||
|                 padding: const EdgeInsets.all(4.0), |  | ||||||
|                 child: Dismissible( |  | ||||||
|                   key: Key(curr.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: |  | ||||||
|                         // Only remove the item from the list. |  | ||||||
|                         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( |  | ||||||
|                     decoration: BoxDecoration( |  | ||||||
|                       borderRadius: BorderRadius.circular(4), |  | ||||||
|                       color: theme.colorScheme.onPrimary, |  | ||||||
|                     ), |  | ||||||
|                     child: Padding( |  | ||||||
|                       padding: const EdgeInsets.all(4.0), |  | ||||||
|                       child: Row( |  | ||||||
|                         mainAxisSize: MainAxisSize.max, |  | ||||||
|                         children: [ |  | ||||||
|                           Column( |  | ||||||
|                             mainAxisSize: MainAxisSize.min, |  | ||||||
|                             crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|                             children: [ |  | ||||||
|                               Text( |  | ||||||
|                                 curr.name, |  | ||||||
|                                 style: TextStyle(fontSize: 20.0), |  | ||||||
|                               ), |  | ||||||
|                               Text( |  | ||||||
|                                 "${curr.cost.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.calcComparableCostDaily().toStringAsFixed(2)} ${Frequency.daily.title}", |  | ||||||
|                                 style: TextStyle(fontSize: 12.0), |  | ||||||
|                               ), |  | ||||||
|                               //if (curr.frequency != Frequency.yearly) |  | ||||||
|                               Text( |  | ||||||
|                                 "$estimateSymbolYearly${curr.calcComparableCost().toStringAsFixed(2)} ${Frequency.yearly.title}", |  | ||||||
|                                 style: TextStyle(fontSize: 12.0), |  | ||||||
|                               ), |  | ||||||
|                             ], |  | ||||||
|                           ), |  | ||||||
|                         ], |  | ||||||
|                       ), |  | ||||||
|                     ), |  | ||||||
|                   ), |  | ||||||
|                 ), |  | ||||||
|               ); |  | ||||||
|             }, |  | ||||||
|           ); |           ); | ||||||
|  |           return snapshot.data!.isEmpty | ||||||
|  |               ? Text( | ||||||
|  |                   "Add expenses to get started.", | ||||||
|  |                   softWrap: true, | ||||||
|  |                 ) | ||||||
|  |               : ListView.builder( | ||||||
|  |                   itemCount: snapshot.data!.length, | ||||||
|  |                   itemBuilder: (_, index) { | ||||||
|  |                     List<Expense> expenses = snapshot.data!; | ||||||
|  |                     final Expense curr = expenses[index]; | ||||||
|  |                     final String estimateSymbolYearly = curr | ||||||
|  |                                 .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), | ||||||
|  |                       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( | ||||||
|  |                           decoration: BoxDecoration( | ||||||
|  |                             borderRadius: BorderRadius.circular(4), | ||||||
|  |                             color: theme.colorScheme.onPrimary, | ||||||
|  |                           ), | ||||||
|  |                           child: Padding( | ||||||
|  |                             padding: const EdgeInsets.all(4.0), | ||||||
|  |                             child: Row( | ||||||
|  |                               mainAxisSize: MainAxisSize.max, | ||||||
|  |                               children: [ | ||||||
|  |                                 Column( | ||||||
|  |                                   mainAxisSize: MainAxisSize.min, | ||||||
|  |                                   crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                                   children: [ | ||||||
|  |                                     Text( | ||||||
|  |                                       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), | ||||||
|  |                                     ), | ||||||
|  |                                   ], | ||||||
|  |                                 ), | ||||||
|  |                               ], | ||||||
|  |                             ), | ||||||
|  |                           ), | ||||||
|  |                         ), | ||||||
|  |                       ), | ||||||
|  |                     ); | ||||||
|  |                   }, | ||||||
|  |                 ); | ||||||
|  |         }); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -178,16 +196,18 @@ 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; |   double _amount = 0; | ||||||
|   Frequency _freq = Frequency.monthly; |   Frequency _freq = Frequency.monthly; | ||||||
|   String _desc = ""; |   String _desc = ""; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     if (widget.expense != null) { |     if (widget.expense != null) { | ||||||
|  |       _id = widget.expense!.id; | ||||||
|       _name = widget.expense!.name; |       _name = widget.expense!.name; | ||||||
|       _cost = widget.expense!.cost; |       _amount = widget.expense!.amount; | ||||||
|       _freq = widget.expense!.frequency; |       _freq = widget.expense!.frequency; | ||||||
|       _desc = widget.expense!.description; |       _desc = widget.expense!.description; | ||||||
|     } |     } | ||||||
| @@ -202,7 +222,7 @@ class _ExpenseInputDialogState extends State<ExpenseInputDialog> { | |||||||
|             onPressed: () { |             onPressed: () { | ||||||
|               if (widget.expense != null) { |               if (widget.expense != null) { | ||||||
|                 setState(() { |                 setState(() { | ||||||
|                   expenses.add(widget.expense!); |                   DatabaseHelper.instance.addExpense(widget.expense!); | ||||||
|                   widget.notifyParent(); |                   widget.notifyParent(); | ||||||
|                 }); |                 }); | ||||||
|               } |               } | ||||||
| @@ -218,122 +238,134 @@ class _ExpenseInputDialogState extends State<ExpenseInputDialog> { | |||||||
|                 ? Text("New Expense") |                 ? Text("New Expense") | ||||||
|                 : Text("Edit Expense"), |                 : Text("Edit Expense"), | ||||||
|           ), |           ), | ||||||
|           content: Form( |           content: FutureBuilder<List<Expense>>( | ||||||
|             key: _expenseFormKey, |               future: DatabaseHelper.instance.getExpenses(), | ||||||
|             child: Column( |               builder: (BuildContext context, | ||||||
|               mainAxisSize: MainAxisSize.min, |                   AsyncSnapshot<List<Expense>> snapshot) { | ||||||
|               children: [ |                 if (!snapshot.hasData) { | ||||||
|                 TextFormField( |                   return Center(child: Text('Loading...')); | ||||||
|                   keyboardType: TextInputType.text, |                 } | ||||||
|                   textCapitalization: TextCapitalization.words, |                 List<Expense> expenses = snapshot.data!; | ||||||
|                   decoration: InputDecoration( |                 return Form( | ||||||
|                     labelText: "Name", |                   key: _expenseFormKey, | ||||||
|                     hintText: "Example: Red Pocket", |                   child: Column( | ||||||
|                     hintStyle: TextStyle(fontSize: 10.0), |                     mainAxisSize: MainAxisSize.min, | ||||||
|                     errorStyle: TextStyle(fontSize: 10.0), |                     children: [ | ||||||
|                   ), |                       TextFormField( | ||||||
|                   initialValue: _name, |                         keyboardType: TextInputType.text, | ||||||
|                   validator: (value) { |                         textCapitalization: TextCapitalization.words, | ||||||
|                     if (value!.isEmpty) { |                         decoration: InputDecoration( | ||||||
|                       return "Name must be provided."; |                           labelText: "Name", | ||||||
|                     } |                           hintText: "Example: Red Pocket", | ||||||
|                     if (!expenses.every((expense) => expense.name != value)) { |                           hintStyle: TextStyle(fontSize: 10.0), | ||||||
|                       return "Name must be unique, already in use."; |                           errorStyle: TextStyle(fontSize: 10.0), | ||||||
|                     } |                         ), | ||||||
|                     return null; |                         initialValue: _name, | ||||||
|                   }, |                         validator: (value) { | ||||||
|                   onSaved: (value) { |                           if (value!.isEmpty) { | ||||||
|                     _name = value!; |                             return "Name must be provided."; | ||||||
|                   }, |                           } | ||||||
|                 ), |                           if (!expenses.every((expense) => | ||||||
|                 TextFormField( |                               expense.name != value || expense.id == _id)) { | ||||||
|                   keyboardType: TextInputType.numberWithOptions(decimal: true), |                             return "Name must be unique, already in use."; | ||||||
|                   decoration: InputDecoration( |                           } | ||||||
|                     labelText: "Cost", |                           return null; | ||||||
|                     hintText: "Example: 10.00", |                         }, | ||||||
|                     hintStyle: TextStyle(fontSize: 10.0), |                         onSaved: (value) { | ||||||
|                     errorStyle: TextStyle(fontSize: 10.0), |                           _name = value!; | ||||||
|                   ), |                         }, | ||||||
|                   initialValue: _cost != 0 ? _cost.toString() : "", |                       ), | ||||||
|                   validator: (value) { |                       TextFormField( | ||||||
|                     if (value == null || value.isEmpty) { |                         keyboardType: | ||||||
|                       return "Cost must be provided."; |                             TextInputType.numberWithOptions(decimal: true), | ||||||
|                     } |                         decoration: InputDecoration( | ||||||
|                     if (double.parse(value) < 0) { |                           labelText: "${Expense.amountText}", | ||||||
|                       return "Please use the Income page rather than having negative expenses."; |                           hintText: "Example: 10.00", | ||||||
|                     } |                           hintStyle: TextStyle(fontSize: 10.0), | ||||||
|                     if (double.parse(value) < 0.01) { |                           errorStyle: TextStyle(fontSize: 10.0), | ||||||
|                       return "Cost must be one hundreth (0.01) or higher."; |                         ), | ||||||
|                     } |                         initialValue: _amount != 0 ? _amount.toString() : "", | ||||||
|                     if (double.tryParse(value) == null) { |                         validator: (value) { | ||||||
|                       return "Cost must be a valid number."; |                           if (value == null || value.isEmpty) { | ||||||
|                     } |                             return "${Expense.amountText} must be provided."; | ||||||
|                     return null; |                           } | ||||||
|                   }, |                           if (double.parse(value) < 0) { | ||||||
|                   onSaved: (value) { |                             return "Please use the Income page rather than having negative expenses."; | ||||||
|                     _cost = double.parse(value!); |                           } | ||||||
|                   }, |                           if (double.parse(value) < 0.01) { | ||||||
|                 ), |                             return "${Expense.amountText} must be one hundreth (0.01) or higher."; | ||||||
|                 DropdownButtonFormField( |                           } | ||||||
|                   items: Frequency.values |                           if (double.tryParse(value) == null) { | ||||||
|                       .map( |                             return "${Expense.amountText} must be a valid number."; | ||||||
|                         (freq) => DropdownMenuItem( |                           } | ||||||
|                           value: freq, |                           return null; | ||||||
|                           child: Row( |                         }, | ||||||
|                             children: [ |                         onSaved: (value) { | ||||||
|                               Text( |                           _amount = double.parse(value!); | ||||||
|                                 freq.title, |                         }, | ||||||
|                               ), |                       ), | ||||||
|                               Padding( |                       DropdownButtonFormField( | ||||||
|                                 padding: EdgeInsets.all(1.0), |                         items: Frequency.values | ||||||
|                                 child: Text( |                             .map( | ||||||
|                                   " (${freq.hint})", |                               (freq) => DropdownMenuItem( | ||||||
|                                   style: TextStyle(fontSize: 10.0), |                                 value: freq, | ||||||
|  |                                 child: Row( | ||||||
|  |                                   children: [ | ||||||
|  |                                     Text( | ||||||
|  |                                       freq.title, | ||||||
|  |                                     ), | ||||||
|  |                                     Padding( | ||||||
|  |                                       padding: EdgeInsets.all(1.0), | ||||||
|  |                                       child: Text( | ||||||
|  |                                         " (${freq.hint})", | ||||||
|  |                                         style: TextStyle(fontSize: 10.0), | ||||||
|  |                                       ), | ||||||
|  |                                     ), | ||||||
|  |                                   ], | ||||||
|                                 ), |                                 ), | ||||||
|                               ), |                               ), | ||||||
|                             ], |                             ) | ||||||
|                           ), |                             .toList(), | ||||||
|  |                         value: _freq, | ||||||
|  |                         decoration: InputDecoration( | ||||||
|  |                           labelText: "Frequency", | ||||||
|  |                           errorStyle: TextStyle(fontSize: 10.0), | ||||||
|                         ), |                         ), | ||||||
|                       ) |                         validator: (value) { | ||||||
|                       .toList(), |                           if (value == null) { | ||||||
|                   value: _freq, |                             return "Frequency must be provided."; | ||||||
|                   decoration: InputDecoration( |                           } | ||||||
|                     labelText: "Frequency", |                           if (!Frequency.values.contains(value)) { | ||||||
|                     errorStyle: TextStyle(fontSize: 10.0), |                             return "Value not valid."; | ||||||
|  |                           } | ||||||
|  |                           return null; | ||||||
|  |                         }, | ||||||
|  |                         onChanged: (value) { | ||||||
|  |                           _freq = value!; | ||||||
|  |                         }, | ||||||
|  |                       ), | ||||||
|  |                       TextFormField( | ||||||
|  |                         keyboardType: TextInputType.text, | ||||||
|  |                         textCapitalization: TextCapitalization.sentences, | ||||||
|  |                         decoration: InputDecoration( | ||||||
|  |                           labelText: "Description", | ||||||
|  |                           hintText: | ||||||
|  |                               "Example: 1GB data with unlimited talk & text.", | ||||||
|  |                           hintStyle: TextStyle(fontSize: 8.0), | ||||||
|  |                           errorStyle: TextStyle(fontSize: 10.0), | ||||||
|  |                         ), | ||||||
|  |                         initialValue: _desc, | ||||||
|  |                         validator: (value) { | ||||||
|  |                           return null; | ||||||
|  |                         }, | ||||||
|  |                         onSaved: (value) { | ||||||
|  |                           _desc = value!; | ||||||
|  |                         }, | ||||||
|  |                       ), | ||||||
|  |                     ], | ||||||
|                   ), |                   ), | ||||||
|                   validator: (value) { |                 ); | ||||||
|                     if (value == null) { |               }), | ||||||
|                       return "Frequency must be provided."; |  | ||||||
|                     } |  | ||||||
|                     if (!Frequency.values.contains(value)) { |  | ||||||
|                       return "Value not valid."; |  | ||||||
|                     } |  | ||||||
|                     return null; |  | ||||||
|                   }, |  | ||||||
|                   onChanged: (value) { |  | ||||||
|                     _freq = value!; |  | ||||||
|                   }, |  | ||||||
|                 ), |  | ||||||
|                 TextFormField( |  | ||||||
|                   keyboardType: TextInputType.text, |  | ||||||
|                   textCapitalization: TextCapitalization.sentences, |  | ||||||
|                   decoration: InputDecoration( |  | ||||||
|                     labelText: "Description", |  | ||||||
|                     hintText: "Example: 1GB data with unlimited talk & text.", |  | ||||||
|                     hintStyle: TextStyle(fontSize: 8.0), |  | ||||||
|                     errorStyle: TextStyle(fontSize: 10.0), |  | ||||||
|                   ), |  | ||||||
|                   initialValue: _desc, |  | ||||||
|                   validator: (value) { |  | ||||||
|                     return null; |  | ||||||
|                   }, |  | ||||||
|                   onSaved: (value) { |  | ||||||
|                     _desc = value!; |  | ||||||
|                   }, |  | ||||||
|                 ), |  | ||||||
|               ], |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|           actions: [ |           actions: [ | ||||||
|             Center( |             Center( | ||||||
|               child: ElevatedButton.icon( |               child: ElevatedButton.icon( | ||||||
| @@ -341,13 +373,22 @@ class _ExpenseInputDialogState extends State<ExpenseInputDialog> { | |||||||
|                   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, | ||||||
|  |                         ); | ||||||
|  |                       } | ||||||
|                     }); |                     }); | ||||||
|                     widget.notifyParent(); |                     widget.notifyParent(); | ||||||
|                     Navigator.of(context).pop(); |                     Navigator.of(context).pop(); | ||||||
|   | |||||||
							
								
								
									
										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, | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -5,8 +5,10 @@ | |||||||
| import FlutterMacOS | import FlutterMacOS | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
|  | import path_provider_foundation | ||||||
| import sqflite_darwin | 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")) |   SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										98
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										98
									
								
								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,61 @@ 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: |   platform: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -184,6 +240,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.5.4+6" |     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: |   sqflite_darwin: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -200,6 +264,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.4.0" |     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: | ||||||
| @@ -248,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: | ||||||
| @@ -264,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.24.0" |   flutter: ">=3.24.0" | ||||||
|   | |||||||
| @@ -9,7 +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: ^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