Implement comprehensive end interaction system with split viewport keypad
This major enhancement transforms the scoring interface into a sophisticated end interaction system that mirrors professional archery scoring workflows. Core behaviors: per-end descending sorting, smart cell selection snapping, active end visual highlighting, cross-end disarm, enhanced keypad pop-up/retract. Split viewport: upper viewport with scrollable scorecard, lower viewport with 30% keypad, 250ms animated transitions, proper separation prevents UI obstruction. Enhanced 4x4 keypad: responsive Expanded widgets, CLEAR and CLOSE buttons, percentage-based sizing eliminates overflow. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,8 @@
|
||||
"Bash(cp:*)",
|
||||
"Bash(flutter analyze:*)",
|
||||
"Bash(flutter build:*)",
|
||||
"Bash(flutter test:*)"
|
||||
"Bash(flutter test:*)",
|
||||
"Bash(flutter run:*)"
|
||||
]
|
||||
},
|
||||
"outputStyle": "koivisto"
|
||||
|
||||
+3
-1
@@ -50,7 +50,9 @@ COMPLETED_*.md
|
||||
INCOMPLETE_*.md
|
||||
|
||||
# User notes - private human thoughts
|
||||
# DO NOT REMOVE FROM GITIGNORE
|
||||
NOTES.txt
|
||||
|
||||
# UI design references. DO NOT REMOVE FROM GITIGNORE
|
||||
# UI design references.
|
||||
# DO NOT REMOVE FROM GITIGNORE
|
||||
.uidesign/
|
||||
@@ -19,10 +19,10 @@ The app follows clean architecture principles with a modular structure ready for
|
||||
### ✅ completed features
|
||||
|
||||
**core UI components**:
|
||||
- `ScoringPage`: Main application interface with settings and layout management
|
||||
- `ScoringPage`: Main application interface with split viewport architecture and animated keypad management
|
||||
- `ScoringGrid`: Complete responsive scorecard with static 12-row layout supporting both 3/6-arrow modes
|
||||
- `ScoringKeypad`: 4x3 grid layout (X,10,9/8,7,6/5,4,3/2,1,M) plus CLEAR button with archery-standard color coding
|
||||
- `ScoringCell`: Individual score input cells with selection highlighting and tap handling
|
||||
- `ScoringKeypad`: Enhanced 4x4 layout with responsive flex architecture and dual-viewport positioning
|
||||
- `ScoringCell`: Individual score input cells with smart selection snapping and active end highlighting
|
||||
- `ToggleSwitches`: Arrow count configuration (3/6 arrows per end) with automatic end inference
|
||||
|
||||
**scoring logic**:
|
||||
@@ -32,7 +32,8 @@ The app follows clean architecture principles with a modular structure ready for
|
||||
- Real-time accumulative running totals with proper bounds checking
|
||||
- X/10/9 counting with dedicated counters
|
||||
- CLEAR functionality for score correction and editing
|
||||
- Auto-advance input progression through scorecard
|
||||
- Sophisticated per-end descending sort (X→10→9→...→1→M) with visual stability
|
||||
- Smart auto-advance with cross-end disarm preventing unintended end transitions
|
||||
|
||||
**visual design & architecture**:
|
||||
- Centralized color system with `ScoringColorScheme` class providing consistent theming
|
||||
@@ -42,6 +43,8 @@ The app follows clean architecture principles with a modular structure ready for
|
||||
- Clean merged header design with "Arrows" label replacing individual column indices
|
||||
- Disabled state colors for conditional UI elements with seamless visual hierarchy
|
||||
- Material 3 theme integration with archery-standard color conventions
|
||||
- Split viewport architecture with animated 250ms transitions and eased curves
|
||||
- Active end visual highlighting with blue glow borders and subtle background tinting
|
||||
|
||||
**technical improvements**:
|
||||
- Flutter lints upgraded to 6.0.0 for modern code quality standards
|
||||
@@ -87,11 +90,14 @@ lib/
|
||||
|
||||
### 🎯 current functionality
|
||||
|
||||
**score input system**:
|
||||
- Tap any cell in the scorecard to select it
|
||||
- Use the keypad to input scores (X, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, M)
|
||||
- Auto-advance moves to the next cell after score input
|
||||
- CLEAR button removes score from selected cell
|
||||
**sophisticated end interaction system**:
|
||||
- Smart cell selection with automatic snapping: empty cell taps redirect to first empty in that end
|
||||
- Active end visual highlighting with blue glow and background tint for entire end group
|
||||
- Keypad pop-up behavior: appears when cell is armed, retracts on completion or manual dismiss
|
||||
- Per-end descending score sorting (X→10→9→...→1→M) maintaining visual stability during input
|
||||
- Cross-end disarm prevents auto-advance between ends, requiring deliberate end transitions
|
||||
- Enhanced 4x4 keypad layout with CLEAR (red) and CLOSE (blue) functional buttons
|
||||
- Background tap dismissal and manual CLOSE button for flexible keypad management
|
||||
|
||||
**calculation engine**:
|
||||
- Real-time end totals with X=10, M=0 scoring and proper bounds checking
|
||||
@@ -107,25 +113,50 @@ lib/
|
||||
- End count automatically inferred: 3-arrow mode = 10 ends (Indoor), 6-arrow mode = 6 ends (Outdoor)
|
||||
- Settings automatically reinitialize scorecard with proper logical grouping
|
||||
|
||||
**split viewport architecture**:
|
||||
- Upper viewport contains settings and scrollable scorecard at dynamic height
|
||||
- Lower viewport shows keypad at 30% screen height when active, 0% when inactive
|
||||
- Smooth 250ms animated transitions with Curves.easeInOut for professional feel
|
||||
- Proper viewport separation prevents UI obstruction during score input
|
||||
- Responsive Expanded flex widgets ensure optimal space utilization
|
||||
|
||||
**adaptive color management**:
|
||||
- Centralized `ScoringColorScheme` architecture with traditional and lowSaturation variants
|
||||
- Intelligent sum of 6 column greying in 3-arrow mode while preserving layout
|
||||
- Disabled state colors maintaining visual hierarchy for inactive elements
|
||||
- Explicit text/background color pairs ensuring optimal contrast across all schemes
|
||||
- Active end highlighting with subtle blue tints and prominent border emphasis
|
||||
|
||||
## next development priorities
|
||||
|
||||
### 🚧 immediate UI improvements (4 remaining)
|
||||
### ✅ comprehensive end interaction system - completed
|
||||
|
||||
**keypad interaction enhancements**:
|
||||
- implement keypad pop-up behavior for better mobile UX
|
||||
- prevent UI push-up when keypad appears on screen
|
||||
- add auto-retract keypad functionality after score input
|
||||
- include manual close button for keypad dismissal
|
||||
**sophisticated end interaction behaviors**:
|
||||
- ✅ per-end descending score sorting (X→10→9→...→1→M) with visual stability
|
||||
- ✅ smart cell selection that snaps empty cell taps to first empty in end
|
||||
- ✅ active end visual highlighting with blue glow and background tint
|
||||
- ✅ keypad pop-up/retract behavior based on cell states
|
||||
- ✅ end completion detection with cross-end disarm (no auto-advance to next end)
|
||||
|
||||
**scorecard polish**:
|
||||
- implement end grouping visual cues to highlight all cells of the currently selected end
|
||||
- implement per-end descending sort for score organization
|
||||
**split viewport layout architecture**:
|
||||
- ✅ upper viewport contains settings and scrollable scorecard
|
||||
- ✅ lower viewport shows keypad at 30% screen height when active, 0% when inactive
|
||||
- ✅ smooth 250ms animated transitions with eased curves
|
||||
- ✅ proper viewport separation prevents UI obstruction
|
||||
|
||||
**enhanced keypad design**:
|
||||
- ✅ 4x4 layout with 4x3 score grid (75% width) + functional column (25% width)
|
||||
- ✅ percentage-based responsive sizing using Expanded flex widgets
|
||||
- ✅ CLEAR button (red) and new CLOSE button (blue) in functional column
|
||||
- ✅ no scrolling needed - all buttons visible within allocated space
|
||||
|
||||
**interaction flow improvements**:
|
||||
- ✅ selecting any cell arms it for input and shows keypad with end highlighting
|
||||
- ✅ empty cell selection automatically snaps to first empty cell in that end
|
||||
- ✅ score input triggers automatic sorting and advances to next empty in same end
|
||||
- ✅ end completion or existing cell modification retracts keypad and disarms
|
||||
- ✅ CLOSE button provides manual keypad dismissal
|
||||
- ✅ background tapping also dismisses keypad
|
||||
|
||||
### 🔧 core architecture improvements
|
||||
|
||||
@@ -280,21 +311,26 @@ The scoring grid exactly matches traditional paper scorecards to ensure familiar
|
||||
- Calculated columns (Sum of 3, Sum of 6, Accumulative) match paper format
|
||||
- X/10/9 counting section at bottom for quick reference
|
||||
|
||||
### keypad design
|
||||
### enhanced keypad design
|
||||
|
||||
Button layout follows the user's exact specifications:
|
||||
- 4x3 grid with logical score arrangement
|
||||
- Color coding matches archery scoring conventions
|
||||
- CLEAR button prominently placed for error correction
|
||||
- Touch-friendly sizing for mobile devices
|
||||
Advanced 4x4 layout with responsive architecture:
|
||||
- 4x3 score grid (75% width) with logical arrangement (X,10,9/8,7,6/5,4,3/2,1,M)
|
||||
- functional column (25% width) with CLEAR (red) and CLOSE (blue) buttons
|
||||
- percentage-based responsive sizing using Expanded flex widgets
|
||||
- color coding matches archery scoring conventions
|
||||
- touch-friendly sizing optimized for mobile devices
|
||||
- split viewport positioning prevents UI obstruction
|
||||
|
||||
### auto-advance behavior
|
||||
### sophisticated end interaction flow
|
||||
|
||||
Score input automatically moves to the next logical cell:
|
||||
- Within an end: moves from arrow #1 to #2 to #3
|
||||
- Between ends: moves to arrow #1 of next end
|
||||
- Reduces tap count and speeds up scoring process
|
||||
- Can be overridden by tapping specific cells
|
||||
Advanced scoring workflow with intelligent behaviors:
|
||||
- smart cell selection: empty cell taps automatically snap to first empty in that end
|
||||
- active end highlighting: entire end group receives blue glow and background tint
|
||||
- auto-advance within ends: moves through arrow #1 to #2 to #3 after score input
|
||||
- cross-end disarm: prevents automatic advance between ends, requiring deliberate transitions
|
||||
- per-end descending sort: scores organize (X→10→9→...→1→M) with visual stability
|
||||
- keypad pop-up/retract: appears on cell arm, retracts on completion or manual dismiss
|
||||
- multiple dismissal methods: CLOSE button, background tap, or automatic on completion
|
||||
|
||||
### calculation accuracy
|
||||
|
||||
@@ -314,9 +350,9 @@ All score calculations follow official archery rules:
|
||||
- `/lib/presentation/pages/scoring_page.dart` - Main scoring interface with state management
|
||||
|
||||
**scoring components**:
|
||||
- `/lib/presentation/widgets/scoring_grid/scoring_grid.dart` - Complete scorecard with calculations
|
||||
- `/lib/presentation/widgets/scoring_grid/scoring_cell.dart` - Individual score input cells
|
||||
- `/lib/presentation/widgets/scoring_grid/scoring_keypad.dart` - 4x3 score input keypad
|
||||
- `/lib/presentation/widgets/scoring_grid/scoring_grid.dart` - Complete scorecard with end highlighting and calculations
|
||||
- `/lib/presentation/widgets/scoring_grid/scoring_cell.dart` - Smart selection cells with snap-to-empty behavior
|
||||
- `/lib/presentation/widgets/scoring_grid/scoring_keypad.dart` - Enhanced 4x4 keypad with split viewport positioning
|
||||
- `/lib/presentation/widgets/common/toggle_switches.dart` - Configuration toggles
|
||||
|
||||
**color architecture**:
|
||||
@@ -375,16 +411,18 @@ All score calculations follow official archery rules:
|
||||
|
||||
## conclusion
|
||||
|
||||
The archery bookkeeper application has evolved into a mature scoring interface with sophisticated visual architecture and polished user experience. The centralized color system, intelligent adaptive behavior, and optimized layouts create a professional-grade foundation that matches traditional paper scorecards while leveraging modern digital capabilities.
|
||||
The archery bookkeeper application has evolved into a sophisticated scoring interface with comprehensive end interaction system and polished user experience. The advanced keypad behaviors, intelligent cell selection, and split viewport architecture create a professional-grade foundation that enhances traditional paper scorecards with modern digital conveniences.
|
||||
|
||||
**current strength highlights**:
|
||||
- Complete centralized color architecture with traditional and lowSaturation schemes
|
||||
- Comprehensive end interaction system with smart selection snapping and visual highlighting
|
||||
- Split viewport architecture preventing UI obstruction with smooth animated transitions
|
||||
- Enhanced 4x4 keypad design with responsive flex layout and dual dismissal methods
|
||||
- Per-end descending score sorting maintaining visual stability during input
|
||||
- Cross-end disarm system preventing unintended end transitions
|
||||
- Complete centralized color architecture with active end highlighting extensions
|
||||
- Robust 6-arrow layout with static 12-row grid and logical-to-visual end mapping
|
||||
- Responsive percentage-based layout eliminating viewport overflow issues
|
||||
- Intelligent column greying maintaining layout consistency in both 3/6-arrow modes
|
||||
- Modern development practices with flutter_lints 6.0.0 and performance-optimized constants
|
||||
- Comprehensive documentation enabling easy maintenance and feature expansion
|
||||
|
||||
The remaining 4 UI improvements focus on keypad interaction polish and end grouping visual cues. With the major 6-arrow layout architecture now complete and stable, the foundation is ready for the next major development phase focusing on data persistence and state management to transform the application from a sophisticated calculator into a complete scoring and session management solution for archery practitioners.
|
||||
With the comprehensive end interaction system now complete, the application provides a sophisticated scoring experience that matches professional archery conventions while offering superior digital usability. The foundation is ready for the next major development phase focusing on data persistence and state management to transform the application from an advanced calculator into a complete scoring and session management solution for archery practitioners.
|
||||
|
||||
The application successfully balances authenticity to archery scoring conventions with modern Flutter development practices, creating a clean, professional foundation ready for advanced features while maintaining code quality and visual consistency throughout.
|
||||
The application successfully balances authenticity to archery scoring conventions with advanced Flutter interaction patterns, creating a polished, professional foundation ready for enterprise features while maintaining exceptional code quality and visual consistency throughout.
|
||||
File diff suppressed because one or more lines are too long
@@ -10,19 +10,37 @@ class ScoringPage extends StatefulWidget {
|
||||
State<ScoringPage> createState() => _ScoringPageState();
|
||||
}
|
||||
|
||||
class _ScoringPageState extends State<ScoringPage> {
|
||||
class _ScoringPageState extends State<ScoringPage> with SingleTickerProviderStateMixin {
|
||||
int arrowsPerEnd = 3;
|
||||
int endsPerSet = 12;
|
||||
int selectedRow = 0;
|
||||
int selectedColumn = 0;
|
||||
List<List<String>> scores = [];
|
||||
bool isKeypadVisible = false;
|
||||
int? activeEndIndex;
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _keypadAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
vsync: this,
|
||||
);
|
||||
_keypadAnimation = CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
_initializeScores();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _initializeScores() {
|
||||
scores = List.generate(
|
||||
endsPerSet,
|
||||
@@ -39,32 +57,141 @@ class _ScoringPageState extends State<ScoringPage> {
|
||||
});
|
||||
}
|
||||
|
||||
void _sortEndScores(int endIndex) {
|
||||
if (endIndex < 0 || endIndex >= scores.length) return;
|
||||
|
||||
List<String> endScores = List.from(scores[endIndex]);
|
||||
|
||||
// Separate empty and non-empty scores
|
||||
List<String> nonEmptyScores = endScores.where((score) => score.isNotEmpty).toList();
|
||||
List<String> emptyScores = endScores.where((score) => score.isEmpty).toList();
|
||||
|
||||
// Sort non-empty scores in descending order (X > 10 > 9 > ... > 1 > M)
|
||||
nonEmptyScores.sort((a, b) {
|
||||
int getScoreValue(String score) {
|
||||
if (score == 'X') return 11;
|
||||
if (score == 'M') return -1;
|
||||
return int.tryParse(score) ?? 0;
|
||||
}
|
||||
return getScoreValue(b).compareTo(getScoreValue(a));
|
||||
});
|
||||
|
||||
// Combine sorted non-empty scores with empty scores
|
||||
List<String> sortedScores = [...nonEmptyScores, ...emptyScores];
|
||||
|
||||
// Update scores array
|
||||
scores[endIndex] = sortedScores;
|
||||
}
|
||||
|
||||
int _findFirstEmptyCellInEnd(int endIndex) {
|
||||
if (endIndex < 0 || endIndex >= scores.length) return 0;
|
||||
|
||||
for (int i = 0; i < arrowsPerEnd; i++) {
|
||||
if (i < scores[endIndex].length && scores[endIndex][i].isEmpty) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0; // Fallback to first cell if all filled
|
||||
}
|
||||
|
||||
void _onCellSelected(int row, int column) {
|
||||
setState(() {
|
||||
selectedRow = row;
|
||||
activeEndIndex = row;
|
||||
|
||||
if (!isKeypadVisible) {
|
||||
isKeypadVisible = true;
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
// If selecting an empty cell, snap to first empty cell in end
|
||||
if (column < scores[row].length && scores[row][column].isEmpty) {
|
||||
selectedColumn = _findFirstEmptyCellInEnd(row);
|
||||
} else {
|
||||
selectedColumn = column;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onBackgroundTapped() {
|
||||
if (isKeypadVisible) {
|
||||
_animationController.reverse().then((_) {
|
||||
setState(() {
|
||||
isKeypadVisible = false;
|
||||
activeEndIndex = null;
|
||||
selectedRow = -1;
|
||||
selectedColumn = -1;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void _onScoreInput(String score) {
|
||||
if (score == 'CLOSE') {
|
||||
// Close keypad - same as background tap
|
||||
_onBackgroundTapped();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedRow < 0 || selectedRow >= scores.length) return;
|
||||
|
||||
if (score == 'CLEAR') {
|
||||
// Clear the selected cell
|
||||
if (selectedRow < scores.length && selectedColumn < scores[selectedRow].length) {
|
||||
setState(() {
|
||||
if (selectedColumn < scores[selectedRow].length && scores[selectedRow][selectedColumn].isNotEmpty) {
|
||||
scores[selectedRow][selectedColumn] = '';
|
||||
_sortEndScores(selectedRow);
|
||||
// Retract keypad and disarm after clearing
|
||||
_animationController.reverse().then((_) {
|
||||
setState(() {
|
||||
isKeypadVisible = false;
|
||||
activeEndIndex = null;
|
||||
selectedRow = -1;
|
||||
selectedColumn = -1;
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Input score
|
||||
if (selectedRow < scores.length && selectedColumn < scores[selectedRow].length) {
|
||||
if (selectedColumn < scores[selectedRow].length) {
|
||||
// Check if this was an empty cell before we modify it
|
||||
bool wasEmpty = scores[selectedRow][selectedColumn].isEmpty;
|
||||
|
||||
setState(() {
|
||||
scores[selectedRow][selectedColumn] = score;
|
||||
// Auto-advance to next cell
|
||||
if (selectedColumn < arrowsPerEnd - 1) {
|
||||
selectedColumn++;
|
||||
} else if (selectedRow < endsPerSet - 1) {
|
||||
selectedRow++;
|
||||
selectedColumn = 0;
|
||||
_sortEndScores(selectedRow);
|
||||
|
||||
if (wasEmpty) {
|
||||
// Find next empty cell in same end
|
||||
int nextEmptyCell = _findFirstEmptyCellInEnd(selectedRow);
|
||||
bool hasMoreEmptyInEnd = nextEmptyCell < arrowsPerEnd &&
|
||||
nextEmptyCell < scores[selectedRow].length &&
|
||||
scores[selectedRow][nextEmptyCell].isEmpty;
|
||||
|
||||
if (hasMoreEmptyInEnd) {
|
||||
// Move to next empty cell in same end
|
||||
selectedColumn = nextEmptyCell;
|
||||
} else {
|
||||
// End is complete - disarm completely
|
||||
_animationController.reverse().then((_) {
|
||||
setState(() {
|
||||
isKeypadVisible = false;
|
||||
activeEndIndex = null;
|
||||
selectedRow = -1;
|
||||
selectedColumn = -1;
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Was editing existing cell - disarm after modification
|
||||
_animationController.reverse().then((_) {
|
||||
setState(() {
|
||||
isKeypadVisible = false;
|
||||
activeEndIndex = null;
|
||||
selectedRow = -1;
|
||||
selectedColumn = -1;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -73,12 +200,31 @@ class _ScoringPageState extends State<ScoringPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _keypadAnimation,
|
||||
builder: (context, child) {
|
||||
// Calculate viewport heights based on animation progress
|
||||
final availableHeight = (MediaQuery.of(context).size.height -
|
||||
MediaQuery.of(context).padding.top -
|
||||
kToolbarHeight).clamp(200.0, double.infinity); // Minimum 200px
|
||||
|
||||
final keypadHeight = (availableHeight * 0.30 * _keypadAnimation.value).clamp(0.0, availableHeight * 0.30);
|
||||
final upperViewportHeight = (availableHeight - keypadHeight).clamp(100.0, availableHeight);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Archery Scoring'),
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
),
|
||||
body: Column(
|
||||
body: GestureDetector(
|
||||
onTap: _onBackgroundTapped,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Column(
|
||||
children: [
|
||||
// Upper viewport - Settings and Scoring grid (scrollable)
|
||||
SizedBox(
|
||||
height: upperViewportHeight,
|
||||
child: Column(
|
||||
children: [
|
||||
// Settings toggles
|
||||
Padding(
|
||||
@@ -89,9 +235,8 @@ class _ScoringPageState extends State<ScoringPage> {
|
||||
),
|
||||
),
|
||||
|
||||
// Scoring grid
|
||||
// Scoring grid (scrollable within upper viewport)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: ScoringGrid(
|
||||
@@ -100,23 +245,42 @@ class _ScoringPageState extends State<ScoringPage> {
|
||||
endsPerSet: endsPerSet,
|
||||
selectedRow: selectedRow,
|
||||
selectedColumn: selectedColumn,
|
||||
activeEndIndex: activeEndIndex,
|
||||
onCellSelected: _onCellSelected,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Scoring keypad
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: ScoringKeypad(
|
||||
onScoreInput: _onScoreInput,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Lower viewport - Keypad (animated height)
|
||||
if (keypadHeight > 0)
|
||||
SizedBox(
|
||||
height: keypadHeight,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
border: Border(
|
||||
top: BorderSide(color: Colors.grey.shade300, width: 1),
|
||||
),
|
||||
),
|
||||
child: Opacity(
|
||||
opacity: _keypadAnimation.value,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: ScoringKeypad(
|
||||
onScoreInput: _onScoreInput,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,14 @@ import '../../../config/constants/scoring_colors.dart';
|
||||
class ScoringCell extends StatelessWidget {
|
||||
final String score;
|
||||
final bool isSelected;
|
||||
final bool isInActiveEnd;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const ScoringCell({
|
||||
super.key,
|
||||
required this.score,
|
||||
required this.isSelected,
|
||||
this.isInActiveEnd = false,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@@ -27,6 +29,20 @@ class ScoringCell extends StatelessWidget {
|
||||
width: ScoringColors.getBorderWidth(isSelected),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
// Add active end highlighting
|
||||
boxShadow: isInActiveEnd ? [
|
||||
BoxShadow(
|
||||
color: Colors.blue.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
] : null,
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
// Add subtle background highlight for active end
|
||||
color: isInActiveEnd ? Colors.blue.withValues(alpha: 0.1) : null,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
@@ -39,6 +55,7 @@ class ScoringCell extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ class ScoringGrid extends StatelessWidget {
|
||||
final int endsPerSet;
|
||||
final int selectedRow;
|
||||
final int selectedColumn;
|
||||
final int? activeEndIndex;
|
||||
final Function(int row, int column) onCellSelected;
|
||||
|
||||
const ScoringGrid({
|
||||
@@ -17,6 +18,7 @@ class ScoringGrid extends StatelessWidget {
|
||||
required this.endsPerSet,
|
||||
required this.selectedRow,
|
||||
required this.selectedColumn,
|
||||
this.activeEndIndex,
|
||||
required this.onCellSelected,
|
||||
});
|
||||
|
||||
@@ -99,9 +101,12 @@ class ScoringGrid extends StatelessWidget {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Keep legacy method for accumulative calculation
|
||||
int _calculateSumOfSix(int endIndex) {
|
||||
return _calculateSumOfSixForLogicalEnd(endIndex);
|
||||
|
||||
bool _isVisualRowInActiveEnd(int visualRow) {
|
||||
if (activeEndIndex == null) return false;
|
||||
|
||||
final logicalEnd = _getLogicalEndFromVisualRow(visualRow);
|
||||
return logicalEnd == activeEndIndex;
|
||||
}
|
||||
|
||||
int _calculateAccumulative(int endIndex) {
|
||||
@@ -279,6 +284,7 @@ class ScoringGrid extends StatelessWidget {
|
||||
child: cellActive ? ScoringCell(
|
||||
score: scores[actualRow][actualColumn],
|
||||
isSelected: selectedRow == actualRow && selectedColumn == actualColumn,
|
||||
isInActiveEnd: _isVisualRowInActiveEnd(visualRow),
|
||||
onTap: () => onCellSelected(actualRow, actualColumn),
|
||||
) : Container(
|
||||
decoration: BoxDecoration(
|
||||
|
||||
@@ -11,51 +11,97 @@ class ScoringKeypad extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Keypad layout matching user's design: 4 rows × 3 columns + CLEAR
|
||||
final List<List<String>> keypadLayout = [
|
||||
// Score buttons: 4 rows × 3 columns
|
||||
final List<List<String>> scoreLayout = [
|
||||
['X', '10', '9'],
|
||||
['8', '7', '6'],
|
||||
['5', '4', '3'],
|
||||
['2', '1', 'M'],
|
||||
];
|
||||
|
||||
return Column(
|
||||
// Functional buttons: 4 rows × 1 column
|
||||
final List<String> functionalButtons = [
|
||||
'CLEAR',
|
||||
'CLOSE',
|
||||
'', // Reserved for future functions
|
||||
'', // Reserved for future functions
|
||||
];
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// Use Flex widgets for proper percentage distribution
|
||||
return Row(
|
||||
children: [
|
||||
// Main keypad grid (4×3)
|
||||
// Score buttons section (75% width)
|
||||
Expanded(
|
||||
child: GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
childAspectRatio: 1.5,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
flex: 75,
|
||||
child: Column(
|
||||
children: List.generate(4, (rowIndex) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 1),
|
||||
child: Row(
|
||||
children: List.generate(3, (colIndex) {
|
||||
final score = scoreLayout[rowIndex][colIndex];
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 1),
|
||||
child: _buildKeypadButton(context, score),
|
||||
),
|
||||
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),
|
||||
const SizedBox(width: 2),
|
||||
|
||||
// CLEAR button (full width)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: _buildKeypadButton(context, 'CLEAR', isWide: true),
|
||||
// Functional buttons section (25% width)
|
||||
Expanded(
|
||||
flex: 25,
|
||||
child: Column(
|
||||
children: List.generate(4, (index) {
|
||||
final buttonText = functionalButtons[index];
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 1),
|
||||
child: buttonText.isNotEmpty
|
||||
? _buildKeypadButton(context, buttonText, isFunctional: true)
|
||||
: Container(), // Empty container for reserved buttons
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKeypadButton(BuildContext context, String score, {bool isWide = false}) {
|
||||
// Use centralized color configuration for consistency
|
||||
final buttonColor = ScoringColors.getScoreBackgroundColor(score);
|
||||
final textColor = ScoringColors.getScoreTextColor(score);
|
||||
Widget _buildKeypadButton(BuildContext context, String score, {bool isFunctional = false}) {
|
||||
// Use different styling for functional buttons
|
||||
Color buttonColor;
|
||||
Color textColor;
|
||||
|
||||
if (isFunctional) {
|
||||
if (score == 'CLEAR') {
|
||||
buttonColor = Colors.red.shade400;
|
||||
textColor = Colors.white;
|
||||
} else if (score == 'CLOSE') {
|
||||
buttonColor = Colors.blue.shade400;
|
||||
textColor = Colors.white;
|
||||
} else {
|
||||
buttonColor = Colors.grey.shade300;
|
||||
textColor = Colors.grey.shade600;
|
||||
}
|
||||
} else {
|
||||
buttonColor = ScoringColors.getScoreBackgroundColor(score);
|
||||
textColor = ScoringColors.getScoreTextColor(score);
|
||||
}
|
||||
|
||||
return ElevatedButton(
|
||||
onPressed: () => onScoreInput(score),
|
||||
@@ -71,8 +117,9 @@ class ScoringKeypad extends StatelessWidget {
|
||||
child: Text(
|
||||
score,
|
||||
style: TextStyle(
|
||||
fontSize: isWide ? 16 : (score.length > 1 ? 14 : 18),
|
||||
fontWeight: ScoringColors.shouldUseBoldText(score) ? FontWeight.bold : FontWeight.w600,
|
||||
fontSize: isFunctional ? 12 : (score.length > 1 ? 14 : 18),
|
||||
fontWeight: isFunctional ? FontWeight.bold :
|
||||
(ScoringColors.shouldUseBoldText(score) ? FontWeight.bold : FontWeight.w600),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user