Changed questions, added export.

This commit is contained in:
Raktbastr 2026-01-23 15:52:29 -06:00
parent a83bf0d533
commit 78618aed9a
8 changed files with 277 additions and 173 deletions

View file

@ -26,4 +26,5 @@
<data android:mimeType="text/plain"/> <data android:mimeType="text/plain"/>
</intent> </intent>
</queries> </queries>
<uses-permission android:name="android.permission.INTERNET" />
</manifest> </manifest>

View file

@ -2,7 +2,7 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
String proxyURL = "https://laserscouter.halfheart.net/"; String proxyURL = "https://laserproxy.halfheart.net/";
class TeamSearchResult { class TeamSearchResult {
final List<String> eventNames; final List<String> eventNames;

View file

@ -84,8 +84,9 @@ final ThemeData laserTheme = ThemeData(
sliderTheme: SliderThemeData( sliderTheme: SliderThemeData(
activeTrackColor: const Color(0xFF00245D), activeTrackColor: const Color(0xFF00245D),
thumbColor: const Color(0xFF00245D), thumbColor: const Color(0xFFFFFFFF),
inactiveTrackColor: Colors.grey.shade800, inactiveTrackColor: Colors.grey.shade800,
valueIndicatorColor: Colors.white
), ),
checkboxTheme: CheckboxThemeData( checkboxTheme: CheckboxThemeData(
@ -101,7 +102,7 @@ final ThemeData laserTheme = ThemeData(
switchTheme: SwitchThemeData( switchTheme: SwitchThemeData(
thumbColor: WidgetStateProperty.resolveWith((states) { thumbColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) { if (states.contains(WidgetState.selected)) {
return const Color(0xFF00245D); return const Color(0xFFFFFFFF);
} }
return null; return null;
}), }),

View file

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
enum ScoutingView { match, pit }
class NotesPage extends StatefulWidget { class NotesPage extends StatefulWidget {
final String teamCode; final String teamCode;
final String eventCode; final String eventCode;
@ -18,10 +20,19 @@ class NotesPage extends StatefulWidget {
} }
class _NotesPageState extends State<NotesPage> { class _NotesPageState extends State<NotesPage> {
final TextEditingController _checkboxes = TextEditingController(); late SharedPreferences _prefs;
final TextEditingController _controller2 = TextEditingController(); ScoutingView _selectedView = ScoutingView.match;
final TextEditingController _switchvalue = TextEditingController(); bool _isLoading = true;
double _slidervalue = 0.0;
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 @override
void initState() { void initState() {
@ -31,40 +42,50 @@ class _NotesPageState extends State<NotesPage> {
@override @override
void dispose() { void dispose() {
_saveNotes(); _autonRundownController.dispose();
_checkboxes.dispose(); _botPositionController.dispose();
_controller2.dispose(); _driveTrainTypeController.dispose();
_switchvalue.dispose(); _generalObservationsController.dispose();
super.dispose(); super.dispose();
} }
String _generateKey(String field) {
return '${widget.teamCode}_${widget.eventCode}_$field';
}
Future<void> _loadNotes() async { Future<void> _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) { if (mounted) {
setState(() { setState(() {
_checkboxes.text = _isLoading = false;
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;
}); });
} }
} }
Future<void> _saveNotes() async { Future<void> _saveString(String field, String value) async {
SharedPreferences prefs = await SharedPreferences.getInstance(); await _prefs.setString(_generateKey(field), value);
await prefs.setString( }
'${widget.teamCode}_${widget.eventCode}_note1', _checkboxes.text);
await prefs.setString( Future<void> _saveBool(String field, bool value) async {
'${widget.teamCode}_${widget.eventCode}_note2', _controller2.text); await _prefs.setBool(_generateKey(field), value);
await prefs.setString( }
'${widget.teamCode}_${widget.eventCode}_note3', _switchvalue.text);
await prefs.setString( Future<void> _saveDouble(String field, double value) async {
'${widget.teamCode}_${widget.eventCode}_note4', _slidervalue.toString()); await _prefs.setDouble(_generateKey(field), value);
} }
@override @override
@ -73,108 +94,166 @@ class _NotesPageState extends State<NotesPage> {
appBar: AppBar( appBar: AppBar(
title: Text('Notes'), title: Text('Notes'),
), ),
body: Padding( body: _isLoading
padding: const EdgeInsets.all(16.0), ? const Center(child: CircularProgressIndicator())
child: ListView( : Column(
children: [ children: [
Text(widget.teamName, style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center), Padding(
Text('Bot Starting Position', padding: const EdgeInsets.only(top: 16.0),
style: Theme.of(context).textTheme.titleMedium), child: Text(widget.teamName, style: Theme.of(context).textTheme.titleLarge)
CheckboxListTile( ),
title: const Text('Left'), Padding(
value: _checkboxes.text.contains('Left'), padding: const EdgeInsets.all(16.0),
onChanged: (bool? value) { 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(() { setState(() {
if (value == true) { _selectedView = newSelection.first;
_checkboxes.text += 'Left ';
} else {
_checkboxes.text =
_checkboxes.text.replaceAll('Left ', '');
}
_saveNotes();
}); });
}, },
), ),
CheckboxListTile( ),
title: const Text('Mid'), Expanded(
value: _checkboxes.text.contains('Mid'), child: AnimatedSwitcher(
onChanged: (bool? value) { duration: const Duration(milliseconds: 300),
setState(() { child: _selectedView == ScoutingView.match
if (value == true) { ? _buildMatchScoutingView()
_checkboxes.text += 'Mid '; : _buildPitScoutingView(),
} else {
_checkboxes.text = _checkboxes.text.replaceAll('Mid ', '');
}
_saveNotes();
});
},
), ),
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);
},
),
],
),
),
],
);
}
} }

View file

@ -56,7 +56,7 @@ class _SettingsPageState extends State<SettingsPage> {
controller: _urlController, controller: _urlController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'URL', labelText: 'URL',
hintText: 'https://laserscouter.halfheart.net/', hintText: 'https://laserproxy.halfheart.net/',
border: OutlineInputBorder(), border: OutlineInputBorder(),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Color.fromARGB(255, 19, 81, 179)), borderSide: BorderSide(color: Color.fromARGB(255, 19, 81, 179)),
@ -64,7 +64,7 @@ class _SettingsPageState extends State<SettingsPage> {
), ),
onChanged: (value) { onChanged: (value) {
if (value.isEmpty) { if (value.isEmpty) {
proxyURL = "https://laserscouter.halfheart.net/"; proxyURL = "https://laserproxy.halfheart.net/";
} }
else { else {
proxyURL = value; proxyURL = value;

View file

@ -1,8 +1,9 @@
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 'package:to_csv/to_csv.dart' as csv_export;
import 'package:shared_preferences/shared_preferences.dart'; 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 { class TeamPicker extends StatefulWidget {
final String eventCode; final String eventCode;
@ -22,16 +23,67 @@ class _TeamPickerState extends State<TeamPicker> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Call the new async method to fetch teams
_fetchTeams(); _fetchTeams();
} }
Future<void> _exportData() async {
List<String> 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<List<String>> 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<String> 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<void> _fetchTeams() async { Future<void> _fetchTeams() async {
try { try {
// Await the result from the refactored API function
final EventSearchResult result = await eventSearch(widget.eventCode); final EventSearchResult result = await eventSearch(widget.eventCode);
// Check if the widget is still mounted before updating state
if (mounted) { if (mounted) {
setState(() { setState(() {
teamNames = result.teamNames; teamNames = result.teamNames;
@ -40,7 +92,6 @@ class _TeamPickerState extends State<TeamPicker> {
}); });
} }
} catch (e) { } catch (e) {
// If an error occurs, update the state to show an error message
if (mounted) { if (mounted) {
setState(() { setState(() {
errorMessage = "Failed to load teams. Please try again."; errorMessage = "Failed to load teams. Please try again.";
@ -50,46 +101,17 @@ class _TeamPickerState extends State<TeamPicker> {
} }
} }
Future<void> makeCSV() async {
List<String> header = [
'Team Number',
'Bot Position',
'Auton Rundown',
'Can Score Algae',
'Coral Level'
];
List<List<String>> dataLists = [header];
SharedPreferences prefs = await SharedPreferences.getInstance();
for (int i = 0; i < teamCodes.length; i++) {
List<String> 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 @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(
icon: const Icon(Icons.share),
onPressed: isLoading || teamCodes.isEmpty ? null : makeCSV,
),
],
), ),
body: _buildBody(), body: _buildBody(),
floatingActionButton: FloatingActionButton(
onPressed: _exportData,
child: const Icon(Icons.share),
),
); );
} }

View file

@ -249,7 +249,7 @@ packages:
source: hosted source: hosted
version: "0.15.6" version: "0.15.6"
http: http:
dependency: transitive dependency: "direct main"
description: description:
name: http name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
@ -372,10 +372,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: objective_c name: objective_c
sha256: "9922a1ad59ac5afb154cc948aa6ded01987a75003651d0a2866afc23f4da624e" sha256: "7fd0c4d8ac8980011753b9bdaed2bf15111365924cdeeeaeb596214ea2b03537"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.2.3" version: "9.2.4"
path: path:
dependency: transitive dependency: transitive
description: description:

View file

@ -39,6 +39,7 @@ dependencies:
to_csv: ^6.0.1 to_csv: ^6.0.1
flutter_launcher_icons: ^0.14.4 flutter_launcher_icons: ^0.14.4
rename_app: ^1.6.5 rename_app: ^1.6.5
http: ^1.6.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: