2026-02-27 23:35:54 +01:00
// ===========================================================================
// FILE: lib/ui/home/home_screen.dart
// ===========================================================================
import ' dart:ui ' ;
2026-03-20 22:00:01 +01:00
import ' dart:math ' ;
2026-02-27 23:35:54 +01:00
import ' package:flutter/material.dart ' ;
import ' package:provider/provider.dart ' ;
import ' package:flutter/services.dart ' ;
2026-03-14 00:00:01 +01:00
import ' package:font_awesome_flutter/font_awesome_flutter.dart ' ;
2026-03-14 19:00:00 +01:00
import ' package:cloud_firestore/cloud_firestore.dart ' ;
2026-03-20 19:00:01 +01:00
import ' package:firebase_auth/firebase_auth.dart ' ;
2026-03-14 00:00:01 +01:00
import ' dart:async ' ;
import ' package:app_links/app_links.dart ' ;
2026-03-01 20:59:06 +01:00
import ' ../../logic/game_controller.dart ' ;
2026-02-27 23:35:54 +01:00
import ' ../../core/theme_manager.dart ' ;
import ' ../../core/app_colors.dart ' ;
import ' ../../services/storage_service.dart ' ;
2026-03-14 19:00:00 +01:00
import ' ../../services/audio_service.dart ' ;
import ' ../../services/multiplayer_service.dart ' ;
2026-02-27 23:35:54 +01:00
import ' ../multiplayer/lobby_screen.dart ' ;
2026-03-12 15:00:00 +01:00
import ' ../admin/admin_screen.dart ' ;
2026-03-20 14:00:00 +01:00
import ' ../settings/settings_screen.dart ' ;
import ' ../game/game_screen.dart ' ;
2026-03-14 00:00:01 +01:00
import ' package:tetraq/l10n/app_localizations.dart ' ;
2026-02-27 23:35:54 +01:00
2026-03-14 00:00:01 +01:00
import ' ../../widgets/painters.dart ' ;
import ' ../../widgets/cyber_border.dart ' ;
import ' ../../widgets/music_theme_widgets.dart ' ;
2026-03-14 19:00:00 +01:00
import ' ../../widgets/home_buttons.dart ' ;
import ' dialog.dart ' ;
2026-03-20 14:00:00 +01:00
import ' home_modals.dart ' ;
2026-02-27 23:35:54 +01:00
class HomeScreen extends StatefulWidget {
const HomeScreen ( { super . key } ) ;
@ override
State < HomeScreen > createState ( ) = > _HomeScreenState ( ) ;
}
class _HomeScreenState extends State < HomeScreen > with WidgetsBindingObserver {
2026-03-01 20:59:06 +01:00
int _debugTapCount = 0 ;
2026-03-12 21:00:08 +01:00
late AppLinks _appLinks ;
StreamSubscription < Uri > ? _linkSubscription ;
2026-03-20 14:00:00 +01:00
StreamSubscription < QuerySnapshot > ? _favoritesSubscription ;
2026-03-20 22:00:01 +01:00
StreamSubscription < QuerySnapshot > ? _invitesSubscription ;
2026-03-20 14:00:00 +01:00
Map < String , DateTime > _lastOnlineNotifications = { } ;
final int _selectedRadius = 4 ;
final ArenaShape _selectedShape = ArenaShape . classic ;
final bool _isPublicRoom = true ;
2026-03-14 19:00:00 +01:00
bool _isLoading = false ;
String ? _myRoomCode ;
bool _roomStarted = false ;
final MultiplayerService _multiplayerService = MultiplayerService ( ) ;
2026-03-12 21:00:08 +01:00
2026-02-27 23:35:54 +01:00
@ override
void initState ( ) {
super . initState ( ) ;
WidgetsBinding . instance . addObserver ( this ) ;
WidgetsBinding . instance . addPostFrameCallback ( ( _ ) {
2026-03-20 23:00:01 +01:00
// MODIFICA QUI: Invece di currentUser == null, controlliamo se il nome è vuoto!
if ( StorageService . instance . playerName . isEmpty ) {
2026-03-20 19:00:01 +01:00
HomeModals . showNameDialog ( context , ( ) {
StorageService . instance . syncLeaderboard ( ) ;
2026-03-20 22:00:01 +01:00
_listenToInvites ( ) ;
2026-03-20 19:00:01 +01:00
setState ( ( ) { } ) ;
} ) ;
} else {
StorageService . instance . syncLeaderboard ( ) ;
2026-03-20 22:00:01 +01:00
_listenToInvites ( ) ;
2026-03-20 14:00:00 +01:00
}
2026-03-15 17:00:01 +01:00
_checkThemeSafety ( ) ;
2026-02-27 23:35:54 +01:00
} ) ;
_checkClipboardForInvite ( ) ;
2026-03-13 22:00:00 +01:00
_initDeepLinks ( ) ;
2026-03-20 14:00:00 +01:00
_listenToFavoritesOnline ( ) ;
2026-02-27 23:35:54 +01:00
}
2026-03-15 17:00:01 +01:00
void _checkThemeSafety ( ) {
String themeStr = StorageService . instance . getTheme ( ) ;
bool exists = AppThemeType . values . any ( ( e ) = > e . toString ( ) = = themeStr ) ;
if ( ! exists ) {
context . read < ThemeManager > ( ) . setTheme ( AppThemeType . doodle ) ;
}
}
2026-02-27 23:35:54 +01:00
@ override
void dispose ( ) {
WidgetsBinding . instance . removeObserver ( this ) ;
2026-03-14 19:00:00 +01:00
_cleanupGhostRoom ( ) ;
2026-03-13 22:00:00 +01:00
_linkSubscription ? . cancel ( ) ;
2026-03-20 14:00:00 +01:00
_favoritesSubscription ? . cancel ( ) ;
2026-03-20 22:00:01 +01:00
_invitesSubscription ? . cancel ( ) ;
2026-02-27 23:35:54 +01:00
super . dispose ( ) ;
}
@ override
void didChangeAppLifecycleState ( AppLifecycleState state ) {
if ( state = = AppLifecycleState . resumed ) {
_checkClipboardForInvite ( ) ;
2026-03-20 14:00:00 +01:00
_listenToFavoritesOnline ( ) ;
2026-03-14 19:00:00 +01:00
} else if ( state = = AppLifecycleState . paused | | state = = AppLifecycleState . detached ) {
_cleanupGhostRoom ( ) ;
}
}
void _cleanupGhostRoom ( ) {
if ( _myRoomCode ! = null & & ! _roomStarted ) {
FirebaseFirestore . instance . collection ( ' games ' ) . doc ( _myRoomCode ) . delete ( ) ;
_myRoomCode = null ;
2026-02-27 23:35:54 +01:00
}
}
2026-03-14 00:00:01 +01:00
Future < void > _initDeepLinks ( ) async {
_appLinks = AppLinks ( ) ;
try {
final initialUri = await _appLinks . getInitialLink ( ) ;
if ( initialUri ! = null ) _handleDeepLink ( initialUri ) ;
} catch ( e ) { debugPrint ( " Errore lettura link iniziale: $ e " ) ; }
_linkSubscription = _appLinks . uriLinkStream . listen ( ( uri ) { _handleDeepLink ( uri ) ; } , onError: ( err ) { debugPrint ( " Errore stream link: $ err " ) ; } ) ;
}
void _handleDeepLink ( Uri uri ) {
if ( uri . scheme = = ' tetraq ' & & uri . host = = ' join ' ) {
String ? code = uri . queryParameters [ ' code ' ] ;
if ( code ! = null & & code . length = = 5 ) {
Future . delayed ( const Duration ( milliseconds: 500 ) , ( ) {
2026-03-20 14:00:00 +01:00
if ( mounted ) HomeModals . showJoinPromptDialog ( context , code . toUpperCase ( ) , _joinRoomByCode ) ;
2026-03-14 00:00:01 +01:00
} ) ;
}
2026-02-27 23:35:54 +01:00
}
}
2026-03-14 00:00:01 +01:00
Future < void > _checkClipboardForInvite ( ) async {
try {
ClipboardData ? data = await Clipboard . getData ( Clipboard . kTextPlain ) ;
String ? text = data ? . text ;
if ( text ! = null & & text . contains ( " TetraQ " ) & & text . contains ( " codice: " ) ) {
RegExp regExp = RegExp ( r'codice:\s*([A-Z0-9]{5})' , caseSensitive: false ) ;
Match ? match = regExp . firstMatch ( text ) ;
if ( match ! = null ) {
String roomCode = match . group ( 1 ) ! . toUpperCase ( ) ;
await Clipboard . setData ( const ClipboardData ( text: ' ' ) ) ;
2026-03-20 14:00:00 +01:00
if ( mounted & & ModalRoute . of ( context ) ? . isCurrent = = true ) {
HomeModals . showJoinPromptDialog ( context , roomCode , _joinRoomByCode ) ;
}
2026-03-14 00:00:01 +01:00
}
}
} catch ( e ) { debugPrint ( " Errore lettura appunti: $ e " ) ; }
}
2026-03-20 14:00:00 +01:00
void _listenToFavoritesOnline ( ) {
_favoritesSubscription ? . cancel ( ) ;
final favs = StorageService . instance . favorites ;
if ( favs . isEmpty ) return ;
List < String > favUids = favs . map ( ( f ) = > f [ ' uid ' ] ! ) . toList ( ) ;
if ( favUids . length > 10 ) favUids = favUids . sublist ( 0 , 10 ) ;
_favoritesSubscription = FirebaseFirestore . instance
. collection ( ' leaderboard ' )
. where ( FieldPath . documentId , whereIn: favUids )
. snapshots ( )
. listen ( ( snapshot ) {
2026-03-20 19:00:01 +01:00
if ( ! mounted ) return ;
2026-03-20 14:00:00 +01:00
for ( var change in snapshot . docChanges ) {
if ( change . type = = DocumentChangeType . modified | | change . type = = DocumentChangeType . added ) {
var data = change . doc . data ( ) ;
if ( data ! = null & & data [ ' lastActive ' ] ! = null ) {
Timestamp lastActive = data [ ' lastActive ' ] ;
2026-03-20 19:00:01 +01:00
int diffInSeconds = DateTime . now ( ) . difference ( lastActive . toDate ( ) ) . inSeconds ;
if ( diffInSeconds . abs ( ) < 180 ) {
2026-03-20 14:00:00 +01:00
String name = data [ ' name ' ] ? ? ' Un amico ' ;
_showFavoriteOnlinePopup ( name ) ;
}
}
}
2026-03-14 19:00:00 +01:00
}
2026-03-20 14:00:00 +01:00
} ) ;
}
void _showFavoriteOnlinePopup ( String name ) {
2026-03-20 19:00:01 +01:00
if ( ! mounted ) return ;
2026-03-20 14:00:00 +01:00
if ( _lastOnlineNotifications . containsKey ( name ) ) {
2026-03-20 19:00:01 +01:00
if ( DateTime . now ( ) . difference ( _lastOnlineNotifications [ name ] ! ) . inMinutes < 1 ) return ;
2026-03-14 19:00:00 +01:00
}
2026-03-20 14:00:00 +01:00
_lastOnlineNotifications [ name ] = DateTime . now ( ) ;
final overlay = Overlay . of ( context ) ;
late OverlayEntry entry ;
bool removed = false ;
2026-03-20 23:00:01 +01:00
// --- FIX OVERLAP POPUP: Più in basso (85) ---
2026-03-20 14:00:00 +01:00
entry = OverlayEntry (
builder: ( context ) = > Positioned (
2026-03-20 23:00:01 +01:00
top: MediaQuery . of ( context ) . padding . top + 85 ,
2026-03-20 14:00:00 +01:00
left: 20 ,
right: 20 ,
child: FavoriteOnlinePopup (
name: name ,
onDismiss: ( ) {
if ( ! removed ) {
removed = true ;
entry . remove ( ) ;
}
} ,
) ,
) ,
) ;
overlay . insert ( entry ) ;
2026-03-14 19:00:00 +01:00
}
2026-03-20 19:00:01 +01:00
void _listenToInvites ( ) {
final user = FirebaseAuth . instance . currentUser ;
if ( user = = null ) return ;
_invitesSubscription ? . cancel ( ) ;
_invitesSubscription = FirebaseFirestore . instance
. collection ( ' invites ' )
. where ( ' toUid ' , isEqualTo: user . uid )
. snapshots ( )
. listen ( ( snapshot ) {
if ( ! mounted ) return ;
for ( var change in snapshot . docChanges ) {
if ( change . type = = DocumentChangeType . added ) {
var data = change . doc . data ( ) ;
if ( data ! = null ) {
String code = data [ ' roomCode ' ] ;
String from = data [ ' fromName ' ] ;
String inviteId = change . doc . id ;
Timestamp ? ts = data [ ' timestamp ' ] ;
if ( ts ! = null ) {
if ( DateTime . now ( ) . difference ( ts . toDate ( ) ) . inMinutes > 2 ) {
FirebaseFirestore . instance . collection ( ' invites ' ) . doc ( inviteId ) . delete ( ) ;
continue ;
}
}
_showInvitePopup ( from , code , inviteId ) ;
}
}
}
} ) ;
}
void _showInvitePopup ( String fromName , String roomCode , String inviteId ) {
final themeType = context . read < ThemeManager > ( ) . currentThemeType ;
final theme = context . read < ThemeManager > ( ) . currentColors ;
showDialog (
context: context ,
barrierDismissible: false ,
builder: ( ctx ) = > AlertDialog (
backgroundColor: themeType = = AppThemeType . doodle ? Colors . white : theme . background ,
shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 20 ) , side: BorderSide ( color: theme . playerRed , width: 2 ) ) ,
title: Row (
children: [
Icon ( Icons . warning_amber_rounded , color: theme . playerRed ) ,
const SizedBox ( width: 10 ) ,
Text ( " SFIDA IN ARRIVO! " , style: getSharedTextStyle ( themeType , TextStyle ( color: theme . playerRed , fontWeight: FontWeight . bold , fontSize: 18 ) ) ) ,
] ,
) ,
content: Text ( " $ fromName ti ha sfidato a duello! \n Accetti la sfida? " , style: getSharedTextStyle ( themeType , TextStyle ( color: theme . text , fontSize: 16 ) ) ) ,
actions: [
TextButton (
onPressed: ( ) {
FirebaseFirestore . instance . collection ( ' invites ' ) . doc ( inviteId ) . delete ( ) ;
Navigator . pop ( ctx ) ;
} ,
child: Text ( " RIFIUTA " , style: getSharedTextStyle ( themeType , const TextStyle ( color: Colors . grey , fontWeight: FontWeight . bold ) ) ) ,
) ,
ElevatedButton (
style: ElevatedButton . styleFrom ( backgroundColor: theme . playerBlue , foregroundColor: Colors . white , shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 10 ) ) ) ,
onPressed: ( ) {
FirebaseFirestore . instance . collection ( ' invites ' ) . doc ( inviteId ) . delete ( ) ;
Navigator . pop ( ctx ) ;
_joinRoomByCode ( roomCode ) ;
} ,
child: Text ( " ACCETTA! " , style: getSharedTextStyle ( themeType , const TextStyle ( fontWeight: FontWeight . bold ) ) ) ,
) ,
] ,
)
) ;
}
2026-03-20 22:00:01 +01:00
void _startDirectChallengeFlow ( String targetUid , String targetName ) {
HomeModals . showChallengeSetupDialog (
context ,
targetName ,
( int radius , ArenaShape shape , String timeMode ) {
_executeSendChallenge ( targetUid , targetName , radius , shape , timeMode ) ;
}
) ;
}
Future < void > _executeSendChallenge ( String targetUid , String targetName , int radius , ArenaShape shape , String timeMode ) async {
2026-03-20 19:00:01 +01:00
setState ( ( ) = > _isLoading = true ) ;
const chars = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ' ;
final rnd = Random ( ) ;
String roomCode = String . fromCharCodes ( Iterable . generate ( 5 , ( _ ) = > chars . codeUnitAt ( rnd . nextInt ( chars . length ) ) ) ) ;
try {
2026-03-20 20:00:01 +01:00
int gameSeed = rnd . nextInt ( 9999999 ) ;
2026-03-20 19:00:01 +01:00
await FirebaseFirestore . instance . collection ( ' games ' ) . doc ( roomCode ) . set ( {
' status ' : ' waiting ' ,
' hostName ' : StorageService . instance . playerName ,
' hostUid ' : FirebaseAuth . instance . currentUser ? . uid ,
2026-03-20 22:00:01 +01:00
' radius ' : radius ,
' shape ' : shape . name ,
' timeMode ' : timeMode ,
' isPublic ' : false ,
2026-03-20 19:00:01 +01:00
' createdAt ' : FieldValue . serverTimestamp ( ) ,
2026-03-20 20:00:01 +01:00
' players ' : [ FirebaseAuth . instance . currentUser ? . uid ] ,
' turn ' : 0 ,
' moves ' : [ ] ,
2026-03-20 22:00:01 +01:00
' seed ' : gameSeed ,
2026-03-20 19:00:01 +01:00
} ) ;
await FirebaseFirestore . instance . collection ( ' invites ' ) . add ( {
' toUid ' : targetUid ,
' fromName ' : StorageService . instance . playerName ,
' roomCode ' : roomCode ,
' timestamp ' : FieldValue . serverTimestamp ( ) ,
} ) ;
setState ( ( ) = > _isLoading = false ) ;
if ( mounted ) {
HomeModals . showWaitingDialog (
context: context ,
code: roomCode ,
isPublicRoom: false ,
2026-03-20 22:00:01 +01:00
selectedRadius: radius ,
selectedShape: shape ,
selectedTimeMode: timeMode ,
2026-03-20 19:00:01 +01:00
multiplayerService: _multiplayerService ,
onRoomStarted: ( ) { } ,
onCleanup: ( ) {
FirebaseFirestore . instance . collection ( ' games ' ) . doc ( roomCode ) . delete ( ) ;
}
) ;
}
} catch ( e ) {
setState ( ( ) = > _isLoading = false ) ;
if ( mounted ) {
ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( " Errore: $ e " , style: const TextStyle ( color: Colors . white ) ) , backgroundColor: Colors . red ) ) ;
}
}
}
2026-03-14 19:00:00 +01:00
Future < void > _joinRoomByCode ( String code ) async {
if ( _isLoading ) return ;
FocusScope . of ( context ) . unfocus ( ) ;
setState ( ( ) = > _isLoading = true ) ;
try {
String playerName = StorageService . instance . playerName ;
if ( playerName . isEmpty ) playerName = " GUEST " ;
Map < String , dynamic > ? roomData = await _multiplayerService . joinGameRoom ( code , playerName ) ;
if ( ! mounted ) return ;
setState ( ( ) = > _isLoading = false ) ;
if ( roomData ! = null ) {
2026-03-20 22:00:01 +01:00
ScaffoldMessenger . of ( context ) . showSnackBar ( const SnackBar ( content: Text ( " La sfida inizierà a breve... " ) , backgroundColor: Colors . green ) ) ;
2026-03-14 19:00:00 +01:00
int hostRadius = roomData [ ' radius ' ] ? ? 4 ;
String shapeStr = roomData [ ' shape ' ] ? ? ' classic ' ;
ArenaShape hostShape = ArenaShape . values . firstWhere ( ( e ) = > e . name = = shapeStr , orElse: ( ) = > ArenaShape . classic ) ;
2026-03-20 22:00:01 +01:00
String hostTimeMode = roomData [ ' timeMode ' ] is String ? roomData [ ' timeMode ' ] : ( roomData [ ' timeMode ' ] = = true ? ' fixed ' : ' relax ' ) ;
2026-03-14 19:00:00 +01:00
context . read < GameController > ( ) . startNewGame ( hostRadius , isOnline: true , roomCode: code , isHost: false , shape: hostShape , timeMode: hostTimeMode ) ;
2026-03-20 22:00:01 +01:00
Navigator . push ( context , MaterialPageRoute ( builder: ( _ ) = > const GameScreen ( ) ) ) ;
2026-03-14 19:00:00 +01:00
} else {
2026-03-20 14:00:00 +01:00
ScaffoldMessenger . of ( context ) . showSnackBar ( const SnackBar ( content: Text ( " Stanza non trovata, piena o partita già iniziata. " , style: TextStyle ( color: Colors . white ) ) , backgroundColor: Colors . red ) ) ;
2026-03-14 19:00:00 +01:00
}
} catch ( e ) {
2026-03-20 14:00:00 +01:00
if ( mounted ) {
setState ( ( ) = > _isLoading = false ) ;
ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( " Errore di connessione: $ e " , style: const TextStyle ( color: Colors . white ) ) , backgroundColor: Colors . red ) ) ;
}
2026-03-14 19:00:00 +01:00
}
}
2026-03-15 15:00:01 +01:00
BoxDecoration _glassBoxDecoration ( ThemeColors theme , AppThemeType themeType ) {
return BoxDecoration (
2026-03-15 16:00:01 +01:00
color: themeType = = AppThemeType . doodle ? Colors . white : null ,
gradient: themeType = = AppThemeType . doodle ? null : LinearGradient (
begin: Alignment . topLeft ,
end: Alignment . bottomRight ,
colors: [
Colors . white . withOpacity ( 0.25 ) ,
Colors . white . withOpacity ( 0.05 ) ,
] ,
) ,
2026-03-15 15:00:01 +01:00
borderRadius: BorderRadius . circular ( 25 ) ,
border: Border . all (
2026-03-15 16:00:01 +01:00
color: themeType = = AppThemeType . doodle ? theme . text : Colors . white . withOpacity ( 0.3 ) ,
2026-03-15 15:00:01 +01:00
width: themeType = = AppThemeType . doodle ? 2 : 1.5 ,
) ,
boxShadow: themeType = = AppThemeType . doodle
? [ BoxShadow ( color: theme . text . withOpacity ( 0.8 ) , offset: const Offset ( 4 , 4 ) , blurRadius: 0 ) ]
: [ BoxShadow ( color: Colors . black . withOpacity ( 0.2 ) , blurRadius: 10 ) ] ,
) ;
}
Widget _buildTopBar ( BuildContext context , ThemeColors theme , AppThemeType themeType , String playerName , int playerLevel ) {
Color inkColor = const Color ( 0xFF111122 ) ;
return Padding (
padding: const EdgeInsets . only ( top: 5.0 , left: 15.0 , right: 15.0 , bottom: 10.0 ) ,
child: Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
GestureDetector (
2026-03-20 14:00:00 +01:00
onTap: ( ) = > HomeModals . showNameDialog ( context , ( ) = > setState ( ( ) { } ) ) ,
2026-03-15 15:00:01 +01:00
child: Container (
padding: const EdgeInsets . symmetric ( horizontal: 12 , vertical: 8 ) ,
decoration: _glassBoxDecoration ( theme , themeType ) ,
child: Row (
mainAxisSize: MainAxisSize . min ,
children: [
CircleAvatar (
radius: 18 ,
backgroundColor: theme . playerBlue . withOpacity ( 0.2 ) ,
child: Icon ( Icons . person , color: theme . playerBlue , size: 20 ) ,
) ,
const SizedBox ( width: 10 ) ,
Column (
crossAxisAlignment: CrossAxisAlignment . start ,
mainAxisSize: MainAxisSize . min ,
children: [
2026-03-20 14:00:00 +01:00
Text (
2026-03-20 19:00:01 +01:00
playerName . toUpperCase ( ) ,
style: getSharedTextStyle ( themeType , TextStyle ( color: themeType = = AppThemeType . doodle ? inkColor : theme . text , fontWeight: FontWeight . bold , fontSize: 16 ) ) ,
overflow: TextOverflow . visible ,
softWrap: false ,
2026-03-20 14:00:00 +01:00
) ,
Text (
2026-03-20 19:00:01 +01:00
" LIV. $ playerLevel " ,
style: getSharedTextStyle ( themeType , TextStyle ( color: themeType = = AppThemeType . doodle ? inkColor . withOpacity ( 0.8 ) : theme . playerBlue , fontWeight: FontWeight . bold , fontSize: 11 ) ) ,
overflow: TextOverflow . visible ,
softWrap: false ,
2026-03-20 14:00:00 +01:00
) ,
2026-03-15 15:00:01 +01:00
] ,
) ,
] ,
) ,
) ,
) ,
2026-03-20 19:00:01 +01:00
// --- BOX STATISTICHE ---
2026-03-15 15:00:01 +01:00
Container (
2026-03-20 19:00:01 +01:00
padding: const EdgeInsets . symmetric ( horizontal: 12 , vertical: 10 ) ,
2026-03-15 15:00:01 +01:00
decoration: _glassBoxDecoration ( theme , themeType ) ,
child: Row (
mainAxisSize: MainAxisSize . min ,
2026-03-20 14:00:00 +01:00
crossAxisAlignment: CrossAxisAlignment . center ,
2026-03-15 15:00:01 +01:00
children: [
Icon ( themeType = = AppThemeType . music ? FontAwesomeIcons . microphone : Icons . emoji_events , color: Colors . amber . shade600 , size: 16 ) ,
2026-03-20 19:00:01 +01:00
const SizedBox ( width: 6 ) ,
2026-03-20 14:00:00 +01:00
Text (
2026-03-20 19:00:01 +01:00
" ${ StorageService . instance . wins } " ,
2026-03-20 14:00:00 +01:00
style: getSharedTextStyle ( themeType , TextStyle (
color: themeType = = AppThemeType . doodle ? inkColor : theme . text ,
fontWeight: FontWeight . w900 ,
fontSize: 16 ,
) ) ,
2026-03-20 19:00:01 +01:00
overflow: TextOverflow . visible ,
softWrap: false ,
2026-03-20 14:00:00 +01:00
) ,
2026-03-20 19:00:01 +01:00
const SizedBox ( width: 10 ) ,
2026-03-15 15:00:01 +01:00
Icon ( themeType = = AppThemeType . music ? FontAwesomeIcons . compactDisc : Icons . sentiment_very_dissatisfied , color: theme . playerRed . withOpacity ( 0.8 ) , size: 16 ) ,
2026-03-20 19:00:01 +01:00
const SizedBox ( width: 6 ) ,
2026-03-15 15:00:01 +01:00
2026-03-20 14:00:00 +01:00
Text (
2026-03-20 19:00:01 +01:00
" ${ StorageService . instance . losses } " ,
2026-03-20 14:00:00 +01:00
style: getSharedTextStyle ( themeType , TextStyle (
color: themeType = = AppThemeType . doodle ? inkColor : theme . text ,
fontWeight: FontWeight . w900 ,
fontSize: 16 ,
) ) ,
2026-03-20 19:00:01 +01:00
overflow: TextOverflow . visible ,
softWrap: false ,
2026-03-20 14:00:00 +01:00
) ,
2026-03-20 19:00:01 +01:00
const SizedBox ( width: 10 ) ,
2026-03-15 15:00:01 +01:00
Container ( width: 1 , height: 20 , color: ( themeType = = AppThemeType . doodle ? inkColor : Colors . white ) . withOpacity ( 0.2 ) ) ,
2026-03-20 14:00:00 +01:00
const SizedBox ( width: 10 ) ,
2026-03-15 15:00:01 +01:00
AnimatedBuilder (
animation: AudioService . instance ,
builder: ( context , child ) {
bool isMuted = AudioService . instance . isMuted ;
return GestureDetector (
behavior: HitTestBehavior . opaque ,
onTap: ( ) {
AudioService . instance . toggleMute ( ) ;
} ,
child: Icon (
isMuted ? Icons . volume_off : Icons . volume_up ,
color: isMuted ? theme . playerRed : ( themeType = = AppThemeType . doodle ? inkColor : theme . text ) ,
size: 20 ,
) ,
) ;
}
) ,
] ,
) ,
) ,
] ,
) ,
) ;
}
2026-02-27 23:35:54 +01:00
Widget _buildCyberCard ( Widget card , AppThemeType themeType ) {
2026-03-14 00:00:01 +01:00
if ( themeType = = AppThemeType . cyberpunk ) return AnimatedCyberBorder ( child: card ) ;
return card ;
2026-02-27 23:35:54 +01:00
}
@ override
Widget build ( BuildContext context ) {
SystemChrome . setEnabledSystemUIMode ( SystemUiMode . immersiveSticky ) ;
SystemChrome . setPreferredOrientations ( [ DeviceOrientation . portraitUp , DeviceOrientation . portraitDown ] ) ;
final themeManager = context . watch < ThemeManager > ( ) ;
final themeType = themeManager . currentThemeType ;
final theme = themeManager . currentColors ;
Color inkColor = const Color ( 0xFF111122 ) ;
2026-03-05 15:00:00 +01:00
final loc = AppLocalizations . of ( context ) ! ;
2026-02-27 23:35:54 +01:00
String ? bgImage ;
2026-03-14 19:00:00 +01:00
if ( themeType = = AppThemeType . doodle ) bgImage = ' assets/images/doodle_bg.jpg ' ;
2026-02-27 23:35:54 +01:00
if ( themeType = = AppThemeType . cyberpunk ) bgImage = ' assets/images/cyber_bg.jpg ' ;
2026-03-13 23:00:01 +01:00
if ( themeType = = AppThemeType . music ) bgImage = ' assets/images/music_bg.jpg ' ;
2026-03-15 02:00:01 +01:00
if ( themeType = = AppThemeType . arcade ) bgImage = ' assets/images/arcade.jpg ' ;
2026-03-15 15:00:01 +01:00
if ( themeType = = AppThemeType . grimorio ) bgImage = ' assets/images/grimorio.jpg ' ;
2026-02-27 23:35:54 +01:00
String playerName = StorageService . instance . playerName ;
if ( playerName . isEmpty ) playerName = " GUEST " ;
2026-03-15 15:00:01 +01:00
int playerLevel = StorageService . instance . playerLevel ;
2026-03-01 20:59:06 +01:00
2026-03-15 16:00:01 +01:00
final double screenHeight = MediaQuery . of ( context ) . size . height ;
final double vScale = ( screenHeight / 920.0 ) . clamp ( 0.50 , 1.0 ) ;
2026-02-27 23:35:54 +01:00
Widget uiContent = SafeArea (
2026-03-15 15:00:01 +01:00
child: Column (
children: [
_buildTopBar ( context , theme , themeType , playerName , playerLevel ) ,
Expanded (
child: SingleChildScrollView (
physics: const BouncingScrollPhysics ( ) ,
child: Padding (
padding: const EdgeInsets . symmetric ( horizontal: 20.0 ) ,
child: Column (
mainAxisAlignment: MainAxisAlignment . center ,
children: [
2026-03-15 16:00:01 +01:00
SizedBox ( height: 20 * vScale ) ,
2026-03-15 15:00:01 +01:00
Center (
child: Transform . rotate (
angle: themeType = = AppThemeType . doodle ? - 0.04 : 0 ,
child: GestureDetector (
onTap: ( ) {
2026-03-15 16:00:01 +01:00
if ( playerName . toUpperCase ( ) = = ' PAOLO ' ) {
_debugTapCount + + ;
if ( _debugTapCount = = 5 ) {
StorageService . instance . addXP ( 2000 ) ;
setState ( ( ) { } ) ;
ScaffoldMessenger . of ( context ) . showSnackBar (
SnackBar ( content: Text ( " 🛠 DEBUG MODE: +20 Livelli! " , style: getSharedTextStyle ( themeType , const TextStyle ( color: Colors . white , fontWeight: FontWeight . bold ) ) ) , backgroundColor: Colors . purpleAccent , behavior: SnackBarBehavior . floating , shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 15 ) ) )
) ;
} else if ( _debugTapCount > = 7 ) {
_debugTapCount = 0 ;
2026-03-20 14:00:00 +01:00
Navigator . push ( context , MaterialPageRoute ( builder: ( _ ) = > AdminScreen ( ) ) ) ;
2026-03-15 16:00:01 +01:00
}
2026-03-15 15:00:01 +01:00
}
} ,
child: FittedBox (
fit: BoxFit . scaleDown ,
child: Text (
2026-03-20 19:00:01 +01:00
loc . appTitle . toUpperCase ( ) ,
style: getSharedTextStyle ( themeType , TextStyle (
fontSize: 65 * vScale ,
fontWeight: FontWeight . w900 ,
color: themeType = = AppThemeType . doodle ? inkColor : theme . text ,
letterSpacing: 10 * vScale ,
shadows: themeType = = AppThemeType . doodle
? [ const Shadow ( color: Colors . white , offset: Offset ( 2.5 , 2.5 ) , blurRadius: 2 ) , const Shadow ( color: Colors . white , offset: Offset ( - 2.5 , - 2.5 ) , blurRadius: 2 ) ]
: [ Shadow ( color: Colors . black . withOpacity ( 0.8 ) , offset: const Offset ( 3 , 4 ) , blurRadius: 8 ) , Shadow ( color: theme . playerBlue . withOpacity ( 0.4 ) , offset: const Offset ( 0 , 0 ) , blurRadius: 20 ) ]
) ) ,
overflow: TextOverflow . visible ,
softWrap: false ,
2026-03-01 20:59:06 +01:00
) ,
) ,
2026-03-15 15:00:01 +01:00
) ,
) ,
) ,
2026-03-15 16:00:01 +01:00
SizedBox ( height: 40 * vScale ) ,
2026-03-15 15:00:01 +01:00
if ( themeType = = AppThemeType . music ) . . . [
2026-03-20 14:00:00 +01:00
MusicCassetteCard ( title: loc . onlineTitle , subtitle: loc . onlineSub , neonColor: Colors . blueAccent , angle: - 0.04 , leftIcon: FontAwesomeIcons . sliders , rightIcon: FontAwesomeIcons . globe , themeType: themeType , onTap: ( ) { Navigator . push ( context , MaterialPageRoute ( builder: ( _ ) = > LobbyScreen ( ) ) ) ; } ) ,
2026-03-15 16:00:01 +01:00
SizedBox ( height: 12 * vScale ) ,
2026-03-20 14:00:00 +01:00
MusicCassetteCard ( title: loc . cpuTitle , subtitle: loc . cpuSub , neonColor: Colors . purpleAccent , angle: 0.03 , leftIcon: FontAwesomeIcons . desktop , rightIcon: FontAwesomeIcons . music , themeType: themeType , onTap: ( ) = > HomeModals . showMatchSetupDialog ( context , true ) ) ,
2026-03-15 16:00:01 +01:00
SizedBox ( height: 12 * vScale ) ,
2026-03-20 14:00:00 +01:00
MusicCassetteCard ( title: loc . localTitle , subtitle: loc . localSub , neonColor: Colors . deepPurpleAccent , angle: - 0.02 , leftIcon: FontAwesomeIcons . headphones , rightIcon: FontAwesomeIcons . headphones , themeType: themeType , onTap: ( ) = > HomeModals . showMatchSetupDialog ( context , false ) ) ,
2026-03-15 16:00:01 +01:00
SizedBox ( height: 30 * vScale ) ,
2026-03-15 15:00:01 +01:00
Row (
mainAxisAlignment: MainAxisAlignment . spaceEvenly , crossAxisAlignment: CrossAxisAlignment . start ,
children: [
2026-03-20 22:00:01 +01:00
Expanded ( child: MusicKnobCard ( title: loc . leaderboardTitle , icon: FontAwesomeIcons . compactDisc , iconColor: Colors . amber , themeType: themeType , onTap: ( ) = > showDialog ( context: context , builder: ( ctx ) = > LeaderboardDialog ( onChallenge: _startDirectChallengeFlow ) ) ) ) ,
2026-03-15 15:00:01 +01:00
Expanded ( child: MusicKnobCard ( title: loc . questsTitle , icon: FontAwesomeIcons . microphoneLines , themeType: themeType , onTap: ( ) = > showDialog ( context: context , builder: ( ctx ) = > const QuestsDialog ( ) ) ) ) ,
2026-03-20 14:00:00 +01:00
Expanded ( child: MusicKnobCard ( title: loc . themesTitle , icon: FontAwesomeIcons . palette , themeType: themeType , onTap: ( ) = > Navigator . push ( context , MaterialPageRoute ( builder: ( _ ) = > SettingsScreen ( ) ) ) ) ) ,
2026-03-15 15:00:01 +01:00
Expanded ( child: MusicKnobCard ( title: loc . tutorialTitle , icon: FontAwesomeIcons . bookOpen , themeType: themeType , onTap: ( ) = > showDialog ( context: context , builder: ( ctx ) = > const TutorialDialog ( ) ) ) ) ,
] ,
) ,
] else . . . [
Column (
crossAxisAlignment: CrossAxisAlignment . stretch ,
children: [
2026-03-20 14:00:00 +01:00
_buildCyberCard ( FeatureCard ( title: loc . onlineTitle , subtitle: loc . onlineSub , icon: Icons . public , color: Colors . lightBlue . shade200 , theme: theme , themeType: themeType , isFeatured: true , onTap: ( ) { Navigator . push ( context , MaterialPageRoute ( builder: ( _ ) = > LobbyScreen ( ) ) ) ; } ) , themeType ) ,
2026-03-15 16:00:01 +01:00
SizedBox ( height: 12 * vScale ) ,
2026-03-20 14:00:00 +01:00
_buildCyberCard ( FeatureCard ( title: loc . cpuTitle , subtitle: loc . cpuSub , icon: Icons . smart_toy , color: Colors . purple . shade200 , theme: theme , themeType: themeType , onTap: ( ) = > HomeModals . showMatchSetupDialog ( context , true ) ) , themeType ) ,
2026-03-15 16:00:01 +01:00
SizedBox ( height: 12 * vScale ) ,
2026-03-20 14:00:00 +01:00
_buildCyberCard ( FeatureCard ( title: loc . localTitle , subtitle: loc . localSub , icon: Icons . people_alt , color: Colors . red . shade200 , theme: theme , themeType: themeType , onTap: ( ) = > HomeModals . showMatchSetupDialog ( context , false ) ) , themeType ) ,
2026-03-15 16:00:01 +01:00
SizedBox ( height: 12 * vScale ) ,
2026-03-01 20:59:06 +01:00
2026-03-15 15:00:01 +01:00
Row (
2026-03-14 19:00:00 +01:00
children: [
2026-03-20 22:00:01 +01:00
Expanded ( child: _buildCyberCard ( FeatureCard ( title: loc . leaderboardTitle , subtitle: " Top 50 Globale " , icon: Icons . leaderboard , color: Colors . amber . shade200 , theme: theme , themeType: themeType , onTap: ( ) = > showDialog ( context: context , builder: ( ctx ) = > LeaderboardDialog ( onChallenge: _startDirectChallengeFlow ) ) , compact: true ) , themeType ) ) ,
2026-03-15 15:00:01 +01:00
const SizedBox ( width: 12 ) ,
Expanded ( child: _buildCyberCard ( FeatureCard ( title: loc . questsTitle , subtitle: " Missioni " , icon: Icons . assignment_turned_in , color: Colors . green . shade200 , theme: theme , themeType: themeType , onTap: ( ) = > showDialog ( context: context , builder: ( ctx ) = > const QuestsDialog ( ) ) , compact: true ) , themeType ) ) ,
2026-03-14 19:00:00 +01:00
] ,
2026-03-01 20:59:06 +01:00
) ,
2026-02-27 23:35:54 +01:00
2026-03-15 16:00:01 +01:00
SizedBox ( height: 12 * vScale ) ,
2026-03-01 20:59:06 +01:00
2026-03-15 15:00:01 +01:00
Row (
children: [
2026-03-20 14:00:00 +01:00
Expanded ( child: _buildCyberCard ( FeatureCard ( title: loc . themesTitle , subtitle: " Personalizza " , icon: Icons . palette , color: Colors . teal . shade200 , theme: theme , themeType: themeType , onTap: ( ) = > Navigator . push ( context , MaterialPageRoute ( builder: ( _ ) = > SettingsScreen ( ) ) ) , compact: true ) , themeType ) ) ,
2026-03-15 15:00:01 +01:00
const SizedBox ( width: 12 ) ,
Expanded ( child: _buildCyberCard ( FeatureCard ( title: loc . tutorialTitle , subtitle: " Come giocare " , icon: Icons . school , color: Colors . indigo . shade200 , theme: theme , themeType: themeType , onTap: ( ) = > showDialog ( context: context , builder: ( ctx ) = > const TutorialDialog ( ) ) , compact: true ) , themeType ) ) ,
] ,
) ,
] ,
) ,
2026-03-01 20:59:06 +01:00
] ,
2026-03-15 16:00:01 +01:00
SizedBox ( height: 40 * vScale ) ,
2026-03-15 15:00:01 +01:00
] ,
2026-02-27 23:35:54 +01:00
) ,
2026-03-01 20:59:06 +01:00
) ,
2026-02-27 23:35:54 +01:00
) ,
2026-03-15 15:00:01 +01:00
) ,
] ,
2026-02-27 23:35:54 +01:00
) ,
) ;
return Scaffold (
2026-03-15 03:32:09 +01:00
backgroundColor: bgImage ! = null ? Colors . transparent : theme . background ,
2026-03-15 16:00:01 +01:00
extendBodyBehindAppBar: true ,
2026-02-27 23:35:54 +01:00
body: Stack (
children: [
2026-03-13 23:00:01 +01:00
Container ( color: themeType = = AppThemeType . doodle ? Colors . white : theme . background ) ,
2026-03-20 14:00:00 +01:00
if ( themeType = = AppThemeType . doodle )
Positioned . fill (
child: CustomPaint (
painter: FullScreenGridPainter ( Colors . blue . withOpacity ( 0.15 ) ) ,
) ,
) ,
2026-02-27 23:35:54 +01:00
if ( bgImage ! = null )
2026-03-14 19:00:00 +01:00
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 19:00:00 +01:00
) ,
) ,
2026-03-20 14: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 19: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-20 14:00:00 +01:00
2026-03-13 23:00:01 +01:00
if ( themeType = = AppThemeType . music )
2026-03-14 19:00:00 +01:00
Positioned . fill (
child: IgnorePointer (
child: CustomPaint (
painter: AudioCablesPainter ( ) ,
) ,
) ,
) ,
2026-03-20 14:00:00 +01:00
2026-02-27 23:35:54 +01:00
Positioned . fill ( child: uiContent ) ,
] ,
) ,
) ;
}
2026-03-20 14:00:00 +01:00
}
class FullScreenGridPainter extends CustomPainter {
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-03-20 19:00:01 +01:00
}
class FavoriteOnlinePopup extends StatefulWidget {
final String name ;
final VoidCallback onDismiss ;
const FavoriteOnlinePopup ( { super . key , required this . name , required this . onDismiss } ) ;
@ override
State < FavoriteOnlinePopup > createState ( ) = > _FavoriteOnlinePopupState ( ) ;
}
class _FavoriteOnlinePopupState extends State < FavoriteOnlinePopup > with SingleTickerProviderStateMixin {
late AnimationController _controller ;
late Animation < Offset > _offsetAnimation ;
@ override
void initState ( ) {
super . initState ( ) ;
_controller = AnimationController ( vsync: this , duration: const Duration ( milliseconds: 400 ) ) ;
_offsetAnimation = Tween < Offset > ( begin: const Offset ( 0.0 , - 1.5 ) , end: Offset . zero )
. animate ( CurvedAnimation ( parent: _controller , curve: Curves . easeOutBack ) ) ;
_controller . forward ( ) ;
Future . delayed ( const Duration ( seconds: 3 ) , ( ) {
if ( mounted ) {
_controller . reverse ( ) . then ( ( _ ) = > widget . onDismiss ( ) ) ;
}
} ) ;
}
@ override
void dispose ( ) {
_controller . dispose ( ) ;
super . dispose ( ) ;
}
@ override
Widget build ( BuildContext context ) {
final themeManager = context . watch < ThemeManager > ( ) ;
final themeType = themeManager . currentThemeType ;
final theme = themeManager . currentColors ;
Color inkColor = const Color ( 0xFF111122 ) ;
return SlideTransition (
position: _offsetAnimation ,
child: Material (
color: Colors . transparent ,
2026-03-20 23:00:01 +01:00
// --- FIX OVERLAP POPUP: Aggiunta Elevation Altissima (100) ---
elevation: 100 ,
2026-03-20 19:00:01 +01:00
child: Container (
padding: const EdgeInsets . symmetric ( horizontal: 16 , vertical: 12 ) ,
decoration: BoxDecoration (
color: themeType = = AppThemeType . doodle ? Colors . white : theme . background ,
borderRadius: BorderRadius . circular ( 20 ) ,
border: Border . all (
color: themeType = = AppThemeType . doodle ? inkColor : theme . playerBlue ,
width: 2
) ,
boxShadow: [
BoxShadow (
color: Colors . black . withOpacity ( 0.3 ) ,
blurRadius: 10 ,
offset: const Offset ( 0 , 5 )
)
] ,
) ,
child: Row (
mainAxisSize: MainAxisSize . min ,
mainAxisAlignment: MainAxisAlignment . center ,
children: [
const Icon ( Icons . circle , color: Colors . greenAccent , size: 14 ) ,
const SizedBox ( width: 10 ) ,
Text (
" ${ widget . name } è online! " ,
style: getSharedTextStyle (
themeType ,
TextStyle (
color: themeType = = AppThemeType . doodle ? inkColor : theme . text ,
fontWeight: FontWeight . bold ,
fontSize: 15
)
) ,
) ,
] ,
) ,
) ,
) ,
) ;
}
2026-02-27 23:35:54 +01:00
}