diff --git a/README.md b/README.md
index abd4ac9..3ce298f 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,4 @@
-
-WIP Logo
+
# Simple FRC scouting app. Developed by me for Laser Robotics and the FRC community!
@@ -9,12 +8,11 @@ Input your team number and event, and start scouting! When you're done export yo
* Better and updated scouting questions, these are examples from Reefscape
* Match scouting
-* Online accessible version
# Some extra notes
-* This is a side project by a me and is my first Flutter project. Please leave suggestions and tips about what I could do better!
-* I am providing APK's, but it may not be the most up to date version.
+* This is a side project by me and is my first Flutter project. Please leave suggestions and tips about what I could do better!
+* I am providing APK's, but it may not be the most up-to-date version.
* To run on iOS you will still need to compile it and install with Xcode
# How to contribute
@@ -28,4 +26,4 @@ Input your team number and event, and start scouting! When you're done export yo
1. Run the command `flutter run` in your terminal while inside project folder.
2. Open the app in a browser, on your phone, or as a computer app.
-* If you plan to test on an iPhone, you must have a Apple Developer account and a MacOS device with Xcode.
\ No newline at end of file
+* If you plan to test on an iPhone, you must have an Apple Developer account and a macOS device with Xcode.
\ No newline at end of file
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 2391af5..84cb48d 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -28,6 +28,8 @@ android {
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
+ val fileName = "${rootProject.name}_v${versionName}_${versionCode}"
+ setProperty("archivesBaseName", fileName)
}
buildTypes {
diff --git a/lib/eventadder.dart b/lib/eventadder.dart
new file mode 100644
index 0000000..7563e24
--- /dev/null
+++ b/lib/eventadder.dart
@@ -0,0 +1,136 @@
+import 'package:flutter/material.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+class EventAdder extends StatefulWidget {
+ const EventAdder({super.key});
+
+ @override
+ State createState() => _EventAdderState();
+}
+
+class _EventAdderState extends State {
+ final _customEventNameController = TextEditingController();
+ final _customEventCodeController = TextEditingController();
+ List customEventNames = [];
+ List customEventCodes = [];
+ final _formKey = GlobalKey();
+
+ @override
+ void initState() {
+ super.initState();
+ _loadEvents();
+ }
+
+ Future _loadEvents() async {
+ final SharedPreferences prefs = await SharedPreferences.getInstance();
+ setState(() {
+ customEventNames = prefs.getStringList('custom_event_names') ?? [];
+ customEventCodes = prefs.getStringList('custom_event_codes') ?? [];
+ });
+ }
+
+ Future _saveEvent() async {
+ if (!_formKey.currentState!.validate()) {
+ return;
+ }
+
+ final SharedPreferences prefs = await SharedPreferences.getInstance();
+ final String eventName = _customEventNameController.text;
+ String eventCode;
+
+ if (_customEventCodeController.text.isEmpty) {
+ eventCode = '${DateTime.now().year.toString()}${eventName.replaceAll(' ', '').substring(0,4)}';
+ } else {
+ eventCode = _customEventCodeController.text;
+ }
+
+ customEventNames.add(eventName);
+ customEventCodes.add(eventCode);
+
+ await prefs.setStringList('custom_event_names', customEventNames);
+ await prefs.setStringList('custom_event_codes', customEventCodes);
+
+ _customEventNameController.clear();
+ _customEventCodeController.clear();
+ setState(() {});
+
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Added Event: $eventName - $eventCode'),
+ behavior: SnackBarBehavior.floating,
+ margin: const EdgeInsets.all(16.0),
+ ),
+ );
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('Add Event')),
+ body: Form(
+ key: _formKey,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(top: 16.0),
+ child: Text('Add Custom Event', style: Theme.of(context).textTheme.titleLarge)
+ ),
+ Padding(
+ padding: const EdgeInsets.only(top: 16.0),
+ child: ListView(
+ shrinkWrap: true,
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ children: [
+ const SizedBox(height: 16),
+ TextFormField(
+ controller: _customEventNameController,
+ decoration: const InputDecoration(
+ labelText: 'Event Name',
+ hintText: 'gm_construct Regional',
+ border: OutlineInputBorder()
+ ),
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return 'Please enter a event name';
+ }
+ return null;
+ },
+ ),
+ const SizedBox(height: 16),
+ TextFormField(
+ controller: _customEventCodeController,
+ decoration: const InputDecoration(
+ labelText: 'Event Code',
+ hintText: '2026gmct',
+ border: OutlineInputBorder()
+ )
+ ),
+ Text('Leave this field EMPTY unless you NEED a custom code.'),
+ const SizedBox(height: 16.0),
+ ElevatedButton(
+ onPressed: () { _saveEvent(); },
+ child: const Text('Create Event'),
+ ),
+ ],
+ )
+ ),
+ Divider(),
+ Text('Custom Events', style: Theme.of(context).textTheme.titleLarge),
+ ListView.builder(
+ shrinkWrap: true,
+ itemCount: customEventNames.length,
+ itemBuilder: (context, index) {
+ return ListTile(
+ title: Text('${customEventNames[index]} - ${customEventCodes[index]}')
+ );
+ }
+ )
+ ],
+ )
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/eventpicker.dart b/lib/eventpicker.dart
index b60e616..6b4acda 100644
--- a/lib/eventpicker.dart
+++ b/lib/eventpicker.dart
@@ -1,30 +1,107 @@
import 'package:flutter/material.dart';
+import 'package:laserscouter/eventadder.dart';
import 'teampicker.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+
+class EventPicker extends StatefulWidget {
+ const EventPicker({super.key, required this.eventNames, required this.eventCodes});
-class EventPicker extends StatelessWidget {
final List eventNames;
final List eventCodes;
- const EventPicker({super.key, required this.eventNames, required this.eventCodes});
+ @override
+ State createState() => _EventPickerState();
+}
+
+class _EventPickerState extends State {
+ List eventNames = [];
+ List eventCodes = [];
+ List isCustom = [];
+
+ @override
+ void initState() {
+ super.initState();
+ eventNames = List.from(widget.eventNames);
+ eventCodes = List.from(widget.eventCodes);
+ isCustom = List.filled(widget.eventCodes.length, false);
+ _refreshEvents();
+ }
+
+ Future _refreshEvents() async {
+ eventNames = widget.eventNames.toList();
+ eventCodes = widget.eventCodes.toList();
+ isCustom = List.filled(widget.eventCodes.length, false).toList();
+
+ final SharedPreferences prefs = await SharedPreferences.getInstance();
+ final storedNames = prefs.getStringList('custom_event_names') ?? [];
+ final storedCodes = prefs.getStringList('custom_event_codes') ?? [];
+
+ for (int i = 0; i < storedNames.length; i++) {
+ if (!eventCodes.contains(storedCodes[i])) {
+ eventNames.add(storedNames[i]);
+ eventCodes.add(storedCodes[i]);
+ isCustom.add(true);
+ }
+ }
+ if (mounted) {
+ setState(() {});
+ }
+ }
@override
Widget build(BuildContext context) {
return Scaffold(
- appBar: AppBar(title: const Text('Event')),
+ appBar: AppBar(
+ title: const Text('Event'),
+ actions: [
+ IconButton(
+ onPressed: () async {
+ await Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => const EventAdder(),
+ ),
+ );
+ _refreshEvents();
+ },
+ icon: Icon(Icons.add),
+ )
+ ]
+ ),
body: ListView.builder(
itemCount: eventNames.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(eventNames[index]),
onTap: () {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (context) => TeamPicker(
- eventCode: eventCodes[index],
+ bool isEventCustom = isCustom[index];
+ String eventCode = eventCodes[index];
+ String eventName = eventCodes[index];
+
+ if (isEventCustom) {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => TeamPicker(
+ eventCode: eventCode,
+ eventName: eventName,
+ isCustomOnly: isEventCustom,
+ ),
),
- ),
- );
+ );
+ } else {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => TeamPicker(
+ isCustomOnly: isEventCustom,
+ eventName: eventName,
+ eventCode: eventCode,
+ ),
+ ),
+ );
+ }
},
);
},
diff --git a/lib/login.dart b/lib/login.dart
index a31463d..7794b7d 100644
--- a/lib/login.dart
+++ b/lib/login.dart
@@ -67,112 +67,116 @@ class _LoginPageState extends State {
appBar: AppBar(
title: const Text('Login'),
),
- body: Padding(
- padding: const EdgeInsets.all(16.0),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Image.asset('assets/main.png', height: 75, alignment: Alignment.center),
- const SizedBox(height: 16.0),
- TextField(
- controller: _teamNumberController,
- decoration: const InputDecoration(
- labelText: 'Team Number',
- border: OutlineInputBorder(),
- ),
- ),
- const SizedBox(height: 16.0),
- ElevatedButton(
- onPressed: _isLoading
- ? null
- : () async {
- final navigator = Navigator.of(context);
- final scaffoldMessenger = ScaffoldMessenger.of(context);
-
- setState(() {
- _isLoading = true;
- });
-
- try {
- await _saveData();
- String teamNumber = _teamNumberController.text;
- final result = await teamSearch(teamNumber);
- navigator.push(
- MaterialPageRoute(
- builder: (context) => EventPicker(
- eventNames: result.eventNames,
- eventCodes: result.eventCodes,
- ),
- ),
- );
- } catch (e) {
- scaffoldMessenger.showSnackBar(
- SnackBar(content: Text('Error: ${e.toString()}')),
- );
- } finally {
- if (mounted) {
- setState(() {
- _isLoading = false;
- });
- }
- }
- },
- child: _isLoading
- ? const SizedBox(
- height: 20,
- width: 20,
- child: CircularProgressIndicator(
- strokeWidth: 2.0,
- color: Colors.white,
+ body: SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ children: [
+ const Spacer(),
+ Image.asset('assets/main.png', height: 75, alignment: Alignment.center),
+ const SizedBox(height: 16.0),
+ TextField(
+ controller: _teamNumberController,
+ decoration: const InputDecoration(
+ labelText: 'Team Number',
+ border: OutlineInputBorder(),
),
- )
- : const Text('Login'),
- )
- ],
- ),
- ),
- bottomNavigationBar: Padding (
- padding: const EdgeInsets.all(50.0),
- child: Row (
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- TextButton(
- child: const Text('Info'),
- onPressed: () {
- showDialog(
- context: context,
- builder: (BuildContext context) {
- return AlertDialog(
- title: const Text('Info'),
- content: const Text(
- "This app makes use of The Blue Alliance APIv3 through Laser Proxy. No API keys are stored on device. Laser Scouter was created by FRC 2077 Laser Robotics. \n\nVersion: Rebuilt 26.1.23"
+ keyboardType: TextInputType.number,
+ ),
+ const SizedBox(height: 16.0),
+ ElevatedButton(
+ onPressed: _isLoading
+ ? null
+ : () async {
+ final navigator = Navigator.of(context);
+ final scaffoldMessenger = ScaffoldMessenger.of(context);
+
+ setState(() {
+ _isLoading = true;
+ });
+
+ try {
+ await _saveData();
+ String teamNumber = _teamNumberController.text;
+ final result = await teamSearch(teamNumber);
+ navigator.push(
+ MaterialPageRoute(
+ builder: (context) => EventPicker(
+ eventNames: result.eventNames,
+ eventCodes: result.eventCodes,
),
- );
+ ),
+ );
+ } catch (e) {
+ scaffoldMessenger.showSnackBar(
+ SnackBar(
+ content: Text('Error: ${e.toString()}'),
+ behavior: SnackBarBehavior.floating,
+ margin: const EdgeInsets.all(16.0),
+ ),
+ );
+ } finally {
+ if (mounted) {
+ setState(() {
+ _isLoading = false;
+ });
}
- );
- },
- ),
- TextButton(
- child: const Text('Github'),
- onPressed: () {
- _launchUrl(Uri.parse('https://github.com/raktbastr/laserscouter'));
- }
- ),
- TextButton(
- onPressed: _isLoading
- ? null
- : () {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (context) => const SettingsPage(),
+ }
+ },
+ child: _isLoading
+ ? const SizedBox(
+ height: 20,
+ width: 20,
+ child: CircularProgressIndicator(
+ strokeWidth: 2.0,
+ color: Colors.white,
+ ),
+ )
+ : const Text('Login'),
+ ),
+ const Spacer(),
+ Row (mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ TextButton(
+ child: const Text('Info'),
+ onPressed: () {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: const Text('Info'),
+ content: const Text(
+ "This app makes use of The Blue Alliance APIv3 through Laser Proxy. No API keys are stored on device. Laser Scouter was created by FRC 2077 Laser Robotics. \n\nVersion: Rebuilt v26.2.22"
+ ),
+ );
+ }
+ );
+ },
+ ),
+ TextButton(
+ child: const Text('Github'),
+ onPressed: () {
+ _launchUrl(Uri.parse('https://github.com/raktbastr/laserscouter'));
+ }
+ ),
+ TextButton(
+ onPressed: _isLoading
+ ? null
+ : () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => const SettingsPage(),
+ )
+ );
+ },
+ child: const Text('Settings'),
)
- );
- },
- child: const Text('Settings'),
- )
- ]
- )
+ ]
+ ),
+ ],
+ ),
+ ),
)
);
}
diff --git a/lib/notespage.dart b/lib/notespage.dart
index f7de9c3..1582628 100644
--- a/lib/notespage.dart
+++ b/lib/notespage.dart
@@ -20,19 +20,22 @@ class NotesPage extends StatefulWidget {
}
class _NotesPageState extends State {
- late SharedPreferences _prefs;
+ late SharedPreferences prefs;
ScoutingView _selectedView = ScoutingView.match;
bool _isLoading = true;
- final _autonRundownController = TextEditingController();
final _botPositionController = TextEditingController();
final _generalObservationsController = TextEditingController();
-
- final _driveTrainTypeController = TextEditingController();
- bool _hasVision = false;
- double _climbLevel = 100.0;
- bool _trenchable = false;
+ final _autonRundownController = TextEditingController();
+ final _intakePositionController = TextEditingController();
+ final _scoreMechanismController = TextEditingController();
+ double _fuelPerCycle = 0;
+ bool _canDriveUnderTrench = false;
+ bool _canDriveOverBump = false;
+ double _climbLevel = 0.0;
double _fuelCapacity = 0.0;
+ bool _canGiveToHumanPlayer = false;
+ double _cycleTime = 0.0;
@override
void initState() {
@@ -44,8 +47,9 @@ class _NotesPageState extends State {
void dispose() {
_autonRundownController.dispose();
_botPositionController.dispose();
- _driveTrainTypeController.dispose();
_generalObservationsController.dispose();
+ _intakePositionController.dispose();
+ _scoreMechanismController.dispose();
super.dispose();
}
@@ -54,21 +58,33 @@ class _NotesPageState extends State {
}
Future _loadNotes() async {
- _prefs = await SharedPreferences.getInstance();
+ prefs = await SharedPreferences.getInstance();
- _autonRundownController.text = _prefs.getString(_generateKey('autonRundown')) ?? '';
- _botPositionController.text = _prefs.getString(_generateKey('botPosition')) ?? '';
- _generalObservationsController.text = _prefs.getString(_generateKey('generalObservations')) ?? '';
- _driveTrainTypeController.text = _prefs.getString(_generateKey('driveTrainType')) ?? '';
- _hasVision = _prefs.getBool(_generateKey('hasVision')) ?? false;
- _climbLevel = _prefs.getDouble(_generateKey('climbLevel')) ?? 0.0;
- _trenchable = _prefs.getBool(_generateKey('trenchable')) ?? false;
- _fuelCapacity = _prefs.getDouble(_generateKey('fuelCapacity')) ?? 0.0;
- _autonRundownController.addListener(() => _saveString('autonRundown', _autonRundownController.text));
+ _botPositionController.text = prefs.getString(_generateKey('botPosition')) ?? '';
_botPositionController.addListener(() => _saveString('botPosition', _botPositionController.text));
- _driveTrainTypeController.addListener(() => _saveString('driveTrainType', _driveTrainTypeController.text));
+
+ _generalObservationsController.text = prefs.getString(_generateKey('generalObservations')) ?? '';
_generalObservationsController.addListener(() => _saveString('generalObservations', _generalObservationsController.text));
+ _autonRundownController.text = prefs.getString(_generateKey('autonRundown')) ?? '';
+ _autonRundownController.addListener(() => _saveString('autonRundown', _autonRundownController.text));
+
+ _intakePositionController.text = prefs.getString(_generateKey('intakePosition')) ?? '';
+ _intakePositionController.addListener(() => _saveString('intakePosition', _intakePositionController.text));
+
+ _scoreMechanismController.text = prefs.getString(_generateKey('scoreMechanism')) ?? '';
+ _scoreMechanismController.addListener(() => _saveString('scoreMechanism', _scoreMechanismController.text));
+
+ _fuelPerCycle = prefs.getDouble(_generateKey('fuelPerCycle')) ?? 0.0;
+ _canDriveUnderTrench = prefs.getBool(_generateKey('canDriveUnderTrench')) ?? false;
+ _canDriveOverBump = prefs.getBool(_generateKey('canDriveOverBump')) ?? false;
+ _climbLevel = prefs.getDouble(_generateKey('climbLevel')) ?? 0.0;
+ _fuelCapacity = prefs.getDouble(_generateKey('fuelCapacity')) ?? 0.0;
+ _canGiveToHumanPlayer = prefs.getBool(_generateKey('canGiveToHumanPlayer')) ?? false;
+ _cycleTime = prefs.getDouble(_generateKey('cycleTime')) ?? 0.0;
+
+
+
if (mounted) {
setState(() {
_isLoading = false;
@@ -77,56 +93,58 @@ class _NotesPageState extends State {
}
Future _saveString(String field, String value) async {
- await _prefs.setString(_generateKey(field), value);
+ await prefs.setString(_generateKey(field), value);
}
Future _saveBool(String field, bool value) async {
- await _prefs.setBool(_generateKey(field), value);
+ await prefs.setBool(_generateKey(field), value);
}
Future _saveDouble(String field, double value) async {
- await _prefs.setDouble(_generateKey(field), value);
+ await prefs.setDouble(_generateKey(field), value);
}
@override
Widget build(BuildContext context) {
return Scaffold(
- appBar: AppBar(
- title: Text('Notes'),
- ),
- body: _isLoading
- ? const Center(child: CircularProgressIndicator())
- : Column(
- children: [
- Padding(
- padding: const EdgeInsets.only(top: 16.0),
- child: Text(widget.teamName, style: Theme.of(context).textTheme.titleLarge)
+ appBar: AppBar(
+ title: Text('Notes'),
+ ),
+ body: _isLoading
+ ? const Center(child: CircularProgressIndicator())
+ : SafeArea(
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(top: 16.0),
+ child: Text(widget.teamName, style: Theme.of(context).textTheme.titleLarge)
+ ),
+ Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: SegmentedButton(
+ segments: const [
+ ButtonSegment(value: ScoutingView.match, label: Text('Match Scouting')),
+ ButtonSegment(value: ScoutingView.pit, label: Text('Pit Scouting')),
+ ],
+ selected: {_selectedView},
+ onSelectionChanged: (newSelection) {
+ setState(() {
+ _selectedView = newSelection.first;
+ });
+ },
+ ),
+ ),
+ Expanded(
+ child: AnimatedSwitcher(
+ duration: const Duration(milliseconds: 300),
+ child: _selectedView == ScoutingView.match
+ ? _buildMatchScoutingView()
+ : _buildPitScoutingView(),
+ ),
+ ),
+ ],
),
- Padding(
- padding: const EdgeInsets.all(16.0),
- child: SegmentedButton(
- segments: const [
- ButtonSegment(value: ScoutingView.match, label: Text('Match Scouting')),
- ButtonSegment(value: ScoutingView.pit, label: Text('Pit Scouting')),
- ],
- selected: {_selectedView},
- onSelectionChanged: (newSelection) {
- setState(() {
- _selectedView = newSelection.first;
- });
- },
- ),
- ),
- Expanded(
- child: AnimatedSwitcher(
- duration: const Duration(milliseconds: 300),
- child: _selectedView == ScoutingView.match
- ? _buildMatchScoutingView()
- : _buildPitScoutingView(),
- ),
- ),
- ],
- ),
+ )
);
}
@@ -146,15 +164,6 @@ class _NotesPageState extends State {
),
),
const SizedBox(height: 16),
- TextField(
- controller: _autonRundownController,
- decoration: const InputDecoration(
- labelText: 'Autonomous Rundown',
- hintText: 'Describe their auto',
- border: OutlineInputBorder(),
- ),
- ),
- const SizedBox(height: 16),
TextField(
controller: _generalObservationsController,
decoration: const InputDecoration(
@@ -175,32 +184,51 @@ class _NotesPageState extends State {
Text('Robot Technical Details', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 16),
TextField(
- controller: _driveTrainTypeController,
+ controller: _autonRundownController,
decoration: const InputDecoration(
- labelText: 'Drivetrain Type',
- hintText: 'e.g., Swerve, Tank',
+ labelText: 'Autonomous Rundown',
+ hintText: 'Describe their auto',
+ border: OutlineInputBorder(),
+ ),
+ ),
+ const SizedBox(height: 16),
+ TextField(
+ controller: _intakePositionController,
+ decoration: const InputDecoration(
+ labelText: 'Intake Position',
+ hintText: 'e.g., Ground',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
SwitchListTile(
- title: Text('Has Vision/AprilTags', style: Theme.of(context).textTheme.titleSmall),
- value: _hasVision,
+ title: Text('Can Drive Over Bump', style: Theme.of(context).textTheme.titleSmall),
+ value: _canDriveOverBump,
onChanged: (bool value) {
setState(() {
- _hasVision = value;
+ _canDriveOverBump = value;
});
- _saveBool('hasVision', value);
+ _saveBool('canDriveOverBump', value);
},
),
SwitchListTile(
title: Text('Can Go Under Trench', style: Theme.of(context).textTheme.titleSmall),
- value: _trenchable,
+ value: _canDriveUnderTrench,
onChanged: (bool value) {
setState(() {
- _trenchable = value;
+ _canDriveUnderTrench = value;
});
- _saveBool('trenchable', value);
+ _saveBool('canDriveUnderTrench', value);
+ },
+ ),
+ SwitchListTile(
+ title: Text('Can Give Fuel To Human Player', style: Theme.of(context).textTheme.titleSmall),
+ value: _canGiveToHumanPlayer,
+ onChanged: (bool value) {
+ setState(() {
+ _canGiveToHumanPlayer = value;
+ });
+ _saveBool('canGiveToHumanPlayer', value);
},
),
const SizedBox(height: 16),
@@ -253,7 +281,57 @@ class _NotesPageState extends State {
],
),
),
+ const SizedBox(height: 16),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Fuel Per Cycle - ${_fuelPerCycle.round()}', style: Theme.of(context).textTheme.titleSmall),
+ Slider(
+ value: _fuelPerCycle,
+ min: 0,
+ max: _fuelCapacity,
+ divisions: 50,
+ label: _fuelPerCycle.round().toString(),
+ onChanged: (double value) {
+ setState(() {
+ _fuelPerCycle = value;
+ });
+ },
+ onChangeEnd: (double value) {
+ _saveDouble('fuelPerCycle', value);
+ },
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Cycle Time - ${_cycleTime.round()}s', style: Theme.of(context).textTheme.titleSmall),
+ Slider(
+ value: _cycleTime,
+ min: 0,
+ max: 60,
+ divisions: 60,
+ label: _cycleTime.round().toString(),
+ onChanged: (double value) {
+ setState(() {
+ _cycleTime = value;
+ });
+ },
+ onChangeEnd: (double value) {
+ _saveDouble('cycleTime', value);
+ },
+ ),
+ ],
+ ),
+ ),
],
);
}
-}
+}
\ No newline at end of file
diff --git a/lib/teamadder.dart b/lib/teamadder.dart
new file mode 100644
index 0000000..b6ed028
--- /dev/null
+++ b/lib/teamadder.dart
@@ -0,0 +1,147 @@
+import 'package:flutter/material.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+class TeamAdder extends StatefulWidget {
+ final String eventCode;
+ const TeamAdder({super.key, required this.eventCode});
+
+ @override
+ State createState() => _TeamAdderState();
+}
+
+class _TeamAdderState extends State {
+ final _teamNumberController = TextEditingController();
+ final _teamNameController = TextEditingController();
+ List customTeamNumbers = [];
+ List customTeamNames = [];
+ final _formKey = GlobalKey();
+
+ @override
+ void initState() {
+ super.initState();
+ _loadTeams();
+ }
+
+ Future _loadTeams() async {
+ final SharedPreferences prefs = await SharedPreferences.getInstance();
+ setState(() {
+ customTeamNumbers = prefs.getStringList('custom_team_numbers_${widget.eventCode}') ?? [];
+ customTeamNames = prefs.getStringList('custom_team_names_${widget.eventCode}') ?? [];
+ });
+ }
+
+ Future _saveTeam() async {
+ if (!_formKey.currentState!.validate()) {
+ return;
+ }
+
+ final String teamNumber = _teamNumberController.text;
+ final String teamName = _teamNameController.text;
+
+ setState(() {
+ customTeamNumbers.add(teamNumber);
+ customTeamNames.add(teamName);
+ });
+
+ final SharedPreferences prefs = await SharedPreferences.getInstance();
+ await prefs.setStringList('custom_team_numbers_${widget.eventCode}', customTeamNumbers);
+ await prefs.setStringList('custom_team_names_${widget.eventCode}', customTeamNames);
+
+ _teamNumberController.clear();
+ _teamNameController.clear();
+
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Added Team: $teamNumber - $teamName'),
+ behavior: SnackBarBehavior.floating,
+ margin: const EdgeInsets.all(16.0),
+ ),
+ );
+ }
+ }
+
+ @override
+ void dispose() {
+ _teamNumberController.dispose();
+ _teamNameController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Add Team'),
+ ),
+ body: Form(
+ key: _formKey,
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Add a New Team', style: Theme.of(context).textTheme.titleLarge),
+ const SizedBox(height: 16),
+ TextFormField(
+ controller: _teamNumberController,
+ decoration: const InputDecoration(
+ labelText: 'Team Number',
+ border: OutlineInputBorder(),
+ ),
+ keyboardType: TextInputType.number,
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return 'Please enter a team number';
+ }
+ return null;
+ },
+ ),
+ const SizedBox(height: 16),
+ TextFormField(
+ controller: _teamNameController,
+ decoration: const InputDecoration(
+ labelText: 'Team Name',
+ border: OutlineInputBorder(),
+ ),
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return 'Please enter a team name';
+ }
+ return null;
+ },
+ ),
+ const SizedBox(height: 24),
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: _saveTeam,
+ child: const Text('Save Team'),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const Divider(height: 32),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: Text('Saved Teams', style: Theme.of(context).textTheme.titleLarge),
+ ),
+ Expanded(
+ child: ListView.builder(
+ itemCount: customTeamNumbers.length,
+ itemBuilder: (context, index) {
+ return ListTile(
+ title: Text('${customTeamNumbers[index]} - ${customTeamNames[index]}'),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/teampicker.dart b/lib/teampicker.dart
index 83badfc..36aca8e 100644
--- a/lib/teampicker.dart
+++ b/lib/teampicker.dart
@@ -1,14 +1,17 @@
import 'package:flutter/material.dart';
import 'package:laserscouter/core/api.dart';
import 'notespage.dart';
+import 'teamadder.dart';
import 'package:to_csv/to_csv.dart' as csv_export;
import 'package:shared_preferences/shared_preferences.dart';
class TeamPicker extends StatefulWidget {
final String eventCode;
+ final String eventName;
+ final bool isCustomOnly;
- const TeamPicker({super.key, required this.eventCode});
+ const TeamPicker({super.key, required this.eventCode, required this.eventName,this.isCustomOnly = false});
@override
State createState() => _TeamPickerState();
@@ -29,14 +32,17 @@ class _TeamPickerState extends State {
Future _exportData() async {
List header = [];
header.add('Team Number');
- header.add('Drivetrain Type');
- header.add('Has Vision');
- header.add('Climb Level');
- header.add('Trenchable');
- header.add('Fuel Capacity');
header.add('Bot Position');
+ header.add('Gen. Observations');
header.add('Auton Rundown');
- header.add('General Observations');
+ header.add('Intake Position');
+ header.add('Can Drive Over Bump');
+ header.add('Can Go Under Trench');
+ header.add('Can Give Fuel to HP');
+ header.add('Climb Level');
+ header.add('Fuel Capacity');
+ header.add('Fuel per Cycle');
+ header.add("Cycle Time");
List> data = [];
for (int i = 0; i < teamCodes.length; i++) {
@@ -44,68 +50,144 @@ class _TeamPickerState extends State {
return '${teamCodes[i]}_${widget.eventCode}_$field';
}
SharedPreferences prefs = await SharedPreferences.getInstance();
- String? autonRundown = prefs.getString(generateKey('autonRundown'));
String? botPosition = prefs.getString(generateKey('botPosition'));
String? generalObservations = prefs.getString(generateKey('generalObservations'));
- String? driveTrainType = prefs.getString(generateKey('driveTrainType'));
- String? hasVision = prefs.getBool(generateKey('hasVision')).toString();
+ String? autonRundown = prefs.getString(generateKey('autonRundown'));
+ String? intakePosition = prefs.getString(generateKey('intakePosition'));
+ String? canDriveOverBump = prefs.getBool(generateKey('canDriveOverBump')).toString();
+ String? canDriveUnderTrench = prefs.getBool(generateKey('canDriveUnderTrench')).toString();
+ String? canGiveToHumanPlayer = prefs.getBool(generateKey('canGiveToHumanPlayer')).toString();
String? climbLevel = prefs.getDouble(generateKey('climbLevel')).toString();
- String? trenchable = prefs.getBool(generateKey('trenchable')).toString();
String? fuelCapacity = prefs.getDouble(generateKey('fuelCapacity')).toString();
+ String? fuelPerCycle = prefs.getDouble(generateKey('fuelPerCycle')).toString();
+ String? cycleTime = prefs.getDouble(generateKey('cycleTime')).toString();
- if (hasVision == 'null') {
- hasVision = '';
+ if (canDriveOverBump == 'null') {
+ canDriveOverBump = '';
+ }
+ if (canDriveUnderTrench == 'null') {
+ canDriveUnderTrench = '';
+ }
+ if (canGiveToHumanPlayer == 'null') {
+ canGiveToHumanPlayer = '';
}
if (climbLevel == 'null') {
climbLevel = '';
}
- if (trenchable == 'null') {
- trenchable = '';
- }
if (fuelCapacity == 'null') {
fuelCapacity = '';
}
+ if (fuelPerCycle == 'null') {
+ fuelPerCycle = '';
+ }
+ if (cycleTime == 'null') {
+ cycleTime = '';
+ }
+
List teamData = [];
teamData.add(teamCodes[i]);
- teamData.add(driveTrainType ?? '');
- teamData.add(hasVision);
- teamData.add(climbLevel);
- teamData.add(trenchable);
- teamData.add(fuelCapacity);
teamData.add(botPosition ?? '');
- teamData.add(autonRundown ?? '');
teamData.add(generalObservations ?? '');
+ teamData.add(autonRundown ?? '');
+ teamData.add(intakePosition ?? '');
+ teamData.add(canDriveOverBump);
+ teamData.add(canDriveUnderTrench);
+ teamData.add(canGiveToHumanPlayer);
+ teamData.add(climbLevel);
+ teamData.add(fuelCapacity);
+ teamData.add(fuelPerCycle);
+ teamData.add(cycleTime);
data.add(teamData);
}
- csv_export.myCSV(header, data, setHeadersInFirstRow: true, emptyRowsConfig: {1: 1}, fileName: 'laserscouter_${widget.eventCode}.csv');
+ csv_export.myCSV(header, data, setHeadersInFirstRow: true, fileName: 'laserscouter_${widget.eventCode}.csv');
}
Future _fetchTeams() async {
try {
- final EventSearchResult result = await eventSearch(widget.eventCode);
+ List apiTeamNames = [];
+ List apiTeamCodes = [];
+
+ if (!widget.isCustomOnly) {
+ final EventSearchResult apiResult = await eventSearch(widget.eventCode);
+ apiTeamNames = apiResult.teamNames;
+ apiTeamCodes = apiResult.teamCodes;
+ }
+
+ final SharedPreferences prefs = await SharedPreferences.getInstance();
+ final List customTeamNames = prefs.getStringList('custom_team_names_${widget.eventName}') ?? [];
+ final List customTeamCodes = prefs.getStringList('custom_team_numbers_${widget.eventCode}') ?? [];
+
+ List combinedNames = [];
+ List combinedCodes = [];
+ for (int i = 0; i < apiTeamCodes.length; i++) {
+ if (!combinedCodes.contains(apiTeamCodes[i])) {
+ combinedCodes.add(apiTeamCodes[i]);
+ if (i < apiTeamNames.length) {
+ combinedNames.add(apiTeamNames[i]);
+ } else {
+ combinedNames.add('');
+ }
+ }
+ }
+ for (int i = 0; i < customTeamCodes.length; i++) {
+ if (!combinedCodes.contains(customTeamCodes[i])) {
+ combinedCodes.add(customTeamCodes[i]);
+ if (i < customTeamNames.length) {
+ combinedNames.add(customTeamNames[i]);
+ } else {
+ combinedNames.add('');
+ }
+ }
+ }
+
if (mounted) {
setState(() {
- teamNames = result.teamNames;
- teamCodes = result.teamCodes;
+ teamNames = combinedNames.toList();
+ teamCodes = combinedCodes.toList();
isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
- errorMessage = "Failed to load teams. Please try again.";
+ errorMessage = widget.isCustomOnly
+ ? "Could not load custom teams."
+ : "Failed to load teams. Please try again.";
isLoading = false;
});
}
}
}
+
+ Future _refreshTeams() async {
+ setState(() {
+ isLoading = true;
+ });
+ await _fetchTeams();
+ }
+
+
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Teams'),
+ actions: [
+ IconButton(
+ onPressed: () async {
+ await Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => TeamAdder(eventCode: widget.eventCode,),
+ ),
+ );
+ _refreshTeams();
+ },
+ icon: Icon(Icons.add),
+ )
+ ]
),
body: _buildBody(),
floatingActionButton: FloatingActionButton(
diff --git a/pubspec.yaml b/pubspec.yaml
index 740e216..58b08bf 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
-version: 26.1.23
+version: 26.2.22
environment:
sdk: ^3.10.7