bups and readme cleanup

This commit is contained in:
2026-05-12 10:12:50 -04:00
parent bd771100b0
commit dc1265190a
14 changed files with 322 additions and 3400 deletions

106
README.md
View File

@@ -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) - Add and manage multiple vehicles with brand icons, VIN, plate, and mileage
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - 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 ## Project Structure
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference. ```
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.

Binary file not shown.

View File

@@ -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<Database> 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<int> insert(Map<String, dynamic> 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<List<Map<String, dynamic>>> 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<int> 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<int> update(Map<String, dynamic> 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<int> delete(int id) async {
Database db = await instance.database;
return await db.delete(table, where: '$columnId = ?', whereArgs: [id]);
}
}

View File

@@ -1,5 +0,0 @@
void main() {
runApp(const FormApp());
}

View File

@@ -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',
);
}

View File

@@ -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<IconPicker> createState() => _IconPickerState();
}
class _IconPickerState extends State<IconPicker> {
late Future mfst;
late final Future<String> manifestJson;
late final Future<List<String>> icons;
late List<String> iconList;
late List<String> iconNames;
late Map iconMap;
late List<String> searchableList = [];
late List<String> items;
TextEditingController editingController = TextEditingController();
@override
void initState() {
super.initState();
icons = loadAssets();
// iconNames = buildIconList(icons);
}
Future<List<String>> 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<String> buildIconList(List icons) {
List<String> iN = [];
for (var i in icons) {
iN.add(i.substring(17, i.length - 4));
}
return iN;
}
void filterSearch(String query, List<String> 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'),
),
);
} */
}

View File

@@ -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<void> insertCar(Car car) async {
final db = await database;
await db.insert(
'cars',
car.toMap(),
conflictAlgorithm: ConflictAlgorithm.abort,
);
}
Future<List<Car>> getCars() async {
final db = await database;
final List<Map<String, dynamic>> 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<void> updateCar(Car car) async {
final db = await database;
await db.update(
'cars',
car.toMap(),
where: 'vin=?',
whereArgs: [car.vin],
);
}
Future<void> 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<Car> _cars = [];
UnmodifiableListView<Car> 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<String, dynamic> 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<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _formKey = GlobalKey<FormState>();
/* _list() => Expanded(
child: Card(
margin: EdgeInsets.fromLTRB(20,30,20,0),
child: ListView.builder(
padding: EdgeInsets.all(8),
itemBuilder: (context, index){
return Column(
children: <Widget>[
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: <Widget>[
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<NewCarScreen> createState() => _NewCarScreenState();
}
class _NewCarScreenState extends State<NewCarScreen> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
Care _care = Care();
List<Care> _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: <Widget>[
//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: <TextInputFormatter>[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 <Widget>[
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<void> _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
}
} */

View File

@@ -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<Car> _cars = [];
late List<Txn> _txns = []; // hold current car txns, repl on new load
final DbHelperSqlite _dbHelper = DbHelperSqlite.instance;
UnmodifiableListView<Car> get cars => UnmodifiableListView(_cars);
UnmodifiableListView<Txn> 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<void> 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<void> getTxns(int carid) async {
_txns = await _dbHelper.fetchTxns(carid);
print('get txns: $_txns');
}
}
class Garage extends StatefulWidget {
const Garage({super.key});
@override
State<Garage> createState() => _Garage();
}
class _Garage extends State<Garage> {
late Future _cars;
@override
void initState() {
_cars = Provider.of<GarageModel>(context, listen: false).getCars();
// _cars = Provider.of<GarageModel>(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<GarageModel>(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<CurrentCar> createState() => _CurrentCar();
}
class _CurrentCar extends State<CurrentCar> {
late Future _txns;
late Car car = widget.car;
@override
void initState() {
_txns = Provider.of<GarageModel>(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<GarageModel>(
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: <Widget>[
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<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
var garage = context.watch<GarageModel>(); // 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 <Widget>[
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 <Widget>[
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<NewCarScreen> createState() => _NewCarScreenState();
}
class _NewCarScreenState extends State<NewCarScreen> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
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: <Widget>[
//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: <TextInputFormatter>[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<GarageModel>(); //implement Provider
garage.add(_car);
_form.reset();
Navigator.pushNamed(context, '/');
}
},
child: Row(
children: const <Widget>[
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<GarageModel>(); //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<GarageModel>()._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<EditCarScreen> createState() => _EditCarScreenState();
}
class _EditCarScreenState extends State<EditCarScreen> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
late Car car = widget.car; //initialization is required
late int carIndex = widget.carIndex;
@override
Widget build(BuildContext context) {
var garage = context.read<GarageModel>();
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: <Widget>[
//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: <TextInputFormatter>[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 <Widget>[
//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 <Widget>[
Icon(Icons.delete),
Text('Delete'),
]),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: ElevatedButton(
onPressed: (() => VoidCallback),
child: Row(children: const <Widget>[
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<NewTxn> createState() => _NewTxnState();
}
class _NewTxnState extends State<NewTxn> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
late Car car = widget.car;
late int carIndex = widget.carIndex;
DateTime datetime = DateTime.now();
Txn txn = Txn();
Future<void> _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: <Widget>[
// 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<GarageModel>();
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 <Widget>[
//expand these children, too tight
Icon(Icons.save),
Text('Save Changes'),
],
),
),
),
],
),
),
),
);
}
}
/* class MyDatePicker extends StatefulWidget {
const MyDatePicker({super.key});
@override
State<MyDatePicker> createState() => _MyDatePicker();
}
class _MyDatePicker extends State<MyDatePicker> {
@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"),
);
}
} */

View File

@@ -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<Car> _cars = [];
late List<Txn> _txns = []; // hold current car txns, repl on new load
final DbHelperSqlite _dbHelper = DbHelperSqlite.instance;
UnmodifiableListView<Car> get cars => UnmodifiableListView(_cars);
UnmodifiableListView<Txn> 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<void> 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<void> getTxns(int carid) async {
_txns = await _dbHelper.fetchTxns(carid);
print('get txns: $_txns');
}
}
class Garage extends StatefulWidget {
const Garage({super.key});
@override
State<Garage> createState() => _Garage();
}
class _Garage extends State<Garage> {
late Future _cars;
@override
void initState() {
_cars = Provider.of<GarageModel>(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<GarageModel>(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<CurrentCar> createState() => _CurrentCar();
}
class _CurrentCar extends State<CurrentCar> {
late Future _txns;
late Car car = widget.car;
@override
void initState() {
_txns = Provider.of<GarageModel>(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<GarageModel>(
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: <Widget>[
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<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
var garage = context.watch<GarageModel>(); // 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 <Widget>[
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 <Widget>[
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<NewCarScreen> createState() => _NewCarScreenState();
}
class _NewCarScreenState extends State<NewCarScreen> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
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: <Widget>[
//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: <TextInputFormatter>[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<GarageModel>();
garage.add(_car);
_form.reset();
Navigator.pushNamed(context, '/');
}
},
child: Row(
children: const <Widget>[
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<GarageModel>(); //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<CarDetailScreen> createState() => _CarDetailScreenState();
}
class _CarDetailScreenState extends State<CarDetailScreen> {
late Car car = context.watch<GarageModel>()._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<EditCarScreen> createState() => _EditCarScreenState();
}
class _EditCarScreenState extends State<EditCarScreen> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
late Car car = widget.car;
late int carIndex = widget.carIndex;
@override
Widget build(BuildContext context) {
var garage = context.read<GarageModel>();
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: <Widget>[
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: <TextInputFormatter>[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 <Widget>[
//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 <Widget>[
Icon(Icons.delete),
Text('Delete'),
]),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: ElevatedButton(
onPressed: (() => VoidCallback),
child: Row(children: const <Widget>[
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<NewTxn> createState() => _NewTxnState();
}
class _NewTxnState extends State<NewTxn> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
late Car car = widget.car;
late int carIndex = widget.carIndex;
DateTime datetime = DateTime.now();
Txn txn = Txn();
bool refresh = false;
Future<void> _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: <Widget>[
// 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<GarageModel>();
garage.update(car, carIndex);
print(txn.toMap());
garage.insertTxn(txn);
form.reset();
Navigator.pop(context);
}
},
child: Row(
children: const <Widget>[
//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"),
);
}
} */

View File

@@ -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<String, dynamic> 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<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@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: <Widget>[
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<NewCarScreen> createState() => _NewCarScreenState();
}
class _NewCarScreenState extends State<NewCarScreen> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
@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: <Widget>[
// 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 <Widget>[
Icon(Icons.add),
Text('Add Car'),
],
),
),
),
],
),
),
),
);
}
}
class SelectCarIconScreen extends StatelessWidget {
const SelectCarIconScreen({super.key});
@override
Widget build(BuildContext context) {
return
}
}

View File

@@ -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<Car> _cars = [];
final DbHelperSqlite _dbHelper = DbHelperSqlite.instance;
UnmodifiableListView<Car> 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<void> 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<Garage> createState() => _Garage();
}
class _Garage extends State<Garage> {
late Future _cars;
@override
void initState() {
_cars = Provider.of<GarageModel>(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<GarageModel>(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<GarageModel>(
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: <Widget>[
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<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
// context.watch<GarageModel>().getCars(); // testing
var garage = context.watch<GarageModel>(); // 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 <Widget>[
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 <Widget>[
Icon(Icons.sync),
Text('Refresh garage'),
],
),
),
),
const Garage(),
/* FutureBuilder(
future:
Provider.of<GarageModel>(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<NewCarScreen> createState() => _NewCarScreenState();
}
class _NewCarScreenState extends State<NewCarScreen> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
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: <Widget>[
//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: <TextInputFormatter>[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<GarageModel>(); //implement Provider
garage.add(_car);
_form.reset();
Navigator.pushNamed(context, '/');
}
},
child: Row(
children: const <Widget>[
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<GarageModel>(); //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<EditCarScreen> createState() => _EditCarScreenState();
}
class _EditCarScreenState extends State<EditCarScreen> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
late Car car = widget.car; //initialization is required
late int carIndex = widget.carIndex;
@override
Widget build(BuildContext context) {
var garage = context.read<GarageModel>();
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: <Widget>[
//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: <TextInputFormatter>[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 <Widget>[
//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 <Widget>[
Icon(Icons.delete),
Text('Delete'),
]),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: ElevatedButton(
onPressed: (() => VoidCallback),
child: Row(children: const <Widget>[
Icon(Icons.sync_alt),
Text('enable/disable'),
]),
),
),
],
),
),
),
);
}
}

View File

@@ -15,7 +15,9 @@ class Car {
nickname = map[colNickname]; nickname = map[colNickname];
plate = map[colPlate]; plate = map[colPlate];
mileage = map[colMileage]; mileage = map[colMileage];
icon = map[colIcon]; icon = map.containsKey(colIcon)
? map[colIcon] ?? 'images/car.png'
: 'images/car.png';
} }
int? id; int? id;

View File

@@ -39,7 +39,7 @@ class DbHelperSqlite {
//docs say to avoid `autoincrement` kw //docs say to avoid `autoincrement` kw
await db.execute("PRAGMA foreign_keys=ON;"); await db.execute("PRAGMA foreign_keys=ON;");
await db.execute(''' await db.execute('''
CREATE TABLE ${Car.tblCars} ( CREATE TABLE IF NOT EXISTS ${Car.tblCars} (
${Car.colId} INTEGER PRIMARY KEY, ${Car.colId} INTEGER PRIMARY KEY,
${Car.colVin} TEXT UNIQUE, ${Car.colVin} TEXT UNIQUE,
${Car.colNickname} TEXT, ${Car.colNickname} TEXT,
@@ -48,7 +48,7 @@ class DbHelperSqlite {
${Car.colIcon} TEXT); ${Car.colIcon} TEXT);
'''); ''');
await db.execute(''' await db.execute('''
CREATE TABLE ${Txn.tblTxns} ( CREATE TABLE IF NOT EXISTS ${Txn.tblTxns} (
${Txn.colId} INTEGER PRIMARY KEY, ${Txn.colId} INTEGER PRIMARY KEY,
${Txn.colType} TEXT, ${Txn.colType} TEXT,
${Txn.colDatetime} INTEGER, ${Txn.colDatetime} INTEGER,
@@ -60,9 +60,20 @@ class DbHelperSqlite {
'''); ''');
} }
Future<Map<String, dynamic>> _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<int> insertCar(Car car) async { Future<int> insertCar(Car car) async {
Database db = await instance.database as Database; 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); conflictAlgorithm: ConflictAlgorithm.abort);
} }
@@ -79,7 +90,7 @@ class DbHelperSqlite {
Future<int> updateCar(Car car) async { Future<int> updateCar(Car car) async {
Database db = await instance.database as Database; Database db = await instance.database as Database;
print('car id to update: ${car.id}'); 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]); where: '${Car.colId}=?', whereArgs: [car.id]);
} }

208
scripts/seed_dummy_data.py Normal file
View File

@@ -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()