2026-03-01 20:59:06 +01:00
|
|
|
// ===========================================================================
|
|
|
|
|
// FILE: lib/services/storage_service.dart
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-02-27 23:35:54 +01:00
|
|
|
import 'dart:convert';
|
2026-03-15 15:00:01 +01:00
|
|
|
import 'dart:io' show Platform, HttpClient;
|
2026-02-27 23:35:54 +01:00
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
2026-03-01 20:59:06 +01:00
|
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
2026-02-27 23:35:54 +01:00
|
|
|
import '../core/app_colors.dart';
|
2026-03-04 18:00:01 +01:00
|
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
|
|
|
import 'package:flutter/foundation.dart';
|
2026-03-15 16:00:01 +01:00
|
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
2026-03-15 17:00:01 +01:00
|
|
|
import 'package:device_info_plus/device_info_plus.dart';
|
2026-02-27 23:35:54 +01:00
|
|
|
|
|
|
|
|
class StorageService {
|
|
|
|
|
static final StorageService instance = StorageService._internal();
|
|
|
|
|
StorageService._internal();
|
|
|
|
|
|
|
|
|
|
late SharedPreferences _prefs;
|
2026-03-15 15:00:01 +01:00
|
|
|
int _sessionStart = 0;
|
2026-02-27 23:35:54 +01:00
|
|
|
|
|
|
|
|
Future<void> init() async {
|
|
|
|
|
_prefs = await SharedPreferences.getInstance();
|
2026-03-13 22:00:00 +01:00
|
|
|
_checkDailyQuests();
|
2026-03-15 15:00:01 +01:00
|
|
|
_fetchLocationData();
|
|
|
|
|
_sessionStart = DateTime.now().millisecondsSinceEpoch;
|
2026-02-27 23:35:54 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-15 15:00:01 +01:00
|
|
|
Future<void> _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';
|
|
|
|
|
|
2026-03-15 18:00:01 +01:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 17:00:01 +01:00
|
|
|
Future<void> saveTheme(String themeStr) async => await _prefs.setString('theme', themeStr);
|
2026-02-27 23:35:54 +01:00
|
|
|
|
|
|
|
|
int get savedRadius => _prefs.getInt('radius') ?? 2;
|
|
|
|
|
Future<void> saveRadius(int radius) async => await _prefs.setInt('radius', radius);
|
|
|
|
|
|
|
|
|
|
bool get isMuted => _prefs.getBool('isMuted') ?? false;
|
|
|
|
|
Future<void> saveMuted(bool muted) async => await _prefs.setBool('isMuted', muted);
|
|
|
|
|
|
2026-03-01 20:59:06 +01:00
|
|
|
int get totalXP => _prefs.getInt('totalXP') ?? 0;
|
|
|
|
|
|
2026-03-21 00:00:01 +01:00
|
|
|
// --- SICUREZZA XP: Inviamo solo INCREMENTI al server ---
|
2026-03-01 20:59:06 +01:00
|
|
|
Future<void> addXP(int xp) async {
|
|
|
|
|
await _prefs.setInt('totalXP', totalXP + xp);
|
2026-03-21 00:00:01 +01:00
|
|
|
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));
|
|
|
|
|
}
|
2026-03-01 20:59:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int get playerLevel => (totalXP / 100).floor() + 1;
|
|
|
|
|
|
2026-02-27 23:35:54 +01:00
|
|
|
int get wins => _prefs.getInt('wins') ?? 0;
|
2026-03-21 00:00:01 +01:00
|
|
|
|
2026-03-01 20:59:06 +01:00
|
|
|
Future<void> addWin() async {
|
|
|
|
|
await _prefs.setInt('wins', wins + 1);
|
2026-03-21 00:00:01 +01:00
|
|
|
final user = FirebaseAuth.instance.currentUser;
|
|
|
|
|
if (user != null) {
|
|
|
|
|
await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({
|
|
|
|
|
'wins': FieldValue.increment(1),
|
|
|
|
|
}, SetOptions(merge: true));
|
|
|
|
|
}
|
2026-03-01 20:59:06 +01:00
|
|
|
}
|
2026-02-27 23:35:54 +01:00
|
|
|
|
|
|
|
|
int get losses => _prefs.getInt('losses') ?? 0;
|
2026-03-21 00:00:01 +01:00
|
|
|
|
|
|
|
|
Future<void> 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));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-27 23:35:54 +01:00
|
|
|
|
|
|
|
|
int get cpuLevel => _prefs.getInt('cpuLevel') ?? 1;
|
|
|
|
|
Future<void> saveCpuLevel(int level) async => await _prefs.setInt('cpuLevel', level);
|
|
|
|
|
|
|
|
|
|
String get playerName => _prefs.getString('playerName') ?? '';
|
2026-03-01 20:59:06 +01:00
|
|
|
Future<void> savePlayerName(String name) async {
|
|
|
|
|
await _prefs.setString('playerName', name);
|
2026-03-13 22:00:00 +01:00
|
|
|
syncLeaderboard();
|
2026-03-01 20:59:06 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-23 01:00:01 +01:00
|
|
|
// ======================================================================
|
|
|
|
|
// FIX: ORA IL SYNC MANDA I DATI REALI ALLA DASHBOARD ADMIN!
|
|
|
|
|
// ======================================================================
|
2026-03-01 20:59:06 +01:00
|
|
|
Future<void> syncLeaderboard() async {
|
2026-03-20 19:00:01 +01:00
|
|
|
try {
|
|
|
|
|
final user = FirebaseAuth.instance.currentUser;
|
|
|
|
|
if (user == null) return;
|
|
|
|
|
|
|
|
|
|
String name = playerName;
|
2026-03-21 00:00:01 +01:00
|
|
|
if (name.isEmpty) name = "GIOCATORE";
|
2026-03-20 19:00:01 +01:00
|
|
|
|
|
|
|
|
String targetUid = user.uid;
|
|
|
|
|
|
2026-03-23 01:00:01 +01:00
|
|
|
// 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
|
2026-03-20 23:00:01 +01:00
|
|
|
Map<String, dynamic> dataToSave = {
|
2026-03-20 19:00:01 +01:00
|
|
|
'name': name,
|
|
|
|
|
'level': playerLevel,
|
|
|
|
|
'lastActive': FieldValue.serverTimestamp(),
|
2026-03-23 01:00:01 +01:00
|
|
|
'appVersion': appVer,
|
|
|
|
|
'deviceModel': devModel,
|
|
|
|
|
'platform': osName,
|
|
|
|
|
'ip': lastIp,
|
|
|
|
|
'city': lastCity,
|
|
|
|
|
'playtime': totalPlaytime,
|
2026-03-20 23:00:01 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (user.metadata.creationTime != null) {
|
|
|
|
|
dataToSave['accountCreated'] = Timestamp.fromDate(user.metadata.creationTime!);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await FirebaseFirestore.instance.collection('leaderboard').doc(targetUid).set(dataToSave, SetOptions(merge: true));
|
2026-03-20 19:00:01 +01:00
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
debugPrint("Errore durante la sincronizzazione della classifica: $e");
|
2026-03-01 20:59:06 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 00:00:01 +01:00
|
|
|
Future<bool> 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 13:00:00 +01:00
|
|
|
List<Map<String, String>> get favorites {
|
|
|
|
|
List<String> favs = _prefs.getStringList('favorites') ?? [];
|
|
|
|
|
return favs.map((e) => Map<String, String>.from(jsonDecode(e))).toList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> toggleFavorite(String uid, String name) async {
|
|
|
|
|
var favs = favorites;
|
|
|
|
|
if (favs.any((f) => f['uid'] == uid)) {
|
2026-03-15 15:00:01 +01:00
|
|
|
favs.removeWhere((f) => f['uid'] == uid);
|
2026-03-15 13:00:00 +01:00
|
|
|
} else {
|
2026-03-15 15:00:01 +01:00
|
|
|
favs.add({'uid': uid, 'name': name});
|
2026-03-15 13:00:00 +01:00
|
|
|
}
|
|
|
|
|
await _prefs.setStringList('favorites', favs.map((e) => jsonEncode(e)).toList());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool isFavorite(String uid) {
|
|
|
|
|
return favorites.any((f) => f['uid'] == uid);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 20:59:06 +01:00
|
|
|
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<void> 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-27 23:35:54 +01:00
|
|
|
|
|
|
|
|
List<Map<String, dynamic>> get matchHistory {
|
|
|
|
|
List<String> history = _prefs.getStringList('matchHistory') ?? [];
|
|
|
|
|
return history.map((e) => jsonDecode(e) as Map<String, dynamic>).toList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> saveMatchToHistory({required String myName, required String opponent, required int myScore, required int oppScore, required bool isOnline}) async {
|
|
|
|
|
List<String> history = _prefs.getStringList('matchHistory') ?? [];
|
|
|
|
|
Map<String, dynamic> match = {
|
|
|
|
|
'date': DateTime.now().toIso8601String(),
|
2026-03-01 20:59:06 +01:00
|
|
|
'myName': myName, 'opponent': opponent, 'myScore': myScore, 'oppScore': oppScore, 'isOnline': isOnline,
|
2026-02-27 23:35:54 +01:00
|
|
|
};
|
|
|
|
|
history.insert(0, jsonEncode(match));
|
2026-03-01 20:59:06 +01:00
|
|
|
if (history.length > 50) history = history.sublist(0, 50);
|
2026-02-27 23:35:54 +01:00
|
|
|
await _prefs.setStringList('matchHistory', history);
|
|
|
|
|
}
|
|
|
|
|
}
|