diff --git a/frontend/lib/screens/pages/wordle_page.dart b/frontend/lib/screens/pages/wordle_page.dart index 6df484e..d03e75c 100644 --- a/frontend/lib/screens/pages/wordle_page.dart +++ b/frontend/lib/screens/pages/wordle_page.dart @@ -13,18 +13,26 @@ class _WordlePageState extends State with TickerProviderStateMixin { static const int MAX_ATTEMPTS = 6; static const int WORD_LENGTH = 5; - List> grid = List.generate(MAX_ATTEMPTS, (_) => List.filled(WORD_LENGTH, '')); - List> gridStates = List.generate(MAX_ATTEMPTS, (_) => List.filled(WORD_LENGTH, LetterState.empty)); + List> grid = List.generate( + MAX_ATTEMPTS, + (_) => List.filled(WORD_LENGTH, ''), + ); + List> gridStates = List.generate( + MAX_ATTEMPTS, + (_) => List.filled(WORD_LENGTH, LetterState.empty), + ); Map keyboardStates = {}; - + int currentRow = 0; int currentCol = 0; + int animatingRow = -1; bool gameWon = false; bool gameLost = false; - - late List _flipControllers; - late List> _flipAnimations; + + late AnimationController _revealController; + late List> _tileAnimations; late FocusNode _focusNode; + Set _pressedKeys = {}; @override void initState() { @@ -32,7 +40,7 @@ class _WordlePageState extends State with TickerProviderStateMixin { _initializeAnimations(); _initializeKeyboardStates(); _focusNode = FocusNode(); - + // Request focus for keyboard input on web/desktop if (kIsWeb || (!kIsWeb && !Platform.isIOS && !Platform.isAndroid)) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -42,18 +50,24 @@ class _WordlePageState extends State with TickerProviderStateMixin { } void _initializeAnimations() { - _flipControllers = List.generate( + _revealController = AnimationController( + duration: Duration(milliseconds: 1500), + vsync: this, + ); + + _tileAnimations = List.generate( WORD_LENGTH, - (index) => AnimationController( - duration: Duration(milliseconds: 600), - vsync: this, + (index) => Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _revealController, + curve: Interval( + index * 0.1, + (index * 0.1) + 0.5, + curve: Curves.elasticOut, + ), + ), ), ); - _flipAnimations = _flipControllers.map((controller) => - Tween(begin: 0.0, end: 1.0).animate( - CurvedAnimation(parent: controller, curve: Curves.easeInOut) - ) - ).toList(); } void _initializeKeyboardStates() { @@ -64,15 +78,16 @@ class _WordlePageState extends State with TickerProviderStateMixin { @override void dispose() { - for (var controller in _flipControllers) { - controller.dispose(); - } + _revealController.dispose(); _focusNode.dispose(); super.dispose(); } void _addLetter(String letter) { - if (currentCol < WORD_LENGTH && currentRow < MAX_ATTEMPTS && !gameWon && !gameLost) { + if (currentCol < WORD_LENGTH && + currentRow < MAX_ATTEMPTS && + !gameWon && + !gameLost) { setState(() { grid[currentRow][currentCol] = letter; currentCol++; @@ -94,26 +109,23 @@ class _WordlePageState extends State with TickerProviderStateMixin { Future _submitWord() async { if (currentCol == WORD_LENGTH && !gameWon && !gameLost) { String guess = grid[currentRow].join(''); - - // Animate the flip - for (int i = 0; i < WORD_LENGTH; i++) { - await Future.delayed(Duration(milliseconds: 100)); - _flipControllers[i].forward(); - } - - await Future.delayed(Duration(milliseconds: 300)); - List newStates = _evaluateGuess(guess); - + + // Set the states and start animating the current row setState(() { gridStates[currentRow] = newStates; _updateKeyboardStates(guess, newStates); + animatingRow = currentRow; }); - // Reset animations - for (var controller in _flipControllers) { - controller.reset(); - } + // Start the reveal animation + _revealController.reset(); + await _revealController.forward(); + + // Clear the animating row and move to next row + setState(() { + animatingRow = -1; + }); if (guess == TARGET_WORD) { setState(() { @@ -172,7 +184,8 @@ class _WordlePageState extends State with TickerProviderStateMixin { LetterState newState = states[i]; if (currentState == LetterState.correct) continue; - if (currentState == LetterState.present && newState == LetterState.absent) continue; + if (currentState == LetterState.present && newState == LetterState.absent) + continue; keyboardStates[letter] = newState; } @@ -185,14 +198,19 @@ class _WordlePageState extends State with TickerProviderStateMixin { builder: (BuildContext context) { return AlertDialog( title: Text(won ? 'Congratulations!' : 'Game Over'), - content: Text(won ? 'You solved today\'s puzzle!' : 'The word was: $TARGET_WORD'), + content: Text( + won ? 'You solved today\'s puzzle!' : 'The word was: $TARGET_WORD', + ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); _resetGame(); }, - child: Text('Play Again', style: TextStyle(color: Color(0xFF6A4C93))), + child: Text( + 'Play Again', + style: TextStyle(color: Color(0xFF6A4C93)), + ), ), ], ); @@ -203,9 +221,13 @@ class _WordlePageState extends State with TickerProviderStateMixin { void _resetGame() { setState(() { grid = List.generate(MAX_ATTEMPTS, (_) => List.filled(WORD_LENGTH, '')); - gridStates = List.generate(MAX_ATTEMPTS, (_) => List.filled(WORD_LENGTH, LetterState.empty)); + gridStates = List.generate( + MAX_ATTEMPTS, + (_) => List.filled(WORD_LENGTH, LetterState.empty), + ); currentRow = 0; currentCol = 0; + animatingRow = -1; gameWon = false; gameLost = false; _initializeKeyboardStates(); @@ -225,166 +247,176 @@ class _WordlePageState extends State with TickerProviderStateMixin { _focusNode.requestFocus(); } }, - child: KeyboardListener( + child: RawKeyboardListener( focusNode: _focusNode, - onKeyEvent: _handleKeyEvent, + onKey: _handleRawKeyEvent, child: Scaffold( - backgroundColor: Colors.white, - appBar: AppBar( - backgroundColor: Colors.white, - elevation: 0, - automaticallyImplyLeading: false, - centerTitle: true, - title: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'وصال', - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w200, - fontFamily: 'Blaka', - color: Color(0xFF6A4C93), - ), + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + automaticallyImplyLeading: false, + centerTitle: true, + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'وصال', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.w200, + fontFamily: 'Blaka', + color: Color(0xFF6A4C93), + ), + ), + SizedBox(width: 8), + Text( + 'Daily Challenge', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.grey[600], + ), + ), + ], ), - SizedBox(width: 8), - Text( - 'Daily Challenge', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Colors.grey[600], - ), + bottom: PreferredSize( + preferredSize: Size.fromHeight(1), + child: Container(height: 1, color: Colors.grey[200]), ), - ], - ), - bottom: PreferredSize( - preferredSize: Size.fromHeight(1), - child: Container(height: 1, color: Colors.grey[200]), - ), - ), - body: Column( - children: [ - Expanded( - child: Center( - child: Container( - constraints: BoxConstraints(maxWidth: 350), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Game Grid - Container( - padding: EdgeInsets.all(16), - child: Column( - children: List.generate(MAX_ATTEMPTS, (row) { - return Padding( - padding: EdgeInsets.symmetric(vertical: 2), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate(WORD_LENGTH, (col) { - return Container( - margin: EdgeInsets.all(2), - width: 62, - height: 62, - child: AnimatedBuilder( - animation: _flipAnimations[col], - builder: (context, child) { - // Check if this tile should be animating - bool isCurrentRowAndAnimating = row == currentRow && _flipAnimations[col].value > 0; - - // If not animating, show the final state - if (!isCurrentRowAndAnimating) { - return Container( - width: 62, - height: 62, - decoration: BoxDecoration( - border: Border.all( - color: _getBorderColor(row, col), - width: 2, - ), - color: _getTileColor(row, col), - ), - child: Center( - child: Text( - grid[row][col], - style: TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - color: _getTextColor(row, col), + ), + body: Column( + children: [ + Expanded( + child: Center( + child: Container( + constraints: BoxConstraints(maxWidth: 350), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Game Grid + Container( + padding: EdgeInsets.all(16), + child: Column( + children: List.generate(MAX_ATTEMPTS, (row) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(WORD_LENGTH, (col) { + bool shouldAnimate = + row == animatingRow && + gridStates[row][col] != + LetterState.empty; + + return Container( + margin: EdgeInsets.all(2), + width: 62, + height: 62, + child: shouldAnimate + ? AnimatedBuilder( + animation: _tileAnimations[col], + builder: (context, child) { + double scale = + 0.8 + + (0.2 * + _tileAnimations[col] + .value); + return Transform.scale( + scale: scale, + child: Container( + width: 62, + height: 62, + decoration: BoxDecoration( + border: Border.all( + color: + _getBorderColor( + row, + col, + ), + width: 2, + ), + color: + _getAnimatedTileColor( + row, + col, + _tileAnimations[col] + .value, + ), + borderRadius: + BorderRadius.circular( + 4, + ), + ), + child: Center( + child: Text( + grid[row][col], + style: TextStyle( + fontSize: 32, + fontWeight: + FontWeight.bold, + color: _getAnimatedTextColor( + row, + col, + _tileAnimations[col] + .value, + ), + ), + ), + ), + ), + ); + }, + ) + : Container( + width: 62, + height: 62, + decoration: BoxDecoration( + border: Border.all( + color: _getBorderColor( + row, + col, + ), + width: 2, + ), + color: _getTileColor( + row, + col, + ), + borderRadius: + BorderRadius.circular(4), ), - ), - ), - ); - } - - // Animation is running - create flip effect - double progress = _flipAnimations[col].value; - double rotationX = progress * 3.14159; - - // Determine colors to show during animation - bool showFinalColors = progress > 0.5; - Color bgColor = showFinalColors ? _getTileColor(row, col) : Colors.white; - Color textColor = showFinalColors ? _getTextColor(row, col) : Colors.black; - - return Container( - width: 62, - height: 62, - decoration: BoxDecoration( - border: Border.all( - color: _getBorderColor(row, col), - width: 2, - ), - color: Colors.white, - ), - child: Stack( - children: [ - // Animated background that flips - Transform( - alignment: Alignment.center, - transform: Matrix4.identity() - ..setEntry(3, 2, 0.001) - ..rotateX(rotationX), - child: Container( - width: 62, - height: 62, - color: bgColor, - ), - ), - // Text that always stays upright and visible - Center( - child: Text( - grid[row][col], - style: TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - color: textColor, + child: Center( + child: Text( + grid[row][col], + style: TextStyle( + fontSize: 32, + fontWeight: + FontWeight.bold, + color: _getTextColor( + row, + col, + ), + ), ), ), ), - ], - ), - ); - }, - ), - ); - }), - ), - ); - }), - ), + ); + }), + ), + ); + }), + ), + ), + ], ), - ], + ), ), ), - ), + // Keyboard + Container(padding: EdgeInsets.all(8), child: _buildKeyboard()), + ], ), - // Keyboard - Container( - padding: EdgeInsets.all(8), - child: _buildKeyboard(), - ), - ], - ), ), ), ), @@ -407,7 +439,7 @@ class _WordlePageState extends State with TickerProviderStateMixin { children: row.map((key) { bool isSpecial = key == 'ENTER' || key == '⌫'; double width = isSpecial ? 65 : 35; - + return Container( margin: EdgeInsets.symmetric(horizontal: 1.5), width: width, @@ -449,23 +481,19 @@ class _WordlePageState extends State with TickerProviderStateMixin { } } - bool _handleKeyEvent(KeyEvent event) { - if (event is KeyDownEvent) { + void _handleRawKeyEvent(RawKeyEvent event) { + if (event is RawKeyDownEvent) { if (event.logicalKey == LogicalKeyboardKey.enter) { _submitWord(); - return true; } else if (event.logicalKey == LogicalKeyboardKey.backspace) { _deleteLetter(); - return true; - } else if (event.logicalKey.keyLabel.length == 1) { - String key = event.logicalKey.keyLabel.toUpperCase(); + } else if (event.character != null && event.character!.isNotEmpty) { + String key = event.character!.toUpperCase(); if (key.codeUnitAt(0) >= 65 && key.codeUnitAt(0) <= 90) { _addLetter(key); - return true; } } } - return false; } Color _getBorderColor(int row, int col) { @@ -482,7 +510,7 @@ class _WordlePageState extends State with TickerProviderStateMixin { Color _getTileColor(int row, int col) { if (row >= MAX_ATTEMPTS || col >= WORD_LENGTH) return Colors.white; if (row >= currentRow && !gameWon) return Colors.white; - + switch (gridStates[row][col]) { case LetterState.correct: return Color(0xFF6AAE7C); @@ -498,14 +526,53 @@ class _WordlePageState extends State with TickerProviderStateMixin { Color _getTextColor(int row, int col) { if (row >= MAX_ATTEMPTS || col >= WORD_LENGTH) return Colors.black; if (row >= currentRow && !gameWon) return Colors.black; - return gridStates[row][col] == LetterState.empty ? Colors.black : Colors.white; + return gridStates[row][col] == LetterState.empty + ? Colors.black + : Colors.white; + } + + Color _getAnimatedTileColor(int row, int col, double animationValue) { + if (row >= MAX_ATTEMPTS || col >= WORD_LENGTH) return Colors.white; + if (row != animatingRow || gridStates[row][col] == LetterState.empty) { + return _getTileColor(row, col); + } + + // Show color based on animation progress + Color targetColor; + switch (gridStates[row][col]) { + case LetterState.correct: + targetColor = Color(0xFF6AAE7C); + break; + case LetterState.present: + targetColor = Color(0xFFC9B037); + break; + case LetterState.absent: + targetColor = Color(0xFF787C7E); + break; + default: + targetColor = Colors.white; + } + + return Color.lerp(Colors.white, targetColor, animationValue) ?? + Colors.white; + } + + Color _getAnimatedTextColor(int row, int col, double animationValue) { + if (row >= MAX_ATTEMPTS || col >= WORD_LENGTH) return Colors.black; + if (row != animatingRow || gridStates[row][col] == LetterState.empty) { + return _getTextColor(row, col); + } + + // Text color changes with animation + return Color.lerp(Colors.black, Colors.white, animationValue) ?? + Colors.black; } Color _getKeyColor(String key) { if (key == 'ENTER' || key == '⌫') { return Colors.grey[300]!; } - + LetterState state = keyboardStates[key] ?? LetterState.empty; switch (state) { case LetterState.correct: @@ -523,15 +590,10 @@ class _WordlePageState extends State with TickerProviderStateMixin { if (key == 'ENTER' || key == '⌫') { return Colors.black; } - + LetterState state = keyboardStates[key] ?? LetterState.empty; return state == LetterState.empty ? Colors.black : Colors.white; } } -enum LetterState { - empty, - absent, - present, - correct, -} \ No newline at end of file +enum LetterState { empty, absent, present, correct }