From 3c28c1f0c2612bd1eb95d897cec1c66896e69e48 Mon Sep 17 00:00:00 2001 From: Paolo Date: Sat, 14 Mar 2026 19:00:00 +0100 Subject: [PATCH] Auto-sync: 20260314_190000 --- .DS_Store | Bin 10244 -> 10244 bytes lib/logic/game_controller.dart | 25 +- lib/services/audio_service.dart | 3 + lib/ui/game/score_board.dart | 60 +++- lib/ui/home/home_screen.dart | 590 ++++++++++++++++++++++++++++---- 5 files changed, 587 insertions(+), 91 deletions(-) diff --git a/.DS_Store b/.DS_Store index 520e0c48c20ace2a5dded23d076fb54a0868d888..928743bc64dd671d7c9be0340f206f616bf7d24d 100644 GIT binary patch delta 14 VcmZn(XbISGLxhoG^Gy+LVE`+g1tb6f delta 14 VcmZn(XbISGLxhoW^Gy+LVE`+m1tkCg diff --git a/lib/logic/game_controller.dart b/lib/logic/game_controller.dart index 46ce077..201c745 100644 --- a/lib/logic/game_controller.dart +++ b/lib/logic/game_controller.dart @@ -326,16 +326,21 @@ class GameController extends ChangeNotifier { void _handleTimeOut() { if (!isTimeMode || isSetupPhase) return; - if (isOnline) { - Line randomMove = AIEngine.getBestMove(board, 5); - handleLineTap(randomMove, _activeTheme, forced: true); - } else if (isVsCPU && board.currentPlayer == Player.red) { - Line randomMove = AIEngine.getBestMove(board, cpuLevel); - handleLineTap(randomMove, _activeTheme, forced: true); - } else if (!isVsCPU) { - Line randomMove = AIEngine.getBestMove(board, 5); - handleLineTap(randomMove, _activeTheme, forced: true); - } + // Solo chi deve giocare può subire il timeout (se è online) + if (isOnline && board.currentPlayer != myPlayer) return; + + // 1. Raccogliamo TUTTE le linee ancora libere e giocabili + List availableLines = board.lines.where((l) => l.owner == Player.none && l.isPlayable).toList(); + + // Sicurezza: se non ci sono mosse, non facciamo nulla + if (availableLines.isEmpty) return; + + // 2. Scegliamo una linea in modo PURAMENTE CASUALE (nessuna intelligenza artificiale) + final random = Random(); + Line randomMove = availableLines[random.nextInt(availableLines.length)]; + + // 3. Eseguiamo la mossa forzata + handleLineTap(randomMove, _activeTheme, forced: true); } void disconnectOnlineGame() { diff --git a/lib/services/audio_service.dart b/lib/services/audio_service.dart index 3f09fee..6ce88b6 100644 --- a/lib/services/audio_service.dart +++ b/lib/services/audio_service.dart @@ -29,8 +29,11 @@ class AudioService extends ChangeNotifier { await prefs.setBool('isMuted', isMuted); if (isMuted) { + // Se abbiamo appena silenziato, FERMA TUTTO immediatamente. await _bgmPlayer.pause(); + await _sfxPlayer.stop(); } else { + // Se riaccendiamo, fai ripartire la canzone playBgm(_currentTheme); } notifyListeners(); diff --git a/lib/ui/game/score_board.dart b/lib/ui/game/score_board.dart index be08260..20c7c40 100644 --- a/lib/ui/game/score_board.dart +++ b/lib/ui/game/score_board.dart @@ -11,6 +11,7 @@ import '../../core/theme_manager.dart'; import '../../services/audio_service.dart'; import '../../core/app_colors.dart'; import '../../services/storage_service.dart'; +import '../home/dialog.dart'; // <--- IMPORTANTE: Importa il TutorialDialog TextStyle _getTextStyle(AppThemeType themeType, TextStyle baseStyle) { if (themeType == AppThemeType.doodle) { @@ -101,13 +102,58 @@ class _ScoreBoardState extends State { : [Shadow(color: Colors.black.withOpacity(0.3), offset: const Offset(1, 2), blurRadius: 2)] )) ), - IconButton( - icon: Icon(isMuted ? Icons.volume_off : Icons.volume_up, color: theme.text.withOpacity(0.7)), - onPressed: () { - setState(() { - AudioService.instance.toggleMute(); - }); - }, + const SizedBox(height: 8), + + // --- ROW DEI PULSANTI AGGIORNATA --- + Row( + mainAxisSize: MainAxisSize.min, + children: [ + // TASTO AUDIO CON CONTORNO + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + AudioService.instance.toggleMute(); + }); + }, + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: themeType == AppThemeType.doodle ? Colors.transparent : theme.text.withOpacity(0.05), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: themeType == AppThemeType.doodle ? const Color(0xFF111122) : theme.text.withOpacity(0.3), width: 1.5), + ), + child: Icon( + isMuted ? Icons.volume_off : Icons.volume_up, + color: themeType == AppThemeType.doodle ? const Color(0xFF111122) : theme.text.withOpacity(0.8), + size: 16 + ), + ), + ), + + const SizedBox(width: 10), + + // TASTO INFORMAZIONI (TUTORIAL) CON CONTORNO + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + showDialog(context: context, builder: (ctx) => const TutorialDialog()); + }, + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: themeType == AppThemeType.doodle ? Colors.transparent : theme.text.withOpacity(0.05), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: themeType == AppThemeType.doodle ? const Color(0xFF111122) : theme.text.withOpacity(0.3), width: 1.5), + ), + child: Icon( + Icons.info_outline, + color: themeType == AppThemeType.doodle ? const Color(0xFF111122) : theme.text.withOpacity(0.8), + size: 16 + ), + ), + ), + ], ), ], ), diff --git a/lib/ui/home/home_screen.dart b/lib/ui/home/home_screen.dart index 8851aa3..503a27a 100644 --- a/lib/ui/home/home_screen.dart +++ b/lib/ui/home/home_screen.dart @@ -8,7 +8,7 @@ import 'package:provider/provider.dart'; import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; // Serve ancora se vuoi fare logica qui, ma per ora teniamo l'import +import 'package:cloud_firestore/cloud_firestore.dart'; import 'dart:async'; import 'package:app_links/app_links.dart'; @@ -18,21 +18,21 @@ import '../../core/app_colors.dart'; import '../game/game_screen.dart'; import '../settings/settings_screen.dart'; import '../../services/storage_service.dart'; +import '../../services/audio_service.dart'; +import '../../services/multiplayer_service.dart'; import '../multiplayer/lobby_screen.dart'; import 'history_screen.dart'; import '../admin/admin_screen.dart'; import 'package:tetraq/l10n/app_localizations.dart'; -// --- IMPORTIAMO I NOSTRI NUOVI WIDGET E DIALOGHI PULITI --- import '../../widgets/painters.dart'; import '../../widgets/cyber_border.dart'; -import '../../widgets/home_buttons.dart'; import '../../widgets/music_theme_widgets.dart'; -import 'dialog.dart'; // Il file che raggruppa Quests, Leaderboard e Tutorial - +import '../../widgets/home_buttons.dart'; +import 'dialog.dart'; // =========================================================================== -// WIDGET LOCALI PER IL SETUP DELLA PARTITA (Ancora qui per comodità) +// WIDGET LOCALI PER IL SETUP DELLA PARTITA // =========================================================================== class _NeonShapeButton extends StatelessWidget { final IconData icon; final String label; final bool isSelected; @@ -184,6 +184,166 @@ class _NeonTimeSwitch extends StatelessWidget { } } +class _NeonPrivacySwitch extends StatelessWidget { + final bool isPublic; + final ThemeColors theme; + final AppThemeType themeType; + final VoidCallback onTap; + + const _NeonPrivacySwitch({required this.isPublic, required this.theme, required this.themeType, required this.onTap}); + + @override + Widget build(BuildContext context) { + if (themeType == AppThemeType.doodle) { + Color doodleColor = isPublic ? Colors.green.shade600 : Colors.red.shade600; + return Transform.rotate( + angle: 0.015, + child: GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + transform: Matrix4.translationValues(0, isPublic ? 3 : 0, 0), + decoration: BoxDecoration( + color: isPublic ? doodleColor : Colors.white, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(15), topRight: Radius.circular(8), + bottomLeft: Radius.circular(6), bottomRight: Radius.circular(15), + ), + border: Border.all(color: isPublic ? theme.text : doodleColor.withOpacity(0.5), width: 2.5), + boxShadow: [BoxShadow(color: isPublic ? theme.text.withOpacity(0.8) : doodleColor.withOpacity(0.2), offset: const Offset(4, 5), blurRadius: 0)], + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(isPublic ? Icons.public : Icons.lock, color: isPublic ? Colors.white : doodleColor, size: 20), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(isPublic ? 'PUBBLICA' : 'PRIVATA', style: getSharedTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : doodleColor, fontWeight: FontWeight.w900, fontSize: 12, letterSpacing: 1.0))), + Text(isPublic ? 'In Bacheca' : 'Solo Codice', style: getSharedTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : doodleColor.withOpacity(0.8), fontSize: 9, fontWeight: FontWeight.bold))), + ], + ), + ], + ), + ), + ), + ); + } + + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isPublic + ? [Colors.greenAccent.withOpacity(0.25), Colors.greenAccent.withOpacity(0.05)] + : [theme.playerRed.withOpacity(0.25), theme.playerRed.withOpacity(0.05)], + ), + border: Border.all(color: isPublic ? Colors.greenAccent : theme.playerRed, width: isPublic ? 2 : 1), + boxShadow: isPublic + ? [BoxShadow(color: Colors.greenAccent.withOpacity(0.3), blurRadius: 15, spreadRadius: 2)] + : [BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 6, offset: const Offset(2, 4))], + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(isPublic ? Icons.public : Icons.lock, color: isPublic ? Colors.greenAccent : theme.playerRed, size: 20), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(isPublic ? 'PUBBLICA' : 'PRIVATA', style: getSharedTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : theme.text.withOpacity(0.8), fontWeight: FontWeight.w900, fontSize: 11, letterSpacing: 1.5))), + Text(isPublic ? 'Tutti ti vedono' : 'Solo con Codice', style: getSharedTextStyle(themeType, TextStyle(color: isPublic ? Colors.greenAccent.shade200 : theme.playerRed.withOpacity(0.7), fontSize: 9, fontWeight: FontWeight.bold))), + ], + ), + ], + ), + ), + ); + } +} + +class _NeonActionButton extends StatelessWidget { + final String label; + final Color color; + final VoidCallback onTap; + final ThemeColors theme; + final AppThemeType themeType; + + const _NeonActionButton({required this.label, required this.color, required this.onTap, required this.theme, required this.themeType}); + + @override + Widget build(BuildContext context) { + if (themeType == AppThemeType.doodle) { + double tilt = (label == "UNISCITI" || label == "ANNULLA") ? -0.015 : 0.02; + return Transform.rotate( + angle: tilt, + child: GestureDetector( + onTap: onTap, + child: Container( + height: 50, + decoration: BoxDecoration( + color: color, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10), topRight: Radius.circular(20), + bottomLeft: Radius.circular(25), bottomRight: Radius.circular(10), + ), + border: Border.all(color: theme.text, width: 3.0), + boxShadow: [BoxShadow(color: theme.text.withOpacity(0.9), offset: const Offset(4, 4), blurRadius: 0)], + ), + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Text(label, style: getSharedTextStyle(themeType, TextStyle(fontSize: 20, fontWeight: FontWeight.w900, letterSpacing: 3.0, color: Colors.white))), + ), + ), + ), + ), + ), + ); + } + + return GestureDetector( + onTap: onTap, + child: Container( + height: 50, + decoration: BoxDecoration( + gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [color.withOpacity(0.9), color.withOpacity(0.6)]), + borderRadius: BorderRadius.circular(15), + border: Border.all(color: Colors.white.withOpacity(0.3), width: 1.5), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.5), offset: const Offset(4, 8), blurRadius: 12), + BoxShadow(color: color.withOpacity(0.3), offset: const Offset(0, 0), blurRadius: 15, spreadRadius: 1), + ], + ), + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Text(label, style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2.0, color: Colors.white, shadows: const [Shadow(color: Colors.black, blurRadius: 2, offset: Offset(1, 1))]))), + ), + ), + ), + ), + ); + } +} + // =========================================================================== // CLASSE PRINCIPALE HOME // =========================================================================== @@ -199,6 +359,18 @@ class _HomeScreenState extends State with WidgetsBindingObserver { int _debugTapCount = 0; late AppLinks _appLinks; StreamSubscription? _linkSubscription; + bool _isCreatingRoom = false; + + int _selectedRadius = 4; + ArenaShape _selectedShape = ArenaShape.classic; + bool _isTimeMode = true; + bool _isPublicRoom = true; + bool _isLoading = false; + String? _myRoomCode; + bool _roomStarted = false; + + final MultiplayerService _multiplayerService = MultiplayerService(); + final TextEditingController _codeController = TextEditingController(); @override void initState() { @@ -215,7 +387,9 @@ class _HomeScreenState extends State with WidgetsBindingObserver { @override void dispose() { WidgetsBinding.instance.removeObserver(this); + _cleanupGhostRoom(); _linkSubscription?.cancel(); + _codeController.dispose(); super.dispose(); } @@ -223,6 +397,15 @@ class _HomeScreenState extends State with WidgetsBindingObserver { void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { _checkClipboardForInvite(); + } else if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) { + _cleanupGhostRoom(); + } + } + + void _cleanupGhostRoom() { + if (_myRoomCode != null && !_roomStarted) { + FirebaseFirestore.instance.collection('games').doc(_myRoomCode).delete(); + _myRoomCode = null; } } @@ -266,10 +449,174 @@ class _HomeScreenState extends State with WidgetsBindingObserver { } catch (e) { debugPrint("Errore lettura appunti: $e"); } } + Future _createRoom() async { + if (_isLoading) return; + setState(() => _isLoading = true); + + try { + String playerName = StorageService.instance.playerName; + if (playerName.isEmpty) playerName = "HOST"; + + String code = await _multiplayerService.createGameRoom( + _selectedRadius, playerName, _selectedShape.name, _isTimeMode, isPublic: _isPublicRoom + ); + + if (!mounted) return; + setState(() { _myRoomCode = code; _isLoading = false; _roomStarted = false; }); + + if (!_isPublicRoom) { + _multiplayerService.shareInviteLink(code); + } + _showWaitingDialog(code); + } catch (e) { + if (mounted) { setState(() => _isLoading = false); _showError("Errore durante la creazione della partita."); } + } + } + + Future _joinRoomByCode(String code) async { + if (_isLoading) return; + FocusScope.of(context).unfocus(); + + code = code.trim().toUpperCase(); + if (code.isEmpty || code.length != 5) { _showError("Inserisci un codice valido di 5 caratteri."); return; } + + setState(() => _isLoading = true); + + try { + String playerName = StorageService.instance.playerName; + if (playerName.isEmpty) playerName = "GUEST"; + + Map? 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().startNewGame(hostRadius, isOnline: true, roomCode: code, isHost: false, shape: hostShape, timeMode: hostTimeMode); + Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen())); + } else { + _showError("Stanza non trovata, piena o partita già iniziata."); + } + } catch (e) { + if (mounted) { setState(() => _isLoading = false); _showError("Errore di connessione: $e"); } + } + } + + void _showError(String message) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message, style: const TextStyle(color: Colors.white)), backgroundColor: Colors.red)); } + // =========================================================================== - // DIALOGHI IN-FILE (Setup e Nome) + // DIALOGHI IN-FILE // =========================================================================== + void _showWaitingDialog(String code) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + final theme = context.watch().currentColors; + final themeType = context.read().currentThemeType; + + Widget dialogContent = Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(color: theme.playerRed), const SizedBox(height: 25), + Text("CODICE STANZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.text.withOpacity(0.6), letterSpacing: 2))), + Text(code, style: getSharedTextStyle(themeType, TextStyle(fontSize: 40, fontWeight: FontWeight.w900, color: theme.playerRed, letterSpacing: 8, shadows: themeType == AppThemeType.doodle ? [] : [Shadow(color: theme.playerRed.withOpacity(0.5), blurRadius: 10)]))), + const SizedBox(height: 25), + Transform.rotate( + angle: themeType == AppThemeType.doodle ? 0.02 : 0, + child: Container( + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: themeType == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: themeType == AppThemeType.doodle ? theme.text : theme.playerBlue.withOpacity(0.3), width: themeType == AppThemeType.doodle ? 2 : 1.5), + boxShadow: themeType == AppThemeType.doodle + ? [BoxShadow(color: theme.text.withOpacity(0.8), offset: const Offset(4, 4))] + : [BoxShadow(color: theme.playerBlue.withOpacity(0.1), blurRadius: 10)] + ), + child: Column( + children: [ + Icon(_isPublicRoom ? Icons.podcasts : Icons.share, color: theme.playerBlue, size: 32), const SizedBox(height: 12), + Text(_isPublicRoom ? "Sei in Bacheca!" : "Invita un amico", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.w900, fontSize: 18))), + const SizedBox(height: 8), + Text(_isPublicRoom ? "Aspettiamo che uno sfidante si unisca dalla lobby pubblica." : "Condividi il codice. La partita inizierà appena si unirà.", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.8), fontSize: 14, height: 1.5))), + ], + ), + ), + ), + ], + ); + + if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) { + dialogContent = AnimatedCyberBorder(child: dialogContent); + } else { + dialogContent = Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: themeType == AppThemeType.doodle ? Colors.white.withOpacity(0.95) : theme.background, + borderRadius: BorderRadius.circular(25), + border: Border.all(color: themeType == AppThemeType.doodle ? theme.text : theme.gridLine.withOpacity(0.5), width: 2), + boxShadow: themeType == AppThemeType.doodle ? [BoxShadow(color: theme.text.withOpacity(0.6), offset: const Offset(8, 8))] : [] + ), + child: dialogContent + ); + } + + return StreamBuilder( + stream: _multiplayerService.listenToRoom(code), + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!.exists) { + var data = snapshot.data!.data() as Map; + if (data['status'] == 'playing') { + _roomStarted = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.pop(context); + context.read().startNewGame(_selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: _selectedShape, timeMode: _isTimeMode); + Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen())); + }); + } + } + + return PopScope( + canPop: false, + onPopInvoked: (didPop) { + if (didPop) return; + _cleanupGhostRoom(); + Navigator.pop(context); + }, + child: Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + dialogContent, + const SizedBox(height: 20), + TextButton( + onPressed: () { + _cleanupGhostRoom(); + Navigator.pop(context); + }, + child: Text("ANNULLA", style: getSharedTextStyle(themeType, TextStyle(color: Colors.red, fontWeight: FontWeight.w900, fontSize: 20, letterSpacing: 2.0, shadows: themeType == AppThemeType.doodle ? [] : [const Shadow(color: Colors.black, blurRadius: 2)]))), + ), + ], + ), + ), + ); + }, + ); + } + ); + } + void _promptJoinRoom(String roomCode) { showDialog( context: context, @@ -287,7 +634,7 @@ class _HomeScreenState extends State with WidgetsBindingObserver { style: ElevatedButton.styleFrom(backgroundColor: themeType == AppThemeType.doodle ? Colors.transparent : theme.playerBlue, elevation: 0, side: themeType == AppThemeType.doodle ? BorderSide(color: theme.text, width: 1.5) : BorderSide.none), onPressed: () { Navigator.of(context).pop(); - Navigator.push(context, MaterialPageRoute(builder: (_) => LobbyScreen(initialRoomCode: roomCode))); + _joinRoomByCode(roomCode); }, child: Text(AppLocalizations.of(context)!.joinMatch, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : Colors.white, fontWeight: FontWeight.bold))), ), @@ -555,6 +902,7 @@ class _HomeScreenState extends State with WidgetsBindingObserver { String? bgImage; if (themeType == AppThemeType.wood) bgImage = 'assets/images/wood_bg.jpg'; + if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg'; if (themeType == AppThemeType.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg'; if (themeType == AppThemeType.music) bgImage = 'assets/images/music_bg.jpg'; @@ -580,87 +928,152 @@ class _HomeScreenState extends State with WidgetsBindingObserver { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + // --- NUOVO HEADER FISSATO --- Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - GestureDetector( - onTap: _showNameDialog, + // BLOCCO SINISTRO: AVATAR, NOME E AUDIO + Expanded( // Permette di occupare lo spazio necessario senza spingere la destra child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - themeType == AppThemeType.doodle - ? CustomPaint( - painter: DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.8), strokeColor: inkColor, seed: 1, isCircle: true), - child: SizedBox(width: 50, height: 50, child: Icon(Icons.person, color: inkColor, size: 30)), - ) - : SizedBox( - width: 50, height: 50, - child: Stack( - fit: StackFit.expand, - children: [ - CircularProgressIndicator(value: xpProgress, color: theme.playerBlue, strokeWidth: 3, backgroundColor: theme.gridLine.withOpacity(0.2)), - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration(shape: BoxShape.circle, boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 4))]), - child: CircleAvatar(backgroundColor: theme.playerBlue.withOpacity(0.2), child: Icon(Icons.person, color: theme.playerBlue, size: 26)), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _showNameDialog, + child: themeType == AppThemeType.doodle + ? CustomPaint( + painter: DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.8), strokeColor: inkColor, seed: 1, isCircle: true), + child: SizedBox(width: 50, height: 50, child: Icon(Icons.person, color: inkColor, size: 30)), + ) + : SizedBox( + width: 50, height: 50, + child: Stack( + fit: StackFit.expand, + children: [ + CircularProgressIndicator(value: xpProgress, color: theme.playerBlue, strokeWidth: 3, backgroundColor: theme.gridLine.withOpacity(0.2)), + Padding( + padding: const EdgeInsets.all(4.0), + child: Container( + decoration: BoxDecoration(shape: BoxShape.circle, boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 4))]), + child: CircleAvatar(backgroundColor: theme.playerBlue.withOpacity(0.2), child: Icon(Icons.person, color: theme.playerBlue, size: 26)), + ), ), - ), - ], + ], + ), ), ), const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text(playerName, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : theme.text, fontSize: 24, fontWeight: FontWeight.w900, letterSpacing: 1.5, shadows: themeType == AppThemeType.doodle ? [] : [Shadow(color: Colors.black.withOpacity(0.5), offset: const Offset(1, 2), blurRadius: 2)]))), - Text("LIV. $level", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.8) : theme.playerBlue, fontSize: 14, fontWeight: FontWeight.bold, letterSpacing: 1))), - ], + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _showNameDialog, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(playerName, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : theme.text, fontSize: 24, fontWeight: FontWeight.w900, letterSpacing: 1.5, shadows: themeType == AppThemeType.doodle ? [] : [Shadow(color: Colors.black.withOpacity(0.5), offset: const Offset(1, 2), blurRadius: 2)]))), + Text("LIV. $level", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.8) : theme.playerBlue, fontSize: 14, fontWeight: FontWeight.bold, letterSpacing: 1))), + ], + ), ), ], ), ), - GestureDetector( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const HistoryScreen())), - child: themeType == AppThemeType.doodle - ? Transform.rotate( - angle: 0.04, - child: CustomPaint( - painter: DoodleBackgroundPainter(fillColor: Colors.yellow.shade100, strokeColor: inkColor, seed: 2), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + // BLOCCO DESTRO: STATISTICHE (SOPRA) E AUDIO (SOTTO) + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + GestureDetector( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const HistoryScreen())), + child: themeType == AppThemeType.doodle + ? Transform.rotate( + angle: 0.04, + child: CustomPaint( + painter: DoodleBackgroundPainter(fillColor: Colors.yellow.shade100, strokeColor: inkColor, seed: 2), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.emoji_events, color: inkColor, size: 20), const SizedBox(width: 6), + Text("$wins", style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900))), const SizedBox(width: 12), + Icon(Icons.sentiment_very_dissatisfied, color: inkColor, size: 20), const SizedBox(width: 6), + Text("$losses", style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900))), + ], + ), + ), + ), + ) + : Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.text.withOpacity(0.15), theme.text.withOpacity(0.02)]), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withOpacity(0.1), width: 1.5), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.3), offset: const Offset(2, 4), blurRadius: 8), BoxShadow(color: Colors.white.withOpacity(0.05), offset: const Offset(-1, -1), blurRadius: 2)], + ), child: Row( + mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.emoji_events, color: inkColor, size: 20), const SizedBox(width: 6), - Text("$wins", style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900))), const SizedBox(width: 12), - Icon(Icons.sentiment_very_dissatisfied, color: inkColor, size: 20), const SizedBox(width: 6), - Text("$losses", style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900))), + Icon(themeType == AppThemeType.music ? FontAwesomeIcons.microphone : Icons.emoji_events, color: Colors.amber.shade600, size: 16), const SizedBox(width: 6), + Text("$wins", style: getSharedTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.w900))), const SizedBox(width: 12), + Icon(themeType == AppThemeType.music ? FontAwesomeIcons.compactDisc : Icons.sentiment_very_dissatisfied, color: theme.playerRed.withOpacity(0.8), size: 16), const SizedBox(width: 6), + Text("$losses", style: getSharedTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.w900))), ], ), ), ), - ) - : Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), - decoration: BoxDecoration( - gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.text.withOpacity(0.15), theme.text.withOpacity(0.02)]), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: Colors.white.withOpacity(0.1), width: 1.5), - boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.3), offset: const Offset(2, 4), blurRadius: 8), BoxShadow(color: Colors.white.withOpacity(0.05), offset: const Offset(-1, -1), blurRadius: 2)], + + const SizedBox(height: 12), + + // PULSANTE AUDIO FISSATO A DESTRA + AnimatedBuilder( + animation: AudioService.instance, + builder: (context, child) { + bool isMuted = AudioService.instance.isMuted; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + AudioService.instance.toggleMute(); + }, + child: themeType == AppThemeType.doodle + ? CustomPaint( + painter: DoodleBackgroundPainter(fillColor: Colors.white, strokeColor: inkColor, seed: 99, isCircle: true), + child: SizedBox( + width: 45, height: 45, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(isMuted ? Icons.volume_off : Icons.volume_up, color: inkColor, size: 18), + Text(isMuted ? "OFF" : "ON", style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: 10, fontWeight: FontWeight.w900))), + ], + ) + ), + ) + : Container( + width: 45, height: 45, + decoration: BoxDecoration( + color: theme.background.withOpacity(0.8), + shape: BoxShape.circle, + border: Border.all(color: theme.gridLine.withOpacity(0.5), width: 1.5), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.3), blurRadius: 5, offset: const Offset(0, 4))], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(isMuted ? Icons.volume_off : Icons.volume_up, color: theme.playerBlue, size: 16), + Text(isMuted ? "OFF" : "ON", style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontSize: 9, fontWeight: FontWeight.bold))), + ], + ), + ), + ); + } ), - child: Row( - children: [ - Icon(themeType == AppThemeType.music ? FontAwesomeIcons.microphone : Icons.emoji_events, color: Colors.amber.shade600, size: 16), const SizedBox(width: 6), - Text("$wins", style: getSharedTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.w900))), const SizedBox(width: 12), - Icon(themeType == AppThemeType.music ? FontAwesomeIcons.compactDisc : Icons.sentiment_very_dissatisfied, color: theme.playerRed.withOpacity(0.8), size: 16), const SizedBox(width: 6), - Text("$losses", style: getSharedTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.w900))), - ], - ), - ), + ], ) ], ), + // --- FINE HEADER FISSATO --- const Spacer(), @@ -762,25 +1175,54 @@ class _HomeScreenState extends State with WidgetsBindingObserver { ); return Scaffold( - backgroundColor: themeType == AppThemeType.doodle ? Colors.white : (bgImage != null ? Colors.transparent : theme.background), + backgroundColor: Colors.transparent, body: Stack( children: [ + // 1. Sfondo base a tinta unita Container(color: themeType == AppThemeType.doodle ? Colors.white : theme.background), - // Sfondo per il tema Doodle - if (themeType == AppThemeType.doodle) - Positioned.fill(child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: Colors.white, strokeColor: Colors.blue.withOpacity(0.15), seed: 0, isCircle: false))), - + // 2. Immagine di Sfondo per tutti i temi che la supportano if (bgImage != null) - Positioned.fill(child: Image.asset(bgImage, fit: BoxFit.cover, alignment: Alignment.center)), + Positioned.fill( + child: Image.asset( + bgImage, + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ), + // 3. Griglia a righe incrociate per il doodle + if (themeType == AppThemeType.doodle) + Positioned.fill( + child: CustomPaint( + painter: FullScreenGridPainter(Colors.blue.withOpacity(0.15)), + ), + ), + + // 4. Patina scura (Cyberpunk e Music) if (bgImage != null && (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music)) - 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)])))), + 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)] + ) + ), + ), + ), - // Cavi musicali per il tema Musica + // 5. Cavi musicali (Tema Musica) if (themeType == AppThemeType.music) - Positioned.fill(child: IgnorePointer(child: CustomPaint(painter: AudioCablesPainter()))), + Positioned.fill( + child: IgnorePointer( + child: CustomPaint( + painter: AudioCablesPainter(), + ), + ), + ), + // 6. UI Positioned.fill(child: uiContent), ], ),