diff --git a/README.md b/README.md index 32a2afa..9444045 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,102 @@ -# dash +# Dash -A new Flutter project. +A Flutter mobile app for tracking car maintenance. Keep a garage of vehicles and log transactions like oil changes, repairs, and costs per car. -## Getting Started +This project is a test for implementing a single codebase that can later be used for a Firebase-backed cloud-hosted option. -This project is a starting point for a Flutter application. +![](/docs/dashboard.png) -A few resources to get you started if this is your first Flutter project: +## Features -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) +- Add and manage multiple vehicles with brand icons, VIN, plate, and mileage +- Log maintenance transactions with type, date, cost, and mileage +- SQLite-backed persistence via `sqflite` +- Provider state management -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +## Project Structure + +``` +lib/ + main.dart # App entry point, root scaffold, drawer + theme.dart # App-wide Material theme + models/ + car.dart # Car data model + DB mapping + txn.dart # Transaction data model + DB mapping + screens/ + car_detail.dart # View a single car and its transactions + car_edit.dart # Edit car details + car_new.dart # Add a new car + txn_new.dart # Log a new transaction + txn_type.dart # Transaction type picker + iconpicker.dart # Manufacturer icon picker + about.dart # About screen + utils/ + dbhelper_sqflite.dart # SQLite singleton (cars + txns tables) + garage_model.dart # ChangeNotifier provider (app state) +scripts/ + seed_dummy_data.py # Creates demo cars + transactions +``` + +## Quick Setup + +This repo was revived mainly to run the Windows desktop app and make screenshots. +Do not use Chrome/web for this version. + +```powershell +flutter pub get +flutter run -d windows +``` + +Android is not currently the happy path because some dependencies are old. + +## SQLite Setup + +The app uses `sqflite` on mobile and `sqflite_common_ffi` on Windows desktop. +Desktop FFI is initialized in `lib/main.dart` before `runApp()`. + +At runtime the app does **not** use `cars_sqlite.db` from the project folder. +It opens the database from the platform documents directory. On Windows this is usually your Documents folder: +```text +$env:userprofile\Documents\cars_sqlite.db +``` + +The app prints the exact path when it opens the DB: +```text +sqlite db path: $env:userprofile\Documents\cars_sqlite.db +``` + +## Dummy Data + +Generate demo cars and transactions in the project folder: + +```powershell +python scripts\seed_dummy_data.py --reset cars_sqlite.db +``` + +Copy that seeded DB to the Windows app location: + +```powershell +Copy-Item .\cars_sqlite.db "$HOME\Documents\cars_sqlite.db" -Force +``` + +Then run the app: + +```powershell +flutter run -d windows +``` + +The seed script creates 4 cars and 28 transactions. + +Important DB version note: `sqflite` uses SQLite `PRAGMA user_version` to decide whether to run `onCreate`. A database created outside Flutter must have `user_version=1`, or the app may try to create tables that already exist. The seed script handles this. + +## Testing / Checks + +Useful commands: + +```powershell +flutter analyze +flutter test +python -m py_compile scripts\seed_dummy_data.py +``` + +Note: no unit tests exist yet. The `bups/` directory contains dated backup snapshots of earlier iterations and is not part of the build. diff --git a/cars_sqlite.db b/cars_sqlite.db deleted file mode 100644 index dea193a..0000000 Binary files a/cars_sqlite.db and /dev/null differ diff --git a/lib/bups/dbhelper_example.dart b/lib/bups/dbhelper_example.dart deleted file mode 100644 index b5e1600..0000000 --- a/lib/bups/dbhelper_example.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'dart:io'; - -import 'package:path/path.dart'; -import 'package:sqflite/sqflite.dart'; -import 'package:path_provider/path_provider.dart'; - -class DatabaseHelper { - static final _databaseName = "MyDatabase.db"; - static final _databaseVersion = 1; - - static final table = 'my_table'; - - static final columnId = '_id'; - static final columnName = 'name'; - static final columnAge = 'age'; - - // make this a singleton class - DatabaseHelper._privateConstructor(); - static final DatabaseHelper instance = DatabaseHelper._privateConstructor(); - - // only have a single app-wide reference to the database - static Database _database; - Future get database async { - if (_database != null) return _database; - // lazily instantiate the db the first time it is accessed - _database = await _initDatabase(); - return _database; - } - - // this opens the database (and creates it if it doesn't exist) - _initDatabase() async { - Directory documentsDirectory = await getApplicationDocumentsDirectory(); - String path = join(documentsDirectory.path, _databaseName); - return await openDatabase(path, - version: _databaseVersion, onCreate: _onCreate); - } - - // SQL code to create the database table - Future _onCreate(Database db, int version) async { - await db.execute(''' - CREATE TABLE $table ( - $columnId INTEGER PRIMARY KEY, - $columnName TEXT NOT NULL, - $columnAge INTEGER NOT NULL - ) - '''); - } - - // Helper methods - - // Inserts a row in the database where each key in the Map is a column name - // and the value is the column value. The return value is the id of the - // inserted row. - Future insert(Map row) async { - Database db = await instance.database; - return await db.insert(table, row); - } - - // All of the rows are returned as a list of maps, where each map is - // a key-value list of columns. - Future>> queryAllRows() async { - Database db = await instance.database; - return await db.query(table); - } - - // All of the methods (insert, query, update, delete) can also be done using - // raw SQL commands. This method uses a raw query to give the row count. - Future queryRowCount() async { - Database db = await instance.database; - return Sqflite.firstIntValue( - await db.rawQuery('SELECT COUNT(*) FROM $table')); - } - - // We are assuming here that the id column in the map is set. The other - // column values will be used to update the row. - Future update(Map row) async { - Database db = await instance.database; - int id = row[columnId]; - return await db.update(table, row, where: '$columnId = ?', whereArgs: [id]); - } - - // Deletes the row specified by the id. The number of affected rows is - // returned. This should be 1 as long as the row exists. - Future delete(int id) async { - Database db = await instance.database; - return await db.delete(table, where: '$columnId = ?', whereArgs: [id]); - } -} diff --git a/lib/bups/dummy.dart b/lib/bups/dummy.dart deleted file mode 100644 index 96326f8..0000000 --- a/lib/bups/dummy.dart +++ /dev/null @@ -1,5 +0,0 @@ - - -void main() { - runApp(const FormApp()); -} \ No newline at end of file diff --git a/lib/bups/firebase_options.dart b/lib/bups/firebase_options.dart deleted file mode 100644 index a3f43fd..0000000 --- a/lib/bups/firebase_options.dart +++ /dev/null @@ -1,83 +0,0 @@ -// File generated by FlutterFire CLI. -// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members -import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; -import 'package:flutter/foundation.dart' - show defaultTargetPlatform, kIsWeb, TargetPlatform; - -/// Default [FirebaseOptions] for use with your Firebase apps. -/// -/// Example: -/// ```dart -/// import 'firebase_options.dart'; -/// // ... -/// await Firebase.initializeApp( -/// options: DefaultFirebaseOptions.currentPlatform, -/// ); -/// ``` -class DefaultFirebaseOptions { - static FirebaseOptions get currentPlatform { - if (kIsWeb) { - return web; - } - switch (defaultTargetPlatform) { - case TargetPlatform.android: - return android; - case TargetPlatform.iOS: - return ios; - case TargetPlatform.macOS: - return macos; - case TargetPlatform.windows: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for windows - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - case TargetPlatform.linux: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for linux - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - default: - throw UnsupportedError( - 'DefaultFirebaseOptions are not supported for this platform.', - ); - } - } - - static const FirebaseOptions web = FirebaseOptions( - apiKey: 'AIzaSyC3r_l3tj1x9Za0XXio89OIFWIY3Um40c8', - appId: '1:497486937927:web:66bcf53b6b41b7d84d5662', - messagingSenderId: '497486937927', - projectId: 'dash-6e8c2', - authDomain: 'dash-6e8c2.firebaseapp.com', - storageBucket: 'dash-6e8c2.appspot.com', - measurementId: 'G-XZVH49N86G', - ); - - static const FirebaseOptions android = FirebaseOptions( - apiKey: 'AIzaSyB5CA0f2J9qB4zt5YLaXCX-lMP8dKkFpmM', - appId: '1:497486937927:android:133640261945f9744d5662', - messagingSenderId: '497486937927', - projectId: 'dash-6e8c2', - storageBucket: 'dash-6e8c2.appspot.com', - ); - - static const FirebaseOptions ios = FirebaseOptions( - apiKey: 'AIzaSyA6mRZkuPnAFsvfYeClMRpeuB174x5-Aqk', - appId: '1:497486937927:ios:0c6b9a9a0c06b4484d5662', - messagingSenderId: '497486937927', - projectId: 'dash-6e8c2', - storageBucket: 'dash-6e8c2.appspot.com', - iosClientId: '497486937927-80niucdhlshtgeragten316bdnpqgo8u.apps.googleusercontent.com', - iosBundleId: 'com.example.dash', - ); - - static const FirebaseOptions macos = FirebaseOptions( - apiKey: 'AIzaSyA6mRZkuPnAFsvfYeClMRpeuB174x5-Aqk', - appId: '1:497486937927:ios:0c6b9a9a0c06b4484d5662', - messagingSenderId: '497486937927', - projectId: 'dash-6e8c2', - storageBucket: 'dash-6e8c2.appspot.com', - iosClientId: '497486937927-80niucdhlshtgeragten316bdnpqgo8u.apps.googleusercontent.com', - iosBundleId: 'com.example.dash', - ); -} diff --git a/lib/bups/iconpicker-1.dart b/lib/bups/iconpicker-1.dart deleted file mode 100644 index 9ff2ccf..0000000 --- a/lib/bups/iconpicker-1.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'dart:convert'; -import 'dart:async'; -import 'package:flutter/material.dart'; -// import '../images/car_icons.svg'; - -class IconPicker extends StatefulWidget { - IconPicker({super.key}); - - late final String? selectedIcon; - - @override - State createState() => _IconPickerState(); -} - -class _IconPickerState extends State { - late Future mfst; - late final Future manifestJson; - late final Future> icons; - late List iconList; - late List iconNames; - late Map iconMap; - late List searchableList = []; - late List items; - - TextEditingController editingController = TextEditingController(); - - @override - void initState() { - super.initState(); - icons = loadAssets(); - // iconNames = buildIconList(icons); - } - - Future> loadAssets() async { - final manifestJson = - await DefaultAssetBundle.of(context).loadString('AssetManifest.json'); - // ignore: no_leading_underscores_for_local_identifiers - final _icons = json - .decode(manifestJson) - .keys - .where((String key) => key.startsWith('images/car_icons/')) - .toList(); - return _icons; - } - - List buildIconList(List icons) { - List iN = []; - for (var i in icons) { - iN.add(i.substring(17, i.length - 4)); - } - return iN; - } - - void filterSearch(String query, List iconList) { - if (query.isNotEmpty) { - for (var item in iconList) { - if (item.contains(query)) { - searchableList.add(item); - } - } - setState(() { - items.clear(); - items.addAll(searchableList); - print(items); - }); - return; - } else { - setState(() { - items.clear(); - items.addAll(iconList); - }); - } - } - - @override - Widget build(BuildContext context) { - // mfst = DefaultAssetBundle.of(context).loadString('AssetManifest.json'); - // icons = json.decode(mfst).keys.where((String key) => key.startsWith('assets/images/car_icons_svg')).toList(); - // icons = loadAssets() as List; - return FutureBuilder( - future: icons, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } else { - if (snapshot.error != null) { - return const Center( - child: Text('An error occurred'), - ); - } else { - iconList = snapshot.data!; - iconNames = buildIconList(iconList); - print('iconlist is ${iconList.sublist(0, 5)}...'); - print('iconNames is ${iconNames.sublist(0, 5)}...'); - return Scaffold( - appBar: AppBar( - title: const Text('test'), - backgroundColor: const Color.fromARGB(255, 185, 47, 5), - ), - body: Column(children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - onChanged: (value) { - filterSearch(value, snapshot.data!); - }, - controller: editingController, - ), - ), - Expanded( - child: ListView.builder( - itemCount: iconList.length, - itemBuilder: (context, index) => ListTile( - leading: CircleAvatar( - backgroundImage: AssetImage(iconList[index]), - backgroundColor: Colors.white, - ), - title: Text(iconNames[index]), - onTap: (() => VoidCallback)), - )) - ])); - } - } - }); - } - -/* Widget build(BuildContext context) { - return ListView.builder( - itemCount: icons.length, - itemBuilder: (context, index) => ListTile( - leading: Image(image: AssetImage(icons[index])), - title: const Text('title'), - ), - ); - } */ -} diff --git a/lib/bups/main - Copy.dart b/lib/bups/main - Copy.dart deleted file mode 100644 index 96f4f06..0000000 --- a/lib/bups/main - Copy.dart +++ /dev/null @@ -1,417 +0,0 @@ -import 'dart:collection'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'dart:async'; -import 'package:path/path.dart'; -import 'package:sqflite/sqflite.dart'; -// import 'package:flutter/src/widgets/form.dart'; - -void main() async { - runApp( - ChangeNotifierProvider( - create: (context) => GarageModel(), - child: MaterialApp( - title: 'Dashboard', - initialRoute: '/', - routes: { - '/' : (context) => const MyApp(), - '/newcar' : (context) => const NewCarScreen(), - '/about': (context) => const AboutScreen(), - }, - // home: MyApp() - ), - ), - ); - WidgetsFlutterBinding.ensureInitialized(); - final database = openDatabase( - join(await getDatabasesPath(), 'dash_db_sqlite.db'), - onCreate: (db, version) { - return db.execute( - '''CREATE TABLE cars( - id INT INCREMENT, - nickname STRING, - vin STRING PRIMARY KEY, - mileage INT, - owner STRING)''', - ); - }, - version: 1, - ); - -/* - Future insertCar(Car car) async { - final db = await database; - await db.insert( - 'cars', - car.toMap(), - conflictAlgorithm: ConflictAlgorithm.abort, - ); - } - - Future> getCars() async { - final db = await database; - final List> maps = await db.query('cars'); - return List.generate(maps.length, (i) { - return Car( - nickname: maps[i]['nickname'], - vin: maps[i]['vin'], - plate: maps[i]['plate'], - mileage: maps[i]['mileage'], - owner: maps[i]['owner'], - ); - }); - } - - Future updateCar(Car car) async { - final db = await database; - await db.update( - 'cars', - car.toMap(), - where: 'vin=?', - whereArgs: [car.vin], - ); - } - - Future deleteCar(Car car) async { - final db = await database; - await db.delete( - 'cars', - where: 'vin=?', - whereArgs: ['vin'], - ); - } - */ -} - -class GarageModel extends ChangeNotifier { - /// internal state of garage - final List _cars = []; - - UnmodifiableListView get cars => UnmodifiableListView(_cars); - - void add(Car car) { - _cars.add(car); - notifyListeners(); - } - - void removeAll() { - _cars.clear(); - notifyListeners(); - } -} - -/* class Car { - final String vin; - final String plate; - final String nickname; - final int mileage; - final String owner; - - const Car({ - required this.vin, - required this.plate, - required this.nickname, - required this.owner, - required this.mileage, - }); - - // build map, keys must match db col names - Map toMap() { - return { - 'vin': vin, - 'plate': plate, - 'nickname': nickname, - 'owner': owner, - 'mileage': mileage, - }; - } - - @override - String toString() { - return 'Car{nickname: $nickname, vin: $vin, plate: $plate, owner: $owner}'; - } -} */ - -class Car { - Car({this.vin, this.plate, this.nickname, this.owner, this.mileage}); - - String? vin; - String? plate; - String? nickname; - String? owner; - int? mileage; -} - -class MyApp extends StatefulWidget { - const MyApp({super.key}); - - @override - State createState() => _MyAppState(); -} - -class _MyAppState extends State { - final _formKey = GlobalKey(); - -/* _list() => Expanded( - child: Card( - margin: EdgeInsets.fromLTRB(20,30,20,0), - child: ListView.builder( - padding: EdgeInsets.all(8), - itemBuilder: (context, index){ - return Column( - children: [ - ListTile( - leading: Icon(Icons.directions_car), - title: Text(_cares[index].nickname ?? ''), - subtitle: Text(_cares[index].vin ?? '')m - ), - Divider(height: 5.0), - ] - ); - }, - itemCount: _cares.length, - ) - ) - ); */ - -/* Widget carInfo = Container( - padding: const EdgeInsets.all(16), - child: _list(), - ); */ - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Colors.indigo, - title: const Text('Dashboard'), - // leading: IconButton( - // icon: const Icon(Icons.menu), - // ), - ), - drawer: Drawer( - child: ListView( - padding: EdgeInsets.zero, - children: [ - const SizedBox( - height: 64.0, - child: DrawerHeader( - decoration: BoxDecoration( - color: Colors.blue, - ), - child: Text( - 'Header', - style: TextStyle( - color: Colors.white, - fontSize: 18, - ), - ), - ), - ), - ListTile( - leading: Icon(Icons.settings), - title: Text('Settings'), - onTap: () { - print('settings'); - }, - // do something - ), - ListTile( - leading: Icon(Icons.sync), - title: Text('Setup Database'), - onTap: () { - print('dbsetup'); - }, - ), - ListTile( - leading: Icon(Icons.info_outline), - title: Text('About'), - onTap: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => const AboutScreen())); - }, - ), - ], - ), - ), - body: Container( - color: Colors.lightBlue, - child: Column( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(32), - child: _CarList(), - ), - ), - const Divider(height: 3, color: Colors.grey), - ] - ) - ), - /* ListView.builder(itemBuilder: (_, index) { - return Container( - padding: const EdgeInsets.all(8), - child: carInfo, - ); - }), */ - floatingActionButton: FloatingActionButton( - child: const Icon(Icons.add), - onPressed: () { //replace with route - Navigator.pushNamed(context, '/newcar'); - /* Navigator.of(context).push( - MaterialPageRoute(builder: (context) => const NewCarScreen())); */ - }, - ), - ); - } - - // _list() => Container(); - - void updateCars(Care care) { - setState(() { - _cares.add(care); - }); - } //will this work??????????????????????????? -} - -class AboutScreen extends StatelessWidget { - // change to AboutDialog - const AboutScreen( - {super.key}); //https://api.flutter.dev/flutter/material/AboutDialog-class.html - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('About Page'), - backgroundColor: Colors.indigo, - ), - body: const Center( - child: Text('About Page Text, Lorum Ipsum and whatnot.'), - )); - } -} - -class NewCarScreen extends StatefulWidget { - const NewCarScreen({super.key}); - - @override - State createState() => _NewCarScreenState(); -} - -class _NewCarScreenState extends State { - final GlobalKey _formKey = GlobalKey(); - Care _care = Care(); - List _cares = []; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Add New Car'), - backgroundColor: Colors.indigo, - ), - body: Container( - padding: EdgeInsets.all(32), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - //new car form text fields - TextFormField( - decoration: const InputDecoration( - labelText: 'Nickname', - ), - onSaved: (val) => setState(() => _care.nickname = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a nickname'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'VIN', - ), - onSaved: (val) => setState(() => _care.vin = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a unique VIN'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Mileage', - ), - onSaved: (val) => - setState(() => _care.mileage = int.parse(val ?? "")), - // inputFormatters: [FilteringTextInputFormatter.digitsOnly] //try this? - keyboardType: TextInputType.number, - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter car\'s current mileage'; - } - return null; - }, - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - _onSubmit(); - } - }, - child: Row( - children: const [ - Icon(Icons.add), - Text('Add Car'), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } - - _onSubmit() { - var form = _formKey.currentState!; - // _formKey.currentState!.save(); - form.save(); //callback in form text fields - print(_care.nickname); - setState(() { //ensure you re-pass values, - _cares.add(Care(vin:_care.vin,nickname:_care.nickname,owner:_care.owner, plate:_care.plate, mileage:_care.mileage)); - }); - setState(() { - widget._list.add(_care)); - }); - // widget.onSubmit(_care); - form.reset(); - } - - Future _returnHome(BuildContext context) async { - final newCar = await Navigator.push( - context, - MaterialPageRoute(builder: (context) => const MyApp()), - ); - - if (!mounted) return; - } -} - - - -/* class SelectCarIconScreen extends StatelessWidget { - const SelectCarIconScreen({super.key}); - - @override - Widget build(BuildContext context) { - return - } -} */ \ No newline at end of file diff --git a/lib/bups/main-20221109.dart b/lib/bups/main-20221109.dart deleted file mode 100644 index dc1336c..0000000 --- a/lib/bups/main-20221109.dart +++ /dev/null @@ -1,911 +0,0 @@ -import 'dart:collection'; -import 'package:dash/utils/dbhelper_sqflite.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -// import 'package:path/path.dart'; -// import 'package:sqflite/sqflite.dart'; -import 'package:dash/models/car.dart'; -import 'package:dash/models/txn.dart'; -// import 'package:flutter/src/widgets/form.dart'; - -void main() async { - runApp( - ChangeNotifierProvider( - create: (context) => GarageModel(), - child: const MaterialApp( - title: 'Dashboard', - home: MyApp(), - /* initialRoute: '/', - routes: { - '/': (context) => const MyApp(), - '/about': (context) => const AboutScreen(), - '/newcar': (context) => const NewCarScreen(), - '/detail': (context) => const CarDetailScreen(), - '/edit': (context) => EditCarScreen(), - }, */ - ), - ), - ); - // WidgetsFlutterBinding.ensureInitialized(); // is this necessary? -} - -class GarageModel extends ChangeNotifier { - /// internal state of garage - late List _cars = []; - late List _txns = []; // hold current car txns, repl on new load - final DbHelperSqlite _dbHelper = DbHelperSqlite.instance; - UnmodifiableListView get cars => UnmodifiableListView(_cars); - UnmodifiableListView get txns => UnmodifiableListView(_txns); - - void add(Car car) { - // _cars.add(car); - _dbHelper.insertCar(car); - getCars(); // prob nec to update `id` - notifyListeners(); - } - - void update(Car car, int carIndex) { - _cars[carIndex] = car; // replace list car with new values - _dbHelper.updateCar(car); - print('updated car ${car.toMap()}'); - getCars(); - // print(_cars); - notifyListeners(); - } - - void delete(Car car) { - // ignore: no_leading_underscores_for_local_identifiers - _cars.removeWhere((_car) => _car.id == car.id); - _dbHelper.deleteCar(car); - print('$car with id ${car.id} deleted'); - getCars(); - notifyListeners(); - } - - void clearCarList() { - // clear _cars; doesn't affect database - // useful for clearing cars that didn't make it into the db - _cars.clear(); - notifyListeners(); - } - - void deleteAllCars() { - // delete rows from CarTable - _cars.clear(); - _dbHelper.deleteAll(); - notifyListeners(); - } - - Future getCars() async { - // _cars.clear(); - print('getcars'); - _cars = await _dbHelper.fetchCars(); - print(_cars); - // notifyListeners(); //for some reason this inits db infinitely - } - - void insertTxn(Txn txn) async { - await _dbHelper.insertTxn(txn); - // update car - await getTxns(txn.carid!); - notifyListeners(); - } - - void deleteTxn(Txn txn) { - _dbHelper.deleteTxn(txn); - // get old mileage from car - notifyListeners(); - } - - Future getTxns(int carid) async { - _txns = await _dbHelper.fetchTxns(carid); - print('get txns: $_txns'); - } -} - -class Garage extends StatefulWidget { - const Garage({super.key}); - @override - State createState() => _Garage(); -} - -class _Garage extends State { - late Future _cars; - - @override - void initState() { - _cars = Provider.of(context, listen: false).getCars(); - // _cars = Provider.of(context).getCars(); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _cars, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator(), - ); - } else { - if (snapshot.error != null) { - return const Center( - child: Text('An error occurred'), - ); - } else { - return Expanded( - child: Consumer(builder: (context, garage, child) { - return ListView.separated( - // padding: const EdgeInsets.all(8), - itemCount: garage._cars.length, - itemBuilder: (context, index) => ListTile( - leading: const Icon(Icons.directions_car), - title: Text(garage.cars[index].nickname ?? "nick_ph"), - subtitle: Text(garage.cars[index].vin ?? "vin_ph"), -/* trailing: IconButton( - onPressed: Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EditCarScreen( - car: garage._cars[index], carIndex: index)), - ), - icon: Icons.edit), - ), - onTap: () => Navigator.pushNamed(context, '/detail'), */ - onTap: () { - Navigator.of(context).push( - // context, - MaterialPageRoute( - builder: (context) => CarDetailScreen(carIndex: index), - ), - ); - }, - ), - separatorBuilder: (BuildContext context, int index) => - const Divider(), - ); - })); - } - } - }, - ); - } -} - -class CurrentCar extends StatefulWidget { - CurrentCar({super.key, required this.car}); - Car car; - - @override - State createState() => _CurrentCar(); -} - -class _CurrentCar extends State { - late Future _txns; - late Car car = widget.car; - - @override - void initState() { - _txns = Provider.of(context, listen: false).getTxns(car.id!); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _txns, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator(), - ); - } else { - if (snapshot.error != null) { - return const Center( - child: Text('An error occurred'), - ); - } else { - return Expanded(child: Consumer( - builder: (context, garage, child) { - return ListView.separated( - // padding: const EdgeInsets.all(8), - itemCount: garage.txns.length, - itemBuilder: (context, index) => ListTile( - // dense: true, - visualDensity: - const VisualDensity(horizontal: 0, vertical: -4), - leading: const Icon(Icons.local_gas_station), - // leading: const Icon(Icons.build), - title: Text(garage.txns[index].txntype ?? "type"), - subtitle: Text(garage.txns[index].note ?? "note"), - onTap: (() => VoidCallback)), - separatorBuilder: (BuildContext context, int index) => - const Divider(), - ); - }, - )); - } - } - }); - } -} - -class MyDrawer extends StatelessWidget { - const MyDrawer({super.key}); - - @override - Widget build(BuildContext context) { - return Drawer( - child: ListView( - padding: EdgeInsets.zero, - children: [ - const SizedBox( - height: 80.0, - child: DrawerHeader( - decoration: BoxDecoration( - color: Color.fromARGB(255, 185, 47, 5), - ), - child: Text( - 'Options', - style: TextStyle( - color: Colors.white, - fontSize: 18, - ), - ), - ), - ), - ListTile( - leading: const Icon(Icons.settings), - title: const Text('Settings'), - onTap: () { - print('settings'); - }, - // do something - ), - ListTile( - leading: const Icon(Icons.sync), - title: const Text('Setup Database'), - onTap: () { - print('dbsetup'); - }, - ), - ListTile( - leading: const Icon(Icons.info_outline), - title: const Text('About'), - onTap: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => const AboutScreen())); - }, - ), - ], - ), - ); - } -} - -class MyApp extends StatefulWidget { - const MyApp({super.key}); - - @override - State createState() => _MyAppState(); -} - -class _MyAppState extends State { - @override - Widget build(BuildContext context) { - var garage = context.watch(); // testing - return Scaffold( - appBar: AppBar( - backgroundColor: const Color.fromARGB(255, 185, 47, 5), - title: const Text('Dashboard'), - ), - drawer: const MyDrawer(), - body: Container( - padding: const EdgeInsets.all(32), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: (() => garage.clearCarList()), - // onPressed: (() => VoidCallback), - onLongPress: () { - garage.deleteAllCars(); - // VoidCallback; - }, - child: Row( - children: const [ - Icon(Icons.delete), - Text('Remove All Cars'), - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: (() => garage.getCars()), - // onPressed: (() => VoidCallback), - child: Row( - children: const [ - Icon(Icons.sync), - Text('Refresh garage'), - ], - ), - ), - ), - const Garage(), // list of cars in garage - ], - ), - ), - floatingActionButton: FloatingActionButton( - child: const Icon(Icons.add), - onPressed: () { - Navigator.push(context, - MaterialPageRoute(builder: (context) => const NewCarScreen())); - }, - ), - ); - } -} - -class AboutScreen extends StatelessWidget { - // change to AboutDialog - const AboutScreen( - {super.key}); //https://api.flutter.dev/flutter/material/AboutDialog-class.html - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('About Page'), - backgroundColor: const Color.fromARGB(255, 185, 47, 5), - ), - body: const Center( - child: Text('About Page Text, Lorum Ipsum and whatnot.'), - )); - } -} - -class NewCarScreen extends StatefulWidget { - const NewCarScreen({super.key}); - - @override - State createState() => _NewCarScreenState(); -} - -class _NewCarScreenState extends State { - final GlobalKey _formKey = GlobalKey(); - Car _car = Car(); //initialization is required - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Add New Car'), - backgroundColor: Colors.indigo, - ), - body: Container( - padding: const EdgeInsets.all(32), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - //new car form text fields - TextFormField( - decoration: const InputDecoration( - labelText: 'Nickname', - ), - onSaved: (val) => setState(() => _car.nickname = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a nickname'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'VIN', - ), - onSaved: (val) => setState(() => _car.vin = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a unique VIN'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'License Plate', - ), - onSaved: (val) => setState(() => _car.plate = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a license plate'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Mileage', - ), - onSaved: (val) => - setState(() => _car.mileage = int.parse(val ?? "")), - // inputFormatters: [FilteringTextInputFormatter.digitsOnly] //try this? - keyboardType: TextInputType.number, - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter car\'s current mileage'; - } - return null; - }, - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: ElevatedButton( - // onPressed: () => _onSubmit(context), - onPressed: () { - final _form = _formKey.currentState!; - if (_form.validate()) { - //validate form - _form - .save(); //save values (reqd before putting them anywhere) - setState(() { - //modify state of car (necessary?) - _car = Car( - vin: _car.vin, - nickname: _car.nickname, - plate: _car.plate, - mileage: _car.mileage); - }); - var garage = - context.read(); //implement Provider - garage.add(_car); - _form.reset(); - Navigator.pushNamed(context, '/'); - } - }, - child: Row( - children: const [ - Icon(Icons.add), - Text('Add Car'), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } - - _onSubmit(context) async { - final _form = _formKey.currentState!; - if (_form.validate()) { - //validate form - _form.save(); //save values (reqd before putting them anywhere) - setState(() { - //modify state of car (necessary?) - _car = Car( - vin: _car.vin, - nickname: _car.nickname, - plate: _car.plate, - mileage: _car.mileage); - }); - var garage = context.read(); //implement Provider - garage.add(_car); - _form.reset(); - Navigator.pushNamed(context, '/'); - } - } -} - -class CarDetailScreen extends StatelessWidget { - // const CarDetailScreen({super.key, required this.car, required this.carIndex}); - CarDetailScreen({super.key, required this.carIndex}); - final int carIndex; //pass this thru for edit screen - // late Car car; - - @override - Widget build(BuildContext context) { - late Car car = context.read()._cars[carIndex]; - return Scaffold( - appBar: AppBar( - backgroundColor: const Color.fromARGB(255, 185, 47, 5), - title: Text(car.nickname ?? ""), - ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - const Text("plh img picker"), - Center( - // edit car screen - child: Ink( - decoration: const ShapeDecoration( - color: Colors.lightBlue, shape: CircleBorder()), - child: IconButton( - icon: const Icon(Icons.edit), - color: Colors.white, - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - EditCarScreen(car: car, carIndex: carIndex)), - ); - }, - ), - ), - ), - ], - ), - IconButton( - iconSize: 48.0, - icon: const Icon(Icons.directions_car), - onPressed: (() => VoidCallback), - ), - Text("Nickname: ${car.nickname}"), - Text("VIN: ${car.vin}"), - Text("License Plate: ${car.plate}"), - Text("Mileage: ${car.mileage.toString()}"), - CurrentCar(car: car), - ], - ), - ), - floatingActionButton: FloatingActionButton( - // add txn - onPressed: () { - Navigator.of(context).push( - // context, - MaterialPageRoute( - builder: (context) => NewTxn( - car: car, - carIndex: carIndex), //add car index so you can update? - ), - ); - }, - child: const Icon(Icons.edit), - ), - ); - } -} - -class EditCarScreen extends StatefulWidget { - EditCarScreen({super.key, required this.car, required this.carIndex}); - Car car; - int carIndex; - - @override - State createState() => _EditCarScreenState(); -} - -class _EditCarScreenState extends State { - final GlobalKey _formKey = GlobalKey(); - late Car car = widget.car; //initialization is required - late int carIndex = widget.carIndex; - - @override - Widget build(BuildContext context) { - var garage = context.read(); - return Scaffold( - appBar: AppBar( - title: Text("Edit ${car.nickname}"), - backgroundColor: const Color.fromARGB(255, 185, 47, 5), - ), - body: Container( - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - //new car form text fields - TextFormField( - initialValue: car.nickname, - decoration: const InputDecoration( - labelText: 'Nickname', - ), - onSaved: (val) => setState(() => car.nickname = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a nickname'; - } - return null; - }, - ), - TextFormField( - initialValue: car.vin, - decoration: const InputDecoration( - labelText: 'VIN', - ), - onSaved: (val) => setState(() => car.vin = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a unique VIN'; - } - return null; - }, - ), - TextFormField( - initialValue: car.plate, - decoration: const InputDecoration( - labelText: 'License Plate', - ), - onSaved: (val) => setState(() => car.plate = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a license plate'; - } - return null; - }, - ), - TextFormField( - initialValue: car.mileage.toString(), - decoration: const InputDecoration( - labelText: 'Mileage', - ), - onSaved: (val) => - setState(() => car.mileage = int.parse(val ?? "")), - // inputFormatters: [FilteringTextInputFormatter.digitsOnly] //try this? - keyboardType: TextInputType.number, - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter car\'s current mileage'; - } - return null; - }, - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: () { - final updateForm = _formKey.currentState!; - if (updateForm.validate()) { - //validate form - updateForm - .save(); //save values (reqd before putting them anywhere) - garage.update(car, carIndex); - updateForm.reset(); - Navigator.pushNamed(context, '/'); - } - }, - child: Row( - children: const [ - //expand these children, too tight - Icon(Icons.save), - Text('Save Changes'), - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: (() => VoidCallback), - onLongPress: () { - garage.delete(car); - Navigator.pushNamed(context, '/'); - }, - child: Row(children: const [ - Icon(Icons.delete), - Text('Delete'), - ]), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: (() => VoidCallback), - child: Row(children: const [ - Icon(Icons.sync_alt), - Text('enable/disable'), - ]), - ), - ), - ], - ), - ), - ), - ); - } -} - -class NewTxn extends StatefulWidget { - NewTxn({super.key, required this.car, required this.carIndex}); - Car car; - int carIndex; - - @override - State createState() => _NewTxnState(); -} - -class _NewTxnState extends State { - final GlobalKey _formKey = GlobalKey(); - late Car car = widget.car; - late int carIndex = widget.carIndex; - DateTime datetime = DateTime.now(); - Txn txn = Txn(); - - Future _selectDate(BuildContext context) async { - final DateTime? timepicked = await showDatePicker( - context: context, - initialDate: datetime, - firstDate: DateTime.utc(1776, 7, 4), - lastDate: DateTime.utc(2222, 2, 22)); - if (timepicked != null && timepicked != datetime) { - setState(() => datetime = timepicked); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text("Add new txn for ${car.nickname}"), - backgroundColor: const Color.fromARGB(255, 185, 47, 5), - ), - body: Container( - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // txntype - change to dropdown - - TextFormField( - decoration: const InputDecoration( - labelText: 'Type', - ), - onSaved: (val) => setState(() => txn.txntype = val), - ), - Row( - children: [ - Flexible( - flex: 1, - child: IconButton( - onPressed: () => _selectDate(context), - icon: const Icon(Icons.calendar_month)), - ), - Flexible( - flex: 3, - child: InputDatePickerFormField( - firstDate: DateTime.utc(1776, 7, 4), - lastDate: DateTime.utc(2222, 2, 22), - initialDate: datetime, - onDateSaved: (val) => setState( - () => txn.datetime = val.millisecondsSinceEpoch)), - ), - ], - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Cost', - ), - onSaved: (val) => - setState(() => txn.cost = double.parse(val ?? "")), - keyboardType: TextInputType.number, - ), - TextFormField( - initialValue: car.mileage.toString(), - decoration: const InputDecoration( - labelText: 'Mileage', - ), - onSaved: (val) => - setState(() => txn.mileage = int.parse(val ?? "")), - keyboardType: TextInputType.number, - validator: (String? value) { - if (value == null || value.isEmpty) { - return "Must be >= current mileage"; - } else if (int.parse(value) < car.mileage!.toInt()) { - return 'Must be >= current mileage'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Note', - ), - onSaved: (val) => setState(() => txn.note = val), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: () { - final form = _formKey.currentState!; - if (form.validate()) { - form.save(); - setState(() { - txn.datetime = DateTime.now().millisecondsSinceEpoch; - txn.carid = car.id; - car.mileage = txn.mileage; // update car mileage too - }); - var garage = context.read(); - garage.update(car, carIndex); - print(txn.toMap()); - garage.insertTxn(txn); - form.reset(); - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - CarDetailScreen(carIndex: carIndex), - ), - ); - // Navigator.of(context).pop(); - } - }, - child: Row( - children: const [ - //expand these children, too tight - Icon(Icons.save), - Text('Save Changes'), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } -} - -/* class MyDatePicker extends StatefulWidget { - const MyDatePicker({super.key}); - @override - State createState() => _MyDatePicker(); -} - -class _MyDatePicker extends State { - @override - Widget build(BuildContext context) { - return Row(children: [ - Flexible( - flex: 1, - child: IconButton( - onPressed: () async { - showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: DateTime.utc(1776, 7, 4), - lastDate: DateTime.utc(2222, 2, 22)); - }, - icon: const Icon(Icons.calendar_month)), - ), - Flexible( - flex: 3, - child: InputDatePickerFormField( - firstDate: DateTime.utc(1776, 7, 4), - lastDate: DateTime.utc(2222, 2, 22), - initialDate: DateTime.now(), - onDateSaved: (val) => - setState(() => txn.datetime = val.millisecondsSinceEpoch)), - ), - ]); - } -} */ - -/* class IconPicker extends SimpleDialog { - IconPicker({super.key}) - - @override - Widget build (BuildContext context) { - return SimpleDialog( - title: const Text("Pick an icon"), - - ); - } -} */ \ No newline at end of file diff --git a/lib/bups/main-20221111.dart b/lib/bups/main-20221111.dart deleted file mode 100644 index c830184..0000000 --- a/lib/bups/main-20221111.dart +++ /dev/null @@ -1,856 +0,0 @@ -import 'dart:collection'; -import 'package:dash/utils/dbhelper_sqflite.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -// import 'package:path/path.dart'; -// import 'package:sqflite/sqflite.dart'; -import 'package:dash/models/car.dart'; -import 'package:dash/models/txn.dart'; -// import 'package:flutter/src/widgets/form.dart'; - -void main() async { - runApp( - ChangeNotifierProvider( - create: (context) => GarageModel(), - child: const MaterialApp( - title: 'Dashboard', - home: MyApp(), -/* initialRoute: '/', - routes: { - '/': (context) => const MyApp(), - '/about': (context) => const AboutScreen(), - '/newcar': (context) => const NewCarScreen(), - '/detail': (context) => CarDetailScreen(), - '/edit': (context) => EditCarScreen(), - }, */ - ), - ), - ); - // WidgetsFlutterBinding.ensureInitialized(); // is this necessary? -} - -class GarageModel extends ChangeNotifier { - /// internal state of garage - late List _cars = []; - late List _txns = []; // hold current car txns, repl on new load - final DbHelperSqlite _dbHelper = DbHelperSqlite.instance; - UnmodifiableListView get cars => UnmodifiableListView(_cars); - UnmodifiableListView get txns => UnmodifiableListView(_txns); - - void add(Car car) { - // _cars.add(car); - _dbHelper.insertCar(car); - getCars(); // prob nec to update `id` - notifyListeners(); - } - - void update(Car car, int carIndex) { - _cars[carIndex] = car; // replace list car with new values - _dbHelper.updateCar(car); - print('updated car ${car.toMap()}'); - getCars(); - // print(_cars); - notifyListeners(); - } - - void delete(Car car) { - // ignore: no_leading_underscores_for_local_identifiers - _cars.removeWhere((_car) => _car.id == car.id); - _dbHelper.deleteCar(car); - print('$car with id ${car.id} deleted'); - getCars(); - notifyListeners(); - } - - void clearCarList() { - // clear _cars; doesn't affect database - // useful for clearing cars that didn't make it into the db - _cars.clear(); - notifyListeners(); - } - - void deleteAllCars() { - // delete rows from CarTable - _cars.clear(); - _dbHelper.deleteAll(); - notifyListeners(); - } - - Future getCars() async { - // _cars.clear(); - print('getcars'); - _cars = await _dbHelper.fetchCars(); - print(_cars); - // notifyListeners(); //for some reason this inits db infinitely - } - - void insertTxn(Txn txn) async { - await _dbHelper.insertTxn(txn); - // update car - await getTxns(txn.carid!); - notifyListeners(); - } - - void deleteTxn(Txn txn) { - _dbHelper.deleteTxn(txn); - // get old mileage from car - notifyListeners(); - } - - Future getTxns(int carid) async { - _txns = await _dbHelper.fetchTxns(carid); - print('get txns: $_txns'); - } -} - -class Garage extends StatefulWidget { - const Garage({super.key}); - @override - State createState() => _Garage(); -} - -class _Garage extends State { - late Future _cars; - - @override - void initState() { - _cars = Provider.of(context, listen: false).getCars(); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _cars, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator(), - ); - } else { - if (snapshot.error != null) { - return const Center( - child: Text('An error occurred'), - ); - } else { - return Expanded( - child: Consumer(builder: (context, garage, child) { - return ListView.separated( - itemCount: garage._cars.length, - itemBuilder: (context, index) => ListTile( - leading: const Icon(Icons.directions_car), - title: Text(garage.cars[index].nickname ?? "nick_ph"), - subtitle: Text(garage.cars[index].vin ?? "vin_ph"), - onTap: () { - Navigator.of(context) - .push(MaterialPageRoute( - builder: (context) => - CarDetailScreen(carIndex: index), - )) - .then((value) => setState(() {})); - }, - ), - separatorBuilder: (BuildContext context, int index) => - const Divider(), - ); - })); - } - } - }, - ); - } -} - -class CurrentCar extends StatefulWidget { - const CurrentCar({super.key, required this.car}); - final Car car; - - @override - State createState() => _CurrentCar(); -} - -class _CurrentCar extends State { - late Future _txns; - late Car car = widget.car; - - @override - void initState() { - _txns = Provider.of(context, listen: false).getTxns(car.id!); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _txns, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator(), - ); - } else { - if (snapshot.error != null) { - return const Center( - child: Text('An error occurred'), - ); - } else { - return Expanded(child: Consumer( - builder: (context, garage, child) { - return ListView.separated( - // padding: const EdgeInsets.all(8), - itemCount: garage.txns.length, - itemBuilder: (context, index) => ListTile( - // dense: true, //only affects text, use visualDensity instead - visualDensity: - const VisualDensity(horizontal: 0, vertical: -4), - leading: const Icon(Icons.local_gas_station), - title: Text(garage.txns[index].txntype ?? "type"), - subtitle: Text(garage.txns[index].note ?? "note"), - onTap: (() => VoidCallback)), - separatorBuilder: (BuildContext context, int index) => - const Divider(), - ); - }, - )); - } - } - }); - } -} - -class MyDrawer extends StatelessWidget { - const MyDrawer({super.key}); - - @override - Widget build(BuildContext context) { - return Drawer( - child: ListView( - padding: EdgeInsets.zero, - children: [ - const SizedBox( - height: 80.0, - child: DrawerHeader( - decoration: BoxDecoration( - color: Color.fromARGB(255, 185, 47, 5), - ), - child: Text( - 'Options', - style: TextStyle( - color: Colors.white, - fontSize: 18, - ), - ), - ), - ), - ListTile( - leading: const Icon(Icons.settings), - title: const Text('Settings'), - onTap: () { - print('settings'); - }, - // do something - ), - ListTile( - leading: const Icon(Icons.sync), - title: const Text('Setup Database'), - onTap: () { - print('dbsetup'); - }, - ), - ListTile( - leading: const Icon(Icons.info_outline), - title: const Text('About'), - onTap: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => const AboutScreen())); - }, - ), - ], - ), - ); - } -} - -class MyApp extends StatefulWidget { - const MyApp({super.key}); - - @override - State createState() => _MyAppState(); -} - -class _MyAppState extends State { - @override - Widget build(BuildContext context) { - var garage = context.watch(); // testing - return Scaffold( - appBar: AppBar( - backgroundColor: const Color.fromARGB(255, 185, 47, 5), - title: const Text('Dashboard'), - ), - drawer: const MyDrawer(), - body: Container( - padding: const EdgeInsets.all(32), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: (() => garage.clearCarList()), - // onPressed: (() => VoidCallback), - onLongPress: () { - garage.deleteAllCars(); - // VoidCallback; - }, - child: Row( - children: const [ - Icon(Icons.delete), - Text('Remove All Cars'), - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: (() => garage.getCars()), - // onPressed: (() => VoidCallback), - child: Row( - children: const [ - Icon(Icons.sync), - Text('Refresh garage'), - ], - ), - ), - ), - const Garage(), // list of cars in garage - ], - ), - ), - floatingActionButton: FloatingActionButton( - child: const Icon(Icons.add), - onPressed: () { - Navigator.push(context, - MaterialPageRoute(builder: (context) => const NewCarScreen())); - }, - ), - ); - } -} - -class AboutScreen extends StatelessWidget { - // change to AboutDialog - const AboutScreen( - {super.key}); //https://api.flutter.dev/flutter/material/AboutDialog-class.html - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('About Page'), - backgroundColor: const Color.fromARGB(255, 185, 47, 5), - ), - body: const Center( - child: Text('About Page Text, Lorum Ipsum and whatnot.'), - )); - } -} - -class NewCarScreen extends StatefulWidget { - const NewCarScreen({super.key}); - - @override - State createState() => _NewCarScreenState(); -} - -class _NewCarScreenState extends State { - final GlobalKey _formKey = GlobalKey(); - Car _car = Car(); //initialization is required - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Add New Car'), - backgroundColor: Colors.indigo, - ), - body: Container( - padding: const EdgeInsets.all(32), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - //new car form text fields - TextFormField( - decoration: const InputDecoration( - labelText: 'Nickname', - ), - onSaved: (val) => setState(() => _car.nickname = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a nickname'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'VIN', - ), - onSaved: (val) => setState(() => _car.vin = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a unique VIN'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'License Plate', - ), - onSaved: (val) => setState(() => _car.plate = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a license plate'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Mileage', - ), - onSaved: (val) => - setState(() => _car.mileage = int.parse(val ?? "")), - // inputFormatters: [FilteringTextInputFormatter.digitsOnly] //try this? - keyboardType: TextInputType.number, - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter car\'s current mileage'; - } - return null; - }, - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: ElevatedButton( - // onPressed: () => _onSubmit(context), - onPressed: () { - final _form = _formKey.currentState!; - if (_form.validate()) { - //validate form - _form - .save(); //save values (reqd before putting them anywhere) - setState(() { - //modify state of car (necessary?) - _car = Car( - vin: _car.vin, - nickname: _car.nickname, - plate: _car.plate, - mileage: _car.mileage); - }); - var garage = context.read(); - garage.add(_car); - _form.reset(); - Navigator.pushNamed(context, '/'); - } - }, - child: Row( - children: const [ - Icon(Icons.add), - Text('Add Car'), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } - - _onSubmit(context) async { - final _form = _formKey.currentState!; - if (_form.validate()) { - //validate form - _form.save(); //save values (reqd before putting them anywhere) - setState(() { - //modify state of car (necessary?) - _car = Car( - vin: _car.vin, - nickname: _car.nickname, - plate: _car.plate, - mileage: _car.mileage); - }); - var garage = context.read(); //implement Provider - garage.add(_car); - _form.reset(); - Navigator.pushNamed(context, '/'); - } - } -} - -class CarDetailScreen extends StatefulWidget { - CarDetailScreen({super.key, required this.carIndex}); - final int carIndex; - @override - State createState() => _CarDetailScreenState(); -} - -class _CarDetailScreenState extends State { - late Car car = context.watch()._cars[widget.carIndex]; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: const Color.fromARGB(255, 185, 47, 5), - title: Text(car.nickname ?? ""), - ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - const Text("plh img picker"), - Center( - // edit car screen - child: Ink( - decoration: const ShapeDecoration( - color: Colors.lightBlue, shape: CircleBorder()), - child: IconButton( - icon: const Icon(Icons.edit), - color: Colors.white, - onPressed: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EditCarScreen( - car: car, carIndex: widget.carIndex)), - ); - }, - ), - ), - ), - ], - ), - IconButton( - iconSize: 48.0, - icon: const Icon(Icons.directions_car), - onPressed: (() => VoidCallback), - ), - Text("Nickname: ${car.nickname}"), - Text("VIN: ${car.vin}"), - Text("License Plate: ${car.plate}"), - Text("Mileage: ${car.mileage.toString()}"), - CurrentCar(car: car), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => NewTxn(car: car, carIndex: widget.carIndex), - ), - ); - }, - child: const Icon(Icons.add), - ), - ); - } -} - -class EditCarScreen extends StatefulWidget { - EditCarScreen({super.key, required this.car, required this.carIndex}); - Car car; - int carIndex; - - @override - State createState() => _EditCarScreenState(); -} - -class _EditCarScreenState extends State { - final GlobalKey _formKey = GlobalKey(); - late Car car = widget.car; - late int carIndex = widget.carIndex; - - @override - Widget build(BuildContext context) { - var garage = context.read(); - return Scaffold( - appBar: AppBar( - title: Text("Edit ${car.nickname}"), - backgroundColor: const Color.fromARGB(255, 185, 47, 5), - ), - body: Container( - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextFormField( - initialValue: car.nickname, - decoration: const InputDecoration( - labelText: 'Nickname', - ), - onSaved: (val) => setState(() => car.nickname = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a nickname'; - } - return null; - }, - ), - TextFormField( - initialValue: car.vin, - decoration: const InputDecoration( - labelText: 'VIN', - ), - onSaved: (val) => setState(() => car.vin = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a unique VIN'; - } - return null; - }, - ), - TextFormField( - initialValue: car.plate, - decoration: const InputDecoration( - labelText: 'License Plate', - ), - onSaved: (val) => setState(() => car.plate = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a license plate'; - } - return null; - }, - ), - TextFormField( - initialValue: car.mileage.toString(), - decoration: const InputDecoration( - labelText: 'Mileage', - ), - onSaved: (val) => - setState(() => car.mileage = int.parse(val ?? "")), - // inputFormatters: [FilteringTextInputFormatter.digitsOnly] //try this? - keyboardType: TextInputType.number, - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter car\'s current mileage'; - } - return null; - }, - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: () { - final updateForm = _formKey.currentState!; - if (updateForm.validate()) { - //validate form - updateForm - .save(); //save values (reqd before putting them anywhere) - garage.update(car, carIndex); - updateForm.reset(); - Navigator.pushNamed(context, '/'); - } - }, - child: Row( - children: const [ - //expand these children, too tight - Icon(Icons.save), - Text('Save Changes'), - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: (() => VoidCallback), - onLongPress: () { - garage.delete(car); - Navigator.pushNamed(context, '/'); - }, - child: Row(children: const [ - Icon(Icons.delete), - Text('Delete'), - ]), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: (() => VoidCallback), - child: Row(children: const [ - Icon(Icons.sync_alt), - Text('enable/disable'), - ]), - ), - ), - ], - ), - ), - ), - ); - } -} - -class NewTxn extends StatefulWidget { - NewTxn({super.key, required this.car, required this.carIndex}); - Car car; - int carIndex; - - @override - State createState() => _NewTxnState(); -} - -class _NewTxnState extends State { - final GlobalKey _formKey = GlobalKey(); - late Car car = widget.car; - late int carIndex = widget.carIndex; - DateTime datetime = DateTime.now(); - Txn txn = Txn(); - bool refresh = false; - - Future _selectDate(BuildContext context) async { - final DateTime? timepicked = await showDatePicker( - context: context, - initialDate: datetime, - firstDate: DateTime.utc(1776, 7, 4), - lastDate: DateTime.utc(2222, 2, 22)); - if (timepicked != null && timepicked != datetime) { - setState(() => datetime = timepicked); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text("Add new txn for ${car.nickname}"), - backgroundColor: const Color.fromARGB(255, 185, 47, 5), - ), - body: Container( - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // txntype - change to dropdown - - TextFormField( - decoration: const InputDecoration( - labelText: 'Type', - ), - onSaved: (val) => setState(() => txn.txntype = val), - ), - Row( - children: [ - Flexible( - flex: 1, - child: IconButton( - onPressed: () => _selectDate(context), - icon: const Icon(Icons.calendar_month)), - ), - Flexible( - flex: 3, - child: InputDatePickerFormField( - firstDate: DateTime.utc(1776, 7, 4), - lastDate: DateTime.utc(2222, 2, 22), - initialDate: datetime, - onDateSaved: (val) => setState( - () => txn.datetime = val.millisecondsSinceEpoch)), - ), - ], - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Cost', - ), - initialValue: "0", - onSaved: (val) => - setState(() => txn.cost = double.parse(val ?? "0")), - keyboardType: TextInputType.number, - ), - TextFormField( - initialValue: car.mileage.toString(), - decoration: const InputDecoration( - labelText: 'Mileage', - ), - onSaved: (val) => - setState(() => txn.mileage = int.parse(val ?? "")), - keyboardType: TextInputType.number, - validator: (String? value) { - if (value == null || value.isEmpty) { - return "Must be >= current mileage"; - } else if (int.parse(value) < car.mileage!.toInt()) { - return 'Must be >= current mileage'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Note', - ), - onSaved: (val) => setState(() => txn.note = val), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: () { - final form = _formKey.currentState!; - if (form.validate()) { - form.save(); - setState(() { - txn.datetime = DateTime.now().millisecondsSinceEpoch; - txn.carid = car.id; - car.mileage = txn.mileage; // update car mileage too - }); - var garage = context.read(); - garage.update(car, carIndex); - print(txn.toMap()); - garage.insertTxn(txn); - form.reset(); - Navigator.pop(context); - } - }, - child: Row( - children: const [ - //expand these children, too tight - Icon(Icons.save), - Text('Save Changes'), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } -} - -/* class IconPicker extends SimpleDialog { - IconPicker({super.key}) - - @override - Widget build (BuildContext context) { - return SimpleDialog( - title: const Text("Pick an icon"), - - ); - } -} */ \ No newline at end of file diff --git a/lib/bups/main-med.dart b/lib/bups/main-med.dart deleted file mode 100644 index bd868c7..0000000 --- a/lib/bups/main-med.dart +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright 2018 The Flutter team. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:dash/utils/dbhelper_sqflite.dart'; -import 'package:flutter/material.dart'; -// import 'package:flutter/provider.dart'; -import 'dart:async'; -import 'package:flutter/widgets.dart'; -import 'package:path/path.dart'; -import 'package:sqflite/sqflite.dart'; - - -void main() async { - runApp(MaterialApp(home: MyApp())); - final dbHelper = DbHelperSqlite.instance; //medium method - - var newCar = const Car( - vin: '123abc', - plate: 'n1ce', - nickname: 'My Car', - owner: 'Ben', - mileage: 7, - ); -} - -class Car { - final String vin; - final String plate; - final String nickname; - final int mileage; - final String owner; - - const Car({ - required this.vin, - required this.plate, - required this.nickname, - required this.owner, - required this.mileage, - }); - - // build map, keys must match db col names - Map toMap() { - return { - 'vin': vin, - 'plate': plate, - 'nickname': nickname, - 'owner': owner, - 'mileage': mileage, - }; - } - - @override - String toString() { - return 'Car{nickname: $nickname, vin: $vin, plate: $plate, owner: $owner}'; - } -} - -class MyApp extends StatefulWidget { - const MyApp({super.key}); - - @override - State createState() => _MyAppState(); -} - -class _MyAppState extends State { - @override - Widget build(BuildContext context) { - Widget carInfo = Container( - padding: const EdgeInsets.all(16), - child: Row(children: [ - Image.asset( - 'images/mazda.png', - ), - Icon( - Icons.directions_car_filled, - color: Colors.red[500], - ), - const Text('Nickname'), - ]), - ); - - return MaterialApp( - title: 'Dashboard App', - // home: const ManageScreen(), - home: Scaffold( - appBar: AppBar( - backgroundColor: Colors.indigo, - title: const Text('Dashboard'), - // leading: IconButton( - // icon: const Icon(Icons.menu), - // ), - ), - drawer: Drawer( - child: ListView( - padding: EdgeInsets.zero, - children: [ - const SizedBox( - height: 64.0, - child: DrawerHeader( - decoration: BoxDecoration( - color: Colors.blue, - ), - child: Text( - 'Header', - style: TextStyle( - color: Colors.white, - fontSize: 18, - ), - ), - ), - ), - ListTile( - leading: Icon(Icons.settings), - title: Text('Settings'), - onTap: () { - print('settings'); - }, - // do something - ), - ListTile( - leading: Icon(Icons.sync), - title: Text('Setup Database'), - onTap: () { - print('dbsetup'); - - }, - ), - ListTile( - leading: Icon(Icons.info_outline), - title: Text('About'), - onTap: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => const AboutScreen())); - }, - ), - ], - ), - ), - body: ListView.builder(itemBuilder: (_, index) { - return Container( - padding: const EdgeInsets.all(8), - child: carInfo, - ); - }), - floatingActionButton: FloatingActionButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => const NewCarScreen())); - }, - ), - ), - ); - } -} - -class AboutScreen extends StatelessWidget { // change to AboutDialog - const AboutScreen({super.key}); //https://api.flutter.dev/flutter/material/AboutDialog-class.html - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('About Page'), - backgroundColor: Colors.indigo, - ), - body: const Center( - child: Text('About Page Text, Lorum Ipsum and whatnot.'), - )); - } -} - -class NewCarScreen extends StatefulWidget { - const NewCarScreen({super.key}); - - @override - State createState() => _NewCarScreenState(); -} - -class _NewCarScreenState extends State { - final GlobalKey _formKey = GlobalKey(); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Add New Car'), - backgroundColor: Colors.indigo, - ), - body: Container( - padding: EdgeInsets.all(32), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Text('Add new car form here'), - TextFormField( - decoration: const InputDecoration( - hintText: 'Nickname for this car', - ), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a nickname'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - hintText: 'Unique VIN', - ), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a unique VIN'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - hintText: 'Mileage', - ), - keyboardType: TextInputType.number, - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter car\'s current mileage'; - } - return null; - }, - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - // process - } - }, - child: Row( - children: const [ - Icon(Icons.add), - Text('Add Car'), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } -} - - -class SelectCarIconScreen extends StatelessWidget { - const SelectCarIconScreen({super.key}); - - @override - Widget build(BuildContext context) { - return - } -} \ No newline at end of file diff --git a/lib/bups/main_20221031.dart b/lib/bups/main_20221031.dart deleted file mode 100644 index 94b60c2..0000000 --- a/lib/bups/main_20221031.dart +++ /dev/null @@ -1,625 +0,0 @@ -import 'dart:collection'; -import 'package:dash/utils/dbhelper_sqflite.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -// import 'package:path/path.dart'; -// import 'package:sqflite/sqflite.dart'; -import 'package:dash/models/car.dart'; -// import 'package:flutter/src/widgets/form.dart'; - -void main() async { - runApp( - ChangeNotifierProvider( - create: (context) => GarageModel(), - child: const MaterialApp( - title: 'Dashboard', - home: MyApp(), - /* initialRoute: '/', - routes: { - '/': (context) => const MyApp(), - '/about': (context) => const AboutScreen(), - '/newcar': (context) => const NewCarScreen(), - '/detail': (context) => const CarDetailScreen(), - '/edit': (context) => EditCarScreen(), - }, */ - ), - ), - ); - // WidgetsFlutterBinding.ensureInitialized(); // is this necessary? -} - -class GarageModel extends ChangeNotifier { - /// internal state of garage - late List _cars = []; - final DbHelperSqlite _dbHelper = DbHelperSqlite.instance; - UnmodifiableListView get cars => UnmodifiableListView(_cars); - - void add(Car car) { - _cars.add(car); - _dbHelper.insertCar(car); - // getCars(); // prob nec to update `id` - notifyListeners(); - } - - void update(Car car, int carIndex) { - _cars[carIndex] = car; // replace list car with new values - _dbHelper.updateCar(car); - print('car.id is ${car.id}'); - getCars(); - notifyListeners(); - } - - void delete(Car car) { - _cars.removeWhere((_car) => _car.id == car.id); - _dbHelper.deleteCar(car); - print('${car} deleted'); - notifyListeners(); - } - - void clearCarList() { - // clear _cars; doesn't affect database - // useful for clearing cars that didn't make it into the db - _cars.clear(); - notifyListeners(); - } - - void deleteAllCars() { - // delete rows from CarTable - _cars.clear(); - _dbHelper.deleteAll(); - notifyListeners(); - } - - Future getCars() async { - // _cars.clear(); - print('getcars'); - _cars = await _dbHelper.fetchCars(); - print(_cars); - // notifyListeners(); //for some reason this inits db infinitely - } -} - -class Garage extends StatefulWidget { - const Garage({super.key}); - @override - State createState() => _Garage(); -} - -class _Garage extends State { - late Future _cars; - - @override - void initState() { - _cars = Provider.of(context, listen: false).getCars(); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _cars, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator(), - ); - } else { - if (snapshot.error != null) { - return const Center( - child: Text('An error occurred'), - ); - } else { - return Expanded( - child: Consumer(builder: (context, garage, child) { - return ListView.separated( - // padding: const EdgeInsets.all(8), - itemCount: garage._cars.length, - itemBuilder: (context, index) => ListTile( - leading: const Icon(Icons.directions_car), - title: Text(garage.cars[index].nickname ?? "nick_ph"), - subtitle: Text(garage.cars[index].vin ?? "vin_ph"), - // onTap: () => Navigator.pushNamed(context, '/detail'), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EditCarScreen( - car: garage._cars[index], carIndex: index)), - ); - }), - separatorBuilder: (BuildContext context, int index) => - const Divider(), - ); - })); - } - } - }, - ); - } -} - -class _CarList extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, garage, child) { - return ListView.separated( - // padding: const EdgeInsets.all(8), - itemCount: garage._cars.length, - itemBuilder: (context, index) => ListTile( - leading: const Icon(Icons.directions_car), - title: Text(garage.cars[index].nickname ?? "nick_ph"), - subtitle: Text(garage.cars[index].vin ?? "vin_ph"), - // onTap: () => Navigator.pushNamed(context, '/detail'), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EditCarScreen( - car: garage._cars[index], carIndex: index)), - ); - }), - separatorBuilder: (BuildContext context, int index) => - const Divider(), - ); - }, - ); - } -} - -class MyDrawer extends StatelessWidget { - const MyDrawer({super.key}); - - @override - Widget build(BuildContext context) { - return Drawer( - child: ListView( - padding: EdgeInsets.zero, - children: [ - const SizedBox( - height: 80.0, - child: DrawerHeader( - decoration: BoxDecoration( - color: Color.fromARGB(255, 185, 47, 5), - ), - child: Text( - 'Options', - style: TextStyle( - color: Colors.white, - fontSize: 18, - ), - ), - ), - ), - ListTile( - leading: const Icon(Icons.settings), - title: const Text('Settings'), - onTap: () { - print('settings'); - }, - // do something - ), - ListTile( - leading: const Icon(Icons.sync), - title: const Text('Setup Database'), - onTap: () { - print('dbsetup'); - }, - ), - ListTile( - leading: const Icon(Icons.info_outline), - title: const Text('About'), - onTap: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => const AboutScreen())); - }, - ), - ], - ), - ); - } -} - -class MyApp extends StatefulWidget { - const MyApp({super.key}); - - @override - State createState() => _MyAppState(); -} - -class _MyAppState extends State { - @override - Widget build(BuildContext context) { - // context.watch().getCars(); // testing - var garage = context.watch(); // testing - return Scaffold( - appBar: AppBar( - backgroundColor: const Color.fromARGB(255, 185, 47, 5), - title: const Text('Dashboard'), - ), - drawer: const MyDrawer(), - body: Container( - padding: const EdgeInsets.all(32), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: (() => garage.clearCarList()), - // onPressed: (() => VoidCallback), - onLongPress: () { - garage.deleteAllCars(); - // VoidCallback; - }, - child: Row( - children: const [ - Icon(Icons.delete), - Text('Remove All Cars'), - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: (() => garage.getCars()), - // onPressed: (() => VoidCallback), - child: Row( - children: const [ - Icon(Icons.sync), - Text('Refresh garage'), - ], - ), - ), - ), - const Garage(), -/* FutureBuilder( - future: - Provider.of(context, listen: false).getCars(), - builder: (context, dataSnapshot) { - if (dataSnapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator(), - ); - } else { - if (dataSnapshot.error != null) { - return const Center( - child: Text('An error occurred'), - ); - } else { - return Expanded( - child: _CarList(), - ); - } - } - }, - ), */ - ], - ), - ), - floatingActionButton: FloatingActionButton( - child: const Icon(Icons.add), - onPressed: () { - Navigator.push(context, - MaterialPageRoute(builder: (context) => const NewCarScreen())); - }, - ), - ); - } -} - -class AboutScreen extends StatelessWidget { - // change to AboutDialog - const AboutScreen( - {super.key}); //https://api.flutter.dev/flutter/material/AboutDialog-class.html - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('About Page'), - backgroundColor: Colors.indigo, - ), - body: const Center( - child: Text('About Page Text, Lorum Ipsum and whatnot.'), - )); - } -} - -class NewCarScreen extends StatefulWidget { - const NewCarScreen({super.key}); - - @override - State createState() => _NewCarScreenState(); -} - -class _NewCarScreenState extends State { - final GlobalKey _formKey = GlobalKey(); - Car _car = Car(); //initialization is required - -/* DbHelperSqlite _dbHelper = DbHelperSqlite.instance; - @override - void initState() { - super.initState(); - setState(() { - _dbHelper = DbHelperSqlite.instance; - }); - } */ - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Add New Car'), - backgroundColor: Colors.indigo, - ), - body: Container( - padding: const EdgeInsets.all(32), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - //new car form text fields - TextFormField( - decoration: const InputDecoration( - labelText: 'Nickname', - ), - onSaved: (val) => setState(() => _car.nickname = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a nickname'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'VIN', - ), - onSaved: (val) => setState(() => _car.vin = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a unique VIN'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Mileage', - ), - onSaved: (val) => - setState(() => _car.mileage = int.parse(val ?? "")), - // inputFormatters: [FilteringTextInputFormatter.digitsOnly] //try this? - keyboardType: TextInputType.number, - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter car\'s current mileage'; - } - return null; - }, - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: ElevatedButton( - // onPressed: () => _onSubmit(context), - onPressed: () { - final _form = _formKey.currentState!; - if (_form.validate()) { - //validate form - _form - .save(); //save values (reqd before putting them anywhere) - setState(() { - //modify state of car (necessary?) - _car = Car( - vin: _car.vin, - nickname: _car.nickname, - plate: _car.plate, - mileage: _car.mileage); - }); - var garage = - context.read(); //implement Provider - garage.add(_car); - _form.reset(); - Navigator.pushNamed(context, '/'); - } - }, - child: Row( - children: const [ - Icon(Icons.add), - Text('Add Car'), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } - - _onSubmit(context) async { - final _form = _formKey.currentState!; - if (_form.validate()) { - //validate form - _form.save(); //save values (reqd before putting them anywhere) - setState(() { - //modify state of car (necessary?) - _car = Car( - vin: _car.vin, - nickname: _car.nickname, - plate: _car.plate, - mileage: _car.mileage); - }); - var garage = context.read(); //implement Provider - garage.add(_car); - _form.reset(); - Navigator.pushNamed(context, '/'); - } - } -} - -class CarDetailScreen extends StatelessWidget { - const CarDetailScreen({super.key}); - - //garage? load car using Provider? - // then feed to update screen - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: const Color.fromARGB(255, 185, 47, 5), - title: const Text('Vehicle Name'), - ), - drawer: const MyDrawer(), - body: Container( - padding: const EdgeInsets.all(32), - child: const Text('Details here'), - ), - floatingActionButton: FloatingActionButton( - child: const Icon(Icons.edit), - onPressed: () { - Navigator.pushNamed(context, '/edit'); - }, - ), - ); - } -} - -class EditCarScreen extends StatefulWidget { - EditCarScreen({super.key, required this.car, required this.carIndex}); - Car car; - int carIndex; - - @override - State createState() => _EditCarScreenState(); -} - -class _EditCarScreenState extends State { - final GlobalKey _formKey = GlobalKey(); - late Car car = widget.car; //initialization is required - late int carIndex = widget.carIndex; - - @override - Widget build(BuildContext context) { - var garage = context.read(); - return Scaffold( - appBar: AppBar( - title: Text("Edit ${car.nickname}"), - backgroundColor: const Color.fromARGB(255, 185, 47, 5), - ), - body: Container( - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - //new car form text fields - TextFormField( - initialValue: car.nickname, - decoration: const InputDecoration( - labelText: 'Nickname', - ), - onSaved: (val) => setState(() => car.nickname = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a nickname'; - } - return null; - }, - ), - TextFormField( - initialValue: car.vin, - decoration: const InputDecoration( - labelText: 'VIN', - ), - onSaved: (val) => setState(() => car.vin = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a unique VIN'; - } - return null; - }, - ), - TextFormField( - initialValue: car.mileage.toString(), - decoration: const InputDecoration( - labelText: 'Mileage', - ), - onSaved: (val) => - setState(() => car.mileage = int.parse(val ?? "")), - // inputFormatters: [FilteringTextInputFormatter.digitsOnly] //try this? - keyboardType: TextInputType.number, - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter car\'s current mileage'; - } - return null; - }, - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: () { - final updateForm = _formKey.currentState!; - if (updateForm.validate()) { - //validate form - updateForm - .save(); //save values (reqd before putting them anywhere) - setState(() { - //modify state of car (necessary?) - car = Car( - vin: car.vin, - nickname: car.nickname, - plate: car.plate, - mileage: car.mileage); - }); //implement Provider - garage.update(car, carIndex); - updateForm.reset(); - Navigator.pushNamed(context, '/'); - } - }, - child: Row( - children: const [ - //expand these children, too tight - Icon(Icons.save), - Text('Save Changes'), - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: (() => VoidCallback), - onLongPress: () { - garage.delete(car); - Navigator.pushNamed(context, '/'); - }, - child: Row(children: const [ - Icon(Icons.delete), - Text('Delete'), - ]), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: (() => VoidCallback), - child: Row(children: const [ - Icon(Icons.sync_alt), - Text('enable/disable'), - ]), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/models/car.dart b/lib/models/car.dart index 76c4517..cafcf09 100644 --- a/lib/models/car.dart +++ b/lib/models/car.dart @@ -15,7 +15,9 @@ class Car { nickname = map[colNickname]; plate = map[colPlate]; mileage = map[colMileage]; - icon = map[colIcon]; + icon = map.containsKey(colIcon) + ? map[colIcon] ?? 'images/car.png' + : 'images/car.png'; } int? id; diff --git a/lib/utils/dbhelper_sqflite.dart b/lib/utils/dbhelper_sqflite.dart index c3cddca..839bcf2 100644 --- a/lib/utils/dbhelper_sqflite.dart +++ b/lib/utils/dbhelper_sqflite.dart @@ -39,7 +39,7 @@ class DbHelperSqlite { //docs say to avoid `autoincrement` kw await db.execute("PRAGMA foreign_keys=ON;"); await db.execute(''' - CREATE TABLE ${Car.tblCars} ( + CREATE TABLE IF NOT EXISTS ${Car.tblCars} ( ${Car.colId} INTEGER PRIMARY KEY, ${Car.colVin} TEXT UNIQUE, ${Car.colNickname} TEXT, @@ -48,7 +48,7 @@ class DbHelperSqlite { ${Car.colIcon} TEXT); '''); await db.execute(''' - CREATE TABLE ${Txn.tblTxns} ( + CREATE TABLE IF NOT EXISTS ${Txn.tblTxns} ( ${Txn.colId} INTEGER PRIMARY KEY, ${Txn.colType} TEXT, ${Txn.colDatetime} INTEGER, @@ -60,9 +60,20 @@ class DbHelperSqlite { '''); } + Future> _carMapForDatabase(Database db, Car car) async { + final map = car.toMap(); + final columns = await db.rawQuery("PRAGMA table_info(${Car.tblCars})"); + final hasIconColumn = + columns.any((column) => column['name'] == Car.colIcon); + if (!hasIconColumn) { + map.remove(Car.colIcon); + } + return map; + } + Future insertCar(Car car) async { Database db = await instance.database as Database; - return await db.insert(Car.tblCars, car.toMap(), + return await db.insert(Car.tblCars, await _carMapForDatabase(db, car), conflictAlgorithm: ConflictAlgorithm.abort); } @@ -79,7 +90,7 @@ class DbHelperSqlite { Future updateCar(Car car) async { Database db = await instance.database as Database; print('car id to update: ${car.id}'); - return await db.update(Car.tblCars, car.toMap(), + return await db.update(Car.tblCars, await _carMapForDatabase(db, car), where: '${Car.colId}=?', whereArgs: [car.id]); } diff --git a/scripts/seed_dummy_data.py b/scripts/seed_dummy_data.py new file mode 100644 index 0000000..a954164 --- /dev/null +++ b/scripts/seed_dummy_data.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +"""Seed the Dash SQLite database with demo garage data. + +Usage: + python scripts/seed_dummy_data.py "C:\\path\\to\\cars_sqlite.db" + python scripts/seed_dummy_data.py --reset "C:\\path\\to\\cars_sqlite.db" + +If no database path is provided, the script writes to ./cars_sqlite.db. +Use the "sqlite db path:" line printed by the app for the active Windows DB. +""" + +from __future__ import annotations + +import argparse +import sqlite3 +from datetime import datetime, timedelta +from pathlib import Path + + +CARS = [ + { + "vin": "1HGBH41JXMN109186", + "nickname": "Civic Commuter", + "plate": "DASH-101", + "mileage": 84250, + "icon": "images/car_icons/honda.png", + }, + { + "vin": "5YJ3E1EA7KF317000", + "nickname": "Model 3", + "plate": "EV-2026", + "mileage": 36640, + "icon": "images/car_icons/tesla.png", + }, + { + "vin": "1FTFW1E50NFA00042", + "nickname": "Weekend Truck", + "plate": "HAUL-42", + "mileage": 58910, + "icon": "images/car_icons/ford.png", + }, + { + "vin": "WBA8E9G52JNU12345", + "nickname": "Blue Sedan", + "plate": "BLUE-5", + "mileage": 72115, + "icon": "images/car_icons/bmw.png", + }, +] + +TXN_TEMPLATES = [ + ("Fuel", 46.82, 120, "Filled tank"), + ("Oil Change", 78.40, 420, "Synthetic oil and filter"), + ("Tire Rotation", 34.99, 960, "Rotated tires"), + ("Repair", 312.65, 1410, "Replaced worn brake pads"), + ("Registration", 128.00, 1850, "Annual registration renewal"), + ("Wash", 18.00, 2020, "Exterior wash"), + ("Insurance", 146.25, 2350, "Monthly premium"), +] + + +def create_schema(conn: sqlite3.Connection) -> None: + conn.executescript( + """ + PRAGMA foreign_keys=ON; + + CREATE TABLE IF NOT EXISTS cars ( + id INTEGER PRIMARY KEY, + vin TEXT UNIQUE, + nickname TEXT, + mileage INTEGER, + plate TEXT, + icon TEXT + ); + + CREATE TABLE IF NOT EXISTS txns ( + id INTEGER PRIMARY KEY, + txntype TEXT, + datetime INTEGER, + cost REAL, + mileage INTEGER, + note TEXT, + carid INTEGER, + FOREIGN KEY(carid) REFERENCES cars(id) + ); + + PRAGMA user_version=1; + """ + ) + + +def table_columns(conn: sqlite3.Connection, table_name: str) -> set[str]: + return {row[1] for row in conn.execute(f"PRAGMA table_info({table_name})")} + + +def reset_data(conn: sqlite3.Connection) -> None: + conn.execute("DELETE FROM txns") + conn.execute("DELETE FROM cars") + + +def seed(conn: sqlite3.Connection) -> tuple[int, int]: + car_count = 0 + txn_count = 0 + today = datetime.now().replace(hour=9, minute=0, second=0, microsecond=0) + car_columns = table_columns(conn, "cars") + has_icon_column = "icon" in car_columns + + for car_index, car in enumerate(CARS): + if has_icon_column: + cursor = conn.execute( + """ + INSERT OR IGNORE INTO cars (vin, nickname, mileage, plate, icon) + VALUES (?, ?, ?, ?, ?) + """, + ( + car["vin"], + car["nickname"], + car["mileage"], + car["plate"], + car["icon"], + ), + ) + else: + cursor = conn.execute( + """ + INSERT OR IGNORE INTO cars (vin, nickname, mileage, plate) + VALUES (?, ?, ?, ?) + """, + ( + car["vin"], + car["nickname"], + car["mileage"], + car["plate"], + ), + ) + if cursor.rowcount: + car_count += 1 + + car_id = conn.execute( + "SELECT id FROM cars WHERE vin = ?", + (car["vin"],), + ).fetchone()[0] + + existing_txns = conn.execute( + "SELECT COUNT(*) FROM txns WHERE carid = ?", + (car_id,), + ).fetchone()[0] + if existing_txns: + continue + + base_mileage = int(car["mileage"]) - 3000 + for txn_index, (txntype, cost, miles_after, note) in enumerate(TXN_TEMPLATES): + when = today - timedelta(days=(txn_index * 24) + (car_index * 5)) + conn.execute( + """ + INSERT INTO txns (txntype, datetime, cost, mileage, note, carid) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + txntype, + int(when.timestamp()), + cost + (car_index * 7.5), + base_mileage + miles_after, + note, + car_id, + ), + ) + txn_count += 1 + + return car_count, txn_count + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Seed cars_sqlite.db with demo data for Dash screenshots." + ) + parser.add_argument( + "db_path", + nargs="?", + default="cars_sqlite.db", + help="Path to cars_sqlite.db. Defaults to ./cars_sqlite.db.", + ) + parser.add_argument( + "--reset", + action="store_true", + help="Delete existing cars and transactions before inserting demo data.", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + db_path = Path(args.db_path).expanduser().resolve() + db_path.parent.mkdir(parents=True, exist_ok=True) + + with sqlite3.connect(db_path) as conn: + create_schema(conn) + if args.reset: + reset_data(conn) + car_count, txn_count = seed(conn) + conn.commit() + + print(f"Seeded {db_path}") + print(f"Inserted {car_count} cars and {txn_count} transactions") + + +if __name__ == "__main__": + main()