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
@@ -1,5 +0,0 @@
package com.example.archery_bookkeeper
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,5 @@
package com.novoyuuparosk.archery_bookkeeper
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
+2 -9
View File
@@ -33,13 +33,8 @@ class _ScoringPageState extends State<ScoringPage> {
void _onArrowsPerEndChanged(int arrows) { void _onArrowsPerEndChanged(int arrows) {
setState(() { setState(() {
arrowsPerEnd = arrows; arrowsPerEnd = arrows;
_initializeScores(); // Infer ends per set based on ruleset
}); endsPerSet = arrows == 3 ? 10 : 6;
}
void _onEndsPerSetChanged(int ends) {
setState(() {
endsPerSet = ends;
_initializeScores(); _initializeScores();
}); });
} }
@@ -90,9 +85,7 @@ class _ScoringPageState extends State<ScoringPage> {
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: ToggleSwitches( child: ToggleSwitches(
arrowsPerEnd: arrowsPerEnd, arrowsPerEnd: arrowsPerEnd,
endsPerSet: endsPerSet,
onArrowsPerEndChanged: _onArrowsPerEndChanged, onArrowsPerEndChanged: _onArrowsPerEndChanged,
onEndsPerSetChanged: _onEndsPerSetChanged,
), ),
), ),
@@ -2,17 +2,16 @@ import 'package:flutter/material.dart';
class ToggleSwitches extends StatelessWidget { class ToggleSwitches extends StatelessWidget {
final int arrowsPerEnd; final int arrowsPerEnd;
final int endsPerSet;
final Function(int) onArrowsPerEndChanged; final Function(int) onArrowsPerEndChanged;
final Function(int) onEndsPerSetChanged;
const ToggleSwitches({ const ToggleSwitches({
super.key, super.key,
required this.arrowsPerEnd, required this.arrowsPerEnd,
required this.endsPerSet,
required this.onArrowsPerEndChanged, required this.onArrowsPerEndChanged,
required this.onEndsPerSetChanged,
}); });
// Inferred ends per set based on ruleset
int get endsPerSet => arrowsPerEnd == 3 ? 10 : 6;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -27,12 +26,7 @@ class ToggleSwitches extends StatelessWidget {
arrowsPerEnd, arrowsPerEnd,
onArrowsPerEndChanged, onArrowsPerEndChanged,
), ),
_buildToggleGroup( _buildRulesetInfo(),
'Ends per set',
[10, 12],
endsPerSet,
onEndsPerSetChanged,
),
], ],
), ),
], ],
@@ -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; return total;
} }
int _calculateSumOfSix(int endIndex) { // Visual-to-logical mapping helpers
// Calculate sum of two consecutive ends (for even-numbered ends) int _getLogicalEndFromVisualRow(int visualRow) {
if (endIndex % 2 == 1 && endIndex > 0) { if (arrowsPerEnd == 3) {
return _calculateEndTotal(scores[endIndex - 1]) + _calculateEndTotal(scores[endIndex]); // 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; return 0;
} }
// Keep legacy method for accumulative calculation
int _calculateSumOfSix(int endIndex) {
return _calculateSumOfSixForLogicalEnd(endIndex);
}
int _calculateAccumulative(int endIndex) { int _calculateAccumulative(int endIndex) {
int total = 0; int total = 0;
@@ -91,36 +153,79 @@ class ScoringGrid extends StatelessWidget {
), ),
child: Row( child: Row(
children: [ children: [
const SizedBox(width: 50, child: Center(child: Text('End\\Shot', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)))), // End column: ~13% (50/395)
...List.generate(arrowsPerEnd, (index) => Expanded(
SizedBox(width: 55, child: Center(child: Text('#${index + 1}', style: const TextStyle(fontWeight: FontWeight.bold))))), flex: 13,
const SizedBox(width: 55, child: Center(child: Text('Sum of 3', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)))), child: Center(child: Text('End\\Shot', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))),
SizedBox( ),
width: 55, // 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: Center(
child: Text( child: Text(
'Sum of 6', 'Sum of 3',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 12, 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( Expanded(
child: ListView.builder( child: ListView.builder(
itemCount: endsPerSet, itemCount: 12, // Always 12 visual rows regardless of ruleset
itemBuilder: (context, row) { itemBuilder: (context, visualRow) {
final endTotal = _calculateEndTotal(scores[row]); // Use logical grouping approach
final sumOfSix = _calculateSumOfSix(row); final isActiveRow = _isVisualRowActive(visualRow);
final accumulative = _calculateAccumulative(row); 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( return Container(
height: 50, height: 50,
@@ -131,97 +236,155 @@ class ScoringGrid extends StatelessWidget {
), ),
child: Row( child: Row(
children: [ children: [
// End number // End number (only show on first row of each end)
Container( Expanded(
width: 50, flex: 13,
padding: const EdgeInsets.symmetric(vertical: 8), child: Container(
decoration: BoxDecoration( padding: const EdgeInsets.symmetric(vertical: 8),
color: Colors.grey.shade100, decoration: BoxDecoration(
border: Border( color: isActiveRow ? Colors.grey.shade100 : ScoringColors.getDisabledBackground(),
right: BorderSide(color: Colors.grey.shade300), border: Border(
), right: BorderSide(
), color: isActiveRow ? Colors.grey.shade300 : ScoringColors.getDisabledBorder(),
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,
), ),
), ),
), child: Center(
child: Center( child: Text(
child: Text( (isActiveRow && rowWithinEnd == 0 && logicalEnd >= 0) ? '#${logicalEnd + 1}' : '',
(arrowsPerEnd == 3 || sumOfSix == 0) ? '' : sumOfSix.toString(), style: TextStyle(
style: TextStyle( fontWeight: FontWeight.w500,
fontWeight: FontWeight.bold, color: isActiveRow ? null : ScoringColors.getDisabledText(),
color: arrowsPerEnd == 3 ),
? ScoringColors.getDisabledText()
: null,
), ),
), ),
), ),
), ),
// Accumulative (running total) // Arrow cells section - 42% total width split evenly among 3 columns
Container( Expanded(
width: 70, flex: 42,
padding: const EdgeInsets.symmetric(vertical: 8), child: Row(
decoration: BoxDecoration( children: List.generate(3, (column) {
color: Colors.green.shade50, // Use logical grouping to determine arrow index
border: Border( final arrowIndex = _getArrowIndexFromVisualPosition(visualRow, column);
left: BorderSide(color: Colors.grey.shade300), 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(), // Sum of 6 - different logic for 3-arrow vs 6-arrow mode
style: const TextStyle(fontWeight: FontWeight.bold), 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,
),
),
), ),
), ),
), ),