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
|
|
@ -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
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: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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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 26.2.14}"
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -172,7 +173,10 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
child: const Text('Settings'),
|
child: const Text('Settings'),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 _scoreMechanisimController = 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();
|
||||||
|
_scoreMechanisimController.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();
|
SharedPreferences 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));
|
||||||
|
|
||||||
|
_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) {
|
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,42 @@ 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(),
|
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 +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: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();
|
||||||
|
|
@ -83,29 +86,89 @@ class _TeamPickerState extends State<TeamPicker> {
|
||||||
|
|
||||||
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(
|
||||||
|
|
|
||||||
|
|
@ -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.14+2
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.10.7
|
sdk: ^3.10.7
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue