Implement static 6-arrow layout with logical grouping system

- 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 <noreply@anthropic.com>
This commit is contained in:
Mikkeli Matlock
2025-08-23 10:55:58 +09:00
parent a1642c1fe7
commit 75b350f08d
5 changed files with 317 additions and 125 deletions
+2 -9
View File
@@ -33,13 +33,8 @@ class _ScoringPageState extends State<ScoringPage> {
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<ScoringPage> {
padding: const EdgeInsets.all(16.0),
child: ToggleSwitches(
arrowsPerEnd: arrowsPerEnd,
endsPerSet: endsPerSet,
onArrowsPerEndChanged: _onArrowsPerEndChanged,
onEndsPerSetChanged: _onEndsPerSetChanged,
),
),
@@ -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,
),
),
],
),
),
],
);
}
}
@@ -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,
),
),
),
),
),