Auto-sync: 20260315_130000

This commit is contained in:
Paolo 2026-03-15 13:00:00 +01:00
parent ea54bf3a12
commit fe8d54b3e1
4 changed files with 315 additions and 82 deletions

View file

@ -13,6 +13,7 @@ class MultiplayerService {
final FirebaseAuth _auth = FirebaseAuth.instance; final FirebaseAuth _auth = FirebaseAuth.instance;
CollectionReference get _gamesCollection => _firestore.collection('games'); CollectionReference get _gamesCollection => _firestore.collection('games');
CollectionReference get _invitesCollection => _firestore.collection('invites');
Future<String> createGameRoom(int boardRadius, String hostName, String shapeName, bool isTimeMode, {bool isPublic = true}) async { Future<String> createGameRoom(int boardRadius, String hostName, String shapeName, bool isTimeMode, {bool isPublic = true}) async {
String roomCode = _generateRoomCode(); String roomCode = _generateRoomCode();
@ -27,7 +28,7 @@ class MultiplayerService {
'moves': [], 'moves': [],
'seed': randomSeed, 'seed': randomSeed,
'hostName': hostName, 'hostName': hostName,
'hostUid': _auth.currentUser?.uid, // NUOVO: Salviamo l'ID univoco del creatore 'hostUid': _auth.currentUser?.uid,
'guestName': '', 'guestName': '',
'shape': shapeName, 'shape': shapeName,
'timeMode': isTimeMode, 'timeMode': isTimeMode,
@ -66,7 +67,10 @@ class MultiplayerService {
String message = "Ehi! Giochiamo a TetraQ? 🎮\n\n" String message = "Ehi! Giochiamo a TetraQ? 🎮\n\n"
"Clicca su questo link per entrare direttamente in stanza:\n" "Clicca su questo link per entrare direttamente in stanza:\n"
"tetraq://join?code=$roomCode\n\n" "tetraq://join?code=$roomCode\n\n"
"Oppure apri l'app e inserisci manualmente il codice: $roomCode"; "Apri l'app e inserisci manualmente il codice: $roomCode\n"
"Se non hai ancora scaricato il gioco lo trovi nei link sotto \n\n"
"🍎 iOS: https://apps.apple.com/it/app/tetraq/id6759522394\n"
"🤖 Android: https://play.google.com/store/apps/details?id=com.amastra.tetraq";
Share.share(message); Share.share(message);
} }
@ -122,4 +126,29 @@ class MultiplayerService {
debugPrint("Errore reset partita: $e"); debugPrint("Errore reset partita: $e");
} }
} }
Future<void> sendInvite(String targetUid, String roomCode, String hostName) async {
try {
await _invitesCollection.add({
'targetUid': targetUid,
'hostName': hostName,
'roomCode': roomCode,
'timestamp': FieldValue.serverTimestamp(),
});
} catch(e) {
debugPrint("Errore invio invito: $e");
}
}
Stream<QuerySnapshot> listenForInvites(String myUid) {
return _invitesCollection.where('targetUid', isEqualTo: myUid).snapshots();
}
Future<void> deleteInvite(String inviteId) async {
try {
await _invitesCollection.doc(inviteId).delete();
} catch(e) {
debugPrint("Errore cancellazione invito: $e");
}
}
} }

View file

@ -77,6 +77,27 @@ class StorageService {
} }
} }
// --- NUOVO: GESTIONE PREFERITI (RUBRICA LOCALE) ---
List<Map<String, String>> get favorites {
List<String> favs = _prefs.getStringList('favorites') ?? [];
return favs.map((e) => Map<String, String>.from(jsonDecode(e))).toList();
}
Future<void> toggleFavorite(String uid, String name) async {
var favs = favorites;
if (favs.any((f) => f['uid'] == uid)) {
favs.removeWhere((f) => f['uid'] == uid); // Rimuove se esiste
} else {
favs.add({'uid': uid, 'name': name}); // Aggiunge se non esiste
}
await _prefs.setStringList('favorites', favs.map((e) => jsonEncode(e)).toList());
}
bool isFavorite(String uid) {
return favorites.any((f) => f['uid'] == uid);
}
// ---------------------------------------------------
void _checkDailyQuests() { void _checkDailyQuests() {
String today = DateTime.now().toIso8601String().substring(0, 10); String today = DateTime.now().toIso8601String().substring(0, 10);
String lastDate = _prefs.getString('quest_date') ?? ''; String lastDate = _prefs.getString('quest_date') ?? '';

View file

@ -13,6 +13,7 @@ import '../../core/app_colors.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
import '../../widgets/painters.dart'; import '../../widgets/painters.dart';
import '../../widgets/cyber_border.dart'; import '../../widgets/cyber_border.dart';
import '../../services/storage_service.dart'; // IMPORT AGGIUNTO
// =========================================================================== // ===========================================================================
// 1. DIALOGO MISSIONI (QUESTS) // 1. DIALOGO MISSIONI (QUESTS)
@ -181,6 +182,12 @@ class LeaderboardDialog extends StatelessWidget {
var data = doc.data() as Map<String, dynamic>; var data = doc.data() as Map<String, dynamic>;
String? myUid = FirebaseAuth.instance.currentUser?.uid; String? myUid = FirebaseAuth.instance.currentUser?.uid;
bool isMe = doc.id == myUid; bool isMe = doc.id == myUid;
String playerName = data['name'] ?? 'Unknown';
// Avvolto in StatefulBuilder per gestire la stella dei preferiti
return StatefulBuilder(
builder: (context, setStateItem) {
bool isFav = StorageService.instance.isFavorite(doc.id);
return Container( return Container(
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
@ -194,20 +201,33 @@ class LeaderboardDialog extends StatelessWidget {
children: [ children: [
Text("#${index + 1}", style: getSharedTextStyle(themeType, TextStyle(fontWeight: FontWeight.w900, color: index == 0 ? Colors.amber : (index == 1 ? Colors.grey.shade400 : (index == 2 ? Colors.brown.shade300 : theme.text.withOpacity(0.5)))))), Text("#${index + 1}", style: getSharedTextStyle(themeType, TextStyle(fontWeight: FontWeight.w900, color: index == 0 ? Colors.amber : (index == 1 ? Colors.grey.shade400 : (index == 2 ? Colors.brown.shade300 : theme.text.withOpacity(0.5)))))),
const SizedBox(width: 15), const SizedBox(width: 15),
Expanded(child: Text(data['name'] ?? 'Unknown', style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: isMe ? FontWeight.w900 : FontWeight.bold, color: theme.text)))), Expanded(child: Text(playerName, style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: isMe ? FontWeight.w900 : FontWeight.bold, color: theme.text)))),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text("Lv. ${data['level'] ?? 1}", style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 12)), Text("Lv. ${data['level'] ?? 1}", style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 12)),
Text("${data['xp'] ?? 0} XP", style: TextStyle(color: theme.text.withOpacity(0.6), fontSize: 10)), Text("${data['xp'] ?? 0} XP", style: TextStyle(color: theme.text.withOpacity(0.6), fontSize: 10)),
], ],
),
// IL BOTTONE DEI PREFERITI
if (!isMe) ...[
const SizedBox(width: 8),
GestureDetector(
onTap: () async {
await StorageService.instance.toggleFavorite(doc.id, playerName);
setStateItem(() {});
},
child: Icon(isFav ? Icons.star : Icons.star_border, color: Colors.amber, size: 24),
) )
]
], ],
), ),
); );
} }
); );
} }
);
}
), ),
), ),

View file

@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'dart:math' as math; import 'dart:math' as math;
import '../../logic/game_controller.dart'; import '../../logic/game_controller.dart';
import '../../models/game_board.dart'; import '../../models/game_board.dart';
import '../../core/theme_manager.dart'; import '../../core/theme_manager.dart';
@ -31,6 +32,10 @@ TextStyle _getTextStyle(AppThemeType themeType, TextStyle baseStyle) {
return baseStyle; return baseStyle;
} }
// ===========================================================================
// WIDGET INTERNI DI SUPPORTO (PULSANTI NEON)
// ===========================================================================
class _NeonShapeButton extends StatelessWidget { class _NeonShapeButton extends StatelessWidget {
final IconData icon; final IconData icon;
final String label; final String label;
@ -388,7 +393,7 @@ class _NeonPrivacySwitch extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(isPublic ? 'PUBBLICA' : 'PRIVATA', style: _getTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : theme.text.withOpacity(0.8), fontWeight: FontWeight.w900, fontSize: 11, letterSpacing: 1.5))), Text(isPublic ? 'STANZA PUBBLICA' : 'PRIVATA', style: _getTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : theme.text.withOpacity(0.8), fontWeight: FontWeight.w900, fontSize: 10, letterSpacing: 1.0))),
Text(isPublic ? 'Tutti ti vedono' : 'Solo con Codice', style: _getTextStyle(themeType, TextStyle(color: isPublic ? Colors.greenAccent.shade200 : theme.playerRed.withOpacity(0.7), fontSize: 9, fontWeight: FontWeight.bold))), Text(isPublic ? 'Tutti ti vedono' : 'Solo con Codice', style: _getTextStyle(themeType, TextStyle(color: isPublic ? Colors.greenAccent.shade200 : theme.playerRed.withOpacity(0.7), fontSize: 9, fontWeight: FontWeight.bold))),
], ],
), ),
@ -399,6 +404,92 @@ class _NeonPrivacySwitch extends StatelessWidget {
} }
} }
// --- NUOVO WIDGET: BOTTONE INVITA PREFERITO ---
class _NeonInviteFavoriteButton extends StatelessWidget {
final ThemeColors theme;
final AppThemeType themeType;
final VoidCallback onTap;
const _NeonInviteFavoriteButton({required this.theme, required this.themeType, required this.onTap});
@override
Widget build(BuildContext context) {
if (themeType == AppThemeType.doodle) {
Color doodleColor = Colors.pink.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),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8), topRight: Radius.circular(15),
bottomLeft: Radius.circular(15), bottomRight: Radius.circular(6),
),
border: Border.all(color: doodleColor.withOpacity(0.5), width: 2.5),
boxShadow: [BoxShadow(color: doodleColor.withOpacity(0.2), offset: const Offset(4, 5), blurRadius: 0)],
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.favorite, color: doodleColor, size: 20),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('PREFERITI', style: _getTextStyle(themeType, TextStyle(color: doodleColor, fontWeight: FontWeight.w900, fontSize: 12, letterSpacing: 1.0))),
Text('Invita amico', style: _getTextStyle(themeType, TextStyle(color: 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: [Colors.pinkAccent.withOpacity(0.25), Colors.pinkAccent.withOpacity(0.05)],
),
border: Border.all(color: Colors.pinkAccent, width: 1.5),
boxShadow: [BoxShadow(color: Colors.pinkAccent.withOpacity(0.3), blurRadius: 15, spreadRadius: 2)],
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.favorite, color: Colors.pinkAccent, size: 20),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('PREFERITI', style: _getTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 11, letterSpacing: 1.5))),
Text('Invita amico', style: _getTextStyle(themeType, TextStyle(color: Colors.pinkAccent.shade200, fontSize: 9, fontWeight: FontWeight.bold))),
],
),
],
),
),
);
}
}
class _NeonActionButton extends StatelessWidget { class _NeonActionButton extends StatelessWidget {
final String label; final String label;
final Color color; final Color color;
@ -524,6 +615,10 @@ class _CyberBorderPainter extends CustomPainter {
bool shouldRepaint(covariant _CyberBorderPainter oldDelegate) => oldDelegate.animationValue != animationValue; bool shouldRepaint(covariant _CyberBorderPainter oldDelegate) => oldDelegate.animationValue != animationValue;
} }
// ===========================================================================
// SCHERMATA LOBBY (PRINCIPALE)
// ===========================================================================
class LobbyScreen extends StatefulWidget { class LobbyScreen extends StatefulWidget {
final String? initialRoomCode; final String? initialRoomCode;
@ -541,7 +636,6 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
String? _myRoomCode; String? _myRoomCode;
String _playerName = ''; String _playerName = '';
// Variabile per gestire l'effetto "sipario"
bool _isCreatingRoom = false; bool _isCreatingRoom = false;
int _selectedRadius = 4; int _selectedRadius = 4;
@ -608,6 +702,29 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
} }
} }
// --- NUOVA LOGICA: CREA STANZA E INVITA IN UN COLPO SOLO ---
Future<void> _createRoomAndInvite(String targetUid, String targetName) async {
if (_isLoading) return;
setState(() => _isLoading = true);
try {
String code = await _multiplayerService.createGameRoom(
_selectedRadius, _playerName, _selectedShape.name, _isTimeMode, isPublic: _isPublicRoom
);
await _multiplayerService.sendInvite(targetUid, code, _playerName);
if (!mounted) return;
setState(() { _myRoomCode = code; _isLoading = false; _roomStarted = false; });
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Sfida inviata a $targetName!"), backgroundColor: Colors.green));
_showWaitingDialog(code);
} catch (e) {
if (mounted) { setState(() => _isLoading = false); _showError("Errore durante la creazione della partita."); }
}
}
Future<void> _joinRoomByCode(String code) async { Future<void> _joinRoomByCode(String code) async {
if (_isLoading) return; if (_isLoading) return;
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
@ -643,6 +760,60 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
void _showError(String message) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message, style: const TextStyle(color: Colors.white)), backgroundColor: Colors.red)); } void _showError(String message) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message, style: const TextStyle(color: Colors.white)), backgroundColor: Colors.red)); }
// --- FINESTRA PREFERITI ---
void _showFavoritesDialogForCreation() {
final favs = StorageService.instance.favorites;
showDialog(
context: context,
builder: (ctx) {
final themeManager = ctx.watch<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
return AlertDialog(
backgroundColor: theme.background,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: Text("I TUOI PREFERITI", style: _getTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold))),
content: Container(
width: double.maxFinite,
height: 300,
decoration: BoxDecoration(
border: Border.all(color: theme.playerRed, width: 2),
borderRadius: BorderRadius.circular(10)
),
child: favs.isEmpty
? Center(child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text("Non hai ancora aggiunto nessun preferito dalla Classifica!", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6)))),
))
: ListView.builder(
itemCount: favs.length,
itemBuilder: (c, i) {
return ListTile(
title: Text(favs[i]['name']!, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontSize: 18, fontWeight: FontWeight.bold))),
trailing: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
onPressed: () {
Navigator.pop(ctx);
_createRoomAndInvite(favs[i]['uid']!, favs[i]['name']!);
},
child: Text("SFIDA", style: _getTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))),
),
);
},
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: Text("CHIUDI", style: _getTextStyle(themeType, TextStyle(color: theme.playerRed))))
],
);
}
);
}
void _showWaitingDialog(String code) { void _showWaitingDialog(String code) {
showDialog( showDialog(
context: context, context: context,
@ -673,7 +844,7 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
child: Column( child: Column(
children: [ children: [
Icon(_isPublicRoom ? Icons.podcasts : Icons.share, color: theme.playerBlue, size: 32), const SizedBox(height: 12), Icon(_isPublicRoom ? Icons.podcasts : Icons.share, color: theme.playerBlue, size: 32), const SizedBox(height: 12),
Text(_isPublicRoom ? "Sei in Bacheca!" : "Invita un amico", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.w900, fontSize: 18))), Text(_isPublicRoom ? "Sei in Bacheca!" : "Condividi link", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.w900, fontSize: 18))),
const SizedBox(height: 8), 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: _getTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.8), fontSize: 14, height: 1.5))), Text(_isPublicRoom ? "Aspettiamo che uno sfidante si unisca dalla lobby pubblica." : "Condividi il codice. La partita inizierà appena si unirà.", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.8), fontSize: 14, height: 1.5))),
], ],
@ -756,8 +927,7 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_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.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg';
bool isChaosUnlocked = true; bool isChaosUnlocked = StorageService.instance.playerLevel >= 10;
Color doodlePenColor = const Color(0xFF00008B);
// --- PANNELLO IMPOSTAZIONI STANZA --- // --- PANNELLO IMPOSTAZIONI STANZA ---
Widget hostPanel = Transform.rotate( Widget hostPanel = Transform.rotate(
@ -818,13 +988,23 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
Divider(color: themeType == AppThemeType.doodle ? theme.text.withOpacity(0.5) : Colors.white.withOpacity(0.05), thickness: themeType == AppThemeType.doodle ? 2.5 : 1.5), Divider(color: themeType == AppThemeType.doodle ? theme.text.withOpacity(0.5) : Colors.white.withOpacity(0.05), thickness: themeType == AppThemeType.doodle ? 2.5 : 1.5),
const SizedBox(height: 12), const SizedBox(height: 12),
Text("REGOLE E VISIBILITÀ", style: _getTextStyle(themeType, TextStyle(fontSize: 10, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.5), letterSpacing: 1.5))), Text("TEMPO E OPZIONI", style: _getTextStyle(themeType, TextStyle(fontSize: 10, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.5), letterSpacing: 1.5))),
const SizedBox(height: 8), const SizedBox(height: 8),
// RIGA 1: TEMPO (OCCUPA TUTTO LO SPAZIO)
Row( Row(
children: [ children: [
Expanded(child: _NeonTimeSwitch(isTimeMode: _isTimeMode, theme: theme, themeType: themeType, onTap: () => setState(() => _isTimeMode = !_isTimeMode))), Expanded(child: _NeonTimeSwitch(isTimeMode: _isTimeMode, theme: theme, themeType: themeType, onTap: () => setState(() => _isTimeMode = !_isTimeMode))),
const SizedBox(width: 8), ],
),
const SizedBox(height: 10),
// RIGA 2: VISIBILITÀ E INVITI
Row(
children: [
Expanded(child: _NeonPrivacySwitch(isPublic: _isPublicRoom, theme: theme, themeType: themeType, onTap: () => setState(() => _isPublicRoom = !_isPublicRoom))), Expanded(child: _NeonPrivacySwitch(isPublic: _isPublicRoom, theme: theme, themeType: themeType, onTap: () => setState(() => _isPublicRoom = !_isPublicRoom))),
const SizedBox(width: 8),
Expanded(child: _NeonInviteFavoriteButton(theme: theme, themeType: themeType, onTap: _showFavoritesDialogForCreation)),
], ],
) )
], ],
@ -839,61 +1019,48 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
Widget uiContent = SafeArea( Widget uiContent = SafeArea(
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
// Padding inferiore aumentato a 60 per evitare il taglio dei pulsanti
padding: EdgeInsets.only(left: 20.0, right: 20.0, top: 10.0, bottom: MediaQuery.of(context).padding.bottom + 60.0), padding: EdgeInsets.only(left: 20.0, right: 20.0, top: 10.0, bottom: MediaQuery.of(context).padding.bottom + 60.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Transform.rotate( IconButton(
angle: themeType == AppThemeType.doodle ? -0.02 : 0, icon: Icon(Icons.arrow_back_ios_new, color: theme.text),
child: Text("MULTIPLAYER", style: _getTextStyle(themeType, TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1, shadows: themeType == AppThemeType.doodle ? [] : [Shadow(color: Colors.black.withOpacity(0.5), offset: const Offset(2, 2), blurRadius: 4)]))), onPressed: () => Navigator.pop(context),
),
Transform.rotate(
angle: themeType == AppThemeType.doodle ? 0.03 : 0,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.white : theme.playerRed.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: themeType == AppThemeType.doodle ? theme.playerRed : theme.playerRed.withOpacity(0.5), width: themeType == AppThemeType.doodle ? 2.5 : 1.0),
boxShadow: themeType == AppThemeType.doodle ? [BoxShadow(color: theme.text.withOpacity(0.6), offset: const Offset(2, 3), blurRadius: 0)] : []
),
child: Text("$_playerName", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: theme.playerRed, letterSpacing: 1))),
), ),
Expanded(
child: Text("MULTIPLAYER", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2))),
), ),
const SizedBox(width: 48), // Bilanciamento
], ],
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// --- L'EFFETTO SIPARIO CON ANIMATED SIZE ---
AnimatedSize( AnimatedSize(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeInOut,
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: _isCreatingRoom child: _isCreatingRoom
? Column( // MENU CREAZIONE (Aperto) ? Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
hostPanel, hostPanel,
const SizedBox(height: 15), const SizedBox(height: 15),
Row( Row(
children: [ children: [
Expanded( // Entrambi in un Expanded "liscio" si dividono il 50% di spazio Expanded(
child: _NeonActionButton(label: "AVVIA", color: theme.playerRed, onTap: _createRoom, theme: theme, themeType: themeType), child: _NeonActionButton(label: "AVVIA", color: theme.playerRed, onTap: _createRoom, theme: theme, themeType: themeType),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( // Entrambi in un Expanded "liscio" si dividono il 50% di spazio Expanded(
child: _NeonActionButton(label: "ANNULLA", color: Colors.grey.shade600, onTap: () => setState(() => _isCreatingRoom = false), theme: theme, themeType: themeType), child: _NeonActionButton(label: "ANNULLA", color: Colors.grey.shade600, onTap: () => setState(() => _isCreatingRoom = false), theme: theme, themeType: themeType),
), ),
], ],
), ),
], ],
) )
: Column( // MENU BASE (Chiuso) : Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_NeonActionButton(label: "CREA PARTITA", color: theme.playerRed, onTap: () { FocusScope.of(context).unfocus(); setState(() => _isCreatingRoom = true); }, theme: theme, themeType: themeType), _NeonActionButton(label: "CREA PARTITA", color: theme.playerRed, onTap: () { FocusScope.of(context).unfocus(); setState(() => _isCreatingRoom = true); }, theme: theme, themeType: themeType),
@ -1067,39 +1234,35 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
return Scaffold( return Scaffold(
backgroundColor: bgImage != null ? Colors.transparent : theme.background, backgroundColor: bgImage != null ? Colors.transparent : theme.background,
extendBodyBehindAppBar: true, extendBodyBehindAppBar: true,
appBar: AppBar(backgroundColor: Colors.transparent, elevation: 0, iconTheme: IconThemeData(color: theme.text)),
body: Stack( body: Stack(
children: [ children: [
Container( Container(color: themeType == AppThemeType.doodle ? Colors.white : theme.background),
decoration: bgImage != null ? BoxDecoration(image: DecorationImage(image: AssetImage(bgImage), fit: BoxFit.cover)) : null, if (bgImage != null)
child: bgImage != null && themeType == AppThemeType.cyberpunk Positioned.fill(
? BackdropFilter(filter: ImageFilter.blur(sigmaX: 3.5, sigmaY: 3.5), child: Container(color: Colors.black.withOpacity(0.2))) child: Image.asset(
: bgImage != null && themeType != AppThemeType.cyberpunk bgImage,
? BackdropFilter(filter: ImageFilter.blur(sigmaX: 3.5, sigmaY: 3.5), child: Container(color: themeType == AppThemeType.doodle ? Colors.white.withOpacity(0.1) : Colors.transparent)) fit: BoxFit.cover,
: null,
),
if (themeType == AppThemeType.doodle)
Positioned(
top: 150, left: -20, right: -20,
child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [
Transform.rotate(angle: -0.06, child: Icon(Icons.wifi_tethering, size: 450, color: doodlePenColor.withOpacity(0.08))),
Transform.rotate(angle: 0.04, child: Icon(Icons.wifi_tethering, size: 430, color: doodlePenColor.withOpacity(0.06))),
Transform.rotate(angle: 0.01, child: Icon(Icons.wifi_tethering, size: 460, color: doodlePenColor.withOpacity(0.05))),
],
), ),
),
if (bgImage != null && (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music || themeType == AppThemeType.arcade || themeType == AppThemeType.grimorio))
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter, end: Alignment.bottomCenter,
colors: [Colors.black.withOpacity(0.6), Colors.black.withOpacity(0.9)]
) )
else
Positioned(
top: 70, left: -50, right: -50,
child: Center(
child: Icon(Icons.wifi_tethering, size: 450, color: theme.playerBlue.withOpacity(0.12)),
), ),
), ),
),
_isLoading ? Center(child: CircularProgressIndicator(color: theme.playerRed)) : uiContent, if (themeType == AppThemeType.doodle)
Positioned.fill(
child: CustomPaint(
painter: FullScreenGridPainter(Colors.blue.withOpacity(0.15)),
),
),
Positioned.fill(child: uiContent),
], ],
), ),
); );