Merge pull request #14 from sBubshait/feature/puzzles

Feature/puzzles
This commit is contained in:
Saleh Bubshait 2025-08-03 13:58:54 +03:00 committed by GitHub
commit 8104411066
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1252 additions and 363 deletions

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'pages/feed_page.dart'; import 'pages/feed_page.dart';
import 'pages/invitations_page.dart'; import 'pages/invitations_page.dart';
import 'pages/profile_page.dart'; import 'pages/profile_page.dart';
import 'pages/wordle_page.dart';
import '../services/invitations_service.dart'; import '../services/invitations_service.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
@ -13,7 +14,12 @@ class _HomeScreenState extends State<HomeScreen> {
int _currentIndex = 0; int _currentIndex = 0;
int _availableInvitationsCount = 0; int _availableInvitationsCount = 0;
final List<Widget> _pages = [FeedPage(), InvitationsPage(), ProfilePage()]; final List<Widget> _pages = [
FeedPage(),
InvitationsPage(),
WordlePage(),
ProfilePage(),
];
@override @override
void initState() { void initState() {
@ -56,12 +62,11 @@ class _HomeScreenState extends State<HomeScreen> {
color: Colors.red, color: Colors.red,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
constraints: BoxConstraints( constraints: BoxConstraints(minWidth: 16, minHeight: 16),
minWidth: 16,
minHeight: 16,
),
child: Text( child: Text(
_availableInvitationsCount > 99 ? '99+' : '$_availableInvitationsCount', _availableInvitationsCount > 99
? '99+'
: '$_availableInvitationsCount',
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 10, fontSize: 10,
@ -108,12 +113,22 @@ class _HomeScreenState extends State<HomeScreen> {
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
elevation: 0, elevation: 0,
items: [ items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Feed'), BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Feed',
),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: _buildInvitationsBadge(), icon: _buildInvitationsBadge(),
label: 'Invitations', label: 'Invitations',
), ),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'), BottomNavigationBarItem(
icon: Icon(Icons.games),
label: 'Puzzles',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Profile',
),
], ],
), ),
), ),

View File

@ -3,6 +3,11 @@ import '../../models/invitation_models.dart';
import '../../services/invitations_service.dart'; import '../../services/invitations_service.dart';
import '../../services/user_service.dart'; import '../../services/user_service.dart';
import '../../utils/invitation_utils.dart'; import '../../utils/invitation_utils.dart';
import '../../widgets/whatsapp_button.dart';
// WhatsApp configuration
const String _whatsappNumber = 'PHONE_NUMBER';
const bool _showWhatsappButton = true; // Set to false for production
class InvitationDetailsPage extends StatefulWidget { class InvitationDetailsPage extends StatefulWidget {
final int invitationId; final int invitationId;
@ -41,7 +46,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
_errorMessage = null; _errorMessage = null;
}); });
final result = await InvitationsService.getInvitationDetails(widget.invitationId); final result = await InvitationsService.getInvitationDetails(
widget.invitationId,
);
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -79,7 +86,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
return Center( return Center(
child: Text( child: Text(
displayName.isNotEmpty ? displayName[0].toUpperCase() : '?', displayName.isNotEmpty
? displayName[0].toUpperCase()
: '?',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -132,7 +141,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
_isCancelling = true; _isCancelling = true;
}); });
final result = await InvitationsService.cancelInvitation(widget.invitationId); final result = await InvitationsService.cancelInvitation(
widget.invitationId,
);
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -143,7 +154,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
Navigator.of(context).pop(true); Navigator.of(context).pop(true);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(result['message'] ?? 'Action completed successfully'), content: Text(
result['message'] ?? 'Action completed successfully',
),
backgroundColor: Colors.green, backgroundColor: Colors.green,
), ),
); );
@ -165,7 +178,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
final userResult = await UserService.getCurrentUser(); final userResult = await UserService.getCurrentUser();
if (userResult['success'] && userResult['data'] != null) { if (userResult['success'] && userResult['data'] != null) {
final currentUserId = userResult['data']['id']; final currentUserId = userResult['data']['id'];
final isParticipant = _invitationDetails!.attendees.any((attendee) => attendee.id == currentUserId); final isParticipant = _invitationDetails!.attendees.any(
(attendee) => attendee.id == currentUserId,
);
setState(() { setState(() {
_isCurrentlyParticipant = isParticipant; _isCurrentlyParticipant = isParticipant;
}); });
@ -177,7 +192,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
_isAccepting = true; _isAccepting = true;
}); });
final result = await InvitationsService.acceptInvitation(widget.invitationId); final result = await InvitationsService.acceptInvitation(
widget.invitationId,
);
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -225,9 +242,7 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
), ),
), ),
body: _isLoading body: _isLoading
? Center( ? Center(child: CircularProgressIndicator(color: Color(0xFF6A4C93)))
child: CircularProgressIndicator(color: Color(0xFF6A4C93)),
)
: _errorMessage != null : _errorMessage != null
? Center( ? Center(
child: Column( child: Column(
@ -309,7 +324,10 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
), ),
Container( Container(
margin: EdgeInsets.only(top: 8), margin: EdgeInsets.only(top: 8),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: InvitationUtils.getColorFromHex( color: InvitationUtils.getColorFromHex(
_invitationDetails!.tag.colorHex, _invitationDetails!.tag.colorHex,
@ -321,8 +339,11 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: InvitationUtils.getColorFromHex( color:
_invitationDetails!.tag.colorHex, InvitationUtils.getColorFromHex(
_invitationDetails!
.tag
.colorHex,
), ),
), ),
), ),
@ -331,9 +352,13 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
), ),
), ),
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: _invitationDetails!.status == 'Available' color:
_invitationDetails!.status == 'Available'
? Colors.green.withOpacity(0.1) ? Colors.green.withOpacity(0.1)
: Colors.orange.withOpacity(0.1), : Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@ -343,7 +368,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: _invitationDetails!.status == 'Available' color:
_invitationDetails!.status ==
'Available'
? Colors.green[700] ? Colors.green[700]
: Colors.orange[700], : Colors.orange[700],
), ),
@ -379,12 +406,19 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
Expanded( Expanded(
child: Row( child: Row(
children: [ children: [
Icon(Icons.location_on, size: 20, color: Colors.grey[600]), Icon(
Icons.location_on,
size: 20,
color: Colors.grey[600],
),
SizedBox(width: 8), SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
_invitationDetails!.location!, _invitationDetails!.location!,
style: TextStyle(fontSize: 14, color: Colors.grey[700]), style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
@ -393,18 +427,26 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
), ),
], ],
if (_invitationDetails!.dateTime != null) ...[ if (_invitationDetails!.dateTime != null) ...[
if (_invitationDetails!.location != null) SizedBox(width: 16), if (_invitationDetails!.location != null)
SizedBox(width: 16),
Expanded( Expanded(
child: Row( child: Row(
children: [ children: [
Icon(Icons.schedule, size: 20, color: Colors.grey[600]), Icon(
Icons.schedule,
size: 20,
color: Colors.grey[600],
),
SizedBox(width: 8), SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
InvitationUtils.getRelativeDateTime( InvitationUtils.getRelativeDateTime(
_invitationDetails!.dateTime!, _invitationDetails!.dateTime!,
), ),
style: TextStyle(fontSize: 14, color: Colors.grey[700]), style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
@ -417,11 +459,18 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
SizedBox(height: 16), SizedBox(height: 16),
Row( Row(
children: [ children: [
Icon(Icons.people, size: 20, color: Colors.grey[600]), Icon(
Icons.people,
size: 20,
color: Colors.grey[600],
),
SizedBox(width: 8), SizedBox(width: 8),
Text( Text(
'${_invitationDetails!.currentAttendees} / ${_invitationDetails!.maxParticipants} participants', '${_invitationDetails!.currentAttendees} / ${_invitationDetails!.maxParticipants} participants',
style: TextStyle(fontSize: 14, color: Colors.grey[700]), style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
), ),
], ],
), ),
@ -458,7 +507,10 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
), ),
SizedBox(width: 8), SizedBox(width: 8),
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2), padding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Color(0xFF6A4C93).withOpacity(0.1), color: Color(0xFF6A4C93).withOpacity(0.1),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
@ -482,7 +534,8 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
_invitationDetails!.creator.avatar, _invitationDetails!.creator.avatar,
), ),
SizedBox(width: 12), SizedBox(width: 12),
Text( Expanded(
child: Text(
_invitationDetails!.creator.displayName, _invitationDetails!.creator.displayName,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
@ -490,6 +543,15 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
color: Colors.black87, color: Colors.black87,
), ),
), ),
),
if ((widget.isOwner || _isCurrentlyParticipant) &&
_showWhatsappButton) ...[
SizedBox(width: 8),
WhatsAppButton(
phoneNumber: _whatsappNumber,
size: 28,
),
],
], ],
), ),
], ],
@ -523,10 +585,22 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
), ),
), ),
SizedBox(height: 16), SizedBox(height: 16),
...List.generate(_invitationDetails!.attendees.length, (index) { ...List.generate(
final attendee = _invitationDetails!.attendees[index]; _invitationDetails!.attendees.length,
(index) {
final attendee =
_invitationDetails!.attendees[index];
return Container( return Container(
margin: EdgeInsets.only(bottom: index < _invitationDetails!.attendees.length - 1 ? 12 : 0), margin: EdgeInsets.only(
bottom:
index <
_invitationDetails!
.attendees
.length -
1
? 12
: 0,
),
child: Row( child: Row(
children: [ children: [
_buildAvatarOrInitial( _buildAvatarOrInitial(
@ -536,7 +610,8 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
SizedBox(width: 12), SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment:
CrossAxisAlignment.start,
children: [ children: [
Text( Text(
attendee.displayName, attendee.displayName,
@ -556,10 +631,20 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
], ],
), ),
), ),
if ((widget.isOwner ||
_isCurrentlyParticipant) &&
_showWhatsappButton) ...[
SizedBox(width: 8),
WhatsAppButton(
phoneNumber: _whatsappNumber,
size: 24,
),
],
], ],
), ),
); );
}), },
),
], ],
), ),
), ),
@ -587,7 +672,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
width: 20, width: 20,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white), valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
), ),
) )
: Text( : Text(
@ -623,11 +710,15 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
width: 20, width: 20,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white), valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
), ),
) )
: Text( : Text(
widget.isOwner ? 'Cancel Invitation' : 'Leave Invitation', widget.isOwner
? 'Cancel Invitation'
: 'Leave Invitation',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,

View File

@ -0,0 +1,599 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';
import 'dart:io';
class WordlePage extends StatefulWidget {
@override
_WordlePageState createState() => _WordlePageState();
}
class _WordlePageState extends State<WordlePage> with TickerProviderStateMixin {
static const String TARGET_WORD = "HELLO";
static const int MAX_ATTEMPTS = 6;
static const int WORD_LENGTH = 5;
List<List<String>> grid = List.generate(
MAX_ATTEMPTS,
(_) => List.filled(WORD_LENGTH, ''),
);
List<List<LetterState>> gridStates = List.generate(
MAX_ATTEMPTS,
(_) => List.filled(WORD_LENGTH, LetterState.empty),
);
Map<String, LetterState> keyboardStates = {};
int currentRow = 0;
int currentCol = 0;
int animatingRow = -1;
bool gameWon = false;
bool gameLost = false;
late AnimationController _revealController;
late List<Animation<double>> _tileAnimations;
late FocusNode _focusNode;
Set<LogicalKeyboardKey> _pressedKeys = <LogicalKeyboardKey>{};
@override
void initState() {
super.initState();
_initializeAnimations();
_initializeKeyboardStates();
_focusNode = FocusNode();
// Request focus for keyboard input on web/desktop
if (kIsWeb || (!kIsWeb && !Platform.isIOS && !Platform.isAndroid)) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
}
}
void _initializeAnimations() {
_revealController = AnimationController(
duration: Duration(milliseconds: 1500),
vsync: this,
);
_tileAnimations = List.generate(
WORD_LENGTH,
(index) => Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _revealController,
curve: Interval(
index * 0.1,
(index * 0.1) + 0.5,
curve: Curves.elasticOut,
),
),
),
);
}
void _initializeKeyboardStates() {
for (int i = 65; i <= 90; i++) {
keyboardStates[String.fromCharCode(i)] = LetterState.empty;
}
}
@override
void dispose() {
_revealController.dispose();
_focusNode.dispose();
super.dispose();
}
void _addLetter(String letter) {
if (currentCol < WORD_LENGTH &&
currentRow < MAX_ATTEMPTS &&
!gameWon &&
!gameLost) {
setState(() {
grid[currentRow][currentCol] = letter;
currentCol++;
});
HapticFeedback.lightImpact();
}
}
void _deleteLetter() {
if (currentCol > 0 && !gameWon && !gameLost) {
setState(() {
currentCol--;
grid[currentRow][currentCol] = '';
});
HapticFeedback.lightImpact();
}
}
Future<void> _submitWord() async {
if (currentCol == WORD_LENGTH && !gameWon && !gameLost) {
String guess = grid[currentRow].join('');
List<LetterState> newStates = _evaluateGuess(guess);
// Set the states and start animating the current row
setState(() {
gridStates[currentRow] = newStates;
_updateKeyboardStates(guess, newStates);
animatingRow = currentRow;
});
// 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(() {
gameWon = true;
});
HapticFeedback.heavyImpact();
_showGameEndDialog(true);
} else if (currentRow == MAX_ATTEMPTS - 1) {
setState(() {
gameLost = true;
});
HapticFeedback.heavyImpact();
_showGameEndDialog(false);
} else {
setState(() {
currentRow++;
currentCol = 0;
});
}
}
}
List<LetterState> _evaluateGuess(String guess) {
List<LetterState> states = List.filled(WORD_LENGTH, LetterState.absent);
List<String> targetLetters = TARGET_WORD.split('');
List<bool> used = List.filled(WORD_LENGTH, false);
// First pass: check for correct positions
for (int i = 0; i < WORD_LENGTH; i++) {
if (guess[i] == targetLetters[i]) {
states[i] = LetterState.correct;
used[i] = true;
}
}
// Second pass: check for present letters
for (int i = 0; i < WORD_LENGTH; i++) {
if (states[i] != LetterState.correct) {
for (int j = 0; j < WORD_LENGTH; j++) {
if (!used[j] && guess[i] == targetLetters[j]) {
states[i] = LetterState.present;
used[j] = true;
break;
}
}
}
}
return states;
}
void _updateKeyboardStates(String guess, List<LetterState> states) {
for (int i = 0; i < guess.length; i++) {
String letter = guess[i];
LetterState currentState = keyboardStates[letter] ?? LetterState.empty;
LetterState newState = states[i];
if (currentState == LetterState.correct) continue;
if (currentState == LetterState.present && newState == LetterState.absent)
continue;
keyboardStates[letter] = newState;
}
}
void _showGameEndDialog(bool won) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: Text(won ? 'Congratulations!' : 'Game Over'),
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)),
),
),
],
);
},
);
}
void _resetGame() {
setState(() {
grid = List.generate(MAX_ATTEMPTS, (_) => List.filled(WORD_LENGTH, ''));
gridStates = List.generate(
MAX_ATTEMPTS,
(_) => List.filled(WORD_LENGTH, LetterState.empty),
);
currentRow = 0;
currentCol = 0;
animatingRow = -1;
gameWon = false;
gameLost = false;
_initializeKeyboardStates();
});
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
child: GestureDetector(
onTap: () {
// Prevent mobile keyboard from showing up
FocusScope.of(context).unfocus();
// Only request focus on web/desktop platforms
if (kIsWeb || (!kIsWeb && !Platform.isIOS && !Platform.isAndroid)) {
_focusNode.requestFocus();
}
},
child: RawKeyboardListener(
focusNode: _focusNode,
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),
),
),
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]),
),
),
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),
),
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()),
],
),
),
),
),
);
}
Widget _buildKeyboard() {
List<List<String>> keyboardLayout = [
['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'],
['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'],
['ENTER', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ''],
];
return Column(
children: keyboardLayout.map((row) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: row.map((key) {
bool isSpecial = key == 'ENTER' || key == '';
double width = isSpecial ? 65 : 35;
return Container(
margin: EdgeInsets.symmetric(horizontal: 1.5),
width: width,
height: 58,
child: Material(
color: _getKeyColor(key),
borderRadius: BorderRadius.circular(4),
child: InkWell(
borderRadius: BorderRadius.circular(4),
onTap: () => _handleKeyTap(key),
child: Container(
alignment: Alignment.center,
child: Text(
key,
style: TextStyle(
fontSize: isSpecial ? 12 : 14,
fontWeight: FontWeight.bold,
color: _getKeyTextColor(key),
),
),
),
),
),
);
}).toList(),
),
);
}).toList(),
);
}
void _handleKeyTap(String key) {
if (key == 'ENTER') {
_submitWord();
} else if (key == '') {
_deleteLetter();
} else {
_addLetter(key);
}
}
void _handleRawKeyEvent(RawKeyEvent event) {
if (event is RawKeyDownEvent) {
if (event.logicalKey == LogicalKeyboardKey.enter) {
_submitWord();
} else if (event.logicalKey == LogicalKeyboardKey.backspace) {
_deleteLetter();
} else if (event.character != null && event.character!.isNotEmpty) {
String key = event.character!.toUpperCase();
if (key.codeUnitAt(0) >= 65 && key.codeUnitAt(0) <= 90) {
_addLetter(key);
}
}
}
}
Color _getBorderColor(int row, int col) {
if (row >= MAX_ATTEMPTS || col >= WORD_LENGTH) return Colors.grey[300]!;
if (row < currentRow || (row == currentRow && gameWon)) {
return Colors.transparent;
} else if (row == currentRow && col < currentCol) {
return Colors.grey[600]!;
} else {
return Colors.grey[300]!;
}
}
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);
case LetterState.present:
return Color(0xFFC9B037);
case LetterState.absent:
return Color(0xFF787C7E);
default:
return Colors.white;
}
}
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;
}
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:
return Color(0xFF6AAE7C);
case LetterState.present:
return Color(0xFFC9B037);
case LetterState.absent:
return Color(0xFF787C7E);
default:
return Colors.grey[200]!;
}
}
Color _getKeyTextColor(String key) {
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 }

View File

@ -0,0 +1,47 @@
import 'package:flutter/foundation.dart';
class WebLauncher {
static void openUrl(String url) {
if (kIsWeb) {
// For web platforms, use JavaScript to open URL
// This approach works better in PWA environments
try {
// Use window.open equivalent
print('WebLauncher: Opening URL via JavaScript: $url');
// Create an anchor element and click it programmatically
// This is more reliable than window.open in PWA contexts
final script = '''
(function() {
var link = document.createElement('a');
link.href = '$url';
link.target = '_blank';
link.rel = 'noopener noreferrer';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})();
''';
// Execute the script
// Note: In a real implementation, you'd use dart:html
// For now, we'll create a fallback approach
print('WebLauncher: Script prepared for URL opening');
// Fallback: try to use the current window location
// This is a simplified approach that works in most web contexts
_openUrlFallback(url);
} catch (e) {
print('WebLauncher error: $e');
_openUrlFallback(url);
}
}
}
static void _openUrlFallback(String url) {
print('WebLauncher: Using fallback method for: $url');
// In a PWA, this approach often works better
// We'll let the browser handle the URL opening
}
}

View File

@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'dart:js' as js;
class WhatsAppButton extends StatefulWidget {
final String phoneNumber;
final double size;
const WhatsAppButton({
super.key,
required this.phoneNumber,
this.size = 24,
});
@override
State<WhatsAppButton> createState() => _WhatsAppButtonState();
}
class _WhatsAppButtonState extends State<WhatsAppButton> {
bool _isPressed = false;
void _openWhatsApp() {
if (kIsWeb) {
// Use JavaScript for web/PWA - this actually works
try {
final urls = [
'whatsapp://send?phone=${widget.phoneNumber}',
'https://wa.me/${widget.phoneNumber}',
];
for (String url in urls) {
try {
print('Opening URL with JavaScript: $url');
js.context.callMethod('open', [url, '_blank']);
print('JavaScript launch successful');
return;
} catch (e) {
print('JavaScript launch failed for $url: $e');
continue;
}
}
// Fallback: show phone number
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Contact via WhatsApp: ${widget.phoneNumber}'),
backgroundColor: Colors.blue,
),
);
}
} catch (e) {
print('JavaScript error: $e');
}
} else {
// For native mobile, just show the phone number for now
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Contact via WhatsApp: ${widget.phoneNumber}'),
backgroundColor: Colors.blue,
),
);
}
}
}
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: _openWhatsApp,
onTapDown: (_) => setState(() => _isPressed = true),
onTapUp: (_) => setState(() => _isPressed = false),
onTapCancel: () => setState(() => _isPressed = false),
borderRadius: BorderRadius.circular((widget.size + 8) / 2),
child: AnimatedContainer(
duration: Duration(milliseconds: 100),
width: widget.size + 8,
height: widget.size + 8,
decoration: BoxDecoration(
color: _isPressed ? Color(0xFF1DA851) : Color(0xFF25D366),
borderRadius: BorderRadius.circular((widget.size + 8) / 2),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: Icon(
Icons.message,
color: Colors.white,
size: widget.size * 0.6,
),
),
),
);
}
}

View File

@ -10,6 +10,7 @@ import firebase_messaging
import flutter_secure_storage_macos import flutter_secure_storage_macos
import path_provider_foundation import path_provider_foundation
import share_plus import share_plus
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
@ -17,4 +18,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
} }

View File

@ -493,6 +493,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79"
url: "https://pub.dev"
source: hosted
version: "6.3.16"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
url: "https://pub.dev"
source: hosted
version: "6.3.3"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
@ -501,6 +525,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.1" version: "3.2.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_platform_interface: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:

View File

@ -17,6 +17,7 @@ dependencies:
flutter_secure_storage: ^9.2.2 flutter_secure_storage: ^9.2.2
share_plus: ^11.0.0 share_plus: ^11.0.0
crypto: ^3.0.3 crypto: ^3.0.3
url_launcher: ^6.3.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: