diff --git a/API/GeneticPolar.py b/API/GeneticPolar.py index 44ba2f8..bf36abd 100644 --- a/API/GeneticPolar.py +++ b/API/GeneticPolar.py @@ -481,11 +481,11 @@ def func(solution, functionInputs): XMatrix.insert(0, 'scouting_data_count', pd.Series(matchScoutingCount)) XMatrix.insert(0, 'match_count', pd.Series(teamMatchCount)) XMatrix.insert(0, 'team_number', pd.Series(teams)) -# XMatrix.insert( -# 0, -# 'total_fuel_cycles', -# XMatrix['auto_fuel_cycles'] + XMatrix['teleop_fuel_cycles'] -# ) + XMatrix.insert( + 0, + 'total_fuel_cycles', + XMatrix['auto_fuel_cycles'] + XMatrix['teleop_fuel_cycles'] +) # print(XMatrix) diff --git a/APP/lib/pages/event_page.dart b/APP/lib/pages/event_page.dart index 03ceeb2..7e880ca 100644 --- a/APP/lib/pages/event_page.dart +++ b/APP/lib/pages/event_page.dart @@ -1955,7 +1955,7 @@ class _MatchScoutingTabState extends State<_MatchScoutingTab> { // .copyWith(fuel_cycles: val)))); // }), _buildCounterRow( - 'Passing Cycles', data.data.auto_scoring.passing_cycles, + 'Passed Balls', data.data.auto_scoring.passing_cycles, (val) { setState(() => data = data.copyWith( data: data.data.copyWith( diff --git a/APP/lib/pages/match_page.dart b/APP/lib/pages/match_page.dart index 1da641b..bfe1786 100644 --- a/APP/lib/pages/match_page.dart +++ b/APP/lib/pages/match_page.dart @@ -6,6 +6,7 @@ import 'package:provider/provider.dart'; import 'package:scouting_app/models/match_details_2026.dart'; import 'package:scouting_app/models/match_scouting_2026.dart'; import 'package:syncfusion_flutter_core/theme.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../utils.dart'; import '../widgets/auto_display_2026.dart'; import '../widgets/field_whiteboard.dart'; @@ -90,6 +91,7 @@ class _MatchPageState extends State { _StatsTab(widget), _RedTab(widget), _BlueTab(widget), + _TBATab(widget) ]; return Scaffold( appBar: PolarForecastAppBar( @@ -127,6 +129,10 @@ class _MatchPageState extends State { activeIcon: Icon(Icons.precision_manufacturing, color: Colors.blue.shade900), label: 'Blue Autos'), + BottomNavigationBarItem( + icon: Icon(Icons.shield_outlined, color: Colors.blue), + activeIcon: Icon(Icons.shield, color: Colors.blue), + label: 'TBAVideo') ], type: BottomNavigationBarType.shifting, selectedLabelStyle: TextStyle(color: Colors.white, fontFamily: 'Font'), @@ -139,6 +145,136 @@ class _MatchPageState extends State { } } +class _TBATab extends StatefulWidget { + final MatchPage widget; + const _TBATab(this.widget); + + @override + _TBATabState createState() => _TBATabState(); +} + +class _TBATabState extends State<_TBATab> { + late final String tbaUrl; + + @override + void initState() { + super.initState(); + tbaUrl = 'https://www.thebluealliance.com/match/${widget.widget.match_key}'; + } + + Future _openTBA() async { + final uri = Uri.parse(tbaUrl); + + if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) { + throw 'Could not launch $tbaUrl'; + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final cs = theme.colorScheme; + + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + padding: const EdgeInsets.all(28), + decoration: BoxDecoration( + color: cs.surface, + borderRadius: BorderRadius.circular(28), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 30, + offset: const Offset(0, 12), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.open_in_new_rounded, + size: 48, + color: Colors.blue, + ), + + const SizedBox(height: 16), + + Text( + "View Match on The Blue Alliance", + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + fontFamily: 'Font', + ), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 12), + + Text( + widget.widget.match_key, + style: TextStyle( + fontSize: 14, + color: Colors.grey, + fontFamily: 'Font', + ), + ), + + const SizedBox(height: 24), + + // 🔥 Clean CTA button + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _openTBA, + icon: const Icon(Icons.link), + label: const Text("Open Match"), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + fontFamily: 'Font', + ), + ), + ), + ), + + const SizedBox(height: 12), + + // subtle link text + TextButton( + onPressed: _openTBA, + child: Text( + tbaUrl, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade500, + fontFamily: 'Font', + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ), + ), + ); + } +} + class _StatsTab extends StatefulWidget { final MatchPage widget; const _StatsTab(this.widget); diff --git a/APP/lib/pages/pick_list_page.dart b/APP/lib/pages/pick_list_page.dart index 4bbe6c1..3108a06 100644 --- a/APP/lib/pages/pick_list_page.dart +++ b/APP/lib/pages/pick_list_page.dart @@ -14,6 +14,8 @@ import 'dart:html' as html; import '../models/group.dart'; import '../models/team_stats_2026.dart'; import '../models/picture_data.dart'; +import '../models/tournament.dart'; +import '../widgets/deaths_form.dart'; final CacheManager picklistAvatarCacheManager = CacheManager( Config( @@ -2279,7 +2281,22 @@ class _QuickCompareDialogState extends State { ]; } - Widget _metricRow(_CompareMetric metric, bool compactMode) { + Future _openDeathsComparison( + String leftTeamNumber, String rightTeamNumber) async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => DeathsComparisonPage( + eventCode: widget.eventCode, + leftTeamNumber: leftTeamNumber, + rightTeamNumber: rightTeamNumber, + teamNames: names, + ), + ), + ); + } + + Widget _metricRow(_CompareMetric metric, bool compactMode, + {VoidCallback? onTap}) { final cs = Theme.of(context).colorScheme; final tied = (metric.left - metric.right).abs() < 1e-9; final leftBetter = metric.lowerIsBetter @@ -2310,55 +2327,63 @@ class _QuickCompareDialogState extends State { final labelWidth = compactMode ? 120.0 : 160.0; - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Row( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), - decoration: BoxDecoration( - color: leftBg, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: leftBorder), - ), - child: Text( - _formatMetric(metric, metric.left), - style: const TextStyle(fontWeight: FontWeight.bold), - ), + final row = Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: leftBg, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: leftBorder), ), - ), - const SizedBox(width: 8), - SizedBox( - width: labelWidth, child: Text( - metric.label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: TextStyle( - fontWeight: FontWeight.w700, - color: cs.onSurface.withOpacity(0.85)), + _formatMetric(metric, metric.left), + style: const TextStyle(fontWeight: FontWeight.bold), ), ), - const SizedBox(width: 8), - Expanded( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), - decoration: BoxDecoration( - color: rightBg, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: rightBorder), - ), - child: Text( - _formatMetric(metric, metric.right), - textAlign: TextAlign.right, - style: const TextStyle(fontWeight: FontWeight.bold), - ), + ), + const SizedBox(width: 8), + SizedBox( + width: labelWidth, + child: Text( + metric.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.w700, + color: cs.onSurface.withOpacity(0.85)), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: rightBg, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: rightBorder), + ), + child: Text( + _formatMetric(metric, metric.right), + textAlign: TextAlign.right, + style: const TextStyle(fontWeight: FontWeight.bold), ), ), - ], - ), + ), + ], + ); + + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: onTap == null + ? row + : InkWell( + borderRadius: BorderRadius.circular(10), + onTap: onTap, + child: row, + ), ); } @@ -2795,11 +2820,22 @@ class _QuickCompareDialogState extends State { Expanded( child: ListView.builder( itemCount: metrics.length, - itemBuilder: (context, - index) => - _metricRow( - metrics[index], - compact), + itemBuilder: + (context, index) { + final metric = + metrics[index]; + return _metricRow( + metric, + compact, + onTap: metric.label == + 'Death Rate' + ? () => + _openDeathsComparison( + aNum, + bNum) + : null, + ); + }, ), ), ], @@ -2913,6 +2949,153 @@ class _CompareMetric { }); } +class DeathsComparisonPage extends StatelessWidget { + final String eventCode; + final String leftTeamNumber; + final String rightTeamNumber; + final Map teamNames; + + const DeathsComparisonPage({ + super.key, + required this.eventCode, + required this.leftTeamNumber, + required this.rightTeamNumber, + required this.teamNames, + }); + + String _teamLabel(String teamNumber) { + final nickname = teamNames[teamNumber]; + if (nickname != null && nickname.isNotEmpty) { + return '$teamNumber | $nickname'; + } + return teamNumber; + } + + int _teamNumberAsInt(String teamNumber) { + return int.tryParse(teamNumber) ?? 0; + } + + @override + Widget build(BuildContext context) { + final apiService = Provider.of(context, listen: false); + final cs = Theme.of(context).colorScheme; + + return Scaffold( + appBar: PolarForecastAppBar(extraText: 'Deaths Comparison - $eventCode'), + body: FutureBuilder>( + future: apiService.fetchTournaments(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (!snapshot.hasData) { + return const Center(child: Text('Unable to load tournament data.')); + } + + final tournament = + snapshot.data!.where((t) => t.key == eventCode).firstOrNull; + + if (tournament == null) { + return Center( + child: Text( + 'Could not find event $eventCode.', + style: TextStyle(color: cs.onSurface.withOpacity(0.8)), + ), + ); + } + + return LayoutBuilder( + builder: (context, constraints) { + final sideBySide = constraints.maxWidth >= 900; + final leftPane = _DeathsTeamPanel( + title: _teamLabel(leftTeamNumber), + tournament: tournament, + teamNumber: _teamNumberAsInt(leftTeamNumber), + ); + final rightPane = _DeathsTeamPanel( + title: _teamLabel(rightTeamNumber), + tournament: tournament, + teamNumber: _teamNumberAsInt(rightTeamNumber), + ); + + if (sideBySide) { + return Padding( + padding: const EdgeInsets.all(10), + child: Row( + children: [ + Expanded(child: leftPane), + const SizedBox(width: 10), + Expanded(child: rightPane), + ], + ), + ); + } + + return Padding( + padding: const EdgeInsets.all(10), + child: Column( + children: [ + Expanded(child: leftPane), + const SizedBox(height: 10), + Expanded(child: rightPane), + ], + ), + ); + }, + ); + }, + ), + ); + } +} + +class _DeathsTeamPanel extends StatelessWidget { + final String title; + final Tournament tournament; + final int teamNumber; + + const _DeathsTeamPanel({ + required this.title, + required this.tournament, + required this.teamNumber, + }); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Container( + decoration: BoxDecoration( + color: cs.surfaceVariant.withOpacity(0.12), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: cs.outline.withOpacity(0.18)), + ), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + color: cs.surfaceVariant.withOpacity(0.35), + child: Text( + 'Team $title', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.w800, + color: cs.onSurface, + ), + ), + ), + Expanded( + child: DeathsForm(tournament, teamNumber, true), + ), + ], + ), + ); + } +} + class TeamImagesDialog extends StatefulWidget { final String teamNumber; final int eventYear;