Auto-sync: 20260315_170000
This commit is contained in:
parent
8caecd401e
commit
917a532529
23 changed files with 637 additions and 313 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
|
|
@ -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
BIN
assets/.DS_Store
vendored
Binary file not shown.
BIN
assets/audio/.DS_Store
vendored
BIN
assets/audio/.DS_Store
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
BIN
assets/images/sfondo_temi.jpg
Normal file
BIN
assets/images/sfondo_temi.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB |
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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:";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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: [
|
||||||
|
Container(color: themeType == AppThemeType.doodle ? Colors.white : theme.background),
|
||||||
|
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: const AssetImage('assets/images/sfondo_temi.jpg'),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.6), BlendMode.darken),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
ListView(
|
||||||
|
padding: EdgeInsets.only(top: 120 * vScale, left: 20 * vScale, right: 20 * vScale, bottom: 40 * vScale),
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
children: [
|
children: [
|
||||||
_ThemeCard(
|
_ThemeCard(
|
||||||
title: "Quaderno (Doodle)",
|
title: "Quaderno",
|
||||||
subtitle: "Sfondo a quadretti, tratto a penna",
|
subtitle: "Sfondo a quadretti, tratto a penna",
|
||||||
type: AppThemeType.doodle,
|
type: AppThemeType.doodle,
|
||||||
previewColors: AppColors.doodle,
|
previewColors: AppColors.doodle,
|
||||||
requiredLevel: 1,
|
requiredLevel: 1,
|
||||||
currentLevel: playerLevel,
|
currentLevel: playerLevel,
|
||||||
|
vScale: vScale,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
SizedBox(height: 25 * vScale),
|
||||||
_ThemeCard(
|
|
||||||
title: "Legno & Fiammiferi",
|
|
||||||
subtitle: "Tavolo di legno, linee come fiammiferi",
|
|
||||||
type: AppThemeType.wood,
|
|
||||||
previewColors: AppColors.wood,
|
|
||||||
requiredLevel: 3,
|
|
||||||
currentLevel: playerLevel,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 15),
|
|
||||||
_ThemeCard(
|
_ThemeCard(
|
||||||
title: "Cyberpunk",
|
title: "Cyberpunk",
|
||||||
subtitle: "Nero profondo, luci al neon",
|
subtitle: "Nero profondo, luci al neon",
|
||||||
type: AppThemeType.cyberpunk,
|
type: AppThemeType.cyberpunk,
|
||||||
previewColors: AppColors.cyberpunk,
|
previewColors: AppColors.cyberpunk,
|
||||||
requiredLevel: 7,
|
requiredLevel: 3,
|
||||||
currentLevel: playerLevel,
|
currentLevel: playerLevel,
|
||||||
|
vScale: vScale,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
SizedBox(height: 25 * vScale),
|
||||||
_ThemeCard(
|
_ThemeCard(
|
||||||
title: "8-Bit Arcade",
|
title: "8-Bit Arcade",
|
||||||
subtitle: "Sale giochi, fosfori verdi e pixel",
|
subtitle: "Sale giochi, fosfori verdi e pixel",
|
||||||
type: AppThemeType.arcade,
|
type: AppThemeType.arcade,
|
||||||
previewColors: AppColors.arcade,
|
previewColors: AppColors.arcade,
|
||||||
requiredLevel: 10,
|
requiredLevel: 7,
|
||||||
currentLevel: playerLevel,
|
currentLevel: playerLevel,
|
||||||
|
vScale: vScale,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
SizedBox(height: 25 * vScale),
|
||||||
_ThemeCard(
|
_ThemeCard(
|
||||||
title: "Grimorio",
|
title: "Grimorio",
|
||||||
subtitle: "Incantesimi antichi, rune magiche",
|
subtitle: "Incantesimi antichi, rune magiche",
|
||||||
type: AppThemeType.grimorio,
|
type: AppThemeType.grimorio,
|
||||||
previewColors: AppColors.grimorio,
|
previewColors: AppColors.grimorio,
|
||||||
requiredLevel: 15,
|
requiredLevel: 10,
|
||||||
currentLevel: playerLevel,
|
currentLevel: playerLevel,
|
||||||
|
vScale: vScale,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
SizedBox(height: 25 * vScale),
|
||||||
_ThemeCard(
|
_ThemeCard(
|
||||||
title: "Musica",
|
title: "Musica",
|
||||||
subtitle: "Vinili, cassette e vibrazioni sonore",
|
subtitle: "Vinili, cassette e vibrazioni sonore",
|
||||||
type: AppThemeType.music,
|
type: AppThemeType.music,
|
||||||
previewColors: AppColors.music,
|
previewColors: AppColors.music,
|
||||||
requiredLevel: 20, // Tema Esclusivo di Livello 20!
|
requiredLevel: 15,
|
||||||
currentLevel: playerLevel,
|
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(
|
||||||
|
// --- LA NUOVA CORNICE CHIARA PER IL TESTO ---
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 15 * vScale, vertical: 10 * vScale),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(title, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: previewColors.text)),
|
FittedBox(
|
||||||
Text(subtitle, style: TextStyle(fontSize: 14, color: previewColors.text.withOpacity(0.7))),
|
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)])),
|
),
|
||||||
const SizedBox(width: 10),
|
SizedBox(width: 15 * vScale),
|
||||||
Container(width: 20, height: 20, decoration: BoxDecoration(color: previewColors.playerBlue, shape: BoxShape.circle, boxShadow: [BoxShadow(color: previewColors.playerBlue.withOpacity(0.5), blurRadius: 4)])),
|
Container(
|
||||||
|
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
312
public/report.html
Normal 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>
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue