From 9327b6120fca451e2dd3698e7fd9ef2273610ebf Mon Sep 17 00:00:00 2001 From: Raktbastr Date: Sat, 14 Feb 2026 13:35:40 -0600 Subject: [PATCH] Added ability to add custom teams and events. --- android/app/build.gradle.kts | 2 + lib/eventadder.dart | 136 ++++++++++++++++++++++ lib/eventpicker.dart | 97 ++++++++++++++-- lib/login.dart | 208 +++++++++++++++++----------------- lib/notespage.dart | 211 +++++++++++++++++++++++------------ lib/teamadder.dart | 147 ++++++++++++++++++++++++ lib/teampicker.dart | 73 +++++++++++- pubspec.yaml | 2 +- 8 files changed, 687 insertions(+), 189 deletions(-) create mode 100644 lib/eventadder.dart create mode 100644 lib/teamadder.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 2391af5..84cb48d 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -28,6 +28,8 @@ android { targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName + val fileName = "${rootProject.name}_v${versionName}_${versionCode}" + setProperty("archivesBaseName", fileName) } buildTypes { diff --git a/lib/eventadder.dart b/lib/eventadder.dart new file mode 100644 index 0000000..7563e24 --- /dev/null +++ b/lib/eventadder.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class EventAdder extends StatefulWidget { + const EventAdder({super.key}); + + @override + State createState() => _EventAdderState(); +} + +class _EventAdderState extends State { + final _customEventNameController = TextEditingController(); + final _customEventCodeController = TextEditingController(); + List customEventNames = []; + List customEventCodes = []; + final _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _loadEvents(); + } + + Future _loadEvents() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + setState(() { + customEventNames = prefs.getStringList('custom_event_names') ?? []; + customEventCodes = prefs.getStringList('custom_event_codes') ?? []; + }); + } + + Future _saveEvent() async { + if (!_formKey.currentState!.validate()) { + return; + } + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final String eventName = _customEventNameController.text; + String eventCode; + + if (_customEventCodeController.text.isEmpty) { + eventCode = '${DateTime.now().year.toString()}${eventName.replaceAll(' ', '').substring(0,4)}'; + } else { + eventCode = _customEventCodeController.text; + } + + customEventNames.add(eventName); + customEventCodes.add(eventCode); + + await prefs.setStringList('custom_event_names', customEventNames); + await prefs.setStringList('custom_event_codes', customEventCodes); + + _customEventNameController.clear(); + _customEventCodeController.clear(); + setState(() {}); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Added Event: $eventName - $eventCode'), + behavior: SnackBarBehavior.floating, + margin: const EdgeInsets.all(16.0), + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Add Event')), + body: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Text('Add Custom Event', style: Theme.of(context).textTheme.titleLarge) + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: ListView( + shrinkWrap: true, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + children: [ + const SizedBox(height: 16), + TextFormField( + controller: _customEventNameController, + decoration: const InputDecoration( + labelText: 'Event Name', + hintText: 'gm_construct Regional', + border: OutlineInputBorder() + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a event name'; + } + return null; + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _customEventCodeController, + decoration: const InputDecoration( + labelText: 'Event Code', + hintText: '2026gmct', + border: OutlineInputBorder() + ) + ), + Text('Leave this field EMPTY unless you NEED a custom code.'), + const SizedBox(height: 16.0), + ElevatedButton( + onPressed: () { _saveEvent(); }, + child: const Text('Create Event'), + ), + ], + ) + ), + Divider(), + Text('Custom Events', style: Theme.of(context).textTheme.titleLarge), + ListView.builder( + shrinkWrap: true, + itemCount: customEventNames.length, + itemBuilder: (context, index) { + return ListTile( + title: Text('${customEventNames[index]} - ${customEventCodes[index]}') + ); + } + ) + ], + ) + ) + ); + } +} \ No newline at end of file diff --git a/lib/eventpicker.dart b/lib/eventpicker.dart index b60e616..6b4acda 100644 --- a/lib/eventpicker.dart +++ b/lib/eventpicker.dart @@ -1,30 +1,107 @@ import 'package:flutter/material.dart'; +import 'package:laserscouter/eventadder.dart'; import 'teampicker.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + + +class EventPicker extends StatefulWidget { + const EventPicker({super.key, required this.eventNames, required this.eventCodes}); -class EventPicker extends StatelessWidget { final List eventNames; final List eventCodes; - const EventPicker({super.key, required this.eventNames, required this.eventCodes}); + @override + State createState() => _EventPickerState(); +} + +class _EventPickerState extends State { + List eventNames = []; + List eventCodes = []; + List isCustom = []; + + @override + void initState() { + super.initState(); + eventNames = List.from(widget.eventNames); + eventCodes = List.from(widget.eventCodes); + isCustom = List.filled(widget.eventCodes.length, false); + _refreshEvents(); + } + + Future _refreshEvents() async { + eventNames = widget.eventNames.toList(); + eventCodes = widget.eventCodes.toList(); + isCustom = List.filled(widget.eventCodes.length, false).toList(); + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final storedNames = prefs.getStringList('custom_event_names') ?? []; + final storedCodes = prefs.getStringList('custom_event_codes') ?? []; + + for (int i = 0; i < storedNames.length; i++) { + if (!eventCodes.contains(storedCodes[i])) { + eventNames.add(storedNames[i]); + eventCodes.add(storedCodes[i]); + isCustom.add(true); + } + } + if (mounted) { + setState(() {}); + } + } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Event')), + appBar: AppBar( + title: const Text('Event'), + actions: [ + IconButton( + onPressed: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const EventAdder(), + ), + ); + _refreshEvents(); + }, + icon: Icon(Icons.add), + ) + ] + ), body: ListView.builder( itemCount: eventNames.length, itemBuilder: (context, index) { return ListTile( title: Text(eventNames[index]), onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TeamPicker( - eventCode: eventCodes[index], + bool isEventCustom = isCustom[index]; + String eventCode = eventCodes[index]; + String eventName = eventCodes[index]; + + if (isEventCustom) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TeamPicker( + eventCode: eventCode, + eventName: eventName, + isCustomOnly: isEventCustom, + ), ), - ), - ); + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TeamPicker( + isCustomOnly: isEventCustom, + eventName: eventName, + eventCode: eventCode, + ), + ), + ); + } }, ); }, diff --git a/lib/login.dart b/lib/login.dart index a31463d..181e6a5 100644 --- a/lib/login.dart +++ b/lib/login.dart @@ -67,112 +67,116 @@ class _LoginPageState extends State { appBar: AppBar( title: const Text('Login'), ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset('assets/main.png', height: 75, alignment: Alignment.center), - const SizedBox(height: 16.0), - TextField( - controller: _teamNumberController, - decoration: const InputDecoration( - labelText: 'Team Number', - border: OutlineInputBorder(), - ), - ), - const SizedBox(height: 16.0), - ElevatedButton( - onPressed: _isLoading - ? null - : () async { - final navigator = Navigator.of(context); - final scaffoldMessenger = ScaffoldMessenger.of(context); - - setState(() { - _isLoading = true; - }); - - try { - await _saveData(); - String teamNumber = _teamNumberController.text; - final result = await teamSearch(teamNumber); - navigator.push( - MaterialPageRoute( - builder: (context) => EventPicker( - eventNames: result.eventNames, - eventCodes: result.eventCodes, - ), - ), - ); - } catch (e) { - scaffoldMessenger.showSnackBar( - SnackBar(content: Text('Error: ${e.toString()}')), - ); - } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - }, - child: _isLoading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2.0, - color: Colors.white, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + const Spacer(), + Image.asset('assets/main.png', height: 75, alignment: Alignment.center), + const SizedBox(height: 16.0), + TextField( + controller: _teamNumberController, + decoration: const InputDecoration( + labelText: 'Team Number', + border: OutlineInputBorder(), ), - ) - : const Text('Login'), - ) - ], - ), - ), - bottomNavigationBar: Padding ( - padding: const EdgeInsets.all(50.0), - child: Row ( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextButton( - child: const Text('Info'), - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Info'), - content: const Text( - "This app makes use of The Blue Alliance APIv3 through Laser Proxy. No API keys are stored on device. Laser Scouter was created by FRC 2077 Laser Robotics. \n\nVersion: Rebuilt 26.1.23" + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16.0), + ElevatedButton( + onPressed: _isLoading + ? null + : () async { + final navigator = Navigator.of(context); + final scaffoldMessenger = ScaffoldMessenger.of(context); + + setState(() { + _isLoading = true; + }); + + try { + await _saveData(); + String teamNumber = _teamNumberController.text; + final result = await teamSearch(teamNumber); + navigator.push( + MaterialPageRoute( + builder: (context) => EventPicker( + eventNames: result.eventNames, + eventCodes: result.eventCodes, ), - ); + ), + ); + } catch (e) { + scaffoldMessenger.showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + behavior: SnackBarBehavior.floating, + margin: const EdgeInsets.all(16.0), + ), + ); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); } - ); - }, - ), - TextButton( - child: const Text('Github'), - onPressed: () { - _launchUrl(Uri.parse('https://github.com/raktbastr/laserscouter')); - } - ), - TextButton( - onPressed: _isLoading - ? null - : () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SettingsPage(), + } + }, + child: _isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: Colors.white, + ), + ) + : const Text('Login'), + ), + const Spacer(), + Row (mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + child: const Text('Info'), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Info'), + content: const Text( + "This app makes use of The Blue Alliance APIv3 through Laser Proxy. No API keys are stored on device. Laser Scouter was created by FRC 2077 Laser Robotics. \n\nVersion: Rebuilt 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'), - ) - ] - ) + ] + ), + ], + ), + ), ) ); } diff --git a/lib/notespage.dart b/lib/notespage.dart index f7de9c3..ee4122f 100644 --- a/lib/notespage.dart +++ b/lib/notespage.dart @@ -20,19 +20,22 @@ class NotesPage extends StatefulWidget { } class _NotesPageState extends State { - late SharedPreferences _prefs; + late SharedPreferences prefs; ScoutingView _selectedView = ScoutingView.match; bool _isLoading = true; - final _autonRundownController = TextEditingController(); final _botPositionController = TextEditingController(); final _generalObservationsController = TextEditingController(); - - final _driveTrainTypeController = TextEditingController(); - bool _hasVision = false; - double _climbLevel = 100.0; - bool _trenchable = false; + final _autonRundownController = TextEditingController(); + final _intakePositionController = TextEditingController(); + final _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 { void dispose() { _autonRundownController.dispose(); _botPositionController.dispose(); - _driveTrainTypeController.dispose(); _generalObservationsController.dispose(); + _intakePositionController.dispose(); + _scoreMechanisimController.dispose(); super.dispose(); } @@ -54,21 +58,33 @@ class _NotesPageState extends State { } Future _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 { } Future _saveString(String field, String value) async { - await _prefs.setString(_generateKey(field), value); + await prefs.setString(_generateKey(field), value); } Future _saveBool(String field, bool value) async { - await _prefs.setBool(_generateKey(field), value); + await prefs.setBool(_generateKey(field), value); } Future _saveDouble(String field, double value) async { - await _prefs.setDouble(_generateKey(field), value); + await prefs.setDouble(_generateKey(field), value); } @override @@ -96,37 +112,39 @@ class _NotesPageState extends State { ), 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( + segments: const [ + ButtonSegment(value: ScoutingView.match, label: Text('Match Scouting')), + ButtonSegment(value: ScoutingView.pit, label: Text('Pit Scouting')), + ], + selected: {_selectedView}, + onSelectionChanged: (newSelection) { + setState(() { + _selectedView = newSelection.first; + }); + }, + ), + ), + Expanded( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: _selectedView == ScoutingView.match + ? _buildMatchScoutingView() + : _buildPitScoutingView(), + ), + ), + ], ), - Padding( - padding: const EdgeInsets.all(16.0), - child: SegmentedButton( - segments: const [ - ButtonSegment(value: ScoutingView.match, label: Text('Match Scouting')), - ButtonSegment(value: ScoutingView.pit, label: Text('Pit Scouting')), - ], - selected: {_selectedView}, - onSelectionChanged: (newSelection) { - setState(() { - _selectedView = newSelection.first; - }); - }, - ), - ), - Expanded( - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: _selectedView == ScoutingView.match - ? _buildMatchScoutingView() - : _buildPitScoutingView(), - ), - ), - ], - ), + ) ); } @@ -146,15 +164,6 @@ class _NotesPageState extends State { ), ), const SizedBox(height: 16), - TextField( - controller: _autonRundownController, - decoration: const InputDecoration( - labelText: 'Autonomous Rundown', - hintText: 'Describe their auto', - border: OutlineInputBorder(), - ), - ), - const SizedBox(height: 16), TextField( controller: _generalObservationsController, decoration: const InputDecoration( @@ -175,32 +184,42 @@ class _NotesPageState extends State { Text('Robot Technical Details', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 16), TextField( - controller: _driveTrainTypeController, + controller: _autonRundownController, decoration: const InputDecoration( - labelText: 'Drivetrain Type', - hintText: 'e.g., Swerve, Tank', + labelText: 'Autonomous Rundown', + hintText: 'Describe their auto', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), 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 { ], ), ), + 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); + }, + ), + ], + ), + ), ], ); } diff --git a/lib/teamadder.dart b/lib/teamadder.dart new file mode 100644 index 0000000..b6ed028 --- /dev/null +++ b/lib/teamadder.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class TeamAdder extends StatefulWidget { + final String eventCode; + const TeamAdder({super.key, required this.eventCode}); + + @override + State createState() => _TeamAdderState(); +} + +class _TeamAdderState extends State { + final _teamNumberController = TextEditingController(); + final _teamNameController = TextEditingController(); + List customTeamNumbers = []; + List customTeamNames = []; + final _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _loadTeams(); + } + + Future _loadTeams() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + setState(() { + customTeamNumbers = prefs.getStringList('custom_team_numbers_${widget.eventCode}') ?? []; + customTeamNames = prefs.getStringList('custom_team_names_${widget.eventCode}') ?? []; + }); + } + + Future _saveTeam() async { + if (!_formKey.currentState!.validate()) { + return; + } + + final String teamNumber = _teamNumberController.text; + final String teamName = _teamNameController.text; + + setState(() { + customTeamNumbers.add(teamNumber); + customTeamNames.add(teamName); + }); + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setStringList('custom_team_numbers_${widget.eventCode}', customTeamNumbers); + await prefs.setStringList('custom_team_names_${widget.eventCode}', customTeamNames); + + _teamNumberController.clear(); + _teamNameController.clear(); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Added Team: $teamNumber - $teamName'), + behavior: SnackBarBehavior.floating, + margin: const EdgeInsets.all(16.0), + ), + ); + } + } + + @override + void dispose() { + _teamNumberController.dispose(); + _teamNameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Add Team'), + ), + body: Form( + key: _formKey, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Add a New Team', style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 16), + TextFormField( + controller: _teamNumberController, + decoration: const InputDecoration( + labelText: 'Team Number', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a team number'; + } + return null; + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _teamNameController, + decoration: const InputDecoration( + labelText: 'Team Name', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a team name'; + } + return null; + }, + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _saveTeam, + child: const Text('Save Team'), + ), + ), + ], + ), + ), + const Divider(height: 32), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text('Saved Teams', style: Theme.of(context).textTheme.titleLarge), + ), + Expanded( + child: ListView.builder( + itemCount: customTeamNumbers.length, + itemBuilder: (context, index) { + return ListTile( + title: Text('${customTeamNumbers[index]} - ${customTeamNames[index]}'), + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/teampicker.dart b/lib/teampicker.dart index 83badfc..25832ad 100644 --- a/lib/teampicker.dart +++ b/lib/teampicker.dart @@ -1,14 +1,17 @@ import 'package:flutter/material.dart'; import 'package:laserscouter/core/api.dart'; import 'notespage.dart'; +import 'teamadder.dart'; import 'package:to_csv/to_csv.dart' as csv_export; import 'package:shared_preferences/shared_preferences.dart'; class TeamPicker extends StatefulWidget { final String eventCode; + final String eventName; + final bool isCustomOnly; - const TeamPicker({super.key, required this.eventCode}); + const TeamPicker({super.key, required this.eventCode, required this.eventName,this.isCustomOnly = false}); @override State createState() => _TeamPickerState(); @@ -83,29 +86,89 @@ class _TeamPickerState extends State { Future _fetchTeams() async { try { - final EventSearchResult result = await eventSearch(widget.eventCode); + List apiTeamNames = []; + List apiTeamCodes = []; + + if (!widget.isCustomOnly) { + final EventSearchResult apiResult = await eventSearch(widget.eventCode); + apiTeamNames = apiResult.teamNames; + apiTeamCodes = apiResult.teamCodes; + } + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final List customTeamNames = prefs.getStringList('custom_team_names_${widget.eventName}') ?? []; + final List customTeamCodes = prefs.getStringList('custom_team_numbers_${widget.eventCode}') ?? []; + + List combinedNames = []; + List combinedCodes = []; + for (int i = 0; i < apiTeamCodes.length; i++) { + if (!combinedCodes.contains(apiTeamCodes[i])) { + combinedCodes.add(apiTeamCodes[i]); + if (i < apiTeamNames.length) { + combinedNames.add(apiTeamNames[i]); + } else { + combinedNames.add(''); + } + } + } + for (int i = 0; i < customTeamCodes.length; i++) { + if (!combinedCodes.contains(customTeamCodes[i])) { + combinedCodes.add(customTeamCodes[i]); + if (i < customTeamNames.length) { + combinedNames.add(customTeamNames[i]); + } else { + combinedNames.add(''); + } + } + } + if (mounted) { setState(() { - teamNames = result.teamNames; - teamCodes = result.teamCodes; + teamNames = combinedNames.toList(); + teamCodes = combinedCodes.toList(); isLoading = false; }); } } catch (e) { if (mounted) { setState(() { - errorMessage = "Failed to load teams. Please try again."; + errorMessage = widget.isCustomOnly + ? "Could not load custom teams." + : "Failed to load teams. Please try again."; isLoading = false; }); } } } + + Future _refreshTeams() async { + setState(() { + isLoading = true; + }); + await _fetchTeams(); + } + + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Teams'), + actions: [ + IconButton( + onPressed: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TeamAdder(eventCode: widget.eventCode,), + ), + ); + _refreshTeams(); + }, + icon: Icon(Icons.add), + ) + ] ), body: _buildBody(), floatingActionButton: FloatingActionButton( diff --git a/pubspec.yaml b/pubspec.yaml index 740e216..a7935b9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 26.1.23 +version: 26.2.14+2 environment: sdk: ^3.10.7