Added ability to add custom teams and events.
This commit is contained in:
parent
84aaf9c527
commit
9327b6120f
8 changed files with 687 additions and 189 deletions
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: () {
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
|||
208
lib/login.dart
208
lib/login.dart
|
|
@ -67,112 +67,116 @@ class _LoginPageState extends State<LoginPage> {
|
|||
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 26.2.14}"
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
),
|
||||
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'),
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 _scoreMechanisimController = 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();
|
||||
_scoreMechanisimController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -54,21 +58,33 @@ class _NotesPageState extends State<NotesPage> {
|
|||
}
|
||||
|
||||
Future<void> _loadNotes() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
SharedPreferences 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));
|
||||
|
||||
_scoreMechanisimController.text = prefs.getString(_generateKey('scoreMechanism')) ?? '';
|
||||
_scoreMechanisimController.addListener(() => _saveString('scoreMechanism', _scoreMechanisimController.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,37 +112,39 @@ class _NotesPageState extends State<NotesPage> {
|
|||
),
|
||||
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)
|
||||
: 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<ScoutingView>(
|
||||
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<ScoutingView>(
|
||||
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<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,42 @@ 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),
|
||||
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 +272,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()}', style: Theme.of(context).textTheme.titleSmall),
|
||||
Slider(
|
||||
value: _cycleTime,
|
||||
min: 0,
|
||||
max: _cycleTime,
|
||||
divisions: 50,
|
||||
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();
|
||||
|
|
@ -83,29 +86,89 @@ class _TeamPickerState extends State<TeamPicker> {
|
|||
|
||||
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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue