2026-02-27 23:35:54 +01:00
// ===========================================================================
// FILE: lib/ui/home/home_screen.dart
// ===========================================================================
import ' dart:ui ' ;
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-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 ;
Map < String , DateTime > _lastOnlineNotifications = { } ;
final int _selectedRadius = 4 ;
final ArenaShape _selectedShape = ArenaShape . classic ;
final bool _isTimeMode = true ;
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 14:00:00 +01:00
if ( StorageService . instance . playerName . isEmpty ) {
HomeModals . showNameDialog ( context , ( ) = > setState ( ( ) { } ) ) ;
}
2026-03-12 21:00:08 +01:00
StorageService . instance . syncLeaderboard ( ) ;
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-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 ) {
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 ' ] ;
if ( DateTime . now ( ) . difference ( lastActive . toDate ( ) ) . inSeconds < 15 ) {
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 ) {
if ( _lastOnlineNotifications . containsKey ( name ) ) {
if ( DateTime . now ( ) . difference ( _lastOnlineNotifications [ name ] ! ) . inMinutes < 5 ) 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 ;
entry = OverlayEntry (
builder: ( context ) = > Positioned (
top: MediaQuery . of ( context ) . padding . top + 15 ,
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
}
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 ) {
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 ) ;
2026-03-20 14:00:00 +01:00
Navigator . pushReplacement ( context , MaterialPageRoute ( builder: ( _ ) = > 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
// Proteggiamo anche nome e livello
Text (
" \u00A0 ${ playerName . toUpperCase ( ) } \u00A0 \u00A0 " ,
style: getSharedTextStyle ( themeType , TextStyle ( color: themeType = = AppThemeType . doodle ? inkColor : theme . text , fontWeight: FontWeight . bold , fontSize: 16 , letterSpacing: 1.0 ) ) ,
) ,
Text (
" \u00A0 LIV. $ playerLevel \u00A0 \u00A0 " ,
style: getSharedTextStyle ( themeType , TextStyle ( color: themeType = = AppThemeType . doodle ? inkColor . withOpacity ( 0.8 ) : theme . playerBlue , fontWeight: FontWeight . bold , fontSize: 11 , letterSpacing: 1.0 ) ) ,
) ,
2026-03-15 15:00:01 +01:00
] ,
) ,
] ,
) ,
) ,
) ,
2026-03-20 14:00:00 +01:00
// --- BOX STATISTICHE BLINDATO ---
2026-03-15 15:00:01 +01:00
Container (
2026-03-20 14:00:00 +01:00
padding: const EdgeInsets . symmetric ( horizontal: 14 , vertical: 10 ) , // Padding compensato
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 14:00:00 +01:00
// Doppio spazio invisibile \u00A0\u00A0 e letterSpacing per salvare la pancia a destra!
Text (
" \u00A0 ${ StorageService . instance . wins } \u00A0 \u00A0 " ,
style: getSharedTextStyle ( themeType , TextStyle (
color: themeType = = AppThemeType . doodle ? inkColor : theme . text ,
fontWeight: FontWeight . w900 ,
fontSize: 16 ,
letterSpacing: 2.0 , // Forza ulteriore spazio orizzontale
) ) ,
) ,
const SizedBox ( width: 4 ) ,
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 14:00:00 +01:00
// Idem per le sconfitte
Text (
" \u00A0 ${ StorageService . instance . losses } \u00A0 \u00A0 " ,
style: getSharedTextStyle ( themeType , TextStyle (
color: themeType = = AppThemeType . doodle ? inkColor : theme . text ,
fontWeight: FontWeight . w900 ,
fontSize: 16 ,
letterSpacing: 2.0 ,
) ) ,
) ,
const SizedBox ( width: 4 ) ,
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 ,
2026-03-20 14:00:00 +01:00
// IL TRUCCO: Spazio vuoto anche nel titolo principale
2026-03-15 15:00:01 +01:00
child: Text (
2026-03-20 14:00:00 +01:00
" ${ loc . appTitle . toUpperCase ( ) } " ,
2026-03-15 15:00:01 +01:00
style: getSharedTextStyle ( themeType , TextStyle (
2026-03-15 16:00:01 +01:00
fontSize: 65 * vScale ,
2026-03-15 15:00:01 +01:00
fontWeight: FontWeight . w900 ,
color: themeType = = AppThemeType . doodle ? inkColor : theme . text ,
2026-03-15 16:00:01 +01:00
letterSpacing: 10 * vScale ,
2026-03-15 15:00:01 +01:00
shadows: themeType = = AppThemeType . doodle
2026-03-15 16:00:01 +01:00
? [ 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 ) ]
2026-03-15 15:00:01 +01:00
) )
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: [
Expanded ( child: MusicKnobCard ( title: loc . leaderboardTitle , icon: FontAwesomeIcons . compactDisc , iconColor: Colors . amber , themeType: themeType , onTap: ( ) = > showDialog ( context: context , builder: ( ctx ) = > const LeaderboardDialog ( ) ) ) ) ,
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-15 15: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 ) = > const LeaderboardDialog ( ) ) , compact: true ) , themeType ) ) ,
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-02-27 23:35:54 +01:00
}