Compare commits

...

6 Commits

Author SHA1 Message Date
e9e885bdd8 removed commented code 2026-05-12 10:40:00 -04:00
dc1265190a bups and readme cleanup 2026-05-12 10:12:50 -04:00
bd771100b0 added snapshots 2026-05-12 09:42:28 -04:00
53af503581 udpated gitignore 2026-05-12 09:14:38 -04:00
a6f3fe4690 init icon list empty instead of late 2026-05-12 08:31:13 -04:00
c01894c562 added db init and print statement. app runs, no icons 2026-05-12 08:23:57 -04:00
29 changed files with 660 additions and 3545 deletions

12
.gitignore vendored
View File

@@ -42,3 +42,15 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
/archive
/lib/bups
# --- emacs ---
*~
\#*\#
.\#*
*.elc
.DS_Store
*~$*
*.db

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)
- [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.

View File

@@ -1,3 +1,5 @@
//file:noinspection Annotator
//file:noinspection Annotator
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

3
devtools_options.yaml Normal file
View File

@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

BIN
docs/car-icons.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
docs/dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
docs/edit-car.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
docs/event-gas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

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

@@ -1,17 +1,20 @@
import 'dart:collection';
import 'dart:io';
import 'package:dash/utils/garage_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:path/path.dart';
// import 'package:sqflite/sqflite.dart';
import 'package:dash/utils/dbhelper_sqflite.dart';
import 'package:dash/models/car.dart';
import 'package:dash/models/txn.dart';
import 'package:dash/screens/screens.dart';
import 'package:dash/theme.dart';
// import 'package:flutter/src/widgets/form.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
}
runApp(
ChangeNotifierProvider(
create: (context) => GarageModel(),
@@ -30,7 +33,6 @@ void main() async {
),
),
);
// WidgetsFlutterBinding.ensureInitialized(); // is this necessary?
}
class MyDrawer extends StatelessWidget {
@@ -94,6 +96,14 @@ class MyDrawer extends StatelessWidget {
MaterialPageRoute(builder: (context) => const AboutScreen()));
},
),
ListTile(
leading: const Icon(Icons.car_crash),
title: const Text('Test'),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const AboutScreen()));
},
),
],
),
);

View File

@@ -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;

View File

@@ -1,6 +1,5 @@
import 'dart:convert';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class IconPicker extends StatefulWidget {
IconPicker({super.key});
@@ -12,12 +11,8 @@ class IconPicker extends StatefulWidget {
}
class _IconPickerState extends State<IconPicker> {
late Future<String> manifestJson;
late List<String> icons;
late List<String> iconList;
late List<String> items;
TextEditingController editingController = TextEditingController();
List<String> icons = [];
List<String> items = [];
@override
void initState() {
@@ -26,14 +21,19 @@ class _IconPickerState extends State<IconPicker> {
}
Future<List<String>> loadAssets() async {
final manifestJson =
await DefaultAssetBundle.of(context).loadString('AssetManifest.json');
// ignore: no_leading_underscores_for_local_identifiers
final _icons = await json
.decode(manifestJson)
.keys
List<String> _icons = [];
try {
final assetManifest = await AssetManifest.loadFromAssetBundle(rootBundle);
_icons = assetManifest
.listAssets()
.where((String key) => key.startsWith('images/car_icons/'))
.toList();
} catch (error) {
print('unable to load car icon assets: $error');
}
if (!mounted) {
return _icons;
}
setState(() {
icons = _icons;
items = _icons;

View File

@@ -31,6 +31,7 @@ class DbHelperSqlite {
print('initializing db');
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String dbPath = join(documentsDirectory.path, _dbName);
print('sqlite db path: $dbPath');
return await openDatabase(dbPath, version: _dbVersion, onCreate: _onCreate);
}
@@ -38,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,
@@ -47,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,
@@ -59,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 {
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);
}
@@ -78,7 +90,7 @@ class DbHelperSqlite {
Future<int> 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]);
}

View File

@@ -5,8 +5,8 @@
import FlutterMacOS
import Foundation
import path_provider_macos
import sqflite
import path_provider_foundation
import sqflite_darwin
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))

View File

@@ -5,72 +5,90 @@ packages:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
source: hosted
version: "2.9.0"
version: "2.11.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.1"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
version: "1.19.1"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.6"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
url: "https://pub.dartlang.org"
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.5"
version: "1.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
url: "https://pub.dartlang.org"
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "2.1.3"
file:
dependency: transitive
description:
name: file
url: "https://pub.dartlang.org"
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "6.1.4"
version: "7.0.1"
flutter:
dependency: "direct main"
description: flutter
@@ -80,257 +98,396 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
url: "https://pub.dartlang.org"
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "3.0.2"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
google_fonts:
dependency: "direct main"
description:
name: google_fonts
url: "https://pub.dartlang.org"
sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "6.3.3"
hooks:
dependency: transitive
description:
name: hooks
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
url: "https://pub.dev"
source: hosted
version: "1.0.3"
http:
dependency: transitive
description:
name: http
url: "https://pub.dartlang.org"
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
url: "https://pub.dev"
source: hosted
version: "0.13.5"
version: "1.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
version: "4.1.1"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
url: "https://pub.dartlang.org"
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "3.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.12"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.1.5"
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.8.0"
version: "1.17.0"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
url: "https://pub.dev"
source: hosted
version: "0.17.6"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
path:
dependency: "direct main"
description:
name: path
url: "https://pub.dartlang.org"
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.8.2"
version: "1.9.1"
path_provider:
dependency: "direct main"
description:
name: path_provider
url: "https://pub.dartlang.org"
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.0.11"
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
url: "https://pub.dartlang.org"
sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
url: "https://pub.dev"
source: hosted
version: "2.0.20"
path_provider_ios:
version: "2.2.15"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_ios
url: "https://pub.dartlang.org"
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.0.11"
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
url: "https://pub.dartlang.org"
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.1.7"
path_provider_macos:
dependency: transitive
description:
name: path_provider_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
url: "https://pub.dartlang.org"
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.0.5"
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
url: "https://pub.dartlang.org"
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
url: "https://pub.dartlang.org"
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
url: "https://pub.dartlang.org"
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
process:
dependency: transitive
description:
name: process
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.4"
version: "2.1.8"
provider:
dependency: "direct main"
description:
name: provider
url: "https://pub.dartlang.org"
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
url: "https://pub.dev"
source: hosted
version: "6.0.4"
version: "6.1.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
version: "1.10.0"
sqflite:
dependency: "direct main"
description:
name: sqflite
url: "https://pub.dartlang.org"
sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb"
url: "https://pub.dev"
source: hosted
version: "2.1.0+1"
version: "2.4.1"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
url: "https://pub.dartlang.org"
sha256: f8a08a13fb8f0f8c590df89d745000bed44a673ed94bac846739e1a016875c21
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "2.5.7"
sqflite_common_ffi:
dependency: "direct main"
description:
name: sqflite_common_ffi
sha256: cd0c7f7de39a08f2d54ef144d9058c46eca8461879aaa648025643455c1e5a20
url: "https://pub.dev"
source: hosted
version: "2.4.0+3"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "96a698e2bc82bd770a4d6aab00b42396a7c63d9e33513a56945cbccb594c2474"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: "56da3e13ed7d28a66f930aa2b2b29db6736a233f08283326e96321dd812030f5"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.3.0"
synchronized:
dependency: transitive
description:
name: synchronized
url: "https://pub.dartlang.org"
sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5"
url: "https://pub.dev"
source: hosted
version: "3.0.0+3"
version: "3.4.0+1"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.4.12"
version: "0.7.10"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.3.1"
version: "1.4.0"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
widgets:
dependency: "direct main"
description:
name: widgets
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.5"
win32:
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
name: vm_service
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "14.3.0"
web:
dependency: transitive
description:
name: web
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
url: "https://pub.dartlang.org"
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "0.2.0+2"
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=2.18.2 <3.0.0"
flutter: ">=3.3.0-0"
dart: ">=3.11.0 <4.0.0"
flutter: ">=3.35.0"

View File

@@ -20,7 +20,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: '>=2.18.2 <3.0.0'
# sdk: '>=2.18.2 <3.0.0'
sdk: '>=3.0.0 <4.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
@@ -32,16 +33,18 @@ dependencies:
flutter:
sdk: flutter
# firebase_database: ^9.1.7
provider: ^6.0.0
sqflite: any
path:
path_provider: ^2.0.11
google_fonts: ^3.0.1
provider: ^6.1.1
sqflite: ^2.3.0
path: ^1.8.3
path_provider: ^2.1.1
# google_fonts: ^6.1.0
google_fonts: 6.3.3
sqflite_common_ffi: ^2.4.0+3
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
widgets: ^1.4.5
# widgets: ^1.4.5
dev_dependencies:
flutter_test:
@@ -52,7 +55,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
flutter_lints: ^3.0.1
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

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

View File

@@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake)
# https://github.com/flutter/flutter/issues/57146.
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
# Set fallback configurations for older versions of the flutter tool.
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
set(FLUTTER_TARGET_PLATFORM "windows-x64")
endif()
# === Flutter Library ===
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
@@ -92,7 +97,7 @@ add_custom_command(
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
windows-x64 $<CONFIG>
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS