Auto-sync: 20260323_010000
This commit is contained in:
parent
c3390609c3
commit
6372935e1c
8 changed files with 717 additions and 277 deletions
|
|
@ -1,3 +1,3 @@
|
|||
index.html,1773586765860,5737ce966fa8786becaf7f36a32992cf44102fb3a217c226c30576c993b33e63
|
||||
404.html,1773344753356,05cbc6f94d7a69ce2e29646eab13be2c884e61ba93e3094df5028866876d18b3
|
||||
report.html,1773588057140,876c6baaa912c9abfb81ee70e9868d84476b1c204ebca4c99f458f300661a36b
|
||||
report.html,1774223974711,2848745a7b4437e80aabba9bd776c1c7f90b1be21e67ddaf062c22a21ac99554
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ class _ClosureResult {
|
|||
final bool closesSomething;
|
||||
final int netValue;
|
||||
final bool causesSwap;
|
||||
_ClosureResult(this.closesSomething, this.netValue, this.causesSwap);
|
||||
final bool isIceTrap; // NUOVO: Identifica le mosse suicide sul ghiaccio
|
||||
|
||||
_ClosureResult(this.closesSomething, this.netValue, this.causesSwap, this.isIceTrap);
|
||||
}
|
||||
|
||||
class AIEngine {
|
||||
|
|
@ -19,6 +21,7 @@ class AIEngine {
|
|||
|
||||
if (availableLines.isEmpty) return board.lines.first;
|
||||
|
||||
// Più il livello è alto, più l'IA è "intelligente"
|
||||
double smartChance = 0.50 + ((level - 1) * 0.10);
|
||||
if (smartChance > 1.0) smartChance = 1.0;
|
||||
|
||||
|
|
@ -29,9 +32,16 @@ class AIEngine {
|
|||
|
||||
List<Line> goodClosingMoves = [];
|
||||
List<Line> badClosingMoves = [];
|
||||
List<Line> iceTraps = []; // Le mosse da evitare assolutamente
|
||||
|
||||
for (var line in availableLines) {
|
||||
var result = _checkClosure(board, line);
|
||||
|
||||
if (result.isIceTrap) {
|
||||
iceTraps.add(line); // Segna la linea come trappola e passa alla prossima
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.closesSomething) {
|
||||
if (result.causesSwap) {
|
||||
if (myScore < oppScore) {
|
||||
|
|
@ -56,10 +66,10 @@ class AIEngine {
|
|||
}
|
||||
}
|
||||
|
||||
// --- REGOLA 2: Mosse Sicure ---
|
||||
// --- REGOLA 2: Mosse Sicure (Ora include le esche del ghiaccio!) ---
|
||||
List<Line> safeMoves = [];
|
||||
for (var line in availableLines) {
|
||||
if (!badClosingMoves.contains(line) && !goodClosingMoves.contains(line) && _isSafeMove(board, line, myScore, oppScore)) {
|
||||
if (!badClosingMoves.contains(line) && !goodClosingMoves.contains(line) && !iceTraps.contains(line) && _isSafeMove(board, line, myScore, oppScore)) {
|
||||
safeMoves.add(line);
|
||||
}
|
||||
}
|
||||
|
|
@ -76,17 +86,19 @@ class AIEngine {
|
|||
|
||||
// --- REGOLA 3: Scegliere il male minore ---
|
||||
if (beSmart) {
|
||||
List<Line> riskyButNotTerrible = availableLines.where((l) => !badClosingMoves.contains(l) && !goodClosingMoves.contains(l)).toList();
|
||||
List<Line> riskyButNotTerrible = availableLines.where((l) => !badClosingMoves.contains(l) && !goodClosingMoves.contains(l) && !iceTraps.contains(l)).toList();
|
||||
if (riskyButNotTerrible.isNotEmpty) {
|
||||
return riskyButNotTerrible[random.nextInt(riskyButNotTerrible.length)];
|
||||
}
|
||||
}
|
||||
|
||||
List<Line> nonTerribleMoves = availableLines.where((l) => !badClosingMoves.contains(l)).toList();
|
||||
// Ultima spiaggia prima del disastro: qualsiasi cosa tranne bombe e trappole ghiacciate
|
||||
List<Line> nonTerribleMoves = availableLines.where((l) => !badClosingMoves.contains(l) && !iceTraps.contains(l)).toList();
|
||||
if (nonTerribleMoves.isNotEmpty) {
|
||||
return nonTerribleMoves[random.nextInt(nonTerribleMoves.length)];
|
||||
}
|
||||
|
||||
// Se l'IA è messa all'angolo ed è costretta a suicidarsi... pesca a caso
|
||||
return availableLines[random.nextInt(availableLines.length)];
|
||||
}
|
||||
|
||||
|
|
@ -94,6 +106,7 @@ class AIEngine {
|
|||
int netValue = 0;
|
||||
bool closesSomething = false;
|
||||
bool causesSwap = false;
|
||||
bool isIceTrap = false;
|
||||
|
||||
for (var box in board.boxes) {
|
||||
if (box.type == BoxType.invisible) continue;
|
||||
|
|
@ -106,17 +119,20 @@ class AIEngine {
|
|||
if (box.right.owner != Player.none || box.right == line) linesCount++;
|
||||
|
||||
if (linesCount == 4) {
|
||||
if (box.type == BoxType.ice && !line.isIceCracked) {
|
||||
// L'IA capisce che questa mossa non chiuderà il box, ma le farà perdere il turno.
|
||||
isIceTrap = true;
|
||||
} else {
|
||||
closesSomething = true;
|
||||
|
||||
// FIX: Togliamo la "vista a raggi X" all'Intelligenza Artificiale!
|
||||
if (box.hiddenJokerOwner == board.currentPlayer) {
|
||||
// L'IA conosce il suo Jolly, sa che vale +2 e cercherà di chiuderlo
|
||||
netValue += 2;
|
||||
} else {
|
||||
// Se c'è il Jolly del giocatore, l'IA NON DEVE SAPERLO e valuta la casella normalmente!
|
||||
if (box.type == BoxType.gold) netValue += 2;
|
||||
else if (box.type == BoxType.bomb) netValue -= 1;
|
||||
else if (box.type == BoxType.swap) netValue += 0;
|
||||
else if (box.type == BoxType.ice) netValue += 0; // Rompere il ghiaccio vale 0 punti, ma fa rigiocare
|
||||
else if (box.type == BoxType.multiplier) netValue += 1; // Leggero boost per dare priorità al x2
|
||||
else netValue += 1;
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +140,8 @@ class AIEngine {
|
|||
}
|
||||
}
|
||||
}
|
||||
return _ClosureResult(closesSomething, netValue, causesSwap);
|
||||
}
|
||||
return _ClosureResult(closesSomething, netValue, causesSwap, isIceTrap);
|
||||
}
|
||||
|
||||
static bool _isSafeMove(GameBoard board, Line line, int myScore, int oppScore) {
|
||||
|
|
@ -139,21 +156,24 @@ class AIEngine {
|
|||
if (box.right.owner != Player.none) currentLinesCount++;
|
||||
|
||||
if (currentLinesCount == 2) {
|
||||
|
||||
// Nuova logica di sicurezza: cosa succede se l'IA lascia questa scatola all'avversario?
|
||||
// L'IA valuta cosa succede se lascia questa casella con 3 linee all'avversario
|
||||
int valueForOpponent = 0;
|
||||
if (box.hiddenJokerOwner == board.currentPlayer) {
|
||||
// Se l'avversario la chiude, becca la trappola dell'IA (-1).
|
||||
// Quindi PER L'IA È SICURISSIMO LASCIARE QUESTA CASELLA APERTA!
|
||||
|
||||
if (box.type == BoxType.ice) {
|
||||
// Il ghiaccio è la trappola perfetta. Lasciarlo con 3 linee spingerà l'avversario a incrinarlo e a perdere il turno.
|
||||
// L'IA valuta questa mossa come SICURISSIMA!
|
||||
valueForOpponent = -5;
|
||||
} else if (box.hiddenJokerOwner == board.currentPlayer) {
|
||||
valueForOpponent = -1;
|
||||
} else {
|
||||
if (box.type == BoxType.gold) valueForOpponent = 2;
|
||||
else if (box.type == BoxType.bomb) valueForOpponent = -1;
|
||||
else if (box.type == BoxType.swap) valueForOpponent = 0;
|
||||
else if (box.type == BoxType.multiplier) valueForOpponent = 1;
|
||||
else valueForOpponent = 1;
|
||||
}
|
||||
|
||||
// Se per l'avversario vale -1 (bomba normale o trappola dell'IA), lasciamogliela!
|
||||
// Se per l'avversario è una trappola (bomba o ghiaccio), lascia pure la mossa libera
|
||||
if (valueForOpponent < 0) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -166,7 +186,7 @@ class AIEngine {
|
|||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return false; // La mossa regalerebbe punti, quindi NON è sicura
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -553,6 +553,7 @@ class GameController extends ChangeNotifier {
|
|||
|
||||
if (isOnline && roomCode != null) {
|
||||
Map<String, dynamic> moveData = {
|
||||
'id': DateTime.now().millisecondsSinceEpoch,
|
||||
'x1': line.p1.x, 'y1': line.p1.y, 'x2': line.p2.x, 'y2': line.p2.y,
|
||||
'player': myPlayer == Player.red ? 'red' : 'blue'
|
||||
};
|
||||
|
|
@ -609,7 +610,7 @@ class GameController extends ChangeNotifier {
|
|||
return unlocks;
|
||||
}
|
||||
|
||||
void _saveMatchResult() {
|
||||
Future<void> _saveMatchResult() async {
|
||||
if (_hasSavedResult) return;
|
||||
_hasSavedResult = true;
|
||||
|
||||
|
|
@ -626,9 +627,9 @@ class GameController extends ChangeNotifier {
|
|||
String oppName = isHost ? onlineGuestName : onlineHostName;
|
||||
int myScore = isHost ? board.scoreRed : board.scoreBlue;
|
||||
int oppScore = isHost ? board.scoreBlue : board.scoreRed;
|
||||
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true);
|
||||
await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true);
|
||||
|
||||
if (isWin) StorageService.instance.updateQuestProgress(0, 1);
|
||||
if (isWin) await StorageService.instance.updateQuestProgress(0, 1);
|
||||
|
||||
} else if (isVsCPU) {
|
||||
int myScore = board.scoreRed; int cpuScore = board.scoreBlue;
|
||||
|
|
@ -636,23 +637,23 @@ class GameController extends ChangeNotifier {
|
|||
calculatedXP = isWin ? (10 + (cpuLevel * 2)) : (isDraw ? 5 : 2);
|
||||
|
||||
if (isWin) {
|
||||
StorageService.instance.addWin();
|
||||
StorageService.instance.updateQuestProgress(1, 1);
|
||||
await StorageService.instance.addWin();
|
||||
await StorageService.instance.updateQuestProgress(1, 1);
|
||||
} else if (cpuScore > myScore) {
|
||||
StorageService.instance.addLoss();
|
||||
await StorageService.instance.addLoss();
|
||||
}
|
||||
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "CPU (Liv. $cpuLevel)", myScore: myScore, oppScore: cpuScore, isOnline: false);
|
||||
await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "CPU (Liv. $cpuLevel)", myScore: myScore, oppScore: cpuScore, isOnline: false);
|
||||
} else {
|
||||
calculatedXP = 2;
|
||||
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "Ospite (Locale)", myScore: board.scoreRed, oppScore: board.scoreBlue, isOnline: false);
|
||||
await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "Ospite (Locale)", myScore: board.scoreRed, oppScore: board.scoreBlue, isOnline: false);
|
||||
}
|
||||
|
||||
if (board.shape != ArenaShape.classic) {
|
||||
StorageService.instance.updateQuestProgress(2, 1);
|
||||
await StorageService.instance.updateQuestProgress(2, 1);
|
||||
}
|
||||
|
||||
lastMatchXP = calculatedXP;
|
||||
StorageService.instance.addXP(calculatedXP);
|
||||
await StorageService.instance.addXP(calculatedXP);
|
||||
|
||||
int newLevel = StorageService.instance.playerLevel;
|
||||
if (newLevel > oldLevel) {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class AudioService extends ChangeNotifier {
|
|||
AudioService._internal();
|
||||
|
||||
bool isMuted = false;
|
||||
final AudioPlayer _sfxPlayer = AudioPlayer();
|
||||
// Abbiamo rimosso _sfxPlayer perché ora ogni suono crea un player usa e getta
|
||||
final AudioPlayer _bgmPlayer = AudioPlayer();
|
||||
|
||||
AppThemeType _currentTheme = AppThemeType.doodle;
|
||||
|
|
@ -30,7 +30,6 @@ class AudioService extends ChangeNotifier {
|
|||
|
||||
if (isMuted) {
|
||||
await _bgmPlayer.pause();
|
||||
await _sfxPlayer.stop();
|
||||
} else {
|
||||
playBgm(_currentTheme);
|
||||
}
|
||||
|
|
@ -92,9 +91,11 @@ class AudioService extends ChangeNotifier {
|
|||
|
||||
if (file.isNotEmpty) {
|
||||
try {
|
||||
await _sfxPlayer.play(AssetSource('audio/sfx/$file'), volume: 1.0);
|
||||
final player = AudioPlayer(); // Player dedicato
|
||||
await player.play(AssetSource('audio/sfx/$file'), volume: 1.0);
|
||||
player.onPlayerComplete.listen((_) => player.dispose());
|
||||
} catch (e) {
|
||||
debugPrint("Errore SFX Linea: $file");
|
||||
debugPrint("Errore SFX Linea: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -115,9 +116,11 @@ class AudioService extends ChangeNotifier {
|
|||
|
||||
if (file.isNotEmpty) {
|
||||
try {
|
||||
await _sfxPlayer.play(AssetSource('audio/sfx/$file'), volume: 1.0);
|
||||
final player = AudioPlayer(); // Player dedicato
|
||||
await player.play(AssetSource('audio/sfx/$file'), volume: 1.0);
|
||||
player.onPlayerComplete.listen((_) => player.dispose());
|
||||
} catch (e) {
|
||||
debugPrint("Errore SFX Box: $file");
|
||||
debugPrint("Errore SFX Box: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -125,14 +128,18 @@ class AudioService extends ChangeNotifier {
|
|||
void playBonusSfx() async {
|
||||
if (isMuted) return;
|
||||
try {
|
||||
await _sfxPlayer.play(AssetSource('audio/sfx/bonus.wav'), volume: 1.0);
|
||||
final player = AudioPlayer(); // Player dedicato
|
||||
await player.play(AssetSource('audio/sfx/bonus.wav'), volume: 1.0);
|
||||
player.onPlayerComplete.listen((_) => player.dispose());
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
void playBombSfx() async {
|
||||
if (isMuted) return;
|
||||
try {
|
||||
await _sfxPlayer.play(AssetSource('audio/sfx/bomb.wav'), volume: 1.0);
|
||||
final player = AudioPlayer(); // Player dedicato
|
||||
await player.play(AssetSource('audio/sfx/bomb.wav'), volume: 1.0);
|
||||
player.onPlayerComplete.listen((_) => player.dispose());
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -66,10 +66,7 @@ class StorageService {
|
|||
|
||||
// --- SICUREZZA XP: Inviamo solo INCREMENTI al server ---
|
||||
Future<void> addXP(int xp) async {
|
||||
// Aggiorniamo il locale per la UI
|
||||
await _prefs.setInt('totalXP', totalXP + xp);
|
||||
|
||||
// Aggiorniamo il server in modo sicuro tramite incremento relativo
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
if (user != null) {
|
||||
await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({
|
||||
|
|
@ -83,7 +80,6 @@ class StorageService {
|
|||
|
||||
int get wins => _prefs.getInt('wins') ?? 0;
|
||||
|
||||
// --- SICUREZZA WINS: Inviamo solo INCREMENTI al server ---
|
||||
Future<void> addWin() async {
|
||||
await _prefs.setInt('wins', wins + 1);
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
|
|
@ -96,7 +92,6 @@ class StorageService {
|
|||
|
||||
int get losses => _prefs.getInt('losses') ?? 0;
|
||||
|
||||
// --- SICUREZZA LOSSES: Inviamo solo INCREMENTI al server ---
|
||||
Future<void> addLoss() async {
|
||||
await _prefs.setInt('losses', losses + 1);
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
|
|
@ -116,10 +111,12 @@ class StorageService {
|
|||
syncLeaderboard();
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// FIX: ORA IL SYNC MANDA I DATI REALI ALLA DASHBOARD ADMIN!
|
||||
// ======================================================================
|
||||
Future<void> syncLeaderboard() async {
|
||||
try {
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
|
||||
if (user == null) return;
|
||||
|
||||
String name = playerName;
|
||||
|
|
@ -127,12 +124,53 @@ class StorageService {
|
|||
|
||||
String targetUid = user.uid;
|
||||
|
||||
// --- SICUREZZA: Non inviamo PIÙ i valori assoluti di xp, wins e losses! ---
|
||||
// Vengono aggiornati solo dagli incrementi protetti nelle funzioni sopra.
|
||||
// 1. Recupero Versione App e Modello Dispositivo
|
||||
String appVer = "N/D";
|
||||
String devModel = "N/D";
|
||||
String osName = kIsWeb ? "Web" : Platform.operatingSystem;
|
||||
|
||||
try {
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
appVer = "${packageInfo.version}+${packageInfo.buildNumber}";
|
||||
|
||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
if (!kIsWeb) {
|
||||
if (Platform.isAndroid) {
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
devModel = "${androidInfo.brand} ${androidInfo.model}".toUpperCase();
|
||||
osName = "Android";
|
||||
} else if (Platform.isIOS) {
|
||||
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
|
||||
devModel = iosInfo.utsname.machine; // Es. "iPhone13,2"
|
||||
osName = "iOS";
|
||||
} else if (Platform.isMacOS) {
|
||||
MacOsDeviceInfo macInfo = await deviceInfo.macOsInfo;
|
||||
devModel = macInfo.model; // Es. "MacBookPro17,1"
|
||||
osName = "macOS";
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Errore device info: $e");
|
||||
}
|
||||
|
||||
// 2. 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
|
||||
|
||||
// 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) {
|
||||
|
|
@ -184,15 +222,12 @@ class StorageService {
|
|||
|
||||
if (today != lastDate) {
|
||||
_prefs.setString('quest_date', today);
|
||||
|
||||
_prefs.setInt('q1_type', 0);
|
||||
_prefs.setInt('q1_prog', 0);
|
||||
_prefs.setInt('q1_target', 3);
|
||||
|
||||
_prefs.setInt('q2_type', 1);
|
||||
_prefs.setInt('q2_prog', 0);
|
||||
_prefs.setInt('q2_target', 2);
|
||||
|
||||
_prefs.setInt('q3_type', 2);
|
||||
_prefs.setInt('q3_prog', 0);
|
||||
_prefs.setInt('q3_target', 2);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import 'dart:ui';
|
||||
import 'dart:math';
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
|
@ -13,6 +14,9 @@ import 'package:cloud_firestore/cloud_firestore.dart';
|
|||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'dart:async';
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:upgrader/upgrader.dart';
|
||||
import 'package:in_app_update/in_app_update.dart';
|
||||
|
||||
import '../../logic/game_controller.dart';
|
||||
import '../../core/theme_manager.dart';
|
||||
|
|
@ -58,6 +62,9 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
|||
String? _myRoomCode;
|
||||
bool _roomStarted = false;
|
||||
|
||||
String _appVersion = '';
|
||||
bool _updateAvailable = false;
|
||||
|
||||
final MultiplayerService _multiplayerService = MultiplayerService();
|
||||
|
||||
@override
|
||||
|
|
@ -80,6 +87,59 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
|||
_checkClipboardForInvite();
|
||||
_initDeepLinks();
|
||||
_listenToFavoritesOnline();
|
||||
_loadAppVersion();
|
||||
_checkStoreForUpdate();
|
||||
}
|
||||
|
||||
Future<void> _loadAppVersion() async {
|
||||
try {
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_appVersion = "v. ${packageInfo.version}";
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Errore lettura versione: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _checkStoreForUpdate() async {
|
||||
|
||||
if (kIsWeb) return;
|
||||
try {
|
||||
if (Platform.isAndroid) {
|
||||
final info = await InAppUpdate.checkForUpdate();
|
||||
if (info.updateAvailability == UpdateAvailability.updateAvailable) {
|
||||
if (mounted) setState(() => _updateAvailable = true);
|
||||
}
|
||||
} else if (Platform.isIOS || Platform.isMacOS) {
|
||||
final upgrader = Upgrader();
|
||||
await upgrader.initialize();
|
||||
if (upgrader.isUpdateAvailable()) {
|
||||
if (mounted) setState(() => _updateAvailable = true);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Errore controllo aggiornamenti: $e");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void _triggerUpdate() async {
|
||||
if (kIsWeb) return;
|
||||
if (Platform.isAndroid) {
|
||||
try {
|
||||
final info = await InAppUpdate.checkForUpdate();
|
||||
if (info.updateAvailability == UpdateAvailability.updateAvailable) {
|
||||
await InAppUpdate.performImmediateUpdate();
|
||||
}
|
||||
} catch(e) {
|
||||
Upgrader().sendUserToAppStore();
|
||||
}
|
||||
} else {
|
||||
Upgrader().sendUserToAppStore();
|
||||
}
|
||||
}
|
||||
|
||||
void _checkThemeSafety() {
|
||||
|
|
@ -179,11 +239,13 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
|||
|
||||
if (diffInSeconds.abs() < 180) {
|
||||
String name = data['name'] ?? 'Un amico';
|
||||
if (ModalRoute.of(context)?.isCurrent == true) {
|
||||
_showFavoriteOnlinePopup(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -245,10 +307,13 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
|||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (ModalRoute.of(context)?.isCurrent == true) {
|
||||
_showInvitePopup(from, code, inviteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -742,12 +807,120 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
|||
),
|
||||
|
||||
Positioned.fill(child: uiContent),
|
||||
|
||||
// --- NUMERO DI VERSIONE APP E BADGE AGGIORNAMENTO ---
|
||||
if (_appVersion.isNotEmpty)
|
||||
Positioned(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 10,
|
||||
left: 20,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: 0.6,
|
||||
child: Text(
|
||||
_appVersion,
|
||||
style: getSharedTextStyle(themeType, TextStyle(
|
||||
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.0,
|
||||
)),
|
||||
),
|
||||
),
|
||||
if (_updateAvailable) ...[
|
||||
const SizedBox(width: 15),
|
||||
_PulsingUpdateBadge(
|
||||
themeType: themeType,
|
||||
theme: theme,
|
||||
onTap: _triggerUpdate,
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- NUOVO WIDGET: BADGE AGGIORNAMENTO PULSANTE ---
|
||||
class _PulsingUpdateBadge extends StatefulWidget {
|
||||
final AppThemeType themeType;
|
||||
final ThemeColors theme;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _PulsingUpdateBadge({
|
||||
required this.themeType,
|
||||
required this.theme,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_PulsingUpdateBadge> createState() => _PulsingUpdateBadgeState();
|
||||
}
|
||||
|
||||
class _PulsingUpdateBadgeState extends State<_PulsingUpdateBadge> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _scaleAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 800))..repeat(reverse: true);
|
||||
_scaleAnimation = Tween<double>(begin: 0.95, end: 1.05).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color badgeColor = widget.themeType == AppThemeType.doodle ? Colors.red.shade700 : widget.theme.playerRed;
|
||||
Color textColor = widget.themeType == AppThemeType.doodle ? Colors.white : widget.theme.playerRed;
|
||||
Color bgColor = widget.themeType == AppThemeType.doodle ? badgeColor : badgeColor.withOpacity(0.15);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
child: ScaleTransition(
|
||||
scale: _scaleAnimation,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: badgeColor, width: 1.5),
|
||||
boxShadow: widget.themeType == AppThemeType.doodle
|
||||
? [const BoxShadow(color: Colors.black26, offset: Offset(2, 2))]
|
||||
: [BoxShadow(color: badgeColor.withOpacity(0.4), blurRadius: 8)],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.system_update_alt, color: textColor, size: 14),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
"AGGIORNAMENTO DISPONIBILE",
|
||||
style: getSharedTextStyle(widget.themeType, TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w900,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// ----------------------------------------------------
|
||||
|
||||
class FullScreenGridPainter extends CustomPainter {
|
||||
final Color gridColor;
|
||||
FullScreenGridPainter(this.gridColor);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import '../logic/game_controller.dart';
|
|||
import '../core/theme_manager.dart';
|
||||
import '../core/app_colors.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import 'painters.dart';
|
||||
|
||||
class GameOverDialog extends StatelessWidget {
|
||||
const GameOverDialog({super.key});
|
||||
|
|
@ -18,6 +19,7 @@ class GameOverDialog extends StatelessWidget {
|
|||
final themeManager = context.read<ThemeManager>();
|
||||
final theme = themeManager.currentColors;
|
||||
final themeType = themeManager.currentThemeType;
|
||||
Color inkColor = const Color(0xFF111122);
|
||||
|
||||
int red = game.board.scoreRed;
|
||||
int blue = game.board.scoreBlue;
|
||||
|
|
@ -51,172 +53,269 @@ class GameOverDialog extends StatelessWidget {
|
|||
winnerColor = theme.playerBlue;
|
||||
} else {
|
||||
winnerText = "PAREGGIO!";
|
||||
winnerColor = theme.text;
|
||||
winnerColor = themeType == AppThemeType.doodle ? inkColor : theme.text;
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
backgroundColor: theme.background,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
side: BorderSide(color: winnerColor.withOpacity(0.5), width: 2),
|
||||
),
|
||||
title: Text("FINE PARTITA", textAlign: TextAlign.center, style: TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22)),
|
||||
content: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
Widget dialogContent = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(winnerText, textAlign: TextAlign.center, style: TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor)),
|
||||
Text(winnerText, textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor))),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.text.withOpacity(0.05),
|
||||
color: themeType == AppThemeType.doodle ? Colors.transparent : theme.text.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: themeType == AppThemeType.doodle ? Border.all(color: inkColor.withOpacity(0.3), width: 1.5) : null,
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("$nameRed: $red", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed)),
|
||||
Text(" - ", style: TextStyle(fontSize: 18, color: theme.text)),
|
||||
Text("$nameBlue: $blue", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue)),
|
||||
Text("$nameRed: $red", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed))),
|
||||
Text(" - ", style: getSharedTextStyle(themeType, TextStyle(fontSize: 18, color: themeType == AppThemeType.doodle ? inkColor : theme.text))),
|
||||
Text("$nameBlue: $blue", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue))),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (game.lastMatchXP > 0) ...[
|
||||
const SizedBox(height: 15),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: themeType == AppThemeType.doodle ? Colors.green.shade700 : Colors.greenAccent, width: 1.5),
|
||||
boxShadow: (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) ? [const BoxShadow(color: Colors.greenAccent, blurRadius: 10, spreadRadius: -5)] : [],
|
||||
),
|
||||
child: Text("+ ${game.lastMatchXP} XP", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.green.shade700 : Colors.greenAccent, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 1.5))),
|
||||
),
|
||||
],
|
||||
|
||||
if (game.isVsCPU) ...[
|
||||
const SizedBox(height: 15),
|
||||
Text("Difficoltà CPU: Livello ${game.cpuLevel}", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7))),
|
||||
Text("Difficoltà CPU: Livello ${game.cpuLevel}", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.7) : theme.text.withOpacity(0.7)))),
|
||||
],
|
||||
|
||||
if (game.isOnline) ...[
|
||||
const SizedBox(height: 20),
|
||||
if (game.rematchRequested && !game.opponentWantsRematch)
|
||||
Text("In attesa di $nameBlue...", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.orange.shade700 : Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic))),
|
||||
if (game.opponentWantsRematch && !game.rematchRequested)
|
||||
Text("$nameBlue vuole la rivincita!", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.green.shade700 : Colors.greenAccent, fontWeight: FontWeight.bold))),
|
||||
if (game.rematchRequested && game.opponentWantsRematch)
|
||||
Text("Avvio nuova partita...", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.green.shade800 : Colors.green, fontWeight: FontWeight.bold))),
|
||||
],
|
||||
|
||||
// --- SEZIONE LEVEL UP E ROADMAP DINAMICA ---
|
||||
if (game.hasLeveledUp && game.unlockedRewards.isNotEmpty) ...[
|
||||
const SizedBox(height: 30),
|
||||
const Divider(),
|
||||
Divider(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.3) : theme.text.withOpacity(0.2)),
|
||||
const SizedBox(height: 15),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.withOpacity(0.2),
|
||||
color: themeType == AppThemeType.doodle ? Colors.amber.withOpacity(0.1) : Colors.amber.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
border: Border.all(color: Colors.amber, width: 2)
|
||||
border: Border.all(color: themeType == AppThemeType.doodle ? Colors.amber.shade700 : Colors.amber, width: 2)
|
||||
),
|
||||
child: Text("🎉 LIVELLO ${game.newlyReachedLevel}! 🎉", style: const TextStyle(color: Colors.amber, fontWeight: FontWeight.w900, fontSize: 18)),
|
||||
child: Text("🎉 LIVELLO ${game.newlyReachedLevel}! 🎉", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.amber.shade700 : Colors.amber, fontWeight: FontWeight.w900, fontSize: 18))),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
...game.unlockedRewards.map((reward) => Container(
|
||||
...game.unlockedRewards.map((reward) {
|
||||
Color rewardColor = themeType == AppThemeType.doodle ? (reward['color'] as Color).withOpacity(0.8) : reward['color'];
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: (reward['color'] as Color).withOpacity(0.1),
|
||||
color: rewardColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: (reward['color'] as Color).withOpacity(0.5), width: 1.5),
|
||||
border: Border.all(color: rewardColor.withOpacity(0.5), width: 1.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: (reward['color'] as Color).withOpacity(0.2),
|
||||
color: rewardColor.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(reward['icon'], color: reward['color'], size: 28),
|
||||
child: Icon(reward['icon'], color: rewardColor, size: 28),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(reward['title'], style: TextStyle(color: reward['color'], fontWeight: FontWeight.w900, fontSize: 16)),
|
||||
Text(reward['title'], style: getSharedTextStyle(themeType, TextStyle(color: rewardColor, fontWeight: FontWeight.w900, fontSize: 16))),
|
||||
const SizedBox(height: 4),
|
||||
Text(reward['desc'], style: TextStyle(color: theme.text.withOpacity(0.9), fontSize: 12, height: 1.3)),
|
||||
Text(reward['desc'], style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.9) : theme.text.withOpacity(0.9), fontSize: 12, height: 1.3))),
|
||||
],
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
)),
|
||||
]
|
||||
// ---------------------------------------------
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
actionsPadding: const EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 10),
|
||||
actionsAlignment: MainAxisAlignment.center,
|
||||
actions: [
|
||||
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// --- BOTTONI AZIONE ---
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (playerBeatCPU)
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: winnerColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
elevation: 5,
|
||||
),
|
||||
onPressed: () {
|
||||
_buildPrimaryButton(
|
||||
"PROSSIMO LIVELLO ➔",
|
||||
winnerColor,
|
||||
themeType,
|
||||
inkColor,
|
||||
() {
|
||||
Navigator.pop(context);
|
||||
game.increaseLevelAndRestart();
|
||||
},
|
||||
child: const Text("PROSSIMO LIVELLO ➔", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
)
|
||||
else if (game.isOnline)
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
elevation: 5,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
if (game.board.isGameOver) {
|
||||
game.requestRematch();
|
||||
}
|
||||
},
|
||||
child: const Text("RIGIOCA ONLINE", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.5)),
|
||||
_buildPrimaryButton(
|
||||
game.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA",
|
||||
game.rematchRequested ? Colors.grey : (winnerColor == (themeType == AppThemeType.doodle ? inkColor : theme.text) ? theme.playerBlue : winnerColor),
|
||||
themeType,
|
||||
inkColor,
|
||||
game.rematchRequested ? () {} : () => game.requestRematch(),
|
||||
)
|
||||
else
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
elevation: 5,
|
||||
),
|
||||
onPressed: () {
|
||||
_buildPrimaryButton(
|
||||
"RIGIOCA",
|
||||
winnerColor == (themeType == AppThemeType.doodle ? inkColor : theme.text) ? theme.playerBlue : winnerColor,
|
||||
themeType,
|
||||
inkColor,
|
||||
() {
|
||||
Navigator.pop(context);
|
||||
game.startNewGame(game.board.radius, vsCPU: game.isVsCPU);
|
||||
},
|
||||
child: const Text("RIGIOCA", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2)),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: theme.text,
|
||||
side: BorderSide(color: theme.text.withOpacity(0.3), width: 2),
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
),
|
||||
onPressed: () {
|
||||
_buildSecondaryButton(
|
||||
"TORNA AL MENU",
|
||||
themeType,
|
||||
inkColor,
|
||||
theme,
|
||||
() {
|
||||
if (game.isOnline) {
|
||||
game.disconnectOnlineGame();
|
||||
}
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text("TORNA AL MENU", style: TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5)),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
if (themeType == AppThemeType.doodle) {
|
||||
dialogContent = Transform.rotate(
|
||||
angle: 0.015,
|
||||
child: CustomPaint(
|
||||
painter: DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.95), strokeColor: inkColor, seed: 500),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(25.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("FINE PARTITA", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 2))),
|
||||
const SizedBox(height: 20),
|
||||
dialogContent,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
dialogContent = Container(
|
||||
padding: const EdgeInsets.all(25.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.background,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: winnerColor.withOpacity(0.5), width: 2),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("FINE PARTITA", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: theme.text))),
|
||||
const SizedBox(height: 20),
|
||||
dialogContent,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
|
||||
child: dialogContent,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPrimaryButton(String label, Color color, AppThemeType themeType, Color inkColor, VoidCallback onTap) {
|
||||
if (themeType == AppThemeType.doodle) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: CustomPaint(
|
||||
painter: DoodleBackgroundPainter(fillColor: color, strokeColor: inkColor, seed: label.length * 7),
|
||||
child: Container(
|
||||
height: 55,
|
||||
alignment: Alignment.center,
|
||||
child: Text(label, style: getSharedTextStyle(themeType, const TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 1.5))),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: color,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
elevation: 5,
|
||||
),
|
||||
onPressed: onTap,
|
||||
child: Text(label, style: getSharedTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.5))),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSecondaryButton(String label, AppThemeType themeType, Color inkColor, ThemeColors theme, VoidCallback onTap) {
|
||||
if (themeType == AppThemeType.doodle) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: CustomPaint(
|
||||
painter: DoodleBackgroundPainter(fillColor: Colors.transparent, strokeColor: inkColor.withOpacity(0.5), seed: label.length * 3),
|
||||
child: Container(
|
||||
height: 55,
|
||||
alignment: Alignment.center,
|
||||
child: Text(label, style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 1.5))),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: theme.text,
|
||||
side: BorderSide(color: theme.text.withOpacity(0.3), width: 2),
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
),
|
||||
onPressed: onTap,
|
||||
child: Text(label, style: getSharedTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,65 +3,140 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Report Statistiche TetraQ</title>
|
||||
<title>Report Giocatori - TetraQ</title>
|
||||
|
||||
<script defer src="/__/firebase/10.8.0/firebase-app-compat.js"></script>
|
||||
<script defer src="/__/firebase/10.8.0/firebase-firestore-compat.js"></script>
|
||||
<script defer src="/__/firebase/init.js"></script>
|
||||
|
||||
<style>
|
||||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f7f6; color: #333; padding: 20px; margin: 0; box-sizing: border-box;}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
color: #2c3e50;
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* STILI LOGIN */
|
||||
#login-view { background-color: #34495e; position: fixed; top: 0; left: 0; width: 100%; height: 100vh; display: flex; justify-content: center; align-items: center; z-index: 1000; padding: 20px; box-sizing: border-box; }
|
||||
.login-box { background: white; padding: 40px; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); text-align: center; width: 100%; max-width: 350px; box-sizing: border-box; }
|
||||
.login-box h2 { color: #2c3e50; margin-top: 0; }
|
||||
.login-box input[type="text"], .login-box input[type="password"] { width: 100%; padding: 12px; margin: 10px 0 20px 0; border: 1px solid #bdc3c7; border-radius: 5px; box-sizing: border-box; font-size: 16px; }
|
||||
.login-box button { width: 100%; background-color: #3498db; color: white; border: none; padding: 12px; font-size: 16px; border-radius: 5px; cursor: pointer; font-weight: bold; transition: 0.2s; }
|
||||
.login-box button:hover { background-color: #2980b9; }
|
||||
/* --- STILI LOGIN --- */
|
||||
#login-view {
|
||||
background-color: #34495e;
|
||||
position: fixed;
|
||||
top: 0; left: 0; width: 100%; height: 100vh;
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
z-index: 1000; padding: 20px; box-sizing: border-box;
|
||||
}
|
||||
.login-box {
|
||||
background: white; padding: 40px; border-radius: 12px;
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.1); text-align: center;
|
||||
width: 100%; max-width: 350px; box-sizing: border-box;
|
||||
}
|
||||
.login-box h2 { color: #2c3e50; margin-top: 0; font-weight: 800; letter-spacing: 1px;}
|
||||
.login-box input {
|
||||
width: 100%; padding: 12px; margin: 10px 0 20px 0;
|
||||
border: 1px solid #e1e5eb; border-radius: 8px; box-sizing: border-box;
|
||||
font-size: 16px; outline: none; transition: border-color 0.2s;
|
||||
}
|
||||
.login-box input:focus { border-color: #3498db; }
|
||||
.login-box button {
|
||||
width: 100%; background-color: #e74c3c; color: white;
|
||||
border: none; padding: 12px; font-size: 16px; border-radius: 8px;
|
||||
cursor: pointer; font-weight: bold; transition: background-color 0.2s;
|
||||
}
|
||||
.login-box button:hover { background-color: #c0392b; }
|
||||
.error { color: #e74c3c; font-weight: bold; font-size: 14px; margin-top: -5px; margin-bottom: 15px; }
|
||||
|
||||
/* STILI DASHBOARD */
|
||||
/* --- STILI DASHBOARD --- */
|
||||
#dashboard-view { display: none; }
|
||||
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); box-sizing: border-box;}
|
||||
.container {
|
||||
max-width: 1200px; margin: 0 auto; background: white;
|
||||
padding: 30px; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.05);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.header-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
h1 { color: #2c3e50; margin: 0; font-size: 24px;}
|
||||
.btn-logout { background-color: #e74c3c; color: white; padding: 8px 15px; border-radius: 5px; text-decoration: none; font-weight: bold; font-size: 14px; white-space: nowrap; border: none; cursor: pointer;}
|
||||
.btn-logout:hover { background-color: #c0392b; }
|
||||
h1 { color: #2c3e50; margin: 0; font-size: 26px; font-weight: 800;}
|
||||
.btn-logout {
|
||||
background-color: transparent; color: #95a5a6; padding: 8px 15px;
|
||||
border-radius: 5px; font-weight: bold; font-size: 14px;
|
||||
border: 1px solid #ecf0f1; cursor: pointer; transition: 0.2s;
|
||||
}
|
||||
.btn-logout:hover { background-color: #f9f9f9; color: #e74c3c; border-color: #e74c3c; }
|
||||
|
||||
.filter-section { background: #eef2f5; padding: 20px; border-radius: 8px; margin-bottom: 30px; display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end; }
|
||||
/* --- FILTRI --- */
|
||||
.filter-section {
|
||||
background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px;
|
||||
display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end;
|
||||
border: 1px solid #e1e5eb;
|
||||
}
|
||||
.form-group { display: flex; flex-direction: column; flex: 1; min-width: 150px; }
|
||||
.form-group label { font-size: 13px; font-weight: bold; color: #34495e; margin-bottom: 5px; }
|
||||
.form-group input, .form-group select { padding: 10px; border: 1px solid #bdc3c7; border-radius: 5px; font-size: 14px; outline: none; box-sizing: border-box; width: 100%;}
|
||||
.form-group label { font-size: 12px; font-weight: bold; color: #7f8c8d; margin-bottom: 5px; text-transform: uppercase; letter-spacing: 0.5px;}
|
||||
.form-group input, .form-group select {
|
||||
padding: 10px; border: 1px solid #bdc3c7; border-radius: 6px;
|
||||
font-size: 14px; outline: none; box-sizing: border-box; width: 100%;
|
||||
}
|
||||
.form-group input:focus, .form-group select:focus { border-color: #3498db; }
|
||||
.btn-filtra { background-color: #3498db; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; font-size: 14px; transition: 0.2s; height: 40px; }
|
||||
.btn-filtra {
|
||||
background-color: #3498db; color: white; padding: 10px 20px; border: none;
|
||||
border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 14px;
|
||||
transition: 0.2s; height: 40px;
|
||||
}
|
||||
.btn-filtra:hover { background-color: #2980b9; }
|
||||
.btn-reset { background-color: #95a5a6; color: white; padding: 10px 15px; border: none; border-radius: 5px; cursor: pointer; text-decoration: none; font-size: 14px; height: 40px; line-height: 20px; box-sizing: border-box; display: inline-block; text-align: center;}
|
||||
.btn-reset {
|
||||
background-color: #95a5a6; color: white; padding: 0 15px; border: none;
|
||||
border-radius: 6px; cursor: pointer; font-size: 14px; height: 40px;
|
||||
line-height: 40px; text-align: center;
|
||||
}
|
||||
.btn-reset:hover { background-color: #7f8c8d; }
|
||||
|
||||
/* --- CARDS RIASSUNTIVE --- */
|
||||
.dashboard { display: flex; justify-content: space-between; margin-bottom: 30px; gap: 20px; text-align: center; flex-wrap: wrap; }
|
||||
.card { flex: 1; min-width: 150px; background: #ecf0f1; padding: 20px; border-radius: 8px; border-left: 5px solid #3498db; box-sizing: border-box;}
|
||||
.card {
|
||||
flex: 1; min-width: 150px; background: white; padding: 20px;
|
||||
border-radius: 8px; border-left: 5px solid #3498db; box-sizing: border-box;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05); border: 1px solid #e1e5eb; border-left-width: 5px;
|
||||
}
|
||||
.card.ios { border-left-color: #e74c3c; }
|
||||
.card.android { border-left-color: #2ecc71; }
|
||||
.card h3 { margin: 0 0 10px 0; font-size: 14px; color: #7f8c8d; text-transform: uppercase; }
|
||||
.card p { margin: 0; font-size: 28px; font-weight: bold; color: #2c3e50; }
|
||||
.card h3 { margin: 0 0 10px 0; font-size: 12px; color: #7f8c8d; text-transform: uppercase; letter-spacing: 0.5px;}
|
||||
.card p { margin: 0; font-size: 28px; font-weight: 800; color: #2c3e50; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 14px; table-layout: fixed; }
|
||||
th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #ddd; word-wrap: break-word;}
|
||||
th { background-color: #34495e; color: white; }
|
||||
tr:hover { background-color: #f1f1f1; }
|
||||
/* --- TABELLA --- */
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 14px; table-layout: fixed; }
|
||||
th, td { padding: 16px 20px; text-align: left; vertical-align: top;}
|
||||
th {
|
||||
background-color: #34495e; color: white; font-weight: 700;
|
||||
font-size: 13px; letter-spacing: 0.5px;
|
||||
}
|
||||
th:first-child { border-top-left-radius: 6px; border-bottom-left-radius: 6px; }
|
||||
th:last-child { border-top-right-radius: 6px; border-bottom-right-radius: 6px; }
|
||||
td { border-bottom: 1px solid #ecf0f1; color: #2c3e50; }
|
||||
tr:hover td { background-color: #fbfcfc; }
|
||||
|
||||
/* Larghezze specifiche per evitare colonne deformate */
|
||||
th:nth-child(1) { width: 15%; }
|
||||
th:nth-child(2) { width: 25%; }
|
||||
th:nth-child(3) { width: 15%; }
|
||||
th:nth-child(4) { width: 20%; }
|
||||
th:nth-child(5) { width: 25%; }
|
||||
th:nth-child(1) { width: 15%; } /* Accesso */
|
||||
th:nth-child(2) { width: 25%; } /* Giocatore */
|
||||
th:nth-child(3) { width: 15%; } /* Statistiche (Nuova) */
|
||||
th:nth-child(4) { width: 25%; } /* Connessione */
|
||||
th:nth-child(5) { width: 20%; } /* Dispositivo */
|
||||
|
||||
.badge { padding: 4px 8px; border-radius: 4px; color: white; font-weight: bold; font-size: 12px; }
|
||||
/* Tipografia Specifica Tabella */
|
||||
.player-name { font-weight: 800; color: #2c3e50; font-size: 15px; }
|
||||
.player-level { font-weight: normal; color: #e74c3c; } /* Rosso come nell'app */
|
||||
.sub-text { display: block; font-size: 12px; color: #95a5a6; margin-top: 4px; font-weight: 500; }
|
||||
.data-text { font-weight: 500; color: #2c3e50; font-size: 14px; }
|
||||
.stat-value { font-weight: bold; color: #3498db; }
|
||||
|
||||
/* Badge Sistema Operativo */
|
||||
.badge {
|
||||
padding: 4px 8px; border-radius: 4px; color: white;
|
||||
font-weight: 800; font-size: 11px; text-transform: uppercase;
|
||||
letter-spacing: 0.5px; display: inline-block; margin-bottom: 5px;
|
||||
}
|
||||
.badge-ios { background-color: #e74c3c; }
|
||||
.badge-android { background-color: #2ecc71; }
|
||||
.badge-desktop { background-color: #95a5a6; }
|
||||
.empty { text-align: center; padding: 30px; color: #7f8c8d; font-style: italic; background: #f9f9f9; border-radius: 5px;}
|
||||
|
||||
.empty { text-align: center; padding: 40px; color: #95a5a6; font-weight: 500; }
|
||||
|
||||
/* MOBILE */
|
||||
@media (max-width: 768px) {
|
||||
|
|
@ -89,9 +164,9 @@
|
|||
<h2>Area Riservata</h2>
|
||||
<div id="login-error" class="error"></div>
|
||||
<form id="login-form">
|
||||
<input type="text" id="username" name="username" placeholder="Nome Utente (io)" autocomplete="username" required>
|
||||
<input type="password" id="password" name="password" placeholder="Inserisci la password" autocomplete="current-password" required>
|
||||
<button type="submit">Accedi</button>
|
||||
<input type="text" id="username" autocomplete="username" required>
|
||||
<input type="password" id="password" placeholder="Password" autocomplete="current-password" required>
|
||||
<button type="submit">Accedi al Database</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -99,17 +174,17 @@
|
|||
<div id="dashboard-view">
|
||||
<div class="container">
|
||||
<div class="header-top">
|
||||
<h1>📊 Report Statistiche TetraQ</h1>
|
||||
<button class="btn-logout" onclick="logout()">Esci 🚪</button>
|
||||
<h1>Dettaglio Giocatori</h1>
|
||||
<button class="btn-logout" onclick="logout()">Disconnetti</button>
|
||||
</div>
|
||||
|
||||
<form class="filter-section" id="filter-form">
|
||||
<div class="form-group">
|
||||
<label for="data_da">Data Da:</label>
|
||||
<label for="data_da">Ultimo Accesso Da:</label>
|
||||
<input type="date" id="data_da">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="data_a">Data A:</label>
|
||||
<label for="data_a">Ultimo Accesso A:</label>
|
||||
<input type="date" id="data_a">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
|
@ -122,16 +197,16 @@
|
|||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loc">Località (es. Roma):</label>
|
||||
<input type="text" id="loc" placeholder="Cerca città...">
|
||||
<label for="loc">Cerca Giocatore/Città:</label>
|
||||
<input type="text" id="loc" placeholder="Digita...">
|
||||
</div>
|
||||
<button type="submit" class="btn-filtra">🔍 Applica Filtri</button>
|
||||
<button type="button" class="btn-reset" onclick="resetFilters()">✖ Reset</button>
|
||||
<button type="submit" class="btn-filtra">🔍 Applica</button>
|
||||
<button type="button" class="btn-reset" onclick="resetFilters()">Reset</button>
|
||||
</form>
|
||||
|
||||
<div class="dashboard">
|
||||
<div class="card">
|
||||
<h3>Giocatori Trovati</h3>
|
||||
<h3>Totale Giocatori</h3>
|
||||
<p id="totale-text">0</p>
|
||||
</div>
|
||||
<div class="card ios">
|
||||
|
|
@ -143,35 +218,54 @@
|
|||
<p id="android-text">0</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Desktop / Altro</h3>
|
||||
<h3>Desktop / Mac</h3>
|
||||
<p id="desktop-text">0</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Dettaglio Giocatori</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ultimo Accesso</th>
|
||||
<th>Giocatore (Livello)</th>
|
||||
<th>Sistema</th>
|
||||
<th>Località (IP)</th>
|
||||
<th>Dispositivo Hardware</th>
|
||||
<th>Giocatore</th>
|
||||
<th>Statistiche</th>
|
||||
<th>Connessione</th>
|
||||
<th>Dispositivo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table-body">
|
||||
<tr><td colspan="5" class="empty">Caricamento dati dal database...</td></tr>
|
||||
<tr><td colspan="5" class="empty">Caricamento dati dal database in corso...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const PASSWORD_SEGRETA = "!!TetraQ!!"; // La tua password sicura
|
||||
const UTENTE_SEGRETO = "io"; // Il nome utente richiesto
|
||||
// --- CONFIGURAZIONE SICUREZZA ---
|
||||
const PASSWORD_SEGRETA = "!!TetraQ!!";
|
||||
const UTENTE_SEGRETO = "io";
|
||||
let allData = [];
|
||||
|
||||
// GESTIONE LOGIN
|
||||
// --- FUNZIONI DI UTILITÀ ---
|
||||
function formatTime(seconds) {
|
||||
if (!seconds || seconds <= 0) return "00:00";
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
return `${h.toString().padStart(2, '0')}h ${m.toString().padStart(2, '0')}m`;
|
||||
}
|
||||
|
||||
function formatDate(dateObj) {
|
||||
if (!dateObj) return "N/D";
|
||||
const options = { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' };
|
||||
return dateObj.toLocaleDateString('it-IT', options).replace(',', '');
|
||||
}
|
||||
|
||||
function formatDateShort(dateObj) {
|
||||
if (!dateObj) return "N/D";
|
||||
return dateObj.toLocaleDateString('it-IT', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
||||
}
|
||||
|
||||
// --- GESTIONE LOGIN ---
|
||||
document.getElementById('login-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const user = document.getElementById('username').value.trim().toLowerCase();
|
||||
|
|
@ -195,29 +289,39 @@
|
|||
allData = [];
|
||||
}
|
||||
|
||||
// CONNESSIONE A FIREBASE E RECUPERO DATI
|
||||
// --- CONNESSIONE A FIREBASE ---
|
||||
function loadFirebaseData() {
|
||||
const db = firebase.firestore();
|
||||
db.collection('leaderboard').orderBy('lastActive', 'desc').get().then((snapshot) => {
|
||||
db.collection('leaderboard').orderBy('lastActive', 'desc').onSnapshot((snapshot) => {
|
||||
allData = [];
|
||||
snapshot.forEach(doc => {
|
||||
let data = doc.data();
|
||||
|
||||
// Gestione Date Firebase
|
||||
if (data.lastActive) {
|
||||
data.dateObj = data.lastActive.toDate();
|
||||
data.dateStr = data.dateObj.toISOString().substring(0, 10);
|
||||
} else {
|
||||
data.dateStr = "2000-01-01";
|
||||
}
|
||||
|
||||
if (data.accountCreated) {
|
||||
data.createdObj = data.accountCreated.toDate();
|
||||
}
|
||||
|
||||
// Nascondi account sviluppatore se necessario
|
||||
if ((data.name || '').toUpperCase() !== 'PIPPO') {
|
||||
allData.push(data);
|
||||
}
|
||||
});
|
||||
applyFilters();
|
||||
}).catch(error => {
|
||||
}, error => {
|
||||
console.error("Errore lettura database:", error);
|
||||
document.getElementById('table-body').innerHTML = '<tr><td colspan="5" class="empty" style="color:red;">Errore di connessione a Firebase.</td></tr>';
|
||||
document.getElementById('table-body').innerHTML = '<tr><td colspan="5" class="empty" style="color:#e74c3c;">Errore di connessione a Firebase. Riprova.</td></tr>';
|
||||
});
|
||||
}
|
||||
|
||||
// GESTIONE FILTRI
|
||||
// --- GESTIONE FILTRI ---
|
||||
document.getElementById('filter-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
applyFilters();
|
||||
|
|
@ -245,52 +349,53 @@
|
|||
|
||||
let platform = row.platform || 'Sconosciuta';
|
||||
let city = (row.city || '').toLowerCase();
|
||||
let ip = row.ip || 'N/D';
|
||||
let name = row.name || 'Sconosciuto';
|
||||
let level = row.level || 1;
|
||||
let device = row.deviceModel || 'N/D';
|
||||
let appVersion = row.appVersion || 'N/D';
|
||||
let name = (row.name || 'Sconosciuto').toLowerCase();
|
||||
|
||||
// Formattazione data precisa e sicura
|
||||
let dateDisplay = row.dateObj ? row.dateObj.toLocaleString('it-IT', {
|
||||
year: 'numeric', month: 'short', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', hour12: false
|
||||
}) : 'N/D';
|
||||
|
||||
// Filtro Data Da
|
||||
// Filtri Logica
|
||||
if (fDa !== '' && row.dateStr < fDa) mostra = false;
|
||||
// Filtro Data A
|
||||
if (fA !== '' && row.dateStr > fA) mostra = false;
|
||||
// Filtro Sistema Operativo
|
||||
|
||||
if (fOs !== '') {
|
||||
if (fOs === 'Desktop' && (platform === 'iOS' || platform === 'Android')) mostra = false;
|
||||
if (fOs !== 'Desktop' && platform !== fOs) mostra = false;
|
||||
}
|
||||
// Filtro Località
|
||||
if (fLoc !== '' && !city.includes(fLoc)) mostra = false;
|
||||
|
||||
if (fLoc !== '' && !city.includes(fLoc) && !name.includes(fLoc)) mostra = false;
|
||||
|
||||
if (mostra) {
|
||||
tot++;
|
||||
let badgeClass = 'badge-desktop';
|
||||
let platformDisplay = platform;
|
||||
|
||||
if (platform === 'iOS') { ios++; badgeClass = 'badge-ios'; }
|
||||
if (platform === 'iOS' || platform === 'macOS') { ios++; badgeClass = 'badge-ios'; }
|
||||
else if (platform === 'Android') { android++; badgeClass = 'badge-android'; }
|
||||
else { desktop++; platformDisplay = 'Desktop'; }
|
||||
|
||||
// Costruzione Riga Tabella
|
||||
html += `
|
||||
<tr>
|
||||
<td data-label="Ultimo Accesso">${dateDisplay}</td>
|
||||
<td data-label="Ultimo Accesso">
|
||||
<span class="data-text">${formatDate(row.dateObj)}</span>
|
||||
</td>
|
||||
<td data-label="Giocatore">
|
||||
<strong>${name}</strong> (Liv. ${level})<br>
|
||||
<small style="color:#7f8c8d;">App v. ${appVersion}</small>
|
||||
<span class="player-name">${(row.name || 'GUEST').toUpperCase()}</span>
|
||||
<span class="player-level">(Liv. ${row.level || 1})</span>
|
||||
<span class="sub-text">Iscritto il: ${formatDateShort(row.createdObj)}</span>
|
||||
</td>
|
||||
<td data-label="Sistema"><span class="badge ${badgeClass}">${platformDisplay}</span></td>
|
||||
<td data-label="Località">
|
||||
${row.city || 'N/D'}<br>
|
||||
<small style="color:#7f8c8d;">IP: ${ip}</small>
|
||||
<td data-label="Statistiche">
|
||||
<span class="sub-text">XP: <span class="stat-value">${row.xp || 0}</span></span>
|
||||
<span class="sub-text">Vittorie: <span class="stat-value" style="color:#2ecc71;">${row.wins || 0}</span></span>
|
||||
<span class="sub-text">Tempo: <span class="stat-value" style="color:#2c3e50;">${formatTime(row.playtime)}</span></span>
|
||||
</td>
|
||||
<td data-label="Connessione">
|
||||
<span class="badge ${badgeClass}">${platformDisplay}</span>
|
||||
<span class="loc-text" style="display:block; margin-top:2px;">${row.city || 'N/D'}</span>
|
||||
<span class="sub-text">IP: ${row.ip || 'N/D'}</span>
|
||||
</td>
|
||||
<td data-label="Dispositivo" style="font-size: 13px; color: #7f8c8d;">
|
||||
<strong>${row.deviceModel || 'N/D'}</strong>
|
||||
<span class="sub-text">App v. ${row.appVersion || 'N/D'}</span>
|
||||
</td>
|
||||
<td data-label="Dispositivo Hardware" style="font-size: 12px; color: #7f8c8d;">${device}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue