Auto-sync: 20260315_170000

This commit is contained in:
Paolo 2026-03-15 17:00:01 +01:00
parent 8caecd401e
commit 917a532529
23 changed files with 637 additions and 313 deletions

BIN
.DS_Store vendored

Binary file not shown.

View file

@ -1,2 +1,3 @@
404.html,1773344753356,05cbc6f94d7a69ce2e29646eab13be2c884e61ba93e3094df5028866876d18b3
index.html,1773586765860,5737ce966fa8786becaf7f36a32992cf44102fb3a217c226c30576c993b33e63 index.html,1773586765860,5737ce966fa8786becaf7f36a32992cf44102fb3a217c226c30576c993b33e63
404.html,1773344753356,05cbc6f94d7a69ce2e29646eab13be2c884e61ba93e3094df5028866876d18b3
report.html,1773588057140,876c6baaa912c9abfb81ee70e9868d84476b1c204ebca4c99f458f300661a36b

BIN
assets/.DS_Store vendored

Binary file not shown.

BIN
assets/audio/.DS_Store vendored

Binary file not shown.

View file

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View file

@ -1206,6 +1206,8 @@ PODS:
- Firebase/Firestore (= 12.9.0) - Firebase/Firestore (= 12.9.0)
- firebase_core - firebase_core
- Flutter - Flutter
- device_info_plus (0.0.1):
- Flutter
- Firebase/Auth (12.9.0): - Firebase/Auth (12.9.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseAuth (~> 12.9.0) - FirebaseAuth (~> 12.9.0)
@ -1410,6 +1412,7 @@ DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`) - app_links (from `.symlinks/plugins/app_links/ios`)
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`) - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`)
- cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`) - cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- firebase_app_check (from `.symlinks/plugins/firebase_app_check/ios`) - firebase_app_check (from `.symlinks/plugins/firebase_app_check/ios`)
- firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
@ -1450,6 +1453,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/audioplayers_darwin/ios" :path: ".symlinks/plugins/audioplayers_darwin/ios"
cloud_firestore: cloud_firestore:
:path: ".symlinks/plugins/cloud_firestore/ios" :path: ".symlinks/plugins/cloud_firestore/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
firebase_app_check: firebase_app_check:
:path: ".symlinks/plugins/firebase_app_check/ios" :path: ".symlinks/plugins/firebase_app_check/ios"
firebase_auth: firebase_auth:
@ -1472,6 +1477,7 @@ SPEC CHECKSUMS:
audioplayers_darwin: ccf9c770ee768abb07e26d90af093f7bab1c12ab audioplayers_darwin: ccf9c770ee768abb07e26d90af093f7bab1c12ab
BoringSSL-GRPC: dded2a44897e45f28f08ae87a55ee4bcd19bc508 BoringSSL-GRPC: dded2a44897e45f28f08ae87a55ee4bcd19bc508
cloud_firestore: 81f6c428ecee874dc3808afe0e0c48a87beb5bdf cloud_firestore: 81f6c428ecee874dc3808afe0e0c48a87beb5bdf
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
Firebase: 065f2bb395062046623036d8e6dc857bc2521d56 Firebase: 065f2bb395062046623036d8e6dc857bc2521d56
firebase_app_check: 33f1df6830ec8ebadee0db0120956c44a65c7213 firebase_app_check: 33f1df6830ec8ebadee0db0120956c44a65c7213
firebase_auth: fecf9fe293464b52063f5f2a7110e63ff2ab3403 firebase_auth: fecf9fe293464b52063f5f2a7110e63ff2ab3403

View file

@ -5,7 +5,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
enum AppThemeType { doodle, wood, cyberpunk, arcade, grimorio, music } enum AppThemeType { doodle, cyberpunk, arcade, grimorio, music }
class ThemeColors { class ThemeColors {
final Color background; final Color background;
@ -34,18 +34,13 @@ class AppColors {
playerRed: Color(0xFFFF007F), playerBlue: Color(0xFF69F0AE), text: Color(0xFFFFFFFF), playerRed: Color(0xFFFF007F), playerBlue: Color(0xFF69F0AE), text: Color(0xFFFFFFFF),
); );
static const ThemeColors wood = ThemeColors(
background: Color(0xFF905D3B), gridLine: Color(0xFF4A301E),
playerRed: Color(0xFFE53935), playerBlue: Color(0xFF29B6F6), text: Color(0xFFFBE9E7),
);
static const ThemeColors arcade = ThemeColors( static const ThemeColors arcade = ThemeColors(
background: Color(0xFF111111), gridLine: Color(0xFF00FF00), background: Color(0xFF111111), gridLine: Color(0xFF00FF00),
playerRed: Color(0xFFFF004D), playerBlue: Color(0xFF00E5FF), text: Color(0xFFFFFFFF), playerRed: Color(0xFFFF004D), playerBlue: Color(0xFF00E5FF), text: Color(0xFFFFFFFF),
); );
static const ThemeColors grimorio = ThemeColors( static const ThemeColors grimorio = ThemeColors(
background: Color(0xFF1E112A), gridLine: Colors.black, // <--- Modificato in nero! background: Color(0xFF1E112A), gridLine: Colors.black,
playerRed: Color(0xFFE91E63), playerBlue: Color(0xFF4FC3F7), text: Color(0xFFFFF3E0), playerRed: Color(0xFFE91E63), playerBlue: Color(0xFF4FC3F7), text: Color(0xFFFFF3E0),
); );
@ -60,7 +55,6 @@ class AppColors {
static ThemeColors getTheme(AppThemeType type) { static ThemeColors getTheme(AppThemeType type) {
switch (type) { switch (type) {
case AppThemeType.doodle: return doodle; case AppThemeType.doodle: return doodle;
case AppThemeType.wood: return wood;
case AppThemeType.cyberpunk: return cyberpunk; case AppThemeType.cyberpunk: return cyberpunk;
case AppThemeType.arcade: return arcade; case AppThemeType.arcade: return arcade;
case AppThemeType.grimorio: return grimorio; case AppThemeType.grimorio: return grimorio;
@ -73,7 +67,6 @@ class ThemeIcons {
static IconData gold(AppThemeType type) { static IconData gold(AppThemeType type) {
switch (type) { switch (type) {
case AppThemeType.doodle: return FontAwesomeIcons.star; case AppThemeType.doodle: return FontAwesomeIcons.star;
case AppThemeType.wood: return FontAwesomeIcons.gem;
case AppThemeType.cyberpunk: return FontAwesomeIcons.microchip; case AppThemeType.cyberpunk: return FontAwesomeIcons.microchip;
case AppThemeType.arcade: return FontAwesomeIcons.coins; case AppThemeType.arcade: return FontAwesomeIcons.coins;
case AppThemeType.grimorio: return FontAwesomeIcons.crown; case AppThemeType.grimorio: return FontAwesomeIcons.crown;
@ -84,7 +77,6 @@ class ThemeIcons {
static IconData bomb(AppThemeType type) { static IconData bomb(AppThemeType type) {
switch (type) { switch (type) {
case AppThemeType.doodle: return FontAwesomeIcons.virus; case AppThemeType.doodle: return FontAwesomeIcons.virus;
case AppThemeType.wood: return FontAwesomeIcons.fire;
case AppThemeType.cyberpunk: return FontAwesomeIcons.bug; case AppThemeType.cyberpunk: return FontAwesomeIcons.bug;
case AppThemeType.arcade: return FontAwesomeIcons.ghost; case AppThemeType.arcade: return FontAwesomeIcons.ghost;
case AppThemeType.grimorio: return FontAwesomeIcons.hatWizard; case AppThemeType.grimorio: return FontAwesomeIcons.hatWizard;
@ -95,7 +87,6 @@ class ThemeIcons {
static IconData swap(AppThemeType type) { static IconData swap(AppThemeType type) {
switch (type) { switch (type) {
case AppThemeType.doodle: return FontAwesomeIcons.arrowsRotate; case AppThemeType.doodle: return FontAwesomeIcons.arrowsRotate;
case AppThemeType.wood: return FontAwesomeIcons.rightLeft;
case AppThemeType.cyberpunk: return FontAwesomeIcons.networkWired; case AppThemeType.cyberpunk: return FontAwesomeIcons.networkWired;
case AppThemeType.arcade: return FontAwesomeIcons.shuffle; case AppThemeType.arcade: return FontAwesomeIcons.shuffle;
case AppThemeType.grimorio: return FontAwesomeIcons.hurricane; case AppThemeType.grimorio: return FontAwesomeIcons.hurricane;
@ -106,7 +97,6 @@ class ThemeIcons {
static IconData joker(AppThemeType type) { static IconData joker(AppThemeType type) {
switch (type) { switch (type) {
case AppThemeType.doodle: return FontAwesomeIcons.faceSmileBeam; case AppThemeType.doodle: return FontAwesomeIcons.faceSmileBeam;
case AppThemeType.wood: return FontAwesomeIcons.key;
case AppThemeType.cyberpunk: return FontAwesomeIcons.robot; case AppThemeType.cyberpunk: return FontAwesomeIcons.robot;
case AppThemeType.arcade: return FontAwesomeIcons.gamepad; case AppThemeType.arcade: return FontAwesomeIcons.gamepad;
case AppThemeType.grimorio: return FontAwesomeIcons.masksTheater; case AppThemeType.grimorio: return FontAwesomeIcons.masksTheater;
@ -117,7 +107,6 @@ class ThemeIcons {
static IconData block(AppThemeType type) { static IconData block(AppThemeType type) {
switch (type) { switch (type) {
case AppThemeType.doodle: return FontAwesomeIcons.squareXmark; case AppThemeType.doodle: return FontAwesomeIcons.squareXmark;
case AppThemeType.wood: return FontAwesomeIcons.ban;
case AppThemeType.cyberpunk: return FontAwesomeIcons.shieldHalved; case AppThemeType.cyberpunk: return FontAwesomeIcons.shieldHalved;
case AppThemeType.arcade: return FontAwesomeIcons.powerOff; case AppThemeType.arcade: return FontAwesomeIcons.powerOff;
case AppThemeType.grimorio: return FontAwesomeIcons.meteor; case AppThemeType.grimorio: return FontAwesomeIcons.meteor;

View file

@ -3,31 +3,64 @@
// =========================================================================== // ===========================================================================
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'app_colors.dart'; import 'app_colors.dart';
import '../services/storage_service.dart'; import '../services/storage_service.dart';
import '../services/audio_service.dart'; // <-- NUOVO IMPORT PER LA MUSICA
class ThemeManager extends ChangeNotifier { // --- ENUM DEI TEMI AGGIORNATO ---
late AppThemeType _currentThemeType; const Map<AppThemeType, IconData> themeIcons = {
AppThemeType.cyberpunk: Icons.electric_bolt,
AppThemeType.doodle: Icons.brush,
AppThemeType.music: Icons.headset_mic,
AppThemeType.arcade: Icons.videogame_asset,
AppThemeType.grimorio: Icons.auto_stories,
};
ThemeManager() { const Map<AppThemeType, String> themeNames = {
// Quando l'app parte, legge il tema dalla memoria! AppThemeType.cyberpunk: "Cyberpunk",
_currentThemeType = AppThemeType.values[StorageService.instance.savedThemeIndex]; AppThemeType.doodle: "Doodle",
AppThemeType.music: "Music",
AppThemeType.arcade: "Arcade",
AppThemeType.grimorio: "Grimorio",
};
// Fai partire subito la colonna sonora del tema salvato! class ThemeManager with ChangeNotifier {
AudioService.instance.playBgm(_currentThemeType); AppThemeType _currentThemeType = AppThemeType.doodle;
} ThemeColors _currentColors = AppColors.getTheme(AppThemeType.doodle);
AppThemeType get currentThemeType => _currentThemeType; AppThemeType get currentThemeType => _currentThemeType;
ThemeColors get currentColors => AppColors.getTheme(_currentThemeType); ThemeColors get currentColors => _currentColors;
ThemeManager() {
_loadTheme();
}
void _loadTheme() async {
String themeStr = StorageService.instance.getTheme();
AppThemeType loadedType = AppThemeType.values.firstWhere(
(e) => e.toString() == themeStr,
orElse: () => AppThemeType.doodle
);
_currentThemeType = loadedType;
_currentColors = AppColors.getTheme(loadedType);
_updateSystemUI();
notifyListeners();
}
void setTheme(AppThemeType type) { void setTheme(AppThemeType type) {
_currentThemeType = type; _currentThemeType = type;
StorageService.instance.saveTheme(type); // Salva la scelta nel "disco fisso" _currentColors = AppColors.getTheme(type);
StorageService.instance.saveTheme(type.toString());
// Cambia magicamente la canzone in sottofondo! _updateSystemUI();
AudioService.instance.playBgm(type);
notifyListeners(); notifyListeners();
} }
void _updateSystemUI() {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: _currentThemeType == AppThemeType.doodle ? Brightness.dark : Brightness.light,
systemNavigationBarColor: _currentColors.background,
systemNavigationBarIconBrightness: _currentThemeType == AppThemeType.doodle ? Brightness.dark : Brightness.light,
));
}
} }

View file

@ -326,20 +326,14 @@ class GameController extends ChangeNotifier {
void _handleTimeOut() { void _handleTimeOut() {
if (!isTimeMode || isSetupPhase) return; if (!isTimeMode || isSetupPhase) return;
// Solo chi deve giocare può subire il timeout (se è online)
if (isOnline && board.currentPlayer != myPlayer) return; if (isOnline && board.currentPlayer != myPlayer) return;
// 1. Raccogliamo TUTTE le linee ancora libere e giocabili
List<Line> availableLines = board.lines.where((l) => l.owner == Player.none && l.isPlayable).toList(); List<Line> availableLines = board.lines.where((l) => l.owner == Player.none && l.isPlayable).toList();
// Sicurezza: se non ci sono mosse, non facciamo nulla
if (availableLines.isEmpty) return; if (availableLines.isEmpty) return;
// 2. Scegliamo una linea in modo PURAMENTE CASUALE (nessuna intelligenza artificiale)
final random = Random(); final random = Random();
Line randomMove = availableLines[random.nextInt(availableLines.length)]; Line randomMove = availableLines[random.nextInt(availableLines.length)];
// 3. Eseguiamo la mossa forzata
handleLineTap(randomMove, _activeTheme, forced: true); handleLineTap(randomMove, _activeTheme, forced: true);
} }
@ -549,16 +543,17 @@ class GameController extends ChangeNotifier {
} }
} }
// --- NUOVI LIVELLI DI SBLOCCO ---
List<String> _getUnlocks(int oldLevel, int newLevel) { List<String> _getUnlocks(int oldLevel, int newLevel) {
List<String> unlocks = []; List<String> unlocks = [];
for(int i = oldLevel + 1; i <= newLevel; i++) { for(int i = oldLevel + 1; i <= newLevel; i++) {
if (i == 3) unlocks.add("Tema: Legno & Fiammiferi"); if (i == 3) unlocks.add("Tema: Cyberpunk");
if (i == 7) unlocks.add("Tema: Cyberpunk"); if (i == 7) {
if (i == 10) {
unlocks.add("Tema: 8-Bit Arcade"); unlocks.add("Tema: 8-Bit Arcade");
unlocks.add("Forma Arena: Caos"); unlocks.add("Forma Arena: Caos");
} }
if (i == 15) unlocks.add("Tema: Grimorio"); if (i == 10) unlocks.add("Tema: Grimorio");
if (i == 15) unlocks.add("Tema: Musica");
} }
return unlocks; return unlocks;
} }

View file

@ -29,11 +29,9 @@ class AudioService extends ChangeNotifier {
await prefs.setBool('isMuted', isMuted); await prefs.setBool('isMuted', isMuted);
if (isMuted) { if (isMuted) {
// Se abbiamo appena silenziato, FERMA TUTTO immediatamente.
await _bgmPlayer.pause(); await _bgmPlayer.pause();
await _sfxPlayer.stop(); await _sfxPlayer.stop();
} else { } else {
// Se riaccendiamo, fai ripartire la canzone
playBgm(_currentTheme); playBgm(_currentTheme);
} }
notifyListeners(); notifyListeners();
@ -54,9 +52,6 @@ class AudioService extends ChangeNotifier {
case AppThemeType.doodle: case AppThemeType.doodle:
audioPath = 'audio/bgm/Quad_Dreams.mp3'; audioPath = 'audio/bgm/Quad_Dreams.mp3';
break; break;
case AppThemeType.wood:
audioPath = 'audio/bgm/Legno_Canopy.mp3';
break;
case AppThemeType.arcade: case AppThemeType.arcade:
audioPath = 'audio/bgm/8-bit_Prowler.mp3'; audioPath = 'audio/bgm/8-bit_Prowler.mp3';
break; break;
@ -64,7 +59,7 @@ class AudioService extends ChangeNotifier {
audioPath = 'audio/bgm/Grimorio_Astral.mp3'; audioPath = 'audio/bgm/Grimorio_Astral.mp3';
break; break;
case AppThemeType.music: case AppThemeType.music:
audioPath = 'audio/bgm/Music_Loop.mp3'; // <-- DEVI INSERIRE QUESTO FILE IN ASSETS audioPath = 'audio/bgm/Music_Loop.mp3';
break; break;
} }
@ -86,10 +81,9 @@ class AudioService extends ChangeNotifier {
String file = ''; String file = '';
switch (theme) { switch (theme) {
case AppThemeType.arcade: case AppThemeType.arcade:
case AppThemeType.music: // Usiamo l'effetto arcade o cyber per la musica case AppThemeType.music:
file = 'minimal_line.wav'; break; file = 'minimal_line.wav'; break;
case AppThemeType.doodle: case AppThemeType.doodle:
case AppThemeType.wood:
file = 'doodle_line.wav'; break; file = 'doodle_line.wav'; break;
case AppThemeType.cyberpunk: case AppThemeType.cyberpunk:
case AppThemeType.grimorio: case AppThemeType.grimorio:
@ -113,7 +107,6 @@ class AudioService extends ChangeNotifier {
case AppThemeType.music: case AppThemeType.music:
file = 'minimal_box.wav'; break; file = 'minimal_box.wav'; break;
case AppThemeType.doodle: case AppThemeType.doodle:
case AppThemeType.wood:
file = 'doodle_box.wav'; break; file = 'doodle_box.wav'; break;
case AppThemeType.cyberpunk: case AppThemeType.cyberpunk:
case AppThemeType.grimorio: case AppThemeType.grimorio:

View file

@ -64,13 +64,17 @@ class MultiplayerService {
} }
void shareInviteLink(String roomCode) { void shareInviteLink(String roomCode) {
// ECCO IL TUO SMART LINK FIREBASE!
String smartLink = "https://tetraq-32a4a.web.app";
String message = "Ehi! Giochiamo a TetraQ? 🎮\n\n" String message = "Ehi! Giochiamo a TetraQ? 🎮\n\n"
"Clicca su questo link per entrare direttamente in stanza:\n" "Apri l'app e inserisci il codice stanza:\n"
"👉 $roomCode\n\n"
"Oppure clicca qui se il tuo telefono lo supporta:\n"
"tetraq://join?code=$roomCode\n\n" "tetraq://join?code=$roomCode\n\n"
"Apri l'app e inserisci manualmente il codice: $roomCode\n" "Non hai ancora il gioco? Scaricalo da qui:\n"
"Se non hai ancora scaricato il gioco lo trovi nei link sotto \n\n" "$smartLink";
"🍎 iOS: https://apps.apple.com/it/app/tetraq/id6759522394\n"
"🤖 Android: https://play.google.com/store/apps/details?id=com.amastra.tetraq";
Share.share(message); Share.share(message);
} }

View file

@ -10,7 +10,7 @@ import '../core/app_colors.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:device_info_plus/device_info_plus.dart'; // <-- NUOVO IMPORT import 'package:device_info_plus/device_info_plus.dart';
class StorageService { class StorageService {
static final StorageService instance = StorageService._internal(); static final StorageService instance = StorageService._internal();
@ -26,7 +26,6 @@ class StorageService {
_sessionStart = DateTime.now().millisecondsSinceEpoch; _sessionStart = DateTime.now().millisecondsSinceEpoch;
} }
// --- RECUPERO IP E CITTÀ IN BACKGROUND ---
Future<void> _fetchLocationData() async { Future<void> _fetchLocationData() async {
if (kIsWeb) return; if (kIsWeb) return;
try { try {
@ -43,10 +42,10 @@ class StorageService {
String get lastIp => _prefs.getString('last_ip') ?? 'Sconosciuto'; String get lastIp => _prefs.getString('last_ip') ?? 'Sconosciuto';
String get lastCity => _prefs.getString('last_city') ?? 'Sconosciuta'; String get lastCity => _prefs.getString('last_city') ?? 'Sconosciuta';
// ------------------------------------------------
int get savedThemeIndex => _prefs.getInt('theme') ?? AppThemeType.doodle.index; // --- METODI TEMA AGGIORNATI A STRINGHE ---
Future<void> saveTheme(AppThemeType theme) async => await _prefs.setInt('theme', theme.index); String getTheme() => _prefs.getString('theme') ?? AppThemeType.doodle.toString();
Future<void> saveTheme(String themeStr) async => await _prefs.setString('theme', themeStr);
int get savedRadius => _prefs.getInt('radius') ?? 2; int get savedRadius => _prefs.getInt('radius') ?? 2;
Future<void> saveRadius(int radius) async => await _prefs.setInt('radius', radius); Future<void> saveRadius(int radius) async => await _prefs.setInt('radius', radius);
@ -87,19 +86,16 @@ class StorageService {
final user = FirebaseAuth.instance.currentUser; final user = FirebaseAuth.instance.currentUser;
if (user != null) { if (user != null) {
// IDENTIFICA IL SISTEMA OPERATIVO E LA VERSIONE APP
String currentPlatform = "Sconosciuta"; String currentPlatform = "Sconosciuta";
String appVersion = "N/D"; String appVersion = "N/D";
String deviceModel = "Sconosciuto"; // <-- NUOVO: MODELLO HARDWARE String deviceModel = "Sconosciuto";
if (!kIsWeb) { if (!kIsWeb) {
// Leggi Piattaforma base
if (Platform.isAndroid) currentPlatform = "Android"; if (Platform.isAndroid) currentPlatform = "Android";
else if (Platform.isIOS) currentPlatform = "iOS"; else if (Platform.isIOS) currentPlatform = "iOS";
else if (Platform.isMacOS) currentPlatform = "macOS"; else if (Platform.isMacOS) currentPlatform = "macOS";
else if (Platform.isWindows) currentPlatform = "Windows"; else if (Platform.isWindows) currentPlatform = "Windows";
// Leggi Versione App
try { try {
PackageInfo packageInfo = await PackageInfo.fromPlatform(); PackageInfo packageInfo = await PackageInfo.fromPlatform();
appVersion = "${packageInfo.version}+${packageInfo.buildNumber}"; appVersion = "${packageInfo.version}+${packageInfo.buildNumber}";
@ -107,25 +103,23 @@ class StorageService {
debugPrint("Errore lettura versione: $e"); debugPrint("Errore lettura versione: $e");
} }
// Leggi Modello Hardware
try { try {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) { if (Platform.isAndroid) {
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
deviceModel = "${androidInfo.manufacturer} ${androidInfo.model}"; // Es. samsung SM-S928B deviceModel = "${androidInfo.manufacturer} ${androidInfo.model}";
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
IosDeviceInfo iosInfo = await deviceInfo.iosInfo; IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
deviceModel = iosInfo.utsname.machine; // Es. iPhone13,2 deviceModel = iosInfo.utsname.machine;
} else if (Platform.isMacOS) { } else if (Platform.isMacOS) {
MacOsDeviceInfo macInfo = await deviceInfo.macOsInfo; MacOsDeviceInfo macInfo = await deviceInfo.macOsInfo;
deviceModel = macInfo.model; // Es. MacBookPro16,1 deviceModel = macInfo.model;
} }
} catch(e) { } catch(e) {
debugPrint("Errore lettura hardware: $e"); debugPrint("Errore lettura hardware: $e");
} }
} }
// AGGIORNA IL TEMPO DI UTILIZZO
if (_sessionStart != 0) { if (_sessionStart != 0) {
int now = DateTime.now().millisecondsSinceEpoch; int now = DateTime.now().millisecondsSinceEpoch;
int sessionSeconds = (now - _sessionStart) ~/ 1000; int sessionSeconds = (now - _sessionStart) ~/ 1000;
@ -145,7 +139,7 @@ class StorageService {
'city': lastCity, 'city': lastCity,
'playtime': totalPlaytime, 'playtime': totalPlaytime,
'appVersion': appVersion, 'appVersion': appVersion,
'deviceModel': deviceModel, // Salva il modello hardware 'deviceModel': deviceModel,
}, SetOptions(merge: true)); }, SetOptions(merge: true));
} }
} catch(e) { } catch(e) {
@ -154,7 +148,6 @@ class StorageService {
} }
} }
// --- GESTIONE PREFERITI (RUBRICA LOCALE) ---
List<Map<String, String>> get favorites { List<Map<String, String>> get favorites {
List<String> favs = _prefs.getStringList('favorites') ?? []; List<String> favs = _prefs.getStringList('favorites') ?? [];
return favs.map((e) => Map<String, String>.from(jsonDecode(e))).toList(); return favs.map((e) => Map<String, String>.from(jsonDecode(e))).toList();

View file

@ -84,10 +84,6 @@ class BoardPainter extends CustomPainter {
if (themeType == AppThemeType.music) { if (themeType == AppThemeType.music) {
fillPaint.color = Colors.white.withOpacity(0.08); fillPaint.color = Colors.white.withOpacity(0.08);
canvas.drawPath(arenaShape, fillPaint); canvas.drawPath(arenaShape, fillPaint);
} else if (themeType == AppThemeType.wood) {
fillPaint.color = Colors.black.withOpacity(0.3);
fillPaint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 15.0);
canvas.drawPath(arenaShape, fillPaint);
} else if (themeType == AppThemeType.cyberpunk) { } else if (themeType == AppThemeType.cyberpunk) {
fillPaint.color = theme.playerBlue.withOpacity(0.1); fillPaint.color = theme.playerBlue.withOpacity(0.1);
canvas.drawPath(arenaShape, fillPaint); canvas.drawPath(arenaShape, fillPaint);
@ -99,7 +95,7 @@ class BoardPainter extends CustomPainter {
final outlinePaint = Paint() final outlinePaint = Paint()
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = baseStroke * 0.5 // Moltiplicato per 0.5 = grande la metà delle linee interne! ..strokeWidth = baseStroke * 0.5
..strokeJoin = StrokeJoin.round; ..strokeJoin = StrokeJoin.round;
if (themeType == AppThemeType.cyberpunk) { if (themeType == AppThemeType.cyberpunk) {
@ -108,12 +104,8 @@ class BoardPainter extends CustomPainter {
} }
else if (themeType == AppThemeType.arcade) { outlinePaint.color = Colors.white; } else if (themeType == AppThemeType.arcade) { outlinePaint.color = Colors.white; }
else if (themeType == AppThemeType.grimorio) { outlinePaint.color = theme.gridLine.withOpacity(0.6); } else if (themeType == AppThemeType.grimorio) { outlinePaint.color = theme.gridLine.withOpacity(0.6); }
else if (themeType == AppThemeType.music) { outlinePaint.color = Colors.black; } // Rimosso lo spessore forzato a 8.0! else if (themeType == AppThemeType.music) { outlinePaint.color = Colors.black; }
else if (themeType == AppThemeType.doodle) { outlinePaint.color = const Color(0xFF111122); } // Rimosso lo spessore forzato a 6.0! else if (themeType == AppThemeType.doodle) { outlinePaint.color = const Color(0xFF111122); }
else if (themeType == AppThemeType.wood) {
outlinePaint.color = const Color(0xFF3E2723);
outlinePaint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 2.0);
}
else { outlinePaint.color = theme.gridLine.withOpacity(0.8); } else { outlinePaint.color = theme.gridLine.withOpacity(0.8); }
// Disegniamo il contorno // Disegniamo il contorno
@ -143,9 +135,7 @@ class BoardPainter extends CustomPainter {
..style = PaintingStyle.fill ..style = PaintingStyle.fill
..color = box.owner == Player.red ? theme.playerRed.withOpacity(0.6) : theme.playerBlue.withOpacity(0.6); ..color = box.owner == Player.red ? theme.playerRed.withOpacity(0.6) : theme.playerBlue.withOpacity(0.6);
if (themeType == AppThemeType.wood) { if (themeType == AppThemeType.doodle) {
_drawFlameBox(canvas, rect, box.owner == Player.red);
} else if (themeType == AppThemeType.doodle) {
Color penColor = box.owner == Player.red ? Colors.redAccent.shade700 : Colors.blueAccent.shade700; Color penColor = box.owner == Player.red ? Colors.redAccent.shade700 : Colors.blueAccent.shade700;
_drawScribbleBox(canvas, rect, penColor); _drawScribbleBox(canvas, rect, penColor);
} else if (themeType == AppThemeType.arcade) { } else if (themeType == AppThemeType.arcade) {
@ -197,7 +187,7 @@ class BoardPainter extends CustomPainter {
// --- DISEGNO DELLA LINEA "INCRINATA" DAL GHIACCIO --- // --- DISEGNO DELLA LINEA "INCRINATA" DAL GHIACCIO ---
if (line.isIceCracked) { if (line.isIceCracked) {
_drawCrackedIceLine(canvas, p1, p2, blinkValue); _drawCrackedIceLine(canvas, p1, p2, blinkValue);
continue; // Non ha ancora un proprietario, passiamo alla prossima! continue;
} }
bool isLastMove = (line == board.lastMove); bool isLastMove = (line == board.lastMove);
@ -205,19 +195,11 @@ class BoardPainter extends CustomPainter {
? theme.gridLine.withOpacity(0.4) ? theme.gridLine.withOpacity(0.4)
: (line.owner == Player.red ? theme.playerRed : theme.playerBlue); : (line.owner == Player.red ? theme.playerRed : theme.playerBlue);
if (isLastMove && line.owner != Player.none && themeType != AppThemeType.wood && themeType != AppThemeType.cyberpunk && themeType != AppThemeType.arcade && themeType != AppThemeType.grimorio) { if (isLastMove && line.owner != Player.none && themeType != AppThemeType.cyberpunk && themeType != AppThemeType.arcade && themeType != AppThemeType.grimorio) {
canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(blinkValue * 0.5)..strokeWidth = 16.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)); canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(blinkValue * 0.5)..strokeWidth = 16.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0));
} }
if (themeType == AppThemeType.wood) { if (themeType == AppThemeType.cyberpunk) {
if (line.owner == Player.none) {
canvas.drawLine(p1, p2, Paint()..color = const Color(0xFF3E2723).withOpacity(0.3)..strokeWidth = 4.5..strokeCap = StrokeCap.round);
} else {
Color headColor = lineColor;
if (isLastMove) headColor = Color.lerp(headColor, Colors.yellow, blinkValue * 0.8) ?? headColor;
_drawRealisticMatch(canvas, p1, p2, headColor, isLastMove: isLastMove, blinkValue: blinkValue);
}
} else if (themeType == AppThemeType.cyberpunk) {
_drawNeonLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue); _drawNeonLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
} else if (themeType == AppThemeType.doodle) { } else if (themeType == AppThemeType.doodle) {
Color doodleColor = line.owner == Player.none ? Colors.black.withOpacity(0.05) : lineColor; Color doodleColor = line.owner == Player.none ? Colors.black.withOpacity(0.05) : lineColor;
@ -228,7 +210,6 @@ class BoardPainter extends CustomPainter {
} else if (themeType == AppThemeType.grimorio) { } else if (themeType == AppThemeType.grimorio) {
_drawGrimorioLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue); _drawGrimorioLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
} else if (themeType == AppThemeType.music) { } else if (themeType == AppThemeType.music) {
// Linee nere per la base nel tema musica
if (line.owner == Player.none) lineColor = Colors.black.withOpacity(0.4); if (line.owner == Player.none) lineColor = Colors.black.withOpacity(0.4);
canvas.drawLine(p1, p2, Paint()..color = lineColor..strokeWidth = isLastMove ? 6.0 + (2.0 * blinkValue) : 6.0..strokeCap = StrokeCap.round); canvas.drawLine(p1, p2, Paint()..color = lineColor..strokeWidth = isLastMove ? 6.0 + (2.0 * blinkValue) : 6.0..strokeCap = StrokeCap.round);
} else { } else {
@ -247,9 +228,7 @@ class BoardPainter extends CustomPainter {
for (var dot in activeDots) { for (var dot in activeDots) {
Offset pos = getScreenPos(dot.x, dot.y); Offset pos = getScreenPos(dot.x, dot.y);
if (themeType == AppThemeType.wood) { if (themeType == AppThemeType.cyberpunk) {
canvas.drawCircle(pos, 3.5, dotPaint..color = const Color(0xFF3E2723).withOpacity(0.2));
} else if (themeType == AppThemeType.cyberpunk) {
canvas.drawCircle(pos, 6.0, Paint()..color = theme.gridLine.withOpacity(0.3)); canvas.drawCircle(pos, 6.0, Paint()..color = theme.gridLine.withOpacity(0.3));
canvas.drawCircle(pos, 3.0, Paint()..color = Colors.white.withOpacity(0.5)); canvas.drawCircle(pos, 3.0, Paint()..color = Colors.white.withOpacity(0.5));
} else if (themeType == AppThemeType.doodle) { } else if (themeType == AppThemeType.doodle) {
@ -262,7 +241,6 @@ class BoardPainter extends CustomPainter {
Path crystal = Path()..moveTo(pos.dx, pos.dy - 5)..lineTo(pos.dx + 3, pos.dy)..lineTo(pos.dx, pos.dy + 5)..lineTo(pos.dx - 3, pos.dy)..close(); Path crystal = Path()..moveTo(pos.dx, pos.dy - 5)..lineTo(pos.dx + 3, pos.dy)..lineTo(pos.dx, pos.dy + 5)..lineTo(pos.dx - 3, pos.dy)..close();
canvas.drawPath(crystal, dotPaint..color = theme.gridLine.withOpacity(0.8)); canvas.drawPath(crystal, dotPaint..color = theme.gridLine.withOpacity(0.8));
} else if (themeType == AppThemeType.music) { } else if (themeType == AppThemeType.music) {
// Pallini (dots) neri per staccare dal fondo chiaro
canvas.drawCircle(pos, 4.5, dotPaint..color = Colors.black87); canvas.drawCircle(pos, 4.5, dotPaint..color = Colors.black87);
} else { } else {
canvas.drawCircle(pos, 5.0, dotPaint..color = theme.text.withOpacity(0.6)); canvas.drawCircle(pos, 5.0, dotPaint..color = theme.text.withOpacity(0.6));
@ -294,7 +272,6 @@ class BoardPainter extends CustomPainter {
..strokeCap = StrokeCap.round ..strokeCap = StrokeCap.round
..maskFilter = const MaskFilter.blur(BlurStyle.solid, 2.0); ..maskFilter = const MaskFilter.blur(BlurStyle.solid, 2.0);
// Effetto linea frammentata
canvas.drawLine(p1, p2, Paint()..color = Colors.cyan.withOpacity(0.2)..strokeWidth=6.0); canvas.drawLine(p1, p2, Paint()..color = Colors.cyan.withOpacity(0.2)..strokeWidth=6.0);
Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy); Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy);
@ -366,19 +343,6 @@ class BoardPainter extends CustomPainter {
canvas.drawPath(thread1, threadPaint); canvas.drawPath(thread2, threadPaint..color = Colors.white.withOpacity(0.5)); canvas.drawPath(thread1, threadPaint); canvas.drawPath(thread2, threadPaint..color = Colors.white.withOpacity(0.5));
} }
void _drawFlameBox(Canvas canvas, Rect baseRect, bool isRed) {
final rand = Random((baseRect.left + baseRect.top).toInt());
Offset center = baseRect.center; double w = baseRect.width * 0.35; double h = baseRect.height * 0.55; Offset bottomCenter = Offset(center.dx, center.dy + h * 0.5);
Color outerColor = isRed ? Colors.red.shade600.withOpacity(0.85) : Colors.blue.shade700.withOpacity(0.85); Color midColor = isRed ? Colors.orangeAccent : Colors.lightBlueAccent; Color coreColor = isRed ? Colors.yellowAccent : Colors.white;
canvas.drawOval(Rect.fromCenter(center: bottomCenter, width: w * 1.5, height: w * 0.5), Paint()..color = Colors.black.withOpacity(0.4)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0));
void drawFlameLayer(double scale, Color color, double tipOffsetX) {
Path path = Path(); double fw = w * scale; double fh = h * scale;
path.moveTo(bottomCenter.dx, bottomCenter.dy); path.cubicTo(bottomCenter.dx + fw, bottomCenter.dy, bottomCenter.dx + fw * 0.8, bottomCenter.dy - fh * 0.6, bottomCenter.dx + tipOffsetX, bottomCenter.dy - fh); path.cubicTo(bottomCenter.dx - fw * 0.8, bottomCenter.dy - fh * 0.6, bottomCenter.dx - fw, bottomCenter.dy, bottomCenter.dx, bottomCenter.dy);
canvas.drawPath(path, Paint()..color = color..style = PaintingStyle.fill..maskFilter = const MaskFilter.blur(BlurStyle.normal, 1.5));
}
double randomTipX = (rand.nextDouble() - 0.5) * w * 0.8; drawFlameLayer(1.0, outerColor, randomTipX); drawFlameLayer(0.65, midColor.withOpacity(0.9), randomTipX * 0.6); drawFlameLayer(0.35, coreColor.withOpacity(0.9), randomTipX * 0.2);
}
void _drawScribbleBox(Canvas canvas, Rect baseRect, Color color) { void _drawScribbleBox(Canvas canvas, Rect baseRect, Color color) {
final rand = Random((baseRect.left + baseRect.top).toInt()); final rand = Random((baseRect.left + baseRect.top).toInt());
final paint = Paint()..color = color.withOpacity(0.85)..style = PaintingStyle.stroke..strokeWidth = 3.5..strokeCap = StrokeCap.round..strokeJoin = StrokeJoin.round; final paint = Paint()..color = color.withOpacity(0.85)..style = PaintingStyle.stroke..strokeWidth = 3.5..strokeCap = StrokeCap.round..strokeJoin = StrokeJoin.round;
@ -388,14 +352,6 @@ class BoardPainter extends CustomPainter {
canvas.drawPath(path, paint); canvas.drawPath(path, paint);
} }
void _drawRealisticMatch(Canvas canvas, Offset p1, Offset p2, Color headColor, {bool isLastMove = false, double blinkValue = 0.0}) {
int seed = (p1.dx * 1000 + p1.dy).toInt(); Random rand = Random(seed); Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy).normalized(); double shrink = 8.0; Offset start = Offset(p1.dx + dir.x * shrink, p1.dy + dir.y * shrink); Offset end = Offset(p2.dx - dir.x * shrink, p2.dy - dir.y * shrink); start += Offset(rand.nextDouble() * 4 - 2, rand.nextDouble() * 4 - 2); end += Offset(rand.nextDouble() * 4 - 2, rand.nextDouble() * 4 - 2); bool headAtEnd = rand.nextBool(); Offset headPos = headAtEnd ? end : start; Offset tailPos = headAtEnd ? start : end; Vector2 matchDir = Vector2(headPos.dx - tailPos.dx, headPos.dy - tailPos.dy).normalized();
canvas.drawLine(tailPos + const Offset(4, 4), headPos + const Offset(4, 4), Paint()..color = Colors.black.withOpacity(0.6)..strokeWidth = 7.0..strokeCap = StrokeCap.round);
if (isLastMove) { canvas.drawCircle(headPos, 8.0 + (blinkValue * 6.0), Paint()..color = Colors.orangeAccent.withOpacity(0.6 * blinkValue)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)); }
canvas.drawLine(tailPos, headPos, Paint()..color = const Color(0xFF6D4C41)..strokeWidth = 7.0..strokeCap = StrokeCap.round); canvas.drawLine(tailPos, headPos, Paint()..color = const Color(0xFFEDC498)..strokeWidth = 4.0..strokeCap = StrokeCap.round); Offset burnPos = Offset(headPos.dx - matchDir.x * 8, headPos.dy - matchDir.y * 8); canvas.drawLine(burnPos, headPos, Paint()..color = const Color(0xFF2E1A14)..strokeWidth = 6.0..strokeCap = StrokeCap.round);
canvas.save(); canvas.translate(headPos.dx, headPos.dy); double angle = atan2(matchDir.y, matchDir.x); canvas.rotate(angle); Rect headOval = Rect.fromCenter(center: Offset.zero, width: 18.0, height: 13.0); canvas.drawOval(headOval.shift(const Offset(1, 2)), Paint()..color = Colors.black.withOpacity(0.6)); canvas.drawOval(headOval, Paint()..color = headColor); canvas.restore();
}
void _drawNeonLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) { void _drawNeonLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
double mainWidth = isConquered ? (isLastMove ? 6.0 + (blinkValue * 3.0) : 6.0) : 3.0; Color coreColor = isConquered ? (isLastMove ? Color.lerp(Colors.white, color, 1.0 - blinkValue)! : Colors.white.withOpacity(0.9)) : color.withOpacity(0.6); double mainWidth = isConquered ? (isLastMove ? 6.0 + (blinkValue * 3.0) : 6.0) : 3.0; Color coreColor = isConquered ? (isLastMove ? Color.lerp(Colors.white, color, 1.0 - blinkValue)! : Colors.white.withOpacity(0.9)) : color.withOpacity(0.6);
canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(isConquered ? (isLastMove ? 0.4 + (0.4 * blinkValue) : 0.4) : 0.2)..strokeWidth = mainWidth * 4..strokeCap = StrokeCap.round..maskFilter = MaskFilter.blur(BlurStyle.normal, isConquered ? 12.0 : 6.0)); canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(isConquered ? (isLastMove ? 0.4 + (0.4 * blinkValue) : 0.4) : 0.2)..strokeWidth = mainWidth * 4..strokeCap = StrokeCap.round..maskFilter = MaskFilter.blur(BlurStyle.normal, isConquered ? 12.0 : 6.0));

View file

@ -236,8 +236,6 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
return Container(decoration: BoxDecoration(color: Colors.black.withOpacity(0.9), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.purpleAccent, width: 2), boxShadow: [BoxShadow(color: Colors.purpleAccent.withOpacity(0.6), blurRadius: 15, spreadRadius: 0)]), child: content); return Container(decoration: BoxDecoration(color: Colors.black.withOpacity(0.9), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.purpleAccent, width: 2), boxShadow: [BoxShadow(color: Colors.purpleAccent.withOpacity(0.6), blurRadius: 15, spreadRadius: 0)]), child: content);
} else if (themeType == AppThemeType.doodle) { } else if (themeType == AppThemeType.doodle) {
return Container(decoration: BoxDecoration(color: const Color(0xFFF9F9F9), borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.black87, width: 3), boxShadow: const [BoxShadow(color: Colors.black26, offset: Offset(6, 6))]), child: content); return Container(decoration: BoxDecoration(color: const Color(0xFFF9F9F9), borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.black87, width: 3), boxShadow: const [BoxShadow(color: Colors.black26, offset: Offset(6, 6))]), child: content);
} else if (themeType == AppThemeType.wood) {
return Container(decoration: BoxDecoration(color: const Color(0xFF5D4037), borderRadius: BorderRadius.circular(15), border: Border.all(color: const Color(0xFF3E2723), width: 4), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.6), blurRadius: 15, offset: const Offset(0, 8))]), child: content);
} else if (themeType == AppThemeType.arcade) { } else if (themeType == AppThemeType.arcade) {
return Container(decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.zero, border: Border.all(color: Colors.greenAccent, width: 4)), child: content); return Container(decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.zero, border: Border.all(color: Colors.greenAccent, width: 4)), child: content);
} else if (themeType == AppThemeType.grimorio) { } else if (themeType == AppThemeType.grimorio) {
@ -290,7 +288,6 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
}); });
String? bgImage; String? bgImage;
if (themeType == AppThemeType.wood) bgImage = 'assets/images/wood_bg.jpg';
if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg'; if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg';
if (themeType == AppThemeType.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg'; if (themeType == AppThemeType.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg';
if (themeType == AppThemeType.music) bgImage = 'assets/images/music_bg.jpg'; if (themeType == AppThemeType.music) bgImage = 'assets/images/music_bg.jpg';
@ -601,7 +598,6 @@ class _WinnerVFXOverlayState extends State<WinnerVFXOverlay> with SingleTickerPr
List<Color> palette = [widget.winnerColor, widget.winnerColor.withOpacity(0.7), Colors.white]; List<Color> palette = [widget.winnerColor, widget.winnerColor.withOpacity(0.7), Colors.white];
if (widget.themeType == AppThemeType.cyberpunk) { palette.add(Colors.cyanAccent); palette.add(Colors.yellowAccent); } if (widget.themeType == AppThemeType.cyberpunk) { palette.add(Colors.cyanAccent); palette.add(Colors.yellowAccent); }
else if (widget.themeType == AppThemeType.doodle) { palette.add(const Color(0xFF00008B)); palette.add(Colors.redAccent); } else if (widget.themeType == AppThemeType.doodle) { palette.add(const Color(0xFF00008B)); palette.add(Colors.redAccent); }
else if (widget.themeType == AppThemeType.wood) { palette = [Colors.orangeAccent, Colors.yellow, Colors.red, Colors.white]; }
else if (widget.themeType == AppThemeType.arcade) { palette = [widget.winnerColor, Colors.white, Colors.greenAccent]; } else if (widget.themeType == AppThemeType.arcade) { palette = [widget.winnerColor, Colors.white, Colors.greenAccent]; }
else if (widget.themeType == AppThemeType.grimorio) { palette = [widget.winnerColor, Colors.deepPurpleAccent, Colors.white]; } else if (widget.themeType == AppThemeType.grimorio) { palette = [widget.winnerColor, Colors.deepPurpleAccent, Colors.white]; }
else if (widget.themeType == AppThemeType.music) { palette.add(Colors.pinkAccent); palette.add(Colors.cyanAccent); } else if (widget.themeType == AppThemeType.music) { palette.add(Colors.pinkAccent); palette.add(Colors.cyanAccent); }
@ -618,7 +614,6 @@ class _WinnerVFXOverlayState extends State<WinnerVFXOverlay> with SingleTickerPr
for (var p in _particles) { for (var p in _particles) {
p.x += p.vx; p.y += p.vy; p.x += p.vx; p.y += p.vy;
if (widget.themeType == AppThemeType.cyberpunk || widget.themeType == AppThemeType.music) { p.vy += 0.1; p.vx *= 0.98; p.vy *= 0.98; } if (widget.themeType == AppThemeType.cyberpunk || widget.themeType == AppThemeType.music) { p.vy += 0.1; p.vx *= 0.98; p.vy *= 0.98; }
else if (widget.themeType == AppThemeType.wood) { p.vy -= 0.2; p.x += math.sin(p.y * 0.05) * 2; }
else if (widget.themeType == AppThemeType.arcade) { p.vy += 0.3; p.spin = 0; p.angle = 0; } else if (widget.themeType == AppThemeType.arcade) { p.vy += 0.3; p.spin = 0; p.angle = 0; }
else if (widget.themeType == AppThemeType.grimorio) { p.vy -= 0.1; p.x += math.sin(p.y * 0.02) * 1.5; p.size *= 0.995; } else if (widget.themeType == AppThemeType.grimorio) { p.vy -= 0.1; p.x += math.sin(p.y * 0.02) * 1.5; p.size *= 0.995; }
else { p.vy += 0.5; } else { p.vy += 0.5; }
@ -645,8 +640,6 @@ class _VFXPainter extends CustomPainter {
if (themeType == AppThemeType.doodle) { if (themeType == AppThemeType.doodle) {
paint.style = PaintingStyle.stroke; paint.strokeWidth = 2.0; paint.style = PaintingStyle.stroke; paint.strokeWidth = 2.0;
if (p.type == 0) { canvas.drawCircle(Offset.zero, p.size, paint); } else { canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size*2, height: p.size*2), paint); } if (p.type == 0) { canvas.drawCircle(Offset.zero, p.size, paint); } else { canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size*2, height: p.size*2), paint); }
} else if (themeType == AppThemeType.wood) {
paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 3.0); canvas.drawCircle(Offset.zero, p.size, paint);
} else if (themeType == AppThemeType.arcade) { } else if (themeType == AppThemeType.arcade) {
canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size * 1.5, height: p.size * 1.5), paint); canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size * 1.5, height: p.size * 1.5), paint);
} else if (themeType == AppThemeType.grimorio) { } else if (themeType == AppThemeType.grimorio) {

View file

@ -13,7 +13,7 @@ import '../../core/app_colors.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
import '../../widgets/painters.dart'; import '../../widgets/painters.dart';
import '../../widgets/cyber_border.dart'; import '../../widgets/cyber_border.dart';
import '../../services/storage_service.dart'; // IMPORT AGGIUNTO import '../../services/storage_service.dart';
// =========================================================================== // ===========================================================================
// 1. DIALOGO MISSIONI (QUESTS) // 1. DIALOGO MISSIONI (QUESTS)
@ -159,17 +159,14 @@ class LeaderboardDialog extends StatelessWidget {
return Center(child: Text("Ancora nessun campione...", style: TextStyle(color: theme.text.withOpacity(0.5)))); return Center(child: Text("Ancora nessun campione...", style: TextStyle(color: theme.text.withOpacity(0.5))));
} }
// 1. ESTRAIAMO TUTTI I DOCUMENTI
final rawDocs = snapshot.data!.docs; final rawDocs = snapshot.data!.docs;
// 2. APPLICHIAMO IL FILTRO PER NASCONDERE "PAOLO"
final filteredDocs = rawDocs.where((doc) { final filteredDocs = rawDocs.where((doc) {
var data = doc.data() as Map<String, dynamic>; var data = doc.data() as Map<String, dynamic>;
String name = (data['name'] ?? '').toString().toUpperCase(); String name = (data['name'] ?? '').toString().toUpperCase();
return name != 'PAOLO'; return name != 'PAOLO';
}).toList(); }).toList();
// 3. SE DOPO IL FILTRO NON C'E' NESSUNO
if (filteredDocs.isEmpty) { if (filteredDocs.isEmpty) {
return Center(child: Text("Ancora nessun campione...", style: TextStyle(color: theme.text.withOpacity(0.5)))); return Center(child: Text("Ancora nessun campione...", style: TextStyle(color: theme.text.withOpacity(0.5))));
} }
@ -184,7 +181,6 @@ class LeaderboardDialog extends StatelessWidget {
bool isMe = doc.id == myUid; bool isMe = doc.id == myUid;
String playerName = data['name'] ?? 'Unknown'; String playerName = data['name'] ?? 'Unknown';
// Avvolto in StatefulBuilder per gestire la stella dei preferiti
return StatefulBuilder( return StatefulBuilder(
builder: (context, setStateItem) { builder: (context, setStateItem) {
bool isFav = StorageService.instance.isFavorite(doc.id); bool isFav = StorageService.instance.isFavorite(doc.id);
@ -209,7 +205,6 @@ class LeaderboardDialog extends StatelessWidget {
Text("${data['xp'] ?? 0} XP", style: TextStyle(color: theme.text.withOpacity(0.6), fontSize: 10)), Text("${data['xp'] ?? 0} XP", style: TextStyle(color: theme.text.withOpacity(0.6), fontSize: 10)),
], ],
), ),
// IL BOTTONE DEI PREFERITI
if (!isMe) ...[ if (!isMe) ...[
const SizedBox(width: 8), const SizedBox(width: 8),
GestureDetector( GestureDetector(
@ -301,11 +296,6 @@ class TutorialDialog extends StatelessWidget {
jokerLabel = "BOT:"; jokerLabel = "BOT:";
swapLabel = "NETWORK:"; swapLabel = "NETWORK:";
blockLabel = "FIREWALL:"; blockLabel = "FIREWALL:";
} else if (themeType == AppThemeType.wood) {
goldLabel = "GEMMA:";
bombLabel = "FUOCO:";
jokerLabel = "CHIAVE:";
blockLabel = "DIVIETO:";
} else if (themeType == AppThemeType.doodle) { } else if (themeType == AppThemeType.doodle) {
bombLabel = "VIRUS:"; bombLabel = "VIRUS:";
} }

View file

@ -34,9 +34,6 @@ import '../../widgets/home_buttons.dart';
import '../../widgets/custom_settings_button.dart'; import '../../widgets/custom_settings_button.dart';
import 'dialog.dart'; import 'dialog.dart';
// ===========================================================================
// CLASSE PRINCIPALE HOME
// ===========================================================================
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
@ -69,11 +66,20 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_checkPlayerName(); _checkPlayerName();
StorageService.instance.syncLeaderboard(); StorageService.instance.syncLeaderboard();
_checkThemeSafety();
}); });
_checkClipboardForInvite(); _checkClipboardForInvite();
_initDeepLinks(); _initDeepLinks();
} }
void _checkThemeSafety() {
String themeStr = StorageService.instance.getTheme();
bool exists = AppThemeType.values.any((e) => e.toString() == themeStr);
if (!exists) {
context.read<ThemeManager>().setTheme(AppThemeType.doodle);
}
}
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
@ -330,7 +336,6 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
); );
} }
// --- FINESTRA DI REGISTRAZIONE/LOGIN ---
void _showNameDialog() { void _showNameDialog() {
final TextEditingController nameController = TextEditingController(text: StorageService.instance.playerName); final TextEditingController nameController = TextEditingController(text: StorageService.instance.playerName);
final TextEditingController passController = TextEditingController(); final TextEditingController passController = TextEditingController();
@ -457,7 +462,6 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
// MESSAGGIO DI ERRORE
if (_errorMessage.isNotEmpty) if (_errorMessage.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.only(bottom: 10),
@ -538,7 +542,6 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
// MESSAGGIO DI ERRORE
if (_errorMessage.isNotEmpty) if (_errorMessage.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.only(bottom: 10),
@ -590,7 +593,7 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
void _showMatchSetupDialog(bool isVsCPU) { void _showMatchSetupDialog(bool isVsCPU) {
int localRadius = 4; ArenaShape localShape = ArenaShape.classic; bool localTimeMode = true; int localRadius = 4; ArenaShape localShape = ArenaShape.classic; bool localTimeMode = true;
bool isChaosUnlocked = StorageService.instance.playerLevel >= 10; bool isChaosUnlocked = StorageService.instance.playerLevel >= 7;
final loc = AppLocalizations.of(context)!; final loc = AppLocalizations.of(context)!;
showDialog( showDialog(
@ -741,14 +744,9 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
); );
} }
// ===========================================================================
// INTERFACCIA PRINCIPALE
// ===========================================================================
BoxDecoration _glassBoxDecoration(ThemeColors theme, AppThemeType themeType) { BoxDecoration _glassBoxDecoration(ThemeColors theme, AppThemeType themeType) {
return BoxDecoration( return BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.white : null, color: themeType == AppThemeType.doodle ? Colors.white : null,
// Sfumatura bianca inserita come richiesto!
gradient: themeType == AppThemeType.doodle ? null : LinearGradient( gradient: themeType == AppThemeType.doodle ? null : LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
@ -772,13 +770,11 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
Color inkColor = const Color(0xFF111122); Color inkColor = const Color(0xFF111122);
return Padding( return Padding(
// Padding ridotto al minimo
padding: const EdgeInsets.only(top: 5.0, left: 15.0, right: 15.0, bottom: 10.0), padding: const EdgeInsets.only(top: 5.0, left: 15.0, right: 15.0, bottom: 10.0),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// --- SINISTRA: RIQUADRO GIOCATORE ---
GestureDetector( GestureDetector(
onTap: _showNameDialog, onTap: _showNameDialog,
child: Container( child: Container(
@ -806,7 +802,6 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
), ),
), ),
// --- DESTRA: RIQUADRO STATISTICHE E AUDIO ---
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
decoration: _glassBoxDecoration(theme, themeType), decoration: _glassBoxDecoration(theme, themeType),
@ -825,7 +820,6 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
Container(width: 1, height: 20, color: (themeType == AppThemeType.doodle ? inkColor : Colors.white).withOpacity(0.2)), Container(width: 1, height: 20, color: (themeType == AppThemeType.doodle ? inkColor : Colors.white).withOpacity(0.2)),
const SizedBox(width: 12), const SizedBox(width: 12),
// --- ICONA VOLUME REINTEGRATA ---
AnimatedBuilder( AnimatedBuilder(
animation: AudioService.instance, animation: AudioService.instance,
builder: (context, child) { builder: (context, child) {
@ -868,7 +862,6 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
final loc = AppLocalizations.of(context)!; final loc = AppLocalizations.of(context)!;
String? bgImage; String? bgImage;
if (themeType == AppThemeType.wood) bgImage = 'assets/images/wood_bg.jpg';
if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg'; if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg';
if (themeType == AppThemeType.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg'; if (themeType == AppThemeType.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg';
if (themeType == AppThemeType.music) bgImage = 'assets/images/music_bg.jpg'; if (themeType == AppThemeType.music) bgImage = 'assets/images/music_bg.jpg';
@ -885,10 +878,8 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
Widget uiContent = SafeArea( Widget uiContent = SafeArea(
child: Column( child: Column(
children: [ children: [
// 1. TOP BAR (Sempre visibile in alto)
_buildTopBar(context, theme, themeType, playerName, playerLevel), _buildTopBar(context, theme, themeType, playerName, playerLevel),
// 2. CONTENUTO SCORREVOLE (Logo + Bottoni) con altezze dinamiche!
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
@ -927,9 +918,7 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
color: themeType == AppThemeType.doodle ? inkColor : theme.text, color: themeType == AppThemeType.doodle ? inkColor : theme.text,
letterSpacing: 10 * vScale, letterSpacing: 10 * vScale,
shadows: themeType == AppThemeType.doodle shadows: themeType == AppThemeType.doodle
// Ombra chiara se il testo è scuro
? [const Shadow(color: Colors.white, offset: Offset(2.5, 2.5), blurRadius: 2), const Shadow(color: Colors.white, offset: Offset(-2.5, -2.5), blurRadius: 2)] ? [const Shadow(color: Colors.white, offset: Offset(2.5, 2.5), blurRadius: 2), const Shadow(color: Colors.white, offset: Offset(-2.5, -2.5), blurRadius: 2)]
// Ombra scura e visibile per tutti gli altri temi
: [Shadow(color: Colors.black.withOpacity(0.8), offset: const Offset(3, 4), blurRadius: 8), Shadow(color: theme.playerBlue.withOpacity(0.4), offset: const Offset(0, 0), blurRadius: 20)] : [Shadow(color: Colors.black.withOpacity(0.8), offset: const Offset(3, 4), blurRadius: 8), Shadow(color: theme.playerBlue.withOpacity(0.4), offset: const Offset(0, 0), blurRadius: 20)]
)) ))
), ),
@ -939,7 +928,6 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
), ),
SizedBox(height: 40 * vScale), SizedBox(height: 40 * vScale),
// --- MENU IN BASE AL TEMA ---
if (themeType == AppThemeType.music) ...[ if (themeType == AppThemeType.music) ...[
MusicCassetteCard(title: loc.onlineTitle, subtitle: loc.onlineSub, neonColor: Colors.blueAccent, angle: -0.04, leftIcon: FontAwesomeIcons.sliders, rightIcon: FontAwesomeIcons.globe, themeType: themeType, onTap: () { Navigator.push(context, MaterialPageRoute(builder: (_) => const LobbyScreen())); }), MusicCassetteCard(title: loc.onlineTitle, subtitle: loc.onlineSub, neonColor: Colors.blueAccent, angle: -0.04, leftIcon: FontAwesomeIcons.sliders, rightIcon: FontAwesomeIcons.globe, themeType: themeType, onTap: () { Navigator.push(context, MaterialPageRoute(builder: (_) => const LobbyScreen())); }),
SizedBox(height: 12 * vScale), SizedBox(height: 12 * vScale),

View file

@ -11,12 +11,13 @@ import 'package:firebase_auth/firebase_auth.dart';
import '../../logic/game_controller.dart'; import '../../logic/game_controller.dart';
import '../../models/game_board.dart'; import '../../models/game_board.dart';
import '../../core/theme_manager.dart'; import '../../core/theme_manager.dart';
import '../../core/app_colors.dart'; // L'import mancante! import '../../core/app_colors.dart';
import '../../services/multiplayer_service.dart'; import '../../services/multiplayer_service.dart';
import '../../services/storage_service.dart'; import '../../services/storage_service.dart';
import '../game/game_screen.dart'; import '../game/game_screen.dart';
import '../../widgets/painters.dart'; import '../../widgets/painters.dart';
import 'lobby_widgets.dart'; // Importa i widget separati import '../../widgets/cyber_border.dart'; // <--- ECCO L'IMPORT MANCANTE!
import 'lobby_widgets.dart';
class LobbyScreen extends StatefulWidget { class LobbyScreen extends StatefulWidget {
final String? initialRoomCode; final String? initialRoomCode;
@ -251,7 +252,7 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
], ],
); );
if (themeType == AppThemeType.cyberpunk) { if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) {
dialogContent = AnimatedCyberBorder(child: dialogContent); dialogContent = AnimatedCyberBorder(child: dialogContent);
} else { } else {
dialogContent = Container( dialogContent = Container(
@ -320,13 +321,14 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
final theme = themeManager.currentColors; final theme = themeManager.currentColors;
String? bgImage; String? bgImage;
if (themeType == AppThemeType.wood) bgImage = 'assets/images/wood_bg.jpg';
if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg'; if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg';
if (themeType == AppThemeType.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg'; if (themeType == AppThemeType.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg';
if (themeType == AppThemeType.music) bgImage = 'assets/images/music_bg.jpg';
if (themeType == AppThemeType.arcade) bgImage = 'assets/images/arcade.jpg';
if (themeType == AppThemeType.grimorio) bgImage = 'assets/images/grimorio.jpg';
bool isChaosUnlocked = true; bool isChaosUnlocked = StorageService.instance.playerLevel >= 7;
// --- PANNELLO IMPOSTAZIONI STANZA ---
Widget hostPanel = Transform.rotate( Widget hostPanel = Transform.rotate(
angle: themeType == AppThemeType.doodle ? 0.01 : 0, angle: themeType == AppThemeType.doodle ? 0.01 : 0,
child: Container( child: Container(
@ -407,7 +409,7 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
), ),
); );
if (themeType == AppThemeType.cyberpunk) { if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) {
hostPanel = AnimatedCyberBorder(child: hostPanel); hostPanel = AnimatedCyberBorder(child: hostPanel);
} }
@ -427,7 +429,7 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
Expanded( Expanded(
child: Text("MULTIPLAYER", textAlign: TextAlign.center, style: getLobbyTextStyle(themeType, TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2))), child: Text("MULTIPLAYER", textAlign: TextAlign.center, style: getLobbyTextStyle(themeType, TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2))),
), ),
const SizedBox(width: 48), // Bilanciamento const SizedBox(width: 48),
], ],
), ),
const SizedBox(height: 20), const SizedBox(height: 20),

View file

@ -3,11 +3,9 @@
// =========================================================================== // ===========================================================================
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import '../../models/game_board.dart';
import '../../core/theme_manager.dart'; import '../../core/theme_manager.dart';
import '../../core/app_colors.dart'; import '../../core/app_colors.dart';
@ -84,7 +82,7 @@ class NeonShapeButton extends StatelessWidget {
children: [ children: [
Icon(isLocked ? Icons.lock : icon, color: isSelected ? Colors.white : doodleColor, size: 20), Icon(isLocked ? Icons.lock : icon, color: isSelected ? Colors.white : doodleColor, size: 20),
const SizedBox(height: 2), const SizedBox(height: 2),
FittedBox(fit: BoxFit.scaleDown, child: Text(isLocked ? "Liv. 10" : label, style: getLobbyTextStyle(themeType, TextStyle(color: isSelected ? Colors.white : doodleColor, fontSize: 9, fontWeight: FontWeight.w900, letterSpacing: 0.2)))), FittedBox(fit: BoxFit.scaleDown, child: Text(isLocked ? "Liv. 7" : label, style: getLobbyTextStyle(themeType, TextStyle(color: isSelected ? Colors.white : doodleColor, fontSize: 9, fontWeight: FontWeight.w900, letterSpacing: 0.2)))),
], ],
), ),
), ),
@ -127,7 +125,7 @@ class NeonShapeButton extends StatelessWidget {
children: [ children: [
Icon(isLocked ? Icons.lock : icon, color: isLocked ? Colors.grey.withOpacity(0.5) : (isSelected ? Colors.white : theme.text.withOpacity(0.6)), size: 20), Icon(isLocked ? Icons.lock : icon, color: isLocked ? Colors.grey.withOpacity(0.5) : (isSelected ? Colors.white : theme.text.withOpacity(0.6)), size: 20),
const SizedBox(height: 4), const SizedBox(height: 4),
FittedBox(fit: BoxFit.scaleDown, child: Text(isLocked ? "Liv. 10" : label, style: getLobbyTextStyle(themeType, TextStyle(color: isLocked ? Colors.grey.withOpacity(0.5) : (isSelected ? Colors.white : theme.text.withOpacity(0.6)), fontSize: 9, fontWeight: isSelected ? FontWeight.w900 : FontWeight.bold)))), FittedBox(fit: BoxFit.scaleDown, child: Text(isLocked ? "Liv. 7" : label, style: getLobbyTextStyle(themeType, TextStyle(color: isLocked ? Colors.grey.withOpacity(0.5) : (isSelected ? Colors.white : theme.text.withOpacity(0.6)), fontSize: 9, fontWeight: isSelected ? FontWeight.w900 : FontWeight.bold)))),
], ],
), ),
), ),
@ -390,7 +388,7 @@ class NeonPrivacySwitch extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(isPublic ? 'STANZA PUBBLICA' : 'STANZA PRIVATA', style: getLobbyTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : theme.text.withOpacity(0.8), fontWeight: FontWeight.w900, fontSize: 10, letterSpacing: 1.0)))), FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(isPublic ? 'STANZA PUBBLICA' : 'STANZA PRIVATA', style: getLobbyTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : theme.text.withOpacity(0.8), fontWeight: FontWeight.w900, fontSize: 10, letterSpacing: 1.0)))),
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(isPublic ? 'In bacheca' : 'Invita con codice', style: getLobbyTextStyle(themeType, TextStyle(color: isPublic ? Colors.greenAccent.shade200 : theme.playerRed.withOpacity(0.7), fontSize: 9, fontWeight: FontWeight.bold)))), FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(isPublic ? 'Tutti ti vedono' : 'Solo con Codice', style: getLobbyTextStyle(themeType, TextStyle(color: isPublic ? Colors.greenAccent.shade200 : theme.playerRed.withOpacity(0.7), fontSize: 9, fontWeight: FontWeight.bold)))),
], ],
), ),
), ),
@ -558,59 +556,3 @@ class NeonActionButton extends StatelessWidget {
); );
} }
} }
class AnimatedCyberBorder extends StatefulWidget {
final Widget child;
const AnimatedCyberBorder({super.key, required this.child});
@override
State<AnimatedCyberBorder> createState() => _AnimatedCyberBorderState();
}
class _AnimatedCyberBorderState extends State<AnimatedCyberBorder> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(seconds: 3))..repeat(); }
@override
void dispose() { _controller.dispose(); super.dispose(); }
@override
Widget build(BuildContext context) {
final theme = context.watch<ThemeManager>().currentColors;
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return CustomPaint(
painter: _CyberBorderPainter(animationValue: _controller.value, color1: theme.playerBlue, color2: theme.playerRed),
child: Container(
decoration: BoxDecoration(color: Colors.transparent, borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.15), blurRadius: 20, spreadRadius: 2)]),
padding: const EdgeInsets.all(3),
child: widget.child,
),
);
},
child: widget.child,
);
}
}
class _CyberBorderPainter extends CustomPainter {
final double animationValue;
final Color color1;
final Color color2;
_CyberBorderPainter({required this.animationValue, required this.color1, required this.color2});
@override
void paint(Canvas canvas, Size size) {
final rect = Offset.zero & size;
final RRect rrect = RRect.fromRectAndRadius(rect, const Radius.circular(20));
final Paint paint = Paint()
..shader = SweepGradient(colors: [color1, color2, color1, color2, color1], stops: const [0.0, 0.25, 0.5, 0.75, 1.0], transform: GradientRotation(animationValue * 2 * math.pi)).createShader(rect)
..style = PaintingStyle.stroke
..strokeWidth = 3.0
..maskFilter = const MaskFilter.blur(BlurStyle.solid, 3);
canvas.drawRRect(rrect, paint);
}
@override
bool shouldRepaint(covariant _CyberBorderPainter oldDelegate) => oldDelegate.animationValue != animationValue;
}

