2026-03-11 22:00:01 +01:00
// ===========================================================================
// FILE: lib/ui/multiplayer/lobby_screen.dart
// ===========================================================================
2026-02-27 23:35:54 +01:00
import ' dart:ui ' ;
import ' package:flutter/material.dart ' ;
import ' package:provider/provider.dart ' ;
import ' package:cloud_firestore/cloud_firestore.dart ' ;
2026-03-11 23:00:01 +01:00
import ' package:firebase_auth/firebase_auth.dart ' ;
2026-02-27 23:35:54 +01:00
import ' dart:math ' as math ;
2026-03-01 20:59:06 +01:00
import ' ../../logic/game_controller.dart ' ;
import ' ../../models/game_board.dart ' ;
2026-02-27 23:35:54 +01:00
import ' ../../core/theme_manager.dart ' ;
import ' ../../core/app_colors.dart ' ;
import ' ../../services/multiplayer_service.dart ' ;
import ' ../../services/storage_service.dart ' ;
import ' ../game/game_screen.dart ' ;
import ' package:google_fonts/google_fonts.dart ' ;
TextStyle _getTextStyle ( AppThemeType themeType , TextStyle baseStyle ) {
if ( themeType = = AppThemeType . doodle ) {
return GoogleFonts . permanentMarker ( textStyle: baseStyle ) ;
2026-03-01 20:59:06 +01:00
} else if ( themeType = = AppThemeType . arcade ) {
return GoogleFonts . pressStart2p ( textStyle: baseStyle . copyWith (
fontSize: baseStyle . fontSize ! = null ? baseStyle . fontSize ! * 0.75 : null ,
letterSpacing: 0.5 ,
) ) ;
} else if ( themeType = = AppThemeType . grimorio ) {
return GoogleFonts . cinzelDecorative ( textStyle: baseStyle . copyWith ( fontWeight: FontWeight . bold ) ) ;
2026-02-27 23:35:54 +01:00
}
return baseStyle ;
}
class _NeonShapeButton extends StatelessWidget {
final IconData icon ;
final String label ;
final bool isSelected ;
final ThemeColors theme ;
final AppThemeType themeType ;
final VoidCallback onTap ;
final bool isLocked ;
final bool isSpecial ;
const _NeonShapeButton ( {
required this . icon , required this . label , required this . isSelected ,
required this . theme , required this . themeType , required this . onTap ,
this . isLocked = false , this . isSpecial = false
} ) ;
Color _getDoodleColor ( ) {
switch ( label ) {
case ' Rombo ' : return Colors . blue . shade700 ;
case ' Croce ' : return Colors . teal . shade700 ;
case ' Buco ' : return Colors . pink . shade600 ;
case ' Clessidra ' : return Colors . deepPurple . shade600 ;
case ' Caos ' : return Colors . blueGrey . shade800 ;
default : return Colors . blue . shade700 ;
}
}
@ override
Widget build ( BuildContext context ) {
if ( themeType = = AppThemeType . doodle ) {
Color doodleColor = isLocked ? Colors . grey : _getDoodleColor ( ) ;
double tilt = ( label . length % 2 = = 0 ) ? - 0.03 : 0.04 ;
return Transform . rotate (
angle: tilt ,
child: GestureDetector (
onTap: isLocked ? null : onTap ,
child: AnimatedContainer (
duration: const Duration ( milliseconds: 200 ) ,
padding: const EdgeInsets . symmetric ( horizontal: 6 , vertical: 6 ) ,
transform: Matrix4 . translationValues ( 0 , isSelected ? 3 : 0 , 0 ) ,
decoration: BoxDecoration (
color: isSelected ? doodleColor : Colors . white ,
borderRadius: const BorderRadius . only (
topLeft: Radius . circular ( 15 ) , topRight: Radius . circular ( 8 ) ,
bottomLeft: Radius . circular ( 6 ) , bottomRight: Radius . circular ( 18 ) ,
) ,
border: Border . all ( color: isSelected ? theme . text : doodleColor . withOpacity ( 0.5 ) , width: isSelected ? 2.5 : 1.5 ) ,
boxShadow: isSelected
? [ BoxShadow ( color: theme . text . withOpacity ( 0.8 ) , offset: const Offset ( 3 , 4 ) , blurRadius: 0 ) ]
: [ BoxShadow ( color: doodleColor . withOpacity ( 0.2 ) , offset: const Offset ( 2 , 2 ) , blurRadius: 0 ) ] ,
) ,
child: Column (
mainAxisSize: MainAxisSize . min ,
children: [
Icon ( isLocked ? Icons . lock : icon , color: isSelected ? Colors . white : doodleColor , size: 20 ) ,
const SizedBox ( height: 2 ) ,
Text ( isLocked ? " Liv. 10 " : label , style: _getTextStyle ( themeType , TextStyle ( color: isSelected ? Colors . white : doodleColor , fontSize: 9 , fontWeight: FontWeight . w900 , letterSpacing: 0.2 ) ) ) ,
] ,
) ,
) ,
) ,
) ;
}
Color mainColor = isSpecial & & ! isLocked ? Colors . purpleAccent : theme . playerBlue ;
return GestureDetector (
onTap: isLocked ? null : onTap ,
child: AnimatedContainer (
duration: const Duration ( milliseconds: 250 ) ,
curve: Curves . easeOutCubic ,
padding: const EdgeInsets . symmetric ( horizontal: 8 , vertical: 8 ) ,
transform: Matrix4 . translationValues ( 0 , isSelected ? 2 : 0 , 0 ) ,
decoration: BoxDecoration (
borderRadius: BorderRadius . circular ( 12 ) ,
gradient: LinearGradient (
begin: Alignment . topLeft ,
end: Alignment . bottomRight ,
colors: isLocked
? [ Colors . grey . withOpacity ( 0.1 ) , Colors . black . withOpacity ( 0.2 ) ]
: isSelected
? [ mainColor . withOpacity ( 0.3 ) , mainColor . withOpacity ( 0.1 ) ]
: [ theme . text . withOpacity ( 0.1 ) , theme . text . withOpacity ( 0.02 ) ] ,
) ,
border: Border . all (
color: isLocked ? Colors . transparent : ( isSelected ? mainColor : Colors . white . withOpacity ( 0.1 ) ) ,
width: isSelected ? 2 : 1 ,
) ,
boxShadow: isLocked ? [ ] : isSelected
? [ BoxShadow ( color: mainColor . withOpacity ( 0.5 ) , blurRadius: 15 , spreadRadius: 1 , offset: const Offset ( 0 , 0 ) ) ]
: [
BoxShadow ( color: Colors . black . withOpacity ( 0.4 ) , blurRadius: 6 , offset: const Offset ( 2 , 4 ) ) ,
BoxShadow ( color: Colors . white . withOpacity ( 0.05 ) , blurRadius: 2 , offset: const Offset ( - 1 , - 1 ) ) ,
] ,
) ,
child: Column (
mainAxisSize: MainAxisSize . min ,
children: [
Icon ( isLocked ? Icons . lock : icon , color: isLocked ? Colors . grey . withOpacity ( 0.5 ) : ( isSelected ? Colors . white : theme . text . withOpacity ( 0.6 ) ) , size: 20 ) ,
const SizedBox ( height: 4 ) ,
Text ( isLocked ? " Liv. 10 " : label , style: _getTextStyle ( themeType , TextStyle ( color: isLocked ? Colors . grey . withOpacity ( 0.5 ) : ( isSelected ? Colors . white : theme . text . withOpacity ( 0.6 ) ) , fontSize: 9 , fontWeight: isSelected ? FontWeight . w900 : FontWeight . bold ) ) ) ,
] ,
) ,
) ,
) ;
}
}
class _NeonSizeButton extends StatelessWidget {
final String label ;
final bool isSelected ;
final ThemeColors theme ;
final AppThemeType themeType ;
final VoidCallback onTap ;
const _NeonSizeButton ( { required this . label , required this . isSelected , required this . theme , required this . themeType , required this . onTap } ) ;
@ override
Widget build ( BuildContext context ) {
if ( themeType = = AppThemeType . doodle ) {
Color doodleColor = label = = ' MAX ' ? Colors . red . shade700 : Colors . blueGrey . shade600 ;
double tilt = ( label = = ' M ' | | label = = ' MAX ' ) ? 0.05 : - 0.04 ;
return Transform . rotate (
angle: tilt ,
child: GestureDetector (
onTap: onTap ,
child: AnimatedContainer (
duration: const Duration ( milliseconds: 200 ) ,
width: 42 , height: 40 ,
transform: Matrix4 . translationValues ( 0 , isSelected ? 3 : 0 , 0 ) ,
decoration: BoxDecoration (
color: isSelected ? doodleColor : Colors . white ,
borderRadius: const BorderRadius . all ( Radius . elliptical ( 25 , 20 ) ) ,
border: Border . all ( color: isSelected ? theme . text : doodleColor . withOpacity ( 0.5 ) , width: 2 ) ,
boxShadow: isSelected
? [ BoxShadow ( color: theme . text . withOpacity ( 0.8 ) , offset: const Offset ( 3 , 4 ) , blurRadius: 0 ) ]
: [ BoxShadow ( color: doodleColor . withOpacity ( 0.2 ) , offset: const Offset ( 2 , 2 ) , blurRadius: 0 ) ] ,
) ,
child: Center (
child: Text ( label , style: _getTextStyle ( themeType , TextStyle ( color: isSelected ? Colors . white : doodleColor , fontSize: 13 , fontWeight: FontWeight . w900 ) ) ) ,
) ,
) ,
) ,
) ;
}
return GestureDetector (
onTap: onTap ,
child: AnimatedContainer (
duration: const Duration ( milliseconds: 250 ) ,
curve: Curves . easeOutCubic ,
width: 42 , height: 42 ,
transform: Matrix4 . translationValues ( 0 , isSelected ? 2 : 0 , 0 ) ,
decoration: BoxDecoration (
shape: BoxShape . circle ,
gradient: LinearGradient (
begin: Alignment . topLeft ,
end: Alignment . bottomRight ,
colors: isSelected
? [ theme . playerRed . withOpacity ( 0.3 ) , theme . playerRed . withOpacity ( 0.1 ) ]
: [ theme . text . withOpacity ( 0.1 ) , theme . text . withOpacity ( 0.02 ) ] ,
) ,
border: Border . all ( color: isSelected ? theme . playerRed : Colors . white . withOpacity ( 0.1 ) , width: isSelected ? 2 : 1 ) ,
boxShadow: isSelected
? [ BoxShadow ( color: theme . playerRed . withOpacity ( 0.5 ) , blurRadius: 15 , spreadRadius: 1 ) ]
: [
BoxShadow ( color: Colors . black . withOpacity ( 0.4 ) , blurRadius: 6 , offset: const Offset ( 2 , 4 ) ) ,
BoxShadow ( color: Colors . white . withOpacity ( 0.05 ) , blurRadius: 2 , offset: const Offset ( - 1 , - 1 ) ) ,
] ,
) ,
child: Center (
child: Text ( label , style: _getTextStyle ( themeType , TextStyle ( color: isSelected ? Colors . white : theme . text . withOpacity ( 0.6 ) , fontSize: 12 , fontWeight: isSelected ? FontWeight . w900 : FontWeight . bold ) ) ) ,
) ,
) ,
) ;
}
}
class _NeonTimeSwitch extends StatelessWidget {
final bool isTimeMode ;
final ThemeColors theme ;
final AppThemeType themeType ;
final VoidCallback onTap ;
const _NeonTimeSwitch ( { required this . isTimeMode , required this . theme , required this . themeType , required this . onTap } ) ;
@ override
Widget build ( BuildContext context ) {
if ( themeType = = AppThemeType . doodle ) {
Color doodleColor = Colors . orange . shade700 ;
return Transform . rotate (
angle: - 0.015 ,
child: GestureDetector (
onTap: onTap ,
child: AnimatedContainer (
duration: const Duration ( milliseconds: 200 ) ,
padding: const EdgeInsets . symmetric ( horizontal: 16 , vertical: 8 ) ,
transform: Matrix4 . translationValues ( 0 , isTimeMode ? 3 : 0 , 0 ) ,
decoration: BoxDecoration (
color: isTimeMode ? doodleColor : Colors . white ,
borderRadius: const BorderRadius . only (
topLeft: Radius . circular ( 8 ) , topRight: Radius . circular ( 15 ) ,
bottomLeft: Radius . circular ( 15 ) , bottomRight: Radius . circular ( 6 ) ,
) ,
border: Border . all ( color: isTimeMode ? theme . text : doodleColor . withOpacity ( 0.5 ) , width: 2.5 ) ,
boxShadow: isTimeMode
? [ BoxShadow ( color: theme . text . withOpacity ( 0.8 ) , offset: const Offset ( 4 , 5 ) , blurRadius: 0 ) ]
: [ BoxShadow ( color: doodleColor . withOpacity ( 0.2 ) , offset: const Offset ( 2 , 2 ) , blurRadius: 0 ) ] ,
) ,
child: Row (
mainAxisSize: MainAxisSize . max ,
mainAxisAlignment: MainAxisAlignment . center ,
children: [
2026-03-11 22:00:01 +01:00
Icon ( isTimeMode ? Icons . timer : Icons . timer_off , color: isTimeMode ? Colors . white : doodleColor , size: 20 ) ,
const SizedBox ( width: 8 ) ,
2026-02-27 23:35:54 +01:00
Column (
crossAxisAlignment: CrossAxisAlignment . start ,
mainAxisSize: MainAxisSize . min ,
children: [
2026-03-11 22:00:01 +01:00
Text ( isTimeMode ? ' A TEMPO ' : ' RELAX ' , style: _getTextStyle ( themeType , TextStyle ( color: isTimeMode ? Colors . white : doodleColor , fontWeight: FontWeight . w900 , fontSize: 12 , letterSpacing: 1.0 ) ) ) ,
Text ( isTimeMode ? ' 15s a mossa ' : ' Senza limiti ' , style: _getTextStyle ( themeType , TextStyle ( color: isTimeMode ? Colors . white : doodleColor . withOpacity ( 0.8 ) , fontSize: 9 , fontWeight: FontWeight . bold ) ) ) ,
2026-02-27 23:35:54 +01:00
] ,
) ,
] ,
) ,
) ,
) ,
) ;
}
return GestureDetector (
onTap: onTap ,
child: AnimatedContainer (
duration: const Duration ( milliseconds: 300 ) ,
curve: Curves . easeInOut ,
padding: const EdgeInsets . symmetric ( horizontal: 16 , vertical: 8 ) ,
decoration: BoxDecoration (
borderRadius: BorderRadius . circular ( 15 ) ,
gradient: LinearGradient (
begin: Alignment . topLeft ,
end: Alignment . bottomRight ,
colors: isTimeMode
? [ Colors . amber . withOpacity ( 0.25 ) , Colors . amber . withOpacity ( 0.05 ) ]
: [ theme . text . withOpacity ( 0.1 ) , theme . text . withOpacity ( 0.02 ) ] ,
) ,
border: Border . all ( color: isTimeMode ? Colors . amber : Colors . white . withOpacity ( 0.1 ) , width: isTimeMode ? 2 : 1 ) ,
boxShadow: isTimeMode
? [ BoxShadow ( color: Colors . amber . withOpacity ( 0.3 ) , blurRadius: 15 , spreadRadius: 2 ) ]
: [
BoxShadow ( color: Colors . black . withOpacity ( 0.4 ) , blurRadius: 6 , offset: const Offset ( 2 , 4 ) ) ,
BoxShadow ( color: Colors . white . withOpacity ( 0.05 ) , blurRadius: 2 , offset: const Offset ( - 1 , - 1 ) ) ,
] ,
) ,
child: Row (
mainAxisSize: MainAxisSize . max ,
mainAxisAlignment: MainAxisAlignment . center ,
children: [
2026-03-11 22:00:01 +01:00
Icon ( isTimeMode ? Icons . timer : Icons . timer_off , color: isTimeMode ? Colors . amber : theme . text . withOpacity ( 0.5 ) , size: 20 ) ,
const SizedBox ( width: 8 ) ,
2026-02-27 23:35:54 +01:00
Column (
crossAxisAlignment: CrossAxisAlignment . start ,
mainAxisSize: MainAxisSize . min ,
children: [
2026-03-11 22:00:01 +01:00
Text ( isTimeMode ? ' A TEMPO ' : ' RELAX ' , style: _getTextStyle ( themeType , TextStyle ( color: isTimeMode ? Colors . white : theme . text . withOpacity ( 0.5 ) , fontWeight: FontWeight . w900 , fontSize: 11 , letterSpacing: 1.5 ) ) ) ,
Text ( isTimeMode ? ' 15s a mossa ' : ' Senza limiti ' , style: _getTextStyle ( themeType , TextStyle ( color: isTimeMode ? Colors . amber . shade200 : theme . text . withOpacity ( 0.4 ) , fontSize: 9 , fontWeight: FontWeight . bold ) ) ) ,
] ,
) ,
] ,
) ,
) ,
) ;
}
}
class _NeonPrivacySwitch extends StatelessWidget {
final bool isPublic ;
final ThemeColors theme ;
final AppThemeType themeType ;
final VoidCallback onTap ;
const _NeonPrivacySwitch ( { required this . isPublic , required this . theme , required this . themeType , required this . onTap } ) ;
@ override
Widget build ( BuildContext context ) {
if ( themeType = = AppThemeType . doodle ) {
Color doodleColor = isPublic ? Colors . green . shade600 : Colors . red . shade600 ;
return Transform . rotate (
angle: 0.015 ,
child: GestureDetector (
onTap: onTap ,
child: AnimatedContainer (
duration: const Duration ( milliseconds: 200 ) ,
padding: const EdgeInsets . symmetric ( horizontal: 16 , vertical: 8 ) ,
transform: Matrix4 . translationValues ( 0 , isPublic ? 3 : 0 , 0 ) ,
decoration: BoxDecoration (
color: isPublic ? doodleColor : Colors . white ,
borderRadius: const BorderRadius . only (
topLeft: Radius . circular ( 15 ) , topRight: Radius . circular ( 8 ) ,
bottomLeft: Radius . circular ( 6 ) , bottomRight: Radius . circular ( 15 ) ,
) ,
border: Border . all ( color: isPublic ? theme . text : doodleColor . withOpacity ( 0.5 ) , width: 2.5 ) ,
boxShadow: [ BoxShadow ( color: isPublic ? theme . text . withOpacity ( 0.8 ) : doodleColor . withOpacity ( 0.2 ) , offset: const Offset ( 4 , 5 ) , blurRadius: 0 ) ] ,
) ,
child: Row (
mainAxisSize: MainAxisSize . max ,
mainAxisAlignment: MainAxisAlignment . center ,
children: [
Icon ( isPublic ? Icons . public : Icons . lock , color: isPublic ? Colors . white : doodleColor , size: 20 ) ,
const SizedBox ( width: 8 ) ,
Column (
crossAxisAlignment: CrossAxisAlignment . start ,
mainAxisSize: MainAxisSize . min ,
children: [
Text ( isPublic ? ' PUBBLICA ' : ' PRIVATA ' , style: _getTextStyle ( themeType , TextStyle ( color: isPublic ? Colors . white : doodleColor , fontWeight: FontWeight . w900 , fontSize: 12 , letterSpacing: 1.0 ) ) ) ,
Text ( isPublic ? ' In Bacheca ' : ' Solo Codice ' , style: _getTextStyle ( themeType , TextStyle ( color: isPublic ? Colors . white : doodleColor . withOpacity ( 0.8 ) , fontSize: 9 , fontWeight: FontWeight . bold ) ) ) ,
] ,
) ,
] ,
) ,
) ,
) ,
) ;
}
return GestureDetector (
onTap: onTap ,
child: AnimatedContainer (
duration: const Duration ( milliseconds: 300 ) ,
curve: Curves . easeInOut ,
padding: const EdgeInsets . symmetric ( horizontal: 16 , vertical: 8 ) ,
decoration: BoxDecoration (
borderRadius: BorderRadius . circular ( 15 ) ,
gradient: LinearGradient (
begin: Alignment . topLeft ,
end: Alignment . bottomRight ,
colors: isPublic
? [ Colors . greenAccent . withOpacity ( 0.25 ) , Colors . greenAccent . withOpacity ( 0.05 ) ]
: [ theme . playerRed . withOpacity ( 0.25 ) , theme . playerRed . withOpacity ( 0.05 ) ] ,
) ,
border: Border . all ( color: isPublic ? Colors . greenAccent : theme . playerRed , width: isPublic ? 2 : 1 ) ,
boxShadow: isPublic
? [ BoxShadow ( color: Colors . greenAccent . withOpacity ( 0.3 ) , blurRadius: 15 , spreadRadius: 2 ) ]
: [
BoxShadow ( color: Colors . black . withOpacity ( 0.4 ) , blurRadius: 6 , offset: const Offset ( 2 , 4 ) ) ,
] ,
) ,
child: Row (
mainAxisSize: MainAxisSize . max ,
mainAxisAlignment: MainAxisAlignment . center ,
children: [
Icon ( isPublic ? Icons . public : Icons . lock , color: isPublic ? Colors . greenAccent : theme . playerRed , size: 20 ) ,
const SizedBox ( width: 8 ) ,
Column (
crossAxisAlignment: CrossAxisAlignment . start ,
mainAxisSize: MainAxisSize . min ,
children: [
Text ( isPublic ? ' PUBBLICA ' : ' PRIVATA ' , style: _getTextStyle ( themeType , TextStyle ( color: isPublic ? Colors . white : theme . text . withOpacity ( 0.8 ) , fontWeight: FontWeight . w900 , fontSize: 11 , letterSpacing: 1.5 ) ) ) ,
Text ( isPublic ? ' Tutti ti vedono ' : ' Solo con Codice ' , style: _getTextStyle ( themeType , TextStyle ( color: isPublic ? Colors . greenAccent . shade200 : theme . playerRed . withOpacity ( 0.7 ) , fontSize: 9 , fontWeight: FontWeight . bold ) ) ) ,
2026-02-27 23:35:54 +01:00
] ,
) ,
] ,
) ,
) ,
) ;
}
}
class _NeonActionButton extends StatelessWidget {
final String label ;
final Color color ;
final VoidCallback onTap ;
final ThemeColors theme ;
final AppThemeType themeType ;
const _NeonActionButton ( { required this . label , required this . color , required this . onTap , required this . theme , required this . themeType } ) ;
@ override
Widget build ( BuildContext context ) {
if ( themeType = = AppThemeType . doodle ) {
2026-03-12 14:00:01 +01:00
double tilt = ( label = = " UNISCITI " | | label = = " ANNULLA " ) ? - 0.015 : 0.02 ;
2026-02-27 23:35:54 +01:00
return Transform . rotate (
angle: tilt ,
child: GestureDetector (
onTap: onTap ,
child: Container (
height: 50 ,
decoration: BoxDecoration (
color: color ,
borderRadius: const BorderRadius . only (
topLeft: Radius . circular ( 10 ) , topRight: Radius . circular ( 20 ) ,
bottomLeft: Radius . circular ( 25 ) , bottomRight: Radius . circular ( 10 ) ,
) ,
border: Border . all ( color: theme . text , width: 3.0 ) ,
boxShadow: [ BoxShadow ( color: theme . text . withOpacity ( 0.9 ) , offset: const Offset ( 4 , 4 ) , blurRadius: 0 ) ] ,
) ,
child: Center (
2026-03-12 14:00:01 +01:00
child: FittedBox (
fit: BoxFit . scaleDown ,
child: Padding (
padding: const EdgeInsets . symmetric ( horizontal: 10.0 ) ,
child: Text ( label , style: _getTextStyle ( themeType , const TextStyle ( fontSize: 20 , fontWeight: FontWeight . w900 , letterSpacing: 3.0 , color: Colors . white ) ) ) ,
) ,
) ,
2026-02-27 23:35:54 +01:00
) ,
) ,
) ,
) ;
}
return GestureDetector (
onTap: onTap ,
child: Container (
height: 50 ,
decoration: BoxDecoration (
gradient: LinearGradient ( begin: Alignment . topLeft , end: Alignment . bottomRight , colors: [ color . withOpacity ( 0.9 ) , color . withOpacity ( 0.6 ) ] ) ,
borderRadius: BorderRadius . circular ( 15 ) ,
border: Border . all ( color: Colors . white . withOpacity ( 0.3 ) , width: 1.5 ) ,
boxShadow: [
BoxShadow ( color: Colors . black . withOpacity ( 0.5 ) , offset: const Offset ( 4 , 8 ) , blurRadius: 12 ) ,
BoxShadow ( color: color . withOpacity ( 0.3 ) , offset: const Offset ( 0 , 0 ) , blurRadius: 15 , spreadRadius: 1 ) ,
] ,
) ,
child: Center (
2026-03-12 14:00:01 +01:00
child: FittedBox (
fit: BoxFit . scaleDown ,
child: Padding (
padding: const EdgeInsets . symmetric ( horizontal: 10.0 ) ,
child: Text ( label , style: _getTextStyle ( themeType , const TextStyle ( fontSize: 16 , fontWeight: FontWeight . w900 , letterSpacing: 2.0 , color: Colors . white , shadows: [ Shadow ( color: Colors . black , blurRadius: 2 , offset: Offset ( 1 , 1 ) ) ] ) ) ) ,
) ,
) ,
2026-02-27 23:35:54 +01:00
) ,
) ,
) ;
}
}
class _AnimatedCyberBorder extends StatefulWidget {
final Widget child ;
const _AnimatedCyberBorder ( { required this . child } ) ;
@ override
State < _AnimatedCyberBorder > createState ( ) = > _AnimatedCyberBorderState ( ) ;
}
class _AnimatedCyberBorderState extends State < _AnimatedCyberBorder > with SingleTickerProviderStateMixin {
late AnimationController _controller ;
@ override
void initState ( ) { super . initState ( ) ; _controller = AnimationController ( vsync: this , duration: const Duration ( seconds: 3 ) ) . . repeat ( ) ; }
@ override
void dispose ( ) { _controller . dispose ( ) ; super . dispose ( ) ; }
@ override
Widget build ( BuildContext context ) {
final theme = context . watch < ThemeManager > ( ) . currentColors ;
return AnimatedBuilder (
animation: _controller ,
builder: ( context , child ) {
return CustomPaint (
painter: _CyberBorderPainter ( animationValue: _controller . value , color1: theme . playerBlue , color2: theme . playerRed ) ,
child: Container (
decoration: BoxDecoration ( color: Colors . transparent , borderRadius: BorderRadius . circular ( 20 ) , boxShadow: [ BoxShadow ( color: theme . playerBlue . withOpacity ( 0.15 ) , blurRadius: 20 , spreadRadius: 2 ) ] ) ,
padding: const EdgeInsets . all ( 3 ) ,
child: widget . child ,
) ,
) ;
} ,
child: widget . child ,
) ;
}
}
class _CyberBorderPainter extends CustomPainter {
final double animationValue ;
final Color color1 ;
final Color color2 ;
_CyberBorderPainter ( { required this . animationValue , required this . color1 , required this . color2 } ) ;
@ override
void paint ( Canvas canvas , Size size ) {
final rect = Offset . zero & size ;
final RRect rrect = RRect . fromRectAndRadius ( rect , const Radius . circular ( 20 ) ) ;
final Paint paint = Paint ( )
. . shader = SweepGradient ( colors: [ color1 , color2 , color1 , color2 , color1 ] , stops: const [ 0.0 , 0.25 , 0.5 , 0.75 , 1.0 ] , transform: GradientRotation ( animationValue * 2 * math . pi ) ) . createShader ( rect )
. . style = PaintingStyle . stroke
. . strokeWidth = 3.0
. . maskFilter = const MaskFilter . blur ( BlurStyle . solid , 3 ) ;
canvas . drawRRect ( rrect , paint ) ;
}
@ override
bool shouldRepaint ( covariant _CyberBorderPainter oldDelegate ) = > oldDelegate . animationValue ! = animationValue ;
}
class LobbyScreen extends StatefulWidget {
final String ? initialRoomCode ;
const LobbyScreen ( { super . key , this . initialRoomCode } ) ;
@ override
State < LobbyScreen > createState ( ) = > _LobbyScreenState ( ) ;
}
2026-03-11 23:00:01 +01:00
class _LobbyScreenState extends State < LobbyScreen > with WidgetsBindingObserver {
2026-02-27 23:35:54 +01:00
final MultiplayerService _multiplayerService = MultiplayerService ( ) ;
late TextEditingController _codeController ;
bool _isLoading = false ;
String ? _myRoomCode ;
String _playerName = ' ' ;
2026-03-12 14:00:01 +01:00
// Variabile per gestire l'effetto "sipario"
bool _isCreatingRoom = false ;
2026-02-27 23:35:54 +01:00
int _selectedRadius = 4 ;
ArenaShape _selectedShape = ArenaShape . classic ;
bool _isTimeMode = true ;
2026-03-11 23:00:01 +01:00
bool _isPublicRoom = true ;
2026-03-12 14:00:01 +01:00
bool _roomStarted = false ;
2026-02-27 23:35:54 +01:00
@ override
void initState ( ) {
super . initState ( ) ;
2026-03-12 14:00:01 +01:00
WidgetsBinding . instance . addObserver ( this ) ;
2026-02-27 23:35:54 +01:00
_codeController = TextEditingController ( ) ;
_playerName = StorageService . instance . playerName ;
if ( widget . initialRoomCode ! = null & & widget . initialRoomCode ! . isNotEmpty ) {
WidgetsBinding . instance . addPostFrameCallback ( ( _ ) {
setState ( ( ) { _codeController . text = widget . initialRoomCode ! ; } ) ;
} ) ;
}
}
@ override
2026-03-11 23:00:01 +01:00
void dispose ( ) {
2026-03-12 14:00:01 +01:00
WidgetsBinding . instance . removeObserver ( this ) ;
_cleanupGhostRoom ( ) ;
2026-03-11 23:00:01 +01:00
_codeController . dispose ( ) ;
super . dispose ( ) ;
}
@ override
void didChangeAppLifecycleState ( AppLifecycleState state ) {
if ( state = = AppLifecycleState . paused | | state = = AppLifecycleState . detached ) {
_cleanupGhostRoom ( ) ;
}
}
void _cleanupGhostRoom ( ) {
if ( _myRoomCode ! = null & & ! _roomStarted ) {
FirebaseFirestore . instance . collection ( ' games ' ) . doc ( _myRoomCode ) . delete ( ) ;
2026-03-12 14:00:01 +01:00
_myRoomCode = null ;
2026-03-11 23:00:01 +01:00
}
}
2026-02-27 23:35:54 +01:00
Future < void > _createRoom ( ) async {
if ( _isLoading ) return ;
setState ( ( ) = > _isLoading = true ) ;
try {
2026-03-11 22:00:01 +01:00
String code = await _multiplayerService . createGameRoom (
_selectedRadius , _playerName , _selectedShape . name , _isTimeMode , isPublic: _isPublicRoom
) ;
2026-02-27 23:35:54 +01:00
if ( ! mounted ) return ;
2026-03-11 23:00:01 +01:00
setState ( ( ) { _myRoomCode = code ; _isLoading = false ; _roomStarted = false ; } ) ;
2026-02-27 23:35:54 +01:00
2026-03-11 22:00:01 +01:00
if ( ! _isPublicRoom ) {
_multiplayerService . shareInviteLink ( code ) ;
}
2026-02-27 23:35:54 +01:00
_showWaitingDialog ( code ) ;
} catch ( e ) {
if ( mounted ) { setState ( ( ) = > _isLoading = false ) ; _showError ( " Errore durante la creazione della partita. " ) ; }
}
}
2026-03-11 22:00:01 +01:00
Future < void > _joinRoomByCode ( String code ) async {
2026-02-27 23:35:54 +01:00
if ( _isLoading ) return ;
FocusScope . of ( context ) . unfocus ( ) ;
2026-03-11 22:00:01 +01:00
code = code . trim ( ) . toUpperCase ( ) ;
2026-02-27 23:35:54 +01:00
if ( code . isEmpty | | code . length ! = 5 ) { _showError ( " Inserisci un codice valido di 5 caratteri. " ) ; return ; }
setState ( ( ) = > _isLoading = true ) ;
try {
Map < String , dynamic > ? roomData = await _multiplayerService . joinGameRoom ( code , _playerName ) ;
if ( ! mounted ) return ;
setState ( ( ) = > _isLoading = false ) ;
if ( roomData ! = null ) {
ScaffoldMessenger . of ( context ) . showSnackBar ( const SnackBar ( content: Text ( " Stanza trovata! Partita in avvio... " ) , backgroundColor: Colors . green ) ) ;
int hostRadius = roomData [ ' radius ' ] ? ? 4 ;
String shapeStr = roomData [ ' shape ' ] ? ? ' classic ' ;
ArenaShape hostShape = ArenaShape . values . firstWhere ( ( e ) = > e . name = = shapeStr , orElse: ( ) = > ArenaShape . classic ) ;
bool hostTimeMode = roomData [ ' timeMode ' ] ? ? true ;
context . read < GameController > ( ) . startNewGame ( hostRadius , isOnline: true , roomCode: code , isHost: false , shape: hostShape , timeMode: hostTimeMode ) ;
Navigator . pushReplacement ( context , MaterialPageRoute ( builder: ( _ ) = > const GameScreen ( ) ) ) ;
} else {
_showError ( " Stanza non trovata, piena o partita già iniziata. " ) ;
}
} catch ( e ) {
if ( mounted ) { setState ( ( ) = > _isLoading = false ) ; _showError ( " Errore di connessione: $ e " ) ; }
}
}
void _showError ( String message ) { ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( message , style: const TextStyle ( color: Colors . white ) ) , backgroundColor: Colors . red ) ) ; }
void _showWaitingDialog ( String code ) {
showDialog (
context: context ,
barrierDismissible: false ,
builder: ( context ) {
final theme = context . watch < ThemeManager > ( ) . currentColors ;
final themeType = context . read < ThemeManager > ( ) . currentThemeType ;
Widget dialogContent = Column (
mainAxisSize: MainAxisSize . min ,
children: [
CircularProgressIndicator ( color: theme . playerRed ) , const SizedBox ( height: 25 ) ,
Text ( " CODICE STANZA " , style: _getTextStyle ( themeType , TextStyle ( fontSize: 16 , fontWeight: FontWeight . bold , color: theme . text . withOpacity ( 0.6 ) , letterSpacing: 2 ) ) ) ,
Text ( code , style: _getTextStyle ( themeType , TextStyle ( fontSize: 40 , fontWeight: FontWeight . w900 , color: theme . playerRed , letterSpacing: 8 , shadows: themeType = = AppThemeType . doodle ? [ ] : [ Shadow ( color: theme . playerRed . withOpacity ( 0.5 ) , blurRadius: 10 ) ] ) ) ) ,
const SizedBox ( height: 25 ) ,
Transform . rotate (
angle: themeType = = AppThemeType . doodle ? 0.02 : 0 ,
child: Container (
padding: const EdgeInsets . all ( 18 ) ,
decoration: BoxDecoration (
color: themeType = = AppThemeType . doodle ? Colors . white : theme . text . withOpacity ( 0.05 ) ,
borderRadius: BorderRadius . circular ( 20 ) ,
border: Border . all ( color: themeType = = AppThemeType . doodle ? theme . text : theme . playerBlue . withOpacity ( 0.3 ) , width: themeType = = AppThemeType . doodle ? 2 : 1.5 ) ,
boxShadow: themeType = = AppThemeType . doodle
? [ BoxShadow ( color: theme . text . withOpacity ( 0.8 ) , offset: const Offset ( 4 , 4 ) ) ]
: [ BoxShadow ( color: theme . playerBlue . withOpacity ( 0.1 ) , blurRadius: 10 ) ]
) ,
child: Column (
children: [
2026-03-11 22:00:01 +01:00
Icon ( _isPublicRoom ? Icons . podcasts : Icons . share , color: theme . playerBlue , size: 32 ) , const SizedBox ( height: 12 ) ,
Text ( _isPublicRoom ? " Sei in Bacheca! " : " Invita un amico " , textAlign: TextAlign . center , style: _getTextStyle ( themeType , TextStyle ( color: theme . text , fontWeight: FontWeight . w900 , fontSize: 18 ) ) ) ,
2026-02-27 23:35:54 +01:00
const SizedBox ( height: 8 ) ,
2026-03-11 22:00:01 +01:00
Text ( _isPublicRoom ? " Aspettiamo che uno sfidante si unisca dalla lobby pubblica. " : " Condividi il codice. La partita inizierà appena si unirà. " , textAlign: TextAlign . center , style: _getTextStyle ( themeType , TextStyle ( color: themeType = = AppThemeType . doodle ? theme . text : theme . text . withOpacity ( 0.8 ) , fontSize: 14 , height: 1.5 ) ) ) ,
2026-02-27 23:35:54 +01:00
] ,
) ,
) ,
) ,
] ,
) ;
if ( themeType = = AppThemeType . cyberpunk ) {
dialogContent = _AnimatedCyberBorder ( child: dialogContent ) ;
} else {
dialogContent = Container (
padding: const EdgeInsets . all ( 20 ) ,
decoration: BoxDecoration (
color: themeType = = AppThemeType . doodle ? Colors . white . withOpacity ( 0.95 ) : theme . background ,
borderRadius: BorderRadius . circular ( 25 ) ,
border: Border . all ( color: themeType = = AppThemeType . doodle ? theme . text : theme . gridLine . withOpacity ( 0.5 ) , width: 2 ) ,
boxShadow: themeType = = AppThemeType . doodle ? [ BoxShadow ( color: theme . text . withOpacity ( 0.6 ) , offset: const Offset ( 8 , 8 ) ) ] : [ ]
) ,
child: dialogContent
) ;
}
return StreamBuilder < DocumentSnapshot > (
stream: _multiplayerService . listenToRoom ( code ) ,
builder: ( context , snapshot ) {
if ( snapshot . hasData & & snapshot . data ! . exists ) {
var data = snapshot . data ! . data ( ) as Map < String , dynamic > ;
if ( data [ ' status ' ] = = ' playing ' ) {
2026-03-12 14:00:01 +01:00
_roomStarted = true ;
2026-02-27 23:35:54 +01:00
WidgetsBinding . instance . addPostFrameCallback ( ( _ ) {
Navigator . pop ( context ) ;
context . read < GameController > ( ) . startNewGame ( _selectedRadius , isOnline: true , roomCode: code , isHost: true , shape: _selectedShape , timeMode: _isTimeMode ) ;
Navigator . pushReplacement ( context , MaterialPageRoute ( builder: ( _ ) = > const GameScreen ( ) ) ) ;
} ) ;
}
}
2026-03-11 23:00:01 +01:00
return PopScope (
canPop: false ,
onPopInvoked: ( didPop ) {
if ( didPop ) return ;
2026-03-12 14:00:01 +01:00
_cleanupGhostRoom ( ) ;
2026-03-11 23:00:01 +01:00
Navigator . pop ( context ) ;
} ,
child: Dialog (
backgroundColor: Colors . transparent ,
insetPadding: const EdgeInsets . all ( 20 ) ,
child: Column (
mainAxisSize: MainAxisSize . min ,
children: [
dialogContent ,
const SizedBox ( height: 20 ) ,
TextButton (
onPressed: ( ) {
2026-03-12 14:00:01 +01:00
_cleanupGhostRoom ( ) ;
2026-03-11 23:00:01 +01:00
Navigator . pop ( context ) ;
} ,
child: Text ( " ANNULLA " , style: _getTextStyle ( themeType , TextStyle ( color: Colors . red , fontWeight: FontWeight . w900 , fontSize: 20 , letterSpacing: 2.0 , shadows: themeType = = AppThemeType . doodle ? [ ] : [ const Shadow ( color: Colors . black , blurRadius: 2 ) ] ) ) ) ,
) ,
] ,
) ,
2026-02-27 23:35:54 +01:00
) ,
) ;
} ,
) ;
}
) ;
}
@ override
Widget build ( BuildContext context ) {
final themeManager = context . watch < ThemeManager > ( ) ;
final themeType = themeManager . currentThemeType ;
final theme = themeManager . currentColors ;
String ? bgImage ;
if ( themeType = = AppThemeType . wood ) bgImage = ' assets/images/wood_bg.jpg ' ;
if ( themeType = = AppThemeType . doodle ) bgImage = ' assets/images/doodle_bg.jpg ' ;
if ( themeType = = AppThemeType . cyberpunk ) bgImage = ' assets/images/cyber_bg.jpg ' ;
2026-03-01 20:59:06 +01:00
bool isChaosUnlocked = true ;
2026-02-27 23:35:54 +01:00
Color doodlePenColor = const Color ( 0xFF00008B ) ;
2026-03-12 14:00:01 +01:00
// --- PANNELLO IMPOSTAZIONI STANZA ---
2026-02-27 23:35:54 +01:00
Widget hostPanel = Transform . rotate (
angle: themeType = = AppThemeType . doodle ? 0.01 : 0 ,
child: Container (
padding: const EdgeInsets . symmetric ( horizontal: 12 , vertical: 15 ) ,
decoration: BoxDecoration (
color: themeType = = AppThemeType . cyberpunk ? Colors . black . withOpacity ( 0.85 ) : ( themeType = = AppThemeType . doodle ? Colors . white . withOpacity ( 0.5 ) : Colors . transparent ) ,
borderRadius: BorderRadius . only (
topLeft: Radius . circular ( themeType = = AppThemeType . doodle ? 5 : 20 ) ,
topRight: const Radius . circular ( 20 ) ,
bottomLeft: const Radius . circular ( 20 ) ,
bottomRight: Radius . circular ( themeType = = AppThemeType . doodle ? 5 : 20 ) ,
) ,
border: themeType = = AppThemeType . cyberpunk ? null : Border . all ( color: themeType = = AppThemeType . doodle ? theme . text . withOpacity ( 0.5 ) : Colors . white . withOpacity ( 0.15 ) , width: themeType = = AppThemeType . doodle ? 2 : 1.5 ) ,
) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
2026-03-11 22:00:01 +01:00
Center ( child: Text ( " IMPOSTAZIONI STANZA " , textAlign: TextAlign . center , style: _getTextStyle ( themeType , TextStyle ( fontSize: 12 , fontWeight: FontWeight . w900 , color: themeType = = AppThemeType . doodle ? theme . text : theme . text . withOpacity ( 0.6 ) , letterSpacing: 2.0 ) ) ) ) ,
2026-02-27 23:35:54 +01:00
const SizedBox ( height: 10 ) ,
Text ( " FORMA ARENA " , style: _getTextStyle ( themeType , TextStyle ( fontSize: 10 , fontWeight: FontWeight . w900 , color: themeType = = AppThemeType . doodle ? theme . text : theme . text . withOpacity ( 0.5 ) , letterSpacing: 1.5 ) ) ) ,
const SizedBox ( height: 6 ) ,
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
2026-03-12 21:32:11 +01:00
Expanded ( child: _NeonShapeButton ( icon: Icons . diamond_outlined , label: ' Rombo ' , isSelected: _selectedShape = = ArenaShape . classic , theme: theme , themeType: themeType , onTap: ( ) = > setState ( ( ) = > _selectedShape = ArenaShape . classic ) ) ) ,
const SizedBox ( width: 4 ) ,
Expanded ( child: _NeonShapeButton ( icon: Icons . add , label: ' Croce ' , isSelected: _selectedShape = = ArenaShape . cross , theme: theme , themeType: themeType , onTap: ( ) = > setState ( ( ) = > _selectedShape = ArenaShape . cross ) ) ) ,
const SizedBox ( width: 4 ) ,
Expanded ( child: _NeonShapeButton ( icon: Icons . donut_large , label: ' Buco ' , isSelected: _selectedShape = = ArenaShape . donut , theme: theme , themeType: themeType , onTap: ( ) = > setState ( ( ) = > _selectedShape = ArenaShape . donut ) ) ) ,
const SizedBox ( width: 4 ) ,
Expanded ( child: _NeonShapeButton ( icon: Icons . hourglass_bottom , label: ' Clessidra ' , isSelected: _selectedShape = = ArenaShape . hourglass , theme: theme , themeType: themeType , onTap: ( ) = > setState ( ( ) = > _selectedShape = ArenaShape . hourglass ) ) ) ,
const SizedBox ( width: 4 ) ,
Expanded ( child: _NeonShapeButton ( icon: Icons . all_inclusive , label: ' Caos ' , isSelected: _selectedShape = = ArenaShape . chaos , theme: theme , themeType: themeType , isSpecial: true , isLocked: ! isChaosUnlocked , onTap: ( ) = > setState ( ( ) = > _selectedShape = ArenaShape . chaos ) ) ) ,
2026-02-27 23:35:54 +01:00
] ,
) ,
const SizedBox ( height: 12 ) ,
Divider ( color: themeType = = AppThemeType . doodle ? theme . text . withOpacity ( 0.5 ) : Colors . white . withOpacity ( 0.05 ) , thickness: themeType = = AppThemeType . doodle ? 2.5 : 1.5 ) ,
const SizedBox ( height: 12 ) ,
Text ( " GRANDEZZA " , style: _getTextStyle ( themeType , TextStyle ( fontSize: 10 , fontWeight: FontWeight . w900 , color: themeType = = AppThemeType . doodle ? theme . text : theme . text . withOpacity ( 0.5 ) , letterSpacing: 1.5 ) ) ) ,
const SizedBox ( height: 8 ) ,
Row (
mainAxisAlignment: MainAxisAlignment . spaceEvenly ,
children: [
_NeonSizeButton ( label: ' S ' , isSelected: _selectedRadius = = 3 , theme: theme , themeType: themeType , onTap: ( ) = > setState ( ( ) = > _selectedRadius = 3 ) ) ,
_NeonSizeButton ( label: ' M ' , isSelected: _selectedRadius = = 4 , theme: theme , themeType: themeType , onTap: ( ) = > setState ( ( ) = > _selectedRadius = 4 ) ) ,
_NeonSizeButton ( label: ' L ' , isSelected: _selectedRadius = = 5 , theme: theme , themeType: themeType , onTap: ( ) = > setState ( ( ) = > _selectedRadius = 5 ) ) ,
_NeonSizeButton ( label: ' MAX ' , isSelected: _selectedRadius = = 6 , theme: theme , themeType: themeType , onTap: ( ) = > setState ( ( ) = > _selectedRadius = 6 ) ) ,
] ,
) ,
const SizedBox ( height: 12 ) ,
Divider ( color: themeType = = AppThemeType . doodle ? theme . text . withOpacity ( 0.5 ) : Colors . white . withOpacity ( 0.05 ) , thickness: themeType = = AppThemeType . doodle ? 2.5 : 1.5 ) ,
const SizedBox ( height: 12 ) ,
2026-03-11 22:00:01 +01:00
Text ( " REGOLE E VISIBILITÀ " , style: _getTextStyle ( themeType , TextStyle ( fontSize: 10 , fontWeight: FontWeight . w900 , color: themeType = = AppThemeType . doodle ? theme . text : theme . text . withOpacity ( 0.5 ) , letterSpacing: 1.5 ) ) ) ,
2026-02-27 23:35:54 +01:00
const SizedBox ( height: 8 ) ,
2026-03-11 22:00:01 +01:00
Row (
children: [
Expanded ( child: _NeonTimeSwitch ( isTimeMode: _isTimeMode , theme: theme , themeType: themeType , onTap: ( ) = > setState ( ( ) = > _isTimeMode = ! _isTimeMode ) ) ) ,
const SizedBox ( width: 8 ) ,
Expanded ( child: _NeonPrivacySwitch ( isPublic: _isPublicRoom , theme: theme , themeType: themeType , onTap: ( ) = > setState ( ( ) = > _isPublicRoom = ! _isPublicRoom ) ) ) ,
] ,
)
2026-02-27 23:35:54 +01:00
] ,
) ,
) ,
) ;
if ( themeType = = AppThemeType . cyberpunk ) {
hostPanel = _AnimatedCyberBorder ( child: hostPanel ) ;
}
Widget uiContent = SafeArea (
child: SingleChildScrollView (
2026-03-12 14:00:01 +01:00
physics: const BouncingScrollPhysics ( ) ,
// Padding inferiore aumentato a 60 per evitare il taglio dei pulsanti
padding: EdgeInsets . only ( left: 20.0 , right: 20.0 , top: 10.0 , bottom: MediaQuery . of ( context ) . padding . bottom + 60.0 ) ,
2026-02-27 23:35:54 +01:00
child: Column (
crossAxisAlignment: CrossAxisAlignment . stretch ,
children: [
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
crossAxisAlignment: CrossAxisAlignment . center ,
children: [
Transform . rotate (
angle: themeType = = AppThemeType . doodle ? - 0.02 : 0 ,
child: Text ( " MULTIPLAYER " , style: _getTextStyle ( themeType , TextStyle ( fontSize: 20 , fontWeight: FontWeight . w900 , color: theme . text , letterSpacing: 1 , shadows: themeType = = AppThemeType . doodle ? [ ] : [ Shadow ( color: Colors . black . withOpacity ( 0.5 ) , offset: const Offset ( 2 , 2 ) , blurRadius: 4 ) ] ) ) ) ,
) ,
Transform . rotate (
angle: themeType = = AppThemeType . doodle ? 0.03 : 0 ,
child: Container (
padding: const EdgeInsets . symmetric ( horizontal: 12 , vertical: 4 ) ,
decoration: BoxDecoration (
color: themeType = = AppThemeType . doodle ? Colors . white : theme . playerRed . withOpacity ( 0.2 ) ,
borderRadius: BorderRadius . circular ( 20 ) ,
border: Border . all ( color: themeType = = AppThemeType . doodle ? theme . playerRed : theme . playerRed . withOpacity ( 0.5 ) , width: themeType = = AppThemeType . doodle ? 2.5 : 1.0 ) ,
boxShadow: themeType = = AppThemeType . doodle ? [ BoxShadow ( color: theme . text . withOpacity ( 0.6 ) , offset: const Offset ( 2 , 3 ) , blurRadius: 0 ) ] : [ ]
) ,
child: Text ( " $ _playerName " , textAlign: TextAlign . center , style: _getTextStyle ( themeType , TextStyle ( fontSize: 14 , fontWeight: FontWeight . bold , color: theme . playerRed , letterSpacing: 1 ) ) ) ,
) ,
) ,
] ,
) ,
const SizedBox ( height: 20 ) ,
2026-03-12 14:00:01 +01:00
// --- L'EFFETTO SIPARIO CON ANIMATED SIZE ---
AnimatedSize (
duration: const Duration ( milliseconds: 300 ) ,
curve: Curves . easeInOut ,
alignment: Alignment . topCenter ,
child: _isCreatingRoom
? Column ( // MENU CREAZIONE (Aperto)
crossAxisAlignment: CrossAxisAlignment . stretch ,
children: [
hostPanel ,
const SizedBox ( height: 15 ) ,
Row (
children: [
Expanded ( // Entrambi in un Expanded "liscio" si dividono il 50% di spazio
child: _NeonActionButton ( label: " AVVIA " , color: theme . playerRed , onTap: _createRoom , theme: theme , themeType: themeType ) ,
) ,
const SizedBox ( width: 10 ) ,
Expanded ( // Entrambi in un Expanded "liscio" si dividono il 50% di spazio
child: _NeonActionButton ( label: " ANNULLA " , color: Colors . grey . shade600 , onTap: ( ) = > setState ( ( ) = > _isCreatingRoom = false ) , theme: theme , themeType: themeType ) ,
) ,
] ,
2026-02-27 23:35:54 +01:00
) ,
2026-03-12 14:00:01 +01:00
] ,
)
: Column ( // MENU BASE (Chiuso)
crossAxisAlignment: CrossAxisAlignment . stretch ,
children: [
_NeonActionButton ( label: " CREA PARTITA " , color: theme . playerRed , onTap: ( ) { FocusScope . of ( context ) . unfocus ( ) ; setState ( ( ) = > _isCreatingRoom = true ) ; } , theme: theme , themeType: themeType ) ,
const SizedBox ( height: 20 ) ,
Row (
children: [
Expanded ( child: Divider ( color: theme . text . withOpacity ( 0.4 ) , thickness: themeType = = AppThemeType . doodle ? 2 : 1.0 ) ) ,
Padding ( padding: const EdgeInsets . symmetric ( horizontal: 10 ) , child: Text ( " OPPURE " , style: _getTextStyle ( themeType , TextStyle ( color: themeType = = AppThemeType . doodle ? theme . text : theme . text . withOpacity ( 0.5 ) , fontWeight: FontWeight . bold , letterSpacing: 2.0 , fontSize: 13 ) ) ) ) ,
Expanded ( child: Divider ( color: theme . text . withOpacity ( 0.4 ) , thickness: themeType = = AppThemeType . doodle ? 2 : 1.0 ) ) ,
] ,
) ,
const SizedBox ( height: 20 ) ,
Transform . rotate (
angle: themeType = = AppThemeType . doodle ? 0.02 : 0 ,
child: Container (
decoration: themeType = = AppThemeType . doodle ? BoxDecoration (
color: Colors . white ,
borderRadius: const BorderRadius . only ( topLeft: Radius . circular ( 20 ) , bottomRight: Radius . circular ( 20 ) , topRight: Radius . circular ( 5 ) , bottomLeft: Radius . circular ( 5 ) ) ,
border: Border . all ( color: theme . text , width: 2.5 ) ,
boxShadow: [ BoxShadow ( color: theme . text . withOpacity ( 0.8 ) , offset: const Offset ( 5 , 5 ) , blurRadius: 0 ) ] ,
) : BoxDecoration (
boxShadow: [ BoxShadow ( color: theme . playerBlue . withOpacity ( 0.15 ) , blurRadius: 15 , spreadRadius: 1 ) ]
) ,
child: TextField (
controller: _codeController , textCapitalization: TextCapitalization . characters , textAlign: TextAlign . center , maxLength: 5 ,
style: _getTextStyle ( themeType , TextStyle ( fontSize: 28 , fontWeight: FontWeight . w900 , color: theme . text , letterSpacing: 12 , shadows: themeType = = AppThemeType . doodle ? [ ] : [ Shadow ( color: theme . playerBlue . withOpacity ( 0.5 ) , blurRadius: 8 ) ] ) ) ,
decoration: InputDecoration (
contentPadding: const EdgeInsets . symmetric ( vertical: 12 ) ,
hintText: " CODICE " , hintStyle: _getTextStyle ( themeType , TextStyle ( color: theme . text . withOpacity ( 0.3 ) , letterSpacing: 10 , fontSize: 20 ) ) , counterText: " " ,
filled: themeType ! = AppThemeType . doodle ,
fillColor: themeType = = AppThemeType . cyberpunk ? Colors . black . withOpacity ( 0.85 ) : theme . text . withOpacity ( 0.05 ) ,
enabledBorder: themeType = = AppThemeType . doodle ? InputBorder . none : OutlineInputBorder ( borderSide: BorderSide ( color: theme . gridLine . withOpacity ( 0.5 ) , width: 2.0 ) , borderRadius: BorderRadius . circular ( 15 ) ) ,
focusedBorder: themeType = = AppThemeType . doodle ? InputBorder . none : OutlineInputBorder ( borderSide: BorderSide ( color: theme . playerBlue , width: 3.0 ) , borderRadius: BorderRadius . circular ( 15 ) ) ,
) ,
) ,
) ,
) ,
const SizedBox ( height: 15 ) ,
_NeonActionButton ( label: " UNISCITI " , color: theme . playerBlue , onTap: ( ) = > _joinRoomByCode ( _codeController . text ) , theme: theme , themeType: themeType ) ,
] ,
2026-02-27 23:35:54 +01:00
) ,
) ,
2026-03-11 22:00:01 +01:00
const SizedBox ( height: 25 ) ,
Row (
children: [
Expanded ( child: Divider ( color: theme . text . withOpacity ( 0.4 ) , thickness: themeType = = AppThemeType . doodle ? 2 : 1.0 ) ) ,
Padding ( padding: const EdgeInsets . symmetric ( horizontal: 10 ) , child: Text ( " LOBBY PUBBLICA " , style: _getTextStyle ( themeType , TextStyle ( color: themeType = = AppThemeType . doodle ? theme . text : theme . text . withOpacity ( 0.5 ) , fontWeight: FontWeight . bold , letterSpacing: 2.0 , fontSize: 13 ) ) ) ) ,
Expanded ( child: Divider ( color: theme . text . withOpacity ( 0.4 ) , thickness: themeType = = AppThemeType . doodle ? 2 : 1.0 ) ) ,
] ,
) ,
const SizedBox ( height: 15 ) ,
// --- LA VERA E PROPRIA BACHECA PUBBLICA ---
StreamBuilder < QuerySnapshot > (
stream: _multiplayerService . getPublicRooms ( ) ,
builder: ( context , snapshot ) {
if ( snapshot . connectionState = = ConnectionState . waiting ) {
return Padding ( padding: const EdgeInsets . all ( 20 ) , child: Center ( child: CircularProgressIndicator ( color: theme . playerBlue ) ) ) ;
}
if ( ! snapshot . hasData | | snapshot . data ! . docs . isEmpty ) {
return Padding (
padding: const EdgeInsets . symmetric ( vertical: 20.0 ) ,
child: Center ( child: Text ( " Nessuna stanza pubblica al momento. \n Creane una tu! " , textAlign: TextAlign . center , style: _getTextStyle ( themeType , TextStyle ( color: theme . text . withOpacity ( 0.6 ) , height: 1.5 ) ) ) ) ,
) ;
}
2026-03-12 14:00:01 +01:00
DateTime now = DateTime . now ( ) ;
2026-03-11 23:00:01 +01:00
String ? myUid = FirebaseAuth . instance . currentUser ? . uid ;
var docs = snapshot . data ! . docs . where ( ( doc ) {
var data = doc . data ( ) as Map < String , dynamic > ;
if ( data [ ' isPublic ' ] ! = true ) return false ;
if ( data [ ' hostUid ' ] ! = null & & data [ ' hostUid ' ] = = myUid ) return false ;
Timestamp ? createdAt = data [ ' createdAt ' ] as Timestamp ? ;
if ( createdAt ! = null ) {
int ageInMinutes = now . difference ( createdAt . toDate ( ) ) . inMinutes ;
if ( ageInMinutes > 15 ) {
FirebaseFirestore . instance . collection ( ' games ' ) . doc ( doc . id ) . delete ( ) ;
return false ;
}
}
2026-03-12 14:00:01 +01:00
return true ;
2026-03-11 23:00:01 +01:00
} ) . toList ( ) ;
if ( docs . isEmpty ) {
return Padding (
padding: const EdgeInsets . symmetric ( vertical: 20.0 ) ,
child: Center ( child: Text ( " Nessuna stanza pubblica al momento. \n Creane una tu! " , textAlign: TextAlign . center , style: _getTextStyle ( themeType , TextStyle ( color: theme . text . withOpacity ( 0.6 ) , height: 1.5 ) ) ) ) ,
) ;
}
2026-03-11 22:00:01 +01:00
docs . sort ( ( a , b ) {
Timestamp ? tA = ( a . data ( ) as Map < String , dynamic > ) [ ' createdAt ' ] as Timestamp ? ;
Timestamp ? tB = ( b . data ( ) as Map < String , dynamic > ) [ ' createdAt ' ] as Timestamp ? ;
if ( tA = = null | | tB = = null ) return 0 ;
return tB . compareTo ( tA ) ;
} ) ;
return ListView . builder (
shrinkWrap: true ,
physics: const NeverScrollableScrollPhysics ( ) ,
padding: EdgeInsets . zero ,
itemCount: docs . length ,
itemBuilder: ( context , index ) {
var doc = docs [ index ] ;
var data = doc . data ( ) as Map < String , dynamic > ;
String host = data [ ' hostName ' ] ? ? ' Sconosciuto ' ;
int r = data [ ' radius ' ] ? ? 4 ;
String shapeStr = data [ ' shape ' ] ? ? ' classic ' ;
bool time = data [ ' timeMode ' ] ? ? true ;
String prettyShape = " Rombo " ;
if ( shapeStr = = ' cross ' ) prettyShape = " Croce " ;
else if ( shapeStr = = ' donut ' ) prettyShape = " Buco " ;
else if ( shapeStr = = ' hourglass ' ) prettyShape = " Clessidra " ;
else if ( shapeStr = = ' chaos ' ) prettyShape = " Caos " ;
return Transform . rotate (
angle: themeType = = AppThemeType . doodle ? ( index % 2 = = 0 ? 0.01 : - 0.01 ) : 0 ,
child: Container (
margin: const EdgeInsets . only ( bottom: 12 ) ,
padding: const EdgeInsets . all ( 12 ) ,
decoration: BoxDecoration (
color: themeType = = AppThemeType . doodle ? Colors . white : theme . text . withOpacity ( 0.05 ) ,
borderRadius: BorderRadius . circular ( 15 ) ,
border: Border . all ( color: themeType = = AppThemeType . doodle ? theme . text : theme . playerBlue . withOpacity ( 0.3 ) , width: themeType = = AppThemeType . doodle ? 2 : 1 ) ,
boxShadow: themeType = = AppThemeType . doodle ? [ BoxShadow ( color: theme . text . withOpacity ( 0.6 ) , offset: const Offset ( 3 , 4 ) ) ] : [ ] ,
) ,
child: Row (
children: [
CircleAvatar ( backgroundColor: theme . playerRed . withOpacity ( 0.2 ) , child: Icon ( Icons . person , color: theme . playerRed ) ) ,
const SizedBox ( width: 12 ) ,
Expanded (
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text ( " Stanza di $ host " , style: _getTextStyle ( themeType , TextStyle ( color: theme . text , fontWeight: FontWeight . bold , fontSize: 16 ) ) ) ,
const SizedBox ( height: 4 ) ,
Text ( " Raggio: $ r • $ prettyShape • ${ time ? ' A Tempo ' : ' Relax ' } " , style: _getTextStyle ( themeType , TextStyle ( color: theme . text . withOpacity ( 0.6 ) , fontSize: 11 ) ) ) ,
] ,
) ,
) ,
ElevatedButton (
style: ElevatedButton . styleFrom (
backgroundColor: theme . playerBlue , foregroundColor: Colors . white ,
shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 10 ) ) ,
elevation: themeType = = AppThemeType . doodle ? 0 : 2 ,
side: themeType = = AppThemeType . doodle ? BorderSide ( color: theme . text , width: 2 ) : BorderSide . none ,
) ,
onPressed: ( ) = > _joinRoomByCode ( doc . id ) ,
child: Text ( " ENTRA " , style: _getTextStyle ( themeType , const TextStyle ( fontWeight: FontWeight . w900 , letterSpacing: 1.0 ) ) ) ,
)
] ,
) ,
) ,
) ;
}
) ;
}
) ,
const SizedBox ( height: 20 ) ,
2026-02-27 23:35:54 +01:00
] ,
) ,
) ,
) ;
return Scaffold (
backgroundColor: bgImage ! = null ? Colors . transparent : theme . background ,
extendBodyBehindAppBar: true ,
appBar: AppBar ( backgroundColor: Colors . transparent , elevation: 0 , iconTheme: IconThemeData ( color: theme . text ) ) ,
body: Stack (
children: [
Container (
decoration: bgImage ! = null ? BoxDecoration ( image: DecorationImage ( image: AssetImage ( bgImage ) , fit: BoxFit . cover ) ) : null ,
child: bgImage ! = null & & themeType = = AppThemeType . cyberpunk
? BackdropFilter ( filter: ImageFilter . blur ( sigmaX: 3.5 , sigmaY: 3.5 ) , child: Container ( color: Colors . black . withOpacity ( 0.2 ) ) )
: bgImage ! = null & & themeType ! = AppThemeType . cyberpunk
? BackdropFilter ( filter: ImageFilter . blur ( sigmaX: 3.5 , sigmaY: 3.5 ) , child: Container ( color: themeType = = AppThemeType . doodle ? Colors . white . withOpacity ( 0.1 ) : Colors . transparent ) )
: null ,
) ,
if ( themeType = = AppThemeType . doodle )
Positioned (
top: 150 , left: - 20 , right: - 20 ,
child: Stack (
alignment: Alignment . center ,
children: [
Transform . rotate ( angle: - 0.06 , child: Icon ( Icons . wifi_tethering , size: 450 , color: doodlePenColor . withOpacity ( 0.08 ) ) ) ,
Transform . rotate ( angle: 0.04 , child: Icon ( Icons . wifi_tethering , size: 430 , color: doodlePenColor . withOpacity ( 0.06 ) ) ) ,
Transform . rotate ( angle: 0.01 , child: Icon ( Icons . wifi_tethering , size: 460 , color: doodlePenColor . withOpacity ( 0.05 ) ) ) ,
] ,
) ,
)
else
Positioned (
top: 70 , left: - 50 , right: - 50 ,
child: Center (
child: Icon ( Icons . wifi_tethering , size: 450 , color: theme . playerBlue . withOpacity ( 0.12 ) ) ,
) ,
) ,
_isLoading ? Center ( child: CircularProgressIndicator ( color: theme . playerRed ) ) : uiContent ,
] ,
) ,
) ;
}
}