2026-02-27 23:35:54 +01:00
// ===========================================================================
// FILE: lib/ui/game/game_screen.dart
// ===========================================================================
import ' dart:ui ' ;
import ' dart:math ' as math ;
import ' package:flutter/material.dart ' ;
import ' package:provider/provider.dart ' ;
2026-03-01 20:59:06 +01:00
2026-02-27 23:35:54 +01:00
import ' ../../logic/game_controller.dart ' ;
import ' ../../core/theme_manager.dart ' ;
import ' ../../core/app_colors.dart ' ;
2026-03-14 00:00:01 +01:00
import ' ../../models/game_board.dart ' ;
2026-02-27 23:35:54 +01:00
import ' board_painter.dart ' ;
import ' score_board.dart ' ;
2026-03-01 20:59:06 +01:00
import ' package:google_fonts/google_fonts.dart ' ;
2026-03-11 22:00:01 +01:00
import ' ../../services/storage_service.dart ' ;
2026-03-01 20:59:06 +01:00
TextStyle _getTextStyle ( AppThemeType themeType , TextStyle baseStyle ) {
if ( themeType = = AppThemeType . doodle ) {
return GoogleFonts . permanentMarker ( textStyle: baseStyle ) ;
} 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-03-13 23:00:01 +01:00
} else if ( themeType = = AppThemeType . music ) {
return GoogleFonts . audiowide ( textStyle: baseStyle . copyWith ( letterSpacing: 1.5 ) ) ;
2026-03-01 20:59:06 +01:00
}
return baseStyle ;
}
2026-02-27 23:35:54 +01:00
class GameScreen extends StatefulWidget {
const GameScreen ( { super . key } ) ;
@ override
State < GameScreen > createState ( ) = > _GameScreenState ( ) ;
}
class _GameScreenState extends State < GameScreen > with TickerProviderStateMixin {
late AnimationController _blinkController ;
bool _gameOverDialogShown = false ;
bool _opponentLeftDialogShown = false ;
2026-03-01 20:59:06 +01:00
bool _hideJokerMessage = false ;
bool _wasSetupPhase = false ;
Player _lastJokerTurn = Player . red ;
2026-02-27 23:35:54 +01:00
@ override
void initState ( ) {
super . initState ( ) ;
2026-03-01 20:59:06 +01:00
_blinkController = AnimationController ( vsync: this , duration: const Duration ( milliseconds: 600 ) ) . . repeat ( reverse: true ) ;
2026-02-27 23:35:54 +01:00
}
@ override
2026-03-01 20:59:06 +01:00
void dispose ( ) { _blinkController . dispose ( ) ; super . dispose ( ) ; }
2026-02-27 23:35:54 +01:00
void _showGameOverDialog ( BuildContext context , GameController game , ThemeColors theme , AppThemeType themeType ) {
_gameOverDialogShown = true ;
showDialog (
barrierDismissible: false ,
context: context ,
builder: ( dialogContext ) = > Consumer < GameController > (
builder: ( context , controller , child ) {
if ( ! controller . isGameOver ) {
WidgetsBinding . instance . addPostFrameCallback ( ( _ ) {
2026-03-01 20:59:06 +01:00
if ( _gameOverDialogShown ) {
_gameOverDialogShown = false ;
if ( Navigator . canPop ( dialogContext ) ) Navigator . pop ( dialogContext ) ;
}
2026-02-27 23:35:54 +01:00
} ) ;
2026-03-01 20:59:06 +01:00
return const SizedBox . shrink ( ) ;
2026-02-27 23:35:54 +01:00
}
2026-03-01 20:59:06 +01:00
int red = controller . board . scoreRed ; int blue = controller . board . scoreBlue ;
2026-02-27 23:35:54 +01:00
bool playerBeatCPU = controller . isVsCPU & & red > blue ;
2026-03-11 22:00:01 +01:00
String myName = StorageService . instance . playerName . toUpperCase ( ) ;
if ( myName . isEmpty ) myName = " TU " ;
String nameRed = controller . isOnline ? controller . onlineHostName . toUpperCase ( ) : myName ;
2026-03-13 23:00:01 +01:00
String nameBlue = controller . isOnline ? controller . onlineGuestName . toUpperCase ( ) : ( themeType = = AppThemeType . cyberpunk | | themeType = = AppThemeType . arcade | | themeType = = AppThemeType . music ? " VERDE " : " BLU " ) ;
2026-02-27 23:35:54 +01:00
if ( controller . isVsCPU ) nameBlue = " CPU " ;
2026-03-01 20:59:06 +01:00
String winnerText = " " ; Color winnerColor = theme . text ;
2026-02-27 23:35:54 +01:00
if ( red > blue ) { winnerText = " VINCE $ nameRed ! " ; winnerColor = theme . playerRed ; }
else if ( blue > red ) { winnerText = " VINCE $ nameBlue ! " ; winnerColor = theme . playerBlue ; }
else { winnerText = " PAREGGIO! " ; winnerColor = theme . text ; }
return AlertDialog (
backgroundColor: theme . background ,
shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 20 ) , side: BorderSide ( color: winnerColor . withOpacity ( 0.5 ) , width: 2 ) ) ,
2026-03-01 20:59:06 +01:00
title: Text ( " FINE PARTITA " , textAlign: TextAlign . center , style: _getTextStyle ( themeType , TextStyle ( color: theme . text , fontWeight: FontWeight . bold , fontSize: 22 ) ) ) ,
2026-02-27 23:35:54 +01:00
content: Column (
mainAxisSize: MainAxisSize . min ,
children: [
2026-03-01 20:59:06 +01:00
Text ( winnerText , textAlign: TextAlign . center , style: _getTextStyle ( themeType , TextStyle ( fontSize: 26 , fontWeight: FontWeight . w900 , color: winnerColor ) ) ) ,
2026-02-27 23:35:54 +01:00
const SizedBox ( height: 20 ) ,
Container (
padding: const EdgeInsets . symmetric ( horizontal: 20 , vertical: 10 ) ,
decoration: BoxDecoration ( color: theme . text . withOpacity ( 0.05 ) , borderRadius: BorderRadius . circular ( 15 ) ) ,
2026-03-04 22:00:00 +01:00
child: FittedBox (
fit: BoxFit . scaleDown ,
child: Row (
mainAxisSize: MainAxisSize . min ,
children: [
Text ( " $ nameRed : $ red " , style: _getTextStyle ( themeType , TextStyle ( fontSize: 16 , fontWeight: FontWeight . bold , color: theme . playerRed ) ) ) ,
Text ( " - " , style: _getTextStyle ( themeType , TextStyle ( fontSize: 18 , color: theme . text ) ) ) ,
Text ( " $ nameBlue : $ blue " , style: _getTextStyle ( themeType , TextStyle ( fontSize: 16 , fontWeight: FontWeight . bold , color: theme . playerBlue ) ) ) ,
] ,
) ,
2026-02-27 23:35:54 +01:00
) ,
) ,
2026-03-01 20:59:06 +01:00
if ( controller . lastMatchXP > 0 ) . . . [
const SizedBox ( height: 15 ) ,
Container (
padding: const EdgeInsets . symmetric ( horizontal: 14 , vertical: 6 ) ,
decoration: BoxDecoration (
color: Colors . green . withOpacity ( 0.15 ) ,
borderRadius: BorderRadius . circular ( 20 ) ,
border: Border . all ( color: Colors . greenAccent , width: 1.5 ) ,
2026-03-13 23:00:01 +01:00
boxShadow: ( themeType = = AppThemeType . cyberpunk | | themeType = = AppThemeType . music ) ? [ const BoxShadow ( color: Colors . greenAccent , blurRadius: 10 , spreadRadius: - 5 ) ] : [ ] ,
2026-03-01 20:59:06 +01:00
) ,
child: Text ( " + ${ controller . lastMatchXP } XP " , style: _getTextStyle ( themeType , const TextStyle ( color: Colors . greenAccent , fontWeight: FontWeight . w900 , fontSize: 16 , letterSpacing: 1.5 ) ) ) ,
) ,
] ,
2026-02-27 23:35:54 +01:00
if ( controller . isVsCPU ) . . . [
const SizedBox ( height: 15 ) ,
2026-03-01 20:59:06 +01:00
Text ( " Difficoltà CPU: Livello ${ controller . cpuLevel } " , style: _getTextStyle ( themeType , TextStyle ( fontSize: 14 , fontWeight: FontWeight . w500 , color: theme . text . withOpacity ( 0.7 ) ) ) ) ,
2026-02-27 23:35:54 +01:00
] ,
if ( controller . isOnline ) . . . [
const SizedBox ( height: 20 ) ,
if ( controller . rematchRequested & & ! controller . opponentWantsRematch )
2026-03-01 20:59:06 +01:00
Text ( " In attesa di $ nameBlue ... " , style: _getTextStyle ( themeType , const TextStyle ( color: Colors . amber , fontWeight: FontWeight . bold , fontStyle: FontStyle . italic ) ) ) ,
2026-02-27 23:35:54 +01:00
if ( controller . opponentWantsRematch & & ! controller . rematchRequested )
2026-03-01 20:59:06 +01:00
Text ( " $ nameBlue vuole la rivincita! " , style: _getTextStyle ( themeType , const TextStyle ( color: Colors . greenAccent , fontWeight: FontWeight . bold ) ) ) ,
2026-02-27 23:35:54 +01:00
if ( controller . rematchRequested & & controller . opponentWantsRematch )
2026-03-01 20:59:06 +01:00
Text ( " Avvio nuova partita... " , style: _getTextStyle ( themeType , const TextStyle ( color: Colors . green , fontWeight: FontWeight . bold ) ) ) ,
2026-02-27 23:35:54 +01:00
]
] ,
) ,
actionsPadding: const EdgeInsets . only ( left: 20 , right: 20 , bottom: 20 , top: 10 ) ,
actionsAlignment: MainAxisAlignment . center ,
actions: [
Column (
crossAxisAlignment: CrossAxisAlignment . stretch ,
children: [
if ( playerBeatCPU )
ElevatedButton (
style: ElevatedButton . styleFrom ( backgroundColor: winnerColor , foregroundColor: Colors . white , padding: const EdgeInsets . symmetric ( vertical: 15 ) , shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 15 ) ) , elevation: 5 ) ,
2026-03-01 20:59:06 +01:00
onPressed: ( ) { controller . increaseLevelAndRestart ( ) ; } ,
child: Text ( " PROSSIMO LIVELLO ➔ " , style: _getTextStyle ( themeType , const TextStyle ( fontWeight: FontWeight . bold , fontSize: 16 ) ) ) ,
2026-02-27 23:35:54 +01:00
)
else if ( controller . isOnline )
ElevatedButton (
style: ElevatedButton . styleFrom ( backgroundColor: controller . rematchRequested ? Colors . grey : ( winnerColor = = theme . text ? theme . playerBlue : winnerColor ) , foregroundColor: Colors . white , padding: const EdgeInsets . symmetric ( vertical: 15 ) , shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 15 ) ) , elevation: 5 ) ,
2026-03-01 20:59:06 +01:00
onPressed: controller . rematchRequested ? null : ( ) { controller . requestRematch ( ) ; } ,
child: Text ( controller . opponentWantsRematch ? " ACCETTA RIVINCITA " : " CHIEDI RIVINCITA " , style: _getTextStyle ( themeType , const TextStyle ( fontWeight: FontWeight . bold , fontSize: 16 , letterSpacing: 1.0 ) ) ) ,
2026-02-27 23:35:54 +01:00
)
else
ElevatedButton (
style: ElevatedButton . styleFrom ( backgroundColor: winnerColor = = theme . text ? theme . playerBlue : winnerColor , foregroundColor: Colors . white , padding: const EdgeInsets . symmetric ( vertical: 15 ) , shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 15 ) ) , elevation: 5 ) ,
2026-03-20 22:00:01 +01:00
onPressed: ( ) { controller . startNewGame ( controller . board . radius , vsCPU: controller . isVsCPU , shape: controller . board . shape , timeMode: controller . timeModeSetting ) ; } ,
2026-03-01 20:59:06 +01:00
child: Text ( " RIGIOCA " , style: _getTextStyle ( themeType , const TextStyle ( fontWeight: FontWeight . bold , fontSize: 16 , letterSpacing: 2 ) ) ) ,
2026-02-27 23:35:54 +01:00
) ,
const SizedBox ( height: 12 ) ,
OutlinedButton (
style: OutlinedButton . styleFrom ( foregroundColor: theme . text , side: BorderSide ( color: theme . text . withOpacity ( 0.3 ) , width: 2 ) , padding: const EdgeInsets . symmetric ( vertical: 15 ) , shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 15 ) ) ) ,
onPressed: ( ) {
if ( controller . isOnline ) controller . disconnectOnlineGame ( ) ;
2026-03-01 20:59:06 +01:00
_gameOverDialogShown = false ;
Navigator . pop ( dialogContext ) ; Navigator . pop ( context ) ;
2026-02-27 23:35:54 +01:00
} ,
2026-03-01 20:59:06 +01:00
child: Text ( " TORNA AL MENU " , style: _getTextStyle ( themeType , TextStyle ( fontWeight: FontWeight . bold , color: theme . text , fontSize: 14 , letterSpacing: 1.5 ) ) ) ,
2026-02-27 23:35:54 +01:00
) ,
] ,
)
] ,
) ;
}
)
) ;
}
2026-03-01 20:59:06 +01:00
Widget _buildThemedJokerMessage ( ThemeColors theme , AppThemeType themeType , GameController gameController ) {
String titleText = " " ;
String subtitleText = " " ;
if ( gameController . isOnline ) {
titleText = gameController . myJokerPlaced ? " In attesa dell'avversario... " : " Nascondi il tuo Jolly! " ;
subtitleText = gameController . myJokerPlaced ? " " : " (Tocca qui per nascondere) " ;
} else if ( gameController . isVsCPU ) {
titleText = " Nascondi il tuo Jolly! " ;
subtitleText = " (Tocca qui per nascondere) " ;
} else {
2026-03-13 23:00:01 +01:00
String pName = gameController . jokerTurn = = Player . red ? " ROSSO " : ( themeType = = AppThemeType . cyberpunk | | themeType = = AppThemeType . arcade | | themeType = = AppThemeType . music ? " VERDE " : " BLU " ) ;
2026-03-01 20:59:06 +01:00
titleText = " TURNO GIOCATORE $ pName " ;
subtitleText = " Passa il dispositivo. \n L'avversario NON deve guardare! \n \n (Tocca qui quando sei pronto) " ;
}
Widget content = Padding (
padding: const EdgeInsets . symmetric ( horizontal: 20 , vertical: 25 ) ,
child: Column (
mainAxisSize: MainAxisSize . min ,
children: [
2026-03-13 23:00:01 +01:00
Icon ( ThemeIcons . joker ( themeType ) , color: themeType = = AppThemeType . cyberpunk | | themeType = = AppThemeType . arcade | | themeType = = AppThemeType . music ? Colors . yellowAccent : theme . playerBlue , size: 50 ) ,
2026-03-01 20:59:06 +01:00
const SizedBox ( height: 15 ) ,
Text (
titleText ,
textAlign: TextAlign . center ,
style: _getTextStyle ( themeType , TextStyle (
color: themeType = = AppThemeType . doodle ? Colors . black87 : theme . text ,
fontSize: 20 ,
fontWeight: FontWeight . bold ,
) ) ,
) ,
const SizedBox ( height: 25 ) ,
Text (
subtitleText ,
textAlign: TextAlign . center ,
style: _getTextStyle ( themeType , TextStyle (
color: themeType = = AppThemeType . doodle ? Colors . black54 : theme . text . withOpacity ( 0.6 ) ,
fontSize: 12 ,
height: 1.5
) ) ,
) ,
] ,
) ,
) ;
2026-03-13 23:00:01 +01:00
if ( themeType = = AppThemeType . cyberpunk | | themeType = = AppThemeType . music ) {
return Container ( decoration: BoxDecoration ( color: Colors . black . withOpacity ( 0.9 ) , borderRadius: BorderRadius . circular ( 20 ) , border: Border . all ( color: Colors . purpleAccent , width: 2 ) , boxShadow: [ BoxShadow ( color: Colors . purpleAccent . withOpacity ( 0.6 ) , blurRadius: 15 , spreadRadius: 0 ) ] ) , child: content ) ;
2026-03-01 20:59:06 +01:00
} else if ( themeType = = AppThemeType . doodle ) {
return Container ( decoration: BoxDecoration ( color: const Color ( 0xFFF9F9F9 ) , borderRadius: BorderRadius . circular ( 8 ) , border: Border . all ( color: Colors . black87 , width: 3 ) , boxShadow: const [ BoxShadow ( color: Colors . black26 , offset: Offset ( 6 , 6 ) ) ] ) , child: content ) ;
} else if ( themeType = = AppThemeType . arcade ) {
return Container ( decoration: BoxDecoration ( color: Colors . black , borderRadius: BorderRadius . zero , border: Border . all ( color: Colors . greenAccent , width: 4 ) ) , child: content ) ;
} else if ( themeType = = AppThemeType . grimorio ) {
return Container ( decoration: BoxDecoration ( color: const Color ( 0xFF2C1E3D ) , borderRadius: BorderRadius . circular ( 30 ) , border: Border . all ( color: const Color ( 0xFFBCAAA4 ) , width: 3 ) , boxShadow: [ BoxShadow ( color: Colors . deepPurpleAccent . withOpacity ( 0.5 ) , blurRadius: 20 , spreadRadius: 5 ) ] ) , child: content ) ;
} else {
2026-03-13 22:00:00 +01:00
return Container ( decoration: BoxDecoration ( color: theme . background . withOpacity ( 0.95 ) , borderRadius: BorderRadius . circular ( 20 ) , border: Border . all ( color: theme . gridLine . withOpacity ( 0.5 ) , width: 2 ) , boxShadow: [ BoxShadow ( color: Colors . black . withOpacity ( 0.3 ) , blurRadius: 20 , offset: const Offset ( 0 , 10 ) ) ] ) , child: content ) ;
2026-03-01 20:59:06 +01:00
}
}
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 ;
final gameController = context . watch < GameController > ( ) ;
2026-03-01 20:59:06 +01:00
if ( gameController . isSetupPhase & & ! _wasSetupPhase ) {
_hideJokerMessage = false ;
_lastJokerTurn = Player . red ;
} else if ( gameController . isSetupPhase & & gameController . jokerTurn ! = _lastJokerTurn ) {
_hideJokerMessage = false ;
_lastJokerTurn = gameController . jokerTurn ;
}
_wasSetupPhase = gameController . isSetupPhase ;
2026-02-27 23:35:54 +01:00
WidgetsBinding . instance . addPostFrameCallback ( ( _ ) {
if ( gameController . opponentLeft & & ! _opponentLeftDialogShown ) {
_opponentLeftDialogShown = true ;
showDialog (
barrierDismissible: false ,
context: context ,
builder: ( dialogContext ) = > AlertDialog (
backgroundColor: theme . background ,
shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 20 ) ) ,
2026-03-01 20:59:06 +01:00
title: Text ( " VITTORIA A TAVOLINO! " , textAlign: TextAlign . center , style: _getTextStyle ( themeType , TextStyle ( color: theme . playerRed , fontWeight: FontWeight . bold ) ) ) ,
content: Text ( " L'avversario ha abbandonato la stanza. \n Sei il vincitore incontestato! " , textAlign: TextAlign . center , style: _getTextStyle ( themeType , TextStyle ( color: theme . text , fontSize: 16 ) ) ) ,
2026-02-27 23:35:54 +01:00
actionsAlignment: MainAxisAlignment . center ,
actions: [
ElevatedButton (
style: ElevatedButton . styleFrom ( backgroundColor: theme . playerBlue , foregroundColor: Colors . white , shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 15 ) ) ) ,
onPressed: ( ) { gameController . disconnectOnlineGame ( ) ; Navigator . pop ( dialogContext ) ; Navigator . pop ( context ) ; } ,
2026-03-01 20:59:06 +01:00
child: Text ( " MENU PRINCIPALE " , style: _getTextStyle ( themeType , const TextStyle ( fontWeight: FontWeight . bold ) ) ) ,
2026-02-27 23:35:54 +01:00
)
] ,
)
) ;
} else if ( gameController . board . isGameOver & & ! _gameOverDialogShown ) {
_showGameOverDialog ( context , gameController , theme , themeType ) ;
}
} ) ;
String ? bgImage ;
2026-03-15 02:00:01 +01:00
if ( themeType = = AppThemeType . doodle ) bgImage = ' assets/images/doodle_bg.jpg ' ;
2026-03-13 23:00:01 +01:00
if ( themeType = = AppThemeType . cyberpunk ) bgImage = ' assets/images/cyber_bg.jpg ' ;
if ( themeType = = AppThemeType . music ) bgImage = ' assets/images/music_bg.jpg ' ;
2026-03-15 03:00:01 +01:00
if ( themeType = = AppThemeType . arcade ) bgImage = ' assets/images/arcade.jpg ' ;
if ( themeType = = AppThemeType . grimorio ) bgImage = ' assets/images/grimorio.jpg ' ;
2026-02-27 23:35:54 +01:00
2026-03-13 23:00:01 +01:00
Color indicatorColor = themeType = = AppThemeType . cyberpunk | | themeType = = AppThemeType . arcade | | themeType = = AppThemeType . music ? Colors . white : Colors . black ;
2026-02-27 23:35:54 +01:00
Widget emojiBar = const SizedBox ( ) ;
if ( gameController . isOnline & & ! gameController . isGameOver ) {
final List < String > emojis = [ ' 😂 ' , ' 😡 ' , ' 😱 ' , ' 🥳 ' , ' 👀 ' ] ;
emojiBar = Container (
padding: const EdgeInsets . symmetric ( horizontal: 8 , vertical: 6 ) ,
decoration: BoxDecoration (
2026-03-13 23:00:01 +01:00
color: themeType = = AppThemeType . cyberpunk | | themeType = = AppThemeType . arcade | | themeType = = AppThemeType . music ? Colors . black . withOpacity ( 0.6 ) : Colors . white . withOpacity ( 0.8 ) ,
2026-02-27 23:35:54 +01:00
borderRadius: BorderRadius . circular ( 30 ) ,
2026-03-13 23:00:01 +01:00
border: Border . all ( color: themeType = = AppThemeType . cyberpunk | | themeType = = AppThemeType . music ? theme . playerBlue . withOpacity ( 0.3 ) : Colors . white24 , width: 2 ) ,
2026-02-27 23:35:54 +01:00
) ,
child: Row (
mainAxisSize: MainAxisSize . min ,
children: emojis . map ( ( e ) = > GestureDetector (
onTap: ( ) = > gameController . sendReaction ( e ) ,
child: Padding ( padding: const EdgeInsets . symmetric ( horizontal: 6 ) , child: Text ( e , style: const TextStyle ( fontSize: 22 ) ) ) ,
) ) . toList ( ) ,
) ,
) ;
}
Widget gameContent = SafeArea (
child: Stack (
children: [
Column (
children: [
const ScoreBoard ( ) ,
Expanded (
child: Center (
child: Padding (
2026-03-15 03:00:01 +01:00
padding: const EdgeInsets . symmetric ( horizontal: 2.0 , vertical: 2.0 ) ,
2026-03-01 20:59:06 +01:00
child: LayoutBuilder (
builder: ( context , constraints ) {
int cols = gameController . board . columns + 1 ;
int rows = gameController . board . rows + 1 ;
double boxSize = constraints . maxWidth / cols ;
double requiredHeight = boxSize * rows ;
if ( requiredHeight > constraints . maxHeight ) { boxSize = constraints . maxHeight / rows ; }
double actualWidth = boxSize * cols ;
double actualHeight = boxSize * rows ;
return SizedBox (
width: actualWidth , height: actualHeight ,
2026-03-14 00:00:01 +01:00
child: Stack (
children: [
2026-03-15 03:00:01 +01:00
Positioned . fill (
child: ClipPath (
clipper: _ArenaClipper ( gameController . board ) ,
child: BackdropFilter (
filter: ImageFilter . blur ( sigmaX: 8.0 , sigmaY: 8.0 ) ,
child: Container (
color: themeType = = AppThemeType . doodle
? Colors . black . withOpacity ( 0.05 )
: Colors . white . withOpacity ( 0.12 ) ,
2026-03-01 20:59:06 +01:00
) ,
2026-03-14 00:00:01 +01:00
) ,
) ,
2026-03-15 03:00:01 +01:00
) ,
2026-03-14 00:00:01 +01:00
GestureDetector (
behavior: HitTestBehavior . opaque ,
onTapDown: ( details ) = > _handleTap ( details . localPosition , actualWidth , actualHeight , gameController , themeType ) ,
child: AnimatedBuilder (
animation: _blinkController ,
builder: ( context , child ) {
return CustomPaint (
size: Size ( actualWidth , actualHeight ) ,
painter: BoardPainter (
board: gameController . board , theme: theme , themeType: themeType ,
blinkValue: _blinkController . value , isOnline: gameController . isOnline ,
isVsCPU: gameController . isVsCPU , isSetupPhase: gameController . isSetupPhase ,
myPlayer: gameController . myPlayer , jokerTurn: gameController . jokerTurn ,
) ,
) ;
}
) ,
) ,
] ,
2026-03-01 20:59:06 +01:00
) ,
) ;
}
2026-02-27 23:35:54 +01:00
) ,
) ,
) ,
) ,
Padding (
2026-03-15 03:00:01 +01:00
padding: const EdgeInsets . only ( bottom: 10.0 , left: 20.0 , right: 20.0 , top: 5.0 ) ,
2026-02-27 23:35:54 +01:00
child: Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
if ( gameController . isVsCPU )
Container (
padding: const EdgeInsets . symmetric ( horizontal: 16 , vertical: 8 ) ,
decoration: BoxDecoration ( color: indicatorColor . withOpacity ( 0.1 ) , borderRadius: BorderRadius . circular ( 20 ) , border: Border . all ( color: indicatorColor . withOpacity ( 0.3 ) ) ) ,
child: Row (
mainAxisSize: MainAxisSize . min ,
children: [
Icon ( Icons . smart_toy_rounded , size: 16 , color: indicatorColor ) , const SizedBox ( width: 8 ) ,
2026-03-01 20:59:06 +01:00
Text ( " LIVELLO CPU: ${ gameController . cpuLevel } " , style: _getTextStyle ( themeType , TextStyle ( color: indicatorColor , fontWeight: FontWeight . bold , fontSize: 11 , letterSpacing: 1.0 ) ) ) ,
2026-02-27 23:35:54 +01:00
] ,
) ,
)
else
emojiBar ,
Container (
decoration: BoxDecoration ( borderRadius: BorderRadius . circular ( 20 ) , boxShadow: [ BoxShadow ( color: Colors . black . withOpacity ( 0.4 ) , offset: const Offset ( 0 , 4 ) , blurRadius: 5 ) ] ) ,
child: TextButton . icon (
2026-03-13 23:00:01 +01:00
style: TextButton . styleFrom ( backgroundColor: bgImage ! = null | | themeType = = AppThemeType . arcade ? Colors . black87 : theme . background , padding: const EdgeInsets . symmetric ( horizontal: 16 , vertical: 10 ) , shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 20 ) , side: BorderSide ( color: Colors . white . withOpacity ( 0.1 ) , width: 1 ) ) ) ,
icon: Icon ( Icons . exit_to_app , color: bgImage ! = null | | themeType = = AppThemeType . arcade ? Colors . white : theme . text , size: 20 ) ,
2026-02-27 23:35:54 +01:00
onPressed: ( ) { gameController . disconnectOnlineGame ( ) ; Navigator . pop ( context ) ; } ,
2026-03-13 23:00:01 +01:00
label: Text ( " ESCI " , style: _getTextStyle ( themeType , TextStyle ( color: bgImage ! = null | | themeType = = AppThemeType . arcade ? Colors . white : theme . text , fontWeight: FontWeight . bold , fontSize: 12 ) ) ) ,
2026-02-27 23:35:54 +01:00
) ,
) ,
] ,
) ,
)
] ,
) ,
if ( gameController . myReaction ! = null )
2026-03-01 20:59:06 +01:00
Positioned ( top: 80 , left: gameController . isHost ? 30 : null , right: gameController . isHost ? null : 30 , child: _BouncingEmoji ( emoji: gameController . myReaction ! ) ) ,
2026-02-27 23:35:54 +01:00
if ( gameController . opponentReaction ! = null )
2026-03-01 20:59:06 +01:00
Positioned ( top: 80 , left: ! gameController . isHost ? 30 : null , right: ! gameController . isHost ? null : 30 , child: _BouncingEmoji ( emoji: gameController . opponentReaction ! ) ) ,
2026-02-27 23:35:54 +01:00
] ,
) ,
) ;
return PopScope (
canPop: true ,
onPopInvoked: ( didPop ) { gameController . disconnectOnlineGame ( ) ; } ,
child: Scaffold (
2026-03-15 02:00:01 +01:00
backgroundColor: themeType = = AppThemeType . doodle ? Colors . white : ( bgImage ! = null ? Colors . transparent : theme . background ) ,
2026-03-14 18:00:00 +01:00
body: Stack (
children: [
Container ( color: themeType = = AppThemeType . doodle ? Colors . white : theme . background ) ,
2026-03-15 02:00:01 +01:00
if ( themeType = = AppThemeType . doodle )
Positioned . fill (
child: CustomPaint (
painter: FullScreenGridPainter ( Colors . blue . withOpacity ( 0.15 ) ) ,
) ,
) ,
2026-03-14 18:00:00 +01:00
if ( bgImage ! = null )
Positioned . fill (
2026-03-15 16:00:01 +01:00
child: Container (
decoration: BoxDecoration (
image: DecorationImage (
image: AssetImage ( bgImage ! ) ,
fit: BoxFit . cover ,
colorFilter: themeType = = AppThemeType . doodle
? ColorFilter . mode ( Colors . white . withOpacity ( 0.5 ) , BlendMode . lighten )
: null ,
) ,
) ,
2026-03-14 18:00:00 +01:00
) ,
) ,
2026-03-15 03:00:01 +01:00
if ( bgImage ! = null & & ( themeType = = AppThemeType . cyberpunk | | themeType = = AppThemeType . music | | themeType = = AppThemeType . arcade | | themeType = = AppThemeType . grimorio ) )
2026-03-14 18:00:00 +01:00
Positioned . fill (
child: Container (
decoration: BoxDecoration (
gradient: LinearGradient (
begin: Alignment . topCenter , end: Alignment . bottomCenter ,
colors: [ Colors . black . withOpacity ( 0.4 ) , Colors . black . withOpacity ( 0.8 ) ]
)
2026-03-13 23:00:01 +01:00
) ,
2026-03-14 18:00:00 +01:00
) ,
) ,
2026-03-13 23:00:01 +01:00
2026-03-14 18:00:00 +01:00
if ( gameController . isTimeMode & & ! gameController . isCPUThinking & & ! gameController . isGameOver & & gameController . timeLeft > 0 & & gameController . timeLeft < = 5 & & ! gameController . isSetupPhase )
Positioned . fill ( child: BlitzBackgroundEffect ( timeLeft: gameController . timeLeft , color: theme . playerRed , themeType: themeType ) ) ,
if ( gameController . effectText . isNotEmpty )
Positioned . fill ( child: SpecialEventBackgroundEffect ( text: gameController . effectText , color: gameController . effectColor , themeType: themeType ) ) ,
Positioned . fill ( child: gameContent ) ,
if ( gameController . isSetupPhase & & ! _hideJokerMessage )
Positioned . fill (
child: Container (
color: themeType = = AppThemeType . cyberpunk | | themeType = = AppThemeType . arcade | | themeType = = AppThemeType . music
? Colors . black . withOpacity ( 0.98 )
: theme . background . withOpacity ( 0.98 ) ,
child: Center (
child: Padding (
padding: const EdgeInsets . symmetric ( horizontal: 30.0 ) ,
child: GestureDetector (
onTap: ( ) { setState ( ( ) { _hideJokerMessage = true ; } ) ; } ,
child: Material ( color: Colors . transparent , child: _buildThemedJokerMessage ( theme , themeType , gameController ) ) ,
2026-02-27 23:35:54 +01:00
) ,
) ,
) ,
2026-03-14 18:00:00 +01:00
) ,
) ,
2026-03-01 20:59:06 +01:00
2026-03-14 18:00:00 +01:00
if ( gameController . isGameOver & & gameController . board . scoreRed ! = gameController . board . scoreBlue )
Positioned . fill ( child: IgnorePointer ( child: WinnerVFXOverlay ( winnerColor: gameController . board . scoreRed > gameController . board . scoreBlue ? theme . playerRed : theme . playerBlue , themeType: themeType ) ) ) ,
] ,
2026-02-27 23:35:54 +01:00
) ,
) ,
) ;
}
2026-03-01 20:59:06 +01:00
void _handleTap ( Offset tapPos , double width , double height , GameController controller , AppThemeType themeType ) {
2026-02-27 23:35:54 +01:00
final board = controller . board ;
if ( board . isGameOver ) return ;
2026-03-01 20:59:06 +01:00
int cols = board . columns + 1 ; double spacing = width / cols ; double offset = spacing / 2 ;
2026-02-27 23:35:54 +01:00
2026-03-01 20:59:06 +01:00
if ( controller . isSetupPhase ) {
int bx = ( ( tapPos . dx - offset ) / spacing ) . floor ( ) ; int by = ( ( tapPos . dy - offset ) / spacing ) . floor ( ) ;
controller . placeJoker ( bx , by ) ; return ;
}
2026-02-27 23:35:54 +01:00
2026-03-01 20:59:06 +01:00
Line ? closestLine ; double minDistance = double . infinity ; double maxTouchDistance = spacing * 0.4 ;
2026-02-27 23:35:54 +01:00
for ( var line in board . lines ) {
if ( line . owner ! = Player . none | | ! line . isPlayable ) continue ;
Offset screenP1 = Offset ( line . p1 . x * spacing + offset , line . p1 . y * spacing + offset ) ;
Offset screenP2 = Offset ( line . p2 . x * spacing + offset , line . p2 . y * spacing + offset ) ;
double dist = _distanceToSegment ( tapPos , screenP1 , screenP2 ) ;
if ( dist < minDistance & & dist < maxTouchDistance ) { minDistance = dist ; closestLine = line ; }
}
if ( closestLine ! = null ) { controller . handleLineTap ( closestLine , themeType ) ; }
}
double _distanceToSegment ( Offset p , Offset a , Offset b ) {
double l2 = ( a . dx - b . dx ) * ( a . dx - b . dx ) + ( a . dy - b . dy ) * ( a . dy - b . dy ) ;
if ( l2 = = 0 ) return ( p - a ) . distance ;
double t = ( ( ( p . dx - a . dx ) * ( b . dx - a . dx ) + ( p . dy - a . dy ) * ( b . dy - a . dy ) ) / l2 ) . clamp ( 0.0 , 1.0 ) ;
Offset projection = Offset ( a . dx + t * ( b . dx - a . dx ) , a . dy + t * ( b . dy - a . dy ) ) ;
return ( p - projection ) . distance ;
}
}
2026-03-14 00:00:01 +01:00
// ===========================================================================
2026-03-15 16:00:01 +01:00
// CLIPPER MAGICO E ALTRI WIDGETS
2026-03-14 00:00:01 +01:00
// ===========================================================================
class _ArenaClipper extends CustomClipper < Path > {
final GameBoard board ;
_ArenaClipper ( this . board ) ;
@ override
Path getClip ( Size size ) {
int cols = board . columns + 1 ;
double spacing = size . width / cols ;
double offset = spacing / 2 ;
Path path = Path ( ) ;
for ( var box in board . boxes ) {
if ( box . type ! = BoxType . invisible ) {
path . addRect ( Rect . fromLTWH (
box . x * spacing + offset ,
box . y * spacing + offset ,
spacing ,
spacing
) ) ;
}
}
return path ;
}
2026-03-15 16:00:01 +01:00
@ override bool shouldReclip ( covariant _ArenaClipper oldClipper ) = > true ;
2026-03-14 00:00:01 +01:00
}
2026-02-27 23:35:54 +01:00
class _Particle {
2026-03-01 20:59:06 +01:00
double x , y , vx , vy , size , angle , spin ;
Color color ; int type ;
2026-02-27 23:35:54 +01:00
_Particle ( { required this . x , required this . y , required this . vx , required this . vy , required this . color , required this . size , required this . angle , required this . spin , required this . type } ) ;
}
class WinnerVFXOverlay extends StatefulWidget {
2026-03-01 20:59:06 +01:00
final Color winnerColor ; final AppThemeType themeType ;
2026-02-27 23:35:54 +01:00
const WinnerVFXOverlay ( { super . key , required this . winnerColor , required this . themeType } ) ;
2026-03-01 20:59:06 +01:00
@ override State < WinnerVFXOverlay > createState ( ) = > _WinnerVFXOverlayState ( ) ;
2026-02-27 23:35:54 +01:00
}
class _WinnerVFXOverlayState extends State < WinnerVFXOverlay > with SingleTickerProviderStateMixin {
late AnimationController _vfxController ;
final List < _Particle > _particles = [ ] ;
final math . Random _rand = math . Random ( ) ;
bool _initialized = false ;
@ override
void initState ( ) {
super . initState ( ) ;
2026-03-01 20:59:06 +01:00
_vfxController = AnimationController ( vsync: this , duration: const Duration ( seconds: 4 ) ) . . addListener ( ( ) { _updateParticles ( ) ; } ) . . forward ( ) ;
2026-02-27 23:35:54 +01:00
}
@ override
void didChangeDependencies ( ) {
super . didChangeDependencies ( ) ;
2026-03-01 20:59:06 +01:00
if ( ! _initialized ) { _initParticles ( MediaQuery . of ( context ) . size ) ; _initialized = true ; }
2026-02-27 23:35:54 +01:00
}
void _initParticles ( Size screenSize ) {
2026-03-13 23:00:01 +01:00
int particleCount = widget . themeType = = AppThemeType . cyberpunk | | widget . themeType = = AppThemeType . music ? 150 : 100 ;
2026-03-01 20:59:06 +01:00
if ( widget . themeType = = AppThemeType . arcade ) particleCount = 80 ;
if ( widget . themeType = = AppThemeType . grimorio ) particleCount = 120 ;
2026-02-27 23:35:54 +01:00
List < Color > palette = [ widget . winnerColor , widget . winnerColor . withOpacity ( 0.7 ) , Colors . white ] ;
2026-03-01 20:59:06 +01:00
if ( widget . themeType = = AppThemeType . cyberpunk ) { palette . add ( Colors . cyanAccent ) ; palette . add ( Colors . yellowAccent ) ; }
else if ( widget . themeType = = AppThemeType . doodle ) { palette . add ( const Color ( 0xFF00008B ) ) ; palette . add ( Colors . redAccent ) ; }
else if ( widget . themeType = = AppThemeType . arcade ) { palette = [ widget . winnerColor , Colors . white , Colors . greenAccent ] ; }
else if ( widget . themeType = = AppThemeType . grimorio ) { palette = [ widget . winnerColor , Colors . deepPurpleAccent , Colors . white ] ; }
2026-03-13 23:00:01 +01:00
else if ( widget . themeType = = AppThemeType . music ) { palette . add ( Colors . pinkAccent ) ; palette . add ( Colors . cyanAccent ) ; }
2026-02-27 23:35:54 +01:00
for ( int i = 0 ; i < particleCount ; i + + ) {
double speed = _rand . nextDouble ( ) * 20 + 5 ;
double theta = _rand . nextDouble ( ) * 2 * math . pi ;
2026-03-01 20:59:06 +01:00
_particles . add ( _Particle ( x: screenSize . width / 2 , y: screenSize . height / 2 , vx: speed * math . cos ( theta ) , vy: speed * math . sin ( theta ) - 5 , color: palette [ _rand . nextInt ( palette . length ) ] , size: _rand . nextDouble ( ) * 10 + 6 , angle: _rand . nextDouble ( ) * math . pi , spin: ( _rand . nextDouble ( ) - 0.5 ) * 0.5 , type: _rand . nextInt ( 3 ) ) ) ;
2026-02-27 23:35:54 +01:00
}
}
void _updateParticles ( ) {
setState ( ( ) {
for ( var p in _particles ) {
2026-03-01 20:59:06 +01:00
p . x + = p . vx ; p . y + = p . vy ;
2026-03-13 23:00:01 +01:00
if ( widget . themeType = = AppThemeType . cyberpunk | | widget . themeType = = AppThemeType . music ) { p . vy + = 0.1 ; p . vx * = 0.98 ; p . vy * = 0.98 ; }
2026-03-01 20:59:06 +01:00
else if ( widget . themeType = = AppThemeType . arcade ) { p . vy + = 0.3 ; p . spin = 0 ; p . angle = 0 ; }
else if ( widget . themeType = = AppThemeType . grimorio ) { p . vy - = 0.1 ; p . x + = math . sin ( p . y * 0.02 ) * 1.5 ; p . size * = 0.995 ; }
else { p . vy + = 0.5 ; }
p . angle + = p . spin ; p . size * = 0.99 ;
2026-02-27 23:35:54 +01:00
}
} ) ;
}
2026-03-01 20:59:06 +01:00
@ override void dispose ( ) { _vfxController . dispose ( ) ; super . dispose ( ) ; }
@ override Widget build ( BuildContext context ) { return CustomPaint ( painter: _VFXPainter ( particles: _particles , themeType: widget . themeType ) , child: Container ( ) ) ; }
2026-02-27 23:35:54 +01:00
}
class _VFXPainter extends CustomPainter {
2026-03-01 20:59:06 +01:00
final List < _Particle > particles ; final AppThemeType themeType ;
2026-02-27 23:35:54 +01:00
_VFXPainter ( { required this . particles , required this . themeType } ) ;
@ override
void paint ( Canvas canvas , Size size ) {
for ( var p in particles ) {
if ( p . size < 0.5 ) continue ;
2026-03-01 20:59:06 +01:00
final paint = Paint ( ) . . color = p . color . . style = PaintingStyle . fill ;
2026-03-13 23:00:01 +01:00
if ( themeType = = AppThemeType . cyberpunk | | themeType = = AppThemeType . music ) { paint . maskFilter = const MaskFilter . blur ( BlurStyle . solid , 4.0 ) ; }
2026-03-01 20:59:06 +01:00
canvas . save ( ) ; canvas . translate ( p . x , p . y ) ; canvas . rotate ( p . angle ) ;
2026-02-27 23:35:54 +01:00
if ( themeType = = AppThemeType . doodle ) {
2026-03-01 20:59:06 +01:00
paint . style = PaintingStyle . stroke ; paint . strokeWidth = 2.0 ;
if ( p . type = = 0 ) { canvas . drawCircle ( Offset . zero , p . size , paint ) ; } else { canvas . drawRect ( Rect . fromCenter ( center: Offset . zero , width: p . size * 2 , height: p . size * 2 ) , paint ) ; }
} else if ( themeType = = AppThemeType . arcade ) {
canvas . drawRect ( Rect . fromCenter ( center: Offset . zero , width: p . size * 1.5 , height: p . size * 1.5 ) , paint ) ;
} else if ( themeType = = AppThemeType . grimorio ) {
paint . maskFilter = const MaskFilter . blur ( BlurStyle . normal , 4.0 ) ;
2026-02-27 23:35:54 +01:00
canvas . drawCircle ( Offset . zero , p . size , paint ) ;
2026-03-01 20:59:06 +01:00
canvas . drawCircle ( Offset . zero , p . size * 0.3 , Paint ( ) . . color = Colors . white . . style = PaintingStyle . fill ) ;
2026-02-27 23:35:54 +01:00
} else {
2026-03-01 20:59:06 +01:00
if ( p . type = = 0 ) { canvas . drawCircle ( Offset . zero , p . size , paint ) ; }
else if ( p . type = = 1 ) { canvas . drawRect ( Rect . fromCenter ( center: Offset . zero , width: p . size * 2 , height: p . size * 2 ) , paint ) ; }
else { var path = Path ( ) . . moveTo ( 0 , - p . size ) . . lineTo ( p . size , p . size ) . . lineTo ( - p . size , p . size ) . . close ( ) ; canvas . drawPath ( path , paint ) ; }
2026-02-27 23:35:54 +01:00
}
canvas . restore ( ) ;
}
}
2026-03-01 20:59:06 +01:00
@ override bool shouldRepaint ( covariant _VFXPainter oldDelegate ) = > true ;
2026-02-27 23:35:54 +01:00
}
class _BouncingEmoji extends StatefulWidget {
2026-03-01 20:59:06 +01:00
final String emoji ; const _BouncingEmoji ( { required this . emoji } ) ;
@ override State < _BouncingEmoji > createState ( ) = > _BouncingEmojiState ( ) ;
2026-02-27 23:35:54 +01:00
}
class _BouncingEmojiState extends State < _BouncingEmoji > with SingleTickerProviderStateMixin {
2026-03-01 20:59:06 +01:00
late AnimationController _ctrl ; late Animation < double > _anim ;
@ override void initState ( ) { super . initState ( ) ; _ctrl = AnimationController ( vsync: this , duration: const Duration ( milliseconds: 500 ) ) . . repeat ( reverse: true ) ; _anim = Tween < double > ( begin: - 10 , end: 10 ) . animate ( CurvedAnimation ( parent: _ctrl , curve: Curves . easeInOut ) ) ; }
@ override void dispose ( ) { _ctrl . dispose ( ) ; super . dispose ( ) ; }
@ override Widget build ( BuildContext context ) { return AnimatedBuilder ( animation: _anim , builder: ( ctx , child ) = > Transform . translate ( offset: Offset ( 0 , _anim . value ) , child: Container ( padding: const EdgeInsets . all ( 8 ) , decoration: const BoxDecoration ( color: Colors . white , shape: BoxShape . circle , boxShadow: [ BoxShadow ( color: Colors . black26 , blurRadius: 5 ) ] ) , child: Text ( widget . emoji , style: const TextStyle ( fontSize: 32 ) ) ) ) ) ; }
2026-02-27 23:35:54 +01:00
}
class FullScreenGridPainter extends CustomPainter {
2026-03-01 20:59:06 +01:00
final Color gridColor ; FullScreenGridPainter ( this . gridColor ) ;
@ override void paint ( Canvas canvas , Size size ) { final Paint paperGridPaint = Paint ( ) . . color = gridColor . . strokeWidth = 1.0 . . style = PaintingStyle . stroke ; double paperStep = 20.0 ; for ( double i = 0 ; i < = size . width ; i + = paperStep ) canvas . drawLine ( Offset ( i , 0 ) , Offset ( i , size . height ) , paperGridPaint ) ; for ( double i = 0 ; i < = size . height ; i + = paperStep ) canvas . drawLine ( Offset ( 0 , i ) , Offset ( size . width , i ) , paperGridPaint ) ; }
@ override bool shouldRepaint ( covariant CustomPainter oldDelegate ) = > false ;
2026-02-27 23:35:54 +01:00
}
class BlitzBackgroundEffect extends StatefulWidget {
2026-03-01 20:59:06 +01:00
final int timeLeft ; final Color color ; final AppThemeType themeType ;
const BlitzBackgroundEffect ( { super . key , required this . timeLeft , required this . color , required this . themeType } ) ;
@ override State < BlitzBackgroundEffect > createState ( ) = > _BlitzBackgroundEffectState ( ) ;
2026-02-27 23:35:54 +01:00
}
class _BlitzBackgroundEffectState extends State < BlitzBackgroundEffect > with SingleTickerProviderStateMixin {
late AnimationController _controller ;
2026-03-01 20:59:06 +01:00
@ override void initState ( ) { super . initState ( ) ; _controller = AnimationController ( vsync: this , duration: const Duration ( milliseconds: 400 ) ) . . repeat ( reverse: true ) ; }
@ override void dispose ( ) { _controller . dispose ( ) ; super . dispose ( ) ; }
@ override Widget build ( BuildContext context ) { return AnimatedBuilder ( animation: _controller , builder: ( context , child ) { return Container ( color: widget . color . withOpacity ( 0.12 * _controller . value ) , child: Center ( child: ImageFiltered ( imageFilter: ImageFilter . blur ( sigmaX: 2.0 , sigmaY: 2.0 ) , child: Text ( ' ${ widget . timeLeft } ' , style: _getTextStyle ( widget . themeType , TextStyle ( fontSize: 300 , fontWeight: FontWeight . w900 , color: widget . color . withOpacity ( 0.35 + ( 0.3 * _controller . value ) ) , height: 1.0 ) ) ) ) ) ) ; } ) ; }
2026-02-27 23:35:54 +01:00
}
class SpecialEventBackgroundEffect extends StatefulWidget {
2026-03-01 20:59:06 +01:00
final String text ; final Color color ; final AppThemeType themeType ;
const SpecialEventBackgroundEffect ( { super . key , required this . text , required this . color , required this . themeType } ) ;
@ override State < SpecialEventBackgroundEffect > createState ( ) = > _SpecialEventBackgroundEffectState ( ) ;
2026-02-27 23:35:54 +01:00
}
class _SpecialEventBackgroundEffectState extends State < SpecialEventBackgroundEffect > with SingleTickerProviderStateMixin {
2026-03-01 20:59:06 +01:00
late AnimationController _controller ; late Animation < double > _scaleAnimation ; late Animation < double > _opacityAnimation ;
@ override void initState ( ) { super . initState ( ) ; _controller = AnimationController ( vsync: this , duration: const Duration ( milliseconds: 1000 ) ) . . forward ( ) ; _scaleAnimation = Tween < double > ( begin: 0.5 , end: 1.5 ) . animate ( CurvedAnimation ( parent: _controller , curve: Curves . easeOutCubic ) ) ; _opacityAnimation = Tween < double > ( begin: 0.9 , end: 0.0 ) . animate ( CurvedAnimation ( parent: _controller , curve: Curves . easeIn ) ) ; }
@ override void didUpdateWidget ( covariant SpecialEventBackgroundEffect oldWidget ) { super . didUpdateWidget ( oldWidget ) ; if ( oldWidget . text ! = widget . text ) { _controller . reset ( ) ; _controller . forward ( ) ; } }
@ override void dispose ( ) { _controller . dispose ( ) ; super . dispose ( ) ; }
@ override Widget build ( BuildContext context ) { return AnimatedBuilder ( animation: _controller , builder: ( context , child ) { return Center ( child: Transform . scale ( scale: _scaleAnimation . value , child: Opacity ( opacity: _opacityAnimation . value , child: ImageFiltered ( imageFilter: ImageFilter . blur ( sigmaX: 3.0 , sigmaY: 3.0 ) , child: Text ( widget . text , textAlign: TextAlign . center , style: _getTextStyle ( widget . themeType , TextStyle ( fontSize: 150 , fontWeight: FontWeight . w900 , color: widget . color , height: 1.0 ) ) ) ) ) ) ) ; } ) ; }
2026-02-27 23:35:54 +01:00
}