Auto-sync: 20260330_160000

This commit is contained in:
Paolo 2026-03-30 16:00:01 +02:00
parent 047df9b914
commit 1eedefb7f7
4 changed files with 12023 additions and 2 deletions

BIN
.DS_Store vendored

Binary file not shown.

View file

@ -28,6 +28,7 @@ import '../multiplayer/lobby_screen.dart';
import '../admin/admin_screen.dart';
import '../settings/settings_screen.dart';
import '../game/game_screen.dart';
import '../profile/profile_screen.dart';
import 'package:tetraq/l10n/app_localizations.dart';
import '../../widgets/painters.dart';
@ -77,7 +78,10 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (StorageService.instance.playerName.isEmpty) {
HomeModals.showNameDialog(context, () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const ProfileScreen()),
).then((_) {
StorageService.instance.syncLeaderboard();
_listenToInvites();
setState(() {});
@ -506,7 +510,12 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () => HomeModals.showNameDialog(context, () => setState(() {})),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const ProfileScreen()),
).then((_) => setState(() {}));
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: _glassBoxDecoration(theme, themeType),

View file

@ -0,0 +1,467 @@
// ===========================================================================
// FILE: lib/ui/profile/profile_screen.dart
// ===========================================================================
import 'dart:ui';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../core/theme_manager.dart';
import '../../core/app_colors.dart';
import '../../services/storage_service.dart';
import '../../widgets/painters.dart';
import '../../widgets/cyber_border.dart';
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key});
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
final TextEditingController _nameController = TextEditingController();
final TextEditingController _passController = TextEditingController();
bool _isLoading = false;
bool _obscurePassword = true;
String _errorMessage = "";
List<String> _nameSuggestions = [];
bool _isGhostMode = false;
late User _currentUser;
@override
void initState() {
super.initState();
_currentUser = FirebaseAuth.instance.currentUser!;
_loadGhostMode();
}
@override
void dispose() {
_nameController.dispose();
_passController.dispose();
super.dispose();
}
Future<void> _loadGhostMode() async {
try {
var doc = await FirebaseFirestore.instance.collection('leaderboard').doc(_currentUser.uid).get();
if (doc.exists && doc.data()!.containsKey('isGhost')) {
setState(() {
_isGhostMode = doc.data()!['isGhost'];
});
}
} catch (e) {
debugPrint("Errore caricamento Ghost Mode: $e");
}
}
Future<void> _toggleGhostMode(bool value) async {
setState(() => _isGhostMode = value);
try {
await FirebaseFirestore.instance.collection('leaderboard').doc(_currentUser.uid).set(
{'isGhost': value}, SetOptions(merge: true)
);
} catch (e) {
debugPrint("Errore salvataggio Ghost Mode: $e");
}
}
String _getPlayerTitle(int level) {
if (level < 5) return "Principiante";
if (level < 10) return "Sfidante";
if (level < 15) return "Maestro dei Quadrati";
return "Leggenda del Neon";
}
Future<void> _handleRegistration() async {
final name = _nameController.text.trim().toUpperCase();
final password = _passController.text.trim();
setState(() { _errorMessage = ""; _nameSuggestions.clear(); _isLoading = true; });
if (name.isEmpty || password.isEmpty) {
setState(() { _errorMessage = "Compila tutti i campi!"; _isLoading = false; });
return;
}
if (password.length < 6) {
setState(() { _errorMessage = "Password troppo corta (min. 6 caratteri)"; _isLoading = false; });
return;
}
try {
// 1. Controllo univocità del nome
var existingUser = await FirebaseFirestore.instance.collection('leaderboard').where('name', isEqualTo: name).get();
if (existingUser.docs.isNotEmpty && existingUser.docs.first.id != _currentUser.uid) {
// Nome già preso, generiamo suggerimenti
List<String> suggestions = [];
int attempts = 0;
final rand = Random();
while(suggestions.length < 3 && attempts < 15) {
String candidate = "$name${rand.nextInt(99) + 1}";
var check = await FirebaseFirestore.instance.collection('leaderboard').where('name', isEqualTo: candidate).get();
if (check.docs.isEmpty && !suggestions.contains(candidate)) {
suggestions.add(candidate);
}
attempts++;
}
setState(() {
_errorMessage = "Nome già in uso! Scegline un altro:";
_nameSuggestions = suggestions;
_isLoading = false;
});
return;
}
// 2. Registrazione sicura
final fakeEmail = "${name.replaceAll(' ', '')}@tetraq.game".toLowerCase();
if (_currentUser.isAnonymous) {
final credential = EmailAuthProvider.credential(email: fakeEmail, password: password);
await _currentUser.linkWithCredential(credential);
}
await StorageService.instance.savePlayerName(name);
await StorageService.instance.syncLeaderboard();
setState(() { _isLoading = false; });
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Account Protetto con Successo!"), backgroundColor: Colors.green));
} on FirebaseAuthException catch (e) {
String msg = "Errore di connessione.";
if (e.code == 'email-already-in-use' || e.code == 'credential-already-in-use') msg = "Utente già registrato. Se sei tu, fai il login.";
setState(() { _errorMessage = msg; _isLoading = false; });
} catch (e) {
setState(() { _errorMessage = "Errore: $e"; _isLoading = false; });
}
}
Future<void> _deleteAccount() async {
bool confirm = await showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: Colors.black87,
title: const Text("ATTENZIONE", style: TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold)),
content: const Text("Stai per eliminare definitivamente il tuo profilo, i tuoi XP e le statistiche.\nL'operazione è irreversibile.\n\nVuoi procedere?", style: TextStyle(color: Colors.white)),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text("ANNULLA", style: TextStyle(color: Colors.grey))),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent),
onPressed: () => Navigator.pop(ctx, true),
child: const Text("SÌ, ELIMINA", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
),
],
)
) ?? false;
if (!confirm) return;
setState(() => _isLoading = true);
try {
// 1. Elimina record da Firestore
await FirebaseFirestore.instance.collection('leaderboard').doc(_currentUser.uid).delete();
// 2. Elimina l'utente Auth
await _currentUser.delete();
// 3. Pulisci i dati locali sensibili
final prefs = await SharedPreferences.getInstance();
await prefs.remove('totalXP');
await prefs.remove('wins');
await prefs.remove('losses');
await prefs.remove('cpuLevel');
await prefs.remove('playerName');
await prefs.remove('favorites');
// 4. Ricrea un anonimo pulito e torna alla Home
await FirebaseAuth.instance.signInAnonymously();
await StorageService.instance.init();
if (mounted) {
Navigator.of(context).popUntil((route) => route.isFirst);
}
} on FirebaseAuthException catch (e) {
setState(() => _isLoading = false);
if (e.code == 'requires-recent-login') {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Per sicurezza, riavvia l'app prima di eliminare l'account."), backgroundColor: Colors.redAccent));
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Errore: ${e.message}"), backgroundColor: Colors.redAccent));
}
}
}
@override
Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
Color inkColor = const Color(0xFF111122);
int wins = StorageService.instance.wins;
int losses = StorageService.instance.losses;
int totalGames = wins + losses;
double winRate = totalGames > 0 ? (wins / totalGames) * 100 : 0.0;
int level = StorageService.instance.playerLevel;
String title = _getPlayerTitle(level);
String playerName = StorageService.instance.playerName;
if (playerName.isEmpty) playerName = "GUEST";
bool isAnon = _currentUser.isAnonymous;
return Scaffold(
backgroundColor: theme.background,
appBar: AppBar(
title: Text("PROFILO GIOCATORE", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : theme.text, fontWeight: FontWeight.w900, letterSpacing: 1.5))),
backgroundColor: Colors.transparent,
elevation: 0,
iconTheme: IconThemeData(color: themeType == AppThemeType.doodle ? inkColor : theme.text),
),
body: Stack(
children: [
if (themeType == AppThemeType.doodle)
Positioned.fill(child: CustomPaint(painter: FullScreenGridPainter(Colors.blue.withOpacity(0.15)))),
SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// --- SEZIONE 1: IDENTITÀ ---
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: themeType == AppThemeType.doodle ? inkColor : theme.playerBlue.withOpacity(0.3), width: 2),
boxShadow: themeType == AppThemeType.doodle ? [BoxShadow(color: inkColor.withOpacity(0.8), offset: const Offset(4, 4))] : [],
),
child: Column(
children: [
CircleAvatar(radius: 40, backgroundColor: theme.playerBlue.withOpacity(0.2), child: Icon(Icons.person, size: 45, color: theme.playerBlue)),
const SizedBox(height: 15),
Text(playerName, style: getSharedTextStyle(themeType, TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? inkColor : theme.text))),
const SizedBox(height: 5),
Text(title, style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: theme.playerRed))),
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(isAnon ? Icons.warning_amber_rounded : Icons.verified_user, color: isAnon ? Colors.orange : Colors.green, size: 18),
const SizedBox(width: 5),
Text(isAnon ? "Account non protetto" : "Account protetto sul Cloud", style: getSharedTextStyle(themeType, TextStyle(color: isAnon ? Colors.orange : Colors.green, fontWeight: FontWeight.bold, fontSize: 12))),
],
)
],
),
),
const SizedBox(height: 20),
// --- SEZIONE 2: STATISTICHE AVANZATE ---
Row(
children: [
Expanded(child: _buildStatCard("Vittorie", "$wins", Icons.emoji_events, Colors.amber, theme, themeType)),
const SizedBox(width: 15),
Expanded(child: _buildStatCard("Sconfitte", "$losses", Icons.sentiment_very_dissatisfied, theme.playerRed, theme, themeType)),
],
),
const SizedBox(height: 15),
_buildStatCard("Win Rate Globale", "${winRate.toStringAsFixed(1)}%", Icons.pie_chart, theme.playerBlue, theme, themeType, isWide: true),
const SizedBox(height: 25),
// --- SEZIONE 3: REGISTRAZIONE (Solo se anonimo) ---
if (isAnon) ...[
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.orange, width: 2),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text("Metti al sicuro i tuoi progressi!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : Colors.white, fontWeight: FontWeight.bold, fontSize: 16))),
const SizedBox(height: 15),
TextField(
controller: _nameController, textCapitalization: TextCapitalization.characters, textAlign: TextAlign.center, maxLength: 8,
style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : Colors.white, fontSize: 20, fontWeight: FontWeight.bold)),
decoration: InputDecoration(hintText: "Scegli un Nome", hintStyle: TextStyle(color: Colors.grey.withOpacity(0.6)), filled: true, fillColor: Colors.black12, counterText: "", border: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none)),
),
const SizedBox(height: 10),
TextField(
controller: _passController, obscureText: _obscurePassword, textAlign: TextAlign.center, maxLength: 20,
style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : Colors.white, fontSize: 20, fontWeight: FontWeight.bold)),
decoration: InputDecoration(
hintText: "Scegli Password", hintStyle: TextStyle(color: Colors.grey.withOpacity(0.6)), filled: true, fillColor: Colors.black12, counterText: "", border: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none),
suffixIcon: IconButton(icon: Icon(_obscurePassword ? Icons.visibility : Icons.visibility_off, color: Colors.grey), onPressed: () => setState(() => _obscurePassword = !_obscurePassword)),
),
),
if (_errorMessage.isNotEmpty) ...[
const SizedBox(height: 10),
Text(_errorMessage, textAlign: TextAlign.center, style: const TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold)),
],
if (_nameSuggestions.isNotEmpty) ...[
const SizedBox(height: 10),
Wrap(
spacing: 8, alignment: WrapAlignment.center,
children: _nameSuggestions.map((s) => ActionChip(
label: Text(s, style: const TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: theme.playerBlue.withOpacity(0.2),
side: BorderSide(color: theme.playerBlue),
onPressed: () { _nameController.text = s; _handleRegistration(); },
)).toList(),
)
],
const SizedBox(height: 15),
_isLoading
? const Center(child: CircularProgressIndicator(color: Colors.orange))
: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.orange, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
onPressed: _handleRegistration,
child: Text("SALVA PROFILO", style: getSharedTextStyle(themeType, const TextStyle(fontWeight: FontWeight.w900, letterSpacing: 1.5))),
)
],
),
),
const SizedBox(height: 25),
],
// --- SEZIONE 4: IMPOSTAZIONI PRIVACY ---
Text("PRIVACY", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.6) : theme.text.withOpacity(0.5), letterSpacing: 1.5))),
const SizedBox(height: 10),
SwitchListTile(
contentPadding: EdgeInsets.zero,
activeColor: theme.playerBlue,
title: Text("Modalità Fantasma", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : theme.text, fontWeight: FontWeight.bold))),
subtitle: Text("Nessuno ti vedrà online o potrà invitarti.", style: TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.6) : theme.text.withOpacity(0.5), fontSize: 12)),
value: _isGhostMode,
onChanged: _toggleGhostMode,
),
const Divider(),
// --- SEZIONE 5: GESTIONE PREFERITI ---
const SizedBox(height: 15),
Text("AMICI PREFERITI", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.6) : theme.text.withOpacity(0.5), letterSpacing: 1.5))),
const SizedBox(height: 10),
_buildFavoritesList(theme, themeType, inkColor),
const SizedBox(height: 40),
// --- SEZIONE 6: DANGER ZONE ---
OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: Colors.redAccent,
side: const BorderSide(color: Colors.redAccent, width: 2),
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
),
icon: const Icon(Icons.delete_forever),
label: Text("ELIMINA PROFILO", style: getSharedTextStyle(themeType, const TextStyle(fontWeight: FontWeight.w900, letterSpacing: 1.5))),
onPressed: _deleteAccount,
),
const SizedBox(height: 30),
],
),
),
if (_isLoading && !isAnon)
Positioned.fill(child: Container(color: Colors.black54, child: const Center(child: CircularProgressIndicator(color: Colors.redAccent)))),
],
),
);
}
Widget _buildStatCard(String title, String value, IconData icon, Color color, ThemeColors theme, AppThemeType themeType, {bool isWide = false}) {
Color inkColor = const Color(0xFF111122);
return Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(15),
border: Border.all(color: themeType == AppThemeType.doodle ? inkColor : color.withOpacity(0.3), width: 1.5),
boxShadow: themeType == AppThemeType.doodle ? [BoxShadow(color: inkColor.withOpacity(0.8), offset: const Offset(3, 3))] : [],
),
child: Column(
crossAxisAlignment: isWide ? CrossAxisAlignment.center : CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 20),
const SizedBox(width: 8),
Text(title, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.6) : theme.text.withOpacity(0.6), fontSize: 11, fontWeight: FontWeight.bold))),
],
),
const SizedBox(height: 10),
Center(
child: Text(value, style: getSharedTextStyle(themeType, TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? inkColor : theme.text))),
),
],
),
);
}
Widget _buildFavoritesList(ThemeColors theme, AppThemeType themeType, Color inkColor) {
final favs = StorageService.instance.favorites;
if (favs.isEmpty) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(15), border: Border.all(color: Colors.grey.withOpacity(0.3), style: BorderStyle.solid)),
child: Center(child: Text("Nessun amico salvato.", style: TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.5) : theme.text.withOpacity(0.5)))),
);
}
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: favs.length,
itemBuilder: (ctx, i) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.02),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.2) : theme.text.withOpacity(0.1)),
),
child: ListTile(
leading: Icon(Icons.star, color: Colors.amber.shade600),
title: Text(favs[i]['name']!, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : theme.text, fontWeight: FontWeight.bold))),
trailing: IconButton(
icon: const Icon(Icons.close, color: Colors.redAccent),
onPressed: () async {
await StorageService.instance.toggleFavorite(favs[i]['uid']!, favs[i]['name']!);
setState(() {});
},
),
),
);
},
);
}
}
class FullScreenGridPainter extends CustomPainter {
final Color gridColor;
FullScreenGridPainter(this.gridColor);
@override
void paint(Canvas canvas, Size size) {
final Paint paperGridPaint = Paint()..color = gridColor..strokeWidth = 1.0..style = PaintingStyle.stroke;
double paperStep = 20.0;
for (double i = 0; i <= size.width; i += paperStep) canvas.drawLine(Offset(i, 0), Offset(i, size.height), paperGridPaint);
for (double i = 0; i <= size.height; i += paperStep) canvas.drawLine(Offset(0, i), Offset(size.width, i), paperGridPaint);
}
@override bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

File diff suppressed because it is too large Load diff