Restructured to use Flutter. Mockup UIs

This commit is contained in:
Mikkeli Matlock
2025-08-22 23:42:12 +09:00
parent ae746c23f4
commit 631a186dfd
143 changed files with 5911 additions and 21548 deletions
@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
class ScoringCell extends StatelessWidget {
final String score;
final bool isSelected;
final VoidCallback onTap;
const ScoringCell({
super.key,
required this.score,
required this.isSelected,
required this.onTap,
});
Color _getScoreColor(String score) {
switch (score) {
case 'X':
return Colors.gold;
case '10':
return Colors.gold.shade200;
case '9':
return Colors.yellow.shade200;
case '8':
return Colors.blue.shade200;
case '7':
return Colors.blue.shade100;
default:
return Colors.transparent;
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: _getScoreColor(score),
border: Border.all(
color: isSelected ? Colors.blue : Colors.grey.shade400,
width: isSelected ? 2 : 1,
),
borderRadius: BorderRadius.circular(4),
),
child: Center(
child: Text(
score,
style: TextStyle(
fontSize: 16,
fontWeight: score == 'X' || score == '10' ? FontWeight.bold : FontWeight.normal,
),
),
),
),
);
}
}
@@ -0,0 +1,314 @@
import 'package:flutter/material.dart';
import 'scoring_cell.dart';
class ScoringGrid extends StatelessWidget {
final List<List<String>> scores;
final int arrowsPerEnd;
final int endsPerSet;
final int selectedRow;
final int selectedColumn;
final Function(int row, int column) onCellSelected;
const ScoringGrid({
super.key,
required this.scores,
required this.arrowsPerEnd,
required this.endsPerSet,
required this.selectedRow,
required this.selectedColumn,
required this.onCellSelected,
});
int _calculateEndTotal(List<String> endScores) {
int total = 0;
for (String score in endScores) {
if (score == 'X') {
total += 10;
} else if (score == 'M' || score.isEmpty) {
total += 0;
} else {
total += int.tryParse(score) ?? 0;
}
}
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]);
}
return 0;
}
int _calculateAccumulative(int endIndex) {
int total = 0;
for (int i = 0; i <= endIndex && i < scores.length; i++) {
total += _calculateEndTotal(scores[i]);
}
return total;
}
Map<String, int> _calculateXAndTenCounts() {
int xCount = 0;
int tenCount = 0;
int nineCount = 0;
for (var end in scores) {
for (var score in end) {
if (score == 'X') {
xCount++;
} else if (score == '10') {
tenCount++;
} else if (score == '9') {
nineCount++;
}
}
}
return {'X': xCount, '10': tenCount, '9': nineCount};
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
// Header row
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
),
child: Row(
children: [
const SizedBox(width: 60, child: Center(child: Text('End\\Shot', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)))),
...List.generate(arrowsPerEnd, (index) =>
Expanded(child: Center(child: Text('#${index + 1}', style: const TextStyle(fontWeight: FontWeight.bold))))),
const SizedBox(width: 70, child: Center(child: Text('Sum of 3', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)))),
const SizedBox(width: 70, child: Center(child: Text('Sum of 6', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)))),
const SizedBox(width: 80, child: Center(child: Text('Accumulative', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)))),
],
),
),
// Scoring rows
Expanded(
child: ListView.builder(
itemCount: endsPerSet,
itemBuilder: (context, row) {
final endTotal = _calculateEndTotal(scores[row]);
final sumOfSix = _calculateSumOfSix(row);
final accumulative = _calculateAccumulative(row);
return Container(
height: 50,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.grey.shade300),
),
),
child: Row(
children: [
// End number
Container(
width: 60,
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) =>
Expanded(
child: ScoringCell(
score: scores[row][column],
isSelected: selectedRow == row && selectedColumn == column,
onTap: () => onCellSelected(row, column),
),
),
),
// Sum of 3 (end total)
Container(
width: 70,
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: 70,
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: row % 2 == 1 ? Colors.yellow.shade100 : Colors.transparent,
border: Border(
left: BorderSide(color: Colors.grey.shade300),
),
),
child: Center(
child: Text(
sumOfSix > 0 ? sumOfSix.toString() : '',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
),
// Accumulative (running total)
Container(
width: 80,
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Colors.green.shade50,
border: Border(
left: BorderSide(color: Colors.grey.shade300),
),
),
child: Center(
child: Text(
accumulative.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
),
],
),
);
},
),
),
// X/10/9 counting and final score section
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade50,
border: Border(
top: BorderSide(color: Colors.grey.shade400, width: 2),
),
),
child: _buildCountingSection(),
),
],
),
);
}
Widget _buildCountingSection() {
final counts = _calculateXAndTenCounts();
final finalScore = _calculateAccumulative(scores.length - 1);
return Row(
children: [
// X count
Container(
width: 60,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.amber.shade100,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('X', style: TextStyle(fontWeight: FontWeight.bold)),
Text(counts['X'].toString(), style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
),
const SizedBox(width: 8),
// 10 count
Container(
width: 60,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.amber.shade100,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('10', style: TextStyle(fontWeight: FontWeight.bold)),
Text(counts['10'].toString(), style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
),
const SizedBox(width: 8),
// 9 count
Container(
width: 60,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.yellow.shade100,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('9', style: TextStyle(fontWeight: FontWeight.bold)),
Text(counts['9'].toString(), style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
),
const Spacer(),
// Final score
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.shade100,
border: Border.all(color: Colors.grey.shade400, width: 2),
borderRadius: BorderRadius.circular(8),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Final score', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
Text(
finalScore.toString(),
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24),
),
],
),
),
],
);
}
}
@@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
class ScoringKeypad extends StatelessWidget {
final Function(String) onScoreInput;
const ScoringKeypad({
super.key,
required this.onScoreInput,
});
@override
Widget build(BuildContext context) {
// Keypad layout matching user's design: 4 rows × 3 columns + CLEAR
final List<List<String>> keypadLayout = [
['X', '10', '9'],
['8', '7', '6'],
['5', '4', '3'],
['2', '1', 'M'],
];
return Column(
children: [
// Main keypad grid (4×3)
Expanded(
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.5,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: 12,
itemBuilder: (context, index) {
final row = index ~/ 3;
final col = index % 3;
final score = keypadLayout[row][col];
return _buildKeypadButton(context, score);
},
),
),
const SizedBox(height: 8),
// CLEAR button (full width)
SizedBox(
width: double.infinity,
height: 50,
child: _buildKeypadButton(context, 'CLEAR', isWide: true),
),
],
);
}
Widget _buildKeypadButton(BuildContext context, String score, {bool isWide = false}) {
Color buttonColor;
Color textColor = Colors.black;
// Color scheme matching your design
switch (score) {
case 'X':
buttonColor = Colors.amber.shade300;
break;
case '10':
buttonColor = Colors.amber.shade300;
break;
case '9':
buttonColor = Colors.yellow.shade300;
break;
case '8':
buttonColor = Colors.red.shade400;
textColor = Colors.white;
break;
case '7':
buttonColor = Colors.red.shade400;
textColor = Colors.white;
break;
case '6':
buttonColor = Colors.blue.shade400;
textColor = Colors.white;
break;
case '5':
buttonColor = Colors.blue.shade400;
textColor = Colors.white;
break;
case '4':
buttonColor = Colors.black87;
textColor = Colors.white;
break;
case '3':
buttonColor = Colors.black87;
textColor = Colors.white;
break;
case 'M':
buttonColor = Colors.grey.shade400;
break;
case 'CLEAR':
buttonColor = Colors.orange.shade400;
textColor = Colors.white;
break;
default:
buttonColor = Colors.grey.shade300;
break;
}
return ElevatedButton(
onPressed: () => onScoreInput(score),
style: ElevatedButton.styleFrom(
backgroundColor: buttonColor,
foregroundColor: textColor,
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.zero,
),
child: Text(
score,
style: TextStyle(
fontSize: isWide ? 16 : (score.length > 1 ? 14 : 18),
fontWeight: FontWeight.bold,
),
),
);
}
}