Compare commits
4 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b082e78eec | |||
| dc7de89690 | |||
| 1c3b52723f | |||
| 9327b6120f |
9 changed files with 747 additions and 223 deletions
10
README.md
10
README.md
|
|
@ -1,5 +1,4 @@
|
|||

|
||||
<sub>WIP Logo</sub>
|
||||

|
||||
|
||||
# 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.
|
||||
* If you plan to test on an iPhone, you must have an Apple Developer account and a macOS device with Xcode.
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
136
lib/eventadder.dart
Normal file
136
lib/eventadder.dart
Normal file
|
|
@ -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<EventAdder> createState() => _EventAdderState();
|
||||
}
|
||||
|
||||
class _EventAdderState extends State<EventAdder> {
|
||||
final _customEventNameController = TextEditingController();
|
||||
final _customEventCodeController = TextEditingController();
|
||||
List<String> customEventNames = [];
|
||||
List<String> customEventCodes = [];
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadEvents();
|
||||
}
|
||||
|
||||
Future<void> _loadEvents() async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
customEventNames = prefs.getStringList('custom_event_names') ?? [];
|
||||
customEventCodes = prefs.getStringList('custom_event_codes') ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _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]}')
|
||||
);
|
||||
}
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> eventNames;
|
||||
final List<String> eventCodes;
|
||||
|
||||
const EventPicker({super.key, required this.eventNames, required this.eventCodes});
|
||||
@override
|
||||
State<EventPicker> createState() => _EventPickerState();
|
||||
}
|
||||
|
||||
class _EventPickerState extends State<EventPicker> {
|
||||
List<String> eventNames = [];
|
||||
List<String> eventCodes = [];
|
||||
List<bool> isCustom = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
eventNames = List<String>.from(widget.eventNames);
|
||||
eventCodes = List<String>.from(widget.eventCodes);
|
||||
isCustom = List<bool>.filled(widget.eventCodes.length, false);
|
||||
_refreshEvents();
|
||||
}
|
||||
|
||||
Future<void> _refreshEvents() async {
|
||||
eventNames = widget.eventNames.toList();
|
||||
eventCodes = widget.eventCodes.toList();
|
||||
isCustom = List<bool>.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: () {
|
||||
bool isEventCustom = isCustom[index];
|
||||
String eventCode = eventCodes[index];
|
||||
String eventName = eventCodes[index];
|
||||
|
||||
if (isEventCustom) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TeamPicker(
|
||||
eventCode: eventCodes[index],
|
||||
eventCode: eventCode,
|
||||
eventName: eventName,
|
||||
isCustomOnly: isEventCustom,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TeamPicker(
|
||||
isCustomOnly: isEventCustom,
|
||||
eventName: eventName,
|
||||
eventCode: eventCode,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -67,11 +67,12 @@ class _LoginPageState extends State<LoginPage> {
|
|||
appBar: AppBar(
|
||||
title: const Text('Login'),
|
||||
),
|
||||
body: Padding(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Spacer(),
|
||||
Image.asset('assets/main.png', height: 75, alignment: Alignment.center),
|
||||
const SizedBox(height: 16.0),
|
||||
TextField(
|
||||
|
|
@ -80,6 +81,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||
labelText: 'Team Number',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
ElevatedButton(
|
||||
|
|
@ -107,7 +109,11 @@ class _LoginPageState extends State<LoginPage> {
|
|||
);
|
||||
} catch (e) {
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(content: Text('Error: ${e.toString()}')),
|
||||
SnackBar(
|
||||
content: Text('Error: ${e.toString()}'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: const EdgeInsets.all(16.0),
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
|
|
@ -127,14 +133,9 @@ class _LoginPageState extends State<LoginPage> {
|
|||
),
|
||||
)
|
||||
: const Text('Login'),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Padding (
|
||||
padding: const EdgeInsets.all(50.0),
|
||||
child: Row (
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
const Spacer(),
|
||||
Row (mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
child: const Text('Info'),
|
||||
|
|
@ -145,7 +146,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||
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"
|
||||
"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"
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -172,7 +173,10 @@ class _LoginPageState extends State<LoginPage> {
|
|||
child: const Text('Settings'),
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,19 +20,22 @@ class NotesPage extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _NotesPageState extends State<NotesPage> {
|
||||
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<NotesPage> {
|
|||
void dispose() {
|
||||
_autonRundownController.dispose();
|
||||
_botPositionController.dispose();
|
||||
_driveTrainTypeController.dispose();
|
||||
_generalObservationsController.dispose();
|
||||
_intakePositionController.dispose();
|
||||
_scoreMechanismController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -54,21 +58,33 @@ class _NotesPageState extends State<NotesPage> {
|
|||
}
|
||||
|
||||
Future<void> _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,15 +93,15 @@ class _NotesPageState extends State<NotesPage> {
|
|||
}
|
||||
|
||||
Future<void> _saveString(String field, String value) async {
|
||||
await _prefs.setString(_generateKey(field), value);
|
||||
await prefs.setString(_generateKey(field), value);
|
||||
}
|
||||
|
||||
Future<void> _saveBool(String field, bool value) async {
|
||||
await _prefs.setBool(_generateKey(field), value);
|
||||
await prefs.setBool(_generateKey(field), value);
|
||||
}
|
||||
|
||||
Future<void> _saveDouble(String field, double value) async {
|
||||
await _prefs.setDouble(_generateKey(field), value);
|
||||
await prefs.setDouble(_generateKey(field), value);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -96,7 +112,8 @@ class _NotesPageState extends State<NotesPage> {
|
|||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Column(
|
||||
: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
|
|
@ -127,6 +144,7 @@ class _NotesPageState extends State<NotesPage> {
|
|||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -146,15 +164,6 @@ class _NotesPageState extends State<NotesPage> {
|
|||
),
|
||||
),
|
||||
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<NotesPage> {
|
|||
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,6 +281,56 @@ class _NotesPageState extends State<NotesPage> {
|
|||
],
|
||||
),
|
||||
),
|
||||
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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
147
lib/teamadder.dart
Normal file
147
lib/teamadder.dart
Normal file
|
|
@ -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<TeamAdder> createState() => _TeamAdderState();
|
||||
}
|
||||
|
||||
class _TeamAdderState extends State<TeamAdder> {
|
||||
final _teamNumberController = TextEditingController();
|
||||
final _teamNameController = TextEditingController();
|
||||
List<String> customTeamNumbers = [];
|
||||
List<String> customTeamNames = [];
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadTeams();
|
||||
}
|
||||
|
||||
Future<void> _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<void> _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]}'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TeamPicker> createState() => _TeamPickerState();
|
||||
|
|
@ -29,14 +32,17 @@ class _TeamPickerState extends State<TeamPicker> {
|
|||
Future<void> _exportData() async {
|
||||
List<String> 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<List<String>> data = [];
|
||||
for (int i = 0; i < teamCodes.length; i++) {
|
||||
|
|
@ -44,68 +50,144 @@ class _TeamPickerState extends State<TeamPicker> {
|
|||
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<String> 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<void> _fetchTeams() async {
|
||||
try {
|
||||
final EventSearchResult result = await eventSearch(widget.eventCode);
|
||||
List<String> apiTeamNames = [];
|
||||
List<String> 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<String> customTeamNames = prefs.getStringList('custom_team_names_${widget.eventName}') ?? [];
|
||||
final List<String> customTeamCodes = prefs.getStringList('custom_team_numbers_${widget.eventCode}') ?? [];
|
||||
|
||||
List<String> combinedNames = [];
|
||||
List<String> 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<void> _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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue