Auto-sync: 20260320_190000
This commit is contained in:
parent
e50ae8f689
commit
b4ab3590be
4 changed files with 425 additions and 257 deletions
|
|
@ -43,19 +43,14 @@ class StorageService {
|
||||||
String get lastIp => _prefs.getString('last_ip') ?? 'Sconosciuto';
|
String get lastIp => _prefs.getString('last_ip') ?? 'Sconosciuto';
|
||||||
String get lastCity => _prefs.getString('last_city') ?? 'Sconosciuta';
|
String get lastCity => _prefs.getString('last_city') ?? 'Sconosciuta';
|
||||||
|
|
||||||
// --- METODI TEMA AGGIORNATI CON GESTIONE MIGRAZIONE SICURA ---
|
|
||||||
String getTheme() {
|
String getTheme() {
|
||||||
final Object? savedTheme = _prefs.get('theme');
|
final Object? savedTheme = _prefs.get('theme');
|
||||||
|
|
||||||
if (savedTheme is String) {
|
if (savedTheme is String) {
|
||||||
return savedTheme;
|
return savedTheme;
|
||||||
} else if (savedTheme is int) {
|
} else if (savedTheme is int) {
|
||||||
// Trovato un vecchio salvataggio in formato intero (causa del crash).
|
|
||||||
// Puliamo la memoria per evitare futuri problemi.
|
|
||||||
_prefs.remove('theme');
|
_prefs.remove('theme');
|
||||||
return AppThemeType.doodle.toString();
|
return AppThemeType.doodle.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppThemeType.doodle.toString();
|
return AppThemeType.doodle.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,71 +89,30 @@ class StorageService {
|
||||||
syncLeaderboard();
|
syncLeaderboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- SINCRONIZZAZIONE BLINDATA: SOLO UTENTI REGISTRATI ---
|
||||||
Future<void> syncLeaderboard() async {
|
Future<void> syncLeaderboard() async {
|
||||||
if (playerName.isNotEmpty) {
|
try {
|
||||||
try {
|
final user = FirebaseAuth.instance.currentUser;
|
||||||
final user = FirebaseAuth.instance.currentUser;
|
|
||||||
|
|
||||||
if (user != null) {
|
// BLOCCO TOTALE: Se non sei loggato con la password, niente database!
|
||||||
String currentPlatform = "Sconosciuta";
|
if (user == null) return;
|
||||||
String appVersion = "N/D";
|
|
||||||
String deviceModel = "Sconosciuto";
|
|
||||||
|
|
||||||
if (!kIsWeb) {
|
String name = playerName;
|
||||||
if (Platform.isAndroid) currentPlatform = "Android";
|
if (name.isEmpty) name = "GIOCATORE"; // Fallback di sicurezza
|
||||||
else if (Platform.isIOS) currentPlatform = "iOS";
|
|
||||||
else if (Platform.isMacOS) currentPlatform = "macOS";
|
|
||||||
else if (Platform.isWindows) currentPlatform = "Windows";
|
|
||||||
|
|
||||||
try {
|
String targetUid = user.uid;
|
||||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
|
||||||
appVersion = "${packageInfo.version}+${packageInfo.buildNumber}";
|
|
||||||
} catch(e) {
|
|
||||||
debugPrint("Errore lettura versione: $e");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
await FirebaseFirestore.instance.collection('leaderboard').doc(targetUid).set({
|
||||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
'name': name,
|
||||||
if (Platform.isAndroid) {
|
'xp': totalXP,
|
||||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
'level': playerLevel,
|
||||||
deviceModel = "${androidInfo.manufacturer} ${androidInfo.model}";
|
'wins': wins,
|
||||||
} else if (Platform.isIOS) {
|
'losses': losses,
|
||||||
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
|
'lastActive': FieldValue.serverTimestamp(),
|
||||||
deviceModel = iosInfo.utsname.machine;
|
}, SetOptions(merge: true));
|
||||||
} else if (Platform.isMacOS) {
|
|
||||||
MacOsDeviceInfo macInfo = await deviceInfo.macOsInfo;
|
|
||||||
deviceModel = macInfo.model;
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
debugPrint("Errore lettura hardware: $e");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_sessionStart != 0) {
|
} catch (e) {
|
||||||
int now = DateTime.now().millisecondsSinceEpoch;
|
debugPrint("Errore durante la sincronizzazione della classifica: $e");
|
||||||
int sessionSeconds = (now - _sessionStart) ~/ 1000;
|
|
||||||
await _prefs.setInt('totalPlaytime', (_prefs.getInt('totalPlaytime') ?? 0) + sessionSeconds);
|
|
||||||
_sessionStart = now;
|
|
||||||
}
|
|
||||||
int totalPlaytime = _prefs.getInt('totalPlaytime') ?? 0;
|
|
||||||
|
|
||||||
await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({
|
|
||||||
'name': playerName,
|
|
||||||
'xp': totalXP,
|
|
||||||
'level': playerLevel,
|
|
||||||
'wins': wins,
|
|
||||||
'lastActive': FieldValue.serverTimestamp(),
|
|
||||||
'platform': currentPlatform,
|
|
||||||
'ip': lastIp,
|
|
||||||
'city': lastCity,
|
|
||||||
'playtime': totalPlaytime,
|
|
||||||
'appVersion': appVersion,
|
|
||||||
'deviceModel': deviceModel,
|
|
||||||
}, SetOptions(merge: true));
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
debugPrint("Errore sinc. classifica: $e");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,10 +119,12 @@ class QuestsDialog extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// 2. DIALOGO CLASSIFICA (LEADERBOARD)
|
// 2. DIALOGO CLASSIFICA (LEADERBOARD) CON CALLBACK SFIDA
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
class LeaderboardDialog extends StatelessWidget {
|
class LeaderboardDialog extends StatelessWidget {
|
||||||
const LeaderboardDialog({super.key});
|
final Function(String uid, String name)? onChallenge; // <-- Aggiunto Callback per inviare i dati alla HomeScreen
|
||||||
|
|
||||||
|
const LeaderboardDialog({super.key, this.onChallenge});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -160,7 +162,6 @@ class LeaderboardDialog extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
final rawDocs = snapshot.data!.docs;
|
final rawDocs = snapshot.data!.docs;
|
||||||
|
|
||||||
final filteredDocs = rawDocs.where((doc) {
|
final filteredDocs = rawDocs.where((doc) {
|
||||||
var data = doc.data() as Map<String, dynamic>;
|
var data = doc.data() as Map<String, dynamic>;
|
||||||
String name = (data['name'] ?? '').toString().toUpperCase();
|
String name = (data['name'] ?? '').toString().toUpperCase();
|
||||||
|
|
@ -181,6 +182,13 @@ class LeaderboardDialog extends StatelessWidget {
|
||||||
bool isMe = doc.id == myUid;
|
bool isMe = doc.id == myUid;
|
||||||
String playerName = data['name'] ?? 'Unknown';
|
String playerName = data['name'] ?? 'Unknown';
|
||||||
|
|
||||||
|
bool isOnline = false;
|
||||||
|
if (data['lastActive'] != null) {
|
||||||
|
Timestamp lastActive = data['lastActive'];
|
||||||
|
int diffInSeconds = DateTime.now().difference(lastActive.toDate()).inSeconds;
|
||||||
|
if (diffInSeconds.abs() < 180) isOnline = true;
|
||||||
|
}
|
||||||
|
|
||||||
return StatefulBuilder(
|
return StatefulBuilder(
|
||||||
builder: (context, setStateItem) {
|
builder: (context, setStateItem) {
|
||||||
bool isFav = StorageService.instance.isFavorite(doc.id);
|
bool isFav = StorageService.instance.isFavorite(doc.id);
|
||||||
|
|
@ -197,7 +205,37 @@ 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(playerName, style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: isMe ? FontWeight.w900 : FontWeight.bold, color: theme.text)))),
|
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
playerName,
|
||||||
|
style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: isMe ? FontWeight.w900 : FontWeight.bold, color: theme.text)),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
if (isFav && !isMe && isOnline) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
PulsingChallengeButton(
|
||||||
|
themeType: themeType,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
// Chiama la funzione passata dalla HomeScreen!
|
||||||
|
if (onChallenge != null) {
|
||||||
|
onChallenge!(doc.id, playerName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(width: 10),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -205,6 +243,7 @@ class LeaderboardDialog extends StatelessWidget {
|
||||||
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)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
if (!isMe) ...[
|
if (!isMe) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
|
|
@ -260,7 +299,6 @@ class TutorialDialog extends StatelessWidget {
|
||||||
final themeType = themeManager.currentThemeType;
|
final themeType = themeManager.currentThemeType;
|
||||||
Color inkColor = const Color(0xFF111122);
|
Color inkColor = const Color(0xFF111122);
|
||||||
|
|
||||||
// ETICHETTE DINAMICHE PER I POTENZIAMENTI
|
|
||||||
String goldLabel = "ORO:";
|
String goldLabel = "ORO:";
|
||||||
String bombLabel = "BOMBA:";
|
String bombLabel = "BOMBA:";
|
||||||
String swapLabel = "SCAMBIO:";
|
String swapLabel = "SCAMBIO:";
|
||||||
|
|
@ -438,4 +476,66 @@ class TutorialStep extends StatelessWidget {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 4. WIDGET ANIMATO PER TASTO SFIDA
|
||||||
|
// ===========================================================================
|
||||||
|
class PulsingChallengeButton extends StatefulWidget {
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final AppThemeType themeType;
|
||||||
|
|
||||||
|
const PulsingChallengeButton({super.key, required this.onTap, required this.themeType});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PulsingChallengeButton> createState() => _PulsingChallengeButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PulsingChallengeButtonState extends State<PulsingChallengeButton> with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _animation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 900))..repeat(reverse: true);
|
||||||
|
_animation = Tween<double>(begin: 0.3, end: 1.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final Color softGreen = Colors.green.shade400;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: widget.onTap,
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: _animation,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: softGreen.withOpacity(0.15),
|
||||||
|
border: Border.all(color: softGreen, width: 1.5),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.circle, color: softGreen, size: 8),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
"SFIDA",
|
||||||
|
style: getSharedTextStyle(widget.themeType, TextStyle(color: softGreen, fontSize: 10, fontWeight: FontWeight.bold))
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -32,7 +32,9 @@ class HomeModals {
|
||||||
String errorMessage = "";
|
String errorMessage = "";
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context, barrierDismissible: false, barrierColor: Colors.black.withOpacity(0.8),
|
context: context,
|
||||||
|
barrierDismissible: false, // Impedisce di chiudere tappando fuori
|
||||||
|
barrierColor: Colors.black.withOpacity(0.8),
|
||||||
builder: (dialogContext) {
|
builder: (dialogContext) {
|
||||||
final themeManager = dialogContext.watch<ThemeManager>();
|
final themeManager = dialogContext.watch<ThemeManager>();
|
||||||
final themeType = themeManager.currentThemeType;
|
final themeType = themeManager.currentThemeType;
|
||||||
|
|
@ -102,7 +104,7 @@ class HomeModals {
|
||||||
children: [
|
children: [
|
||||||
Text(loc.welcomeTitle, style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900, fontSize: 24, letterSpacing: 2.0)), textAlign: TextAlign.center),
|
Text(loc.welcomeTitle, style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900, fontSize: 24, letterSpacing: 2.0)), textAlign: TextAlign.center),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text('Scegli Nome e Password.\nTi serviranno per recuperare gli XP!', style: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.8), fontSize: 13)), textAlign: TextAlign.center),
|
Text('Scegli una Password per il Cloud.\nI tuoi XP e il tuo Livello saranno protetti e non li perderai mai!', style: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.8), fontSize: 13, fontWeight: FontWeight.bold)), textAlign: TextAlign.center),
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
TextField(
|
TextField(
|
||||||
controller: nameController, textCapitalization: TextCapitalization.characters, textAlign: TextAlign.center, maxLength: 8,
|
controller: nameController, textCapitalization: TextCapitalization.characters, textAlign: TextAlign.center, maxLength: 8,
|
||||||
|
|
@ -132,7 +134,7 @@ class HomeModals {
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
if (errorMessage.isNotEmpty)
|
if (errorMessage.isNotEmpty)
|
||||||
Padding(padding: const EdgeInsets.only(bottom: 10), child: Text(errorMessage, style: getSharedTextStyle(themeType, const TextStyle(color: Colors.red, fontSize: 14, fontWeight: FontWeight.bold)), textAlign: TextAlign.center)),
|
Padding(padding: const EdgeInsets.only(bottom: 10), child: Text(errorMessage, style: getSharedTextStyle(themeType, const TextStyle(color: Colors.red, fontSize: 14, fontWeight: FontWeight.bold)), textAlign: TextAlign.center)),
|
||||||
Text("💡 Nota: Non serve una vera email. Usa una password facile da ricordare!", style: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.6), fontSize: 11, height: 1.3)), textAlign: TextAlign.center),
|
Text("💡 Usa una password facile da ricordare!", style: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.6), fontSize: 11, height: 1.3)), textAlign: TextAlign.center),
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
isLoadingAuth ? CircularProgressIndicator(color: inkColor) : Row(
|
isLoadingAuth ? CircularProgressIndicator(color: inkColor) : Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -157,7 +159,7 @@ class HomeModals {
|
||||||
children: [
|
children: [
|
||||||
Text(loc.welcomeTitle, style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text, fontWeight: FontWeight.w900, fontSize: 20, letterSpacing: 1.5)), textAlign: TextAlign.center),
|
Text(loc.welcomeTitle, style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text, fontWeight: FontWeight.w900, fontSize: 20, letterSpacing: 1.5)), textAlign: TextAlign.center),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text('Scegli Nome e Password.\nTi serviranno per recuperare gli XP!', style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text.withOpacity(0.8), fontSize: 13)), textAlign: TextAlign.center),
|
Text('Scegli una Password per il Cloud.\nI tuoi XP e il tuo Livello saranno protetti e non li perderai mai!', style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text.withOpacity(0.8), fontSize: 13, fontWeight: FontWeight.bold)), textAlign: TextAlign.center),
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
TextField(
|
TextField(
|
||||||
controller: nameController, textCapitalization: TextCapitalization.characters, textAlign: TextAlign.center, maxLength: 8,
|
controller: nameController, textCapitalization: TextCapitalization.characters, textAlign: TextAlign.center, maxLength: 8,
|
||||||
|
|
@ -187,7 +189,7 @@ class HomeModals {
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
if (errorMessage.isNotEmpty)
|
if (errorMessage.isNotEmpty)
|
||||||
Padding(padding: const EdgeInsets.only(bottom: 10), child: Text(errorMessage, style: getSharedTextStyle(themeType, const TextStyle(color: Colors.redAccent, fontSize: 14, fontWeight: FontWeight.bold)), textAlign: TextAlign.center)),
|
Padding(padding: const EdgeInsets.only(bottom: 10), child: Text(errorMessage, style: getSharedTextStyle(themeType, const TextStyle(color: Colors.redAccent, fontSize: 14, fontWeight: FontWeight.bold)), textAlign: TextAlign.center)),
|
||||||
Text("💡 Nota: Non serve una vera email. Usa una password facile da ricordare!", style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text.withOpacity(0.6), fontSize: 11, height: 1.3)), textAlign: TextAlign.center),
|
Text("💡 Usa una password facile da ricordare!", style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text.withOpacity(0.6), fontSize: 11, height: 1.3)), textAlign: TextAlign.center),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
isLoadingAuth ? CircularProgressIndicator(color: themeManager.currentColors.playerBlue) : Row(
|
isLoadingAuth ? CircularProgressIndicator(color: themeManager.currentColors.playerBlue) : Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -203,7 +205,12 @@ class HomeModals {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) dialogContent = AnimatedCyberBorder(child: dialogContent);
|
if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) dialogContent = AnimatedCyberBorder(child: dialogContent);
|
||||||
return Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: dialogContent);
|
|
||||||
|
// LA PROTEZIONE ANTI-BACK DI ANDROID: Impedisce l'uscita non autorizzata
|
||||||
|
return PopScope(
|
||||||
|
canPop: false,
|
||||||
|
child: Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: dialogContent)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -277,7 +284,7 @@ class HomeModals {
|
||||||
Transform.rotate(
|
Transform.rotate(
|
||||||
angle: -0.02,
|
angle: -0.02,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () { Navigator.pop(ctx); context.read<GameController>().startNewGame(localRadius, vsCPU: isVsCPU, shape: localShape, timeMode: localTimeMode); Navigator.push(context, MaterialPageRoute(builder: (_) => GameScreen())); },
|
onTap: () { Navigator.pop(ctx); context.read<GameController>().startNewGame(localRadius, vsCPU: isVsCPU, shape: localShape, timeMode: localTimeMode); Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen())); },
|
||||||
child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: Colors.green.shade200, strokeColor: inkColor, seed: 300), child: Container(height: 65, width: double.infinity, alignment: Alignment.center, child: Text(loc.startGame, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.w900, letterSpacing: 3.0, color: inkColor))))),
|
child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: Colors.green.shade200, strokeColor: inkColor, seed: 300), child: Container(height: 65, width: double.infinity, alignment: Alignment.center, child: Text(loc.startGame, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.w900, letterSpacing: 3.0, color: inkColor))))),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -342,7 +349,7 @@ class HomeModals {
|
||||||
width: double.infinity, height: 60,
|
width: double.infinity, height: 60,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: isVsCPU ? Colors.purple.shade400 : theme.playerRed, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
style: ElevatedButton.styleFrom(backgroundColor: isVsCPU ? Colors.purple.shade400 : theme.playerRed, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
||||||
onPressed: () { Navigator.pop(ctx); context.read<GameController>().startNewGame(localRadius, vsCPU: isVsCPU, shape: localShape, timeMode: localTimeMode); Navigator.push(context, MaterialPageRoute(builder: (_) => GameScreen())); },
|
onPressed: () { Navigator.pop(ctx); context.read<GameController>().startNewGame(localRadius, vsCPU: isVsCPU, shape: localShape, timeMode: localTimeMode); Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen())); },
|
||||||
child: Text(loc.startGame, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, letterSpacing: 2)),
|
child: Text(loc.startGame, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, letterSpacing: 2)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -438,7 +445,7 @@ class HomeModals {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
context.read<GameController>().startNewGame(selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: selectedShape, timeMode: isTimeMode);
|
context.read<GameController>().startNewGame(selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: selectedShape, timeMode: isTimeMode);
|
||||||
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => GameScreen()));
|
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -549,154 +556,13 @@ class HomeModals {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text("CHIUDI", style: getLobbyTextStyle(themeType, TextStyle(color: theme.playerRed))))
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: Text("CHIUDI", style: getLobbyTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold))),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// --- WIDGET POPUP PER AMICO ONLINE ---
|
|
||||||
class FavoriteOnlinePopup extends StatefulWidget {
|
|
||||||
final String name;
|
|
||||||
final VoidCallback onDismiss;
|
|
||||||
const FavoriteOnlinePopup({super.key, required this.name, required this.onDismiss});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<FavoriteOnlinePopup> createState() => _FavoriteOnlinePopupState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FavoriteOnlinePopupState extends State<FavoriteOnlinePopup> with SingleTickerProviderStateMixin {
|
|
||||||
late AnimationController _ctrl;
|
|
||||||
late Animation<double> _fade;
|
|
||||||
late Animation<Offset> _slide;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 400));
|
|
||||||
_fade = Tween<double>(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut));
|
|
||||||
_slide = Tween<Offset>(begin: const Offset(0, -0.5), end: Offset.zero).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOutBack));
|
|
||||||
|
|
||||||
_ctrl.forward();
|
|
||||||
|
|
||||||
Future.delayed(const Duration(seconds: 3), () async {
|
|
||||||
if (mounted) {
|
|
||||||
await _ctrl.reverse();
|
|
||||||
widget.onDismiss();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_ctrl.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleTap() {
|
|
||||||
final game = context.read<GameController>();
|
|
||||||
final themeManager = context.read<ThemeManager>();
|
|
||||||
final theme = themeManager.currentColors;
|
|
||||||
final themeType = themeManager.currentThemeType;
|
|
||||||
|
|
||||||
// Se il gioco è attivo (non finito e siamo oltre la fase di setup)
|
|
||||||
bool isInGame = !game.isGameOver && (!game.isSetupPhase || game.isOnline || game.isVsCPU);
|
|
||||||
|
|
||||||
if (isInGame) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (ctx) => AlertDialog(
|
|
||||||
backgroundColor: theme.background,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15), side: BorderSide(color: theme.playerRed, width: 2)),
|
|
||||||
title: Text("Sei in partita!", style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold))),
|
|
||||||
content: Text("Vuoi abbandonare la partita attuale per raggiungere la lobby multiplayer?", style: TextStyle(color: theme.text)),
|
|
||||||
actions: [
|
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text("Annulla", style: TextStyle(color: theme.text))),
|
|
||||||
ElevatedButton(
|
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: theme.playerRed),
|
|
||||||
onPressed: () {
|
|
||||||
game.disconnectOnlineGame();
|
|
||||||
Navigator.pop(ctx);
|
|
||||||
widget.onDismiss();
|
|
||||||
Navigator.popUntil(context, (route) => route.isFirst);
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (_) => LobbyScreen()));
|
|
||||||
},
|
|
||||||
child: const Text("Abbandona e Vai", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
widget.onDismiss();
|
|
||||||
Navigator.popUntil(context, (route) => route.isFirst);
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (_) => LobbyScreen()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final themeManager = context.watch<ThemeManager>();
|
|
||||||
final theme = themeManager.currentColors;
|
|
||||||
final themeType = themeManager.currentThemeType;
|
|
||||||
|
|
||||||
return Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: SlideTransition(
|
|
||||||
position: _slide,
|
|
||||||
child: FadeTransition(
|
|
||||||
opacity: _fade,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: _handleTap,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [theme.playerBlue.withOpacity(0.95), theme.playerBlue.withOpacity(0.8)],
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
border: Border.all(color: Colors.white.withOpacity(0.4), width: 1.5),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 15, offset: const Offset(0, 8)),
|
|
||||||
BoxShadow(color: theme.playerBlue.withOpacity(0.4), blurRadius: 10, spreadRadius: 1),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white.withOpacity(0.2),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.star_rounded, color: Colors.amber, size: 24),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 15),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text("Giocatore Online!", style: getSharedTextStyle(themeType, TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 11, fontWeight: FontWeight.bold, letterSpacing: 1.5))),
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
Text("${widget.name} è in partita", style: getSharedTextStyle(themeType, const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w900))),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () async {
|
|
||||||
await _ctrl.reverse();
|
|
||||||
widget.onDismiss();
|
|
||||||
},
|
|
||||||
child: Icon(Icons.close, color: Colors.white.withOpacity(0.6), size: 20),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -3,11 +3,13 @@
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
import 'dart:math'; // Aggiunto per generare il codice della stanza randomico
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:app_links/app_links.dart';
|
import 'package:app_links/app_links.dart';
|
||||||
|
|
||||||
|
|
@ -43,6 +45,7 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
late AppLinks _appLinks;
|
late AppLinks _appLinks;
|
||||||
StreamSubscription<Uri>? _linkSubscription;
|
StreamSubscription<Uri>? _linkSubscription;
|
||||||
StreamSubscription<QuerySnapshot>? _favoritesSubscription;
|
StreamSubscription<QuerySnapshot>? _favoritesSubscription;
|
||||||
|
StreamSubscription<QuerySnapshot>? _invitesSubscription; // <--- Nuovo Listener per gli inviti in arrivo
|
||||||
|
|
||||||
Map<String, DateTime> _lastOnlineNotifications = {};
|
Map<String, DateTime> _lastOnlineNotifications = {};
|
||||||
|
|
||||||
|
|
@ -62,10 +65,16 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (StorageService.instance.playerName.isEmpty) {
|
if (FirebaseAuth.instance.currentUser == null) {
|
||||||
HomeModals.showNameDialog(context, () => setState(() {}));
|
HomeModals.showNameDialog(context, () {
|
||||||
|
StorageService.instance.syncLeaderboard();
|
||||||
|
_listenToInvites(); // <--- Ascoltiamo gli inviti appena loggati
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
StorageService.instance.syncLeaderboard();
|
||||||
|
_listenToInvites(); // <--- Ascoltiamo gli inviti se eravamo già loggati
|
||||||
}
|
}
|
||||||
StorageService.instance.syncLeaderboard();
|
|
||||||
_checkThemeSafety();
|
_checkThemeSafety();
|
||||||
});
|
});
|
||||||
_checkClipboardForInvite();
|
_checkClipboardForInvite();
|
||||||
|
|
@ -87,6 +96,7 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
_cleanupGhostRoom();
|
_cleanupGhostRoom();
|
||||||
_linkSubscription?.cancel();
|
_linkSubscription?.cancel();
|
||||||
_favoritesSubscription?.cancel();
|
_favoritesSubscription?.cancel();
|
||||||
|
_invitesSubscription?.cancel(); // <--- Chiusura Listener
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,12 +168,16 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
.where(FieldPath.documentId, whereIn: favUids)
|
.where(FieldPath.documentId, whereIn: favUids)
|
||||||
.snapshots()
|
.snapshots()
|
||||||
.listen((snapshot) {
|
.listen((snapshot) {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
for (var change in snapshot.docChanges) {
|
for (var change in snapshot.docChanges) {
|
||||||
if (change.type == DocumentChangeType.modified || change.type == DocumentChangeType.added) {
|
if (change.type == DocumentChangeType.modified || change.type == DocumentChangeType.added) {
|
||||||
var data = change.doc.data();
|
var data = change.doc.data();
|
||||||
if (data != null && data['lastActive'] != null) {
|
if (data != null && data['lastActive'] != null) {
|
||||||
Timestamp lastActive = data['lastActive'];
|
Timestamp lastActive = data['lastActive'];
|
||||||
if (DateTime.now().difference(lastActive.toDate()).inSeconds < 15) {
|
int diffInSeconds = DateTime.now().difference(lastActive.toDate()).inSeconds;
|
||||||
|
|
||||||
|
if (diffInSeconds.abs() < 180) {
|
||||||
String name = data['name'] ?? 'Un amico';
|
String name = data['name'] ?? 'Un amico';
|
||||||
_showFavoriteOnlinePopup(name);
|
_showFavoriteOnlinePopup(name);
|
||||||
}
|
}
|
||||||
|
|
@ -174,8 +188,10 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showFavoriteOnlinePopup(String name) {
|
void _showFavoriteOnlinePopup(String name) {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
if (_lastOnlineNotifications.containsKey(name)) {
|
if (_lastOnlineNotifications.containsKey(name)) {
|
||||||
if (DateTime.now().difference(_lastOnlineNotifications[name]!).inMinutes < 5) return;
|
if (DateTime.now().difference(_lastOnlineNotifications[name]!).inMinutes < 1) return;
|
||||||
}
|
}
|
||||||
_lastOnlineNotifications[name] = DateTime.now();
|
_lastOnlineNotifications[name] = DateTime.now();
|
||||||
|
|
||||||
|
|
@ -202,6 +218,141 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
overlay.insert(entry);
|
overlay.insert(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// SISTEMA INVITI DIRETTO TRAMITE FIRESTORE
|
||||||
|
// =========================================================================
|
||||||
|
void _listenToInvites() {
|
||||||
|
final user = FirebaseAuth.instance.currentUser;
|
||||||
|
if (user == null) return;
|
||||||
|
|
||||||
|
_invitesSubscription?.cancel();
|
||||||
|
_invitesSubscription = FirebaseFirestore.instance
|
||||||
|
.collection('invites')
|
||||||
|
.where('toUid', isEqualTo: user.uid)
|
||||||
|
.snapshots()
|
||||||
|
.listen((snapshot) {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
for (var change in snapshot.docChanges) {
|
||||||
|
if (change.type == DocumentChangeType.added) {
|
||||||
|
var data = change.doc.data();
|
||||||
|
if (data != null) {
|
||||||
|
String code = data['roomCode'];
|
||||||
|
String from = data['fromName'];
|
||||||
|
String inviteId = change.doc.id;
|
||||||
|
|
||||||
|
// Filtro sicurezza: Evita di mostrare inviti fantasma vecchi di oltre 2 minuti
|
||||||
|
Timestamp? ts = data['timestamp'];
|
||||||
|
if (ts != null) {
|
||||||
|
if (DateTime.now().difference(ts.toDate()).inMinutes > 2) {
|
||||||
|
FirebaseFirestore.instance.collection('invites').doc(inviteId).delete();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_showInvitePopup(from, code, inviteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showInvitePopup(String fromName, String roomCode, String inviteId) {
|
||||||
|
final themeType = context.read<ThemeManager>().currentThemeType;
|
||||||
|
final theme = context.read<ThemeManager>().currentColors;
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
backgroundColor: themeType == AppThemeType.doodle ? Colors.white : theme.background,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: theme.playerRed, width: 2)),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.warning_amber_rounded, color: theme.playerRed),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text("SFIDA IN ARRIVO!", style: getSharedTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 18))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Text("$fromName ti ha sfidato a duello!\nAccetti la sfida?", style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontSize: 16))),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
FirebaseFirestore.instance.collection('invites').doc(inviteId).delete();
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
},
|
||||||
|
child: Text("RIFIUTA", style: getSharedTextStyle(themeType, const TextStyle(color: Colors.grey, fontWeight: FontWeight.bold))),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
|
||||||
|
onPressed: () {
|
||||||
|
FirebaseFirestore.instance.collection('invites').doc(inviteId).delete();
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
_joinRoomByCode(roomCode);
|
||||||
|
},
|
||||||
|
child: Text("ACCETTA!", style: getSharedTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold))),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _sendChallenge(String targetUid, String targetName) async {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
|
// Generiamo un codice stanza casuale univoco
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
final rnd = Random();
|
||||||
|
String roomCode = String.fromCharCodes(Iterable.generate(5, (_) => chars.codeUnitAt(rnd.nextInt(chars.length))));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Creiamo la stanza sul database
|
||||||
|
await FirebaseFirestore.instance.collection('games').doc(roomCode).set({
|
||||||
|
'status': 'waiting',
|
||||||
|
'hostName': StorageService.instance.playerName,
|
||||||
|
'hostUid': FirebaseAuth.instance.currentUser?.uid,
|
||||||
|
'radius': 4,
|
||||||
|
'shape': 'classic',
|
||||||
|
'timeMode': true,
|
||||||
|
'isPublic': false, // È una stanza privata!
|
||||||
|
'createdAt': FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Inviamo l'invito al nostro avversario
|
||||||
|
await FirebaseFirestore.instance.collection('invites').add({
|
||||||
|
'toUid': targetUid,
|
||||||
|
'fromName': StorageService.instance.playerName,
|
||||||
|
'roomCode': roomCode,
|
||||||
|
'timestamp': FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
|
||||||
|
// 3. Apriamo il radar d'attesa (che ascolta quando lui accetta)
|
||||||
|
if (mounted) {
|
||||||
|
HomeModals.showWaitingDialog(
|
||||||
|
context: context,
|
||||||
|
code: roomCode,
|
||||||
|
isPublicRoom: false,
|
||||||
|
selectedRadius: 4,
|
||||||
|
selectedShape: ArenaShape.classic,
|
||||||
|
isTimeMode: true,
|
||||||
|
multiplayerService: _multiplayerService,
|
||||||
|
onRoomStarted: () {},
|
||||||
|
onCleanup: () {
|
||||||
|
// Se noi annulliamo, cancelliamo la stanza
|
||||||
|
FirebaseFirestore.instance.collection('games').doc(roomCode).delete();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Errore: $e", style: const TextStyle(color: Colors.white)), backgroundColor: Colors.red));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
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();
|
||||||
|
|
@ -225,7 +376,7 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
bool hostTimeMode = roomData['timeMode'] ?? true;
|
bool hostTimeMode = roomData['timeMode'] ?? true;
|
||||||
|
|
||||||
context.read<GameController>().startNewGame(hostRadius, isOnline: true, roomCode: code, isHost: false, shape: hostShape, timeMode: hostTimeMode);
|
context.read<GameController>().startNewGame(hostRadius, isOnline: true, roomCode: code, isHost: false, shape: hostShape, timeMode: hostTimeMode);
|
||||||
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => GameScreen()));
|
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen()));
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Stanza non trovata, piena o partita già iniziata.", style: TextStyle(color: Colors.white)), backgroundColor: Colors.red));
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Stanza non trovata, piena o partita già iniziata.", style: TextStyle(color: Colors.white)), backgroundColor: Colors.red));
|
||||||
}
|
}
|
||||||
|
|
@ -286,14 +437,17 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Proteggiamo anche nome e livello
|
|
||||||
Text(
|
Text(
|
||||||
"\u00A0${playerName.toUpperCase()}\u00A0\u00A0",
|
playerName.toUpperCase(),
|
||||||
style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : theme.text, fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.0)),
|
style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : theme.text, fontWeight: FontWeight.bold, fontSize: 16)),
|
||||||
|
overflow: TextOverflow.visible,
|
||||||
|
softWrap: false,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"\u00A0LIV. $playerLevel\u00A0\u00A0",
|
"LIV. $playerLevel",
|
||||||
style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.8) : theme.playerBlue, fontWeight: FontWeight.bold, fontSize: 11, letterSpacing: 1.0)),
|
style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.8) : theme.playerBlue, fontWeight: FontWeight.bold, fontSize: 11)),
|
||||||
|
overflow: TextOverflow.visible,
|
||||||
|
softWrap: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -302,42 +456,44 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// --- BOX STATISTICHE BLINDATO ---
|
// --- BOX STATISTICHE ---
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), // Padding compensato
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
decoration: _glassBoxDecoration(theme, themeType),
|
decoration: _glassBoxDecoration(theme, themeType),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(themeType == AppThemeType.music ? FontAwesomeIcons.microphone : Icons.emoji_events, color: Colors.amber.shade600, size: 16),
|
Icon(themeType == AppThemeType.music ? FontAwesomeIcons.microphone : Icons.emoji_events, color: Colors.amber.shade600, size: 16),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
|
||||||
// Doppio spazio invisibile \u00A0\u00A0 e letterSpacing per salvare la pancia a destra!
|
|
||||||
Text(
|
Text(
|
||||||
"\u00A0${StorageService.instance.wins}\u00A0\u00A0",
|
"${StorageService.instance.wins}",
|
||||||
style: getSharedTextStyle(themeType, TextStyle(
|
style: getSharedTextStyle(themeType, TextStyle(
|
||||||
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
|
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
letterSpacing: 2.0, // Forza ulteriore spazio orizzontale
|
|
||||||
)),
|
)),
|
||||||
|
overflow: TextOverflow.visible,
|
||||||
|
softWrap: false,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 10),
|
||||||
Icon(themeType == AppThemeType.music ? FontAwesomeIcons.compactDisc : Icons.sentiment_very_dissatisfied, color: theme.playerRed.withOpacity(0.8), size: 16),
|
Icon(themeType == AppThemeType.music ? FontAwesomeIcons.compactDisc : Icons.sentiment_very_dissatisfied, color: theme.playerRed.withOpacity(0.8), size: 16),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
|
||||||
// Idem per le sconfitte
|
|
||||||
Text(
|
Text(
|
||||||
"\u00A0${StorageService.instance.losses}\u00A0\u00A0",
|
"${StorageService.instance.losses}",
|
||||||
style: getSharedTextStyle(themeType, TextStyle(
|
style: getSharedTextStyle(themeType, TextStyle(
|
||||||
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
|
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
letterSpacing: 2.0,
|
|
||||||
)),
|
)),
|
||||||
|
overflow: TextOverflow.visible,
|
||||||
|
softWrap: false,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 10),
|
||||||
Container(width: 1, height: 20, color: (themeType == AppThemeType.doodle ? inkColor : Colors.white).withOpacity(0.2)),
|
Container(width: 1, height: 20, color: (themeType == AppThemeType.doodle ? inkColor : Colors.white).withOpacity(0.2)),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
|
|
||||||
|
|
@ -431,18 +587,19 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
},
|
},
|
||||||
child: FittedBox(
|
child: FittedBox(
|
||||||
fit: BoxFit.scaleDown,
|
fit: BoxFit.scaleDown,
|
||||||
// IL TRUCCO: Spazio vuoto anche nel titolo principale
|
|
||||||
child: Text(
|
child: Text(
|
||||||
"${loc.appTitle.toUpperCase()} ",
|
loc.appTitle.toUpperCase(),
|
||||||
style: getSharedTextStyle(themeType, TextStyle(
|
style: getSharedTextStyle(themeType, TextStyle(
|
||||||
fontSize: 65 * vScale,
|
fontSize: 65 * vScale,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
|
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
|
||||||
letterSpacing: 10 * vScale,
|
letterSpacing: 10 * vScale,
|
||||||
shadows: themeType == AppThemeType.doodle
|
shadows: themeType == AppThemeType.doodle
|
||||||
? [const Shadow(color: Colors.white, offset: Offset(2.5, 2.5), blurRadius: 2), const Shadow(color: Colors.white, offset: Offset(-2.5, -2.5), blurRadius: 2)]
|
? [const Shadow(color: Colors.white, offset: Offset(2.5, 2.5), blurRadius: 2), const Shadow(color: Colors.white, offset: Offset(-2.5, -2.5), blurRadius: 2)]
|
||||||
: [Shadow(color: Colors.black.withOpacity(0.8), offset: const Offset(3, 4), blurRadius: 8), Shadow(color: theme.playerBlue.withOpacity(0.4), offset: const Offset(0, 0), blurRadius: 20)]
|
: [Shadow(color: Colors.black.withOpacity(0.8), offset: const Offset(3, 4), blurRadius: 8), Shadow(color: theme.playerBlue.withOpacity(0.4), offset: const Offset(0, 0), blurRadius: 20)]
|
||||||
))
|
)),
|
||||||
|
overflow: TextOverflow.visible,
|
||||||
|
softWrap: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -460,7 +617,7 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: MusicKnobCard(title: loc.leaderboardTitle, icon: FontAwesomeIcons.compactDisc, iconColor: Colors.amber, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const LeaderboardDialog()))),
|
Expanded(child: MusicKnobCard(title: loc.leaderboardTitle, icon: FontAwesomeIcons.compactDisc, iconColor: Colors.amber, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => LeaderboardDialog(onChallenge: _sendChallenge)))),
|
||||||
Expanded(child: MusicKnobCard(title: loc.questsTitle, icon: FontAwesomeIcons.microphoneLines, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const QuestsDialog()))),
|
Expanded(child: MusicKnobCard(title: loc.questsTitle, icon: FontAwesomeIcons.microphoneLines, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const QuestsDialog()))),
|
||||||
Expanded(child: MusicKnobCard(title: loc.themesTitle, icon: FontAwesomeIcons.palette, themeType: themeType, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => SettingsScreen())))),
|
Expanded(child: MusicKnobCard(title: loc.themesTitle, icon: FontAwesomeIcons.palette, themeType: themeType, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => SettingsScreen())))),
|
||||||
Expanded(child: MusicKnobCard(title: loc.tutorialTitle, icon: FontAwesomeIcons.bookOpen, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const TutorialDialog()))),
|
Expanded(child: MusicKnobCard(title: loc.tutorialTitle, icon: FontAwesomeIcons.bookOpen, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const TutorialDialog()))),
|
||||||
|
|
@ -479,7 +636,7 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: _buildCyberCard(FeatureCard(title: loc.leaderboardTitle, subtitle: "Top 50 Globale", icon: Icons.leaderboard, color: Colors.amber.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const LeaderboardDialog()), compact: true), themeType)),
|
Expanded(child: _buildCyberCard(FeatureCard(title: loc.leaderboardTitle, subtitle: "Top 50 Globale", icon: Icons.leaderboard, color: Colors.amber.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => LeaderboardDialog(onChallenge: _sendChallenge)), compact: true), themeType)),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(child: _buildCyberCard(FeatureCard(title: loc.questsTitle, subtitle: "Missioni", icon: Icons.assignment_turned_in, color: Colors.green.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const QuestsDialog()), compact: true), themeType)),
|
Expanded(child: _buildCyberCard(FeatureCard(title: loc.questsTitle, subtitle: "Missioni", icon: Icons.assignment_turned_in, color: Colors.green.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const QuestsDialog()), compact: true), themeType)),
|
||||||
],
|
],
|
||||||
|
|
@ -578,4 +735,95 @@ class FullScreenGridPainter extends CustomPainter {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- WIDGET POPUP AMICO ONLINE (Ripristinato in coda!) ---
|
||||||
|
class FavoriteOnlinePopup extends StatefulWidget {
|
||||||
|
final String name;
|
||||||
|
final VoidCallback onDismiss;
|
||||||
|
|
||||||
|
const FavoriteOnlinePopup({super.key, required this.name, required this.onDismiss});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FavoriteOnlinePopup> createState() => _FavoriteOnlinePopupState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FavoriteOnlinePopupState extends State<FavoriteOnlinePopup> with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<Offset> _offsetAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400));
|
||||||
|
_offsetAnimation = Tween<Offset>(begin: const Offset(0.0, -1.5), end: Offset.zero)
|
||||||
|
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutBack));
|
||||||
|
|
||||||
|
_controller.forward();
|
||||||
|
|
||||||
|
// Chiude il popup automaticamente dopo 3 secondi
|
||||||
|
Future.delayed(const Duration(seconds: 3), () {
|
||||||
|
if (mounted) {
|
||||||
|
_controller.reverse().then((_) => widget.onDismiss());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final themeManager = context.watch<ThemeManager>();
|
||||||
|
final themeType = themeManager.currentThemeType;
|
||||||
|
final theme = themeManager.currentColors;
|
||||||
|
Color inkColor = const Color(0xFF111122);
|
||||||
|
|
||||||
|
return SlideTransition(
|
||||||
|
position: _offsetAnimation,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: themeType == AppThemeType.doodle ? Colors.white : theme.background,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(
|
||||||
|
color: themeType == AppThemeType.doodle ? inkColor : theme.playerBlue,
|
||||||
|
width: 2
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.3),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 5)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.circle, color: Colors.greenAccent, size: 14),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text(
|
||||||
|
"${widget.name} è online!",
|
||||||
|
style: getSharedTextStyle(
|
||||||
|
themeType,
|
||||||
|
TextStyle(
|
||||||
|
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 15
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue