diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e659f42..1623c75 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -26,4 +26,5 @@ + diff --git a/lib/core/api.dart b/lib/core/api.dart index ff0d958..427df21 100644 --- a/lib/core/api.dart +++ b/lib/core/api.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; -String proxyURL = "https://laserscouter.halfheart.net/"; +String proxyURL = "https://laserproxy.halfheart.net/"; class TeamSearchResult { final List eventNames; diff --git a/lib/core/theme.dart b/lib/core/theme.dart index a7358dc..e1b80f9 100644 --- a/lib/core/theme.dart +++ b/lib/core/theme.dart @@ -84,8 +84,9 @@ final ThemeData laserTheme = ThemeData( sliderTheme: SliderThemeData( activeTrackColor: const Color(0xFF00245D), - thumbColor: const Color(0xFF00245D), + thumbColor: const Color(0xFFFFFFFF), inactiveTrackColor: Colors.grey.shade800, + valueIndicatorColor: Colors.white ), checkboxTheme: CheckboxThemeData( @@ -101,7 +102,7 @@ final ThemeData laserTheme = ThemeData( switchTheme: SwitchThemeData( thumbColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) { - return const Color(0xFF00245D); + return const Color(0xFFFFFFFF); } return null; }), diff --git a/lib/notespage.dart b/lib/notespage.dart index 6df2aa0..f7de9c3 100644 --- a/lib/notespage.dart +++ b/lib/notespage.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; +enum ScoutingView { match, pit } + class NotesPage extends StatefulWidget { final String teamCode; final String eventCode; @@ -18,10 +20,19 @@ class NotesPage extends StatefulWidget { } class _NotesPageState extends State { - final TextEditingController _checkboxes = TextEditingController(); - final TextEditingController _controller2 = TextEditingController(); - final TextEditingController _switchvalue = TextEditingController(); - double _slidervalue = 0.0; + 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; + double _fuelCapacity = 0.0; @override void initState() { @@ -31,40 +42,50 @@ class _NotesPageState extends State { @override void dispose() { - _saveNotes(); - _checkboxes.dispose(); - _controller2.dispose(); - _switchvalue.dispose(); + _autonRundownController.dispose(); + _botPositionController.dispose(); + _driveTrainTypeController.dispose(); + _generalObservationsController.dispose(); super.dispose(); } + String _generateKey(String field) { + return '${widget.teamCode}_${widget.eventCode}_$field'; + } + Future _loadNotes() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); + _prefs = await SharedPreferences.getInstance(); + + _autonRundownController.text = _prefs.getString(_generateKey('autonRundown')) ?? ''; + _botPositionController.text = _prefs.getString(_generateKey('botPosition')) ?? ''; + _generalObservationsController.text = _prefs.getString(_generateKey('generalObservations')) ?? ''; + _driveTrainTypeController.text = _prefs.getString(_generateKey('driveTrainType')) ?? ''; + _hasVision = _prefs.getBool(_generateKey('hasVision')) ?? false; + _climbLevel = _prefs.getDouble(_generateKey('climbLevel')) ?? 0.0; + _trenchable = _prefs.getBool(_generateKey('trenchable')) ?? false; + _fuelCapacity = _prefs.getDouble(_generateKey('fuelCapacity')) ?? 0.0; + _autonRundownController.addListener(() => _saveString('autonRundown', _autonRundownController.text)); + _botPositionController.addListener(() => _saveString('botPosition', _botPositionController.text)); + _driveTrainTypeController.addListener(() => _saveString('driveTrainType', _driveTrainTypeController.text)); + _generalObservationsController.addListener(() => _saveString('generalObservations', _generalObservationsController.text)); + if (mounted) { setState(() { - _checkboxes.text = - prefs.getString('${widget.teamCode}_${widget.eventCode}_note1') ?? ''; - _controller2.text = - prefs.getString('${widget.teamCode}_${widget.eventCode}_note2') ?? ''; - _switchvalue.text = - prefs.getString('${widget.teamCode}_${widget.eventCode}_note3') ?? ''; - _slidervalue = double.tryParse(prefs - .getString('${widget.teamCode}_${widget.eventCode}_note4') ?? - '0.0') ?? 0.0; + _isLoading = false; }); } } - Future _saveNotes() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setString( - '${widget.teamCode}_${widget.eventCode}_note1', _checkboxes.text); - await prefs.setString( - '${widget.teamCode}_${widget.eventCode}_note2', _controller2.text); - await prefs.setString( - '${widget.teamCode}_${widget.eventCode}_note3', _switchvalue.text); - await prefs.setString( - '${widget.teamCode}_${widget.eventCode}_note4', _slidervalue.toString()); + Future _saveString(String field, String value) async { + await _prefs.setString(_generateKey(field), value); + } + + Future _saveBool(String field, bool value) async { + await _prefs.setBool(_generateKey(field), value); + } + + Future _saveDouble(String field, double value) async { + await _prefs.setDouble(_generateKey(field), value); } @override @@ -73,108 +94,166 @@ class _NotesPageState extends State { appBar: AppBar( title: Text('Notes'), ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: ListView( - children: [ - Text(widget.teamName, style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center), - Text('Bot Starting Position', - style: Theme.of(context).textTheme.titleMedium), - CheckboxListTile( - title: const Text('Left'), - value: _checkboxes.text.contains('Left'), - onChanged: (bool? value) { + 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) + ), + 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(() { - if (value == true) { - _checkboxes.text += 'Left '; - } else { - _checkboxes.text = - _checkboxes.text.replaceAll('Left ', ''); - } - _saveNotes(); + _selectedView = newSelection.first; }); }, ), - CheckboxListTile( - title: const Text('Mid'), - value: _checkboxes.text.contains('Mid'), - onChanged: (bool? value) { - setState(() { - if (value == true) { - _checkboxes.text += 'Mid '; - } else { - _checkboxes.text = _checkboxes.text.replaceAll('Mid ', ''); - } - _saveNotes(); - }); - }, + ), + Expanded( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: _selectedView == ScoutingView.match + ? _buildMatchScoutingView() + : _buildPitScoutingView(), ), - CheckboxListTile( - title: const Text('Right'), - value: _checkboxes.text.contains('Right'), - onChanged: (bool? value) { - setState(() { - if (value == true) { - _checkboxes.text += 'Right '; - } else { - _checkboxes.text = - _checkboxes.text.replaceAll('Right ', ''); - } - _saveNotes(); - }); - }, - ), - const Divider(), - TextField( - controller: _controller2, - decoration: const InputDecoration(labelText: 'Auton Rundown'), - maxLines: null, - keyboardType: TextInputType.multiline, - onChanged: (text) => _saveNotes(), - ), - const Divider(), - CheckboxListTile( - title: const Text('Can Score Algae'), - value: _switchvalue.text.contains('Can Score Algae'), - onChanged: (bool? value) { - setState(() { - _switchvalue.text = - value == true ? 'Can Score Algae' : ''; - _saveNotes(); - }); - }, - ), - CheckboxListTile( - title: const Text('Cannot Score Algae'), - value: _switchvalue.text.contains('Cannot Score Algae'), - onChanged: (bool? value) { - setState(() { - _switchvalue.text = - value == true ? 'Cannot Score Algae' : ''; - _saveNotes(); - }); - }, - ), - const Divider(), - Text('Coral Level', style: Theme.of(context).textTheme.titleMedium), - Slider( - value: _slidervalue, - onChanged: (double value) { - setState(() { - _slidervalue = value; - }); - }, - onChangeEnd: (double value) { - _saveNotes(); - }, - min: 0, - max: 3, - divisions: 3, - label: _slidervalue.round().toString(), - ), - ], - ), + ), + ], ), ); } + + Widget _buildMatchScoutingView() { + return ListView( + key: const ValueKey('match'), + padding: const EdgeInsets.symmetric(horizontal: 16.0), + children: [ + Text('Match-Specific Observations', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 16), + TextField( + controller: _botPositionController, + decoration: const InputDecoration( + labelText: 'Bot Position / Role', + hintText: 'e.g., Shooter, Defense, Collector', + border: OutlineInputBorder(), + ), + ), + 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( + labelText: 'Observations', + hintText: 'General observations', + border: OutlineInputBorder(), + ), + ) + ], + ); + } + + Widget _buildPitScoutingView() { + return ListView( + key: const ValueKey('pit'), + padding: const EdgeInsets.symmetric(horizontal: 16.0), + children: [ + Text('Robot Technical Details', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 16), + TextField( + controller: _driveTrainTypeController, + decoration: const InputDecoration( + labelText: 'Drivetrain Type', + hintText: 'e.g., Swerve, Tank', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + SwitchListTile( + title: Text('Has Vision/AprilTags', style: Theme.of(context).textTheme.titleSmall), + value: _hasVision, + onChanged: (bool value) { + setState(() { + _hasVision = value; + }); + _saveBool('hasVision', value); + }, + ), + SwitchListTile( + title: Text('Can Go Under Trench', style: Theme.of(context).textTheme.titleSmall), + value: _trenchable, + onChanged: (bool value) { + setState(() { + _trenchable = value; + }); + _saveBool('trenchable', value); + }, + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Max Climb Level - ${_climbLevel.round()}', style: Theme.of(context).textTheme.titleSmall), + Slider( + value: _climbLevel, + min: 0, + max: 3, + divisions: 3, + label: _climbLevel.round().toString(), + onChanged: (double value) { + setState(() { + _climbLevel = value; + }); + }, + onChangeEnd: (double value) { + _saveDouble('climbLevel', value); + }, + ), + ], + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Fuel Capacity - ${_fuelCapacity.round()}', style: Theme.of(context).textTheme.titleSmall), + Slider( + value: _fuelCapacity, + min: 0, + max: 50, + divisions: 50, + label: _fuelCapacity.round().toString(), + onChanged: (double value) { + setState(() { + _fuelCapacity = value; + }); + }, + onChangeEnd: (double value) { + _saveDouble('fuelCapacity', value); + }, + ), + ], + ), + ), + ], + ); + } } diff --git a/lib/settings.dart b/lib/settings.dart index db1de22..a35592f 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -56,7 +56,7 @@ class _SettingsPageState extends State { controller: _urlController, decoration: const InputDecoration( labelText: 'URL', - hintText: 'https://laserscouter.halfheart.net/', + hintText: 'https://laserproxy.halfheart.net/', border: OutlineInputBorder(), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Color.fromARGB(255, 19, 81, 179)), @@ -64,7 +64,7 @@ class _SettingsPageState extends State { ), onChanged: (value) { if (value.isEmpty) { - proxyURL = "https://laserscouter.halfheart.net/"; + proxyURL = "https://laserproxy.halfheart.net/"; } else { proxyURL = value; diff --git a/lib/teampicker.dart b/lib/teampicker.dart index 29a2e3f..83badfc 100644 --- a/lib/teampicker.dart +++ b/lib/teampicker.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:laserscouter/core/api.dart'; +import 'notespage.dart'; +import 'package:to_csv/to_csv.dart' as csv_export; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:to_csv/to_csv.dart' as exportcsv; -import 'notespage.dart'; // Import the new notes page file + class TeamPicker extends StatefulWidget { final String eventCode; @@ -22,16 +23,67 @@ class _TeamPickerState extends State { @override void initState() { super.initState(); - // Call the new async method to fetch teams _fetchTeams(); } + Future _exportData() async { + List header = []; + header.add('Team Number'); + header.add('Drivetrain Type'); + header.add('Has Vision'); + header.add('Climb Level'); + header.add('Trenchable'); + header.add('Fuel Capacity'); + header.add('Bot Position'); + header.add('Auton Rundown'); + header.add('General Observations'); + + List> data = []; + for (int i = 0; i < teamCodes.length; i++) { + String generateKey(String field) { + return '${teamCodes[i]}_${widget.eventCode}_$field'; + } + SharedPreferences prefs = await SharedPreferences.getInstance(); + String? autonRundown = prefs.getString(generateKey('autonRundown')); + String? botPosition = prefs.getString(generateKey('botPosition')); + String? generalObservations = prefs.getString(generateKey('generalObservations')); + String? driveTrainType = prefs.getString(generateKey('driveTrainType')); + String? hasVision = prefs.getBool(generateKey('hasVision')).toString(); + String? climbLevel = prefs.getDouble(generateKey('climbLevel')).toString(); + String? trenchable = prefs.getBool(generateKey('trenchable')).toString(); + String? fuelCapacity = prefs.getDouble(generateKey('fuelCapacity')).toString(); + + if (hasVision == 'null') { + hasVision = ''; + } + if (climbLevel == 'null') { + climbLevel = ''; + } + if (trenchable == 'null') { + trenchable = ''; + } + if (fuelCapacity == 'null') { + fuelCapacity = ''; + } + + List teamData = []; + teamData.add(teamCodes[i]); + teamData.add(driveTrainType ?? ''); + teamData.add(hasVision); + teamData.add(climbLevel); + teamData.add(trenchable); + teamData.add(fuelCapacity); + teamData.add(botPosition ?? ''); + teamData.add(autonRundown ?? ''); + teamData.add(generalObservations ?? ''); + data.add(teamData); + } + csv_export.myCSV(header, data, setHeadersInFirstRow: true, emptyRowsConfig: {1: 1}, fileName: 'laserscouter_${widget.eventCode}.csv'); + } + Future _fetchTeams() async { try { - // Await the result from the refactored API function final EventSearchResult result = await eventSearch(widget.eventCode); - - // Check if the widget is still mounted before updating state if (mounted) { setState(() { teamNames = result.teamNames; @@ -40,7 +92,6 @@ class _TeamPickerState extends State { }); } } catch (e) { - // If an error occurs, update the state to show an error message if (mounted) { setState(() { errorMessage = "Failed to load teams. Please try again."; @@ -50,46 +101,17 @@ class _TeamPickerState extends State { } } - Future makeCSV() async { - List header = [ - 'Team Number', - 'Bot Position', - 'Auton Rundown', - 'Can Score Algae', - 'Coral Level' - ]; - List> dataLists = [header]; - - SharedPreferences prefs = await SharedPreferences.getInstance(); - for (int i = 0; i < teamCodes.length; i++) { - List data = []; - data.add(teamCodes[i]); - String botPosition = prefs.getString('${teamCodes[i]}_${widget.eventCode}_note1') ?? ''; - String autonRundown = prefs.getString('${teamCodes[i]}_${widget.eventCode}_note2') ?? ''; - String canScoreAlgae = prefs.getString('${teamCodes[i]}_${widget.eventCode}_note3') ?? ''; - String coralLevel = prefs.getString('${teamCodes[i]}_${widget.eventCode}_note4') ?? '0.0'; - data.add(botPosition.trim()); - data.add(autonRundown); - data.add(canScoreAlgae); - data.add(coralLevel); - dataLists.add(data); - } - exportcsv.myCSV(header, dataLists, fileName: '${widget.eventCode}-scouting-data'); - } - @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Teams'), - actions: [ - IconButton( - icon: const Icon(Icons.share), - onPressed: isLoading || teamCodes.isEmpty ? null : makeCSV, - ), - ], ), body: _buildBody(), + floatingActionButton: FloatingActionButton( + onPressed: _exportData, + child: const Icon(Icons.share), + ), ); } @@ -140,4 +162,4 @@ class _TeamPickerState extends State { }, ); } -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index 76bed0f..7efa8ce 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -249,7 +249,7 @@ packages: source: hosted version: "0.15.6" http: - dependency: transitive + dependency: "direct main" description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" @@ -372,10 +372,10 @@ packages: dependency: transitive description: name: objective_c - sha256: "9922a1ad59ac5afb154cc948aa6ded01987a75003651d0a2866afc23f4da624e" + sha256: "7fd0c4d8ac8980011753b9bdaed2bf15111365924cdeeeaeb596214ea2b03537" url: "https://pub.dev" source: hosted - version: "9.2.3" + version: "9.2.4" path: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6416eb1..00fd429 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: to_csv: ^6.0.1 flutter_launcher_icons: ^0.14.4 rename_app: ^1.6.5 + http: ^1.6.0 dev_dependencies: flutter_test: