Auto-sync: 20260324_140000
This commit is contained in:
parent
1395fc32f6
commit
027c41a75c
22 changed files with 166301 additions and 76 deletions
|
|
@ -549,7 +549,7 @@ class GameController extends ChangeNotifier {
|
||||||
|
|
||||||
if (!forced) _playEffects(newClosed, newGhosts: newGhosts, isOpponent: false);
|
if (!forced) _playEffects(newClosed, newGhosts: newGhosts, isOpponent: false);
|
||||||
|
|
||||||
_startTimer(); notifyListeners();
|
notifyListeners(); // Modificato per non riavviare ciecamente il timer
|
||||||
|
|
||||||
if (isOnline && roomCode != null) {
|
if (isOnline && roomCode != null) {
|
||||||
Map<String, dynamic> moveData = {
|
Map<String, dynamic> moveData = {
|
||||||
|
|
@ -567,17 +567,27 @@ class GameController extends ChangeNotifier {
|
||||||
if (board.isGameOver) {
|
if (board.isGameOver) {
|
||||||
_saveMatchResult();
|
_saveMatchResult();
|
||||||
if (isHost) FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'finished'});
|
if (isHost) FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'finished'});
|
||||||
|
} else {
|
||||||
|
_startTimer(); // Rimesso il timer se si continua a giocare online
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (board.isGameOver) _saveMatchResult();
|
if (board.isGameOver) {
|
||||||
else if (isVsCPU && board.currentPlayer == Player.blue) _checkCPUTurn();
|
_saveMatchResult();
|
||||||
|
} else if (isVsCPU && board.currentPlayer == Player.blue) {
|
||||||
|
_checkCPUTurn(); // Se tocca alla CPU, la CPU fermerà il timer internamente
|
||||||
|
} else {
|
||||||
|
_startTimer(); // Se tocca all'umano, facciamo (ri)partire il timer!
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _checkCPUTurn() async {
|
void _checkCPUTurn() async {
|
||||||
if (isVsCPU && board.currentPlayer == Player.blue && !board.isGameOver) {
|
if (isVsCPU && board.currentPlayer == Player.blue && !board.isGameOver) {
|
||||||
isCPUThinking = true; _blitzTimer?.cancel(); notifyListeners();
|
isCPUThinking = true;
|
||||||
|
_blitzTimer?.cancel(); // La CPU inizia a pensare, congela il timer del giocatore
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
await Future.delayed(const Duration(milliseconds: 600));
|
await Future.delayed(const Duration(milliseconds: 600));
|
||||||
|
|
||||||
if (!board.isGameOver) {
|
if (!board.isGameOver) {
|
||||||
|
|
@ -592,10 +602,18 @@ class GameController extends ChangeNotifier {
|
||||||
|
|
||||||
_playEffects(newClosed, newGhosts: newGhosts, isOpponent: true);
|
_playEffects(newClosed, newGhosts: newGhosts, isOpponent: true);
|
||||||
|
|
||||||
isCPUThinking = false; _startTimer(); notifyListeners();
|
isCPUThinking = false;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
if (board.isGameOver) _saveMatchResult();
|
if (board.isGameOver) {
|
||||||
else _checkCPUTurn();
|
_saveMatchResult();
|
||||||
|
} else if (board.currentPlayer == Player.blue) {
|
||||||
|
// La CPU ha chiuso un quadrato e ha diritto a un'altra mossa
|
||||||
|
_checkCPUTurn();
|
||||||
|
} else {
|
||||||
|
// Turno passato all'umano: il timer riparte!
|
||||||
|
_startTimer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io' show Platform, HttpClient;
|
import 'dart:io' show Platform, HttpClient;
|
||||||
|
import 'dart:async'; // <--- AGGIUNTO PER IL TIMER DELL'HEARTBEAT
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import '../core/app_colors.dart';
|
import '../core/app_colors.dart';
|
||||||
|
|
@ -18,6 +19,7 @@ class StorageService {
|
||||||
|
|
||||||
late SharedPreferences _prefs;
|
late SharedPreferences _prefs;
|
||||||
int _sessionStart = 0;
|
int _sessionStart = 0;
|
||||||
|
Timer? _heartbeatTimer; // <--- IL NOSTRO BATTITO CARDIACO
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
_prefs = await SharedPreferences.getInstance();
|
_prefs = await SharedPreferences.getInstance();
|
||||||
|
|
@ -26,6 +28,20 @@ class StorageService {
|
||||||
_sessionStart = DateTime.now().millisecondsSinceEpoch;
|
_sessionStart = DateTime.now().millisecondsSinceEpoch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- NUOVI METODI PER GESTIRE LA PRESENZA ---
|
||||||
|
void startHeartbeat() {
|
||||||
|
_heartbeatTimer?.cancel();
|
||||||
|
// Esegue il sync leggero ogni 60 secondi
|
||||||
|
_heartbeatTimer = Timer.periodic(const Duration(seconds: 120), (_) {
|
||||||
|
syncLeaderboard(isHeartbeat: true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void stopHeartbeat() {
|
||||||
|
_heartbeatTimer?.cancel();
|
||||||
|
}
|
||||||
|
// ----------------------------------------------
|
||||||
|
|
||||||
Future<void> _fetchLocationData() async {
|
Future<void> _fetchLocationData() async {
|
||||||
if (kIsWeb) return;
|
if (kIsWeb) return;
|
||||||
try {
|
try {
|
||||||
|
|
@ -112,9 +128,9 @@ class StorageService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================================================================
|
// ======================================================================
|
||||||
// FIX: ORA IL SYNC MANDA I DATI REALI ALLA DASHBOARD ADMIN!
|
// LOGICA SYNC AGGIORNATA: GESTIONE HEARTBEAT LEGGERO
|
||||||
// ======================================================================
|
// ======================================================================
|
||||||
Future<void> syncLeaderboard() async {
|
Future<void> syncLeaderboard({bool isHeartbeat = false}) async {
|
||||||
try {
|
try {
|
||||||
final user = FirebaseAuth.instance.currentUser;
|
final user = FirebaseAuth.instance.currentUser;
|
||||||
if (user == null) return;
|
if (user == null) return;
|
||||||
|
|
@ -124,7 +140,23 @@ class StorageService {
|
||||||
|
|
||||||
String targetUid = user.uid;
|
String targetUid = user.uid;
|
||||||
|
|
||||||
// 1. Recupero Versione App e Modello Dispositivo
|
// 1. Calcolo del Playtime effettivo (aggiornato ad ogni sync)
|
||||||
|
int sessionDurationSec = (DateTime.now().millisecondsSinceEpoch - _sessionStart) ~/ 1000;
|
||||||
|
int savedPlaytime = _prefs.getInt('total_playtime') ?? 0;
|
||||||
|
int totalPlaytime = savedPlaytime + sessionDurationSec;
|
||||||
|
await _prefs.setInt('total_playtime', totalPlaytime);
|
||||||
|
_sessionStart = DateTime.now().millisecondsSinceEpoch; // Resetta il timer di sessione
|
||||||
|
|
||||||
|
// 2. Creazione del payload di base (dati leggeri che cambiano spesso)
|
||||||
|
Map<String, dynamic> dataToSave = {
|
||||||
|
'name': name,
|
||||||
|
'level': playerLevel,
|
||||||
|
'lastActive': FieldValue.serverTimestamp(),
|
||||||
|
'playtime': totalPlaytime,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Se NON è un heartbeat, raccogliamo anche i dati "pesanti" (Device info, ecc.)
|
||||||
|
if (!isHeartbeat) {
|
||||||
String appVer = "N/D";
|
String appVer = "N/D";
|
||||||
String devModel = "N/D";
|
String devModel = "N/D";
|
||||||
String osName = kIsWeb ? "Web" : Platform.operatingSystem;
|
String osName = kIsWeb ? "Web" : Platform.operatingSystem;
|
||||||
|
|
@ -153,29 +185,16 @@ class StorageService {
|
||||||
debugPrint("Errore device info: $e");
|
debugPrint("Errore device info: $e");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Calcolo del Playtime effettivo (aggiornato ad ogni sync)
|
dataToSave['appVersion'] = appVer;
|
||||||
int sessionDurationSec = (DateTime.now().millisecondsSinceEpoch - _sessionStart) ~/ 1000;
|
dataToSave['deviceModel'] = devModel;
|
||||||
int savedPlaytime = _prefs.getInt('total_playtime') ?? 0;
|
dataToSave['platform'] = osName;
|
||||||
int totalPlaytime = savedPlaytime + sessionDurationSec;
|
dataToSave['ip'] = lastIp;
|
||||||
await _prefs.setInt('total_playtime', totalPlaytime);
|
dataToSave['city'] = lastCity;
|
||||||
_sessionStart = DateTime.now().millisecondsSinceEpoch; // Resetta il timer di sessione
|
|
||||||
|
|
||||||
// 3. Creazione del payload per Firebase
|
|
||||||
Map<String, dynamic> dataToSave = {
|
|
||||||
'name': name,
|
|
||||||
'level': playerLevel,
|
|
||||||
'lastActive': FieldValue.serverTimestamp(),
|
|
||||||
'appVersion': appVer,
|
|
||||||
'deviceModel': devModel,
|
|
||||||
'platform': osName,
|
|
||||||
'ip': lastIp,
|
|
||||||
'city': lastCity,
|
|
||||||
'playtime': totalPlaytime,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (user.metadata.creationTime != null) {
|
if (user.metadata.creationTime != null) {
|
||||||
dataToSave['accountCreated'] = Timestamp.fromDate(user.metadata.creationTime!);
|
dataToSave['accountCreated'] = Timestamp.fromDate(user.metadata.creationTime!);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await FirebaseFirestore.instance.collection('leaderboard').doc(targetUid).set(dataToSave, SetOptions(merge: true));
|
await FirebaseFirestore.instance.collection('leaderboard').doc(targetUid).set(dataToSave, SetOptions(merge: true));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,60 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
|
||||||
@override
|
@override
|
||||||
void dispose() { _blinkController.dispose(); super.dispose(); }
|
void dispose() { _blinkController.dispose(); super.dispose(); }
|
||||||
|
|
||||||
|
// --- NUOVO DIALOG: CONFERMA USCITA (ANTI-FUGA) ---
|
||||||
|
void _showExitConfirmationDialog(BuildContext context, GameController gameController, ThemeColors theme, AppThemeType themeType) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
backgroundColor: 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),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
"ABBANDONARE?",
|
||||||
|
style: _getTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 18))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Text(
|
||||||
|
"Se esci ora, la partita verrà registrata automaticamente come una SCONFITTA.\n\nSei sicuro di voler fuggire?",
|
||||||
|
style: _getTextStyle(themeType, TextStyle(color: theme.text, fontSize: 15, height: 1.4)),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: Text("ANNULLA", style: _getTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6), fontWeight: FontWeight.bold))),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: theme.playerRed,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
// 1. Assegna la sconfitta!
|
||||||
|
StorageService.instance.addLoss();
|
||||||
|
// 2. Disconnette e pulisce
|
||||||
|
gameController.disconnectOnlineGame();
|
||||||
|
// 3. Chiude il dialog
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
// 4. Torna al menu
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text("SÌ, ESCI", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold))),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _showGameOverDialog(BuildContext context, GameController game, ThemeColors theme, AppThemeType themeType) {
|
void _showGameOverDialog(BuildContext context, GameController game, ThemeColors theme, AppThemeType themeType) {
|
||||||
_gameOverDialogShown = true;
|
_gameOverDialogShown = true;
|
||||||
|
|
||||||
|
|
@ -406,7 +460,18 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
style: TextButton.styleFrom(backgroundColor: bgImage != null || themeType == AppThemeType.arcade ? Colors.black87 : theme.background, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: Colors.white.withOpacity(0.1), width: 1))),
|
style: TextButton.styleFrom(backgroundColor: bgImage != null || themeType == AppThemeType.arcade ? Colors.black87 : theme.background, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: Colors.white.withOpacity(0.1), width: 1))),
|
||||||
icon: Icon(Icons.exit_to_app, color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, size: 20),
|
icon: Icon(Icons.exit_to_app, color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, size: 20),
|
||||||
onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(context); },
|
|
||||||
|
// --- NUOVO ON PRESSED ANTI-FUGA ---
|
||||||
|
onPressed: () {
|
||||||
|
if (!gameController.isGameOver && !gameController.isSetupPhase) {
|
||||||
|
_showExitConfirmationDialog(context, gameController, theme, themeType);
|
||||||
|
} else {
|
||||||
|
gameController.disconnectOnlineGame();
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// ------------------------
|
||||||
|
|
||||||
label: Text("ESCI", style: _getTextStyle(themeType, TextStyle(color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, fontWeight: FontWeight.bold, fontSize: 12))),
|
label: Text("ESCI", style: _getTextStyle(themeType, TextStyle(color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, fontWeight: FontWeight.bold, fontSize: 12))),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -424,9 +489,20 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// --- NUOVA LOGICA: Impedisce la chiusura accidentale o la fuga (Tasto Indietro) ---
|
||||||
|
bool shouldConfirmExit = !gameController.isGameOver && !gameController.isSetupPhase;
|
||||||
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: true,
|
canPop: !shouldConfirmExit,
|
||||||
onPopInvoked: (didPop) { gameController.disconnectOnlineGame(); },
|
onPopInvoked: (didPop) {
|
||||||
|
if (didPop) {
|
||||||
|
gameController.disconnectOnlineGame();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (shouldConfirmExit) {
|
||||||
|
_showExitConfirmationDialog(context, gameController, theme, themeType);
|
||||||
|
}
|
||||||
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: themeType == AppThemeType.doodle ? Colors.white : (bgImage != null ? Colors.transparent : theme.background),
|
backgroundColor: themeType == AppThemeType.doodle ? Colors.white : (bgImage != null ? Colors.transparent : theme.background),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,10 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
|
// --- AVVIA IL BATTITO CARDIACO ---
|
||||||
|
StorageService.instance.startHeartbeat();
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (StorageService.instance.playerName.isEmpty) {
|
if (StorageService.instance.playerName.isEmpty) {
|
||||||
HomeModals.showNameDialog(context, () {
|
HomeModals.showNameDialog(context, () {
|
||||||
|
|
@ -105,7 +109,6 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _checkStoreForUpdate() async {
|
Future<void> _checkStoreForUpdate() async {
|
||||||
|
|
||||||
if (kIsWeb) return;
|
if (kIsWeb) return;
|
||||||
try {
|
try {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
|
|
@ -123,7 +126,6 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Errore controllo aggiornamenti: $e");
|
debugPrint("Errore controllo aggiornamenti: $e");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _triggerUpdate() async {
|
void _triggerUpdate() async {
|
||||||
|
|
@ -153,6 +155,7 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
StorageService.instance.stopHeartbeat(); // <-- Assicurati di fermarlo
|
||||||
_cleanupGhostRoom();
|
_cleanupGhostRoom();
|
||||||
_linkSubscription?.cancel();
|
_linkSubscription?.cancel();
|
||||||
_favoritesSubscription?.cancel();
|
_favoritesSubscription?.cancel();
|
||||||
|
|
@ -163,9 +166,19 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
// --- L'UTENTE TORNA NELL'APP: RIPRENDI IL BATTITO E AGGIORNA SUBITO ---
|
||||||
|
StorageService.instance.syncLeaderboard();
|
||||||
|
StorageService.instance.startHeartbeat();
|
||||||
|
|
||||||
_checkClipboardForInvite();
|
_checkClipboardForInvite();
|
||||||
_listenToFavoritesOnline();
|
_listenToFavoritesOnline();
|
||||||
} else if (state == AppLifecycleState.detached) {
|
}
|
||||||
|
else if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive) {
|
||||||
|
// --- L'UTENTE ESCE DALL'APP O LA METTE IN BACKGROUND: FERMA IL BATTITO ---
|
||||||
|
StorageService.instance.stopHeartbeat();
|
||||||
|
}
|
||||||
|
else if (state == AppLifecycleState.detached) {
|
||||||
|
StorageService.instance.stopHeartbeat();
|
||||||
_cleanupGhostRoom();
|
_cleanupGhostRoom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -252,8 +265,9 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
void _showFavoriteOnlinePopup(String name) {
|
void _showFavoriteOnlinePopup(String name) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
|
// Se lo abbiamo già notificato nell'ultima ora, ignoriamo l'aggiornamento
|
||||||
if (_lastOnlineNotifications.containsKey(name)) {
|
if (_lastOnlineNotifications.containsKey(name)) {
|
||||||
if (DateTime.now().difference(_lastOnlineNotifications[name]!).inMinutes < 1) return;
|
if (DateTime.now().difference(_lastOnlineNotifications[name]!).inMinutes < 60) return;
|
||||||
}
|
}
|
||||||
_lastOnlineNotifications[name] = DateTime.now();
|
_lastOnlineNotifications[name] = DateTime.now();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -186,35 +186,93 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
|
||||||
borderRadius: BorderRadius.circular(10)
|
borderRadius: BorderRadius.circular(10)
|
||||||
),
|
),
|
||||||
child: favs.isEmpty
|
child: favs.isEmpty
|
||||||
? Center(child: Padding(
|
? Center(
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(20.0),
|
padding: const EdgeInsets.all(20.0),
|
||||||
child: Text("Non hai ancora aggiunto nessun preferito dalla Classifica!", textAlign: TextAlign.center, style: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6)))),
|
child: Text("Non hai ancora aggiunto nessun preferito dalla Classifica!", textAlign: TextAlign.center, style: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6)))),
|
||||||
))
|
)
|
||||||
: ListView.builder(
|
)
|
||||||
|
: StreamBuilder<QuerySnapshot>(
|
||||||
|
// Interroghiamo Firebase solo per gli UID dei nostri preferiti (max 10 per limiti di Firestore)
|
||||||
|
stream: FirebaseFirestore.instance.collection('leaderboard')
|
||||||
|
.where(FieldPath.documentId, whereIn: favs.map((f) => f['uid']).take(10).toList())
|
||||||
|
.snapshots(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return Center(child: CircularProgressIndicator(color: theme.playerBlue));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mappiamo i risultati di Firebase per un accesso rapido
|
||||||
|
Map<String, Map<String, dynamic>> liveData = {};
|
||||||
|
for (var doc in snapshot.data!.docs) {
|
||||||
|
liveData[doc.id] = doc.data() as Map<String, dynamic>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
itemCount: favs.length,
|
itemCount: favs.length,
|
||||||
itemBuilder: (c, i) {
|
itemBuilder: (c, i) {
|
||||||
|
String uid = favs[i]['uid']!;
|
||||||
|
String name = favs[i]['name']!;
|
||||||
|
|
||||||
|
bool isOnline = false;
|
||||||
|
if (liveData.containsKey(uid) && liveData[uid]!['lastActive'] != null) {
|
||||||
|
Timestamp lastActive = liveData[uid]!['lastActive'];
|
||||||
|
int diffInSeconds = DateTime.now().difference(lastActive.toDate()).inSeconds;
|
||||||
|
// Se ha fatto un'azione negli ultimi 3 minuti, lo consideriamo online
|
||||||
|
if (diffInSeconds.abs() < 180) isOnline = true;
|
||||||
|
}
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(favs[i]['name']!, style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontSize: 18, fontWeight: FontWeight.bold))),
|
leading: Icon(
|
||||||
trailing: ElevatedButton(
|
Icons.circle,
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
|
color: isOnline ? Colors.greenAccent : Colors.redAccent.withOpacity(0.5),
|
||||||
|
size: 14
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
name,
|
||||||
|
style: getLobbyTextStyle(themeType, TextStyle(
|
||||||
|
color: isOnline ? theme.text : theme.text.withOpacity(0.5),
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold
|
||||||
|
))
|
||||||
|
),
|
||||||
|
trailing: isOnline
|
||||||
|
? ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: theme.playerBlue,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))
|
||||||
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
_createRoomAndInvite(favs[i]['uid']!, favs[i]['name']!);
|
_createRoomAndInvite(uid, name);
|
||||||
},
|
},
|
||||||
child: Text("SFIDA", style: getLobbyTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))),
|
child: Text("SFIDA", style: getLobbyTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))),
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(10)
|
||||||
|
),
|
||||||
|
child: Text("OFFLINE", style: getLobbyTextStyle(themeType, const TextStyle(color: Colors.grey, fontSize: 12, fontWeight: FontWeight.bold))),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
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))),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showWaitingDialog(String code) {
|
void _showWaitingDialog(String code) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -434,9 +492,9 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
_buildTimeOption('PRO', '-2s A PARTITA', 'dynamic', theme, themeType),
|
||||||
_buildTimeOption('10s', 'FISSO', 'fixed', theme, themeType),
|
_buildTimeOption('10s', 'FISSO', 'fixed', theme, themeType),
|
||||||
_buildTimeOption('RELAX', 'INFINITO', 'relax', theme, themeType),
|
_buildTimeOption('RELAX', 'INFINITO', 'relax', theme, themeType),
|
||||||
_buildTimeOption('DINAMICO', '-2s', 'dynamic', theme, themeType),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|
@ -618,7 +676,7 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
|
||||||
String tMode = data['timeMode'] is String ? data['timeMode'] : (data['timeMode'] == true ? 'fixed' : 'relax');
|
String tMode = data['timeMode'] is String ? data['timeMode'] : (data['timeMode'] == true ? 'fixed' : 'relax');
|
||||||
String prettyTime = "10s";
|
String prettyTime = "10s";
|
||||||
if (tMode == 'relax') prettyTime = "Relax";
|
if (tMode == 'relax') prettyTime = "Relax";
|
||||||
else if (tMode == 'dynamic') prettyTime = "Dinamico";
|
else if (tMode == 'dynamic') prettyTime = "PRO";
|
||||||
|
|
||||||
String prettyShape = "Rombo";
|
String prettyShape = "Rombo";
|
||||||
if (shapeStr == 'cross') prettyShape = "Croce";
|
if (shapeStr == 'cross') prettyShape = "Croce";
|
||||||
|
|
|
||||||
8573
report/TetraQ_15-03-26_11-48.txt
Normal file
8573
report/TetraQ_15-03-26_11-48.txt
Normal file
File diff suppressed because it is too large
Load diff
8573
report/TetraQ_15-03-26_11.51.txt
Normal file
8573
report/TetraQ_15-03-26_11.51.txt
Normal file
File diff suppressed because it is too large
Load diff
8930
report/TetraQ_15-03-26_14.47.txt
Normal file
8930
report/TetraQ_15-03-26_14.47.txt
Normal file
File diff suppressed because it is too large
Load diff
9070
report/TetraQ_15-03-26_16.25.txt
Normal file
9070
report/TetraQ_15-03-26_16.25.txt
Normal file
File diff suppressed because it is too large
Load diff
9060
report/TetraQ_15-03-26_20.51.txt
Normal file
9060
report/TetraQ_15-03-26_20.51.txt
Normal file
File diff suppressed because it is too large
Load diff
9073
report/TetraQ_18-03-26_13.14.txt
Normal file
9073
report/TetraQ_18-03-26_13.14.txt
Normal file
File diff suppressed because it is too large
Load diff
9145
report/TetraQ_20-03-26_12.51.txt
Normal file
9145
report/TetraQ_20-03-26_12.51.txt
Normal file
File diff suppressed because it is too large
Load diff
9833
report/TetraQ_20-03-26_21.39.txt
Normal file
9833
report/TetraQ_20-03-26_21.39.txt
Normal file
File diff suppressed because it is too large
Load diff
9897
report/TetraQ_20-03-26_21.47.txt
Normal file
9897
report/TetraQ_20-03-26_21.47.txt
Normal file
File diff suppressed because it is too large
Load diff
9914
report/TetraQ_20-03-26_22.02.txt
Normal file
9914
report/TetraQ_20-03-26_22.02.txt
Normal file
File diff suppressed because it is too large
Load diff
9970
report/TetraQ_20-03-26_23.00.txt
Normal file
9970
report/TetraQ_20-03-26_23.00.txt
Normal file
File diff suppressed because it is too large
Load diff
10424
report/TetraQ_20-03-26_23.29.txt
Normal file
10424
report/TetraQ_20-03-26_23.29.txt
Normal file
File diff suppressed because it is too large
Load diff
10429
report/TetraQ_21-03-26_10.28.txt
Normal file
10429
report/TetraQ_21-03-26_10.28.txt
Normal file
File diff suppressed because it is too large
Load diff
10677
report/TetraQ_21-03-26_10.43.txt
Normal file
10677
report/TetraQ_21-03-26_10.43.txt
Normal file
File diff suppressed because it is too large
Load diff
10425
report/TetraQ_22-03-26_23.54.txt
Normal file
10425
report/TetraQ_22-03-26_23.54.txt
Normal file
File diff suppressed because it is too large
Load diff
10564
report/TetraQ_23-03-26_00.16.txt
Normal file
10564
report/TetraQ_23-03-26_00.16.txt
Normal file
File diff suppressed because it is too large
Load diff
11483
report/TetraQ_24-03-26_13.52.txt
Normal file
11483
report/TetraQ_24-03-26_13.52.txt
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue