Auto-sync: 20260315_130000
This commit is contained in:
parent
ea54bf3a12
commit
fe8d54b3e1
4 changed files with 315 additions and 82 deletions
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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') ?? '';
|
||||||
|
|
|
||||||
|
|
@ -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,29 +182,48 @@ 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';
|
||||||
|
|
||||||
return Container(
|
// Avvolto in StatefulBuilder per gestire la stella dei preferiti
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
return StatefulBuilder(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
builder: (context, setStateItem) {
|
||||||
decoration: BoxDecoration(
|
bool isFav = StorageService.instance.isFavorite(doc.id);
|
||||||
color: isMe ? theme.playerBlue.withOpacity(0.2) : theme.text.withOpacity(0.05),
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
return Container(
|
||||||
border: isMe ? Border.all(color: theme.playerBlue, width: 1.5) : null
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
child: Row(
|
decoration: BoxDecoration(
|
||||||
children: [
|
color: isMe ? theme.playerBlue.withOpacity(0.2) : theme.text.withOpacity(0.05),
|
||||||
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)))))),
|
borderRadius: BorderRadius.circular(10),
|
||||||
const SizedBox(width: 15),
|
border: isMe ? Border.all(color: theme.playerBlue, width: 1.5) : null
|
||||||
Expanded(child: Text(data['name'] ?? 'Unknown', style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: isMe ? FontWeight.w900 : FontWeight.bold, color: theme.text)))),
|
),
|
||||||
Column(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
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("Lv. ${data['level'] ?? 1}", style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 12)),
|
const SizedBox(width: 15),
|
||||||
Text("${data['xp'] ?? 0} XP", style: TextStyle(color: theme.text.withOpacity(0.6), fontSize: 10)),
|
Expanded(child: Text(playerName, style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: isMe ? FontWeight.w900 : FontWeight.bold, color: theme.text)))),
|
||||||
],
|
Column(
|
||||||
)
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
],
|
children: [
|
||||||
),
|
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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// 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),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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(
|
Expanded(
|
||||||
angle: themeType == AppThemeType.doodle ? 0.03 : 0,
|
child: Text("MULTIPLAYER", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2))),
|
||||||
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))),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
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))),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Positioned(
|
|
||||||
top: 70, left: -50, right: -50,
|
|
||||||
child: Center(
|
|
||||||
child: Icon(Icons.wifi_tethering, size: 450, color: theme.playerBlue.withOpacity(0.12)),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (bgImage != null && (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music || themeType == AppThemeType.arcade || themeType == AppThemeType.grimorio))
|
||||||
_isLoading ? Center(child: CircularProgressIndicator(color: theme.playerRed)) : uiContent,
|
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)]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (themeType == AppThemeType.doodle)
|
||||||
|
Positioned.fill(
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: FullScreenGridPainter(Colors.blue.withOpacity(0.15)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned.fill(child: uiContent),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue