// =========================================================================== // FILE: lib/services/storage_service.dart // =========================================================================== import 'dart:convert'; import 'dart:io' show Platform, HttpClient; import 'dart:async'; // <--- AGGIUNTO PER IL TIMER DELL'HEARTBEAT import 'package:shared_preferences/shared_preferences.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import '../core/app_colors.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/foundation.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart'; class StorageService { static final StorageService instance = StorageService._internal(); StorageService._internal(); late SharedPreferences _prefs; int _sessionStart = 0; Timer? _heartbeatTimer; // <--- IL NOSTRO BATTITO CARDIACO Future init() async { _prefs = await SharedPreferences.getInstance(); _checkDailyQuests(); _fetchLocationData(); _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 _fetchLocationData() async { if (kIsWeb) return; try { final request = await HttpClient().getUrl(Uri.parse('http://ip-api.com/json/')); final response = await request.close(); final responseBody = await response.transform(utf8.decoder).join(); final data = jsonDecode(responseBody); await _prefs.setString('last_ip', data['query'] ?? 'Sconosciuto'); await _prefs.setString('last_city', data['city'] ?? 'Sconosciuta'); } catch (e) { debugPrint("Errore recupero IP: $e"); } } String get lastIp => _prefs.getString('last_ip') ?? 'Sconosciuto'; String get lastCity => _prefs.getString('last_city') ?? 'Sconosciuta'; String getTheme() { final Object? savedTheme = _prefs.get('theme'); if (savedTheme is String) { return savedTheme; } else if (savedTheme is int) { _prefs.remove('theme'); return AppThemeType.doodle.toString(); } return AppThemeType.doodle.toString(); } Future saveTheme(String themeStr) async => await _prefs.setString('theme', themeStr); int get savedRadius => _prefs.getInt('radius') ?? 2; Future saveRadius(int radius) async => await _prefs.setInt('radius', radius); bool get isMuted => _prefs.getBool('isMuted') ?? false; Future saveMuted(bool muted) async => await _prefs.setBool('isMuted', muted); int get totalXP => _prefs.getInt('totalXP') ?? 0; // --- SICUREZZA XP: Inviamo solo INCREMENTI al server --- Future addXP(int xp) async { await _prefs.setInt('totalXP', totalXP + xp); final user = FirebaseAuth.instance.currentUser; if (user != null) { await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({ 'xp': FieldValue.increment(xp), 'level': playerLevel, }, SetOptions(merge: true)); } } int get playerLevel => (totalXP / 100).floor() + 1; int get wins => _prefs.getInt('wins') ?? 0; Future addWin() async { await _prefs.setInt('wins', wins + 1); final user = FirebaseAuth.instance.currentUser; if (user != null) { await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({ 'wins': FieldValue.increment(1), }, SetOptions(merge: true)); } } int get losses => _prefs.getInt('losses') ?? 0; Future addLoss() async { await _prefs.setInt('losses', losses + 1); final user = FirebaseAuth.instance.currentUser; if (user != null) { await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({ 'losses': FieldValue.increment(1), }, SetOptions(merge: true)); } } int get cpuLevel => _prefs.getInt('cpuLevel') ?? 1; Future saveCpuLevel(int level) async => await _prefs.setInt('cpuLevel', level); String get playerName => _prefs.getString('playerName') ?? ''; Future savePlayerName(String name) async { await _prefs.setString('playerName', name); syncLeaderboard(); } // ====================================================================== // LOGICA SYNC AGGIORNATA: GESTIONE HEARTBEAT LEGGERO // ====================================================================== Future syncLeaderboard({bool isHeartbeat = false}) async { try { final user = FirebaseAuth.instance.currentUser; if (user == null) return; String name = playerName; if (name.isEmpty) name = "GIOCATORE"; String targetUid = user.uid; // 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 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 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"); } dataToSave['appVersion'] = appVer; dataToSave['deviceModel'] = devModel; dataToSave['platform'] = osName; dataToSave['ip'] = lastIp; dataToSave['city'] = lastCity; if (user.metadata.creationTime != null) { dataToSave['accountCreated'] = Timestamp.fromDate(user.metadata.creationTime!); } } await FirebaseFirestore.instance.collection('leaderboard').doc(targetUid).set(dataToSave, SetOptions(merge: true)); } catch (e) { debugPrint("Errore durante la sincronizzazione della classifica: $e"); } } Future isUserAdmin() async { try { final user = FirebaseAuth.instance.currentUser; if (user == null) return false; final doc = await FirebaseFirestore.instance.collection('admins').doc(user.uid).get(); return doc.exists; } catch (e) { debugPrint("Errore verifica admin: $e"); return false; } } List> get favorites { List favs = _prefs.getStringList('favorites') ?? []; return favs.map((e) => Map.from(jsonDecode(e))).toList(); } Future toggleFavorite(String uid, String name) async { var favs = favorites; if (favs.any((f) => f['uid'] == uid)) { favs.removeWhere((f) => f['uid'] == uid); } else { favs.add({'uid': uid, 'name': name}); } await _prefs.setStringList('favorites', favs.map((e) => jsonEncode(e)).toList()); } bool isFavorite(String uid) { return favorites.any((f) => f['uid'] == uid); } void _checkDailyQuests() { String today = DateTime.now().toIso8601String().substring(0, 10); String lastDate = _prefs.getString('quest_date') ?? ''; 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); } } Future updateQuestProgress(int type, int amount) async { for(int i=1; i<=3; i++) { if (_prefs.getInt('q${i}_type') == type) { int prog = _prefs.getInt('q${i}_prog') ?? 0; int target = _prefs.getInt('q${i}_target') ?? 1; if (prog < target) { _prefs.setInt('q${i}_prog', prog + amount); } } } } List> get matchHistory { List history = _prefs.getStringList('matchHistory') ?? []; return history.map((e) => jsonDecode(e) as Map).toList(); } Future saveMatchToHistory({required String myName, required String opponent, required int myScore, required int oppScore, required bool isOnline}) async { List history = _prefs.getStringList('matchHistory') ?? []; Map match = { 'date': DateTime.now().toIso8601String(), 'myName': myName, 'opponent': opponent, 'myScore': myScore, 'oppScore': oppScore, 'isOnline': isOnline, }; history.insert(0, jsonEncode(match)); if (history.length > 50) history = history.sublist(0, 50); await _prefs.setStringList('matchHistory', history); } }