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(cp:*)",
|
||||||
"Bash(flutter analyze:*)",
|
"Bash(flutter analyze:*)",
|
||||||
"Bash(flutter build:*)",
|
"Bash(flutter build:*)",
|
||||||
"Bash(flutter test:*)"
|
"Bash(flutter test:*)",
|
||||||
|
"Bash(flutter run:*)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"outputStyle": "koivisto"
|
"outputStyle": "koivisto"
|
||||||
|
|||||||
+3
-1
@@ -50,7 +50,9 @@ COMPLETED_*.md
|
|||||||
INCOMPLETE_*.md
|
INCOMPLETE_*.md
|
||||||
|
|
||||||
# User notes - private human thoughts
|
# User notes - private human thoughts
|
||||||
|
# DO NOT REMOVE FROM GITIGNORE
|
||||||
NOTES.txt
|
NOTES.txt
|
||||||
|
|
||||||
# UI design references. DO NOT REMOVE FROM GITIGNORE
|
# UI design references.
|
||||||
|
# DO NOT REMOVE FROM GITIGNORE
|
||||||
.uidesign/
|
.uidesign/
|
||||||
@@ -19,10 +19,10 @@ The app follows clean architecture principles with a modular structure ready for
|
|||||||
### ✅ completed features
|
### ✅ completed features
|
||||||
|
|
||||||
**core UI components**:
|
**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
|
- `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
|
- `ScoringKeypad`: Enhanced 4x4 layout with responsive flex architecture and dual-viewport positioning
|
||||||
- `ScoringCell`: Individual score input cells with selection highlighting and tap handling
|
- `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
|
- `ToggleSwitches`: Arrow count configuration (3/6 arrows per end) with automatic end inference
|
||||||
|
|
||||||
**scoring logic**:
|
**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
|
- Real-time accumulative running totals with proper bounds checking
|
||||||
- X/10/9 counting with dedicated counters
|
- X/10/9 counting with dedicated counters
|
||||||
- CLEAR functionality for score correction and editing
|
- 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**:
|
**visual design & architecture**:
|
||||||
- Centralized color system with `ScoringColorScheme` class providing consistent theming
|
- 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
|
- Clean merged header design with "Arrows" label replacing individual column indices
|
||||||
- Disabled state colors for conditional UI elements with seamless visual hierarchy
|
- Disabled state colors for conditional UI elements with seamless visual hierarchy
|
||||||
- Material 3 theme integration with archery-standard color conventions
|
- 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**:
|
**technical improvements**:
|
||||||
- Flutter lints upgraded to 6.0.0 for modern code quality standards
|
- Flutter lints upgraded to 6.0.0 for modern code quality standards
|
||||||
@@ -87,11 +90,14 @@ lib/
|
|||||||
|
|
||||||
### 🎯 current functionality
|
### 🎯 current functionality
|
||||||
|
|
||||||
**score input system**:
|
**sophisticated end interaction system**:
|
||||||
- Tap any cell in the scorecard to select it
|
- Smart cell selection with automatic snapping: empty cell taps redirect to first empty in that end
|
||||||
- Use the keypad to input scores (X, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, M)
|
- Active end visual highlighting with blue glow and background tint for entire end group
|
||||||
- Auto-advance moves to the next cell after score input
|
- Keypad pop-up behavior: appears when cell is armed, retracts on completion or manual dismiss
|
||||||
- CLEAR button removes score from selected cell
|
- 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**:
|
**calculation engine**:
|
||||||
- Real-time end totals with X=10, M=0 scoring and proper bounds checking
|
- 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)
|
- End count automatically inferred: 3-arrow mode = 10 ends (Indoor), 6-arrow mode = 6 ends (Outdoor)
|
||||||
- Settings automatically reinitialize scorecard with proper logical grouping
|
- 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**:
|
**adaptive color management**:
|
||||||
- Centralized `ScoringColorScheme` architecture with traditional and lowSaturation variants
|
- Centralized `ScoringColorScheme` architecture with traditional and lowSaturation variants
|
||||||
- Intelligent sum of 6 column greying in 3-arrow mode while preserving layout
|
- Intelligent sum of 6 column greying in 3-arrow mode while preserving layout
|
||||||
- Disabled state colors maintaining visual hierarchy for inactive elements
|
- Disabled state colors maintaining visual hierarchy for inactive elements
|
||||||
- Explicit text/background color pairs ensuring optimal contrast across all schemes
|
- 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
|
## next development priorities
|
||||||
|
|
||||||
### 🚧 immediate UI improvements (4 remaining)
|
### ✅ comprehensive end interaction system - completed
|
||||||
|
|
||||||
**keypad interaction enhancements**:
|
**sophisticated end interaction behaviors**:
|
||||||
- implement keypad pop-up behavior for better mobile UX
|
- ✅ per-end descending score sorting (X→10→9→...→1→M) with visual stability
|
||||||
- prevent UI push-up when keypad appears on screen
|
- ✅ smart cell selection that snaps empty cell taps to first empty in end
|
||||||
- add auto-retract keypad functionality after score input
|
- ✅ active end visual highlighting with blue glow and background tint
|
||||||
- include manual close button for keypad dismissal
|
- ✅ keypad pop-up/retract behavior based on cell states
|
||||||
|
- ✅ end completion detection with cross-end disarm (no auto-advance to next end)
|
||||||
|
|
||||||
**scorecard polish**:
|
**split viewport layout architecture**:
|
||||||
- implement end grouping visual cues to highlight all cells of the currently selected end
|
- ✅ upper viewport contains settings and scrollable scorecard
|
||||||
- implement per-end descending sort for score organization
|
- ✅ 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
|
### 🔧 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
|
- Calculated columns (Sum of 3, Sum of 6, Accumulative) match paper format
|
||||||
- X/10/9 counting section at bottom for quick reference
|
- X/10/9 counting section at bottom for quick reference
|
||||||
|
|
||||||
### keypad design
|
### enhanced keypad design
|
||||||
|
|
||||||
Button layout follows the user's exact specifications:
|
Advanced 4x4 layout with responsive architecture:
|
||||||
- 4x3 grid with logical score arrangement
|
- 4x3 score grid (75% width) with logical arrangement (X,10,9/8,7,6/5,4,3/2,1,M)
|
||||||
- Color coding matches archery scoring conventions
|
- functional column (25% width) with CLEAR (red) and CLOSE (blue) buttons
|
||||||
- CLEAR button prominently placed for error correction
|
- percentage-based responsive sizing using Expanded flex widgets
|
||||||
- Touch-friendly sizing for mobile devices
|
- 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:
|
Advanced scoring workflow with intelligent behaviors:
|
||||||
- Within an end: moves from arrow #1 to #2 to #3
|
- smart cell selection: empty cell taps automatically snap to first empty in that end
|
||||||
- Between ends: moves to arrow #1 of next end
|
- active end highlighting: entire end group receives blue glow and background tint
|
||||||
- Reduces tap count and speeds up scoring process
|
- auto-advance within ends: moves through arrow #1 to #2 to #3 after score input
|
||||||
- Can be overridden by tapping specific cells
|
- 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
|
### 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
|
- `/lib/presentation/pages/scoring_page.dart` - Main scoring interface with state management
|
||||||
|
|
||||||
**scoring components**:
|
**scoring components**:
|
||||||
- `/lib/presentation/widgets/scoring_grid/scoring_grid.dart` - Complete scorecard with calculations
|
- `/lib/presentation/widgets/scoring_grid/scoring_grid.dart` - Complete scorecard with end highlighting and calculations
|
||||||
- `/lib/presentation/widgets/scoring_grid/scoring_cell.dart` - Individual score input cells
|
- `/lib/presentation/widgets/scoring_grid/scoring_cell.dart` - Smart selection cells with snap-to-empty behavior
|
||||||
- `/lib/presentation/widgets/scoring_grid/scoring_keypad.dart` - 4x3 score input keypad
|
- `/lib/presentation/widgets/scoring_grid/scoring_keypad.dart` - Enhanced 4x4 keypad with split viewport positioning
|
||||||
- `/lib/presentation/widgets/common/toggle_switches.dart` - Configuration toggles
|
- `/lib/presentation/widgets/common/toggle_switches.dart` - Configuration toggles
|
||||||
|
|
||||||
**color architecture**:
|
**color architecture**:
|
||||||
@@ -375,16 +411,18 @@ All score calculations follow official archery rules:
|
|||||||
|
|
||||||
## conclusion
|
## 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**:
|
**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
|
- 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
|
- 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();
|
State<ScoringPage> createState() => _ScoringPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ScoringPageState extends State<ScoringPage> {
|
class _ScoringPageState extends State<ScoringPage> with SingleTickerProviderStateMixin {
|
||||||
int arrowsPerEnd = 3;
|
int arrowsPerEnd = 3;
|
||||||
int endsPerSet = 12;
|
int endsPerSet = 12;
|
||||||
int selectedRow = 0;
|
int selectedRow = 0;
|
||||||
int selectedColumn = 0;
|
int selectedColumn = 0;
|
||||||
List<List<String>> scores = [];
|
List<List<String>> scores = [];
|
||||||
|
bool isKeypadVisible = false;
|
||||||
|
int? activeEndIndex;
|
||||||
|
late AnimationController _animationController;
|
||||||
|
late Animation<double> _keypadAnimation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_animationController = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
_keypadAnimation = CurvedAnimation(
|
||||||
|
parent: _animationController,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
_initializeScores();
|
_initializeScores();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_animationController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void _initializeScores() {
|
void _initializeScores() {
|
||||||
scores = List.generate(
|
scores = List.generate(
|
||||||
endsPerSet,
|
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) {
|
void _onCellSelected(int row, int column) {
|
||||||
setState(() {
|
setState(() {
|
||||||
selectedRow = row;
|
selectedRow = row;
|
||||||
selectedColumn = column;
|
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) {
|
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') {
|
if (score == 'CLEAR') {
|
||||||
// Clear the selected cell
|
// Clear the selected cell
|
||||||
if (selectedRow < scores.length && selectedColumn < scores[selectedRow].length) {
|
if (selectedColumn < scores[selectedRow].length && scores[selectedRow][selectedColumn].isNotEmpty) {
|
||||||
setState(() {
|
scores[selectedRow][selectedColumn] = '';
|
||||||
scores[selectedRow][selectedColumn] = '';
|
_sortEndScores(selectedRow);
|
||||||
|
// Retract keypad and disarm after clearing
|
||||||
|
_animationController.reverse().then((_) {
|
||||||
|
setState(() {
|
||||||
|
isKeypadVisible = false;
|
||||||
|
activeEndIndex = null;
|
||||||
|
selectedRow = -1;
|
||||||
|
selectedColumn = -1;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Input score
|
// 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(() {
|
setState(() {
|
||||||
scores[selectedRow][selectedColumn] = score;
|
scores[selectedRow][selectedColumn] = score;
|
||||||
// Auto-advance to next cell
|
_sortEndScores(selectedRow);
|
||||||
if (selectedColumn < arrowsPerEnd - 1) {
|
|
||||||
selectedColumn++;
|
if (wasEmpty) {
|
||||||
} else if (selectedRow < endsPerSet - 1) {
|
// Find next empty cell in same end
|
||||||
selectedRow++;
|
int nextEmptyCell = _findFirstEmptyCellInEnd(selectedRow);
|
||||||
selectedColumn = 0;
|
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,50 +200,87 @@ class _ScoringPageState extends State<ScoringPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return AnimatedBuilder(
|
||||||
appBar: AppBar(
|
animation: _keypadAnimation,
|
||||||
title: const Text('Archery Scoring'),
|
builder: (context, child) {
|
||||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
// Calculate viewport heights based on animation progress
|
||||||
),
|
final availableHeight = (MediaQuery.of(context).size.height -
|
||||||
body: Column(
|
MediaQuery.of(context).padding.top -
|
||||||
children: [
|
kToolbarHeight).clamp(200.0, double.infinity); // Minimum 200px
|
||||||
// Settings toggles
|
|
||||||
Padding(
|
final keypadHeight = (availableHeight * 0.30 * _keypadAnimation.value).clamp(0.0, availableHeight * 0.30);
|
||||||
padding: const EdgeInsets.all(16.0),
|
final upperViewportHeight = (availableHeight - keypadHeight).clamp(100.0, availableHeight);
|
||||||
child: ToggleSwitches(
|
|
||||||
arrowsPerEnd: arrowsPerEnd,
|
return Scaffold(
|
||||||
onArrowsPerEndChanged: _onArrowsPerEndChanged,
|
appBar: AppBar(
|
||||||
|
title: const Text('Archery Scoring'),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||||
|
),
|
||||||
|
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(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: ToggleSwitches(
|
||||||
|
arrowsPerEnd: arrowsPerEnd,
|
||||||
|
onArrowsPerEndChanged: _onArrowsPerEndChanged,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Scoring grid (scrollable within upper viewport)
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: ScoringGrid(
|
||||||
|
scores: scores,
|
||||||
|
arrowsPerEnd: arrowsPerEnd,
|
||||||
|
endsPerSet: endsPerSet,
|
||||||
|
selectedRow: selectedRow,
|
||||||
|
selectedColumn: selectedColumn,
|
||||||
|
activeEndIndex: activeEndIndex,
|
||||||
|
onCellSelected: _onCellSelected,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
// Scoring grid
|
},
|
||||||
Expanded(
|
|
||||||
flex: 2,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: ScoringGrid(
|
|
||||||
scores: scores,
|
|
||||||
arrowsPerEnd: arrowsPerEnd,
|
|
||||||
endsPerSet: endsPerSet,
|
|
||||||
selectedRow: selectedRow,
|
|
||||||
selectedColumn: selectedColumn,
|
|
||||||
onCellSelected: _onCellSelected,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Scoring keypad
|
|
||||||
Expanded(
|
|
||||||
flex: 1,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: ScoringKeypad(
|
|
||||||
onScoreInput: _onScoreInput,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,12 +4,14 @@ import '../../../config/constants/scoring_colors.dart';
|
|||||||
class ScoringCell extends StatelessWidget {
|
class ScoringCell extends StatelessWidget {
|
||||||
final String score;
|
final String score;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
|
final bool isInActiveEnd;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
const ScoringCell({
|
const ScoringCell({
|
||||||
super.key,
|
super.key,
|
||||||
required this.score,
|
required this.score,
|
||||||
required this.isSelected,
|
required this.isSelected,
|
||||||
|
this.isInActiveEnd = false,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -27,14 +29,29 @@ class ScoringCell extends StatelessWidget {
|
|||||||
width: ScoringColors.getBorderWidth(isSelected),
|
width: ScoringColors.getBorderWidth(isSelected),
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(4),
|
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: Center(
|
child: Container(
|
||||||
child: Text(
|
decoration: BoxDecoration(
|
||||||
score,
|
// Add subtle background highlight for active end
|
||||||
style: TextStyle(
|
color: isInActiveEnd ? Colors.blue.withValues(alpha: 0.1) : null,
|
||||||
fontSize: 16,
|
borderRadius: BorderRadius.circular(4),
|
||||||
color: ScoringColors.getScoreTextColor(score),
|
),
|
||||||
fontWeight: ScoringColors.shouldUseBoldText(score) ? FontWeight.bold : FontWeight.normal,
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
score,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: ScoringColors.getScoreTextColor(score),
|
||||||
|
fontWeight: ScoringColors.shouldUseBoldText(score) ? FontWeight.bold : FontWeight.normal,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class ScoringGrid extends StatelessWidget {
|
|||||||
final int endsPerSet;
|
final int endsPerSet;
|
||||||
final int selectedRow;
|
final int selectedRow;
|
||||||
final int selectedColumn;
|
final int selectedColumn;
|
||||||
|
final int? activeEndIndex;
|
||||||
final Function(int row, int column) onCellSelected;
|
final Function(int row, int column) onCellSelected;
|
||||||
|
|
||||||
const ScoringGrid({
|
const ScoringGrid({
|
||||||
@@ -17,6 +18,7 @@ class ScoringGrid extends StatelessWidget {
|
|||||||
required this.endsPerSet,
|
required this.endsPerSet,
|
||||||
required this.selectedRow,
|
required this.selectedRow,
|
||||||
required this.selectedColumn,
|
required this.selectedColumn,
|
||||||
|
this.activeEndIndex,
|
||||||
required this.onCellSelected,
|
required this.onCellSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -99,9 +101,12 @@ class ScoringGrid extends StatelessWidget {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep legacy method for accumulative calculation
|
|
||||||
int _calculateSumOfSix(int endIndex) {
|
bool _isVisualRowInActiveEnd(int visualRow) {
|
||||||
return _calculateSumOfSixForLogicalEnd(endIndex);
|
if (activeEndIndex == null) return false;
|
||||||
|
|
||||||
|
final logicalEnd = _getLogicalEndFromVisualRow(visualRow);
|
||||||
|
return logicalEnd == activeEndIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
int _calculateAccumulative(int endIndex) {
|
int _calculateAccumulative(int endIndex) {
|
||||||
@@ -279,6 +284,7 @@ class ScoringGrid extends StatelessWidget {
|
|||||||
child: cellActive ? ScoringCell(
|
child: cellActive ? ScoringCell(
|
||||||
score: scores[actualRow][actualColumn],
|
score: scores[actualRow][actualColumn],
|
||||||
isSelected: selectedRow == actualRow && selectedColumn == actualColumn,
|
isSelected: selectedRow == actualRow && selectedColumn == actualColumn,
|
||||||
|
isInActiveEnd: _isVisualRowInActiveEnd(visualRow),
|
||||||
onTap: () => onCellSelected(actualRow, actualColumn),
|
onTap: () => onCellSelected(actualRow, actualColumn),
|
||||||
) : Container(
|
) : Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|||||||
@@ -11,51 +11,97 @@ class ScoringKeypad extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Keypad layout matching user's design: 4 rows × 3 columns + CLEAR
|
// Score buttons: 4 rows × 3 columns
|
||||||
final List<List<String>> keypadLayout = [
|
final List<List<String>> scoreLayout = [
|
||||||
['X', '10', '9'],
|
['X', '10', '9'],
|
||||||
['8', '7', '6'],
|
['8', '7', '6'],
|
||||||
['5', '4', '3'],
|
['5', '4', '3'],
|
||||||
['2', '1', 'M'],
|
['2', '1', 'M'],
|
||||||
];
|
];
|
||||||
|
|
||||||
return Column(
|
// Functional buttons: 4 rows × 1 column
|
||||||
children: [
|
final List<String> functionalButtons = [
|
||||||
// Main keypad grid (4×3)
|
'CLEAR',
|
||||||
Expanded(
|
'CLOSE',
|
||||||
child: GridView.builder(
|
'', // Reserved for future functions
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
'', // Reserved for future functions
|
||||||
crossAxisCount: 3,
|
];
|
||||||
childAspectRatio: 1.5,
|
|
||||||
crossAxisSpacing: 8,
|
return LayoutBuilder(
|
||||||
mainAxisSpacing: 8,
|
builder: (context, constraints) {
|
||||||
|
// Use Flex widgets for proper percentage distribution
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
// Score buttons section (75% width)
|
||||||
|
Expanded(
|
||||||
|
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) {
|
const SizedBox(width: 2),
|
||||||
final row = index ~/ 3;
|
|
||||||
final col = index % 3;
|
// Functional buttons section (25% width)
|
||||||
final score = keypadLayout[row][col];
|
Expanded(
|
||||||
return _buildKeypadButton(context, score);
|
flex: 25,
|
||||||
},
|
child: Column(
|
||||||
),
|
children: List.generate(4, (index) {
|
||||||
),
|
final buttonText = functionalButtons[index];
|
||||||
|
return Expanded(
|
||||||
const SizedBox(height: 8),
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 1),
|
||||||
// CLEAR button (full width)
|
child: buttonText.isNotEmpty
|
||||||
SizedBox(
|
? _buildKeypadButton(context, buttonText, isFunctional: true)
|
||||||
width: double.infinity,
|
: Container(), // Empty container for reserved buttons
|
||||||
height: 50,
|
),
|
||||||
child: _buildKeypadButton(context, 'CLEAR', isWide: true),
|
);
|
||||||
),
|
}),
|
||||||
],
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildKeypadButton(BuildContext context, String score, {bool isWide = false}) {
|
Widget _buildKeypadButton(BuildContext context, String score, {bool isFunctional = false}) {
|
||||||
// Use centralized color configuration for consistency
|
// Use different styling for functional buttons
|
||||||
final buttonColor = ScoringColors.getScoreBackgroundColor(score);
|
Color buttonColor;
|
||||||
final textColor = ScoringColors.getScoreTextColor(score);
|
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(
|
return ElevatedButton(
|
||||||
onPressed: () => onScoreInput(score),
|
onPressed: () => onScoreInput(score),
|
||||||
@@ -71,8 +117,9 @@ class ScoringKeypad extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
score,
|
score,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: isWide ? 16 : (score.length > 1 ? 14 : 18),
|
fontSize: isFunctional ? 12 : (score.length > 1 ? 14 : 18),
|
||||||
fontWeight: ScoringColors.shouldUseBoldText(score) ? FontWeight.bold : FontWeight.w600,
|
fontWeight: isFunctional ? FontWeight.bold :
|
||||||
|
(ScoringColors.shouldUseBoldText(score) ? FontWeight.bold : FontWeight.w600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user