Compare commits

...

4 commits

9 changed files with 747 additions and 223 deletions

View file

@ -1,5 +1,4 @@
![Laser Scouter](https://raw.githubusercontent.com/Raktbastr/laserscouter/refs/heads/main/assets/laserscouterlogo.png) ![Laser Scouter](https://raw.githubusercontent.com/Raktbastr/laserscouter/refs/heads/main/assets/main.png)
<sub>WIP Logo</sub>
# Simple FRC scouting app. Developed by me for Laser Robotics and the FRC community! # 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 * Better and updated scouting questions, these are examples from Reefscape
* Match scouting * Match scouting
* Online accessible version
# Some extra notes # 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! * 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. * 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 * To run on iOS you will still need to compile it and install with Xcode
# How to contribute # 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. 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. 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.

View file

@ -28,6 +28,8 @@ android {
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName versionName = flutter.versionName
val fileName = "${rootProject.name}_v${versionName}_${versionCode}"
setProperty("archivesBaseName", fileName)
} }
buildTypes { buildTypes {

136
lib/eventadder.dart Normal file
View 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]}')
);
}
)
],
)
)
);
}
}

View file

@ -1,30 +1,107 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:laserscouter/eventadder.dart';
import 'teampicker.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> eventNames;
final List<String> eventCodes; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( 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( body: ListView.builder(
itemCount: eventNames.length, itemCount: eventNames.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return ListTile( return ListTile(
title: Text(eventNames[index]), title: Text(eventNames[index]),
onTap: () { onTap: () {
bool isEventCustom = isCustom[index];
String eventCode = eventCodes[index];
String eventName = eventCodes[index];
if (isEventCustom) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => TeamPicker( 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,
),
),
);
}
}, },
); );
}, },

View file

@ -67,11 +67,12 @@ class _LoginPageState extends State<LoginPage> {
appBar: AppBar( appBar: AppBar(
title: const Text('Login'), title: const Text('Login'),
), ),
body: Padding( body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Spacer(),
Image.asset('assets/main.png', height: 75, alignment: Alignment.center), Image.asset('assets/main.png', height: 75, alignment: Alignment.center),
const SizedBox(height: 16.0), const SizedBox(height: 16.0),
TextField( TextField(
@ -80,6 +81,7 @@ class _LoginPageState extends State<LoginPage> {
labelText: 'Team Number', labelText: 'Team Number',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
keyboardType: TextInputType.number,
), ),
const SizedBox(height: 16.0), const SizedBox(height: 16.0),
ElevatedButton( ElevatedButton(
@ -107,7 +109,11 @@ class _LoginPageState extends State<LoginPage> {
); );
} catch (e) { } catch (e) {
scaffoldMessenger.showSnackBar( scaffoldMessenger.showSnackBar(
SnackBar(content: Text('Error: ${e.toString()}')), SnackBar(
content: Text('Error: ${e.toString()}'),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16.0),
),
); );
} finally { } finally {
if (mounted) { if (mounted) {
@ -127,14 +133,9 @@ class _LoginPageState extends State<LoginPage> {
), ),
) )
: const Text('Login'), : const Text('Login'),
)
],
), ),
), const Spacer(),
bottomNavigationBar: Padding ( Row (mainAxisAlignment: MainAxisAlignment.center,
padding: const EdgeInsets.all(50.0),
child: Row (
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
TextButton( TextButton(
child: const Text('Info'), child: const Text('Info'),
@ -145,7 +146,7 @@ class _LoginPageState extends State<LoginPage> {
return AlertDialog( return AlertDialog(
title: const Text('Info'), title: const Text('Info'),
content: const Text( 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'), child: const Text('Settings'),
) )
] ]
) ),
],
),
),
) )
); );
} }

View file

@ -20,19 +20,22 @@ class NotesPage extends StatefulWidget {
} }
class _NotesPageState extends State<NotesPage> { class _NotesPageState extends State<NotesPage> {
late SharedPreferences _prefs; late SharedPreferences prefs;
ScoutingView _selectedView = ScoutingView.match; ScoutingView _selectedView = ScoutingView.match;
bool _isLoading = true; bool _isLoading = true;
final _autonRundownController = TextEditingController();
final _botPositionController = TextEditingController(); final _botPositionController = TextEditingController();
final _generalObservationsController = TextEditingController(); final _generalObservationsController = TextEditingController();
final _autonRundownController = TextEditingController();
final _driveTrainTypeController = TextEditingController(); final _intakePositionController = TextEditingController();
bool _hasVision = false; final _scoreMechanismController = TextEditingController();
double _climbLevel = 100.0; double _fuelPerCycle = 0;
bool _trenchable = false; bool _canDriveUnderTrench = false;
bool _canDriveOverBump = false;
double _climbLevel = 0.0;
double _fuelCapacity = 0.0; double _fuelCapacity = 0.0;
bool _canGiveToHumanPlayer = false;
double _cycleTime = 0.0;
@override @override
void initState() { void initState() {
@ -44,8 +47,9 @@ class _NotesPageState extends State<NotesPage> {
void dispose() { void dispose() {
_autonRundownController.dispose(); _autonRundownController.dispose();
_botPositionController.dispose(); _botPositionController.dispose();
_driveTrainTypeController.dispose();
_generalObservationsController.dispose(); _generalObservationsController.dispose();
_intakePositionController.dispose();
_scoreMechanismController.dispose();
super.dispose(); super.dispose();
} }
@ -54,21 +58,33 @@ class _NotesPageState extends State<NotesPage> {
} }
Future<void> _loadNotes() async { Future<void> _loadNotes() async {
_prefs = await SharedPreferences.getInstance(); prefs = await SharedPreferences.getInstance();
_autonRundownController.text = _prefs.getString(_generateKey('autonRundown')) ?? ''; _botPositionController.text = prefs.getString(_generateKey('botPosition')) ?? '';
_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.addListener(() => _saveString('botPosition', _botPositionController.text)); _botPositionController.addListener(() => _saveString('botPosition', _botPositionController.text));
_driveTrainTypeController.addListener(() => _saveString('driveTrainType', _driveTrainTypeController.text));
_generalObservationsController.text = prefs.getString(_generateKey('generalObservations')) ?? '';
_generalObservationsController.addListener(() => _saveString('generalObservations', _generalObservationsController.text)); _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) { if (mounted) {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
@ -77,15 +93,15 @@ class _NotesPageState extends State<NotesPage> {
} }
Future<void> _saveString(String field, String value) async { 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 { 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 { Future<void> _saveDouble(String field, double value) async {
await _prefs.setDouble(_generateKey(field), value); await prefs.setDouble(_generateKey(field), value);
} }
@override @override
@ -96,7 +112,8 @@ class _NotesPageState extends State<NotesPage> {
), ),
body: _isLoading body: _isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: Column( : SafeArea(
child: Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(top: 16.0), 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), const SizedBox(height: 16),
TextField(
controller: _autonRundownController,
decoration: const InputDecoration(
labelText: 'Autonomous Rundown',
hintText: 'Describe their auto',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField( TextField(
controller: _generalObservationsController, controller: _generalObservationsController,
decoration: const InputDecoration( decoration: const InputDecoration(
@ -175,32 +184,51 @@ class _NotesPageState extends State<NotesPage> {
Text('Robot Technical Details', style: Theme.of(context).textTheme.titleMedium), Text('Robot Technical Details', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 16), const SizedBox(height: 16),
TextField( TextField(
controller: _driveTrainTypeController, controller: _autonRundownController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Drivetrain Type', labelText: 'Autonomous Rundown',
hintText: 'e.g., Swerve, Tank', 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(), border: OutlineInputBorder(),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
SwitchListTile( SwitchListTile(
title: Text('Has Vision/AprilTags', style: Theme.of(context).textTheme.titleSmall), title: Text('Can Drive Over Bump', style: Theme.of(context).textTheme.titleSmall),
value: _hasVision, value: _canDriveOverBump,
onChanged: (bool value) { onChanged: (bool value) {
setState(() { setState(() {
_hasVision = value; _canDriveOverBump = value;
}); });
_saveBool('hasVision', value); _saveBool('canDriveOverBump', value);
}, },
), ),
SwitchListTile( SwitchListTile(
title: Text('Can Go Under Trench', style: Theme.of(context).textTheme.titleSmall), title: Text('Can Go Under Trench', style: Theme.of(context).textTheme.titleSmall),
value: _trenchable, value: _canDriveUnderTrench,
onChanged: (bool value) { onChanged: (bool value) {
setState(() { 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), 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
View 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]}'),
);
},
),
),
],
),
),
);
}
}

View file

@ -1,14 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:laserscouter/core/api.dart'; import 'package:laserscouter/core/api.dart';
import 'notespage.dart'; import 'notespage.dart';
import 'teamadder.dart';
import 'package:to_csv/to_csv.dart' as csv_export; import 'package:to_csv/to_csv.dart' as csv_export;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class TeamPicker extends StatefulWidget { class TeamPicker extends StatefulWidget {
final String eventCode; 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 @override
State<TeamPicker> createState() => _TeamPickerState(); State<TeamPicker> createState() => _TeamPickerState();
@ -29,14 +32,17 @@ class _TeamPickerState extends State<TeamPicker> {
Future<void> _exportData() async { Future<void> _exportData() async {
List<String> header = []; List<String> header = [];
header.add('Team Number'); 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('Bot Position');
header.add('Gen. Observations');
header.add('Auton Rundown'); 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 = []; List<List<String>> data = [];
for (int i = 0; i < teamCodes.length; i++) { for (int i = 0; i < teamCodes.length; i++) {
@ -44,68 +50,144 @@ class _TeamPickerState extends State<TeamPicker> {
return '${teamCodes[i]}_${widget.eventCode}_$field'; return '${teamCodes[i]}_${widget.eventCode}_$field';
} }
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
String? autonRundown = prefs.getString(generateKey('autonRundown'));
String? botPosition = prefs.getString(generateKey('botPosition')); String? botPosition = prefs.getString(generateKey('botPosition'));
String? generalObservations = prefs.getString(generateKey('generalObservations')); String? generalObservations = prefs.getString(generateKey('generalObservations'));
String? driveTrainType = prefs.getString(generateKey('driveTrainType')); String? autonRundown = prefs.getString(generateKey('autonRundown'));
String? hasVision = prefs.getBool(generateKey('hasVision')).toString(); 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? climbLevel = prefs.getDouble(generateKey('climbLevel')).toString();
String? trenchable = prefs.getBool(generateKey('trenchable')).toString();
String? fuelCapacity = prefs.getDouble(generateKey('fuelCapacity')).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') { if (canDriveOverBump == 'null') {
hasVision = ''; canDriveOverBump = '';
}
if (canDriveUnderTrench == 'null') {
canDriveUnderTrench = '';
}
if (canGiveToHumanPlayer == 'null') {
canGiveToHumanPlayer = '';
} }
if (climbLevel == 'null') { if (climbLevel == 'null') {
climbLevel = ''; climbLevel = '';
} }
if (trenchable == 'null') {
trenchable = '';
}
if (fuelCapacity == 'null') { if (fuelCapacity == 'null') {
fuelCapacity = ''; fuelCapacity = '';
} }
if (fuelPerCycle == 'null') {
fuelPerCycle = '';
}
if (cycleTime == 'null') {
cycleTime = '';
}
List<String> teamData = []; List<String> teamData = [];
teamData.add(teamCodes[i]); teamData.add(teamCodes[i]);
teamData.add(driveTrainType ?? '');
teamData.add(hasVision);
teamData.add(climbLevel);
teamData.add(trenchable);
teamData.add(fuelCapacity);
teamData.add(botPosition ?? ''); teamData.add(botPosition ?? '');
teamData.add(autonRundown ?? '');
teamData.add(generalObservations ?? ''); 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); 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 { Future<void> _fetchTeams() async {
try { 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) { if (mounted) {
setState(() { setState(() {
teamNames = result.teamNames; teamNames = combinedNames.toList();
teamCodes = result.teamCodes; teamCodes = combinedCodes.toList();
isLoading = false; isLoading = false;
}); });
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
setState(() { 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; isLoading = false;
}); });
} }
} }
} }
Future<void> _refreshTeams() async {
setState(() {
isLoading = true;
});
await _fetchTeams();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Teams'), 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(), body: _buildBody(),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(

View file

@ -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 # 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 # 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. # of the product and file versions while build-number is used as the build suffix.
version: 26.1.23 version: 26.2.22
environment: environment:
sdk: ^3.10.7 sdk: ^3.10.7