commit
8104411066
@ -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',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
599
frontend/lib/screens/pages/wordle_page.dart
Normal file
599
frontend/lib/screens/pages/wordle_page.dart
Normal 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 }
|
||||||
47
frontend/lib/utils/web_launcher.dart
Normal file
47
frontend/lib/utils/web_launcher.dart
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
102
frontend/lib/widgets/whatsapp_button.dart
Normal file
102
frontend/lib/widgets/whatsapp_button.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user