View file

@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
import '../../core/theme_manager.dart'; import '../../core/theme_manager.dart';
import '../../core/app_colors.dart'; import '../../core/app_colors.dart';
import '../../services/storage_service.dart'; import '../../services/storage_service.dart';
import '../../widgets/painters.dart';
class SettingsScreen extends StatefulWidget { class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key}); const SettingsScreen({super.key});
@ -20,72 +21,104 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>(); final themeManager = context.watch<ThemeManager>();
final theme = themeManager.currentColors; final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
int playerLevel = StorageService.instance.playerLevel; int playerLevel = StorageService.instance.playerLevel;
final double screenHeight = MediaQuery.of(context).size.height;
final double vScale = (screenHeight / 920.0).clamp(0.7, 1.2);
return Scaffold( return Scaffold(
backgroundColor: theme.background, backgroundColor: theme.background,
extendBodyBehindAppBar: true,
appBar: AppBar( appBar: AppBar(
title: Text("SELEZIONA TEMA", style: TextStyle(fontWeight: FontWeight.bold, color: theme.text)), toolbarHeight: 80 * vScale,
title: Text(
"SELEZIONA TEMA",
style: getSharedTextStyle(themeType, TextStyle(
fontWeight: FontWeight.w900,
color: Colors.white,
letterSpacing: 2.0,
fontSize: 20 * vScale,
shadows: [Shadow(color: Colors.black.withOpacity(0.8), blurRadius: 5, offset: const Offset(2, 2))]
))
),
centerTitle: true,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
elevation: 0, elevation: 0,
iconTheme: IconThemeData(color: theme.text), iconTheme: IconThemeData(color: Colors.white, size: 28 * vScale),
), ),
body: ListView( body: Stack(
padding: const EdgeInsets.all(20),
children: [ children: [
_ThemeCard( Container(color: themeType == AppThemeType.doodle ? Colors.white : theme.background),
title: "Quaderno (Doodle)",
subtitle: "Sfondo a quadretti, tratto a penna", Positioned.fill(
type: AppThemeType.doodle, child: Container(
previewColors: AppColors.doodle, decoration: BoxDecoration(
requiredLevel: 1, image: DecorationImage(
currentLevel: playerLevel, image: const AssetImage('assets/images/sfondo_temi.jpg'),
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.6), BlendMode.darken),
),
),
),
), ),
const SizedBox(height: 15),
_ThemeCard( ListView(
title: "Legno & Fiammiferi", padding: EdgeInsets.only(top: 120 * vScale, left: 20 * vScale, right: 20 * vScale, bottom: 40 * vScale),
subtitle: "Tavolo di legno, linee come fiammiferi", physics: const BouncingScrollPhysics(),
type: AppThemeType.wood, children: [
previewColors: AppColors.wood, _ThemeCard(
requiredLevel: 3, title: "Quaderno",
currentLevel: playerLevel, subtitle: "Sfondo a quadretti, tratto a penna",
), type: AppThemeType.doodle,
const SizedBox(height: 15), previewColors: AppColors.doodle,
_ThemeCard( requiredLevel: 1,
title: "Cyberpunk", currentLevel: playerLevel,
subtitle: "Nero profondo, luci al neon", vScale: vScale,
type: AppThemeType.cyberpunk, ),
previewColors: AppColors.cyberpunk, SizedBox(height: 25 * vScale),
requiredLevel: 7, _ThemeCard(
currentLevel: playerLevel, title: "Cyberpunk",
), subtitle: "Nero profondo, luci al neon",
const SizedBox(height: 15), type: AppThemeType.cyberpunk,
_ThemeCard( previewColors: AppColors.cyberpunk,
title: "8-Bit Arcade", requiredLevel: 3,
subtitle: "Sale giochi, fosfori verdi e pixel", currentLevel: playerLevel,
type: AppThemeType.arcade, vScale: vScale,
previewColors: AppColors.arcade, ),
requiredLevel: 10, SizedBox(height: 25 * vScale),
currentLevel: playerLevel, _ThemeCard(
), title: "8-Bit Arcade",
const SizedBox(height: 15), subtitle: "Sale giochi, fosfori verdi e pixel",
_ThemeCard( type: AppThemeType.arcade,
title: "Grimorio", previewColors: AppColors.arcade,
subtitle: "Incantesimi antichi, rune magiche", requiredLevel: 7,
type: AppThemeType.grimorio, currentLevel: playerLevel,
previewColors: AppColors.grimorio, vScale: vScale,
requiredLevel: 15, ),
currentLevel: playerLevel, SizedBox(height: 25 * vScale),
), _ThemeCard(
const SizedBox(height: 15), title: "Grimorio",
_ThemeCard( subtitle: "Incantesimi antichi, rune magiche",
title: "Musica", type: AppThemeType.grimorio,
subtitle: "Vinili, cassette e vibrazioni sonore", previewColors: AppColors.grimorio,
type: AppThemeType.music, requiredLevel: 10,
previewColors: AppColors.music, currentLevel: playerLevel,
requiredLevel: 20, // Tema Esclusivo di Livello 20! vScale: vScale,
currentLevel: playerLevel, ),
SizedBox(height: 25 * vScale),
_ThemeCard(
title: "Musica",
subtitle: "Vinili, cassette e vibrazioni sonore",
type: AppThemeType.music,
previewColors: AppColors.music,
requiredLevel: 15,
currentLevel: playerLevel,
vScale: vScale,
),
SizedBox(height: 40 * vScale),
],
), ),
], ],
), ),
@ -100,6 +133,7 @@ class _ThemeCard extends StatelessWidget {
final ThemeColors previewColors; final ThemeColors previewColors;
final int requiredLevel; final int requiredLevel;
final int currentLevel; final int currentLevel;
final double vScale;
const _ThemeCard({ const _ThemeCard({
required this.title, required this.title,
@ -108,6 +142,7 @@ class _ThemeCard extends StatelessWidget {
required this.previewColors, required this.previewColors,
required this.requiredLevel, required this.requiredLevel,
required this.currentLevel, required this.currentLevel,
required this.vScale,
}); });
@override @override
@ -116,6 +151,33 @@ class _ThemeCard extends StatelessWidget {
bool isSelected = themeManager.currentThemeType == type; bool isSelected = themeManager.currentThemeType == type;
bool isLocked = currentLevel < requiredLevel; bool isLocked = currentLevel < requiredLevel;
String? bgImage;
if (type == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg';
if (type == AppThemeType.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg';
if (type == AppThemeType.music) bgImage = 'assets/images/music_bg.jpg';
if (type == AppThemeType.arcade) bgImage = 'assets/images/arcade.jpg';
if (type == AppThemeType.grimorio) bgImage = 'assets/images/grimorio.jpg';
Border border;
// OMBRA MARCATA PER DARE SPESSORE AL TASTO (Offset 0, 10)
List<BoxShadow> shadows = [
BoxShadow(color: Colors.black.withOpacity(0.8), offset: const Offset(0, 10), blurRadius: 15)
];
if (type == AppThemeType.doodle) {
border = Border.all(color: isSelected ? previewColors.playerBlue : const Color(0xFF111122).withOpacity(0.8), width: isSelected ? 4 : 2);
if (isSelected) shadows.add(const BoxShadow(color: Color(0xFF111122), offset: Offset(4, 5)));
} else if (type == AppThemeType.cyberpunk || type == AppThemeType.music) {
border = Border.all(color: isSelected ? previewColors.playerBlue : previewColors.gridLine.withOpacity(0.8), width: isSelected ? 3 : 1.5);
if (isSelected) shadows.add(BoxShadow(color: previewColors.playerBlue.withOpacity(0.8), blurRadius: 25, spreadRadius: 3));
} else if (type == AppThemeType.arcade) {
border = Border.all(color: isSelected ? previewColors.gridLine : Colors.white54, width: isSelected ? 4 : 2);
if (isSelected) shadows.add(BoxShadow(color: previewColors.gridLine.withOpacity(0.5), offset: const Offset(4, 4)));
} else {
border = Border.all(color: isSelected ? Colors.amber : previewColors.gridLine.withOpacity(0.8), width: isSelected ? 3 : 1.5);
if (isSelected) shadows.add(BoxShadow(color: Colors.amber.withOpacity(0.6), blurRadius: 20));
}
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
if (isLocked) { if (isLocked) {
@ -130,63 +192,128 @@ class _ThemeCard extends StatelessWidget {
); );
return; return;
} }
themeManager.setTheme(type); themeManager.setTheme(type);
Navigator.pop(context); Navigator.pop(context);
}, },
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.all(20), height: 140 * vScale, // Leggermente più alto per ospitare la cornice del testo
padding: EdgeInsets.symmetric(horizontal: 20 * vScale, vertical: 15 * vScale),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isLocked ? previewColors.background.withOpacity(0.4) : previewColors.background, color: isLocked ? Colors.black87 : previewColors.background,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all( border: border,
color: isSelected boxShadow: shadows, // L'ombra per lo spessore viene applicata qui
? previewColors.playerBlue image: bgImage != null ? DecorationImage(
: (isLocked ? Colors.grey.withOpacity(0.3) : previewColors.gridLine.withOpacity(0.5)), image: AssetImage(bgImage!),
width: isSelected ? 4 : 2, fit: BoxFit.cover,
), // Scuriamo leggermente di più le card per far risaltare la targa chiara
boxShadow: isSelected ? [BoxShadow(color: previewColors.playerBlue.withOpacity(0.4), blurRadius: 10, spreadRadius: 2)] : [], colorFilter: type == AppThemeType.doodle
? ColorFilter.mode(Colors.white.withOpacity(isLocked ? 0.9 : 0.4), BlendMode.lighten)
: ColorFilter.mode(Colors.black.withOpacity(isLocked ? 0.85 : 0.5), BlendMode.darken),
) : null,
), ),
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
Opacity( Opacity(
opacity: isLocked ? 0.25 : 1.0, opacity: isLocked ? 0.3 : 1.0,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded( Expanded(
child: Column( // --- LA NUOVA CORNICE CHIARA PER IL TESTO ---
crossAxisAlignment: CrossAxisAlignment.start, child: Container(
children: [ padding: EdgeInsets.symmetric(horizontal: 15 * vScale, vertical: 10 * vScale),
Text(title, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: previewColors.text)), decoration: BoxDecoration(
Text(subtitle, style: TextStyle(fontSize: 14, color: previewColors.text.withOpacity(0.7))), color: Colors.white.withOpacity(0.9), // Sfondo chiaro semitrasparente
], borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.3), blurRadius: 8, offset: const Offset(2, 4))
]
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
title,
style: getSharedTextStyle(
type,
TextStyle(
fontSize: (type == AppThemeType.arcade ? 15 : 22) * vScale,
fontWeight: FontWeight.w900,
color: const Color(0xFF111122), // Testo scuro per contrastare lo sfondo chiaro
letterSpacing: type == AppThemeType.music ? 1.5 : 0.5,
)
)
),
),
SizedBox(height: 4 * vScale),
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
subtitle,
style: getSharedTextStyle(
type,
TextStyle(
fontSize: (type == AppThemeType.arcade ? 8 : 12) * vScale,
color: const Color(0xFF333344), // Grigio scuro per il sottotitolo
fontWeight: FontWeight.bold,
)
)
),
),
],
),
), ),
), ),
Container(width: 20, height: 20, decoration: BoxDecoration(color: previewColors.playerRed, shape: BoxShape.circle, boxShadow: [BoxShadow(color: previewColors.playerRed.withOpacity(0.5), blurRadius: 4)])), SizedBox(width: 15 * vScale),
const SizedBox(width: 10), Container(
Container(width: 20, height: 20, decoration: BoxDecoration(color: previewColors.playerBlue, shape: BoxShape.circle, boxShadow: [BoxShadow(color: previewColors.playerBlue.withOpacity(0.5), blurRadius: 4)])), width: 28 * vScale, height: 28 * vScale,
decoration: BoxDecoration(
color: previewColors.playerRed,
shape: type == AppThemeType.arcade ? BoxShape.rectangle : BoxShape.circle,
border: Border.all(color: Colors.white, width: 2), // Bordino bianco per far risaltare il colore
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 5, offset: const Offset(1, 2))]
)
),
SizedBox(width: 12 * vScale),
Container(
width: 28 * vScale, height: 28 * vScale,
decoration: BoxDecoration(
color: previewColors.playerBlue,
shape: type == AppThemeType.arcade ? BoxShape.rectangle : BoxShape.circle,
border: Border.all(color: Colors.white, width: 2), // Bordino bianco per far risaltare il colore
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 5, offset: const Offset(1, 2))]
)
),
], ],
), ),
), ),
if (isLocked) if (isLocked)
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: EdgeInsets.symmetric(horizontal: 16 * vScale, vertical: 10 * vScale),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withOpacity(0.85), color: Colors.black.withOpacity(0.95),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white.withOpacity(0.2), width: 1.5), border: Border.all(color: previewColors.playerRed.withOpacity(0.8), width: 2),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 10, offset: const Offset(0, 4))], boxShadow: [BoxShadow(color: previewColors.playerRed.withOpacity(0.5), blurRadius: 20)],
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.lock_rounded, color: Colors.white, size: 20), Icon(Icons.lock_rounded, color: Colors.white, size: 20 * vScale),
const SizedBox(width: 8), SizedBox(width: 8 * vScale),
Text( Text(
"LIV. $requiredLevel", "LIV. $requiredLevel",
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 2) style: getSharedTextStyle(type, TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 16 * vScale, letterSpacing: 2))
), ),
], ],
), ),

312
public/report.html Normal file
View file

@ -0,0 +1,312 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Report Statistiche 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;}
/* 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; }
.error { color: #e74c3c; font-weight: bold; font-size: 14px; margin-top: -5px; margin-bottom: 15px; }
/* 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;}
.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; }
.filter-section { background: #eef2f5; padding: 20px; border-radius: 8px; margin-bottom: 30px; display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end; }
.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 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: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;}
.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.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; }
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; }
/* 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%; }
.badge { padding: 4px 8px; border-radius: 4px; color: white; font-weight: bold; font-size: 12px; }
.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;}
/* MOBILE */
@media (max-width: 768px) {
body { padding: 10px; }
.container { padding: 15px; }
.header-top { flex-direction: column; text-align: center; gap: 15px; }
.filter-section { flex-direction: column; align-items: stretch; gap: 10px; padding: 15px;}
.form-group { min-width: 100%; }
.btn-filtra, .btn-reset { width: 100%; margin-top: 5px; height: auto; padding: 12px;}
.dashboard { flex-direction: column; gap: 15px; }
.card { min-width: 100%; }
table, thead, tbody, th, td, tr { display: block; width: 100%; box-sizing: border-box; }
thead { display: none; }
tr { margin-bottom: 15px; border: 1px solid #ddd; border-radius: 8px; overflow: hidden; background: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
td { display: flex; flex-direction: column; text-align: left; border-bottom: 1px solid #eee; padding: 12px 15px; position: relative; width: 100%; }
td::before { content: attr(data-label); font-weight: bold; margin-bottom: 5px; color: #34495e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
td:last-child { border-bottom: none; }
}
</style>
</head>
<body>
<div id="login-view">
<div class="login-box">
<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>
</form>
</div>
</div>
<div id="dashboard-view">
<div class="container">
<div class="header-top">
<h1>📊 Report Statistiche TetraQ</h1>
<button class="btn-logout" onclick="logout()">Esci 🚪</button>
</div>
<form class="filter-section" id="filter-form">
<div class="form-group">
<label for="data_da">Data Da:</label>
<input type="date" id="data_da">
</div>
<div class="form-group">
<label for="data_a">Data A:</label>
<input type="date" id="data_a">
</div>
<div class="form-group">
<label for="os">Sistema Operativo:</label>
<select id="os">
<option value="">Tutti i sistemi</option>
<option value="iOS">Apple iOS</option>
<option value="Android">Google Android</option>
<option value="Desktop">Desktop (Mac/Win)</option>
</select>
</div>
<div class="form-group">
<label for="loc">Località (es. Roma):</label>
<input type="text" id="loc" placeholder="Cerca città...">
</div>
<button type="submit" class="btn-filtra">🔍 Applica Filtri</button>
<button type="button" class="btn-reset" onclick="resetFilters()">✖ Reset</button>
</form>
<div class="dashboard">
<div class="card">
<h3>Giocatori Trovati</h3>
<p id="totale-text">0</p>
</div>
<div class="card ios">
<h3>Apple iOS</h3>
<p id="ios-text">0</p>
</div>
<div class="card android">
<h3>Google Android</h3>
<p id="android-text">0</p>
</div>
<div class="card">
<h3>Desktop / Altro</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>
</tr>
</thead>
<tbody id="table-body">
<tr><td colspan="5" class="empty">Caricamento dati dal database...</td></tr>
</tbody>
</table>
</div>
</div>
<script>
const PASSWORD_SEGRETA = "!!TetraQ!!"; // La tua password sicura
const UTENTE_SEGRETO = "io"; // Il nome utente richiesto
let allData = [];
// GESTIONE LOGIN
document.getElementById('login-form').addEventListener('submit', function(e) {
e.preventDefault();
const user = document.getElementById('username').value.trim().toLowerCase();
const pwd = document.getElementById('password').value;
if (user === UTENTE_SEGRETO && pwd === PASSWORD_SEGRETA) {
document.getElementById('login-view').style.display = 'none';
document.getElementById('dashboard-view').style.display = 'block';
loadFirebaseData();
} else {
document.getElementById('login-error').innerText = "Credenziali errate! Riprova.";
}
});
function logout() {
document.getElementById('password').value = '';
document.getElementById('login-error').innerText = '';
document.getElementById('dashboard-view').style.display = 'none';
document.getElementById('login-view').style.display = 'flex';
document.getElementById('table-body').innerHTML = '<tr><td colspan="5" class="empty">Caricamento dati dal database...</td></tr>';
allData = [];
}
// CONNESSIONE A FIREBASE E RECUPERO DATI
function loadFirebaseData() {
const db = firebase.firestore();
db.collection('leaderboard').orderBy('lastActive', 'desc').get().then((snapshot) => {
allData = [];
snapshot.forEach(doc => {
let data = doc.data();
if (data.lastActive) {
data.dateObj = data.lastActive.toDate();
data.dateStr = data.dateObj.toISOString().substring(0, 10);
} else {
data.dateStr = "2000-01-01";
}
allData.push(data);
});
applyFilters();
}).catch(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>';
});
}
// GESTIONE FILTRI
document.getElementById('filter-form').addEventListener('submit', function(e) {
e.preventDefault();
applyFilters();
});
function resetFilters() {
document.getElementById('data_da').value = '';
document.getElementById('data_a').value = '';
document.getElementById('os').value = '';
document.getElementById('loc').value = '';
applyFilters();
}
function applyFilters() {
const fDa = document.getElementById('data_da').value;
const fA = document.getElementById('data_a').value;
const fOs = document.getElementById('os').value;
const fLoc = document.getElementById('loc').value.toLowerCase();
let tot = 0, ios = 0, android = 0, desktop = 0;
let html = '';
allData.forEach(row => {
let mostra = true;
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';
// 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
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 (mostra) {
tot++;
let badgeClass = 'badge-desktop';
let platformDisplay = platform;
if (platform === 'iOS') { ios++; badgeClass = 'badge-ios'; }
else if (platform === 'Android') { android++; badgeClass = 'badge-android'; }
else { desktop++; platformDisplay = 'Desktop'; }
html += `
<tr>
<td data-label="Ultimo Accesso">${dateDisplay}</td>
<td data-label="Giocatore">
<strong>${name}</strong> (Liv. ${level})<br>
<small style="color:#7f8c8d;">App v. ${appVersion}</small>
</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>
<td data-label="Dispositivo Hardware" style="font-size: 12px; color: #7f8c8d;">${device}</td>
</tr>
`;
}
});
if (tot === 0) {
html = '<tr><td colspan="5" class="empty">Nessun giocatore corrisponde ai filtri selezionati.</td></tr>';
}
// Aggiorna la vista
document.getElementById('table-body').innerHTML = html;
document.getElementById('totale-text').innerText = tot;
document.getElementById('ios-text').innerText = ios;
document.getElementById('android-text').innerText = android;
document.getElementById('desktop-text').innerText = desktop;
}
</script>
</body>
</html>

View file

@ -1,7 +1,7 @@
name: tetraq name: tetraq
description: A new Flutter project. description: A new Flutter project.
publish_to: 'none' publish_to: 'none'
version: 1.1.4+6 version: 1.1.5+7
environment: environment:
sdk: ^3.10.7 sdk: ^3.10.7