From 75b350f08dc20bd2ffcbb7f41f781a191a31b63a Mon Sep 17 00:00:00 2001 From: Mikkeli Matlock Date: Sat, 23 Aug 2025 10:55:58 +0900 Subject: [PATCH] Implement static 6-arrow layout with logical grouping system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix dynamic column generation issue that created 6 columns instead of 3 - Implement static 12-row grid with intelligent visual state management - Add visual-to-logical mapping system for proper end/arrow indexing - Fix sum calculations using logical end groupings instead of visual rows - Replace ends per set selector with automatic ruleset inference - Convert to percentage-based responsive layout eliminating viewport overflow - Add clean merged arrow header replacing individual column indices Core improvements: - 3-arrow (Indoor): 10 ends, sum-3 active, sum-6 on even ends - 6-arrow (Outdoor): 6 ends, sum-3 greyed, sum-6 every 2 visual rows - No more RenderFlex overflow errors with responsive column widths - Proper bounds checking prevents range errors on inactive rows - Package identifier updated to com.novoyuuparosk.archery_bookkeeper 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../archery_bookkeeper/MainActivity.kt | 5 - .../archery_bookkeeper/MainActivity.kt | 5 + lib/presentation/pages/scoring_page.dart | 11 +- .../widgets/common/toggle_switches.dart | 56 ++- .../widgets/scoring_grid/scoring_grid.dart | 365 +++++++++++++----- 5 files changed, 317 insertions(+), 125 deletions(-) delete mode 100644 android/app/src/main/kotlin/com/example/archery_bookkeeper/MainActivity.kt create mode 100644 android/app/src/main/kotlin/com/novoyuuparosk/archery_bookkeeper/MainActivity.kt diff --git a/android/app/src/main/kotlin/com/example/archery_bookkeeper/MainActivity.kt b/android/app/src/main/kotlin/com/example/archery_bookkeeper/MainActivity.kt deleted file mode 100644 index 5592689..0000000 --- a/android/app/src/main/kotlin/com/example/archery_bookkeeper/MainActivity.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.archery_bookkeeper - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity : FlutterActivity() diff --git a/android/app/src/main/kotlin/com/novoyuuparosk/archery_bookkeeper/MainActivity.kt b/android/app/src/main/kotlin/com/novoyuuparosk/archery_bookkeeper/MainActivity.kt new file mode 100644 index 0000000..c2c8795 --- /dev/null +++ b/android/app/src/main/kotlin/com/novoyuuparosk/archery_bookkeeper/MainActivity.kt @@ -0,0 +1,5 @@ +package com.novoyuuparosk.archery_bookkeeper + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() \ No newline at end of file diff --git a/lib/presentation/pages/scoring_page.dart b/lib/presentation/pages/scoring_page.dart index 0d3e38b..bdc28ac 100644 --- a/lib/presentation/pages/scoring_page.dart +++ b/lib/presentation/pages/scoring_page.dart @@ -33,13 +33,8 @@ class _ScoringPageState extends State { void _onArrowsPerEndChanged(int arrows) { setState(() { arrowsPerEnd = arrows; - _initializeScores(); - }); - } - - void _onEndsPerSetChanged(int ends) { - setState(() { - endsPerSet = ends; + // Infer ends per set based on ruleset + endsPerSet = arrows == 3 ? 10 : 6; _initializeScores(); }); } @@ -90,9 +85,7 @@ class _ScoringPageState extends State { padding: const EdgeInsets.all(16.0), child: ToggleSwitches( arrowsPerEnd: arrowsPerEnd, - endsPerSet: endsPerSet, onArrowsPerEndChanged: _onArrowsPerEndChanged, - onEndsPerSetChanged: _onEndsPerSetChanged, ), ), diff --git a/lib/presentation/widgets/common/toggle_switches.dart b/lib/presentation/widgets/common/toggle_switches.dart index c10b27f..047e135 100644 --- a/lib/presentation/widgets/common/toggle_switches.dart +++ b/lib/presentation/widgets/common/toggle_switches.dart @@ -2,17 +2,16 @@ import 'package:flutter/material.dart'; class ToggleSwitches extends StatelessWidget { final int arrowsPerEnd; - final int endsPerSet; final Function(int) onArrowsPerEndChanged; - final Function(int) onEndsPerSetChanged; const ToggleSwitches({ super.key, required this.arrowsPerEnd, - required this.endsPerSet, required this.onArrowsPerEndChanged, - required this.onEndsPerSetChanged, }); + + // Inferred ends per set based on ruleset + int get endsPerSet => arrowsPerEnd == 3 ? 10 : 6; @override Widget build(BuildContext context) { @@ -27,12 +26,7 @@ class ToggleSwitches extends StatelessWidget { arrowsPerEnd, onArrowsPerEndChanged, ), - _buildToggleGroup( - 'Ends per set', - [10, 12], - endsPerSet, - onEndsPerSetChanged, - ), + _buildRulesetInfo(), ], ), ], @@ -71,4 +65,46 @@ class ToggleSwitches extends StatelessWidget { ], ); } + + Widget _buildRulesetInfo() { + final rulesetName = arrowsPerEnd == 3 ? 'Indoor' : 'Outdoor'; + return Column( + children: [ + Text( + 'Ruleset', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: Colors.blue.shade50, + border: Border.all(color: Colors.blue.shade200), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + Text( + rulesetName, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.blue.shade700, + ), + ), + Text( + '$endsPerSet ends', + style: TextStyle( + fontSize: 12, + color: Colors.blue.shade600, + ), + ), + ], + ), + ), + ], + ); + } } \ No newline at end of file diff --git a/lib/presentation/widgets/scoring_grid/scoring_grid.dart b/lib/presentation/widgets/scoring_grid/scoring_grid.dart index 9c2eab5..a8bb3ad 100644 --- a/lib/presentation/widgets/scoring_grid/scoring_grid.dart +++ b/lib/presentation/widgets/scoring_grid/scoring_grid.dart @@ -34,13 +34,75 @@ class ScoringGrid extends StatelessWidget { return total; } - int _calculateSumOfSix(int endIndex) { - // Calculate sum of two consecutive ends (for even-numbered ends) - if (endIndex % 2 == 1 && endIndex > 0) { - return _calculateEndTotal(scores[endIndex - 1]) + _calculateEndTotal(scores[endIndex]); + // Visual-to-logical mapping helpers + int _getLogicalEndFromVisualRow(int visualRow) { + if (arrowsPerEnd == 3) { + // 3-arrow mode: 1 visual row = 1 logical end + return visualRow; + } else { + // 6-arrow mode: 2 visual rows = 1 logical end + return visualRow ~/ 2; + } + } + + int _getArrowIndexFromVisualPosition(int visualRow, int column) { + if (arrowsPerEnd == 3) { + // 3-arrow mode: column directly maps to arrow index + return column; + } else { + // 6-arrow mode: row within end determines arrow offset + final rowWithinEnd = visualRow % 2; + return (rowWithinEnd * 3) + column; + } + } + + bool _isVisualRowActive(int visualRow) { + if (arrowsPerEnd == 3) { + return visualRow < 10; // 10 ends for 3-arrow mode + } else { + return visualRow < 12; // All 12 rows active for 6-arrow mode + } + } + + // Fixed sum calculations using logical groupings + int _calculateSumOfThreeForLogicalEnd(int logicalEnd) { + if (logicalEnd >= scores.length) return 0; + + // For 3-arrow mode: sum first 3 arrows + // For 6-arrow mode: sum first 3 arrows (arrows 0-2) + int total = 0; + final maxArrows = arrowsPerEnd == 3 ? 3 : 3; // Always sum first 3 arrows for Sum of 3 + + for (int i = 0; i < maxArrows && i < scores[logicalEnd].length; i++) { + final score = scores[logicalEnd][i]; + if (score == 'X') { + total += 10; + } else if (score == 'M' || score.isEmpty) { + total += 0; + } else { + total += int.tryParse(score) ?? 0; + } + } + return total; + } + + int _calculateSumOfSixForLogicalEnd(int logicalEnd) { + if (arrowsPerEnd == 6) { + // 6-arrow mode: sum all 6 arrows of this logical end + return _calculateEndTotal(scores[logicalEnd]); + } else { + // 3-arrow mode: sum this end + previous end (6 arrows total) + if (logicalEnd % 2 == 1 && logicalEnd > 0) { + return _calculateEndTotal(scores[logicalEnd - 1]) + _calculateEndTotal(scores[logicalEnd]); + } } return 0; } + + // Keep legacy method for accumulative calculation + int _calculateSumOfSix(int endIndex) { + return _calculateSumOfSixForLogicalEnd(endIndex); + } int _calculateAccumulative(int endIndex) { int total = 0; @@ -91,36 +153,79 @@ class ScoringGrid extends StatelessWidget { ), child: Row( children: [ - const SizedBox(width: 50, child: Center(child: Text('End\\Shot', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)))), - ...List.generate(arrowsPerEnd, (index) => - SizedBox(width: 55, child: Center(child: Text('#${index + 1}', style: const TextStyle(fontWeight: FontWeight.bold))))), - const SizedBox(width: 55, child: Center(child: Text('Sum of 3', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)))), - SizedBox( - width: 55, + // End column: ~13% (50/395) + Expanded( + flex: 13, + child: Center(child: Text('End\\Shot', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))), + ), + // Arrow columns: ~42% (165/395) + Expanded( + flex: 42, + child: Center( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.blue.shade100, + borderRadius: BorderRadius.circular(4), + ), + child: const Text('Arrows', style: TextStyle(fontWeight: FontWeight.bold)), + ), + ), + ), + // Sum of 3: ~14% (55/395) + Expanded( + flex: 14, child: Center( child: Text( - 'Sum of 6', + 'Sum of 3', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 12, - color: arrowsPerEnd == 3 ? ScoringColors.getDisabledText() : null, + color: arrowsPerEnd == 6 ? ScoringColors.getDisabledText() : null, ), ), ), ), - const SizedBox(width: 70, child: Center(child: Text('Accumulative', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)))), + // Sum of 6: ~14% (55/395) + Expanded( + flex: 14, + child: Center( + child: Text( + 'Sum of 6', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), + // Accumulative: ~17% (70/395) + Expanded( + flex: 17, + child: Center(child: Text('Accumulative', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))), + ), ], ), ), - // Scoring rows + // Scoring rows - always 12 static rows Expanded( child: ListView.builder( - itemCount: endsPerSet, - itemBuilder: (context, row) { - final endTotal = _calculateEndTotal(scores[row]); - final sumOfSix = _calculateSumOfSix(row); - final accumulative = _calculateAccumulative(row); + itemCount: 12, // Always 12 visual rows regardless of ruleset + itemBuilder: (context, visualRow) { + // Use logical grouping approach + final isActiveRow = _isVisualRowActive(visualRow); + final logicalEnd = isActiveRow ? _getLogicalEndFromVisualRow(visualRow) : -1; + final rowsPerEnd = arrowsPerEnd == 6 ? 2 : 1; + final rowWithinEnd = visualRow % rowsPerEnd; + + // Calculate sums using logical groupings with bounds checking + final sumOfThree = (isActiveRow && logicalEnd >= 0 && logicalEnd < scores.length) + ? _calculateSumOfThreeForLogicalEnd(logicalEnd) : 0; + final sumOfSix = (isActiveRow && logicalEnd >= 0 && logicalEnd < scores.length) + ? _calculateSumOfSixForLogicalEnd(logicalEnd) : 0; + final accumulative = (isActiveRow && logicalEnd >= 0 && logicalEnd < scores.length) + ? _calculateAccumulative(logicalEnd) : 0; return Container( height: 50, @@ -131,97 +236,155 @@ class ScoringGrid extends StatelessWidget { ), child: Row( children: [ - // End number - Container( - width: 50, - padding: const EdgeInsets.symmetric(vertical: 8), - decoration: BoxDecoration( - color: Colors.grey.shade100, - border: Border( - right: BorderSide(color: Colors.grey.shade300), - ), - ), - child: Center( - child: Text( - '#${row + 1}', - style: const TextStyle(fontWeight: FontWeight.w500), - ), - ), - ), - - // Arrow score cells - ...List.generate(arrowsPerEnd, (column) => - SizedBox( - width: 55, - child: ScoringCell( - score: scores[row][column], - isSelected: selectedRow == row && selectedColumn == column, - onTap: () => onCellSelected(row, column), - ), - ), - ), - - // Sum of 3 (end total) - Container( - width: 55, - padding: const EdgeInsets.symmetric(vertical: 8), - decoration: BoxDecoration( - color: Colors.blue.shade50, - border: Border( - left: BorderSide(color: Colors.grey.shade300), - ), - ), - child: Center( - child: Text( - endTotal.toString(), - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - - // Sum of 6 (two-end total, only for even rows) - Container( - width: 55, - padding: const EdgeInsets.symmetric(vertical: 8), - decoration: BoxDecoration( - color: arrowsPerEnd == 3 - ? ScoringColors.getDisabledBackground() - : (row % 2 == 1 ? Colors.yellow.shade100 : Colors.transparent), - border: Border( - left: BorderSide( - color: arrowsPerEnd == 3 - ? ScoringColors.getDisabledBorder() - : Colors.grey.shade300, + // End number (only show on first row of each end) + Expanded( + flex: 13, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: isActiveRow ? Colors.grey.shade100 : ScoringColors.getDisabledBackground(), + border: Border( + right: BorderSide( + color: isActiveRow ? Colors.grey.shade300 : ScoringColors.getDisabledBorder(), + ), ), ), - ), - child: Center( - child: Text( - (arrowsPerEnd == 3 || sumOfSix == 0) ? '' : sumOfSix.toString(), - style: TextStyle( - fontWeight: FontWeight.bold, - color: arrowsPerEnd == 3 - ? ScoringColors.getDisabledText() - : null, + child: Center( + child: Text( + (isActiveRow && rowWithinEnd == 0 && logicalEnd >= 0) ? '#${logicalEnd + 1}' : '', + style: TextStyle( + fontWeight: FontWeight.w500, + color: isActiveRow ? null : ScoringColors.getDisabledText(), + ), ), ), ), ), - // Accumulative (running total) - Container( - width: 70, - padding: const EdgeInsets.symmetric(vertical: 8), - decoration: BoxDecoration( - color: Colors.green.shade50, - border: Border( - left: BorderSide(color: Colors.grey.shade300), + // Arrow cells section - 42% total width split evenly among 3 columns + Expanded( + flex: 42, + child: Row( + children: List.generate(3, (column) { + // Use logical grouping to determine arrow index + final arrowIndex = _getArrowIndexFromVisualPosition(visualRow, column); + final actualRow = logicalEnd; + final actualColumn = arrowIndex; + + // Check if this cell should be active + final cellActive = isActiveRow && arrowIndex < arrowsPerEnd && + logicalEnd >= 0 && logicalEnd < scores.length; + + return Expanded( + child: cellActive ? ScoringCell( + score: scores[actualRow][actualColumn], + isSelected: selectedRow == actualRow && selectedColumn == actualColumn, + onTap: () => onCellSelected(actualRow, actualColumn), + ) : Container( + decoration: BoxDecoration( + color: ScoringColors.getDisabledBackground(), + border: Border.all(color: ScoringColors.getDisabledBorder()), + ), + ), + ); + }), + ), + ), + + // Sum of 3 (end total) - completely greyed in 6-arrow mode + Expanded( + flex: 14, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: (arrowsPerEnd == 6 || !isActiveRow) + ? ScoringColors.getDisabledBackground() + : Colors.blue.shade50, + border: Border( + left: BorderSide( + color: (arrowsPerEnd == 6 || !isActiveRow) + ? ScoringColors.getDisabledBorder() + : Colors.grey.shade300, + ), + ), + ), + child: Center( + child: Text( + // Show Sum of 3 only in 3-arrow mode on the single row per end + (arrowsPerEnd == 6 || !isActiveRow) ? '' : sumOfThree.toString(), + style: TextStyle( + fontWeight: FontWeight.bold, + color: (arrowsPerEnd == 6 || !isActiveRow) + ? ScoringColors.getDisabledText() + : null, + ), + ), ), ), - child: Center( - child: Text( - accumulative.toString(), - style: const TextStyle(fontWeight: FontWeight.bold), + ), + + // Sum of 6 - different logic for 3-arrow vs 6-arrow mode + Expanded( + flex: 14, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: !isActiveRow || visualRow % 2 == 0 + ? ScoringColors.getDisabledBackground() + : Colors.yellow.shade100, + border: Border( + left: BorderSide( + color: !isActiveRow || visualRow % 2 == 0 + ? ScoringColors.getDisabledBorder() + : Colors.grey.shade300, + ), + ), + ), + child: Center( + child: Text( + // Show Sum of 6 on odd visual rows (0-indexed), when sum is available + !isActiveRow || sumOfSix == 0 || visualRow % 2 == 0 + ? '' : sumOfSix.toString(), + style: TextStyle( + fontWeight: FontWeight.bold, + color: !isActiveRow || visualRow % 2 == 0 + ? ScoringColors.getDisabledText() + : null, + ), + ), + ), + ), + ), + + // Accumulative (running total) - show on last row of each end + Expanded( + flex: 17, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: !isActiveRow || (arrowsPerEnd == 6 && rowWithinEnd == 0) + ? ScoringColors.getDisabledBackground() + : Colors.green.shade50, + border: Border( + left: BorderSide( + color: !isActiveRow || (arrowsPerEnd == 6 && rowWithinEnd == 0) + ? ScoringColors.getDisabledBorder() + : Colors.grey.shade300, + ), + ), + ), + child: Center( + child: Text( + // Show accumulative only on last row of each logical end + !isActiveRow || (arrowsPerEnd == 6 && rowWithinEnd == 0) + ? '' : accumulative.toString(), + style: TextStyle( + fontWeight: FontWeight.bold, + color: !isActiveRow || (arrowsPerEnd == 6 && rowWithinEnd == 0) + ? ScoringColors.getDisabledText() + : null, + ), + ), ), ), ),